/// Authentication State Provider /// /// Manages authentication state for the Worker application. /// Handles login, logout, and user session management. /// /// Uses Riverpod 3.0 with code generation for type-safe state management. library; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/core/network/dio_client.dart'; import 'package:worker/core/services/frappe_auth_service.dart'; import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart'; import 'package:worker/features/auth/domain/entities/user.dart'; part 'auth_provider.g.dart'; /// Provide FlutterSecureStorage instance @riverpod FlutterSecureStorage secureStorage(Ref ref) { return const FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), ); } /// Provide AuthLocalDataSource instance @riverpod AuthLocalDataSource authLocalDataSource(Ref ref) { final secureStorage = ref.watch(secureStorageProvider); return AuthLocalDataSource(secureStorage); } /// Provide FrappeAuthService instance @riverpod Future frappeAuthService(Ref ref) async { final dio = await ref.watch(dioProvider.future); final secureStorage = ref.watch(secureStorageProvider); return FrappeAuthService(dio, secureStorage); } /// Provide AuthRemoteDataSource instance @riverpod Future authRemoteDataSource(Ref ref) async { final dio = await ref.watch(dioProvider.future); return AuthRemoteDataSource(dio); } /// Authentication state result /// /// Represents the result of authentication operations. /// Contains either the authenticated user or null if logged out. typedef AuthState = AsyncValue; /// Authentication Provider /// /// Main provider for authentication state management. /// Provides login and logout functionality with async state handling. /// /// Usage in widgets: /// ```dart /// final authState = ref.watch(authProvider); /// authState.when( /// data: (user) => user != null ? HomeScreen() : LoginScreen(), /// loading: () => LoadingIndicator(), /// error: (error, stack) => ErrorWidget(error), /// ); /// ``` @riverpod class Auth extends _$Auth { /// Get auth local data source AuthLocalDataSource get _localDataSource => ref.read(authLocalDataSourceProvider); /// Get Frappe auth service Future get _frappeAuthService async => await ref.read(frappeAuthServiceProvider.future); /// Initialize with saved session if available @override Future build() async { // Simple initialization - just check if user is logged in // Do this ONCE on app startup and don't rebuild try { // Check if "Remember Me" was enabled final rememberMe = await _localDataSource.getRememberMe(); if (!rememberMe) { // User didn't check "Remember Me", don't restore session return null; } // Check if we have a stored session final secureStorage = ref.read(secureStorageProvider); final sid = await secureStorage.read(key: 'frappe_sid'); final userId = await secureStorage.read(key: 'frappe_user_id'); final fullName = await secureStorage.read(key: 'frappe_full_name'); if (sid != null && userId != null && userId != ApiConstants.frappePublicUserId) { // User is logged in and wants to be remembered, create User entity final now = DateTime.now(); return User( userId: userId, phoneNumber: userId, fullName: fullName ?? 'User', email: '', role: UserRole.customer, status: UserStatus.active, loyaltyTier: LoyaltyTier.gold, totalPoints: 0, companyInfo: null, cccd: null, attachments: [], address: null, avatarUrl: null, referralCode: null, referredBy: null, erpnextCustomerId: null, createdAt: now.subtract(const Duration(days: 30)), updatedAt: now, lastLoginAt: now, ); } } catch (e) { // Failed to check session, but don't prevent app from starting print('Failed to check saved session: $e'); } return null; } /// Login with phone number /// /// Uses Frappe ERPNext API authentication flow: /// 1. Get current session from storage (should exist from app startup) /// 2. Call login API with phone number /// 3. Get new authenticated session with user credentials /// 4. Update FlutterSecureStorage with new session /// 5. Update Dio interceptors with new session for subsequent API calls /// 6. Save rememberMe preference if enabled /// /// Parameters: /// - [phoneNumber]: User's phone number (Vietnamese format) /// - [password]: User's password (reserved for future use, not sent yet) /// - [rememberMe]: If true, session will be restored on next app launch /// /// Returns: Authenticated User object on success /// /// Throws: Exception on authentication failure Future login({ required String phoneNumber, required String password, bool rememberMe = false, }) async { // Set loading state state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { // Validation if (phoneNumber.isEmpty) { throw Exception('Số điện thoại không được để trống'); } final frappeService = await _frappeAuthService; // Get current session (should exist from app startup) final currentSession = await frappeService.getStoredSession(); if (currentSession == null) { // If no session, get a new one await frappeService.getSession(); final newSession = await frappeService.getStoredSession(); if (newSession == null) { throw Exception('Failed to get session'); } } // Call login API and store session final loginResponse = await frappeService.login(phoneNumber, password: password); // Save rememberMe preference await _localDataSource.saveRememberMe(rememberMe); // Create and return User entity final now = DateTime.now(); return User( userId: phoneNumber, phoneNumber: phoneNumber, fullName: loginResponse.fullName, email: '', role: UserRole.customer, status: UserStatus.active, loyaltyTier: LoyaltyTier.gold, totalPoints: 0, companyInfo: null, cccd: null, attachments: [], address: null, avatarUrl: null, referralCode: null, referredBy: null, erpnextCustomerId: null, createdAt: now.subtract(const Duration(days: 30)), updatedAt: now, lastLoginAt: now, ); }); } /// Logout current user /// /// Clears authentication state and removes saved session. /// Gets a new public session for registration/login. Future logout() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final frappeService = await _frappeAuthService; // Clear saved session await _localDataSource.clearSession(); await frappeService.clearSession(); // Get new public session for registration/login await frappeService.getSession(); // Return null to indicate logged out return null; }); } /// Get current authenticated user /// /// Returns the current user if logged in, null otherwise. User? get currentUser => state.value; /// Check if user is authenticated /// /// Returns true if there is a logged-in user. bool get isAuthenticated => currentUser != null; /// Check if authentication is in progress /// /// Returns true during login/logout operations. bool get isLoading => state.isLoading; /// Get authentication error if any /// /// Returns error message or null if no error. Object? get error => state.error; } /// Convenience provider for checking if user is authenticated /// /// Usage: /// ```dart /// final isLoggedIn = ref.watch(isAuthenticatedProvider); /// if (isLoggedIn) { /// // Show home screen /// } /// ``` @riverpod bool isAuthenticated(Ref ref) { final authState = ref.watch(authProvider); return authState.value != null; } /// Convenience provider for getting current user /// /// Usage: /// ```dart /// final user = ref.watch(currentUserProvider); /// if (user != null) { /// Text('Welcome ${user.fullName}'); /// } /// ``` @riverpod User? currentUser(Ref ref) { final authState = ref.watch(authProvider); return authState.value; } /// Convenience provider for user's loyalty tier /// /// Returns the current user's loyalty tier or null if not logged in. /// /// Usage: /// ```dart /// final tier = ref.watch(userLoyaltyTierProvider); /// if (tier != null) { /// Text('Tier: ${tier.displayName}'); /// } /// ``` @riverpod LoyaltyTier? userLoyaltyTier(Ref ref) { final user = ref.watch(currentUserProvider); return user?.loyaltyTier; } /// Convenience provider for user's total points /// /// Returns the current user's total loyalty points or 0 if not logged in. /// /// Usage: /// ```dart /// final points = ref.watch(userTotalPointsProvider); /// Text('Points: $points'); /// ``` @riverpod int userTotalPoints(Ref ref) { final user = ref.watch(currentUserProvider); return user?.totalPoints ?? 0; } /// Initialize Frappe session /// /// Call this to ensure a Frappe session exists before making API calls. /// This is separate from the Auth provider to avoid disposal issues. /// /// Usage: /// ```dart /// // On login page or before API calls that need session /// await ref.read(initializeFrappeSessionProvider.future); /// ``` @riverpod Future initializeFrappeSession(Ref ref) async { try { final frappeService = await ref.watch(frappeAuthServiceProvider.future); // Check if we already have a session final storedSession = await frappeService.getStoredSession(); if (storedSession == null) { // No session exists, get a public one await frappeService.getSession(); } } catch (e) { // Log error but don't throw - allow app to continue print('Failed to initialize Frappe session: $e'); } }