452 lines
14 KiB
Dart
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();
|
|
}
|
|
}
|
|
}
|