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
5fb93f2d
Commit
5fb93f2d
authored
Apr 23, 2025
by
DatHV
Browse files
update logic authen
parent
73074efa
Changes
39
Show whitespace changes
Inline
Side-by-side
lib/screen/create_pass/reset_create_password_repository.dart
View file @
5fb93f2d
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/screen/create_pass/signup_create_password_repository.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../login/login_screen.dart'
;
import
'../splash/splash_screen_viewmodel.dart'
;
class
ResetCreatePasswordRepository
extends
RestfulApiViewModel
implements
ICreatePasswordRepository
{
...
...
@@ -19,7 +19,7 @@ class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICrea
hideLoading
();
if
(
value
.
status
==
"success"
||
value
.
code
==
200
)
{
print
(
"Reset password success"
);
Get
.
off
(()
=>
L
oginScreen
(
phoneNumber
:
phoneNumber
)
)
;
Get
.
off
Named
(
l
oginScreen
,
arguments
:
phoneNumber
);
}
return
value
;
});
...
...
lib/screen/create_pass/signup_create_password_repository.dart
View file @
5fb93f2d
...
...
@@ -2,11 +2,11 @@ import 'dart:async';
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/screen/login/login_screen.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../biometric/biometric_screen.dart'
;
import
'../main_tab_screen/main_tab_screen.dart'
;
import
'../splash/splash_screen_viewmodel.dart'
;
...
...
@@ -43,7 +43,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
await
DataPreference
.
instance
.
saveLoginToken
(
response
.
data
!);
_getUserProfile
();
}
else
{
Get
.
off
(()
=>
L
oginScreen
(
phoneNumber
:
phoneNumber
)
)
;
Get
.
off
Named
(
l
oginScreen
,
arguments
:
phoneNumber
);
}
});
}
...
...
@@ -62,7 +62,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
}
}
else
{
DataPreference
.
instance
.
clearLoginToken
();
Get
.
off
(()
=>
L
oginScreen
(
phoneNumber
:
phoneNumber
)
)
;
Get
.
off
Named
(
l
oginScreen
,
arguments
:
phoneNumber
);
}
});
}
...
...
lib/screen/delete_account/delete_account_dialog.dart
0 → 100644
View file @
5fb93f2d
// delete_account_dialog.dart
import
'package:flutter/material.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../../preference/point/point_manager.dart'
;
import
'../../resouce/button_style.dart'
;
import
'../../resouce/text_style.dart'
;
import
'../../shared/router_gage.dart'
;
import
'../pageDetail/campaign_detail_screen.dart'
;
import
'../pageDetail/model/detail_page_rule_type.dart'
;
class
DeleteAccountDialog
extends
StatefulWidget
{
const
DeleteAccountDialog
({
super
.
key
});
@override
State
<
DeleteAccountDialog
>
createState
()
=>
_DeleteAccountDialogState
();
}
class
_DeleteAccountDialogState
extends
State
<
DeleteAccountDialog
>
{
bool
agreed
=
false
;
late
TapGestureRecognizer
_termConditionRecognizer
;
@override
void
initState
()
{
super
.
initState
();
_termConditionRecognizer
=
TapGestureRecognizer
()..
onTap
=
_onTermConditionPressed
;
}
@override
void
dispose
()
{
_termConditionRecognizer
.
dispose
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
final
int
userPoints
=
UserPointManager
().
point
;
return
Padding
(
padding:
const
EdgeInsets
.
fromLTRB
(
8
,
0
,
8
,
32
),
child:
Column
(
mainAxisSize:
MainAxisSize
.
min
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
Image
.
asset
(
'assets/images/ic_pipi_03.png'
,
height:
220
),
const
SizedBox
(
height:
16
),
Text
(
"Bạn có chắc chắn muốn xoá tài khoản?"
,
style:
AppTextStyle
.
title
,
textAlign:
TextAlign
.
center
),
const
SizedBox
(
height:
12
),
RichText
(
textAlign:
TextAlign
.
center
,
text:
TextSpan
(
style:
AppTextStyle
.
content
,
children:
[
const
TextSpan
(
text:
"Toàn bộ "
),
TextSpan
(
text:
"
$userPoints
điểm "
,
style:
AppTextStyle
.
boldContent
),
const
TextSpan
(
text:
"và "
),
WidgetSpan
(
child:
GestureDetector
(
onTap:
_onUnUsedVoucherPressed
,
child:
Text
(
"Ưu đãi chưa sử dụng "
,
style:
AppTextStyle
.
link
.
copyWith
(
color:
Colors
.
blue
,
decoration:
TextDecoration
.
underline
),
),
),
),
const
TextSpan
(
text:
"sẽ bị mất và bạn không thể đăng ký lại tài khoản trong vòng "
),
const
TextSpan
(
text:
"30 ngày"
,
style:
AppTextStyle
.
boldContent
),
const
TextSpan
(
text:
" kể từ thời điểm xoá."
),
],
),
),
const
SizedBox
(
height:
16
),
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Theme
(
data:
Theme
.
of
(
context
).
copyWith
(
checkboxTheme:
CheckboxThemeData
(
fillColor:
MaterialStateProperty
.
resolveWith
<
Color
>((
states
)
{
if
(
states
.
contains
(
MaterialState
.
selected
))
{
return
Colors
.
red
;
// ✅ Checked: màu đỏ
}
return
Colors
.
white
;
// ❌ Unchecked: ô trắng
}),
checkColor:
MaterialStateProperty
.
all
(
Colors
.
white
),
// ✅ Tick màu trắng
side:
const
BorderSide
(
color:
Colors
.
grey
),
),
),
child:
Checkbox
(
value:
agreed
,
onChanged:
(
value
)
=>
setState
(()
=>
agreed
=
value
??
false
),
visualDensity:
const
VisualDensity
(
horizontal:
-
4
,
vertical:
-
4
),
),
),
const
SizedBox
(
width:
8
),
Expanded
(
child:
RichText
(
text:
TextSpan
(
style:
AppTextStyle
.
content
,
children:
[
const
TextSpan
(
text:
"Tôi đã đọc và đồng ý với "
),
TextSpan
(
text:
"Điều khoản xoá tài khoản của MyPoint."
,
style:
AppTextStyle
.
link
.
copyWith
(
color:
Colors
.
blue
,
decoration:
TextDecoration
.
underline
),
recognizer:
_termConditionRecognizer
,
),
],
),
),
),
],
),
const
SizedBox
(
height:
16
),
SizedBox
(
width:
double
.
infinity
,
height:
40
,
child:
ElevatedButton
(
onPressed:
agreed
?
_onConfirmDelete
:
null
,
style:
AppButtonStyle
.
secondary
,
child:
const
Text
(
"Xác nhận xoá"
),
),
),
const
SizedBox
(
height:
8
),
SizedBox
(
width:
double
.
infinity
,
height:
40
,
child:
ElevatedButton
(
onPressed:
()
=>
Get
.
back
(),
style:
AppButtonStyle
.
primary
,
child:
const
Text
(
"Để sau"
),
),
),
],
),
);
}
void
_onConfirmDelete
()
{
if
(
DataPreference
.
instance
.
profile
?.
userAgreements
?.
hideDeleteAccount
==
false
)
{
}
else
{
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
onboardingScreen
);
}
}
void
_onUnUsedVoucherPressed
()
{
print
(
"Đi đến màn hình ưu đãi chưa sử dụng"
);
// TODO: Get.toNamed('/unused-voucher'); hoặc mở webview
}
void
_onTermConditionPressed
()
{
Get
.
to
(
CampaignDetailScreen
(
type:
DetailPageRuleType
.
policyDeleteAccount
));
}
}
lib/screen/delete_account/delete_account_viewmodel.dart
0 → 100644
View file @
5fb93f2d
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../configs/constants.dart'
;
import
'../otp/delete_account_otp_repository.dart'
;
import
'../otp/otp_screen.dart'
;
import
'../otp/verify_otp_repository.dart'
;
class
DeleteAccountViewModel
extends
RestfulApiViewModel
{
RxBool
agreed
=
false
.
obs
;
void
confirmDelete
()
{
if
(
agreed
.
value
)
{
showLoading
();
client
.
requestOtpDeleteAccount
().
then
((
value
)
{
hideLoading
();
if
(
value
.
isSuccess
)
{
final
phone
=
DataPreference
.
instance
.
phone
??
""
;
Get
.
to
(
()
=>
OtpScreen
(
repository:
DeleteAccountOtpRepository
(
phone
,
value
.
data
?.
resendAfterSecond
??
0
)),
);
}
else
{
final
mgs
=
value
.
errorMessage
??
Constants
.
commonError
;
Get
.
snackbar
(
"Thông báo"
,
mgs
);
}
});
}
else
{
Get
.
snackbar
(
"Thông báo"
,
"Bạn cần đồng ý với điều khoản để tiếp tục."
);
}
}
}
lib/screen/home/home_screen.dart
View file @
5fb93f2d
...
...
@@ -2,7 +2,7 @@
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../setting/setting_screen.dart'
;
class
HomeScreen
extends
StatelessWidget
{
...
...
@@ -13,7 +13,7 @@ class HomeScreen extends StatelessWidget {
return
Scaffold
(
body:
Center
(
child:
Column
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
// ✅ căn giữa dọc
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
center
,
children:
[
ElevatedButton
(
onPressed:
()
=>
_logout
(
context
),
child:
const
Text
(
'Đăng xuất'
)),
...
...
@@ -40,11 +40,30 @@ class HomeScreen extends StatelessWidget {
if
(
confirm
==
true
)
{
DataPreference
.
instance
.
clearLoginToken
();
Get
.
offAllNamed
(
'/onboarding'
);
_safeBackToLogin
();
// Get.until((route) => route.settings.name == loginScreen);
}
}
void
_safeBackToLogin
()
{
bool
found
=
false
;
Navigator
.
popUntil
(
Get
.
context
!,
(
route
)
{
final
matched
=
route
.
settings
.
name
==
loginScreen
;
if
(
matched
)
found
=
true
;
return
matched
;
});
final
phone
=
DataPreference
.
instance
.
phone
;
if
(
phone
!=
null
)
{
if
(!
found
)
{
Get
.
offAllNamed
(
loginScreen
,
arguments:
phone
);
}
}
else
{
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
onboardingScreen
);
}
}
void
_showSetting
(
BuildContext
context
)
async
{
Get
.
to
(()
=>
const
S
ettingScreen
()
);
Get
.
to
Named
(
s
ettingScreen
);
}
}
\ No newline at end of file
lib/screen/login/login_screen.dart
View file @
5fb93f2d
...
...
@@ -13,9 +13,7 @@ import '../../widgets/support_button.dart';
import
'login_viewmodel.dart'
;
class
LoginScreen
extends
BaseScreen
{
final
String
phoneNumber
;
final
String
?
fullName
;
const
LoginScreen
({
super
.
key
,
required
this
.
phoneNumber
,
this
.
fullName
});
const
LoginScreen
({
super
.
key
});
@override
State
<
LoginScreen
>
createState
()
=>
_LoginScreenState
();
...
...
@@ -26,9 +24,19 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
final
FocusNode
_focusNode
=
FocusNode
();
final
loginVM
=
Get
.
put
(
LoginViewModel
());
late
final
String
phoneNumber
;
late
String
fullName
=
""
;
@override
void
initState
()
{
super
.
initState
();
final
args
=
Get
.
arguments
;
if
(
args
is
String
)
{
phoneNumber
=
args
;
}
else
if
(
args
is
Map
)
{
phoneNumber
=
args
[
'phone'
];
fullName
=
args
[
'fullName'
];
}
loginVM
.
onShowChangePass
=
(
message
)
{
Get
.
dialog
(
CustomAlertDialog
(
...
...
@@ -67,7 +75,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
AlertButton
(
text:
"Quên mật khẩu"
,
onPressed:
()
{
loginVM
.
onForgotPassPressed
(
widget
.
phoneNumber
);
loginVM
.
onForgotPassPressed
(
phoneNumber
);
},
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
...
...
@@ -160,9 +168,9 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
style:
const
TextStyle
(
fontSize:
14
,
color:
BaseColor
.
second500
),
children:
[
const
TextSpan
(
text:
"Xin chào "
),
TextSpan
(
text:
widget
.
fullName
?
?
"Quý Khách "
),
TextSpan
(
text:
fullName
.
isEmpty
?
"Quý Khách
"
:
"
$fullName
"
),
TextSpan
(
text:
widget
.
phoneNumber
,
text:
phoneNumber
,
style:
const
TextStyle
(
fontWeight:
FontWeight
.
w500
,
color:
BaseColor
.
primary500
),
),
],
...
...
@@ -231,7 +239,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
),
TextButton
(
onPressed:
()
{
vm
.
onForgotPassPressed
(
widget
.
phoneNumber
);
vm
.
onForgotPassPressed
(
phoneNumber
);
},
child:
const
Text
(
"Quên mật khẩu?"
,
style:
TextStyle
(
fontSize:
14
,
color:
Color
(
0xFF3662FE
))),
),
...
...
@@ -256,7 +264,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
children:
[
IconButton
(
icon:
Icon
(
icon
,
size:
36
),
onPressed:
()
=>
vm
.
onBiometricLoginPressed
(
widget
.
phoneNumber
),
onPressed:
()
=>
vm
.
onBiometricLoginPressed
(
phoneNumber
),
),
Text
(
"Đăng nhập bằng
$label
"
),
],
...
...
@@ -295,7 +303,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
shape:
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
circular
(
8
)),
),
onPressed:
()
{
enabled
?
vm
.
onLoginPressed
(
widget
.
phoneNumber
)
:
null
;
enabled
?
vm
.
onLoginPressed
(
phoneNumber
)
:
null
;
},
child:
const
Text
(
"Đăng nhập"
,
...
...
lib/screen/login/login_viewmodel.dart
View file @
5fb93f2d
...
...
@@ -129,12 +129,12 @@ class LoginViewModel extends RestfulApiViewModel {
onShowAlertError
?.
call
(
"Thiết bị không hỗ trợ sinh trắc học"
);
return
;
}
final
bioToken
=
await
DataPreference
.
instance
.
getBioToken
(
phone
);
if
(
bioToken
==
null
)
{
final
bioToken
=
await
DataPreference
.
instance
.
getBioToken
(
phone
)
??
""
;
if
(
bioToken
.
isEmpty
)
{
onShowAlertError
?.
call
(
"Tài khoản này chưa kích hoạt đăng nhập bằng sinh trắc học!
\n
Vui lòng đăng nhập > cài đặt để kích hoạt tính năng"
);
return
;
}
client
.
login
(
phone
,
password
.
valu
e
).
then
((
value
)
async
{
client
.
login
WithBiometric
(
phon
e
).
then
((
value
)
async
{
hideLoading
();
_handleLoginResponse
(
value
,
phone
);
});
...
...
lib/screen/onboarding/onboarding_screen.dart
View file @
5fb93f2d
...
...
@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import
'package:flutter_widget_from_html/flutter_widget_from_html.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/base_response_model.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../configs/constants.dart'
;
...
...
@@ -25,7 +26,7 @@ class OnboardingScreen extends BaseScreen {
}
class
_OnboardingScreenState
extends
BaseState
<
OnboardingScreen
>
with
BasicState
{
final
OnboardingViewModel
_viewModel
=
Get
.
find
<
OnboardingViewModel
>
();
final
OnboardingViewModel
_viewModel
=
Get
.
put
(
OnboardingViewModel
(
)
);
final
FocusNode
_focusNode
=
FocusNode
();
@override
...
...
@@ -72,7 +73,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
return
;
}
if
(
response
.
nextAction
==
"login"
)
{
Get
.
to
(()
=>
L
oginScreen
(
phoneNumber
:
_viewModel
.
phoneNumber
.
value
)
)
;
Get
.
to
Named
(
l
oginScreen
,
arguments
:
_viewModel
.
phoneNumber
.
value
);
}
}
...
...
lib/screen/otp/delete_account_otp_repository.dart
0 → 100644
View file @
5fb93f2d
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../splash/splash_screen_viewmodel.dart'
;
import
'otp_viewmodel.dart'
;
class
DeleteAccountOtpRepository
extends
RestfulApiViewModel
implements
IOtpRepository
{
DeleteAccountOtpRepository
(
this
.
phoneNumber
,
this
.
otpTtl
);
@override
int
otpTtl
;
@override
String
phoneNumber
;
@override
Future
<
void
>
sendOtp
()
async
{}
@override
Future
<
BaseResponseModel
<
EmptyCodable
>>
verifyOtp
(
String
otpCode
)
async
{
showLoading
();
return
client
.
verifyDeleteAccount
(
otpCode
).
then
((
value
)
{
hideLoading
();
if
(
value
.
isSuccess
)
{
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
onboardingScreen
);
}
return
value
;
});
}
@override
Future
<
int
?>
resendOtp
()
async
{
// showLoading();
// return client.resendOTP(mfaToken).then((value) {
// hideLoading();
// return value.data?.otpTtl;
// });
}
}
lib/screen/otp/verify_otp_repository.dart
View file @
5fb93f2d
...
...
@@ -2,6 +2,7 @@
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../create_pass/create_pass_screen.dart'
;
...
...
@@ -30,7 +31,7 @@ class VerifyOtpRepository extends RestfulApiViewModel implements IOtpRepository
if
(
value
.
data
?.
claim
?.
action
==
"signup"
)
{
Get
.
off
(()
=>
CreatePasswordScreen
(
repository:
SignUpCreatePasswordRepository
(
phoneNumber
)));
}
else
if
(
value
.
data
?.
claim
?.
action
==
"login"
)
{
Get
.
off
(()
=>
L
oginScreen
(
phoneNumber
:
phoneNumber
)
)
;
Get
.
off
Named
(
l
oginScreen
,
arguments
:
phoneNumber
);
}
return
value
;
});
...
...
lib/screen/setting/setting_screen.dart
View file @
5fb93f2d
...
...
@@ -2,12 +2,32 @@
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'package:mypoint_flutter_app/screen/setting/setting_viewmodel.dart'
;
import
'../../widgets/bottom_sheet_helper.dart'
;
import
'../../widgets/custom_app_bar.dart'
;
import
'../change_pass/change_pass_screen.dart'
;
import
'../delete_account/delete_account_dialog.dart'
;
class
SettingScreen
extends
State
less
Widget
{
class
SettingScreen
extends
State
ful
Widget
{
const
SettingScreen
({
super
.
key
});
@override
State
<
SettingScreen
>
createState
()
=>
_SettingScreenState
();
}
class
_SettingScreenState
extends
State
<
SettingScreen
>
{
final
SettingViewModel
viewModel
=
SettingViewModel
();
@override
void
initState
()
{
super
.
initState
();
viewModel
.
loadBiometricStatus
().
then
((
enabled
)
{
setState
(()
{
viewModel
.
biometricEnabled
=
enabled
;
});
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
...
...
@@ -17,17 +37,9 @@ class SettingScreen extends StatelessWidget {
children:
[
Container
(
width:
double
.
infinity
,
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
0
,
vertical:
16
),
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
0
,
vertical:
0
),
decoration:
BoxDecoration
(
color:
Colors
.
white
,
// borderRadius: BorderRadius.circular(12),
// boxShadow: [
// BoxShadow(
// color: Colors.black12,
// blurRadius: 8,
// offset: const Offset(0, 4),
// ),
// ],
),
child:
Column
(
children:
[
...
...
@@ -47,6 +59,19 @@ class SettingScreen extends StatelessWidget {
icon:
Icons
.
fingerprint
,
title:
'Xác thực sinh trắc học'
,
showTrailing:
false
,
trailing:
Switch
(
value:
viewModel
.
biometricEnabled
,
onChanged:
(
value
)
async
{
final
result
=
await
viewModel
.
toggleBiometric
(
value
);
setState
(()
{
viewModel
.
biometricEnabled
=
result
;
});
},
activeColor:
Colors
.
white
,
activeTrackColor:
Colors
.
green
,
inactiveThumbColor:
Colors
.
white
,
inactiveTrackColor:
Colors
.
grey
.
shade400
,
),
onTap:
()
{},
),
_buildDivider
(),
...
...
@@ -59,7 +84,11 @@ class SettingScreen extends StatelessWidget {
_buildSettingItem
(
icon:
Icons
.
delete_outline
,
title:
'Xóa tài khoản'
,
onTap:
()
{},
onTap:
()
{
BottomSheetHelper
.
showBottomSheetPopup
(
child:
const
DeleteAccountDialog
(),
);
},
textColor:
Colors
.
red
,
iconColor:
Colors
.
red
,
showTrailing:
false
,
...
...
@@ -77,6 +106,7 @@ class SettingScreen extends StatelessWidget {
required
IconData
icon
,
required
String
title
,
required
VoidCallback
onTap
,
Widget
?
trailing
,
Color
?
textColor
,
Color
?
iconColor
,
bool
showTrailing
=
true
,
...
...
@@ -91,7 +121,7 @@ class SettingScreen extends StatelessWidget {
fontWeight:
FontWeight
.
w500
,
),
),
trailing:
showTrailing
?
const
Icon
(
Icons
.
chevron_right
,
color:
Colors
.
black26
)
:
null
,
trailing:
trailing
??
(
showTrailing
?
const
Icon
(
Icons
.
chevron_right
,
color:
Colors
.
black26
)
:
null
)
,
onTap:
onTap
,
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
),
);
...
...
lib/screen/setting/setting_viewmodel.dart
0 → 100644
View file @
5fb93f2d
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'../../base/restful_api_viewmodel.dart'
;
import
'../../permission/biometric_manager.dart'
;
import
'../../preference/data_preference.dart'
;
class
SettingViewModel
extends
RestfulApiViewModel
{
bool
biometricEnabled
=
false
;
Future
<
bool
>
loadBiometricStatus
()
async
{
final
phone
=
DataPreference
.
instance
.
phone
;
if
(
phone
!=
null
)
{
final
token
=
await
DataPreference
.
instance
.
getBioToken
(
phone
)
??
""
;
biometricEnabled
=
token
.
isNotEmpty
;
}
else
{
biometricEnabled
=
false
;
}
return
biometricEnabled
;
}
Future
<
bool
>
toggleBiometric
(
bool
enable
)
async
{
final
phone
=
DataPreference
.
instance
.
phone
;
if
(
phone
==
null
)
return
biometricEnabled
;
final
canCheckBiometrics
=
BiometricManager
().
canCheckBiometrics
();
if
(!(
await
canCheckBiometrics
))
{
return
biometricEnabled
;
}
final
didAuthenticate
=
BiometricManager
().
authenticateBiometric
();
if
(!(
await
didAuthenticate
))
return
biometricEnabled
;
showLoading
();
if
(
enable
)
{
client
.
registerBiometric
().
then
((
value
)
{
final
token
=
value
.
data
?.
bioToken
??
""
;
DataPreference
.
instance
.
saveBioToken
(
token
);
hideLoading
();
return
true
;
});
}
else
{
client
.
unRegisterBiometric
().
then
((
value
)
{
DataPreference
.
instance
.
clearBioToken
(
phone
!);
hideLoading
();
return
false
;
});
}
return
enable
;
}
}
lib/screen/splash/splash_screen.dart
View file @
5fb93f2d
...
...
@@ -4,21 +4,25 @@ import 'package:flutter/services.dart';
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/configs/api_paths.dart'
;
import
'package:mypoint_flutter_app/dio_http_service/api_helper.dart'
;
import
'package:mypoint_flutter_app/networking/api_service.dart'
;
import
'package:mypoint_flutter_app/screen/splash/splash_screen_viewmodel.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'package:mypoint_flutter_app/widgets/alert/custom_alert_dialog.dart'
;
import
'../../base/base_screen.dart'
;
import
'../../base/basic_state.dart'
;
import
'../../model/check_update_response_model.dart'
;
import
'../../resouce/base_color.dart'
;
import
'../../widgets/alert/data_alert_model.dart'
;
import
'../onboarding/onboarding_screen.dart'
;
class
SplashScreen
extends
StatefulWidget
{
class
SplashScreen
extends
BaseScreen
{
const
SplashScreen
({
super
.
key
});
@override
State
<
SplashScreen
>
createState
()
=>
_SplashScreenState
();
}
class
_SplashScreenState
extends
State
<
SplashScreen
>
with
ApiHelper
{
class
_SplashScreenState
extends
Base
State
<
SplashScreen
>
with
BasicState
,
ApiHelper
{
final
SplashScreenViewModel
_viewModel
=
Get
.
put
(
SplashScreenViewModel
());
@override
...
...
@@ -26,10 +30,26 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
super
.
initState
();
initNetWork
(
APIPaths
.
baseUrl
);
_viewModel
.
checkUpdateApp
();
_viewModel
.
infoAppUpdate
.
listen
((
response
)
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
final
list
=
response
.
data
?.
updateRequest
;
if
(
list
==
null
||
list
.
isEmpty
)
{
_viewModel
.
getUserProfile
();
return
;
}
var
result
=
response
?.
data
?.
updateRequest
?.
first
;
var
status
=
result
?.
status
??
UpdateStatus
.
none
;
if
(
result
==
null
&&
status
==
UpdateStatus
.
none
)
{
_navigateToBeforCheckUpdate
();
}
else
{
_showSuggestUpdateAlert
(
result
!);
}
});
});
}
@override
Widget
build
(
BuildContext
context
)
{
Widget
createBody
(
)
{
return
Scaffold
(
backgroundColor:
Colors
.
blue
,
body:
Stack
(
...
...
@@ -46,16 +66,6 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
if
(
_viewModel
.
isLoading
.
value
)
{
return
Center
(
child:
CircularProgressIndicator
());
}
else
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
var
status
=
_viewModel
.
infoAppUpdate
.
value
?.
data
?.
data
?.
updateRequest
?.
first
?.
status
??
UpdateStatus
.
none
;
if
(
status
==
UpdateStatus
.
force
)
{
_showForceUpdateAlert
();
}
else
if
(
status
==
UpdateStatus
.
suggest
)
{
_showSuggestUpdateAlert
();
}
else
{
_navigateToBeforCheckUpdate
();
}
});
return
Container
(
width:
double
.
infinity
,
height:
double
.
infinity
,
color:
Colors
.
black
.
withOpacity
(
0.5
));
}
}),
...
...
@@ -73,75 +83,45 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
}
void
_navigateToBeforCheckUpdate
()
{
Get
.
to
(
O
nboardingScreen
()
);
Get
.
to
Named
(
o
nboardingScreen
);
}
void
_showForceUpdateAlert
()
{
showDialog
(
context:
context
,
barrierDismissible:
false
,
// Không cho dismiss ngoài Alert
builder:
(
context
)
{
return
AlertDialog
(
title:
Text
(
"Cập nhật bắt buộc"
),
content:
Text
(
"Phiên bản app của bạn đã cũ. Bạn phải cập nhật để tiếp tục sử dụng."
),
actions:
[
TextButton
(
onPressed:
()
async
{
// Sau khi nhấn update, bạn có thể đóng app nếu không cập nhật được.
_exitApp
();
},
child:
Text
(
"Cập nhật ngay"
),
),
TextButton
(
void
_showSuggestUpdateAlert
(
CheckUpdateResponseModel
data
)
{
final
buttons
=
data
.
status
==
UpdateStatus
.
force
?
[
AlertButton
(
text:
"Cập nhật ngay"
,
onPressed:
()
{
// Nếu người dùng không cập nhật, đóng app.
_exitApp
();
},
child:
Text
(
"Thoát"
),
),
],
);
_viewModel
.
openLink
();
},
);
}
void
_showSuggestUpdateAlert
()
{
showDialog
(
context:
context
,
barrierDismissible:
false
,
// Buộc người dùng chọn
builder:
(
context
)
{
return
AlertDialog
(
title:
Text
(
"Gợi ý cập nhật"
),
content:
Text
(
"Có phiên bản mới của app. Bạn có muốn cập nhật không?"
),
actions:
[
TextButton
(
onPressed:
()
async
{},
child:
Text
(
"Cập nhật ngay"
)),
TextButton
(
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
isPrimary:
true
,
),]
:
[
AlertButton
(
text:
"Cập nhật"
,
onPressed:
()
{
// Cho phép sử dụng app mà không cập nhật ngay
_navigateToBeforCheckUpdate
();
_viewModel
.
openLink
();
},
child:
Text
(
"Để sau"
),
bgColor:
BaseColor
.
primary500
,
textColor:
Colors
.
white
,
isPrimary:
true
,
),
],
);
AlertButton
(
text:
"Để sau"
,
onPressed:
()
{
Get
.
back
();
_viewModel
.
getUserProfile
();
},
bgColor:
Colors
.
white
,
textColor:
BaseColor
.
primary500
,
isPrimary:
false
,
),];
final
model
=
DataAlertModel
(
background:
"assets/images/ic_pipi_03.png"
,
title:
data
.
updateTitle
??
"Cập nhật phiên bản mới"
,
content:
data
.
updateMessage
??
"Cập nhật phiên bản mới"
,
buttons:
buttons
,
);
}
Future
<
void
>
fetchCheckUpdate
()
async
{
final
response
=
await
ApiService
().
checkUpdateWithRequestManager
();
if
(
response
!=
null
)
{
if
(
response
.
status
==
UpdateStatus
.
force
)
{
_showForceUpdateAlert
();
}
else
if
(
response
.
status
==
UpdateStatus
.
suggest
)
{
_showSuggestUpdateAlert
();
}
else
{
_showForceUpdateAlert
();
// _navigateToHome();
}
}
else
{
_showSuggestUpdateAlert
();
// _navigateToHome();
}
showAlert
(
data:
model
,
showCloseButton:
false
,
direction:
ButtonsDirection
.
row
);
}
}
lib/screen/splash/splash_screen_viewmodel.dart
View file @
5fb93f2d
import
'package:get/get.dart'
;
import
'package:mypoint_flutter_app/base/restful_api_viewmodel.dart'
;
import
'package:mypoint_flutter_app/networking/restful_api_request.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
import
'../../base/base_response_model.dart'
;
import
'../../model/update_response_object.dart'
;
import
'../../configs/constants.dart'
;
import
'../../model/update_response_model.dart'
;
import
'../../preference/data_preference.dart'
;
import
'../main_tab_screen/main_tab_screen.dart'
;
import
'../onboarding/onboarding_screen.dart'
;
import
'package:url_launcher/url_launcher.dart'
;
class
SplashScreenViewModel
extends
RestfulApiViewModel
{
var
infoAppUpdate
=
BaseResponseModel
<
UpdateResponse
Object
>().
obs
;
var
infoAppUpdate
=
BaseResponseModel
<
UpdateResponse
Model
>().
obs
;
var
isLoading
=
false
.
obs
;
void
checkUpdateApp
()
{
...
...
@@ -17,6 +23,34 @@ class SplashScreenViewModel extends RestfulApiViewModel {
isLoading
(
false
);
});
}
Future
<
void
>
openLink
()
async
{
final
updateLink
=
infoAppUpdate
.
value
.
data
?.
updateRequest
?.
first
?.
updateLink
??
""
;
if
(
updateLink
.
isEmpty
)
return
;
final
Uri
url
=
Uri
.
parse
(
updateLink
);
if
(
await
canLaunchUrl
(
url
))
{
await
launchUrl
(
url
);
}
}
Future
<
void
>
getUserProfile
()
async
{
if
(!(
await
DataPreference
.
instance
.
logged
))
{
Get
.
toNamed
(
onboardingScreen
);
return
;
}
showLoading
();
client
.
getUserProfile
().
then
((
value
)
async
{
hideLoading
();
final
userProfile
=
value
.
data
;
if
(
value
.
isSuccess
&&
userProfile
!=
null
)
{
await
DataPreference
.
instance
.
saveUserProfile
(
userProfile
);
Get
.
toNamed
(
mainScreen
);
}
else
{
DataPreference
.
instance
.
clearLoginToken
();
Get
.
toNamed
(
onboardingScreen
);
}
});
}
}
class
EmptyCodable
{
...
...
lib/shared/router_gage.dart
0 → 100644
View file @
5fb93f2d
import
'package:get/get_navigation/src/routes/get_route.dart'
;
import
'../screen/login/login_screen.dart'
;
import
'../screen/main_tab_screen/main_tab_screen.dart'
;
import
'../screen/onboarding/onboarding_screen.dart'
;
import
'../screen/setting/setting_screen.dart'
;
import
'../screen/splash/splash_screen.dart'
;
const
splashScreen
=
'/splash'
;
const
onboardingScreen
=
'/onboarding'
;
const
loginScreen
=
'/login'
;
const
mainScreen
=
'/main'
;
const
settingScreen
=
'/setting'
;
class
RouterPage
{
static
List
<
GetPage
>
pages
()
{
List
<
GetPage
>
list
=
[];
list
.
addAll
(
_pages
());
return
list
;
}
static
List
<
GetPage
>
_pages
()
{
return
[
GetPage
(
name:
splashScreen
,
page:
()
=>
const
SplashScreen
()),
GetPage
(
name:
onboardingScreen
,
page:
()
=>
const
OnboardingScreen
()),
GetPage
(
name:
loginScreen
,
page:
()
=>
const
LoginScreen
()),
GetPage
(
name:
mainScreen
,
page:
()
=>
const
MainTabScreen
()),
GetPage
(
name:
settingScreen
,
page:
()
=>
const
SettingScreen
()),
];
}
}
lib/widgets/alert/custom_alert_dialog.dart
View file @
5fb93f2d
...
...
@@ -8,8 +8,9 @@ enum ButtonsDirection { row, column }
class
CustomAlertDialog
extends
StatelessWidget
{
final
DataAlertModel
alertData
;
final
ButtonsDirection
direction
;
final
bool
showCloseButton
;
const
CustomAlertDialog
({
super
.
key
,
required
this
.
alertData
,
this
.
direction
=
ButtonsDirection
.
column
});
const
CustomAlertDialog
({
super
.
key
,
required
this
.
alertData
,
this
.
direction
=
ButtonsDirection
.
column
,
this
.
showCloseButton
=
true
,
});
@override
Widget
build
(
BuildContext
context
)
{
...
...
@@ -65,6 +66,7 @@ class CustomAlertDialog extends StatelessWidget {
),
),
// Close Button (X) ở góc phải trên
if
(
showCloseButton
)
Positioned
(
top:
0
,
right:
0
,
...
...
lib/widgets/back_button.dart
View file @
5fb93f2d
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
import
'package:get/get_core/src/get_main.dart'
;
import
'package:mypoint_flutter_app/preference/data_preference.dart'
;
import
'package:mypoint_flutter_app/resouce/base_color.dart'
;
import
'package:mypoint_flutter_app/shared/router_gage.dart'
;
class
CustomBackButton
extends
StatelessWidget
{
final
VoidCallback
?
onPressed
;
...
...
@@ -33,7 +37,12 @@ class CustomBackButton extends StatelessWidget {
onPressed:
onPressed
??
()
{
if
(
Navigator
.
canPop
(
context
))
{
Navigator
.
pop
(
context
);
}
else
{
DataPreference
.
instance
.
clearData
();
Get
.
offAllNamed
(
onboardingScreen
);
}
},
),
),
...
...
lib/widgets/bottom_sheet_helper.dart
0 → 100644
View file @
5fb93f2d
// bottom_sheet_helper.dart
import
'package:flutter/material.dart'
;
import
'package:get/get.dart'
;
class
BottomSheetHelper
{
static
void
showBottomSheetPopup
({
required
Widget
child
,
bool
isDismissible
=
true
,
})
{
showModalBottomSheet
(
context:
Get
.
context
!,
isScrollControlled:
true
,
isDismissible:
isDismissible
,
backgroundColor:
Colors
.
transparent
,
barrierColor:
Colors
.
black
.
withOpacity
(
0.5
),
shape:
const
RoundedRectangleBorder
(
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
),
builder:
(
context
)
{
return
Padding
(
padding:
MediaQuery
.
of
(
context
).
viewInsets
.
add
(
const
EdgeInsets
.
only
(
bottom:
0
),
// 👈 Safe area bottom
),
child:
Wrap
(
children:
[
Container
(
decoration:
const
BoxDecoration
(
color:
Colors
.
white
,
borderRadius:
BorderRadius
.
vertical
(
top:
Radius
.
circular
(
16
)),
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
16
),
child:
child
,
),
SizedBox
(
height:
32
,),
],
),
);
},
);
}
}
\ No newline at end of file
lib/widgets/custom_toast_message.dart
0 → 100644
View file @
5fb93f2d
import
'package:fluttertoast/fluttertoast.dart'
;
import
'package:flutter/material.dart'
;
void
showToastMessage
(
String
message
,
{
ToastGravity
gravity
=
ToastGravity
.
BOTTOM
,
Color
backgroundColor
=
Colors
.
black87
,
Color
textColor
=
Colors
.
white
,
int
timeInSec
=
2
,
})
{
Fluttertoast
.
showToast
(
msg:
message
,
gravity:
gravity
,
backgroundColor:
backgroundColor
,
textColor:
textColor
,
toastLength:
Toast
.
LENGTH_SHORT
,
timeInSecForIosWeb:
timeInSec
,
fontSize:
14
,
);
}
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