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
Hide whitespace changes
Inline
Side-by-side
lib/base/base_screen.dart
View file @
928c3660
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/app_loading.dart'
;
import
'package:mypoint_flutter_app/networking/app_navigator.dart'
;
import
'package:mypoint_flutter_app/networking/app_navigator.dart'
;
import
'package:mypoint_flutter_app/main.dart'
show
routeObserver
;
import
'package:mypoint_flutter_app/main.dart'
show
routeObserver
;
import
'../networking/dio_http_service.dart'
;
import
'../resources/base_color.dart'
;
import
'../resources/base_color.dart'
;
import
'../widgets/alert/custom_alert_dialog.dart'
;
import
'../widgets/alert/custom_alert_dialog.dart'
;
import
'../widgets/alert/data_alert_model.dart'
;
import
'../widgets/alert/data_alert_model.dart'
;
...
@@ -17,20 +15,13 @@ abstract class BaseScreen extends StatefulWidget {
...
@@ -17,20 +15,13 @@ abstract class BaseScreen extends StatefulWidget {
abstract
class
BaseState
<
Screen
extends
BaseScreen
>
extends
State
<
Screen
>
abstract
class
BaseState
<
Screen
extends
BaseScreen
>
extends
State
<
Screen
>
with
WidgetsBindingObserver
,
RouteAware
{
with
WidgetsBindingObserver
,
RouteAware
{
bool
_isVisible
=
false
;
bool
_isVisible
=
false
;
bool
_isPaused
=
false
;
ModalRoute
<
dynamic
>?
_route
;
ModalRoute
<
dynamic
>?
_route
;
@override
@override
void
initState
()
{
void
initState
()
{
super
.
initState
();
super
.
initState
();
WidgetsBinding
.
instance
.
addObserver
(
this
);
WidgetsBinding
.
instance
.
addObserver
(
this
);
if
(
kDebugMode
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
=>
onInit
());
print
(
"_show:
$runtimeType
"
);
}
// Gọi onInit sau khi initState
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
onInit
();
});
}
}
@override
@override
...
@@ -38,9 +29,8 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -38,9 +29,8 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
WidgetsBinding
.
instance
.
removeObserver
(
this
);
WidgetsBinding
.
instance
.
removeObserver
(
this
);
if
(
_route
!=
null
)
{
if
(
_route
!=
null
)
{
routeObserver
.
unsubscribe
(
this
);
routeObserver
.
unsubscribe
(
this
);
_route
=
null
;
}
}
onD
estroy
();
onD
ispose
();
super
.
dispose
();
super
.
dispose
();
}
}
...
@@ -49,39 +39,12 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -49,39 +39,12 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
super
.
didChangeAppLifecycleState
(
state
);
super
.
didChangeAppLifecycleState
(
state
);
switch
(
state
)
{
switch
(
state
)
{
case
AppLifecycleState
.
resumed
:
case
AppLifecycleState
.
resumed
:
if
(
_isPaused
)
{
onAppResumed
();
_isPaused
=
false
;
onAppResumed
();
if
(
_isVisible
)
{
// App back to foreground while this route is visible → appear again
onWillAppear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
onDidAppear
();
});
}
}
break
;
break
;
case
AppLifecycleState
.
paused
:
case
AppLifecycleState
.
paused
:
if
(!
_isPaused
)
{
onAppPaused
();
_isPaused
=
true
;
onAppPaused
();
if
(
_isVisible
)
{
// App goes to background while this route is visible → disappear
onWillDisappear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
onDidDisappear
();
});
}
}
break
;
case
AppLifecycleState
.
inactive
:
onAppInactive
();
break
;
case
AppLifecycleState
.
detached
:
onAppDetached
();
break
;
break
;
case
AppLifecycleState
.
hidden
:
default
:
onAppHidden
();
break
;
break
;
}
}
}
}
...
@@ -89,131 +52,107 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -89,131 +52,107 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
@override
@override
void
didChangeDependencies
()
{
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
super
.
didChangeDependencies
();
// Subscribe to RouteObserver when route is available
_setupRouteObserver
();
}
void
_setupRouteObserver
()
{
final
modalRoute
=
ModalRoute
.
of
(
context
);
final
modalRoute
=
ModalRoute
.
of
(
context
);
if
(
modalRoute
!=
null
&&
modalRoute
is
PageRoute
&&
modalRoute
!=
_route
)
{
if
(
modalRoute
!=
null
&&
modalRoute
is
PageRoute
&&
modalRoute
!=
_route
)
{
_route
=
modalRoute
;
_route
=
modalRoute
;
routeObserver
.
subscribe
(
this
,
modalRoute
);
routeObserver
.
subscribe
(
this
,
modalRoute
);
}
}
if
(!
_isVisible
)
{
_isVisible
=
true
;
// First time becoming visible in the tree
onWillAppear
();
// Call did-appear after the frame
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
if
(
_isVisible
&&
!
_isPaused
)
{
onDidAppear
();
}
});
}
}
}
@override
// MARK: - Core Lifecycle Methods
void
didUpdateWidget
(
Screen
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
// Gọi khi widget được update (có thể do navigation, state changes)
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
if
(
_isVisible
&&
!
_isPaused
)
{
onStart
();
}
});
}
// MARK: - Flutter Lifecycle Hooks (Override these in your screens)
/// Called when the widget is first inserted into the tree (similar to viewDidLoad in iOS)
/// Called when the widget is first inserted into the tree.
/// Use this to initialize data, setup listeners, etc.
void
onInit
()
{
void
onInit
()
{
// Override in subclasses
if
(
kDebugMode
)
print
(
"onInit:
$runtimeType
"
);
}
}
/// Called when the widget is about to become visible (similar to viewWillAppear in iOS)
/// Called when the widget is removed from the tree.
void
onResume
()
{
/// Use this to cleanup resources, cancel timers, etc.
// Override in subclasses
void
onDispose
()
{
if
(
kDebugMode
)
print
(
"onDispose:
$runtimeType
"
);
}
}
/// Called when the widget has become visible (similar to viewDidAppear in iOS)
// MARK: - Route Visibility Methods
void
onStart
()
{
// Override in subclasses
/// Called when the route is about to become visible (push or uncovered).
/// Use this to prepare data, start animations, etc.
void
onRouteWillAppear
()
{
if
(
kDebugMode
)
print
(
"onRouteWillAppear:
$runtimeType
"
);
}
}
/// Called when the widget is about to become invisible (similar to viewWillDisappear in iOS)
/// Called when the route has become visible.
void
onPause
()
{
/// Use this to start timers, refresh data, etc.
// Override in subclasses
void
onRouteDidAppear
()
{
if
(
kDebugMode
)
print
(
"onRouteDidAppear:
$runtimeType
"
);
}
}
/// Called when the widget has become invisible (similar to viewDidDisappear in iOS)
/// Called when the route is about to be covered or popped.
void
onStop
()
{
/// Use this to pause operations, save state, etc.
// Override in subclasses
void
onRouteWillDisappear
()
{
if
(
kDebugMode
)
print
(
"onRouteWillDisappear:
$runtimeType
"
);
}
}
/// Called when the widget is removed from the tree (similar to viewDidUnload in iOS)
/// Called when the route has been covered or popped.
void
onDestroy
()
{
/// Use this to stop timers, cleanup temporary resources, etc.
// Override in subclasses
void
onRouteDidDisappear
()
{
if
(
kDebugMode
)
print
(
"onRouteDidDisappear:
$runtimeType
"
);
}
}
// MARK: - Route visibility hooks (Navigator push/pop)
// MARK: - App Lifecycle Methods
/// Called right before the route appears (push or uncovered)
/// Called when the app becomes active (foreground).
void
onWillAppear
()
{}
/// Use this to resume operations, refresh data, etc.
/// Called right after the route appeared
void
onDidAppear
()
{}
/// Called right before another route covers this one
void
onWillDisappear
()
{}
/// Called right after this route is covered or popped
void
onDidDisappear
()
{}
/// Called when app becomes active (similar to applicationDidBecomeActive in iOS)
void
onAppResumed
()
{
void
onAppResumed
()
{
// Override in subclasses
if
(
kDebugMode
)
print
(
"onAppResumed:
$runtimeType
"
);
}
}
/// Called when app becomes inactive (similar to applicationWillResignActive in iOS)
/// Called when the app becomes inactive (background).
/// Use this to pause operations, save state, etc.
void
onAppPaused
()
{
void
onAppPaused
()
{
// Override in subclasses
if
(
kDebugMode
)
print
(
"onAppPaused:
$runtimeType
"
);
}
/// Called when app becomes inactive (similar to applicationWillResignActive in iOS)
void
onAppInactive
()
{
// Override in subclasses
}
/// Called when app is detached (similar to applicationWillTerminate in iOS)
void
onAppDetached
()
{
// Override in subclasses
}
/// Called when app is hidden (similar to applicationDidEnterBackground in iOS)
void
onAppHidden
()
{
// Override in subclasses
}
}
// MARK: - UI Helper Methods
/// Shows a popup dialog with custom data
void
showPopup
({
void
showPopup
({
required
PopupDataModel
data
,
required
PopupDataModel
data
,
bool
?
barrierDismissibl
,
bool
?
barrierDismissibl
e
,
bool
showCloseButton
=
false
,
bool
showCloseButton
=
false
,
ButtonsDirection
direction
=
ButtonsDirection
.
column
,
ButtonsDirection
direction
=
ButtonsDirection
.
column
,
})
{
})
{
Get
.
dialog
(
Get
.
dialog
(
CustomAlertDialog
(
alertData:
data
.
dataAlertModel
,
showCloseButton:
showCloseButton
,
direction:
direction
),
CustomAlertDialog
(
barrierDismissible:
barrierDismissibl
??
true
,
alertData:
data
.
dataAlertModel
,
showCloseButton:
showCloseButton
,
direction:
direction
,
),
barrierDismissible:
barrierDismissible
??
true
,
);
);
}
}
/// Shows an alert dialog with custom data
void
showAlert
({
void
showAlert
({
required
DataAlertModel
data
,
required
DataAlertModel
data
,
bool
?
barrierDismissibl
,
bool
?
barrierDismissibl
e
,
bool
showCloseButton
=
true
,
bool
showCloseButton
=
true
,
ButtonsDirection
direction
=
ButtonsDirection
.
column
,
ButtonsDirection
direction
=
ButtonsDirection
.
column
,
})
{
})
{
Get
.
dialog
(
Get
.
dialog
(
CustomAlertDialog
(
alertData:
data
,
showCloseButton:
showCloseButton
,
direction:
direction
),
CustomAlertDialog
(
barrierDismissible:
barrierDismissibl
??
false
,
alertData:
data
,
showCloseButton:
showCloseButton
,
direction:
direction
,
),
barrierDismissible:
barrierDismissible
??
false
,
);
);
}
}
/// Shows an error alert with default styling
void
showAlertError
({
void
showAlertError
({
required
String
content
,
required
String
content
,
bool
?
barrierDismissible
,
bool
?
barrierDismissible
,
...
@@ -234,9 +173,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -234,9 +173,7 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
text:
"Đã Hiểu"
,
text:
"Đã Hiểu"
,
onPressed:
()
{
onPressed:
()
{
Get
.
back
();
Get
.
back
();
if
(
onConfirmed
!=
null
)
{
onConfirmed
?.
call
();
onConfirmed
();
}
},
},
bgColor:
BaseColor
.
primary500
,
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
textColor:
Colors
.
white
,
...
@@ -248,51 +185,34 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
...
@@ -248,51 +185,34 @@ abstract class BaseState<Screen extends BaseScreen> extends State<Screen>
);
);
}
}
/// Hides the keyboard
void
hideKeyboard
()
{
void
hideKeyboard
()
{
FocusScope
.
of
(
context
).
unfocus
();
FocusScope
.
of
(
context
).
unfocus
();
}
}
void
printDebug
(
dynamic
data
)
{
// MARK: - RouteAware Implementation
if
(
kDebugMode
)
{
print
(
data
);
}
}
Widget
?
createBottomBar
()
{
return
null
;
}
// MARK: - RouteAware overrides mapping to hooks
@override
@override
void
didPush
()
{
void
didPush
()
{
onWillAppear
();
_isVisible
=
true
;
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
_handleRouteAppear
();
onDidAppear
();
});
}
}
@override
@override
void
didPopNext
()
{
void
didPopNext
()
=>
_handleRouteAppear
();
onWillAppear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
onDidAppear
();
});
}
@override
@override
void
didPushNext
()
{
void
didPushNext
()
=>
_handleRouteDisappear
();
onWillDisappear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
onDidDisappear
();
});
}
@override
@override
void
didPop
()
{
void
didPop
()
=>
_handleRouteDisappear
();
onWillDisappear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
void
_handleRouteAppear
()
{
onDidDisappear
();
onRouteWillAppear
();
});
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
=>
onRouteDidAppear
());
}
void
_handleRouteDisappear
()
{
onRouteWillDisappear
();
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
=>
onRouteDidDisappear
());
}
}
}
}
lib/base/basic_state.dart
View file @
928c3660
...
@@ -16,7 +16,7 @@ mixin BasicState<Screen extends BaseScreen> on BaseState<Screen> {
...
@@ -16,7 +16,7 @@ mixin BasicState<Screen extends BaseScreen> on BaseState<Screen> {
backgroundColor:
colorView
,
backgroundColor:
colorView
,
key:
_scaffoldStateKey
,
key:
_scaffoldStateKey
,
appBar:
appBar
,
appBar:
appBar
,
bottomNavigationBar:
createBottomBar
(),
//
bottomNavigationBar: createBottomBar(),
body:
isSafeArea
==
true
body:
isSafeArea
==
true
?
Container
(
?
Container
(
color:
colorSafeArea
,
color:
colorSafeArea
,
...
@@ -39,7 +39,7 @@ mixin BasicState<Screen extends BaseScreen> on BaseState<Screen> {
...
@@ -39,7 +39,7 @@ mixin BasicState<Screen extends BaseScreen> on BaseState<Screen> {
child:
Scaffold
(
child:
Scaffold
(
backgroundColor:
colorView
,
backgroundColor:
colorView
,
appBar:
appBar
,
appBar:
appBar
,
bottomNavigationBar:
createBottomBar
(),
//
bottomNavigationBar: createBottomBar(),
body:
isSafeArea
==
true
body:
isSafeArea
==
true
?
GestureDetector
(
?
GestureDetector
(
onPanUpdate:
(
details
)
{
onPanUpdate:
(
details
)
{
...
...
lib/configs/api_paths.dart
View file @
928c3660
...
@@ -16,6 +16,7 @@ class APIPaths {//sandbox
...
@@ -16,6 +16,7 @@ class APIPaths {//sandbox
static
const
String
login
=
"/iam/v1/authentication/account-login"
;
static
const
String
login
=
"/iam/v1/authentication/account-login"
;
static
const
String
loginWithBiometric
=
"/iam/v1/authentication/bio-login"
;
static
const
String
loginWithBiometric
=
"/iam/v1/authentication/bio-login"
;
static
const
String
getUserInfo
=
"/user/api/v2.0/mypoint/me"
;
static
const
String
getUserInfo
=
"/user/api/v2.0/mypoint/me"
;
static
const
String
refreshToken
=
"/accountAccessTokenRefresh/3.0.0"
;
static
const
String
bioCredential
=
"/iam/v1/account/me/bio-credential"
;
static
const
String
bioCredential
=
"/iam/v1/account/me/bio-credential"
;
static
const
String
accountLoginForPasswordChange
=
"/accountLoginForPasswordChange/1.0.0"
;
static
const
String
accountLoginForPasswordChange
=
"/accountLoginForPasswordChange/1.0.0"
;
static
const
String
accountPasswordChange
=
"/accountPasswordChange/1.0.0"
;
static
const
String
accountPasswordChange
=
"/accountPasswordChange/1.0.0"
;
...
...
lib/directional/directional_screen.dart
View file @
928c3660
...
@@ -91,7 +91,7 @@ class DirectionalScreen {
...
@@ -91,7 +91,7 @@ class DirectionalScreen {
return
true
;
return
true
;
case
DirectionalScreenName
.
viewVoucherWithCountTime
:
case
DirectionalScreenName
.
viewVoucherWithCountTime
:
final
countDownSecond
=
int
.
tryParse
(
clickActionParam
??
''
)
??
0
;
final
countDownSecond
=
int
.
tryParse
(
clickActionParam
??
''
)
??
0
;
Get
.
toNamed
(
voucher
Detail
Screen
,
arguments:
{
"countDownSecond"
:
countDownSecond
});
Get
.
toNamed
(
voucher
s
Screen
,
arguments:
{
"countDownSecond"
:
countDownSecond
});
return
true
;
return
true
;
case
DirectionalScreenName
.
popViewController
:
case
DirectionalScreenName
.
popViewController
:
if
(
Get
.
isOverlaysOpen
)
{
if
(
Get
.
isOverlaysOpen
)
{
...
...
lib/extensions/string_extension.dart
View file @
928c3660
...
@@ -5,7 +5,9 @@ import 'package:intl/intl.dart' as intl;
...
@@ -5,7 +5,9 @@ import 'package:intl/intl.dart' as intl;
extension
PhoneValidator
on
String
{
extension
PhoneValidator
on
String
{
bool
isPhoneValid
()
{
bool
isPhoneValid
()
{
return
RegExp
(
r'^0\d{9}$'
).
hasMatch
(
this
);
final
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
(
phone
);
}
}
}
}
...
...
lib/model/auth/login_token_response_model.dart
View file @
928c3660
...
@@ -8,6 +8,28 @@ class LoginTokenResponseModel {
...
@@ -8,6 +8,28 @@ class LoginTokenResponseModel {
bool
?
forceResetPassword
;
bool
?
forceResetPassword
;
LoginTokenResponseModel
({
this
.
accessToken
,
this
.
refreshToken
,
this
.
forceResetPassword
});
LoginTokenResponseModel
({
this
.
accessToken
,
this
.
refreshToken
,
this
.
forceResetPassword
});
LoginTokenResponseModel
.
fromRefreshToken
(
TokenRefreshResponseModel
refresh
)
:
accessToken
=
refresh
.
accessToken
,
refreshToken
=
refresh
.
refreshToken
,
forceResetPassword
=
false
;
factory
LoginTokenResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$LoginTokenResponseModelFromJson
(
json
);
factory
LoginTokenResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
_$LoginTokenResponseModelFromJson
(
json
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$LoginTokenResponseModelToJson
(
this
);
Map
<
String
,
dynamic
>
toJson
()
=>
_$LoginTokenResponseModelToJson
(
this
);
}
class
TokenRefreshResponseModel
{
String
?
accessToken
;
String
?
refreshToken
;
TokenRefreshResponseModel
({
this
.
accessToken
,
this
.
refreshToken
});
factory
TokenRefreshResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
=>
TokenRefreshResponseModel
(
accessToken:
json
[
'access_token'
]
as
String
?,
refreshToken:
json
[
'refresh_token'
]
as
String
?,
);
Map
<
String
,
dynamic
>
toJson
()
=>
{
'access_token'
:
accessToken
,
'refresh_token'
:
refreshToken
,
};
}
}
\ No newline at end of file
lib/networking/interceptor/auth_interceptor.dart
View file @
928c3660
import
'package:dio/dio.dart'
;
import
'package:dio/dio.dart'
;
import
'../../configs/constants.dart'
;
import
'../../configs/constants.dart'
;
import
'../app_navigator.dart'
;
import
'../app_navigator.dart'
;
import
'../dio_http_service.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../../services/token_refresh_service.dart'
;
class
AuthInterceptor
extends
Interceptor
{
class
AuthInterceptor
extends
Interceptor
{
bool
_isHandlingAuth
=
false
;
bool
_isHandlingAuth
=
false
;
...
@@ -35,8 +37,8 @@ class AuthInterceptor extends Interceptor {
...
@@ -35,8 +37,8 @@ class AuthInterceptor extends Interceptor {
final
statusCode
=
err
.
response
?.
statusCode
;
final
statusCode
=
err
.
response
?.
statusCode
;
if
(
alreadyHandled
)
return
;
if
(
alreadyHandled
)
return
;
if
(
statusCode
==
401
||
_isTokenInvalid
(
data
))
{
if
(
statusCode
==
401
||
_isTokenInvalid
(
data
))
{
await
_handleAuthError
(
data
);
await
_handleAuthError
(
data
,
originalRequest:
err
.
requestOptions
,
handler:
handler
,
originalError:
err
);
return
handler
.
reject
(
err
)
;
return
;
}
}
handler
.
next
(
err
);
handler
.
next
(
err
);
}
}
...
@@ -49,20 +51,52 @@ class AuthInterceptor extends Interceptor {
...
@@ -49,20 +51,52 @@ class AuthInterceptor extends Interceptor {
return
false
;
return
false
;
}
}
Future
<
void
>
_handleAuthError
(
dynamic
data
)
async
{
Future
<
void
>
_handleAuthError
(
dynamic
data
,
{
RequestOptions
?
originalRequest
,
ErrorInterceptorHandler
?
handler
,
DioException
?
originalError
}
)
async
{
if
(
_isHandlingAuth
)
return
;
if
(
_isHandlingAuth
)
return
;
_isHandlingAuth
=
true
;
_isHandlingAuth
=
true
;
try
{
try
{
await
DataPreference
.
instance
.
clearData
();
// Thử refresh token trước khi logout
String
?
message
;
final
refreshService
=
TokenRefreshService
();
if
(
data
is
Map
<
String
,
dynamic
>)
{
if
(!
refreshService
.
isRefreshing
)
{
message
=
data
[
'error_message'
]?.
toString
()
??
await
refreshService
.
refreshToken
((
success
)
async
{
data
[
'errorMessage'
]?.
toString
()
??
if
(
success
&&
originalRequest
!=
null
&&
handler
!=
null
)
{
data
[
'message'
]?.
toString
();
try
{
final
RequestOptions
retryOptions
=
originalRequest
.
copyWith
();
retryOptions
.
extra
.
remove
(
_kAuthHandledKey
);
final
dio
=
DioHttpService
().
dio
;
final
Response
retried
=
await
dio
.
fetch
(
retryOptions
);
handler
.
resolve
(
retried
);
return
;
}
catch
(
e
)
{
handler
.
reject
(
errFrom
(
e
,
originalRequest
));
}
}
else
if
(!
success
)
{
_performLogout
(
data
);
if
(
handler
!=
null
&&
originalError
!=
null
)
handler
.
reject
(
originalError
);
}
});
}
else
{
_performLogout
(
data
);
if
(
handler
!=
null
&&
originalError
!=
null
)
handler
.
reject
(
originalError
);
}
}
await
AppNavigator
.
showAuthAlertAndGoLogin
(
message
??
ErrorCodes
.
tokenInvalidMessage
);
}
finally
{
}
finally
{
_isHandlingAuth
=
false
;
_isHandlingAuth
=
false
;
}
}
}
}
DioException
errFrom
(
Object
e
,
RequestOptions
req
)
{
if
(
e
is
DioException
)
return
e
;
return
DioException
(
requestOptions:
req
,
error:
e
);
}
Future
<
void
>
_performLogout
(
dynamic
data
)
async
{
await
DataPreference
.
instance
.
clearData
();
String
?
message
;
if
(
data
is
Map
<
String
,
dynamic
>)
{
message
=
data
[
'error_message'
]?.
toString
()
??
data
[
'errorMessage'
]?.
toString
()
??
data
[
'message'
]?.
toString
();
}
await
AppNavigator
.
showAuthAlertAndGoLogin
(
message
??
ErrorCodes
.
tokenInvalidMessage
);
}
}
}
lib/networking/restful_api_client_all_request.dart
View file @
928c3660
...
@@ -179,9 +179,19 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
...
@@ -179,9 +179,19 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
return
requestNormal
(
APIPaths
.
getUserInfo
,
Method
.
GET
,
{},
(
data
)
=>
ProfileResponseModel
.
fromJson
(
data
as
Json
));
return
requestNormal
(
APIPaths
.
getUserInfo
,
Method
.
GET
,
{},
(
data
)
=>
ProfileResponseModel
.
fromJson
(
data
as
Json
));
}
}
Future
<
BaseResponseModel
<
TokenRefreshResponseModel
>>
refreshToken
()
async
{
String
?
token
=
DataPreference
.
instance
.
token
??
""
;
String
?
refreshToken
=
DataPreference
.
instance
.
refreshToken
??
""
;
final
body
=
{
"access_token_old"
:
token
,
"refresh_token"
:
refreshToken
,
'lang'
:
'vi'
,
};
return
requestNormal
(
APIPaths
.
refreshToken
,
Method
.
POST
,
body
,
(
data
)
=>
TokenRefreshResponseModel
.
fromJson
(
data
as
Json
));
}
Future
<
BaseResponseModel
<
CreateOTPResponseModel
>>
otpCreateNew
(
String
ownerId
)
async
{
Future
<
BaseResponseModel
<
CreateOTPResponseModel
>>
otpCreateNew
(
String
ownerId
)
async
{
// var deviceKey = await DeviceInfo.getDeviceId();
final
body
=
{
"owner_id"
:
ownerId
,
"ttl"
:
Constants
.
otpTtl
,
"resend_after_second"
:
Constants
.
otpTtl
,
'lang'
:
'vi'
};
final
body
=
{
"owner_id"
:
ownerId
,
"ttl"
:
Constants
.
otpTtl
,
"resend_after_second"
:
Constants
.
otpTtl
};
return
requestNormal
(
return
requestNormal
(
APIPaths
.
otpCreateNew
,
APIPaths
.
otpCreateNew
,
Method
.
POST
,
Method
.
POST
,
...
...
lib/preference/data_preference.dart
View file @
928c3660
...
@@ -58,6 +58,7 @@ class DataPreference {
...
@@ -58,6 +58,7 @@ class DataPreference {
}
}
String
?
get
rankName
=>
_profile
?.
workingSite
?.
primaryMembership
?.
membershipLevel
?.
levelName
??
""
;
String
?
get
rankName
=>
_profile
?.
workingSite
?.
primaryMembership
?.
membershipLevel
?.
levelName
??
""
;
String
?
get
token
=>
_loginToken
?.
accessToken
;
String
?
get
token
=>
_loginToken
?.
accessToken
;
String
?
get
refreshToken
=>
_loginToken
?.
refreshToken
;
String
?
get
phone
=>
_profile
?.
workerSite
?.
phoneNumber
;
String
?
get
phone
=>
_profile
?.
workerSite
?.
phoneNumber
;
bool
get
logged
=>
(
token
??
""
).
isNotEmpty
;
bool
get
logged
=>
(
token
??
""
).
isNotEmpty
;
ProfileResponseModel
?
get
profile
=>
_profile
;
ProfileResponseModel
?
get
profile
=>
_profile
;
...
...
lib/screen/data_network_service/data_network_service_screen.dart
View file @
928c3660
...
@@ -2,6 +2,7 @@ import 'dart:async';
...
@@ -2,6 +2,7 @@ import 'dart:async';
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/extensions/num_extension.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/screen/data_network_service/product_network_data_model.dart'
;
import
'package:mypoint_flutter_app/screen/data_network_service/product_network_data_model.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_empty_widget.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'
;
...
@@ -79,7 +80,7 @@ class _DataNetworkServiceScreenState extends BaseState<DataNetworkServiceScreen>
...
@@ -79,7 +80,7 @@ class _DataNetworkServiceScreenState extends BaseState<DataNetworkServiceScreen>
Widget
_buildButton
()
{
Widget
_buildButton
()
{
return
Obx
(()
{
return
Obx
(()
{
final
isValidInput
=
_viewModel
.
validateP
honeNumber
()
&&
(
_viewModel
.
selectedProduct
.
value
!=
null
);
final
isValidInput
=
_viewModel
.
p
honeNumber
.
value
.
isPhoneValid
()
&&
(
_viewModel
.
selectedProduct
.
value
!=
null
);
return
ElevatedButton
(
return
ElevatedButton
(
onPressed:
isValidInput
?
_redeemProductMobileCard
:
null
,
onPressed:
isValidInput
?
_redeemProductMobileCard
:
null
,
style:
ElevatedButton
.
styleFrom
(
style:
ElevatedButton
.
styleFrom
(
...
...
lib/screen/data_network_service/data_network_service_viewmodel.dart
View file @
928c3660
...
@@ -12,7 +12,7 @@ import '../voucher/models/product_model.dart';
...
@@ -12,7 +12,7 @@ import '../voucher/models/product_model.dart';
import
'../voucher/models/product_type.dart'
;
import
'../voucher/models/product_type.dart'
;
class
DataNetworkServiceViewModel
extends
RestfulApiViewModel
{
class
DataNetworkServiceViewModel
extends
RestfulApiViewModel
{
var
histories
=
RxList
<
String
>
()
;
final
RxList
<
String
>
histories
=
<
String
>
[].
obs
;
final
RxList
<
ProductBrandModel
>
topUpBrands
=
<
ProductBrandModel
>[].
obs
;
final
RxList
<
ProductBrandModel
>
topUpBrands
=
<
ProductBrandModel
>[].
obs
;
final
RxList
<
TopUpNetworkDataModel
>
topUpNetworkData
=
<
TopUpNetworkDataModel
>[].
obs
;
final
RxList
<
TopUpNetworkDataModel
>
topUpNetworkData
=
<
TopUpNetworkDataModel
>[].
obs
;
final
Map
<
String
,
List
<
TopUpNetworkDataModel
>>
_allValue
=
{};
final
Map
<
String
,
List
<
TopUpNetworkDataModel
>>
_allValue
=
{};
...
@@ -30,12 +30,6 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -30,12 +30,6 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
return
UserPointManager
().
point
>=
payPoint
;
return
UserPointManager
().
point
>=
payPoint
;
}
}
bool
validatePhoneNumber
()
{
final
phone
=
phoneNumber
.
value
.
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
(
phone
);
}
@override
@override
void
onInit
()
{
void
onInit
()
{
super
.
onInit
();
super
.
onInit
();
...
@@ -43,13 +37,13 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -43,13 +37,13 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
phoneNumber
.
value
=
myPhone
;
phoneNumber
.
value
=
myPhone
;
ContactStorageService
().
getUsedContacts
().
then
((
value
)
{
ContactStorageService
().
getUsedContacts
().
then
((
value
)
{
if
(
value
.
isNotEmpty
)
{
if
(
value
.
isNotEmpty
)
{
histories
.
value
=
value
;
histories
.
assignAll
(
value
)
;
}
else
{
}
else
{
histories
.
value
=
[
myPhone
];
histories
.
assignAll
(
[
myPhone
]
)
;
}
}
});
});
if
(!
histories
.
contains
(
myPhone
))
{
if
(!
histories
.
contains
(
myPhone
))
{
histories
.
value
.
insert
(
0
,
myPhone
);
histories
.
insert
(
0
,
myPhone
);
ContactStorageService
().
saveUsedContact
(
myPhone
);
ContactStorageService
().
saveUsedContact
(
myPhone
);
}
}
}
}
...
@@ -61,7 +55,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -61,7 +55,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
_getNetworkBrands
()
{
_getNetworkBrands
()
{
showLoading
();
showLoading
();
client
.
productTopUpBrands
().
then
((
response
)
{
client
.
productTopUpBrands
().
then
((
response
)
{
topUpBrands
.
value
=
response
.
data
??
[];
topUpBrands
.
assignAll
(
response
.
data
??
[]
)
;
hideLoading
();
hideLoading
();
checkMobileNetwork
();
checkMobileNetwork
();
}).
catchError
((
error
)
{
}).
catchError
((
error
)
{
...
@@ -83,7 +77,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -83,7 +77,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
hideLoading
();
hideLoading
();
getTelcoDetail
();
getTelcoDetail
();
}).
catchError
((
error
)
{
}).
catchError
((
error
)
{
final
first
=
topUpBrands
.
value
.
firstOrNull
;
final
first
=
topUpBrands
.
firstOrNull
;
if
(
first
!=
null
)
{
if
(
first
!=
null
)
{
selectedBrand
.
value
=
first
;
selectedBrand
.
value
=
first
;
}
}
...
@@ -119,7 +113,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -119,7 +113,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
// Dùng cache nếu có
// Dùng cache nếu có
if
(
_allValue
.
containsKey
(
code
))
{
if
(
_allValue
.
containsKey
(
code
))
{
final
cached
=
_allValue
[
code
]!;
final
cached
=
_allValue
[
code
]!;
topUpNetworkData
.
value
=
cached
;
topUpNetworkData
.
assignAll
(
cached
)
;
makeSelected
(
cached
);
makeSelected
(
cached
);
return
;
return
;
}
}
...
@@ -131,7 +125,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
...
@@ -131,7 +125,7 @@ class DataNetworkServiceViewModel extends RestfulApiViewModel {
.
where
((
e
)
=>
e
.
products
?.
isNotEmpty
==
true
)
.
where
((
e
)
=>
e
.
products
?.
isNotEmpty
==
true
)
.
toList
();
.
toList
();
_allValue
[
code
??
""
]
=
data
;
_allValue
[
code
??
""
]
=
data
;
topUpNetworkData
.
value
=
data
;
topUpNetworkData
.
assignAll
(
data
)
;
makeSelected
(
data
);
makeSelected
(
data
);
hideLoading
();
hideLoading
();
}
catch
(
error
)
{
}
catch
(
error
)
{
...
...
lib/screen/home/home_tab_viewmodel.dart
View file @
928c3660
...
@@ -26,8 +26,8 @@ class HomeTabViewModel extends RestfulApiViewModel {
...
@@ -26,8 +26,8 @@ class HomeTabViewModel extends RestfulApiViewModel {
final
RxList
<
AffiliateBrandModel
>
affiliates
=
<
AffiliateBrandModel
>[].
obs
;
final
RxList
<
AffiliateBrandModel
>
affiliates
=
<
AffiliateBrandModel
>[].
obs
;
final
RxList
<
MyProductModel
>
myProducts
=
<
MyProductModel
>[].
obs
;
final
RxList
<
MyProductModel
>
myProducts
=
<
MyProductModel
>[].
obs
;
final
RxList
<
MainSectionConfigModel
>
sectionLayouts
=
<
MainSectionConfigModel
>[].
obs
;
final
RxList
<
MainSectionConfigModel
>
sectionLayouts
=
<
MainSectionConfigModel
>[].
obs
;
var
flashSaleData
=
Rxn
<
FlashSaleModel
>();
final
Rxn
<
FlashSaleModel
>
flashSaleData
=
Rxn
<
FlashSaleModel
>();
var
hoverData
=
Rxn
<
HoverDataModel
>();
final
Rxn
<
HoverDataModel
>
hoverData
=
Rxn
<
HoverDataModel
>();
@override
@override
void
onInit
()
{
void
onInit
()
{
...
@@ -44,16 +44,16 @@ class HomeTabViewModel extends RestfulApiViewModel {
...
@@ -44,16 +44,16 @@ class HomeTabViewModel extends RestfulApiViewModel {
showLoading
();
showLoading
();
try
{
try
{
final
response
=
await
client
.
getSectionLayoutHome
();
final
response
=
await
client
.
getSectionLayoutHome
();
sectionLayouts
.
value
=
response
.
data
??
[];
sectionLayouts
.
assignAll
(
response
.
data
??
[]
)
;
hideLoading
();
hideLoading
();
}
catch
(
error
)
{
}
catch
(
error
)
{
sectionLayouts
.
value
=
await
_loadSectionLayoutHomeFromCache
();
sectionLayouts
.
assignAll
(
await
_loadSectionLayoutHomeFromCache
()
)
;
hideLoading
();
hideLoading
();
}
finally
{
}
finally
{
if
(
sectionLayouts
.
value
.
isEmpty
)
{
if
(
sectionLayouts
.
isEmpty
)
{
sectionLayouts
.
value
=
await
_loadSectionLayoutHomeFromCache
();
sectionLayouts
.
assignAll
(
await
_loadSectionLayoutHomeFromCache
()
)
;
}
}
for
(
final
section
in
sectionLayouts
.
value
)
{
for
(
final
section
in
sectionLayouts
)
{
await
_processSection
(
section
);
await
_processSection
(
section
);
}
}
}
}
...
@@ -63,7 +63,6 @@ class HomeTabViewModel extends RestfulApiViewModel {
...
@@ -63,7 +63,6 @@ class HomeTabViewModel extends RestfulApiViewModel {
try
{
try
{
final
result
=
await
client
.
getDataPiPiHome
();
final
result
=
await
client
.
getDataPiPiHome
();
hoverData
.
value
=
result
.
data
;
hoverData
.
value
=
result
.
data
;
hoverData
.
refresh
();
}
catch
(
error
)
{
}
catch
(
error
)
{
print
(
"Error fetching loadDataPiPiHome:
$error
"
);
print
(
"Error fetching loadDataPiPiHome:
$error
"
);
}
}
...
@@ -83,35 +82,35 @@ class HomeTabViewModel extends RestfulApiViewModel {
...
@@ -83,35 +82,35 @@ class HomeTabViewModel extends RestfulApiViewModel {
path
,
path
,
(
json
)
=>
MainServiceModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
MainServiceModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
services
.
value
=
res
.
data
??
[];
services
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
banner
:
case
HeaderSectionType
.
banner
:
final
res
=
await
client
.
fetchList
<
BannerModel
>(
final
res
=
await
client
.
fetchList
<
BannerModel
>(
path
,
path
,
(
json
)
=>
BannerModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
BannerModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
banners
.
value
=
res
.
data
??
[];
banners
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
campaign
:
case
HeaderSectionType
.
campaign
:
final
res
=
await
client
.
fetchList
<
AchievementModel
>(
final
res
=
await
client
.
fetchList
<
AchievementModel
>(
path
,
path
,
(
json
)
=>
AchievementModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
AchievementModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
achievements
.
value
=
res
.
data
??
[];
achievements
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
product
:
case
HeaderSectionType
.
product
:
final
res
=
await
client
.
fetchList
<
ProductModel
>(
final
res
=
await
client
.
fetchList
<
ProductModel
>(
path
,
path
,
(
json
)
=>
ProductModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
ProductModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
products
.
value
=
res
.
data
??
[];
products
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
news
:
case
HeaderSectionType
.
news
:
final
res
=
await
client
.
fetchList
<
PageItemModel
>(
final
res
=
await
client
.
fetchList
<
PageItemModel
>(
path
,
path
,
(
json
)
=>
PageItemModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
PageItemModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
news
.
value
=
res
.
data
??
[];
news
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
flashSale
:
case
HeaderSectionType
.
flashSale
:
final
res
=
await
client
.
fetchObject
<
FlashSaleModel
>(
final
res
=
await
client
.
fetchObject
<
FlashSaleModel
>(
...
@@ -125,21 +124,21 @@ class HomeTabViewModel extends RestfulApiViewModel {
...
@@ -125,21 +124,21 @@ class HomeTabViewModel extends RestfulApiViewModel {
path
,
path
,
(
json
)
=>
BrandModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
BrandModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
brands
.
value
=
res
.
data
??
[];
brands
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
case
HeaderSectionType
.
pointPartner
:
case
HeaderSectionType
.
pointPartner
:
final
res
=
await
client
.
fetchList
<
AffiliateBrandModel
>(
final
res
=
await
client
.
fetchList
<
AffiliateBrandModel
>(
path
,
path
,
(
json
)
=>
AffiliateBrandModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
AffiliateBrandModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
affiliates
.
value
=
(
res
.
data
??
[]).
take
(
6
).
toList
();
affiliates
.
assignAll
(
(
res
.
data
??
[]).
take
(
6
).
toList
()
)
;
break
;
break
;
case
HeaderSectionType
.
myProduct
:
case
HeaderSectionType
.
myProduct
:
final
res
=
await
client
.
fetchList
<
MyProductModel
>(
final
res
=
await
client
.
fetchList
<
MyProductModel
>(
path
,
path
,
(
json
)
=>
MyProductModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
(
json
)
=>
MyProductModel
.
fromJson
(
json
as
Map
<
String
,
dynamic
>),
);
);
myProducts
.
value
=
res
.
data
??
[];
myProducts
.
assignAll
(
res
.
data
??
[]
)
;
break
;
break
;
default
:
default
:
print
(
"Unknown section type:
${section.headerSectionType}
"
);
print
(
"Unknown section type:
${section.headerSectionType}
"
);
...
...
lib/screen/login/login_screen.dart
View file @
928c3660
...
@@ -32,7 +32,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -32,7 +32,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
final
args
=
Get
.
arguments
;
final
args
=
Get
.
arguments
;
if
(
args
is
Map
)
{
if
(
args
is
Map
)
{
phoneNumber
=
args
[
'phone'
];
phoneNumber
=
args
[
'phone'
];
fullName
=
args
[
'fullName'
]
??
''
;
fullName
=
args
[
'fullName'
]
??
'
Quý khách
'
;
}
}
loginVM
.
onShowChangePass
=
(
message
)
{
loginVM
.
onShowChangePass
=
(
message
)
{
Get
.
dialog
(
Get
.
dialog
(
...
@@ -45,7 +45,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -45,7 +45,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
AlertButton
(
AlertButton
(
text:
"Cài đặt ngay"
,
text:
"Cài đặt ngay"
,
onPressed:
()
{
onPressed:
()
{
loginVM
.
onForgotPassPressed
(
phoneNumber
);
},
},
bgColor:
BaseColor
.
primary500
,
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
textColor:
Colors
.
white
,
...
@@ -57,7 +57,9 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -57,7 +57,9 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
};
};
loginVM
.
onShowDeviceError
=
(
message
)
{
loginVM
.
onShowDeviceError
=
(
message
)
{
loginVM
.
onChangePhonePressed
();
showAlertError
(
content:
message
,
onConfirmed:
()
{
loginVM
.
onChangePhonePressed
();
});
};
};
loginVM
.
onShowInvalidAccount
=
(
message
)
{
loginVM
.
onShowInvalidAccount
=
(
message
)
{
...
@@ -281,23 +283,10 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
...
@@ -281,23 +283,10 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
return
Obx
(()
{
return
Obx
(()
{
bool
enabled
=
false
;
bool
enabled
=
false
;
Color
color
=
BaseColor
.
second400
;
Color
color
=
BaseColor
.
second400
;
switch
(
vm
.
loginState
.
value
)
{
if
(
vm
.
loginState
.
value
==
LoginState
.
typing
&&
vm
.
password
.
value
.
length
>=
6
)
{
case
LoginState
.
typing
:
color
=
BaseColor
.
primary500
;
if
(
vm
.
password
.
value
.
isNotEmpty
)
{
enabled
=
true
;
color
=
BaseColor
.
primary500
;
enabled
=
true
;
}
else
{
enabled
=
false
;
color
=
BaseColor
.
second400
;
}
break
;
case
LoginState
.
error
:
case
LoginState
.
idle
:
enabled
=
false
;
color
=
BaseColor
.
second400
;
break
;
}
}
return
Container
(
return
Container
(
color:
Colors
.
white
,
color:
Colors
.
white
,
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
8
),
...
...
lib/screen/login/login_viewmodel.dart
View file @
928c3660
import
'dart:convert'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/configs/constants.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/networking/restful_api_client_all_request.dart'
;
import
'package:mypoint_flutter_app/screen/otp/forgot_pass_otp_repository.dart'
;
import
'package:mypoint_flutter_app/screen/otp/forgot_pass_otp_repository.dart'
;
import
'package:mypoint_flutter_app/screen/otp/otp_screen.dart'
;
import
'package:mypoint_flutter_app/screen/otp/otp_screen.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../networking/restful_api_viewmodel.dart'
;
import
'../../networking/restful_api_viewmodel.dart'
;
import
'../../model/auth/login_token_response_model.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../firebase/push_token_service.dart'
;
import
'../../services/login_service.dart'
;
import
'../main_tab_screen/main_tab_screen.dart'
;
// login_state_enum.dart
enum
LoginState
{
idle
,
typing
,
error
}
enum
LoginState
{
idle
,
typing
,
error
}
class
LoginViewModel
extends
RestfulApiViewModel
{
class
LoginViewModel
extends
RestfulApiViewModel
{
...
@@ -27,6 +22,7 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -27,6 +22,7 @@ class LoginViewModel extends RestfulApiViewModel {
void
Function
(
String
message
)?
onShowDeviceError
;
void
Function
(
String
message
)?
onShowDeviceError
;
void
Function
(
String
message
)?
onShowChangePass
;
void
Function
(
String
message
)?
onShowChangePass
;
void
Function
(
String
message
)?
onShowInvalidAccount
;
void
Function
(
String
message
)?
onShowInvalidAccount
;
final
LoginService
_loginService
=
LoginService
();
@override
@override
void
onInit
()
{
void
onInit
()
{
...
@@ -52,27 +48,48 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -52,27 +48,48 @@ class LoginViewModel extends RestfulApiViewModel {
isPasswordVisible
.
value
=
!
isPasswordVisible
.
value
;
isPasswordVisible
.
value
=
!
isPasswordVisible
.
value
;
}
}
/// REFACTORED: Clean login method using LoginService
Future
<
void
>
onLoginPressed
(
String
phone
)
async
{
Future
<
void
>
onLoginPressed
(
String
phone
)
async
{
if
(
password
.
value
.
isEmpty
)
return
;
if
(
password
.
value
.
isEmpty
)
return
;
showLoading
();
showLoading
();
final
response
=
await
client
.
login
(
phone
,
password
.
value
);
try
{
hideLoading
();
final
result
=
await
_loginService
.
login
(
phone
,
password
.
value
);
_handleLoginResponse
(
response
,
phone
);
}
Future
<
void
>
_getUserProfile
()
async
{
showLoading
();
final
response
=
await
client
.
getUserProfile
();
final
userProfile
=
response
.
data
;
if
(
response
.
isSuccess
&&
userProfile
!=
null
)
{
await
DataPreference
.
instance
.
saveUserProfile
(
userProfile
);
hideLoading
();
hideLoading
();
Get
.
offAllNamed
(
mainScreen
);
_handleLoginResult
(
result
,
phone
);
}
else
{
}
catch
(
e
)
{
hideLoading
();
hideLoading
();
await
DataPreference
.
instance
.
clearLoginToken
();
print
(
'Login error:
${e.toString()}
'
);
final
mgs
=
response
.
errorMessage
??
Constants
.
commonError
;
onShowAlertError
?.
call
(
Constants
.
commonError
);
onShowAlertError
?.
call
(
mgs
);
}
}
/// REFACTORED: Handle login result with proper error handling
void
_handleLoginResult
(
LoginResponse
result
,
String
phone
)
{
switch
(
result
.
result
)
{
case
LoginResult
.
success
:
Get
.
offAllNamed
(
mainScreen
);
break
;
case
LoginResult
.
deviceUndefined
:
onShowDeviceError
?.
call
(
result
.
message
??
Constants
.
commonError
);
break
;
case
LoginResult
.
requiredChangePass
:
onShowChangePass
?.
call
(
result
.
message
??
Constants
.
commonError
);
break
;
case
LoginResult
.
invalidAccount
:
onShowInvalidAccount
?.
call
(
result
.
message
??
Constants
.
commonError
);
break
;
case
LoginResult
.
bioTokenInvalid
:
_loginService
.
clearBiometricToken
(
phone
);
onShowAlertError
?.
call
(
result
.
message
??
Constants
.
commonError
);
break
;
case
LoginResult
.
invalidCredentials
:
loginState
.
value
=
LoginState
.
error
;
break
;
case
LoginResult
.
networkError
:
case
LoginResult
.
unknownError
:
default
:
onShowAlertError
?.
call
(
result
.
message
??
Constants
.
commonError
);
break
;
}
}
}
}
...
@@ -87,42 +104,26 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -87,42 +104,26 @@ class LoginViewModel extends RestfulApiViewModel {
Future
<
void
>
onForgotPassPressed
(
String
phone
)
async
{
Future
<
void
>
onForgotPassPressed
(
String
phone
)
async
{
showLoading
();
showLoading
();
final
response
=
await
client
.
otpCreateNew
(
phone
);
try
{
hideLoading
();
final
response
=
await
client
.
otpCreateNew
(
phone
);
if
(!
response
.
isSuccess
)
return
;
hideLoading
();
Get
.
to
(
if
(!
response
.
isSuccess
)
{
OtpScreen
(
onShowAlertError
?.
call
(
response
.
errorMessage
??
Constants
.
commonError
);
repository:
ForgotPassOTPRepository
(
phone
,
response
.
data
?.
resendAfterSecond
??
Constants
.
otpTtl
),
return
;
),
);
}
Future
<
void
>
_handleLoginResponse
(
BaseResponseModel
<
LoginTokenResponseModel
>
response
,
String
phone
)
async
{
if
(
response
.
isSuccess
&&
response
.
data
!=
null
)
{
await
DataPreference
.
instance
.
saveLoginToken
(
response
.
data
!);
// Upload FCM token after login
await
PushTokenService
.
uploadIfLogged
();
await
_getUserProfile
();
return
;
}
final
errorMsg
=
response
.
errorMessage
??
Constants
.
commonError
;
final
errorCode
=
response
.
errorCode
;
if
(
errorCode
==
ErrorCodes
.
deviceUndefined
)
{
onShowDeviceError
?.
call
(
errorMsg
);
}
else
if
(
errorCode
==
ErrorCodes
.
requiredChangePass
)
{
onShowChangePass
?.
call
(
errorMsg
);
}
else
if
(
errorCode
==
ErrorCodes
.
invalidAccount
)
{
onShowInvalidAccount
?.
call
(
errorMsg
);
}
else
{
if
(
errorCode
==
ErrorCodes
.
bioTokenInvalid
)
{
DataPreference
.
instance
.
clearBioToken
(
phone
);
}
}
onShowAlertError
?.
call
(
errorMsg
);
Get
.
to
(
OtpScreen
(
repository:
ForgotPassOTPRepository
(
phone
,
response
.
data
?.
resendAfterSecond
??
Constants
.
otpTtl
),
),
);
}
catch
(
e
)
{
hideLoading
();
print
(
'OTP error:
${e.toString()}
'
);
onShowAlertError
?.
call
(
Constants
.
commonError
);
}
}
}
}
///
Xác thực đăng nhập bằng sinh trắc
///
REFACTORED: Biometric login using LoginService
Future
<
void
>
onBiometricLoginPressed
(
String
phone
)
async
{
Future
<
void
>
onBiometricLoginPressed
(
String
phone
)
async
{
final
isSupported
=
await
BiometricManager
().
isDeviceSupported
();
final
isSupported
=
await
BiometricManager
().
isDeviceSupported
();
if
(!
isSupported
)
{
if
(!
isSupported
)
{
...
@@ -136,8 +137,14 @@ class LoginViewModel extends RestfulApiViewModel {
...
@@ -136,8 +137,14 @@ class LoginViewModel extends RestfulApiViewModel {
return
;
return
;
}
}
showLoading
();
showLoading
();
final
response
=
await
client
.
loginWithBiometric
(
phone
);
try
{
hideLoading
();
final
result
=
await
_loginService
.
biometricLogin
(
phone
);
_handleLoginResponse
(
response
,
phone
);
hideLoading
();
_handleLoginResult
(
result
,
phone
);
}
catch
(
e
)
{
hideLoading
();
print
(
'Biometric login error:
${e.toString()}
'
);
onShowAlertError
?.
call
(
Constants
.
commonError
);
}
}
}
}
}
lib/screen/personal/personal_edit_viewmodel.dart
View file @
928c3660
...
@@ -7,6 +7,7 @@ import '../../networking/restful_api_viewmodel.dart';
...
@@ -7,6 +7,7 @@ import '../../networking/restful_api_viewmodel.dart';
import
'../../configs/constants.dart'
;
import
'../../configs/constants.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../../utils/validation_utils.dart'
;
import
'../location_address/location_address_viewmodel.dart'
;
import
'../location_address/location_address_viewmodel.dart'
;
class
PersonalEditViewModel
extends
RestfulApiViewModel
{
class
PersonalEditViewModel
extends
RestfulApiViewModel
{
...
@@ -155,7 +156,6 @@ class PersonalEditViewModel extends RestfulApiViewModel {
...
@@ -155,7 +156,6 @@ class PersonalEditViewModel extends RestfulApiViewModel {
}
}
bool
isValidEmail
(
String
email
)
{
bool
isValidEmail
(
String
email
)
{
final
emailRegex
=
RegExp
(
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'
);
return
ValidationUtils
.
isValidEmail
(
email
);
return
emailRegex
.
hasMatch
(
email
);
}
}
}
}
lib/screen/splash/splash_screen_viewmodel.dart
View file @
928c3660
...
@@ -70,17 +70,17 @@ class SplashScreenViewModel extends RestfulApiViewModel {
...
@@ -70,17 +70,17 @@ class SplashScreenViewModel extends RestfulApiViewModel {
}
}
void
_directionWhenTokenInvalid
()
{
void
_directionWhenTokenInvalid
()
{
// TODO: handle later
Get
.
toNamed
(
onboardingScreen
);
Get
.
toNamed
(
onboardingScreen
);
return
;
// if (kIsWeb) {
if
(
kIsWeb
)
{
// print('❌ No token found on web, cannot proceed');
print
(
'❌ No token found on web, cannot proceed'
);
// webCloseApp({
webCloseApp
({
// 'message': 'No token found, cannot proceed',
'message'
:
'No token found, cannot proceed'
,
// 'timestamp': DateTime.now().millisecondsSinceEpoch,
'timestamp'
:
DateTime
.
now
().
millisecondsSinceEpoch
,
// });
});
// } else {
}
else
{
// Get.toNamed(onboardingScreen);
Get
.
toNamed
(
onboardingScreen
);
// }
}
}
}
void
_freshDataAndToMainScreen
(
ProfileResponseModel
userProfile
)
async
{
void
_freshDataAndToMainScreen
(
ProfileResponseModel
userProfile
)
async
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
async
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
async
{
...
...
lib/screen/topup/topup_screen.dart
View file @
928c3660
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'package:intl/intl.dart'
;
import
'package:intl/intl.dart'
;
import
'package:mypoint_flutter_app/extensions/string_extension.dart'
;
import
'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart'
;
import
'package:mypoint_flutter_app/screen/topup/topup_viewmodel.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'
;
import
'package:mypoint_flutter_app/widgets/custom_navigation_bar.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
import
'package:mypoint_flutter_app/widgets/image_loader.dart'
;
...
@@ -331,7 +332,7 @@ class _PhoneTopUpScreenState extends BaseState<PhoneTopUpScreen> with BasicState
...
@@ -331,7 +332,7 @@ class _PhoneTopUpScreenState extends BaseState<PhoneTopUpScreen> with BasicState
),
),
const
Spacer
(),
const
Spacer
(),
ElevatedButton
(
ElevatedButton
(
onPressed:
_viewModel
.
validateP
honeNumber
()
?
()
{
onPressed:
_viewModel
.
p
honeNumber
.
value
.
isPhoneValid
()
?
()
{
Get
.
toNamed
(
Get
.
toNamed
(
transactionDetailScreen
,
transactionDetailScreen
,
arguments:
{
"product"
:
product
,
"quantity"
:
1
,
"targetPhoneNumber"
:
_viewModel
.
phoneNumber
.
value
},
arguments:
{
"product"
:
product
,
"quantity"
:
1
,
"targetPhoneNumber"
:
_viewModel
.
phoneNumber
.
value
},
...
...
lib/screen/topup/topup_viewmodel.dart
View file @
928c3660
...
@@ -10,7 +10,7 @@ import '../voucher/models/product_model.dart';
...
@@ -10,7 +10,7 @@ import '../voucher/models/product_model.dart';
import
'../voucher/models/product_type.dart'
;
import
'../voucher/models/product_type.dart'
;
class
TopUpViewModel
extends
RestfulApiViewModel
{
class
TopUpViewModel
extends
RestfulApiViewModel
{
var
histories
=
RxList
<
String
>
()
;
final
RxList
<
String
>
histories
=
<
String
>
[].
obs
;
final
RxList
<
ProductBrandModel
>
topUpBrands
=
<
ProductBrandModel
>[].
obs
;
final
RxList
<
ProductBrandModel
>
topUpBrands
=
<
ProductBrandModel
>[].
obs
;
final
RxList
<
ProductModel
>
products
=
<
ProductModel
>[].
obs
;
final
RxList
<
ProductModel
>
products
=
<
ProductModel
>[].
obs
;
var
selectedBrand
=
Rxn
<
ProductBrandModel
>();
var
selectedBrand
=
Rxn
<
ProductBrandModel
>();
...
@@ -25,23 +25,17 @@ class TopUpViewModel extends RestfulApiViewModel {
...
@@ -25,23 +25,17 @@ class TopUpViewModel extends RestfulApiViewModel {
phoneNumber
.
value
=
myPhone
;
phoneNumber
.
value
=
myPhone
;
ContactStorageService
().
getUsedContacts
().
then
((
value
)
{
ContactStorageService
().
getUsedContacts
().
then
((
value
)
{
if
(
value
.
isNotEmpty
)
{
if
(
value
.
isNotEmpty
)
{
histories
.
value
=
value
;
histories
.
assignAll
(
value
)
;
}
else
{
}
else
{
histories
.
value
=
[
myPhone
];
histories
.
assignAll
(
[
myPhone
]
)
;
}
}
});
});
if
(!
histories
.
contains
(
myPhone
))
{
if
(!
histories
.
contains
(
myPhone
))
{
histories
.
value
.
insert
(
0
,
myPhone
);
histories
.
insert
(
0
,
myPhone
);
ContactStorageService
().
saveUsedContact
(
myPhone
);
ContactStorageService
().
saveUsedContact
(
myPhone
);
}
}
}
}
bool
validatePhoneNumber
()
{
final
phone
=
phoneNumber
.
value
.
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
(
phone
);
}
firstLoadTopUpData
()
async
{
firstLoadTopUpData
()
async
{
_getTopUpBrands
();
_getTopUpBrands
();
}
}
...
@@ -50,7 +44,7 @@ class TopUpViewModel extends RestfulApiViewModel {
...
@@ -50,7 +44,7 @@ class TopUpViewModel extends RestfulApiViewModel {
await
callApi
<
List
<
ProductBrandModel
>>(
await
callApi
<
List
<
ProductBrandModel
>>(
request:
()
=>
client
.
getTopUpBrands
(
ProductType
.
topupMobile
),
request:
()
=>
client
.
getTopUpBrands
(
ProductType
.
topupMobile
),
onSuccess:
(
data
,
_
)
{
onSuccess:
(
data
,
_
)
{
topUpBrands
.
value
=
data
;
topUpBrands
.
assignAll
(
data
)
;
checkMobileNetwork
();
checkMobileNetwork
();
},
},
showAppNavigatorDialog:
true
,
showAppNavigatorDialog:
true
,
...
@@ -66,7 +60,7 @@ class TopUpViewModel extends RestfulApiViewModel {
...
@@ -66,7 +60,7 @@ class TopUpViewModel extends RestfulApiViewModel {
?
topUpBrands
.
firstWhere
(
?
topUpBrands
.
firstWhere
(
(
brand
)
=>
brand
.
code
==
brandCode
,
(
brand
)
=>
brand
.
code
==
brandCode
,
orElse:
()
=>
topUpBrands
.
first
,
orElse:
()
=>
topUpBrands
.
first
,
)
:
topUpBrands
.
value
.
firstOrNull
;
)
:
topUpBrands
.
firstOrNull
;
selectedBrand
.
value
=
brand
;
selectedBrand
.
value
=
brand
;
getTelcoDetail
();
getTelcoDetail
();
},
},
...
@@ -102,7 +96,7 @@ class TopUpViewModel extends RestfulApiViewModel {
...
@@ -102,7 +96,7 @@ class TopUpViewModel extends RestfulApiViewModel {
// Dùng cache nếu có
// Dùng cache nếu có
if
(
_allValue
.
containsKey
(
code
))
{
if
(
_allValue
.
containsKey
(
code
))
{
final
cached
=
_allValue
[
code
]!;
final
cached
=
_allValue
[
code
]!;
products
.
value
=
cached
;
products
.
assignAll
(
cached
)
;
makeSelected
(
cached
);
makeSelected
(
cached
);
return
;
return
;
}
}
...
@@ -116,7 +110,7 @@ class TopUpViewModel extends RestfulApiViewModel {
...
@@ -116,7 +110,7 @@ class TopUpViewModel extends RestfulApiViewModel {
request:
()
=>
client
.
getProducts
(
body
),
request:
()
=>
client
.
getProducts
(
body
),
onSuccess:
(
data
,
_
)
{
onSuccess:
(
data
,
_
)
{
_allValue
[
code
]
=
data
;
_allValue
[
code
]
=
data
;
products
.
value
=
data
;
products
.
assignAll
(
data
)
;
makeSelected
(
data
);
makeSelected
(
data
);
},
},
showAppNavigatorDialog:
true
,
showAppNavigatorDialog:
true
,
...
...
lib/screen/voucher/my_voucher/my_product_list_viewmodel.dart
View file @
928c3660
...
@@ -25,14 +25,23 @@ class MyProductListViewModel extends RestfulApiViewModel {
...
@@ -25,14 +25,23 @@ class MyProductListViewModel extends RestfulApiViewModel {
"size"
:
20
,
"size"
:
20
,
"status"
:
selectedTabIndex
.
value
,
"status"
:
selectedTabIndex
.
value
,
};
};
if
(
isRefresh
)
{
showLoading
();
}
client
.
getCustomerProducts
(
body
).
then
((
response
)
{
client
.
getCustomerProducts
(
body
).
then
((
response
)
{
final
result
=
response
.
data
??
[];
final
result
=
response
.
data
??
[];
if
(
isRefresh
)
{
if
(
isRefresh
)
{
hideLoading
();
myProducts
.
clear
();
myProducts
.
clear
();
}
}
myProducts
.
addAll
(
result
);
myProducts
.
addAll
(
result
);
}).
catchError
((
error
)
{
}).
catchError
((
error
)
{
myProducts
.
clear
();
hideLoading
();
if
(
isRefresh
)
{
myProducts
.
clear
();
}
print
(
'Error fetching products:
$error
'
);
print
(
'Error fetching products:
$error
'
);
});
});
}
}
...
...
lib/screen/voucher/voucher_list/voucher_list_screen.dart
View file @
928c3660
import
'dart:async'
;
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get.dart'
;
import
'../../../base/base_screen.dart'
;
import
'../../../base/base_screen.dart'
;
...
@@ -37,7 +36,7 @@ class _VoucherListScreenState extends BaseState<VoucherListScreen> with BasicSta
...
@@ -37,7 +36,7 @@ class _VoucherListScreenState extends BaseState<VoucherListScreen> with BasicSta
isHotProduct
=
args
[
'isHotProduct'
]
??
false
;
isHotProduct
=
args
[
'isHotProduct'
]
??
false
;
isFavorite
=
args
[
'favorite'
]
??
false
;
isFavorite
=
args
[
'favorite'
]
??
false
;
_viewModel
=
Get
.
put
(
VoucherListViewModel
(
isHotProduct:
isHotProduct
,
isFavorite:
isFavorite
));
_viewModel
=
Get
.
put
(
VoucherListViewModel
(
isHotProduct:
isHotProduct
,
isFavorite:
isFavorite
));
_remainingSeconds
=
10
;
//
args['countDownSecond'] ?? 0;
_remainingSeconds
=
args
[
'countDownSecond'
]
??
0
;
_viewModel
.
submitCampaignViewVoucherResponse
=
(
response
)
{
_viewModel
.
submitCampaignViewVoucherResponse
=
(
response
)
{
final
popup
=
response
.
data
?.
popup
;
final
popup
=
response
.
data
?.
popup
;
if
(
popup
!=
null
)
{
if
(
popup
!=
null
)
{
...
@@ -55,28 +54,25 @@ class _VoucherListScreenState extends BaseState<VoucherListScreen> with BasicSta
...
@@ -55,28 +54,25 @@ class _VoucherListScreenState extends BaseState<VoucherListScreen> with BasicSta
});
});
}
}
@override
@override
void
dispose
()
{
void
onRouteWillDisappear
()
{
_countdownTimer
?.
cancel
();
super
.
onRouteWillDisappear
();
super
.
dispose
();
// Pause timer khi route bị che phủ (push sang màn hình khác)
_pauseCountdown
();
}
}
@override
@override
void
didChangeDependencies
()
{
void
onRouteDidAppear
()
{
super
.
didChangeDependencies
();
super
.
onRouteDidAppear
();
// Khi màn hình trở lại visible (route current) → resume timer nếu còn thời gian
// Resume timer khi route trở lại visible
final
isCurrent
=
ModalRoute
.
of
(
context
)?.
isCurrent
??
true
;
_resumeCountdownIfNeeded
();
if
(
isCurrent
)
{
_resumeCountdownIfNeeded
();
}
}
}
@override
@override
void
deactivate
()
{
void
onDispose
()
{
// Luôn pause khi rời màn hình để tránh timer chạy nền
_pauseCountdown
();
_pauseCountdown
();
super
.
deactivate
();
super
.
onDispose
();
print
(
'VoucherListScreen deactivate'
);
}
}
void
_startCountdownIfNeeded
()
{
void
_startCountdownIfNeeded
()
{
...
...
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