diff --git a/lib/main.dart b/lib/main.dart index fa642c2..e6a7b06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:fluro/fluro.dart' as fluro; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -299,6 +300,7 @@ class _YumiApplicationState extends State WidgetsBinding.instance.addPostFrameCallback((_) { _initRouter(); unawaited(_initLink()); + unawaited(_reconcilePendingGooglePurchases(force: true)); }); } @@ -325,6 +327,24 @@ class _YumiApplicationState extends State context.read().releaseRtcEngineForAppTermination(), ); } + if (state == AppLifecycleState.resumed) { + unawaited(_reconcilePendingGooglePurchases()); + } + } + + Future _reconcilePendingGooglePurchases({bool force = false}) async { + if (!Platform.isAndroid) { + return; + } + try { + await context + .read() + .reconcilePendingGooglePurchases(context: context, force: force); + } catch (error) { + if (kDebugMode) { + debugPrint('reconcile pending google purchases failed: $error'); + } + } } @override diff --git a/lib/services/payment/google_payment_manager.dart b/lib/services/payment/google_payment_manager.dart index 22fc38e..fc097df 100644 --- a/lib/services/payment/google_payment_manager.dart +++ b/lib/services/payment/google_payment_manager.dart @@ -33,6 +33,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 维护商品类型映射 final Map _productTypeMap = {}; + final Set _verifyingPurchaseKeys = {}; + bool _isReconcilingPastPurchases = false; + DateTime? _lastPastPurchaseReconcileAt; // 公开访问器 bool get isAvailable => _isAvailable; @@ -60,12 +63,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { SCTts.show("Google Play payment service is unavailable"); return; } - _subscription?.cancel(); - // 设置购买流监听 - _subscription = iap.purchaseStream.listen( - _handlePurchase, - onError: (error, stackTrace) => _handleError(error, stackTrace), - ); + _ensurePurchaseStreamListener(); // 先获取商品配置 await fetchProductConfiguration(); @@ -75,6 +73,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 最后恢复购买,处理未完成交易 await recoverTransactions(); + await reconcilePendingGooglePurchases(force: true); } catch (e) { SCTts.show("init fail: $e"); _errorMessage = 'init fail: ${e.toString()}'; @@ -84,6 +83,13 @@ class AndroidPaymentProcessor extends ChangeNotifier { } } + void _ensurePurchaseStreamListener() { + _subscription ??= iap.purchaseStream.listen( + _handlePurchase, + onError: (error, stackTrace) => _handleError(error, stackTrace), + ); + } + ///用户获取商店配置的商品列表 Future fetchProductConfiguration() async { productMap.clear(); @@ -155,6 +161,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 获取商品类型 String _getProductType(String productId) { + if (productId.startsWith('coins.')) { + return 'consumable'; + } return _productTypeMap[productId] ?? 'nonConsumable'; } @@ -283,7 +292,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { case PurchaseStatus.restored: // 处理恢复的购买 - _verifyPayment(purchase); + _verifyPayment(purchase, forceSubmit: true); break; default: break; @@ -304,21 +313,20 @@ class AndroidPaymentProcessor extends ChangeNotifier { listen: false, ); try { - // 如果是消耗型商品,尝试消耗 - if (_getProductType(purchase.productID) == 'consumable') { - await consumePurchase(purchase); - // 消耗后重新获取余额 - profileManager.balance(); - _refreshFirstRechargeRewardState(firstRechargeManager); - SCTts.show('outstanding purchases have been reinstated'); - } + await reconcilePendingGooglePurchases(force: true, showToast: true); + profileManager.balance(); + _refreshFirstRechargeRewardState(firstRechargeManager); } catch (e) { debugPrint('handle already-owned purchase failed: $e'); } } // 验证支付凭证 - Future _verifyPayment(PurchaseDetails purchase) async { + Future _verifyPayment( + PurchaseDetails purchase, { + bool forceSubmit = false, + bool showToast = true, + }) async { final profileManager = Provider.of( context, listen: false, @@ -331,12 +339,22 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 1. 基本验证 if (purchase.verificationData.serverVerificationData.isEmpty) { _errorMessage = '无效的支付凭证'; - SCTts.show("Invalid payment voucher"); + if (showToast) { + SCTts.show("Invalid payment voucher"); + } return; } - // 2. 检查是否已经处理过这个购买 - if (!purchase.pendingCompletePurchase) { + // 2. 正常购买流会依赖 pendingCompletePurchase;启动/回前台补偿流必须允许 + // 已确认但仍未消费的一次性商品再次上报后端,否则用户付款后退出 App 会漏单。 + if (!forceSubmit && !purchase.pendingCompletePurchase) { + return; + } + + final purchaseKey = + purchase.purchaseID ?? + purchase.verificationData.serverVerificationData; + if (purchaseKey.isNotEmpty && !_verifyingPurchaseKeys.add(purchaseKey)) { return; } @@ -371,16 +389,93 @@ class AndroidPaymentProcessor extends ChangeNotifier { profileManager.balance(); _refreshFirstRechargeRewardState(firstRechargeManager); - SCTts.show('purchase successful'); + if (showToast) { + SCTts.show('purchase successful'); + } } catch (e) { - SCTts.show("verification failed: $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 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(); + 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 _deliverProduct(String productId) async { // 实现您的业务逻辑 @@ -506,7 +601,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { if (productType == 'consumable') { success = await iap.buyConsumable( purchaseParam: purchaseParam, - autoConsume: kReleaseMode, // 生产环境自动消耗,调试时手动控制 + autoConsume: false, ); } else { success = await iap.buyNonConsumable(purchaseParam: purchaseParam); @@ -528,6 +623,8 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 新增:检查未完成的购买 Future _checkPendingPurchases() async { try { + await reconcilePendingGooglePurchases(); + // 恢复购买以获取所有未完成交易 await iap.restorePurchases(); diff --git a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart index bfa3d2d..7ab02a7 100644 --- a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart +++ b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -1336,7 +1335,7 @@ class _RoomRedPacketRechargeSheetState SCLoadingManager.show(); final success = await processor.iap.buyConsumable( purchaseParam: PurchaseParam(productDetails: product.produc), - autoConsume: kReleaseMode, + autoConsume: false, ); if (!success) { SCTts.show('Purchase failed, please check your network connection'); diff --git a/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart b/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart index cd1fdb4..29eaa32 100644 --- a/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart +++ b/lib/ui_kit/widgets/room/room_recharge_bottom_sheet.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -367,7 +366,7 @@ class _RoomRechargeBottomSheetState extends State { SCLoadingManager.show(); final success = await processor.iap.buyConsumable( purchaseParam: PurchaseParam(productDetails: product.produc), - autoConsume: kReleaseMode, + autoConsume: false, ); if (!success) { SCTts.show('Purchase failed, please check your network connection');