691 lines
20 KiB
Dart
691 lines
20 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/shared/tools/sc_string_utils.dart';
|
||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||
|
||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
|
||
|
||
class AndroidPaymentProcessor extends ChangeNotifier {
|
||
StreamSubscription<List<PurchaseDetails>>? _subscription;
|
||
final InAppPurchase iap = InAppPurchase.instance;
|
||
|
||
// 状态管理变量
|
||
bool _isAvailable = false;
|
||
String _errorMessage = '';
|
||
final List<SelecteProductConfig> _products = [];
|
||
List<PurchaseDetails> _purchases = [];
|
||
|
||
///商品列表
|
||
List<SCProductConfigRes> productConfigs = [];
|
||
|
||
// 维护商品类型映射
|
||
final Map<String, String> _productTypeMap = {};
|
||
final Set<String> _verifyingPurchaseKeys = {};
|
||
bool _isReconcilingPastPurchases = false;
|
||
DateTime? _lastPastPurchaseReconcileAt;
|
||
|
||
// 公开访问器
|
||
bool get isAvailable => _isAvailable;
|
||
|
||
String get errorMessage => _errorMessage;
|
||
|
||
List<SelecteProductConfig> get products => _products;
|
||
|
||
List<PurchaseDetails> get purchases => _purchases;
|
||
late BuildContext context;
|
||
Map<String, SCProductConfigRes> productMap = {};
|
||
|
||
Map<String, String> productTypes = {};
|
||
|
||
// 初始化支付系统
|
||
Future<void> initializePaymentProcessor(BuildContext context) async {
|
||
this.context = context;
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
|
||
// 检查支付是否可用
|
||
_isAvailable = await iap.isAvailable();
|
||
if (!_isAvailable) {
|
||
_errorMessage = 'Google Play payment service is unavailable';
|
||
SCTts.show("Google Play payment service is unavailable");
|
||
return;
|
||
}
|
||
_ensurePurchaseStreamListener();
|
||
|
||
// 先获取商品配置
|
||
await fetchProductConfiguration();
|
||
|
||
// 然后获取谷歌商品信息
|
||
await _fetchProducts();
|
||
|
||
// 最后恢复购买,处理未完成交易
|
||
await recoverTransactions();
|
||
await reconcilePendingGooglePurchases(force: true);
|
||
} catch (e) {
|
||
SCTts.show("init fail: $e");
|
||
_errorMessage = 'init fail: ${e.toString()}';
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
void _ensurePurchaseStreamListener() {
|
||
_subscription ??= iap.purchaseStream.listen(
|
||
_handlePurchase,
|
||
onError: (error, stackTrace) => _handleError(error, stackTrace),
|
||
);
|
||
}
|
||
|
||
///用户获取商店配置的商品列表
|
||
Future fetchProductConfiguration() async {
|
||
productMap.clear();
|
||
productConfigs = await SCConfigRepositoryImp().productConfig();
|
||
int index = 0;
|
||
for (var value in productConfigs) {
|
||
productMap[value.productPackage!] = value;
|
||
productTypes[value.productPackage!] = "consumable";
|
||
index = index + 1;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
// 获取商品信息
|
||
Future<void> _fetchProducts() async {
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
notifyListeners();
|
||
|
||
Set<String> productIds = productTypes.keys.toSet();
|
||
|
||
ProductDetailsResponse response = await iap.queryProductDetails(
|
||
productIds,
|
||
);
|
||
if (response.notFoundIDs.isNotEmpty) {
|
||
_errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}';
|
||
}
|
||
var dtails = response.productDetails;
|
||
if (dtails.isNotEmpty) {
|
||
dtails.sort((a, b) {
|
||
int ia = 0;
|
||
int ib = 0;
|
||
if (a.id.contains(".")) {
|
||
ia = SCStringUtils.convertToInteger(a.id.split(".").last);
|
||
}
|
||
if (b.id.contains(".")) {
|
||
ib = SCStringUtils.convertToInteger(b.id.split(".").last);
|
||
}
|
||
return ia.compareTo(ib);
|
||
});
|
||
}
|
||
int index = 0;
|
||
_products.clear();
|
||
for (var v in dtails) {
|
||
if (index == 0) {
|
||
_products.add(SelecteProductConfig(v, true));
|
||
} else {
|
||
_products.add(SelecteProductConfig(v, false));
|
||
}
|
||
index++;
|
||
}
|
||
|
||
// 初始化商品类型映射
|
||
for (var product in _products) {
|
||
if (productTypes.containsKey(product.produc.id)) {
|
||
_productTypeMap[product.produc.id] = productTypes[product.produc.id]!;
|
||
} else {
|
||
_productTypeMap[product.produc.id] = 'nonConsumable'; // 默认类型
|
||
}
|
||
}
|
||
} catch (e) {
|
||
SCTts.show("Failed to retrieve the product: $e");
|
||
_errorMessage = '获取商品失败: ${e.toString()}';
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 获取商品类型
|
||
String _getProductType(String productId) {
|
||
if (productId.startsWith('coins.')) {
|
||
return 'consumable';
|
||
}
|
||
return _productTypeMap[productId] ?? 'nonConsumable';
|
||
}
|
||
|
||
// 发起购买
|
||
Future<void> processPurchase() async {
|
||
ProductDetails? product;
|
||
for (final d in _products) {
|
||
if (d.isSelecte) {
|
||
product = d.produc;
|
||
}
|
||
}
|
||
if (product == null) {
|
||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
|
||
return;
|
||
}
|
||
SmartDialog.show(
|
||
tag: "showConfirmDialog",
|
||
alignment: Alignment.center,
|
||
debounce: true,
|
||
animationType: SmartAnimationType.fade,
|
||
builder: (_) {
|
||
return MsgDialog(
|
||
title: SCAppLocalizations.of(context)!.tips,
|
||
msg: SCAppLocalizations.of(context)!.areYouRureRoRecharge,
|
||
btnText: SCAppLocalizations.of(context)!.confirm,
|
||
onEnsure: () {
|
||
_goBuy(product!);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Future<void> processFirstRechargePurchase({
|
||
required BuildContext context,
|
||
required int rechargeAmountCents,
|
||
String productPackage = '',
|
||
}) async {
|
||
this.context = context;
|
||
if (!Platform.isAndroid) {
|
||
SCTts.show('Google Play payment is only available on Android');
|
||
return;
|
||
}
|
||
if (!_isAvailable || _products.isEmpty || productMap.isEmpty) {
|
||
await initializePaymentProcessor(context);
|
||
}
|
||
if (!_isAvailable) {
|
||
return;
|
||
}
|
||
|
||
final product = _findFirstRechargeProduct(
|
||
rechargeAmountCents: rechargeAmountCents,
|
||
productPackage: productPackage,
|
||
);
|
||
if (product == null) {
|
||
SCTts.show('No matching Google Play product was found');
|
||
return;
|
||
}
|
||
|
||
for (final item in _products) {
|
||
item.isSelecte = identical(item, product);
|
||
}
|
||
notifyListeners();
|
||
await _goBuy(product.produc);
|
||
}
|
||
|
||
SelecteProductConfig? _findFirstRechargeProduct({
|
||
required int rechargeAmountCents,
|
||
required String productPackage,
|
||
}) {
|
||
final targetProduct = productPackage.trim();
|
||
if (targetProduct.isNotEmpty) {
|
||
for (final product in _products) {
|
||
if (product.produc.id == targetProduct) {
|
||
return product;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (rechargeAmountCents <= 0) {
|
||
return null;
|
||
}
|
||
|
||
for (final product in _products) {
|
||
final config = productMap[product.produc.id];
|
||
if (_matchesAmountCents(config?.unitPrice, rechargeAmountCents)) {
|
||
return product;
|
||
}
|
||
if (_matchesAmountCents(product.produc.rawPrice, rechargeAmountCents)) {
|
||
return product;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
bool _matchesAmountCents(num? value, int targetCents) {
|
||
if (value == null) {
|
||
return false;
|
||
}
|
||
final cents = value >= 20 ? value.round() : (value * 100).round();
|
||
return (cents - targetCents).abs() <= 1;
|
||
}
|
||
|
||
// 处理购买结果
|
||
void _handlePurchase(List<PurchaseDetails> purchases) {
|
||
_purchases = purchases;
|
||
|
||
for (var purchase in purchases) {
|
||
switch (purchase.status) {
|
||
case PurchaseStatus.purchased:
|
||
_verifyPayment(purchase);
|
||
break;
|
||
|
||
case PurchaseStatus.error:
|
||
if (purchase.error != null) {
|
||
_handleIAPError(purchase.error!);
|
||
// 特别处理 "item-already-owned" 错误
|
||
if (purchase.error!.code == 'item-already-owned') {
|
||
_handleAlreadyOwnedItem(purchase);
|
||
}
|
||
}
|
||
break;
|
||
|
||
case PurchaseStatus.pending:
|
||
break;
|
||
|
||
case PurchaseStatus.restored:
|
||
// 处理恢复的购买
|
||
_verifyPayment(purchase, forceSubmit: true);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
notifyListeners();
|
||
}
|
||
|
||
// 新增:处理已拥有商品的情况
|
||
Future<void> _handleAlreadyOwnedItem(PurchaseDetails purchase) async {
|
||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
try {
|
||
await reconcilePendingGooglePurchases(force: true, showToast: true);
|
||
profileManager.balance();
|
||
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||
} catch (e) {
|
||
debugPrint('handle already-owned purchase failed: $e');
|
||
}
|
||
}
|
||
|
||
// 验证支付凭证
|
||
Future<void> _verifyPayment(
|
||
PurchaseDetails purchase, {
|
||
bool forceSubmit = false,
|
||
bool showToast = true,
|
||
}) async {
|
||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
try {
|
||
// 1. 基本验证
|
||
if (purchase.verificationData.serverVerificationData.isEmpty) {
|
||
_errorMessage = '无效的支付凭证';
|
||
if (showToast) {
|
||
SCTts.show("Invalid payment voucher");
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2. 正常购买流依赖 pendingCompletePurchase;启动/回前台补偿流需要允许
|
||
// 已确认但仍未消费的一次性商品再次上报后端,否则用户付款后退出 App 会漏单。
|
||
if (!forceSubmit && !purchase.pendingCompletePurchase) {
|
||
return;
|
||
}
|
||
|
||
final purchaseKey =
|
||
purchase.purchaseID ??
|
||
purchase.verificationData.serverVerificationData;
|
||
if (purchaseKey.isNotEmpty && !_verifyingPurchaseKeys.add(purchaseKey)) {
|
||
return;
|
||
}
|
||
|
||
// 3. 获取购买数据
|
||
String purchaseData = purchase.verificationData.localVerificationData;
|
||
String signature = "";
|
||
|
||
if (purchase is GooglePlayPurchaseDetails) {
|
||
signature = purchase.billingClientPurchase.signature;
|
||
}
|
||
|
||
// 4. 服务器验证
|
||
await _submitGooglePayVerification(
|
||
purchase.productID,
|
||
signature,
|
||
purchaseData,
|
||
);
|
||
|
||
// 5. 交付商品
|
||
await _deliverProduct(purchase.productID);
|
||
|
||
// 6. 完成购买
|
||
await iap.completePurchase(purchase);
|
||
|
||
// 7. 如果是消耗型商品,确保消耗
|
||
if (_getProductType(purchase.productID) == 'consumable') {
|
||
await _ensurePurchaseConsumed(purchase);
|
||
}
|
||
|
||
// 8. 更新用户余额
|
||
profileManager.fetchUserProfileData();
|
||
profileManager.balance();
|
||
_refreshFirstRechargeRewardState(firstRechargeManager);
|
||
|
||
if (showToast) {
|
||
SCTts.show('purchase successful');
|
||
}
|
||
} catch (e) {
|
||
if (showToast) {
|
||
SCTts.show("verification failed: $e");
|
||
}
|
||
_errorMessage = '验证失败: ${e.toString()}';
|
||
} finally {
|
||
final purchaseKey =
|
||
purchase.purchaseID ??
|
||
purchase.verificationData.serverVerificationData;
|
||
if (purchaseKey.isNotEmpty) {
|
||
_verifyingPurchaseKeys.remove(purchaseKey);
|
||
}
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<void> reconcilePendingGooglePurchases({
|
||
BuildContext? context,
|
||
bool force = false,
|
||
bool showToast = false,
|
||
}) async {
|
||
if (!Platform.isAndroid) {
|
||
return;
|
||
}
|
||
if (context != null) {
|
||
this.context = context;
|
||
}
|
||
if (_isReconcilingPastPurchases) {
|
||
return;
|
||
}
|
||
final lastCheck = _lastPastPurchaseReconcileAt;
|
||
if (!force &&
|
||
lastCheck != null &&
|
||
DateTime.now().difference(lastCheck) < const Duration(seconds: 20)) {
|
||
return;
|
||
}
|
||
|
||
_isReconcilingPastPurchases = true;
|
||
_lastPastPurchaseReconcileAt = DateTime.now();
|
||
try {
|
||
_ensurePurchaseStreamListener();
|
||
_isAvailable = await iap.isAvailable();
|
||
if (!_isAvailable) {
|
||
return;
|
||
}
|
||
|
||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||
final response = await androidAddition.queryPastPurchases();
|
||
if (response.error != null) {
|
||
debugPrint('query past google purchases failed: ${response.error}');
|
||
return;
|
||
}
|
||
|
||
final purchases =
|
||
response.pastPurchases.where((purchase) {
|
||
final isPurchased =
|
||
purchase.status == PurchaseStatus.purchased ||
|
||
purchase.status == PurchaseStatus.restored;
|
||
final productType = _getProductType(purchase.productID);
|
||
final isUnacknowledged =
|
||
purchase.pendingCompletePurchase ||
|
||
!purchase.billingClientPurchase.isAcknowledged;
|
||
// Google 只会返回未消费的一次性商品;即使它已确认,也说明仍可补上报后端。
|
||
return isPurchased &&
|
||
(isUnacknowledged || productType == 'consumable');
|
||
}).toList();
|
||
|
||
if (purchases.isEmpty) {
|
||
return;
|
||
}
|
||
_purchases = purchases;
|
||
for (final purchase in purchases) {
|
||
await _verifyPayment(purchase, forceSubmit: true, showToast: showToast);
|
||
}
|
||
} catch (e) {
|
||
debugPrint('reconcile pending google purchases failed: $e');
|
||
} finally {
|
||
_isReconcilingPastPurchases = false;
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<SCGooglePayRes> _submitGooglePayVerification(
|
||
String product,
|
||
String signature,
|
||
String purchaseData,
|
||
) async {
|
||
const api = 'order/purchase-pay/google';
|
||
final paramsForLog = <String, dynamic>{
|
||
'product': product,
|
||
'signature': _clipForDebugLog(signature),
|
||
'purchaseData': _clipForDebugLog(purchaseData),
|
||
};
|
||
if (kDebugMode) {
|
||
debugPrint('[GooglePay] request api=$api params=$paramsForLog');
|
||
}
|
||
final result = await SCConfigRepositoryImp().googlePay(
|
||
product,
|
||
signature,
|
||
purchaseData,
|
||
);
|
||
if (kDebugMode) {
|
||
debugPrint('[GooglePay] response api=$api result=${result.toJson()}');
|
||
}
|
||
return result;
|
||
}
|
||
|
||
String _clipForDebugLog(String value) {
|
||
if (value.length <= 240) {
|
||
return value;
|
||
}
|
||
return '${value.substring(0, 120)}...${value.substring(value.length - 80)}';
|
||
}
|
||
|
||
// 交付商品
|
||
Future<void> _deliverProduct(String productId) async {
|
||
// 实现您的业务逻辑
|
||
// 例如:增加用户余额、解锁功能等
|
||
}
|
||
|
||
void _refreshFirstRechargeRewardState(SCAppGeneralManager manager) {
|
||
try {
|
||
unawaited(manager.refreshFirstRechargeRewardHome(forceRefresh: true));
|
||
} catch (error) {
|
||
if (kDebugMode) {
|
||
debugPrint('[FirstRecharge][payment] refresh state failed: $error');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 新增:确保购买被消耗
|
||
Future<void> _ensurePurchaseConsumed(PurchaseDetails purchase) async {
|
||
try {
|
||
// 等待一段时间确保购买完成
|
||
await Future.delayed(Duration(seconds: 1));
|
||
|
||
// 尝试消耗购买
|
||
await consumePurchase(purchase);
|
||
} catch (e) {
|
||
debugPrint('consume purchase failed: $e');
|
||
}
|
||
}
|
||
|
||
void _handleError(Object error, StackTrace stackTrace) {
|
||
SCTts.show("Payment error: $error");
|
||
if (error is IAPError) {
|
||
// 处理IAP特定错误
|
||
_handleIAPError(error);
|
||
} else {
|
||
// 处理其他类型的错误
|
||
_errorMessage = '未知错误: ${error.toString()}';
|
||
SCTts.show(_errorMessage);
|
||
}
|
||
}
|
||
|
||
// 添加了专门的IAP错误处理方法
|
||
void _handleIAPError(IAPError error) {
|
||
_errorMessage = '支付错误: ${error.message} (${error.code})';
|
||
|
||
// 处理特定错误代码
|
||
switch (error.code) {
|
||
case 'payment-invalid':
|
||
SCTts.show(
|
||
'Payment configuration error, please contact customer service',
|
||
);
|
||
break;
|
||
case 'item-already-owned':
|
||
SCTts.show('Unfinished purchase detected, processing...');
|
||
break;
|
||
case 'user-cancelled':
|
||
SCTts.show('Purchase cancelled');
|
||
break;
|
||
case 'service-timeout':
|
||
case 'service-unavailable':
|
||
SCTts.show(
|
||
'Google Play services are temporarily unavailable, please try again later',
|
||
);
|
||
break;
|
||
default:
|
||
SCTts.show('Payment failed: ${error.message}');
|
||
break;
|
||
}
|
||
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
|
||
// 恢复购买
|
||
Future<void> recoverTransactions() async {
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
notifyListeners();
|
||
|
||
await iap.restorePurchases();
|
||
|
||
// 给恢复操作一些时间
|
||
await Future.delayed(Duration(milliseconds: 1000));
|
||
} catch (e) {
|
||
_errorMessage = '恢复购买失败: ${e.toString()}';
|
||
SCTts.show("Failed to restore purchase: ${e.toString()}");
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 消耗型商品手动消耗
|
||
Future<void> consumePurchase(PurchaseDetails purchase) async {
|
||
try {
|
||
if (Platform.isAndroid) {
|
||
final InAppPurchaseAndroidPlatformAddition androidAddition =
|
||
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||
await androidAddition.consumePurchase(purchase);
|
||
}
|
||
} catch (e) {
|
||
_errorMessage = '消耗商品失败: ${e.toString()}';
|
||
}
|
||
}
|
||
|
||
// 购买流程
|
||
Future<void> _goBuy(ProductDetails product) async {
|
||
try {
|
||
SCLoadingManager.show(context: context);
|
||
notifyListeners();
|
||
|
||
// 购买前检查是否有未完成的购买
|
||
await _checkPendingPurchases();
|
||
|
||
final PurchaseParam purchaseParam = PurchaseParam(
|
||
productDetails: product,
|
||
applicationUserName: null,
|
||
);
|
||
|
||
String productType = _getProductType(product.id);
|
||
|
||
bool success;
|
||
if (productType == 'consumable') {
|
||
success = await iap.buyConsumable(
|
||
purchaseParam: purchaseParam,
|
||
autoConsume: false,
|
||
);
|
||
} else {
|
||
success = await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||
}
|
||
|
||
if (!success) {
|
||
_errorMessage = '购买启动失败,请检查网络连接';
|
||
SCTts.show(_errorMessage);
|
||
}
|
||
} catch (e) {
|
||
SCTts.show("Purchase failed: $e");
|
||
_errorMessage = '购买失败: ${e.toString()}';
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
// 新增:检查未完成的购买
|
||
Future<void> _checkPendingPurchases() async {
|
||
try {
|
||
await reconcilePendingGooglePurchases();
|
||
|
||
// 恢复购买以获取所有未完成交易
|
||
await iap.restorePurchases();
|
||
|
||
// 给一点时间处理恢复的购买
|
||
await Future.delayed(Duration(milliseconds: 550));
|
||
} catch (e) {
|
||
debugPrint('check pending purchases failed: $e');
|
||
}
|
||
}
|
||
|
||
// 添加调试方法
|
||
void logCurrentPurchaseStatus() {
|
||
debugPrint('current purchase count: ${_purchases.length}');
|
||
}
|
||
|
||
// 释放资源
|
||
@override
|
||
void dispose() {
|
||
_subscription?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
void chooseProductConfig(int index) {
|
||
for (var v in _products) {
|
||
v.isSelecte = false;
|
||
}
|
||
_products[index].isSelecte = true;
|
||
notifyListeners();
|
||
}
|
||
}
|