Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Hoàng Văn Đạt
mypoint_flutter_app
Commits
928c3660
Commit
928c3660
authored
Oct 15, 2025
by
DatHV
Browse files
cập nhật logic, refactor code
parent
6c72edcb
Changes
24
Show whitespace changes
Inline
Side-by-side
lib/screen/voucher/voucher_list/voucher_list_viewmodel.dart
View file @
928c3660
...
@@ -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
;
...
...
lib/services/login_service.dart
0 → 100644
View file @
928c3660
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
);
}
}
lib/services/token_refresh_service.dart
0 → 100644
View file @
928c3660
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
;
}
lib/utils/validation_utils.dart
0 → 100644
View file @
928c3660
/// 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
;
}
}
Prev
1
2
Next
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment