Files
worker/lib/features/auth/presentation/providers/auth_provider.dart
2025-11-26 17:46:09 +07:00

364 lines
11 KiB
Dart

/// 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:dio/dio.dart';
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/data/models/auth_session_model.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> 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> 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<User?>;
/// 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<FrappeAuthService> get _frappeAuthService async =>
await ref.read(frappeAuthServiceProvider.future);
/// Get auth remote data source
Future<AuthRemoteDataSource> get _remoteDataSource async =>
await ref.read(authRemoteDataSourceProvider.future);
/// Initialize with saved session if available
@override
Future<User?> 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<void> 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;
final remoteDataSource = await _remoteDataSource;
// 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');
}
}
// Get stored session again
final session = await frappeService.getStoredSession();
if (session == null) {
throw Exception('Session not available');
}
// Call login API with current session
final loginResponse = await remoteDataSource.login(
phone: phoneNumber,
csrfToken: session['csrfToken']!,
sid: session['sid']!,
password: password, // Reserved for future use
);
// Update FlutterSecureStorage with new authenticated session
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<void> 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<void> 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');
}
}