chatapp3-flutter/lib/services/payment/mifa_pay_manager.dart
2026-04-20 18:52:03 +08:00

452 lines
14 KiB
Dart

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<SCMiFaPayCountryRes> _countries = <SCMiFaPayCountryRes>[];
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<SCMiFaPayCountryRes> get countries => _countries;
SCMiFaPayCountryRes? get selectedCountry => _selectedCountry;
SCMiFaPayCommodityRes? get commodityRes => _commodityRes;
List<SCMiFaPayCommodityItemRes> get commodities =>
_commodityRes?.commodity ?? <SCMiFaPayCommodityItemRes>[];
List<SCMiFaPayChannelRes> get channels =>
_commodityRes?.channels ?? <SCMiFaPayChannelRes>[];
SCMiFaPayCommodityItemRes? get selectedCommodity => _selectedCommodity;
SCMiFaPayChannelRes? get selectedChannel => _selectedChannel;
bool get canCreateRecharge =>
_selectedCountry != null &&
_selectedCommodity != null &&
_selectedChannel != null &&
!_isLoading &&
!_isCreatingOrder;
Future<void> 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<void> 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<void> 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<String, dynamic> payload = <String, dynamic>{
"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<SocialChatUserProfileManager>(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<void>(
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<void> _loadCountries() async {
List<SCMiFaPayCountryRes> countries = <SCMiFaPayCountryRes>[];
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 = <SCMiFaPayCountryRes>[_fallbackCountry()];
}
_countries = countries;
final String currentCountryId = _selectedCountry?.id ?? '';
_selectedCountry = countries.firstWhere(
(SCMiFaPayCountryRes item) => item.id == currentCountryId,
orElse: () => countries.first,
);
}
Future<void> _loadCommodity() async {
final String payCountryId =
_selectedCountry?.id?.isNotEmpty == true
? _selectedCountry!.id!
: defaultPayCountryId;
final Map<String, dynamic> queryParams = <String, dynamic>{
"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<SCMiFaPayCommodityItemRes> commodityList = commodities;
final String currentGoodsId = _selectedCommodity?.id ?? '';
_selectedCommodity =
commodityList.isEmpty
? null
: commodityList.firstWhere(
(SCMiFaPayCommodityItemRes item) => item.id == currentGoodsId,
orElse: () => commodityList.first,
);
final List<SCMiFaPayChannelRes> 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<void> _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<void> _pollWalletState(
SocialChatUserProfileManager profileManager,
) async {
for (int index = 0; index < 2; index++) {
await Future<void>.delayed(const Duration(seconds: 3));
await _refreshWalletState(profileManager);
}
}
Future<bool> _waitForRechargeResult(
SocialChatUserProfileManager profileManager,
double previousBalance,
) async {
await _refreshWalletState(profileManager);
if (profileManager.myBalance > previousBalance) {
return true;
}
for (int index = 0; index < 5; index++) {
await Future<void>.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<String, dynamic>? queryParams,
Map<String, dynamic>? 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<String, dynamic>? queryParams,
Map<String, dynamic>? 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();
}
}
}