This commit is contained in:
Phuoc Nguyen
2025-11-10 14:21:27 +07:00
parent 2a71c65577
commit 36bdf6613b
33 changed files with 2206 additions and 252 deletions

View File

@@ -6,9 +6,14 @@
/// 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';
@@ -30,6 +35,21 @@ AuthLocalDataSource authLocalDataSource(Ref ref) {
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.
@@ -56,19 +76,141 @@ class Auth extends _$Auth {
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 {
// Check for saved session in secure storage
final session = await _localDataSource.getSession();
if (session != null) {
// User has saved session, create User entity
// Simple initialization - just check if user is logged in
// Don't call getSession() here to avoid ref disposal issues
try {
final secureStorage = ref.read(secureStorageProvider);
// 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 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: 'user_saved', // TODO: Get from API
phoneNumber: '', // TODO: Get from saved user data
fullName: session.fullName,
email: '', // TODO: Get from saved user data
userId: phoneNumber,
phoneNumber: phoneNumber,
fullName: loginResponse.fullName,
email: '',
role: UserRole.customer,
status: UserStatus.active,
loyaltyTier: LoyaltyTier.gold,
@@ -81,94 +223,6 @@ class Auth extends _$Auth {
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,
@@ -178,17 +232,20 @@ class Auth extends _$Auth {
/// Logout current user
///
/// Clears authentication state and removes saved session from Hive.
/// 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 {
// Clear saved session from Hive
final frappeService = await _frappeAuthService;
// Clear saved session
await _localDataSource.clearSession();
await frappeService.clearSession();
// TODO: Call logout API to invalidate token on server
await Future<void>.delayed(const Duration(milliseconds: 500));
// Get new public session for registration/login
await frappeService.getSession();
// Return null to indicate logged out
return null;
@@ -277,3 +334,31 @@ 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');
}
}

View File

@@ -113,6 +113,99 @@ final class AuthLocalDataSourceProvider
String _$authLocalDataSourceHash() =>
r'f104de00a8ab431f6736387fb499c2b6e0ab4924';
/// Provide FrappeAuthService instance
@ProviderFor(frappeAuthService)
const frappeAuthServiceProvider = FrappeAuthServiceProvider._();
/// Provide FrappeAuthService instance
final class FrappeAuthServiceProvider
extends
$FunctionalProvider<
AsyncValue<FrappeAuthService>,
FrappeAuthService,
FutureOr<FrappeAuthService>
>
with
$FutureModifier<FrappeAuthService>,
$FutureProvider<FrappeAuthService> {
/// Provide FrappeAuthService instance
const FrappeAuthServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'frappeAuthServiceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$frappeAuthServiceHash();
@$internal
@override
$FutureProviderElement<FrappeAuthService> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<FrappeAuthService> create(Ref ref) {
return frappeAuthService(ref);
}
}
String _$frappeAuthServiceHash() => r'db239119c9a8510d3439a2d05a7fae1743be11c5';
/// Provide AuthRemoteDataSource instance
@ProviderFor(authRemoteDataSource)
const authRemoteDataSourceProvider = AuthRemoteDataSourceProvider._();
/// Provide AuthRemoteDataSource instance
final class AuthRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<AuthRemoteDataSource>,
AuthRemoteDataSource,
FutureOr<AuthRemoteDataSource>
>
with
$FutureModifier<AuthRemoteDataSource>,
$FutureProvider<AuthRemoteDataSource> {
/// Provide AuthRemoteDataSource instance
const AuthRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authRemoteDataSourceHash();
@$internal
@override
$FutureProviderElement<AuthRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<AuthRemoteDataSource> create(Ref ref) {
return authRemoteDataSource(ref);
}
}
String _$authRemoteDataSourceHash() =>
r'3c05cf67fe479a973fc4ce2db68a0abde37974a5';
/// Authentication Provider
///
/// Main provider for authentication state management.
@@ -179,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth();
}
String _$authHash() => r'6f410d1abe6c53a6cbfa52fde7ea7a2d22a7f78d';
String _$authHash() => r'3f0562ffb573be47d8aae8beebccb1946240cbb6';
/// Authentication Provider
///