Commit 5fb93f2d authored by DatHV's avatar DatHV
Browse files

update logic authen

parent 73074efa
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_request.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/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/base_response_model.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../base/restful_api_viewmodel.dart';
import '../login/login_screen.dart';
import '../splash/splash_screen_viewmodel.dart'; import '../splash/splash_screen_viewmodel.dart';
class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICreatePasswordRepository { class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICreatePasswordRepository {
...@@ -19,7 +19,7 @@ class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICrea ...@@ -19,7 +19,7 @@ class ResetCreatePasswordRepository extends RestfulApiViewModel implements ICrea
hideLoading(); hideLoading();
if (value.status == "success" || value.code == 200) { if (value.status == "success" || value.code == 200) {
print("Reset password success"); print("Reset password success");
Get.off(() => LoginScreen(phoneNumber: phoneNumber)); Get.offNamed(loginScreen, arguments: phoneNumber);
} }
return value; return value;
}); });
......
...@@ -2,11 +2,11 @@ import 'dart:async'; ...@@ -2,11 +2,11 @@ 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/networking/restful_api_request.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/base_response_model.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../base/restful_api_viewmodel.dart';
import '../../permission/biometric_manager.dart'; import '../../permission/biometric_manager.dart';
import '../../preference/data_preference.dart'; import '../../preference/data_preference.dart';
import '../../shared/router_gage.dart';
import '../biometric/biometric_screen.dart'; import '../biometric/biometric_screen.dart';
import '../main_tab_screen/main_tab_screen.dart'; import '../main_tab_screen/main_tab_screen.dart';
import '../splash/splash_screen_viewmodel.dart'; import '../splash/splash_screen_viewmodel.dart';
...@@ -43,7 +43,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre ...@@ -43,7 +43,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
await DataPreference.instance.saveLoginToken(response.data!); await DataPreference.instance.saveLoginToken(response.data!);
_getUserProfile(); _getUserProfile();
} else { } else {
Get.off(() => LoginScreen(phoneNumber: phoneNumber)); Get.offNamed(loginScreen, arguments: phoneNumber);
} }
}); });
} }
...@@ -62,7 +62,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre ...@@ -62,7 +62,7 @@ class SignUpCreatePasswordRepository extends RestfulApiViewModel implements ICre
} }
} else { } else {
DataPreference.instance.clearLoginToken(); DataPreference.instance.clearLoginToken();
Get.off(() => LoginScreen(phoneNumber: phoneNumber)); Get.offNamed(loginScreen, arguments: phoneNumber);
} }
}); });
} }
......
// 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));
}
}
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.");
}
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
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/preference/data_preference.dart'; import 'package:mypoint_flutter_app/preference/data_preference.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../setting/setting_screen.dart'; import '../setting/setting_screen.dart';
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
...@@ -13,7 +13,7 @@ class HomeScreen extends StatelessWidget { ...@@ -13,7 +13,7 @@ class HomeScreen extends StatelessWidget {
return Scaffold( return Scaffold(
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, // ✅ căn giữa dọc mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')), ElevatedButton(onPressed: () => _logout(context), child: const Text('Đăng xuất')),
...@@ -40,11 +40,30 @@ class HomeScreen extends StatelessWidget { ...@@ -40,11 +40,30 @@ class HomeScreen extends StatelessWidget {
if (confirm == true) { if (confirm == true) {
DataPreference.instance.clearLoginToken(); 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 { void _showSetting(BuildContext context) async {
Get.to(() => const SettingScreen()); Get.toNamed(settingScreen);
} }
} }
\ No newline at end of file
...@@ -13,9 +13,7 @@ import '../../widgets/support_button.dart'; ...@@ -13,9 +13,7 @@ import '../../widgets/support_button.dart';
import 'login_viewmodel.dart'; import 'login_viewmodel.dart';
class LoginScreen extends BaseScreen { class LoginScreen extends BaseScreen {
final String phoneNumber; const LoginScreen({super.key});
final String? fullName;
const LoginScreen({super.key, required this.phoneNumber, this.fullName});
@override @override
State<LoginScreen> createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
...@@ -26,9 +24,19 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -26,9 +24,19 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
final loginVM = Get.put(LoginViewModel()); final loginVM = Get.put(LoginViewModel());
late final String phoneNumber;
late String fullName = "";
@override @override
void initState() { void initState() {
super.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) { loginVM.onShowChangePass = (message) {
Get.dialog( Get.dialog(
CustomAlertDialog( CustomAlertDialog(
...@@ -67,7 +75,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -67,7 +75,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
AlertButton( AlertButton(
text: "Quên mật khẩu", text: "Quên mật khẩu",
onPressed: () { onPressed: () {
loginVM.onForgotPassPressed(widget.phoneNumber); loginVM.onForgotPassPressed(phoneNumber);
}, },
bgColor: BaseColor.primary500, bgColor: BaseColor.primary500,
textColor: Colors.white, textColor: Colors.white,
...@@ -160,9 +168,9 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -160,9 +168,9 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
style: const TextStyle(fontSize: 14, color: BaseColor.second500), style: const TextStyle(fontSize: 14, color: BaseColor.second500),
children: [ children: [
const TextSpan(text: "Xin chào "), const TextSpan(text: "Xin chào "),
TextSpan(text: widget.fullName ?? "Quý Khách "), TextSpan(text: fullName.isEmpty ? "Quý Khách " : "$fullName "),
TextSpan( TextSpan(
text: widget.phoneNumber, text: phoneNumber,
style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500), style: const TextStyle(fontWeight: FontWeight.w500, color: BaseColor.primary500),
), ),
], ],
...@@ -231,7 +239,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -231,7 +239,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
vm.onForgotPassPressed(widget.phoneNumber); vm.onForgotPassPressed(phoneNumber);
}, },
child: const Text("Quên mật khẩu?", style: TextStyle(fontSize: 14, color: Color(0xFF3662FE))), 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 { ...@@ -256,7 +264,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
children: [ children: [
IconButton( IconButton(
icon: Icon(icon, size: 36), icon: Icon(icon, size: 36),
onPressed: () => vm.onBiometricLoginPressed(widget.phoneNumber), onPressed: () => vm.onBiometricLoginPressed(phoneNumber),
), ),
Text("Đăng nhập bằng $label"), Text("Đăng nhập bằng $label"),
], ],
...@@ -295,7 +303,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState { ...@@ -295,7 +303,7 @@ class _LoginScreenState extends BaseState<LoginScreen> with BasicState {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
), ),
onPressed: () { onPressed: () {
enabled ? vm.onLoginPressed(widget.phoneNumber) : null; enabled ? vm.onLoginPressed(phoneNumber) : null;
}, },
child: const Text( child: const Text(
"Đăng nhập", "Đăng nhập",
......
...@@ -129,12 +129,12 @@ class LoginViewModel extends RestfulApiViewModel { ...@@ -129,12 +129,12 @@ class LoginViewModel extends RestfulApiViewModel {
onShowAlertError?.call("Thiết bị không hỗ trợ sinh trắc học"); onShowAlertError?.call("Thiết bị không hỗ trợ sinh trắc học");
return; return;
} }
final bioToken = await DataPreference.instance.getBioToken(phone); final bioToken = await DataPreference.instance.getBioToken(phone) ?? "";
if (bioToken == null) { 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!\nVui lòng đăng nhập > cài đặt để kích hoạt tính năng"); onShowAlertError?.call("Tài khoản này chưa kích hoạt đăng nhập bằng sinh trắc học!\nVui lòng đăng nhập > cài đặt để kích hoạt tính năng");
return; return;
} }
client.login(phone, password.value).then((value) async { client.loginWithBiometric(phone).then((value) async {
hideLoading(); hideLoading();
_handleLoginResponse(value, phone); _handleLoginResponse(value, phone);
}); });
......
...@@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; ...@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/base_response_model.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/base_screen.dart';
import '../../base/basic_state.dart'; import '../../base/basic_state.dart';
import '../../configs/constants.dart'; import '../../configs/constants.dart';
...@@ -25,7 +26,7 @@ class OnboardingScreen extends BaseScreen { ...@@ -25,7 +26,7 @@ class OnboardingScreen extends BaseScreen {
} }
class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState { class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState {
final OnboardingViewModel _viewModel = Get.find<OnboardingViewModel>(); final OnboardingViewModel _viewModel = Get.put(OnboardingViewModel());
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
@override @override
...@@ -72,7 +73,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState ...@@ -72,7 +73,7 @@ class _OnboardingScreenState extends BaseState<OnboardingScreen> with BasicState
return; return;
} }
if (response.nextAction == "login") { if (response.nextAction == "login") {
Get.to(() => LoginScreen(phoneNumber: _viewModel.phoneNumber.value)); Get.toNamed(loginScreen, arguments: _viewModel.phoneNumber.value);
} }
} }
......
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;
// });
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
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/networking/restful_api_request.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/base_response_model.dart';
import '../../base/restful_api_viewmodel.dart'; import '../../base/restful_api_viewmodel.dart';
import '../create_pass/create_pass_screen.dart'; import '../create_pass/create_pass_screen.dart';
...@@ -30,7 +31,7 @@ class VerifyOtpRepository extends RestfulApiViewModel implements IOtpRepository ...@@ -30,7 +31,7 @@ class VerifyOtpRepository extends RestfulApiViewModel implements IOtpRepository
if (value.data?.claim?.action == "signup") { if (value.data?.claim?.action == "signup") {
Get.off(() => CreatePasswordScreen(repository: SignUpCreatePasswordRepository(phoneNumber))); Get.off(() => CreatePasswordScreen(repository: SignUpCreatePasswordRepository(phoneNumber)));
} else if (value.data?.claim?.action == "login") { } else if (value.data?.claim?.action == "login") {
Get.off(() => LoginScreen(phoneNumber: phoneNumber)); Get.offNamed(loginScreen, arguments: phoneNumber);
} }
return value; return value;
}); });
......
...@@ -2,12 +2,32 @@ ...@@ -2,12 +2,32 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_core/src/get_main.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 '../../widgets/custom_app_bar.dart';
import '../change_pass/change_pass_screen.dart'; import '../change_pass/change_pass_screen.dart';
import '../delete_account/delete_account_dialog.dart';
class SettingScreen extends StatelessWidget { class SettingScreen extends StatefulWidget {
const SettingScreen({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
...@@ -17,17 +37,9 @@ class SettingScreen extends StatelessWidget { ...@@ -17,17 +37,9 @@ class SettingScreen extends StatelessWidget {
children: [ children: [
Container( Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 16), margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// boxShadow: [
// BoxShadow(
// color: Colors.black12,
// blurRadius: 8,
// offset: const Offset(0, 4),
// ),
// ],
), ),
child: Column( child: Column(
children: [ children: [
...@@ -47,6 +59,19 @@ class SettingScreen extends StatelessWidget { ...@@ -47,6 +59,19 @@ class SettingScreen extends StatelessWidget {
icon: Icons.fingerprint, icon: Icons.fingerprint,
title: 'Xác thực sinh trắc học', title: 'Xác thực sinh trắc học',
showTrailing: false, 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: () {}, onTap: () {},
), ),
_buildDivider(), _buildDivider(),
...@@ -59,7 +84,11 @@ class SettingScreen extends StatelessWidget { ...@@ -59,7 +84,11 @@ class SettingScreen extends StatelessWidget {
_buildSettingItem( _buildSettingItem(
icon: Icons.delete_outline, icon: Icons.delete_outline,
title: 'Xóa tài khoản', title: 'Xóa tài khoản',
onTap: () {}, onTap: () {
BottomSheetHelper.showBottomSheetPopup(
child: const DeleteAccountDialog(),
);
},
textColor: Colors.red, textColor: Colors.red,
iconColor: Colors.red, iconColor: Colors.red,
showTrailing: false, showTrailing: false,
...@@ -77,6 +106,7 @@ class SettingScreen extends StatelessWidget { ...@@ -77,6 +106,7 @@ class SettingScreen extends StatelessWidget {
required IconData icon, required IconData icon,
required String title, required String title,
required VoidCallback onTap, required VoidCallback onTap,
Widget? trailing,
Color? textColor, Color? textColor,
Color? iconColor, Color? iconColor,
bool showTrailing = true, bool showTrailing = true,
...@@ -91,7 +121,7 @@ class SettingScreen extends StatelessWidget { ...@@ -91,7 +121,7 @@ class SettingScreen extends StatelessWidget {
fontWeight: FontWeight.w500, 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, onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
); );
......
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;
}
}
...@@ -4,21 +4,25 @@ import 'package:flutter/services.dart'; ...@@ -4,21 +4,25 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/configs/api_paths.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/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/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 '../../model/check_update_response_model.dart';
import '../../resouce/base_color.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../onboarding/onboarding_screen.dart'; import '../onboarding/onboarding_screen.dart';
class SplashScreen extends StatefulWidget { class SplashScreen extends BaseScreen {
const SplashScreen({super.key}); const SplashScreen({super.key});
@override @override
State<SplashScreen> createState() => _SplashScreenState(); State<SplashScreen> createState() => _SplashScreenState();
} }
class _SplashScreenState extends State<SplashScreen> with ApiHelper { class _SplashScreenState extends BaseState<SplashScreen> with BasicState, ApiHelper {
final SplashScreenViewModel _viewModel = Get.put(SplashScreenViewModel()); final SplashScreenViewModel _viewModel = Get.put(SplashScreenViewModel());
@override @override
...@@ -26,10 +30,26 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper { ...@@ -26,10 +30,26 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
super.initState(); super.initState();
initNetWork(APIPaths.baseUrl); initNetWork(APIPaths.baseUrl);
_viewModel.checkUpdateApp(); _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 @override
Widget build(BuildContext context) { Widget createBody() {
return Scaffold( return Scaffold(
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
body: Stack( body: Stack(
...@@ -46,16 +66,6 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper { ...@@ -46,16 +66,6 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
if (_viewModel.isLoading.value) { if (_viewModel.isLoading.value) {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} else { } 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)); 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 { ...@@ -73,75 +83,45 @@ class _SplashScreenState extends State<SplashScreen> with ApiHelper {
} }
void _navigateToBeforCheckUpdate() { void _navigateToBeforCheckUpdate() {
Get.to(OnboardingScreen()); Get.toNamed(onboardingScreen);
} }
void _showForceUpdateAlert() { void _showSuggestUpdateAlert(CheckUpdateResponseModel data) {
showDialog( final buttons = data.status == UpdateStatus.force
context: context, ? [AlertButton(
barrierDismissible: false, // Không cho dismiss ngoài Alert text: "Cập nhật ngay",
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(
onPressed: () { onPressed: () {
// Nếu người dùng không cập nhật, đóng app. _viewModel.openLink();
_exitApp();
},
child: Text("Thoát"),
),
],
);
}, },
); bgColor: BaseColor.primary500,
} textColor: Colors.white,
isPrimary: true,
void _showSuggestUpdateAlert() { ),]
showDialog( : [AlertButton(
context: context, text: "Cập nhật",
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(
onPressed: () { onPressed: () {
// Cho phép sử dụng app mà không cập nhật ngay _viewModel.openLink();
_navigateToBeforCheckUpdate();
}, },
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,
); );
} showAlert(data: model, showCloseButton: false, direction: ButtonsDirection.row);
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();
}
} }
} }
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:mypoint_flutter_app/base/restful_api_viewmodel.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/networking/restful_api_request.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import '../../base/base_response_model.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 { class SplashScreenViewModel extends RestfulApiViewModel {
var infoAppUpdate = BaseResponseModel<UpdateResponseObject>().obs; var infoAppUpdate = BaseResponseModel<UpdateResponseModel>().obs;
var isLoading = false.obs; var isLoading = false.obs;
void checkUpdateApp() { void checkUpdateApp() {
...@@ -17,6 +23,34 @@ class SplashScreenViewModel extends RestfulApiViewModel { ...@@ -17,6 +23,34 @@ class SplashScreenViewModel extends RestfulApiViewModel {
isLoading(false); 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 { class EmptyCodable {
......
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()),
];
}
}
...@@ -8,8 +8,9 @@ enum ButtonsDirection { row, column } ...@@ -8,8 +8,9 @@ enum ButtonsDirection { row, column }
class CustomAlertDialog extends StatelessWidget { class CustomAlertDialog extends StatelessWidget {
final DataAlertModel alertData; final DataAlertModel alertData;
final ButtonsDirection direction; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -65,6 +66,7 @@ class CustomAlertDialog extends StatelessWidget { ...@@ -65,6 +66,7 @@ class CustomAlertDialog extends StatelessWidget {
), ),
), ),
// Close Button (X) ở góc phải trên // Close Button (X) ở góc phải trên
if (showCloseButton)
Positioned( Positioned(
top: 0, top: 0,
right: 0, right: 0,
......
import 'package:flutter/material.dart'; 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/resouce/base_color.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
class CustomBackButton extends StatelessWidget { class CustomBackButton extends StatelessWidget {
final VoidCallback? onPressed; final VoidCallback? onPressed;
...@@ -33,7 +37,12 @@ class CustomBackButton extends StatelessWidget { ...@@ -33,7 +37,12 @@ class CustomBackButton extends StatelessWidget {
onPressed: onPressed:
onPressed ?? onPressed ??
() { () {
if (Navigator.canPop(context)) {
Navigator.pop(context); Navigator.pop(context);
} else {
DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
}, },
), ),
), ),
......
// 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
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,
);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment