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

cập nhật logic, refactor code

parent 6c72edcb
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;
} }
onDestroy(); onDispose();
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? barrierDismissible,
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? barrierDismissible,
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());
} }
} }
...@@ -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) {
......
...@@ -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";
......
...@@ -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(voucherDetailScreen, arguments: {"countDownSecond": countDownSecond}); Get.toNamed(vouchersScreen, arguments: {"countDownSecond": countDownSecond});
return true; return true;
case DirectionalScreenName.popViewController: case DirectionalScreenName.popViewController:
if (Get.isOverlaysOpen) { if (Get.isOverlaysOpen) {
......
...@@ -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);
} }
} }
......
...@@ -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
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);
}
} }
...@@ -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,
......
...@@ -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;
......
...@@ -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.validatePhoneNumber() && (_viewModel.selectedProduct.value != null); final isValidInput = _viewModel.phoneNumber.value.isPhoneValid() && (_viewModel.selectedProduct.value != null);
return ElevatedButton( return ElevatedButton(
onPressed: isValidInput ? _redeemProductMobileCard : null, onPressed: isValidInput ? _redeemProductMobileCard : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
......
...@@ -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) {
......
...@@ -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}");
......
...@@ -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),
......
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);
}
} }
} }
...@@ -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);
} }
} }
...@@ -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 {
......
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.validatePhoneNumber() ? () { onPressed: _viewModel.phoneNumber.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},
......
...@@ -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,
......
...@@ -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');
}); });
} }
......
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() {
......
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