add auth, format
This commit is contained in:
279
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
279
lib/features/auth/presentation/providers/auth_provider.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
/// 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/features/auth/data/datasources/auth_local_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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Initialize with saved session if available
|
||||
@override
|
||||
Future<User?> build() async {
|
||||
// Check for saved session in secure storage
|
||||
final session = await _localDataSource.getSession();
|
||||
if (session != null) {
|
||||
// User has saved session, create User entity
|
||||
final now = DateTime.now();
|
||||
return User(
|
||||
userId: 'user_saved', // TODO: Get from API
|
||||
phoneNumber: '', // TODO: Get from saved user data
|
||||
fullName: session.fullName,
|
||||
email: '', // TODO: Get from saved user data
|
||||
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: session.createdAt,
|
||||
updatedAt: now,
|
||||
lastLoginAt: now,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Login with phone number and password
|
||||
///
|
||||
/// Simulates ERPNext API authentication with mock response.
|
||||
/// Stores session data (SID, CSRF token) in Hive.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - [phoneNumber]: User's phone number (Vietnamese format)
|
||||
/// - [password]: User's password
|
||||
///
|
||||
/// Returns: Authenticated User object on success
|
||||
///
|
||||
/// Throws: Exception on authentication failure
|
||||
Future<void> login({
|
||||
required String phoneNumber,
|
||||
required String password,
|
||||
}) async {
|
||||
// Set loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// Simulate API call delay
|
||||
state = await AsyncValue.guard(() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
|
||||
// Mock validation
|
||||
if (phoneNumber.isEmpty || password.isEmpty) {
|
||||
throw Exception('Số điện thoại và mật khẩu không được để trống');
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw Exception('Mật khẩu phải có ít nhất 6 ký tự');
|
||||
}
|
||||
|
||||
// Simulate API response matching ERPNext format
|
||||
final mockApiResponse = AuthSessionResponse(
|
||||
sessionExpired: 1,
|
||||
message: const LoginMessage(
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
sid: 'df7fd4e7ef1041aa3422b0ee861315ba8c28d4fe008a7d7e0e7e0e01',
|
||||
csrfToken: '6b6e37563854e951c36a7af4177956bb15ca469ca4f498b742648d70',
|
||||
apps: [
|
||||
AppInfo(
|
||||
appTitle: 'App nhân viên kinh doanh',
|
||||
appEndpoint: '/ecommerce/app-sales',
|
||||
appLogo:
|
||||
'https://assets.digitalbiz.com.vn/DBIZ_Internal/Logo/logo_app_sales.png',
|
||||
),
|
||||
],
|
||||
),
|
||||
homePage: '/apps',
|
||||
fullName: 'Tân Duy Nguyễn',
|
||||
);
|
||||
|
||||
// Save session data to Hive
|
||||
final sessionData = SessionData.fromAuthResponse(mockApiResponse);
|
||||
await _localDataSource.saveSession(sessionData);
|
||||
|
||||
// Create and return User entity
|
||||
final now = DateTime.now();
|
||||
return User(
|
||||
userId: 'user_${phoneNumber.replaceAll('+84', '')}',
|
||||
phoneNumber: phoneNumber,
|
||||
fullName: mockApiResponse.fullName,
|
||||
email: 'user@eurotile.vn',
|
||||
role: UserRole.customer,
|
||||
status: UserStatus.active,
|
||||
loyaltyTier: LoyaltyTier.gold,
|
||||
totalPoints: 1500,
|
||||
companyInfo: const CompanyInfo(
|
||||
name: 'Công ty TNHH XYZ',
|
||||
taxId: '0123456789',
|
||||
businessType: 'Xây dựng',
|
||||
),
|
||||
cccd: '001234567890',
|
||||
attachments: [],
|
||||
address: '123 Đường ABC, Quận 1, TP.HCM',
|
||||
avatarUrl: null,
|
||||
referralCode: 'REF${phoneNumber.replaceAll('+84', '').substring(0, 6)}',
|
||||
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 from Hive.
|
||||
Future<void> logout() async {
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
// Clear saved session from Hive
|
||||
await _localDataSource.clearSession();
|
||||
|
||||
// TODO: Call logout API to invalidate token on server
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user