Commit 928c3660 authored by DatHV's avatar DatHV
Browse files

cập nhật logic, refactor code

parent 6c72edcb
...@@ -12,7 +12,7 @@ class VoucherListViewModel extends RestfulApiViewModel { ...@@ -12,7 +12,7 @@ class VoucherListViewModel extends RestfulApiViewModel {
final bool isFavorite; final bool isFavorite;
final bool isHotProduct; final bool isHotProduct;
Timer? _debounce; Timer? _debounce;
var products = <ProductModel>[].obs; final RxList<ProductModel> products = <ProductModel>[].obs;
var isLoading = false.obs; var isLoading = false.obs;
var isLoadMore = false.obs; var isLoadMore = false.obs;
int _currentPage = 0; int _currentPage = 0;
......
import 'package:mypoint_flutter_app/base/base_response_model.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/preference/data_preference.dart';
import '../firebase/push_token_service.dart';
import '../model/auth/login_token_response_model.dart';
import '../networking/restful_api_viewmodel.dart';
/// Login result enum để handle các trạng thái khác nhau
enum LoginResult {
success,
invalidCredentials,
deviceUndefined,
requiredChangePass,
invalidAccount,
bioTokenInvalid,
networkError,
unknownError,
}
/// Login response model
class LoginResponse {
final LoginResult result;
final String? message;
final String? errorCode;
LoginResponse({
required this.result,
this.message,
this.errorCode,
});
}
/// Centralized Login Service
class LoginService extends RestfulApiViewModel {
static final LoginService _instance = LoginService._internal();
factory LoginService() => _instance;
LoginService._internal();
/// Main login method với proper error handling
Future<LoginResponse> login(String phone, String password) async {
try {
// Step 1: Authenticate user
final authResponse = await client.login(phone, password);
if (!authResponse.isSuccess || authResponse.data == null) {
return _handleAuthError(authResponse);
}
// Step 2: Save token
await DataPreference.instance.saveLoginToken(authResponse.data!);
// Step 3: Get user profile (critical step)
final profileResult = await _getUserProfileWithRetry();
if (profileResult.result != LoginResult.success) {
// Rollback: Clear token if profile fetch fails
await DataPreference.instance.clearLoginToken();
return LoginResponse(
result: LoginResult.networkError,
message: profileResult.message ?? Constants.commonError,
);
}
// Step 4: Upload FCM token (non-critical, don't fail login if this fails)
await _uploadFCMTokenSafely();
return LoginResponse(result: LoginResult.success);
} catch (e) {
return LoginResponse(
result: LoginResult.networkError,
message: 'Đăng nhập thất bại: ${e.toString()}',
);
}
}
/// Handle authentication errors
LoginResponse _handleAuthError(BaseResponseModel<LoginTokenResponseModel> response) {
final errorCode = response.errorCode;
final errorMsg = response.errorMessage ?? Constants.commonError;
switch (errorCode) {
case ErrorCodes.deviceUndefined:
return LoginResponse(
result: LoginResult.deviceUndefined,
message: errorMsg,
errorCode: errorCode,
);
case ErrorCodes.requiredChangePass:
return LoginResponse(
result: LoginResult.requiredChangePass,
message: errorMsg,
errorCode: errorCode,
);
case ErrorCodes.invalidAccount:
return LoginResponse(
result: LoginResult.invalidAccount,
message: errorMsg,
errorCode: errorCode,
);
case ErrorCodes.bioTokenInvalid:
return LoginResponse(
result: LoginResult.bioTokenInvalid,
message: errorMsg,
errorCode: errorCode,
);
default:
return LoginResponse(
result: LoginResult.invalidCredentials,
message: errorMsg,
errorCode: errorCode,
);
}
}
/// Get user profile with retry mechanism
Future<LoginResponse> _getUserProfileWithRetry({int maxRetries = 3}) async {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
final response = await client.getUserProfile();
if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveUserProfile(response.data!);
return LoginResponse(result: LoginResult.success);
}
// If not last attempt, wait before retry
if (attempt < maxRetries) {
await Future.delayed(Duration(seconds: attempt * 2));
}
} catch (e) {
if (attempt == maxRetries) {
return LoginResponse(
result: LoginResult.networkError,
message: 'Không thể tải thông tin người dùng: ${e.toString()}',
);
}
await Future.delayed(Duration(seconds: attempt * 2));
}
}
return LoginResponse(
result: LoginResult.networkError,
message: 'Không thể tải thông tin người dùng sau $maxRetries lần thử',
);
}
/// Upload FCM token safely (non-blocking)
Future<void> _uploadFCMTokenSafely() async {
try {
await PushTokenService.uploadIfLogged();
} catch (e) {
// Log error but don't fail login
print('Warning: Failed to upload FCM token: $e');
}
}
/// Biometric login
Future<LoginResponse> biometricLogin(String phone) async {
try {
final response = await client.loginWithBiometric(phone);
if (!response.isSuccess || response.data == null) {
return _handleAuthError(response);
}
await DataPreference.instance.saveLoginToken(response.data!);
final profileResult = await _getUserProfileWithRetry();
if (profileResult.result != LoginResult.success) {
await DataPreference.instance.clearLoginToken();
return LoginResponse(
result: LoginResult.networkError,
message: profileResult.message ?? Constants.commonError,
);
}
await _uploadFCMTokenSafely();
return LoginResponse(result: LoginResult.success);
} catch (e) {
return LoginResponse(
result: LoginResult.networkError,
message: 'Đăng nhập sinh trắc thất bại: ${e.toString()}',
);
}
}
/// Clear biometric token for specific phone
void clearBiometricToken(String phone) {
DataPreference.instance.clearBioToken(phone);
}
}
import 'package:flutter/widgets.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../model/auth/login_token_response_model.dart';
import '../networking/restful_api_viewmodel.dart';
import '../preference/data_preference.dart';
import '../networking/app_navigator.dart';
class TokenRefreshService extends RestfulApiViewModel {
static final TokenRefreshService _instance = TokenRefreshService._internal();
factory TokenRefreshService() => _instance;
TokenRefreshService._internal();
bool _isRefreshing = false;
final List<Function(bool)> _callbacks = [];
/// Refresh token với retry mechanism
Future<void> refreshToken(Function(bool) callback) async {
_callbacks.add(callback);
if (_isRefreshing) return;
final token = DataPreference.instance.token;
if (token == null || token.isEmpty) {
_handleRefreshFailure();
return;
}
_isRefreshing = true;
try {
final response = await client.refreshToken().timeout(const Duration(seconds: 10));
if (response.isSuccess && response.data != null) {
final token = LoginTokenResponseModel.fromRefreshToken(response.data!);
await DataPreference.instance.saveLoginToken(token);
await _getUserProfileAfterRefresh();
_handleRefreshSuccess();
} else {
_handleRefreshFailure();
}
} catch (e) {
print('Token refresh error: $e');
_handleRefreshFailure();
} finally {
_isRefreshing = false;
}
}
/// Lấy user profile sau khi refresh token thành công
Future<void> _getUserProfileAfterRefresh() async {
try {
final response = await client.getUserProfile();
if (response.isSuccess && response.data != null) {
await DataPreference.instance.saveUserProfile(response.data!);
}
} catch (e) {
print('Get user profile after refresh error: $e');
}
}
/// Xử lý refresh thành công
void _handleRefreshSuccess() {
for (final callback in _callbacks) {
callback(true);
}
_callbacks.clear();
}
/// Xử lý refresh thất bại - redirect về login
void _handleRefreshFailure() {
DataPreference.instance.clearData();
for (final callback in _callbacks) {
callback(false);
}
_callbacks.clear();
// Redirect về login screen (ensure on UI thread)
WidgetsBinding.instance.addPostFrameCallback((_) {
AppNavigator.showAuthAlertAndGoLogin('Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.');
});
}
/// Kiểm tra xem có đang refresh không
bool get isRefreshing => _isRefreshing;
}
/// Centralized validation utilities
class ValidationUtils {
/// Validate phone number with Vietnamese format
static bool isValidPhoneNumber(String phone) {
final cleanPhone = phone.replaceAll(RegExp(r'\s+'), '');
final regex = RegExp(r'^(0|\+84)(3[2-9]|5[6|8|9]|7[0|6-9]|8[1-5]|9[0-4|6-9])[0-9]{7}$');
return regex.hasMatch(cleanPhone);
}
/// Validate email format
static bool isValidEmail(String email) {
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
return emailRegex.hasMatch(email);
}
/// Validate password strength
static bool isValidPassword(String password) {
// At least 6 characters
if (password.length < 6) return false;
// Contains at least one letter and one number
final hasLetter = RegExp(r'[a-zA-Z]').hasMatch(password);
final hasNumber = RegExp(r'[0-9]').hasMatch(password);
return hasLetter && hasNumber;
}
/// Get phone number error message
static String? getPhoneNumberError(String phone) {
if (phone.isEmpty) return 'Vui lòng nhập số điện thoại';
if (!isValidPhoneNumber(phone)) return 'Số điện thoại không hợp lệ';
return null;
}
/// Get email error message
static String? getEmailError(String email) {
if (email.isEmpty) return 'Vui lòng nhập email';
if (!isValidEmail(email)) return 'Email không hợp lệ';
return null;
}
/// Get password error message
static String? getPasswordError(String password) {
if (password.isEmpty) return 'Vui lòng nhập mật khẩu';
if (!isValidPassword(password)) return 'Mật khẩu phải có ít nhất 6 ký tự và bao gồm cả chữ và số';
return null;
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment