diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 54c531f..cd87ca8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -524,7 +524,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 161; + CURRENT_PROJECT_VERSION = 162; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -533,7 +533,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.1; + MARKETING_VERSION = 1.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 161; + CURRENT_PROJECT_VERSION = 162; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -724,7 +724,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.1; + MARKETING_VERSION = 1.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -744,7 +744,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 161; + CURRENT_PROJECT_VERSION = 162; DEVELOPMENT_TEAM = S9YG87C297; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -753,7 +753,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.6.1; + MARKETING_VERSION = 1.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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..08b30d7 100644 --- a/lib/services/payment/google_payment_manager.dart +++ b/lib/services/payment/google_payment_manager.dart @@ -15,6 +15,7 @@ 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'; @@ -33,6 +34,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 维护商品类型映射 final Map _productTypeMap = {}; + final Set _verifyingPurchaseKeys = {}; + bool _isReconcilingPastPurchases = false; + DateTime? _lastPastPurchaseReconcileAt; // 公开访问器 bool get isAvailable => _isAvailable; @@ -60,12 +64,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 +74,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 +84,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 +162,9 @@ class AndroidPaymentProcessor extends ChangeNotifier { // 获取商品类型 String _getProductType(String productId) { + if (productId.startsWith('coins.')) { + return 'consumable'; + } return _productTypeMap[productId] ?? 'nonConsumable'; } @@ -283,7 +293,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { case PurchaseStatus.restored: // 处理恢复的购买 - _verifyPayment(purchase); + _verifyPayment(purchase, forceSubmit: true); break; default: break; @@ -304,21 +314,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 +340,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; } @@ -349,7 +368,7 @@ class AndroidPaymentProcessor extends ChangeNotifier { } // 4. 服务器验证 - await SCConfigRepositoryImp().googlePay( + await _submitGooglePayVerification( purchase.productID, signature, purchaseData, @@ -371,16 +390,125 @@ 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 _submitGooglePayVerification( + String product, + String signature, + String purchaseData, + ) async { + const api = 'order/purchase-pay/google'; + final paramsForLog = { + '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 _deliverProduct(String productId) async { // 实现您的业务逻辑 @@ -506,7 +634,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 +656,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'); diff --git a/local_packages/pag-1.0.7-patched/android/build.gradle b/local_packages/pag-1.0.7-patched/android/build.gradle index bcb266f..15ec078 100644 --- a/local_packages/pag-1.0.7-patched/android/build.gradle +++ b/local_packages/pag-1.0.7-patched/android/build.gradle @@ -42,6 +42,6 @@ android { dependencies { - implementation "com.tencent.tav:libpag:4.3.68" + implementation "com.tencent.tav:libpag:4.5.70" implementation 'com.jakewharton:disklrucache:2.0.2' } diff --git a/pubspec.yaml b/pubspec.yaml index 457ee0e..1eef535 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.6.1+161 +version: 1.6.2+162 environment: