Commit a0bcdab2 authored by DatHV's avatar DatHV
Browse files

refactor. update logic,

parent 9f4cb968
......@@ -35,6 +35,7 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
@override
void initState() {
super.initState();
_headerHomeVM.freshData();
runPopupCheck(DirectionalScreenName.home);
}
......@@ -240,7 +241,7 @@ class _HomeScreenState extends State<HomeScreen> with PopupOnInit {
}
Future<void> _onRefresh() async {
await _viewModel.getSectionLayoutHome();
await _viewModel.getSectionLayoutHome(showLoading: false);
await _viewModel.loadDataPiPiHome();
await _headerHomeVM.freshData();
}
......
......@@ -40,7 +40,7 @@ class HomeTabViewModel extends RestfulApiViewModel {
return sectionLayouts.firstWhereOrNull((section) => section.headerSectionType == type);
}
Future<void> getSectionLayoutHome() async {
Future<void> getSectionLayoutHome({bool showLoading = true}) async {
await callApi<List<MainSectionConfigModel>>(
request: () => client.getSectionLayoutHome(),
onSuccess: (data, _) {
......@@ -57,6 +57,7 @@ class HomeTabViewModel extends RestfulApiViewModel {
await _processSection(section);
}
},
withLoading: showLoading,
);
}
......
......@@ -89,11 +89,11 @@ class LoginViewModel extends RestfulApiViewModel {
}
}
void onChangePhonePressed() {
Future<void> onChangePhonePressed() async {
if (Get.key.currentState?.canPop() == true) {
Get.back();
} else {
DataPreference.instance.clearData();
await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
}
......
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
......@@ -23,29 +22,34 @@ class DeleteAccountOtpRepository extends RestfulApiViewModel implements IOtpRepo
@override
Future<BaseResponseModel<EmptyCodable>> verifyOtp(String otpCode) async {
showLoading();
return client.verifyDeleteAccount(otpCode).then((value) {
hideLoading();
try {
final value = await client.verifyDeleteAccount(otpCode);
if (value.isSuccess) {
DataPreference.instance.clearBioToken(phoneNumber);
DataPreference.instance.clearData();
await DataPreference.instance.clearBioToken(phoneNumber);
await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
showToastMessage("Xóa tài khoản thành công");
}
return value;
});
} finally {
hideLoading();
}
}
@override
Future<int?> resendOtp() async {
showLoading();
client.requestOtpDeleteAccount().then((value) {
hideLoading();
try {
final value = await client.requestOtpDeleteAccount();
if (value.isSuccess) {
otpTtl = value.data?.resendAfterSecond ?? Constants.otpTtl;
} else {
return otpTtl;
}
final mgs = value.errorMessage ?? Constants.commonError;
Get.snackbar("Thông báo", mgs);
return null;
} finally {
hideLoading();
}
});
}
}
......@@ -12,6 +12,7 @@ import '../../preference/point/header_home_model.dart';
import '../../resources/base_color.dart';
import '../../shared/router_gage.dart';
import '../../services/logout_service.dart';
import 'package:mypoint_flutter_app/web/web_helper.dart';
import '../../widgets/alert/data_alert_model.dart';
import '../home/header_home_viewmodel.dart';
import '../popup_manager/popup_runner_helper.dart';
......@@ -31,7 +32,6 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
void initState() {
super.initState();
_loadAppInfo();
_headerHomeVM.freshData();
runPopupCheck(DirectionalScreenName.personal);
}
......@@ -317,9 +317,7 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
AlertButton(
text: "Đồng ý",
onPressed: () async {
LogoutService.logout();
DataPreference.instance.clearLoginToken();
_safeBackToLogin();
await _handleLogout();
},
bgColor: BaseColor.primary500,
textColor: Colors.white,
......@@ -330,14 +328,30 @@ class _PersonalScreenState extends BaseState<PersonalScreen> with BasicState, Po
showAlert(data: dataAlert);
}
void _safeBackToLogin() {
Future<void> _handleLogout() async {
final phone = DataPreference.instance.phoneNumberUsedForLoginScreen;
final displayName = DataPreference.instance.displayName;
await LogoutService.logout();
if (kIsWeb) {
await DataPreference.instance.clearData();
final closed = await webCloseApp({
'message': 'User logged out successfully',
'timestamp': DateTime.now().millisecondsSinceEpoch,
});
if (!closed) {
Get.offAllNamed(onboardingScreen);
}
return;
}
print("Safe back to login screen with phone: $phone, displayName: $displayName");
if (phone != null) {
if (phone.isNotEmpty) {
await DataPreference.instance.clearLoginToken();
Get.offAllNamed(loginScreen, arguments: {"phone": phone, 'fullName': displayName});
} else {
DataPreference.instance.clearData();
await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
}
......
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mypoint_flutter_app/screen/support/support_item_model.dart';
import 'package:mypoint_flutter_app/screen/support/support_screen_viewmodel.dart';
import 'package:mypoint_flutter_app/shared/router_gage.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../resources/base_color.dart';
import '../../widgets/back_button.dart';
import 'package:universal_html/js_util.dart' as js_util;
import '../../widgets/custom_navigation_bar.dart';
import '../faqs/faqs_screen.dart';
import '../pageDetail/campaign_detail_screen.dart';
import '../pageDetail/model/detail_page_rule_type.dart';
import '../../web/web_helper.dart';
class SupportScreen extends StatefulWidget {
const SupportScreen({super.key});
......@@ -30,11 +30,29 @@ class _SupportScreenState extends State<SupportScreen> {
}
break;
case SupportItemType.phone:
final Uri phoneUri = Uri(scheme: 'tel', path: value);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
final phone = value.trim();
if (phone.isEmpty) {
if (kDebugMode) {
print('⚠️ SupportScreen: phone number is empty');
}
break;
return;
}
if (kIsWeb) {
try {
final result = await webCallPhone(phone);
if (!_isSdkSuccess(result)) {
await _launchTelUrl(phone);
}
} catch (e) {
if (kDebugMode) {
print('❌ webCallPhone failed: $e');
}
await _launchTelUrl(phone);
}
} else {
await _launchTelUrl(phone);
}
return;
case SupportItemType.facebook:
final Uri fbUri = Uri.parse(value);
if (await canLaunchUrl(fbUri)) {
......@@ -43,10 +61,13 @@ class _SupportScreenState extends State<SupportScreen> {
break;
case SupportItemType.question:
Get.to(FAQScreen());
return;
case SupportItemType.termsOfUse:
Get.toNamed(campaignDetailScreen, arguments: {"type": DetailPageRuleType.termsOfUse});
return;
case SupportItemType.privacyPolicy:
Get.toNamed(campaignDetailScreen, arguments: {"type": DetailPageRuleType.privacyPolicy});
return;
}
}
......@@ -116,4 +137,28 @@ class _SupportScreenState extends State<SupportScreen> {
type == SupportItemType.termsOfUse ||
type == SupportItemType.privacyPolicy;
}
Future<void> _launchTelUrl(String phone) async {
final Uri phoneUri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
}
}
bool _isSdkSuccess(dynamic result) {
try {
if (result == null) return false;
if (result is Map) {
final status = result['status']?.toString().toLowerCase();
return status == 'success';
}
if (js_util.hasProperty(result, 'status')) {
final status = js_util.getProperty(result, 'status');
if (status is String) {
return status.toLowerCase() == 'success';
}
}
} catch (_) {}
return false;
}
}
......@@ -3,7 +3,6 @@ import 'package:get/get.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../../../base/base_response_model.dart';
import '../../../networking/restful_api_viewmodel.dart';
import '../../../widgets/alert/popup_data_model.dart';
import '../models/product_model.dart';
import '../models/product_type.dart';
......
......@@ -22,15 +22,17 @@ class VoucherTabScreen extends StatefulWidget {
}
class _VoucherTabScreenState extends State<VoucherTabScreen> with PopupOnInit {
final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel());
@override
void initState() {
super.initState();
runPopupCheck(DirectionalScreenName.productVoucher);
viewModel.refreshData();
}
@override
Widget build(BuildContext context) {
final VoucherTabViewModel viewModel = Get.put(VoucherTabViewModel());
return Scaffold(
appBar: CustomNavigationBar(
title: "Ưu đãi",
......@@ -45,11 +47,10 @@ class _VoucherTabScreenState extends State<VoucherTabScreen> with PopupOnInit {
],
),
body: Obx(() {
if (viewModel.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return RefreshIndicator(
onRefresh: viewModel.refreshData,
onRefresh: () async {
await viewModel.refreshData(isShowLoading: false);
},
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo is ScrollUpdateNotification &&
......@@ -76,7 +77,7 @@ class _VoucherTabScreenState extends State<VoucherTabScreen> with PopupOnInit {
VoucherItemGrid(items: viewModel.hotProducts),
const HeaderSectionTitle(title: 'Tất cả ưu đãi'),
VoucherItemList(items: viewModel.allProducts),
if (viewModel.isLoadMore.value)
if (viewModel.isLoadMore.value && viewModel.allProducts.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
......
......@@ -8,28 +8,25 @@ import 'models/product_model.dart';
class VoucherTabViewModel extends RestfulApiViewModel {
final RxList<ProductModel> hotProducts = <ProductModel>[].obs;
final RxList<ProductModel> allProducts = <ProductModel>[].obs;
final RxBool isLoading = false.obs;
final RxBool isLoadMore = false.obs;
bool get _isDataAvailable {
return hotProducts.isNotEmpty || allProducts.isNotEmpty;
}
int _currentPage = 0;
final int _pageSize = 20;
bool _hasMore = true;
bool get hasMore => _hasMore;
@override
void onInit() {
super.onInit();
refreshData();
}
Future<void> refreshData() async {
isLoading.value = true;
Future<void> refreshData({bool isShowLoading = true}) async {
if (isShowLoading && _isDataAvailable) return;
if (isShowLoading) showLoading();
await Future.wait([
getHotProducts(),
getAllProducts(reset: true),
]);
isLoading.value = false;
if (isShowLoading) hideLoading();
}
Future<void> getHotProducts() async {
......@@ -51,7 +48,6 @@ class VoucherTabViewModel extends RestfulApiViewModel {
if (reset) {
_currentPage = 0;
_hasMore = true;
allProducts.clear();
} else {
_currentPage = allProducts.length;
}
......@@ -71,7 +67,11 @@ class VoucherTabViewModel extends RestfulApiViewModel {
if (fetchedData.isEmpty || fetchedData.length < _pageSize) {
_hasMore = false;
}
if (reset) {
allProducts.value = fetchedData;
} else {
allProducts.addAll(fetchedData);
}
} catch (error) {
print("Error fetching all products: $error");
} finally {
......
import 'package:flutter/widgets.dart';
import 'package:mypoint_flutter_app/configs/constants.dart';
import 'package:mypoint_flutter_app/extensions/string_extension.dart';
import 'package:mypoint_flutter_app/networking/restful_api_client_all_request.dart';
import '../model/auth/login_token_response_model.dart';
......@@ -22,7 +21,7 @@ class TokenRefreshService extends RestfulApiViewModel {
final token = DataPreference.instance.token;
final refreshToken = DataPreference.instance.refreshToken;
if (token.orEmpty.isEmpty || refreshToken.orEmpty.isEmpty) {
_handleRefreshFailure();
await _handleRefreshFailure();
return;
}
_isRefreshing = true;
......@@ -34,11 +33,11 @@ class TokenRefreshService extends RestfulApiViewModel {
await _getUserProfileAfterRefresh();
_handleRefreshSuccess();
} else {
_handleRefreshFailure();
await _handleRefreshFailure();
}
} catch (e) {
print('Token refresh error: $e');
_handleRefreshFailure();
await _handleRefreshFailure();
} finally {
_isRefreshing = false;
}
......@@ -65,8 +64,8 @@ class TokenRefreshService extends RestfulApiViewModel {
}
/// Xử lý refresh thất bại - redirect về login
void _handleRefreshFailure() {
DataPreference.instance.clearData();
Future<void> _handleRefreshFailure() async {
await DataPreference.instance.clearData();
for (final callback in _callbacks) {
callback(false);
}
......
// Stub implementations for non-web platforms
import 'package:flutter/foundation.dart';
/// Initialize x-app-sdk service (no-op on non-web)
Future<void> webInitializeXAppSDK() async {
......@@ -11,8 +12,8 @@ Future<String?> webGetToken() async {
}
/// Close app and return to Super App (no-op on non-web)
Future<void> webCloseApp([Map<String, dynamic>? data]) async {
// no-op on non-web
Future<bool> webCloseApp([Map<String, dynamic>? data]) async {
return false;
}
/// Check if x-app-sdk is initialized (no-op on non-web)
......@@ -39,3 +40,83 @@ void webClearTokenCache() {
void webResetSDK() {
// no-op on non-web
}
Future<dynamic> webConfigUIApp(Map<String, dynamic> config) async {
return null;
}
Future<dynamic> webCallPhone(String phoneNumber) async {
return null;
}
Future<dynamic> webCall(String phoneNumber) async {
return null;
}
Future<dynamic> webSendSms(String phoneNumber) async {
return null;
}
Future<dynamic> webSms(String phoneNumber) async {
return null;
}
Future<dynamic> webVibrate() async {
return null;
}
Future<dynamic> webCurrentLocation() async {
return null;
}
Future<dynamic> webRequestLocationPermission() async {
return null;
}
Future<dynamic> webOpenPickerImage(dynamic type) async {
return null;
}
Future<dynamic> webOpenPickerFile([dynamic options]) async {
return null;
}
Future<dynamic> webPaymentRequest(Map<String, dynamic> payload) async {
return null;
}
Future<VoidCallback?> webListenNotificationEvent(
ValueChanged<dynamic> onEvent,
) async {
return null;
}
Future<VoidCallback?> webListenPaymentEvent(
ValueChanged<dynamic> onEvent,
) async {
return null;
}
Future<dynamic> webPermissionsRequest(dynamic type) async {
return null;
}
Future<dynamic> webPremissionsRequest(dynamic type) async {
return null;
}
Future<dynamic> webSaveStore(dynamic data) async {
return null;
}
Future<dynamic> webGetStore() async {
return null;
}
Future<dynamic> webClearStore() async {
return null;
}
Future<dynamic> webGetInfo(dynamic key) async {
return null;
}
// Web-specific implementations for x-app-sdk
// ignore: avoid_web_libraries_in_flutter
import 'dart:convert';
import 'package:universal_html/html.dart' as html;
import 'package:flutter/foundation.dart';
import 'x_app_sdk_service.dart';
/// Web-specific helper functions for x-app-sdk integration
......@@ -26,11 +24,12 @@ Future<String?> webGetToken() async {
}
/// Close app and return to Super App
Future<void> webCloseApp([Map<String, dynamic>? data]) async {
Future<bool> webCloseApp([Map<String, dynamic>? data]) async {
try {
await XAppSDKService().closeApp(data);
return await XAppSDKService().closeApp(data);
} catch (e) {
print('❌ Error closing app: $e');
return false;
}
}
......@@ -81,3 +80,158 @@ void webResetSDK() {
print('❌ Error resetting SDK: $e');
}
}
Future<dynamic> webConfigUIApp(Map<String, dynamic> config) async {
try {
return await XAppSDKService().configUIApp(config);
} catch (e) {
print('❌ Error configuring UI app: $e');
return null;
}
}
Future<dynamic> webCallPhone(String phoneNumber) async {
try {
return await XAppSDKService().callPhone(phoneNumber);
} catch (e) {
print('❌ Error calling phone: $e');
return null;
}
}
Future<dynamic> webCall(String phoneNumber) => webCallPhone(phoneNumber);
Future<dynamic> webSendSms(String phoneNumber) async {
try {
return await XAppSDKService().sendSms(phoneNumber);
} catch (e) {
print('❌ Error sending SMS: $e');
return null;
}
}
Future<dynamic> webSms(String phoneNumber) => webSendSms(phoneNumber);
Future<dynamic> webVibrate() async {
try {
return await XAppSDKService().vibrate();
} catch (e) {
print('❌ Error vibrating device: $e');
return null;
}
}
Future<dynamic> webCurrentLocation() async {
try {
return await XAppSDKService().currentLocation();
} catch (e) {
print('❌ Error getting current location: $e');
return null;
}
}
Future<dynamic> webRequestLocationPermission() async {
try {
return await XAppSDKService().requestLocationPermission();
} catch (e) {
print('❌ Error requesting location permission: $e');
return null;
}
}
Future<dynamic> webOpenPickerImage(dynamic type) async {
try {
return await XAppSDKService().openPickerImage(type);
} catch (e) {
print('❌ Error opening image picker: $e');
return null;
}
}
Future<dynamic> webOpenPickerFile([dynamic options]) async {
try {
return await XAppSDKService().openPickerFile(options);
} catch (e) {
print('❌ Error opening file picker: $e');
return null;
}
}
Future<dynamic> webPaymentRequest(Map<String, dynamic> payload) async {
try {
return await XAppSDKService().paymentRequest(payload);
} catch (e) {
print('❌ Error creating payment request: $e');
return null;
}
}
Future<VoidCallback?> webListenNotificationEvent(
ValueChanged<dynamic> onEvent,
) async {
try {
return await XAppSDKService().listenNotificationEvent(onEvent);
} catch (e) {
print('❌ Error registering notification listener: $e');
return null;
}
}
Future<VoidCallback?> webListenPaymentEvent(
ValueChanged<dynamic> onEvent,
) async {
try {
return await XAppSDKService().listenPaymentEvent(onEvent);
} catch (e) {
print('❌ Error registering payment listener: $e');
return null;
}
}
Future<dynamic> webPermissionsRequest(dynamic type) async {
try {
return await XAppSDKService().permissionsRequest(type);
} catch (e) {
print('❌ Error requesting permission: $e');
return null;
}
}
Future<dynamic> webPremissionsRequest(dynamic type) =>
webPermissionsRequest(type);
Future<dynamic> webSaveStore(dynamic data) async {
try {
return await XAppSDKService().saveStore(data);
} catch (e) {
print('❌ Error saving store data: $e');
return null;
}
}
Future<dynamic> webGetStore() async {
try {
return await XAppSDKService().getStore();
} catch (e) {
print('❌ Error getting store data: $e');
return null;
}
}
Future<dynamic> webClearStore() async {
try {
return await XAppSDKService().clearStore();
} catch (e) {
print('❌ Error clearing store: $e');
return null;
}
}
Future<dynamic> webGetInfo(dynamic key) async {
try {
return await XAppSDKService().getInfo(key);
} catch (e) {
print('❌ Error getting info: $e');
return null;
}
}
......@@ -3,7 +3,7 @@ import 'package:universal_html/html.dart' as html;
import 'package:universal_html/js_util.dart';
/// X-App-SDK Service for Flutter Web
/// Provides simple integration with x-app-sdk for token retrieval and app closing
/// Provides integration with x-app-sdk module and exposes supported APIs to Dart
class XAppSDKService {
static final XAppSDKService _instance = XAppSDKService._internal();
factory XAppSDKService() => _instance;
......@@ -13,6 +13,8 @@ class XAppSDKService {
dynamic _sdkModule;
String? _cachedToken;
String? _lastError;
final Set<dynamic> _listenerDisposers = <dynamic>{};
bool _browserMode = false;
/// Check if SDK is available and initialized
bool get isInitialized => _isInitialized;
......@@ -26,7 +28,10 @@ class XAppSDKService {
/// Initialize the SDK service
Future<void> initialize() async {
if (!kIsWeb) {
print('⚠️ XAppSDKService: Not running on web platform');
print('⚠️ XAppSDKService: initialize() called on non-web platform');
return;
}
if (_isInitialized && _sdkModule != null) {
return;
}
......@@ -40,8 +45,8 @@ class XAppSDKService {
}
_sdkModule = module;
_isInitialized = true;
_lastError = null;
print('✅ XAppSDKService: Initialized successfully');
} catch (e) {
_lastError = 'Failed to initialize SDK: $e';
print('❌ XAppSDKService: $_lastError');
......@@ -50,39 +55,16 @@ class XAppSDKService {
/// Get token from x-app-sdk
Future<String?> getToken() async {
if (!kIsWeb) {
print('⚠️ XAppSDKService: getToken() called on non-web platform');
if (!await _ensureSdkReady()) {
return null;
}
if (!_isInitialized) {
print('⚠️ XAppSDKService: SDK not initialized, attempting to initialize...');
await initialize();
if (!_isInitialized) {
_lastError = 'SDK initialization failed';
return null;
}
}
try {
print('🔍 XAppSDKService: Getting token...');
final sdk = await _loadSdkModule();
if (sdk == null) {
_lastError = 'x-app-sdk not available';
print('❌ XAppSDKService: $_lastError');
return null;
}
final hasGetToken = _hasProperty(sdk, 'getToken');
if (!hasGetToken) {
_lastError = 'getToken method not found in SDK';
print('❌ XAppSDKService: $_lastError');
return null;
}
// Execute getToken method from SDK
final result = await promiseToFuture(callMethod(sdk, 'getToken', []));
try {
final result = await _invokeSdkMethod(
'getToken',
ensureInitialized: false,
);
if (result != null) {
final status = getProperty(result, 'status')?.toString();
......@@ -94,19 +76,21 @@ class XAppSDKService {
final tokenString = data.toString();
_cachedToken = tokenString;
_lastError = null;
final preview = tokenString.length > 8 ? tokenString.substring(0, 8) : tokenString;
print('✅ XAppSDKService: Token retrieved successfully: $preview...');
final preview =
tokenString.length > 8 ? tokenString.substring(0, 8) : tokenString;
print(
'✅ XAppSDKService: Token retrieved successfully: $preview...');
return _cachedToken;
} else {
final details = errorMessage?.isNotEmpty == true ? ' - $errorMessage' : '';
final details =
errorMessage?.isNotEmpty == true ? ' - $errorMessage' : '';
_lastError = 'SDK returned status: $status$details';
print('❌ XAppSDKService: $_lastError');
}
} else {
_lastError = 'getToken returned null';
_lastError ??= 'getToken returned null';
print('❌ XAppSDKService: $_lastError');
}
} catch (e) {
_lastError = 'Error getting token: $e';
print('❌ XAppSDKService: $_lastError');
......@@ -116,61 +100,143 @@ class XAppSDKService {
}
/// Close app and return to Super App
Future<void> closeApp([Map<String, dynamic>? data]) async {
if (!kIsWeb) {
print('⚠️ XAppSDKService: closeApp() called on non-web platform');
return;
Future<bool> closeApp([Map<String, dynamic>? data]) async {
if (!await _ensureSdkReady()) {
print('❌ XAppSDKService: closeApp skipped - SDK not ready');
_fallbackClose();
return false;
}
if (!_isInitialized) {
print('⚠️ XAppSDKService: SDK not initialized, attempting to initialize...');
await initialize();
if (!_isInitialized) {
print('❌ XAppSDKService: Cannot close app - SDK not initialized');
return;
}
if (_browserMode) {
print('ℹ️ XAppSDKService: Running in browser mode, falling back from closeApp');
_fallbackClose();
return false;
}
try {
print('🔍 XAppSDKService: Closing app...');
final sdk = await _loadSdkModule();
if (sdk == null) {
print('❌ XAppSDKService: x-app-sdk not available for closeApp');
return;
}
final hasCloseApp = _hasProperty(sdk, 'closeApp');
if (!hasCloseApp) {
print('❌ XAppSDKService: closeApp method not found in SDK');
return;
}
// Execute closeApp method from SDK with optional data
final result = await _invokeSdkMethod(
'closeApp',
args: data != null ? <dynamic>[data] : const <dynamic>[],
expectPromise: false,
ensureInitialized: false,
);
final success = _lastError == null && _isSuccessResult(result);
if (success) {
if (data != null) {
callMethod(sdk, 'closeApp', [data]);
print('✅ XAppSDKService: App closed with data: $data');
} else {
callMethod(sdk, 'closeApp', []);
print('✅ XAppSDKService: App closed successfully');
}
} catch (e) {
print('❌ XAppSDKService: Error closing app: $e');
// Fallback: try to close window or go back
try {
if (html.window.history.length > 1) {
html.window.history.back();
} else {
html.window.close();
return true;
}
print('✅ XAppSDKService: Fallback close executed');
} catch (fallbackError) {
print('❌ XAppSDKService: Fallback close also failed: $fallbackError');
final reason = _lastError ?? 'closeApp returned non-success result';
print('❌ XAppSDKService: $reason');
_fallbackClose();
return false;
}
/// Config UI inside Super App
Future<dynamic> configUIApp(Map<String, dynamic> config) =>
_invokeAfterEnsure('configUIApp', args: <dynamic>[config]);
/// Trigger a phone call via Super App
Future<dynamic> callPhone(String phoneNumber) =>
_invokeAfterEnsure('call', args: <dynamic>[phoneNumber]);
/// Trigger sending SMS via Super App
Future<dynamic> sendSms(String phoneNumber) =>
_invokeAfterEnsure('sms', args: <dynamic>[phoneNumber]);
/// Vibrate the device
Future<dynamic> vibrate() => _invokeAfterEnsure('vibrate');
/// Get current device location
Future<dynamic> currentLocation() => _invokeAfterEnsure('currentLocation');
/// Request location permission
Future<dynamic> requestLocationPermission() =>
_invokeAfterEnsure('requestLocationPermission');
/// Open image picker
Future<dynamic> openPickerImage(dynamic type) =>
_invokeAfterEnsure('openPickerImage', args: <dynamic>[type]);
/// Open file picker
Future<dynamic> openPickerFile([dynamic options]) => options == null
? _invokeAfterEnsure('openPickerFile')
: _invokeAfterEnsure('openPickerFile', args: <dynamic>[options]);
/// Send payment request
Future<dynamic> paymentRequest(Map<String, dynamic> payload) =>
_invokeAfterEnsure('paymentRequest', args: <dynamic>[payload]);
/// Listen to notification event
Future<VoidCallback?> listenNotificationEvent(
ValueChanged<dynamic> onEvent,
) async {
final disposer = await _invokeAfterEnsure(
'listenNotifiactionEvent',
args: <dynamic>[
allowInterop((dynamic event) {
try {
onEvent(event);
} catch (error, stackTrace) {
print(
'❌ XAppSDKService: Error in notification listener: $error');
debugPrintStack(stackTrace: stackTrace);
}
}),
],
expectPromise: false,
);
return _registerListenerDisposer('listenNotifiactionEvent', disposer);
}
/// Listen to payment events
Future<VoidCallback?> listenPaymentEvent(
ValueChanged<dynamic> onEvent,
) async {
final disposer = await _invokeAfterEnsure(
'listenPaymentEvent',
args: <dynamic>[
allowInterop((dynamic event) {
try {
onEvent(event);
} catch (error, stackTrace) {
print('❌ XAppSDKService: Error in payment listener: $error');
debugPrintStack(stackTrace: stackTrace);
}
}),
],
expectPromise: false,
);
return _registerListenerDisposer('listenPaymentEvent', disposer);
}
/// Request permissions (SDK uses misspelled 'premissionsRequest')
Future<dynamic> permissionsRequest(dynamic type) =>
_invokeAfterEnsure('premissionsRequest', args: <dynamic>[type]);
/// Alias for the SDK method name
Future<dynamic> premissionsRequest(dynamic type) =>
permissionsRequest(type);
/// Persist data into Super App store
Future<dynamic> saveStore(dynamic data) =>
_invokeAfterEnsure('saveStore', args: <dynamic>[data]);
/// Retrieve data from Super App store
Future<dynamic> getStore() => _invokeAfterEnsure('getStore');
/// Clear Super App store data
Future<dynamic> clearStore() => _invokeAfterEnsure('clearStore');
/// Get user info
Future<dynamic> getInfo(dynamic key) =>
_invokeAfterEnsure('getInfo', args: <dynamic>[key]);
/// Clear cached token
void clearToken() {
_cachedToken = null;
......@@ -180,23 +246,218 @@ class XAppSDKService {
/// Reset service state
void reset() {
_disposeAllListeners();
_isInitialized = false;
_sdkModule = null;
_cachedToken = null;
_lastError = null;
_browserMode = false;
try {
final loader = getProperty(html.window, '__xAppSdkLoader');
if (loader != null) {
if (_hasProperty(loader, 'resetCachedModule')) {
callMethod(loader, 'resetCachedModule', []);
}
if (loader != null && _hasProperty(loader, 'resetCachedModule')) {
callMethod(loader, 'resetCachedModule', <dynamic>[]);
}
} catch (_) {}
print('🔄 XAppSDKService: Service reset');
}
Future<bool> _ensureSdkReady() async {
if (!kIsWeb) {
print('⚠️ XAppSDKService: SDK requested on non-web platform');
return false;
}
if (_isInitialized && _sdkModule != null) {
return true;
}
print(
'⚠️ XAppSDKService: SDK not initialized, attempting to initialize...');
await initialize();
return _isInitialized && _sdkModule != null;
}
Future<dynamic> _invokeAfterEnsure(
String methodName, {
List<dynamic> args = const <dynamic>[],
bool expectPromise = true,
}) async {
if (!await _ensureSdkReady()) {
return null;
}
return _invokeSdkMethod(
methodName,
args: args,
expectPromise: expectPromise,
ensureInitialized: false,
);
}
Future<dynamic> _invokeSdkMethod(
String methodName, {
List<dynamic> args = const <dynamic>[],
bool expectPromise = true,
bool ensureInitialized = true,
}) async {
if (ensureInitialized && !await _ensureSdkReady()) {
return null;
}
final sdk = await _loadSdkModule();
if (sdk == null) {
_lastError = 'x-app-sdk not available';
print('❌ XAppSDKService: $_lastError');
return null;
}
if (!_hasProperty(sdk, methodName)) {
_lastError = '$methodName method not found in SDK';
print('❌ XAppSDKService: $_lastError');
return null;
}
try {
final preparedArgs = args.isEmpty
? <dynamic>[]
: List<dynamic>.from(args.map(_prepareArgument));
final result = callMethod(sdk, methodName, preparedArgs);
if (expectPromise) {
try {
final awaited = await promiseToFuture(result);
_lastError = null;
return awaited;
} catch (_) {
// Not a promise, fall through and return raw value.
}
}
_lastError = null;
return result;
} catch (e) {
_lastError = 'Error invoking $methodName: $e';
print('❌ XAppSDKService: $_lastError');
return null;
}
}
dynamic _prepareArgument(dynamic value) {
if (value == null) {
return null;
}
if (value is Map || value is Iterable) {
try {
return jsify(value);
} catch (_) {
// Fall through and return value as-is if jsify fails.
}
}
return value;
}
VoidCallback? _registerListenerDisposer(
String methodName,
dynamic disposer,
) {
if (disposer == null) {
if (_lastError != null) {
print(
'❌ XAppSDKService: Failed to register $methodName listener - $_lastError');
} else {
print(
'⚠️ XAppSDKService: $methodName did not return a disposer function');
}
return null;
}
_listenerDisposers.add(disposer);
print('🔔 XAppSDKService: $methodName listener registered');
return () {
_invokeJsFunction(disposer);
_listenerDisposers.remove(disposer);
};
}
void _invokeJsFunction(dynamic fn) {
if (fn == null) {
return;
}
try {
final result = callMethod(fn, 'call', <dynamic>[null]);
try {
promiseToFuture(result);
} catch (_) {
// Ignore non-Promise results.
}
} catch (e) {
print('❌ XAppSDKService: Failed to invoke JS callback: $e');
}
}
void _disposeAllListeners() {
if (_listenerDisposers.isEmpty) {
return;
}
final disposers = List<dynamic>.from(_listenerDisposers);
for (final disposer in disposers) {
_invokeJsFunction(disposer);
}
_listenerDisposers.clear();
}
void _fallbackClose() {
try {
if (html.window.history.length > 1) {
html.window.history.back();
} else {
html.window.close();
}
print('✅ XAppSDKService: Fallback close executed');
} catch (fallbackError) {
print('❌ XAppSDKService: Fallback close failed: $fallbackError');
}
}
bool _isSuccessResult(dynamic result) {
if (result == null) {
return true;
}
if (result is bool) {
return result;
}
if (result is Map) {
final status = result['status']?.toString().toLowerCase();
if (status != null) {
return status == 'success';
}
if (result.containsKey('success')) {
final successValue = result['success'];
if (successValue is bool) {
return successValue;
}
return successValue?.toString().toLowerCase() == 'true';
}
}
try {
if (_hasProperty(result, 'status')) {
final status = getProperty(result, 'status');
if (status is String) {
return status.toLowerCase() == 'success';
}
}
if (_hasProperty(result, 'success')) {
final successValue = getProperty(result, 'success');
if (successValue is bool) {
return successValue;
}
return successValue?.toString().toLowerCase() == 'true';
}
} catch (_) {}
return true;
}
Future<dynamic> _loadSdkModule() async {
if (_sdkModule != null) {
_browserMode = _detectBrowserMode(_sdkModule);
return _sdkModule;
}
......@@ -216,7 +477,9 @@ class XAppSDKService {
}
try {
final module = await promiseToFuture(callMethod(loader, 'loadXAppSdkModule', []));
final module = await promiseToFuture(
callMethod(loader, 'loadXAppSdkModule', <dynamic>[]),
);
if (module == null) {
_lastError = 'x-app-sdk module resolved to null';
print('❌ XAppSDKService: $_lastError');
......@@ -228,15 +491,15 @@ class XAppSDKService {
print('🔗 XAppSDKService: Module loaded from $source');
}
final hasGetToken = _hasProperty(module, 'getToken');
final hasCloseApp = _hasProperty(module, 'closeApp');
if (!hasGetToken || !hasCloseApp) {
if (!_hasProperty(module, 'getToken') ||
!_hasProperty(module, 'closeApp')) {
_lastError = 'x-app-sdk module missing required exports';
print('❌ XAppSDKService: $_lastError');
return null;
}
_sdkModule = module;
_browserMode = _detectBrowserMode(module);
return _sdkModule;
} catch (e) {
_lastError = 'Failed to load x-app-sdk module: $e';
......@@ -253,4 +516,17 @@ class XAppSDKService {
return false;
}
}
bool _detectBrowserMode(dynamic module) {
try {
final fltSdk = getProperty(module, 'fltSDK');
if (fltSdk != null && _hasProperty(fltSdk, 'getBrowserMode')) {
final mode = callMethod(fltSdk, 'getBrowserMode', <dynamic>[]);
if (mode is bool) {
return mode;
}
}
} catch (_) {}
return false;
}
}
......@@ -37,11 +37,11 @@ class CustomBackButton extends StatelessWidget {
icon: Icon(Icons.arrow_back_ios_rounded, color: iconColor ?? BaseColor.second600, size: iconSize ?? 16),
onPressed:
onPressed ??
() {
() async {
if (Get.key.currentState?.canPop() == true) {
Get.back();
} else {
DataPreference.instance.clearData();
await DataPreference.instance.clearData();
Get.offAllNamed(onboardingScreen);
}
},
......
......@@ -8,18 +8,46 @@ const SDK_CANDIDATE_PATHS = CANDIDATE_RELATIVE_PATHS.map((relative) =>
new URL(relative, import.meta.url).href,
);
const REQUIRED_METHODS = ['getToken', 'closeApp'];
const OPTIONAL_METHODS = [
'configUIApp',
'call',
'sms',
'vibrate',
'currentLocation',
'requestLocationPermission',
'openPickerImage',
'openPickerFile',
'listenNotifiactionEvent',
'paymentRequest',
'listenPaymentEvent',
'premissionsRequest',
'saveStore',
'getStore',
'clearStore',
'getInfo',
];
const KNOWN_METHODS = [...REQUIRED_METHODS, ...OPTIONAL_METHODS];
let cachedModulePromise = null;
function ensureSdkShape(module, source) {
if (!module) {
throw new Error(`Module from ${source} is undefined`);
}
const requiredMethods = ['getToken', 'closeApp'];
for (const method of requiredMethods) {
for (const method of REQUIRED_METHODS) {
if (typeof module[method] !== 'function') {
throw new Error(`Module from ${source} is missing required export: ${method}`);
}
}
const missingOptional = OPTIONAL_METHODS.filter(
(method) => typeof module[method] !== 'function',
);
if (missingOptional.length) {
console.warn(
`⚠️ x-app-sdk module from ${source} missing optional exports: ${missingOptional.join(', ')}`,
);
}
}
async function importWithFallback() {
......@@ -72,22 +100,30 @@ export async function bootstrapXAppSdk() {
return module;
}
async function getToken() {
function createMethodProxy(methodName) {
return async (...args) => {
const module = await loadXAppSdkModule();
return module.getToken();
const target = module?.[methodName];
if (typeof target !== 'function') {
const available = Object.keys(module ?? {});
throw new Error(
`x-app-sdk method "${methodName}" is not available. Exported methods: ${available.join(', ')}`,
);
}
return target(...args);
};
}
async function closeApp(payload) {
const module = await loadXAppSdkModule();
return module.closeApp(payload);
}
const loaderApiMethods = KNOWN_METHODS.reduce((acc, methodName) => {
acc[methodName] = createMethodProxy(methodName);
return acc;
}, {});
const loaderApi = {
loadXAppSdkModule,
bootstrapXAppSdk,
resetCachedModule,
getToken,
closeApp,
...loaderApiMethods,
};
if (!globalThis.__xAppSdkLoader) {
......
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