Commit a0bcdab2 authored by DatHV's avatar DatHV
Browse files

refactor. update logic,

parent 9f4cb968
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
## Tổng quan ## Tổng quan
Tài liệu này mô tả cách tích hợp mini app với `x-app-sdk` để lấy token và đóng app từ Super App. Implementation này đơn giản và chỉ sử dụng 2 API chính: `getToken()` `closeApp()`. Tài liệu này mô tả cách tích hợp mini app với `x-app-sdk` và cách Flutter web wrapper (`XAppSDKService`) expose đầy đủ API như `getToken()`, `closeApp()`, `configUIApp()` cùng các tiện ích khác từ Super App.
## Cài đặt ## Cài đặt
...@@ -45,14 +45,70 @@ webCloseApp({ ...@@ -45,14 +45,70 @@ webCloseApp({
### Web Helper Functions ### Web Helper Functions
**Init & Diagnostics**
- `webInitializeXAppSDK()`: Khởi tạo x-app-sdk service - `webInitializeXAppSDK()`: Khởi tạo x-app-sdk service
- `webGetToken()`: Lấy token từ Super App
- `webCloseApp(data)`: Đóng app và trả về Super App với data
- `webIsSDKInitialized()`: Kiểm tra SDK đã khởi tạo chưa - `webIsSDKInitialized()`: Kiểm tra SDK đã khởi tạo chưa
- `webGetToken()`: Lấy token từ Super App
- `webGetCachedToken()`: Lấy token đã cache - `webGetCachedToken()`: Lấy token đã cache
- `webGetLastError()`: Lấy error message cuối cùng - `webGetLastError()`: Lấy error message cuối cùng
- `webClearTokenCache()`: Xóa token cache - `webClearTokenCache()`: Xóa token cache
- `webResetSDK()`: Reset SDK service - `webResetSDK()`: Reset SDK service
- `webCloseApp(data) -> bool`: Đóng app, trả về `true` nếu host xử lý thành công; `false` khi chạy browser mode (dùng fallback điều hướng tại Flutter)
**Config**
- `webConfigUIApp(config)`: Thiết lập UI trong Super App
```dart
import 'package:flutter/foundation.dart';
import 'package:mypoint_flutter_app/web/web_helper.dart';
Future<void> configureHeader() async {
if (!webIsSDKInitialized()) {
await webInitializeXAppSDK();
}
final response = await webConfigUIApp({
'headerTitle': 'Tên ứng dụng',
'headerColor': '#ffffff',
'headerTextColor': '#000000',
});
if (response != null) {
debugPrint('Cấu hình thành công: $response');
} else {
debugPrint('Cấu hình thất bại: ${webGetLastError()}');
}
}
```
**Device**
- `webCallPhone(phone)` / `webCall(phone)`: Gọi điện
- `webSendSms(phone)` / `webSms(phone)`: Mở app SMS
- `webVibrate()`: Rung thiết bị
**Location**
- `webCurrentLocation()`: Lấy vị trí hiện tại
- `webRequestLocationPermission()`: Xin quyền vị trí
**Media**
- `webOpenPickerImage(type)`: Mở image picker
- `webOpenPickerFile([options])`: Mở file picker
**Notification & Payment**
- `webListenNotificationEvent(onEvent)`: Lắng nghe notification
- `webPaymentRequest(payload)`: Gửi yêu cầu thanh toán
- `webListenPaymentEvent(onEvent)`: Lắng nghe sự kiện thanh toán
**Permission**
- `webPermissionsRequest(type)` / `webPremissionsRequest(type)`: Xin quyền theo SDK
**Store**
- `webSaveStore(data)`: Lưu dữ liệu
- `webGetStore()`: Lấy dữ liệu
- `webClearStore()`: Xóa dữ liệu
**User**
- `webGetInfo(key)`: Lấy thông tin user
### XAppSDKService ### XAppSDKService
...@@ -66,12 +122,21 @@ await service.initialize(); ...@@ -66,12 +122,21 @@ await service.initialize();
String? token = await service.getToken(); String? token = await service.getToken();
// Đóng app // Đóng app
await service.closeApp({'message': 'Done'}); final closed = await service.closeApp({'message': 'Done'});
if (!closed) {
// Không có Super App host => fallback
Navigator.of(context).pushReplacementNamed(onboardingRoute);
}
// Kiểm tra trạng thái // Kiểm tra trạng thái
bool isReady = service.isInitialized; bool isReady = service.isInitialized;
String? cachedToken = service.cachedToken; String? cachedToken = service.cachedToken;
String? error = service.lastError; String? error = service.lastError;
// Ví dụ lắng nghe notification
final removeNotification = await service.listenNotificationEvent((event) {
debugPrint('Notification event: $event');
});
``` ```
## Luồng hoạt động ## Luồng hoạt động
......
...@@ -4,4 +4,3 @@ ...@@ -4,4 +4,3 @@
"t3Token":"runner-env-flavor-dev", "t3Token":"runner-env-flavor-dev",
"enableLogging":true "enableLogging":true
} }
...@@ -64,6 +64,7 @@ class AppLoading { ...@@ -64,6 +64,7 @@ class AppLoading {
double size = 56, double size = 56,
double strokeWidth = 4, double strokeWidth = 4,
}) { }) {
print('AppLoading.show called');
// Đưa thao tác vào hàng đợi, không làm ngay // Đưa thao tác vào hàng đợi, không làm ngay
_ops.add(() { _ops.add(() {
if (isShowing) { if (isShowing) {
...@@ -108,6 +109,7 @@ class AppLoading { ...@@ -108,6 +109,7 @@ class AppLoading {
} }
void hide() { void hide() {
print('AppLoading.hide called');
_ops.add(() { _ops.add(() {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
......
...@@ -35,20 +35,26 @@ class AppNavigator { ...@@ -35,20 +35,26 @@ class AppNavigator {
buttons: [ buttons: [
AlertButton( AlertButton(
text: "Đã hiểu", text: "Đã hiểu",
onPressed: () { onPressed: () async {
_authDialogShown = false; _authDialogShown = false;
// if (kIsWeb) { final fallbackMessage = message.isNotEmpty ? message : description;
// webCloseApp({ if (kIsWeb) {
// 'message': message.isNotEmpty ? message : description, await DataPreference.instance.clearData();
// 'timestamp': DateTime.now().millisecondsSinceEpoch, final closed = await webCloseApp({
// }); 'message': fallbackMessage,
// return; 'timestamp': DateTime.now().millisecondsSinceEpoch,
// } });
if (!closed) {
Get.offAllNamed(onboardingScreen);
}
return;
}
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen; final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
if (phone.isNotEmpty) { if (phone.isNotEmpty) {
await DataPreference.instance.clearLoginToken();
Get.offAllNamed(loginScreen, arguments: {'phone': phone}); Get.offAllNamed(loginScreen, arguments: {'phone': phone});
} else { } else {
DataPreference.instance.clearData(); await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen); Get.offAllNamed(onboardingScreen);
} }
}, },
...@@ -114,7 +120,14 @@ class AppNavigator { ...@@ -114,7 +120,14 @@ class AppNavigator {
VoidCallback? onConfirmed, VoidCallback? onConfirmed,
}) { }) {
print("Show alert error: $_errorDialogShown"); print("Show alert error: $_errorDialogShown");
if (_errorDialogShown || _ctx == null) return; if (_errorDialogShown) return;
final context = _ctx ?? Get.context ?? Get.overlayContext;
if (context == null) {
if (kDebugMode) {
print('⚠️ AppNavigator: Unable to show alert, no context available');
}
return;
}
_errorDialogShown = true; _errorDialogShown = true;
Get.dialog( Get.dialog(
CustomAlertDialog( CustomAlertDialog(
......
...@@ -41,6 +41,7 @@ class AppInitializer { ...@@ -41,6 +41,7 @@ class AppInitializer {
try { try {
// Initialize x-app-sdk // Initialize x-app-sdk
await webInitializeXAppSDK(); await webInitializeXAppSDK();
await _configureWebSdkHeader();
print('✅ Web features initialized successfully'); print('✅ Web features initialized successfully');
} catch (e) { } catch (e) {
print('❌ Error initializing web features: $e'); print('❌ Error initializing web features: $e');
...@@ -70,6 +71,21 @@ class AppInitializer { ...@@ -70,6 +71,21 @@ class AppInitializer {
} }
} }
static Future<void> _configureWebSdkHeader() async {
try {
final response = await webConfigUIApp({
'headerTitle': 'MyPoint',
'headerColor': '#E71D28',
'headerTextColor': '#ffffff',
});
if (response != null && kDebugMode) {
print('🧭 x-app-sdk header configured: $response');
}
} catch (error) {
print('❌ Failed to configure x-app-sdk header: $error');
}
}
/// Setup post-initialization callbacks /// Setup post-initialization callbacks
static void setupPostInitCallbacks() { static void setupPostInitCallbacks() {
try { try {
...@@ -98,4 +114,4 @@ class AppInitializer { ...@@ -98,4 +114,4 @@ class AppInitializer {
}); });
} catch (_) {} } catch (_) {}
} }
} }
\ No newline at end of file
import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'dart:io';
import '../env_loader.dart'; import '../env_loader.dart';
import 'interceptor/auth_interceptor.dart'; import 'interceptor/auth_interceptor.dart';
import 'interceptor/exception_interceptor.dart'; import 'interceptor/exception_interceptor.dart';
...@@ -34,14 +31,6 @@ class DioHttpService { ...@@ -34,14 +31,6 @@ class DioHttpService {
..interceptors.addAll(kReleaseMode ? const [] : [LoggerInterceptor()]) ..interceptors.addAll(kReleaseMode ? const [] : [LoggerInterceptor()])
..interceptors.add(AuthInterceptor()) ..interceptors.add(AuthInterceptor())
..interceptors.add(ExceptionInterceptor()); ..interceptors.add(ExceptionInterceptor());
// ..interceptors.add(
// InterceptorsWrapper(
// onError: (e, h) {
// if (e.response != null) return h.resolve(e.response!);
// h.next(e);
// },
// ),
// );
void setDefaultHeaders(Map<String, dynamic> headers) { void setDefaultHeaders(Map<String, dynamic> headers) {
dio.options.headers.addAll(headers); dio.options.headers.addAll(headers);
......
...@@ -27,7 +27,7 @@ class AuthInterceptor extends Interceptor { ...@@ -27,7 +27,7 @@ class AuthInterceptor extends Interceptor {
if (_isTokenInvalid(data)) { if (_isTokenInvalid(data)) {
response.requestOptions.extra[_kAuthHandledKey] = true; response.requestOptions.extra[_kAuthHandledKey] = true;
// Kiểm tra xem path này có cần skip refresh token không // Kiểm tra xem path này có cần skip refresh token không
if (_shouldSkipRefreshToken(response.requestOptions.path) || response.requestOptions.method == 'GET') { if (_shouldSkipRefreshToken(response.requestOptions.path)) {
handler.reject( handler.reject(
DioException( DioException(
requestOptions: response.requestOptions requestOptions: response.requestOptions
......
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:universal_html/html.dart' as html;
class NetworkConnectivity { class NetworkConnectivity {
NetworkConnectivity._(); NetworkConnectivity._();
...@@ -12,6 +14,14 @@ class NetworkConnectivity { ...@@ -12,6 +14,14 @@ class NetworkConnectivity {
String host = 'one.one.one.one', String host = 'one.one.one.one',
Duration timeout = const Duration(seconds: 2), Duration timeout = const Duration(seconds: 2),
}) async { }) async {
if (kIsWeb) {
try {
final online = html.window.navigator.onLine;
return online ?? true;
} catch (_) {
return true;
}
}
try { try {
final res = await InternetAddress.lookup(host).timeout(timeout); final res = await InternetAddress.lookup(host).timeout(timeout);
return res.isNotEmpty && res.first.rawAddress.isNotEmpty; return res.isNotEmpty && res.first.rawAddress.isNotEmpty;
......
...@@ -59,6 +59,10 @@ class RestfulAPIClient { ...@@ -59,6 +59,10 @@ class RestfulAPIClient {
_debug('DioException: $e'); _debug('DioException: $e');
final status = e.response?.statusCode; final status = e.response?.statusCode;
final map = _asJson(e.response?.data); final map = _asJson(e.response?.data);
final errorCode = map['error_code']?.toString() ?? map['errorCode']?.toString() ?? e.error?.toString();
if (errorCode != null && ErrorCodes.tokenInvalidCodes.contains(errorCode)) {
rethrow;
}
final msg = _extractMessage(map, status) ?? e.message ?? Constants.commonError; final msg = _extractMessage(map, status) ?? e.message ?? Constants.commonError;
return BaseResponseModel<T>(status: "fail", message: msg, data: null, code: status); return BaseResponseModel<T>(status: "fail", message: msg, data: null, code: status);
} catch (e) { } catch (e) {
...@@ -118,4 +122,4 @@ class RestfulAPIClient { ...@@ -118,4 +122,4 @@ class RestfulAPIClient {
print('=== API DEBUG === $e'); print('=== API DEBUG === $e');
} }
} }
} }
\ No newline at end of file
...@@ -169,7 +169,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient { ...@@ -169,7 +169,7 @@ extension RestfulAPIClientAllRequest on RestfulAPIClient {
"device_key": deviceKey, "device_key": deviceKey,
"lang": "vi", "lang": "vi",
}; };
return requestNormal(APIPaths.login, Method.POST, body, (data) => EmptyCodable.fromJson(data as Json)); return requestNormal(APIPaths.logout, Method.POST, body, (data) => EmptyCodable.fromJson(data as Json));
} }
Future<BaseResponseModel<LoginTokenResponseModel>> loginWithBiometric(String phone) async { Future<BaseResponseModel<LoginTokenResponseModel>> loginWithBiometric(String phone) async {
......
...@@ -38,12 +38,10 @@ class RestfulApiViewModel extends BaseViewModel { ...@@ -38,12 +38,10 @@ class RestfulApiViewModel extends BaseViewModel {
} else { } else {
final msg = res.errorMessage ?? defaultError; final msg = res.errorMessage ?? defaultError;
final hasInternet = await NetworkConnectivity().hasInternet(); final hasInternet = await NetworkConnectivity().hasInternet();
if (hasInternet) { if (showAppNavigatorDialog) {
if (showAppNavigatorDialog) { AppNavigator.showAlertError(content: hasInternet ? msg : ErrorCodes.networkError);
AppNavigator.showAlertError(content: msg); } else {
} else { await onFailure?.call(hasInternet ? msg : ErrorCodes.networkError, res, null);
await onFailure?.call(msg, res, null);
}
} }
} }
} catch (e) { } catch (e) {
...@@ -55,12 +53,10 @@ class RestfulApiViewModel extends BaseViewModel { ...@@ -55,12 +53,10 @@ class RestfulApiViewModel extends BaseViewModel {
msg = ErrorMapper.map(e); msg = ErrorMapper.map(e);
} }
final hasInternet = await NetworkConnectivity().hasInternet(); final hasInternet = await NetworkConnectivity().hasInternet();
if (hasInternet) { if (showAppNavigatorDialog) {
if (showAppNavigatorDialog) { AppNavigator.showAlertError(content: hasInternet ? msg : ErrorCodes.networkError);
AppNavigator.showAlertError(content: msg); } else {
} else { await onFailure?.call(hasInternet ? msg : ErrorCodes.networkError, res, null);
await onFailure?.call(msg, res, null);
}
} }
} finally { } finally {
if (withLoading) hideLoading(); if (withLoading) hideLoading();
...@@ -82,4 +78,4 @@ class RestfulApiViewModel extends BaseViewModel { ...@@ -82,4 +78,4 @@ class RestfulApiViewModel extends BaseViewModel {
onComplete?.call(); onComplete?.call();
} }
} }
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart'; ...@@ -4,6 +4,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../model/auth/login_token_response_model.dart'; import '../model/auth/login_token_response_model.dart';
import '../model/auth/profile_response_model.dart'; import '../model/auth/profile_response_model.dart';
import '../screen/popup_manager/popup_manager_viewmodel.dart'; import '../screen/popup_manager/popup_manager_viewmodel.dart';
import '../web/web_helper.dart';
class DataPreference { class DataPreference {
static final DataPreference _instance = DataPreference._internal(); static final DataPreference _instance = DataPreference._internal();
...@@ -89,6 +90,7 @@ class DataPreference { ...@@ -89,6 +90,7 @@ class DataPreference {
} }
Future<void> clearData() async { Future<void> clearData() async {
await webClearStore();
await clearLoginToken(); await clearLoginToken();
await clearUserProfile(); await clearUserProfile();
} }
...@@ -128,4 +130,4 @@ class DataPreference { ...@@ -128,4 +130,4 @@ class DataPreference {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('biometric_login_token_$phone'); await prefs.remove('biometric_login_token_$phone');
} }
} }
\ No newline at end of file
...@@ -42,6 +42,7 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS ...@@ -42,6 +42,7 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS
showAffiliateBrandPopup(context, data.$1, title: data.$2); showAffiliateBrandPopup(context, data.$1, title: data.$2);
}; };
runPopupCheck(DirectionalScreenName.pointBack); runPopupCheck(DirectionalScreenName.pointBack);
viewModel.refreshData();
} }
@override @override
...@@ -66,11 +67,10 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS ...@@ -66,11 +67,10 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS
], ],
), ),
body: Obx(() { body: Obx(() {
if (viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator( return RefreshIndicator(
onRefresh: viewModel.refreshData, onRefresh: () async {
await viewModel.refreshData(isShowLoading: false);
},
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 100), padding: const EdgeInsets.fromLTRB(16, 12, 16, 100),
child: Column( child: Column(
...@@ -116,15 +116,15 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS ...@@ -116,15 +116,15 @@ class _AffiliateTabScreenState extends BaseState<AffiliateTabScreen> with BasicS
], ],
), ),
), ),
AffiliateBrand(brands: viewModel.affiliateBrands), AffiliateBrand(brands: viewModel.affiliateBrands.value),
AffiliateCategory(categories: viewModel.affiliateCategories, onTap: (category) { AffiliateCategory(categories: viewModel.affiliateCategories.value, onTap: (category) {
if (category.code == AffiliateCategoryType.other) { if (category.code == AffiliateCategoryType.other) {
Get.toNamed(affiliateCategoryGridScreen, arguments: {"categories": viewModel.allAffiliateCategories}); Get.toNamed(affiliateCategoryGridScreen, arguments: {"categories": viewModel.allAffiliateCategories});
return; return;
} }
viewModel.affiliateBrandGetListBuyCategory(category); viewModel.affiliateBrandGetListBuyCategory(category);
},), },),
AffiliateProductTopSale(products: viewModel.affiliateProducts), AffiliateProductTopSale(products: viewModel.affiliateProducts.value),
], ],
), ),
), ),
......
...@@ -16,21 +16,22 @@ class AffiliateTabViewModel extends RestfulApiViewModel { ...@@ -16,21 +16,22 @@ class AffiliateTabViewModel extends RestfulApiViewModel {
final Rxn<CashbackOverviewModel> overview = Rxn<CashbackOverviewModel>(); final Rxn<CashbackOverviewModel> overview = Rxn<CashbackOverviewModel>();
void Function((List<AffiliateBrandModel>, String) data)? onShowAffiliateBrandPopup; void Function((List<AffiliateBrandModel>, String) data)? onShowAffiliateBrandPopup;
@override bool get isDataAvailable {
void onInit() { return affiliateBrands.isNotEmpty ||
super.onInit(); affiliateCategories.isNotEmpty ||
refreshData(); affiliateProducts.isNotEmpty;
_getAffiliateOverview();
} }
Future<void> refreshData() async { Future<void> refreshData({bool isShowLoading = true}) async {
isLoading.value = true; if (isShowLoading && isDataAvailable) return;
if (isShowLoading) showLoading();
await Future.wait([ await Future.wait([
_getAffiliateOverview(),
_getAffiliateBrandGetList(), _getAffiliateBrandGetList(),
_getAffiliateCategoryGetList(), _getAffiliateCategoryGetList(),
_getAffiliateProductTopSale(), _getAffiliateProductTopSale(),
]); ]);
isLoading.value = false; if (isShowLoading) hideLoading();
} }
Future<void> _getAffiliateBrandGetList() async { Future<void> _getAffiliateBrandGetList() async {
...@@ -42,6 +43,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel { ...@@ -42,6 +43,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel {
onFailure: (msg, _, __) async { onFailure: (msg, _, __) async {
affiliateBrands.clear(); affiliateBrands.clear();
}, },
withLoading: false,
); );
} }
...@@ -60,10 +62,11 @@ class AffiliateTabViewModel extends RestfulApiViewModel { ...@@ -60,10 +62,11 @@ class AffiliateTabViewModel extends RestfulApiViewModel {
limitedData.add(category); limitedData.add(category);
affiliateCategories.assignAll(limitedData); affiliateCategories.assignAll(limitedData);
}, },
onFailure: (msg, _, __) async { onFailure: (msg, _, _) async {
affiliateCategories.clear(); affiliateCategories.clear();
allAffiliateCategories.clear(); allAffiliateCategories.clear();
}, },
withLoading: false,
); );
} }
...@@ -76,6 +79,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel { ...@@ -76,6 +79,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel {
onFailure: (msg, _, __) async { onFailure: (msg, _, __) async {
affiliateProducts.clear(); affiliateProducts.clear();
}, },
withLoading: false,
); );
} }
...@@ -88,7 +92,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel { ...@@ -88,7 +92,7 @@ class AffiliateTabViewModel extends RestfulApiViewModel {
onFailure: (msg, _, __) async { onFailure: (msg, _, __) async {
overview.value = null; overview.value = null;
}, },
showAppNavigatorDialog: true, withLoading: false,
); );
} }
......
...@@ -167,9 +167,9 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState ...@@ -167,9 +167,9 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState
localHeaderImage: "assets/images/ic_pipi_03.png", localHeaderImage: "assets/images/ic_pipi_03.png",
buttons: [AlertButton( buttons: [AlertButton(
text: "Đồng ý", text: "Đồng ý",
onPressed: () { onPressed: () async {
DataPreference.instance.clearLoginToken(); await DataPreference.instance.clearLoginToken();
_safeBackToLogin(); await _safeBackToLogin();
}, },
bgColor: BaseColor.primary500, bgColor: BaseColor.primary500,
textColor: Colors.white, textColor: Colors.white,
...@@ -231,7 +231,7 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState ...@@ -231,7 +231,7 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState
}); });
} }
void _safeBackToLogin() { Future<void> _safeBackToLogin() async {
bool found = false; bool found = false;
Navigator.popUntil(Get.context!, (route) { Navigator.popUntil(Get.context!, (route) {
final matched = route.settings.name == loginScreen; final matched = route.settings.name == loginScreen;
...@@ -244,7 +244,7 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState ...@@ -244,7 +244,7 @@ class _ChangePassScreenState extends BaseState<ChangePassScreen> with BasicState
Get.offAllNamed(loginScreen, arguments: {'phone': phone}); Get.offAllNamed(loginScreen, arguments: {'phone': phone});
} }
} else { } else {
DataPreference.instance.clearData(); await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen); Get.offAllNamed(onboardingScreen);
} }
} }
......
...@@ -123,7 +123,11 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> { ...@@ -123,7 +123,11 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
width: double.infinity, width: double.infinity,
height: 48, height: 48,
child: ElevatedButton( child: ElevatedButton(
onPressed: _viewModel.agreed.value ? _onConfirmDelete : null, onPressed: _viewModel.agreed.value
? () async {
await _onConfirmDelete();
}
: null,
style: AppButtonStyle.secondary, style: AppButtonStyle.secondary,
child: const Text("Xác nhận xoá"), child: const Text("Xác nhận xoá"),
), ),
...@@ -143,11 +147,11 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> { ...@@ -143,11 +147,11 @@ class _DeleteAccountDialogState extends State<DeleteAccountDialog> {
); );
} }
void _onConfirmDelete() { Future<void> _onConfirmDelete() async {
if (DataPreference.instance.profile?.userAgreements?.hideDeleteAccount == false) { if (DataPreference.instance.profile?.userAgreements?.hideDeleteAccount == false) {
_viewModel.confirmDelete(); await _viewModel.confirmDelete();
} else { } else {
DataPreference.instance.clearData(); await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen); Get.offAllNamed(onboardingScreen);
} }
} }
......
...@@ -67,46 +67,56 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu ...@@ -67,46 +67,56 @@ class _GameTabScreenState extends BaseState<GameTabScreen> with BasicState, Popu
], ],
), ),
body: Obx(() { body: Obx(() {
if (_viewModel.games.isEmpty) { final games = _viewModel.games;
return const Center(child: EmptyWidget()); return RefreshIndicator(
} onRefresh: _viewModel.getGames,
return Column( child: ListView.separated(
crossAxisAlignment: CrossAxisAlignment.start, physics: const AlwaysScrollableScrollPhysics(),
children: [ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
Padding( itemCount: games.isEmpty ? 1 : games.length + 1,
padding: const EdgeInsets.all(16), separatorBuilder: (_, index) {
child: Text( if (games.isEmpty) return const SizedBox.shrink();
_viewModel.turnsNumberText.value, if (index == 0) {
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), return SizedBox(height: _viewModel.turnsNumberText.value.isEmpty ? 4 : 12);
), }
), return const SizedBox(height: 4);
Expanded( },
child: ListView.separated( itemBuilder: (context, index) {
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), if (games.isEmpty) {
itemCount: _viewModel.games.length, return SizedBox(
separatorBuilder: (_, __) => const SizedBox(height: 4), height: MediaQuery.of(context).size.height * 0.8,
itemBuilder: (context, index) { child: const Center(child: EmptyWidget()),
final item = _viewModel.games[index]; );
return GestureDetector( }
onTap: () { if (index == 0) {
_viewModel.getGameDetail(item.id ?? ""); final turnsText = _viewModel.turnsNumberText.value;
}, if (turnsText.isEmpty) {
child: AspectRatio( return const SizedBox.shrink();
aspectRatio: 343 / 132, }
child: ClipRRect( return Text(
borderRadius: BorderRadius.circular(16), turnsText,
child: loadNetworkImage( style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
url: item.icon ?? '', );
fit: BoxFit.fitWidth, }
placeholderAsset: 'assets/images/bg_default_169.png', final item = games[index - 1];
), return GestureDetector(
), onTap: () {
), _viewModel.getGameDetail(item.id ?? "");
);
}, },
), child: AspectRatio(
), aspectRatio: 343 / 132,
], child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: loadNetworkImage(
url: item.icon ?? '',
fit: BoxFit.fitWidth,
placeholderAsset: 'assets/images/bg_default_169.png',
),
),
),
);
},
),
); );
}), }),
); );
......
...@@ -3,7 +3,6 @@ import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.da ...@@ -3,7 +3,6 @@ import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.da
import 'package:mypoint_flutter_app/screen/game/models/game_bundle_item_model.dart'; import 'package:mypoint_flutter_app/screen/game/models/game_bundle_item_model.dart';
import '../../networking/restful_api_viewmodel.dart'; import '../../networking/restful_api_viewmodel.dart';
import '../../configs/constants.dart';
import 'models/game_bundle_response.dart'; import 'models/game_bundle_response.dart';
class GameTabViewModel extends RestfulApiViewModel { class GameTabViewModel extends RestfulApiViewModel {
...@@ -12,8 +11,8 @@ class GameTabViewModel extends RestfulApiViewModel { ...@@ -12,8 +11,8 @@ class GameTabViewModel extends RestfulApiViewModel {
void Function(String message)? onShowAlertError; void Function(String message)? onShowAlertError;
void Function(GameBundleItemModel data)? gotoGameDetail; void Function(GameBundleItemModel data)? gotoGameDetail;
void getGames() { Future<void> getGames() {
callApi<GameBundleResponse>( return callApi<GameBundleResponse>(
request: () => client.getGames(), request: () => client.getGames(),
onSuccess: (data, _) { onSuccess: (data, _) {
games.assignAll(data.games ?? []); games.assignAll(data.games ?? []);
...@@ -36,4 +35,4 @@ class GameTabViewModel extends RestfulApiViewModel { ...@@ -36,4 +35,4 @@ class GameTabViewModel extends RestfulApiViewModel {
}, },
); );
} }
} }
\ No newline at end of file
...@@ -121,3 +121,4 @@ class HealthBookItem extends StatelessWidget { ...@@ -121,3 +121,4 @@ class HealthBookItem extends StatelessWidget {
} }
...@@ -19,15 +19,12 @@ class HeaderHomeViewModel extends RestfulApiViewModel { ...@@ -19,15 +19,12 @@ class HeaderHomeViewModel extends RestfulApiViewModel {
greeting: 'Xin chào!', greeting: 'Xin chào!',
totalVoucher: 0, totalVoucher: 0,
totalPointActive: 0, totalPointActive: 0,
background: background: '',
'https://api.mypoint.com.vn/8854/gup2start/rest/photoReader/1.0.0/F31FF2E775D7BFC940156709FB79E883/1746430303',
); );
} }
Future<void> freshData() async { Future<void> freshData() async {
if (_headerHomeData.value == null) { await _getDynamicHeaderHome();
await _getDynamicHeaderHome();
}
await _getNotificationUnread(); await _getNotificationUnread();
} }
......
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