import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/modules/wallet/recharge/mifa_pay_webview_page.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; class MiFaPayManager extends ChangeNotifier { static const String defaultApplicationId = '2048200000000000701'; static const String defaultPayCountryId = '2048200000000000401'; static const String defaultPayCountryName = 'Saudi Arabia'; static const String defaultPayCountryCode = 'SA'; bool _initialized = false; bool _isLoading = false; bool _isCreatingOrder = false; String _errorMessage = ''; String _applicationId = defaultApplicationId; List _countries = []; SCMiFaPayCountryRes? _selectedCountry; SCMiFaPayCommodityRes? _commodityRes; SCMiFaPayCommodityItemRes? _selectedCommodity; SCMiFaPayChannelRes? _selectedChannel; bool get initialized => _initialized; bool get isLoading => _isLoading; bool get isCreatingOrder => _isCreatingOrder; String get errorMessage => _errorMessage; String get applicationId => _applicationId; List get countries => _countries; SCMiFaPayCountryRes? get selectedCountry => _selectedCountry; SCMiFaPayCommodityRes? get commodityRes => _commodityRes; List get commodities => _commodityRes?.commodity ?? []; List get channels => _commodityRes?.channels ?? []; SCMiFaPayCommodityItemRes? get selectedCommodity => _selectedCommodity; SCMiFaPayChannelRes? get selectedChannel => _selectedChannel; bool get canCreateRecharge => _selectedCountry != null && _selectedCommodity != null && _selectedChannel != null && !_isLoading && !_isCreatingOrder; Future initialize({bool force = false}) async { if (_isLoading) { return; } if (_initialized && !force) { return; } _isLoading = true; _errorMessage = ''; notifyListeners(); try { await _loadCountries(); await _loadCommodity(); _initialized = true; } catch (error) { _errorMessage = _readErrorMessage(error); debugPrint('MiFaPay initialize failed: $error'); } finally { _isLoading = false; notifyListeners(); } } Future reload() async { await initialize(force: true); } void chooseCommodityById(String goodsId) { if (goodsId.isEmpty) { return; } for (final SCMiFaPayCommodityItemRes item in commodities) { if (item.id == goodsId) { _selectedCommodity = item; notifyListeners(); return; } } } void chooseChannelByCode(String channelCode) { if (channelCode.isEmpty) { return; } for (final SCMiFaPayChannelRes item in channels) { if (item.channelCode == channelCode) { _selectedChannel = item; notifyListeners(); return; } } } Future createRecharge(BuildContext context) async { final SCMiFaPayCountryRes? country = _selectedCountry; final SCMiFaPayCommodityItemRes? commodity = _selectedCommodity; final SCMiFaPayChannelRes? channel = _selectedChannel; final String userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; final NavigatorState navigator = Navigator.of(context); final String purchaseSuccessMessage = SCAppLocalizations.of(context)!.purchaseIsSuccessful; const String paymentPendingMessage = 'Payment confirmation in progress'; final Map payload = { "applicationId": _applicationId, "goodsId": commodity?.id ?? '', "payCountryId": country?.id ?? '', "userId": userId, "channelCode": channel?.channelCode ?? '', "newVersion": true, }; if (country?.id?.isNotEmpty != true || commodity?.id?.isNotEmpty != true || channel?.channelCode?.isNotEmpty != true) { SCTts.show('Please select MiFaPay amount and channel first'); return; } if (userId.isEmpty) { SCTts.show('User id is empty'); return; } if (_isCreatingOrder) { return; } final SocialChatUserProfileManager profileManager = Provider.of(context, listen: false); final double balanceBeforeRecharge = profileManager.myBalance; try { _isCreatingOrder = true; notifyListeners(); SCLoadingManager.show(context: context); _logRequest( method: 'POST', path: '/order/web/pay/recharge', body: payload, ); final SCMiFaPayRechargeRes res = await SCConfigRepositoryImp() .mifaPayRecharge( applicationId: _applicationId, goodsId: commodity!.id!, payCountryId: country!.id!, userId: userId, channelCode: channel!.channelCode!, ); SCLoadingManager.hide(); _logResponse(tag: 'recharge', data: res.toJson()); if ((res.requestUrl ?? "").isEmpty) { SCTts.show('MiFaPay requestUrl is empty'); return; } await navigator.push( MaterialPageRoute( builder: (_) => MiFaPayWebViewPage( title: 'MiFaPay', requestUrl: res.requestUrl!, ), ), ); final bool rechargeSucceeded = await _waitForRechargeResult( profileManager, balanceBeforeRecharge, ); SCTts.show( rechargeSucceeded ? purchaseSuccessMessage : paymentPendingMessage, ); } catch (error) { _logError( tag: 'recharge', path: '/order/web/pay/recharge', error: error, body: payload, ); final String message = _readErrorMessage(error); SCTts.show(message); debugPrint('MiFaPay createRecharge failed: $error'); } finally { SCLoadingManager.hide(); _isCreatingOrder = false; notifyListeners(); } } Future _loadCountries() async { List countries = []; try { _logRequest(method: 'GET', path: '/order/web/pay/country'); countries = await SCConfigRepositoryImp().mifaPayCountries(); _logResponse( tag: 'country', data: countries.map((SCMiFaPayCountryRes item) => item.toJson()).toList(), ); } catch (error) { _logError(tag: 'country', path: '/order/web/pay/country', error: error); debugPrint('MiFaPay countries fallback: $error'); } if (countries.isEmpty) { countries = [_fallbackCountry()]; } _countries = countries; final String currentCountryId = _selectedCountry?.id ?? ''; _selectedCountry = countries.firstWhere( (SCMiFaPayCountryRes item) => item.id == currentCountryId, orElse: () => countries.first, ); } Future _loadCommodity() async { final String payCountryId = _selectedCountry?.id?.isNotEmpty == true ? _selectedCountry!.id! : defaultPayCountryId; final Map queryParams = { "applicationId": _applicationId, "payCountryId": payCountryId, "type": "GOLD", }; try { _logRequest( method: 'GET', path: '/order/web/pay/commodity', queryParams: queryParams, ); final SCMiFaPayCommodityRes res = await SCConfigRepositoryImp() .mifaPayCommodity( applicationId: _applicationId, payCountryId: payCountryId, ); _logResponse(tag: 'commodity', data: res.toJson()); _commodityRes = res; _applicationId = res.application?.id ?? _applicationId; final List commodityList = commodities; final String currentGoodsId = _selectedCommodity?.id ?? ''; _selectedCommodity = commodityList.isEmpty ? null : commodityList.firstWhere( (SCMiFaPayCommodityItemRes item) => item.id == currentGoodsId, orElse: () => commodityList.first, ); final List channelList = channels; final String currentChannelCode = _selectedChannel?.channelCode ?? ''; _selectedChannel = channelList.isEmpty ? null : channelList.firstWhere( (SCMiFaPayChannelRes item) => item.channelCode == currentChannelCode, orElse: () => channelList.first, ); } catch (error) { _logError( tag: 'commodity', path: '/order/web/pay/commodity', error: error, queryParams: queryParams, ); rethrow; } } Future _refreshWalletState( SocialChatUserProfileManager profileManager, ) async { try { final double balance = await SCAccountRepository().balance(); profileManager.updateBalance(balance); } catch (error) { debugPrint('MiFaPay balance refresh failed: $error'); } try { await profileManager.fetchUserProfileData(loadGuardCount: false); } catch (error) { debugPrint('MiFaPay profile refresh failed: $error'); } } Future _pollWalletState( SocialChatUserProfileManager profileManager, ) async { for (int index = 0; index < 2; index++) { await Future.delayed(const Duration(seconds: 3)); await _refreshWalletState(profileManager); } } Future _waitForRechargeResult( SocialChatUserProfileManager profileManager, double previousBalance, ) async { await _refreshWalletState(profileManager); if (profileManager.myBalance > previousBalance) { return true; } for (int index = 0; index < 5; index++) { await Future.delayed(const Duration(seconds: 2)); await _refreshWalletState(profileManager); if (profileManager.myBalance > previousBalance) { return true; } } unawaited(_pollWalletState(profileManager)); return false; } SCMiFaPayCountryRes _fallbackCountry() { return SCMiFaPayCountryRes( id: defaultPayCountryId, countryName: defaultPayCountryName, alphaTwo: defaultPayCountryCode, nationalFlag: '', ); } String _readErrorMessage(Object error) { final String raw = error.toString(); return raw .replaceFirst('Exception: ', '') .replaceFirst('DioException [unknown]: ', '') .trim(); } void _logRequest({ required String method, required String path, Map? queryParams, Map? body, }) { debugPrint('[MiFaPay][Request] $method ${_buildUrl(path)}'); if (queryParams != null && queryParams.isNotEmpty) { debugPrint('[MiFaPay][Query] ${_safeJson(queryParams)}'); } if (body != null && body.isNotEmpty) { debugPrint('[MiFaPay][Body] ${_safeJson(body)}'); } } void _logResponse({required String tag, required dynamic data}) { debugPrint('[MiFaPay][Response][$tag] ${_safeJson(data)}'); } void _logError({ required String tag, required String path, required Object error, Map? queryParams, Map? body, }) { debugPrint('[MiFaPay][Error][$tag] ${_buildUrl(path)}'); if (queryParams != null && queryParams.isNotEmpty) { debugPrint('[MiFaPay][ErrorQuery][$tag] ${_safeJson(queryParams)}'); } if (body != null && body.isNotEmpty) { debugPrint('[MiFaPay][ErrorBody][$tag] ${_safeJson(body)}'); } if (error is DioException) { debugPrint('[MiFaPay][Dio][$tag] type=${error.type}'); debugPrint( '[MiFaPay][Dio][$tag] status=${error.response?.statusCode} message=${error.message}', ); if (error.requestOptions.path.isNotEmpty) { debugPrint( '[MiFaPay][Dio][$tag] requestPath=${error.requestOptions.path}', ); } if (error.requestOptions.queryParameters.isNotEmpty) { debugPrint( '[MiFaPay][Dio][$tag] requestQuery=${_safeJson(error.requestOptions.queryParameters)}', ); } if (error.requestOptions.data != null) { debugPrint( '[MiFaPay][Dio][$tag] requestData=${_safeJson(error.requestOptions.data)}', ); } if (error.response?.data != null) { debugPrint( '[MiFaPay][Dio][$tag] responseData=${_safeJson(error.response?.data)}', ); } } else { debugPrint('[MiFaPay][Error][$tag] $error'); } } String _buildUrl(String path) { final String host = SCGlobalConfig.apiHost; if (host.endsWith('/') && path.startsWith('/')) { return '${host.substring(0, host.length - 1)}$path'; } if (!host.endsWith('/') && !path.startsWith('/')) { return '$host/$path'; } return '$host$path'; } String _safeJson(dynamic data) { try { return jsonEncode(data); } catch (_) { return data.toString(); } } }