iOS端In-App Purchase订阅流返回错误AppStorePurchaseDetails问题
问题描述
- 在App Store Connect中创建了单个非消耗型订阅,iOS端重复执行以下操作5-7次后出现异常:
- 调用
InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam)完成测试交易 - 删除并重装App
- 调用
InAppPurchase.instance.restorePurchases()恢复订阅
- 调用
- 异常表现:订阅流返回大量错误的AppStorePurchaseDetails列表
- 预期结果:因仅存在一个非消耗型订阅,购买或恢复时应仅返回一个交易详情对象(Android端功能正常)
问题分析
从提供的_onPurchaseUpdate代码来看,存在以下核心问题:
- iOS端未正确完成交易:在
PurchaseStatus.purchased分支中,仅调用了finishTransaction()但未执行InAppPurchase.instance.completePurchase(purchaseDetails),导致App Store的交易未被标记为完成,后续恢复操作会反复返回历史交易记录。 - 恢复状态(restored)未处理:代码仅打印日志,既没有验证恢复的交易,也没有完成交易流程,未收尾的恢复交易会被框架持续返回。
- 跨平台逻辑混淆:Android端的
consumePurchase和交易验证逻辑被放在了通用的purchased分支中,iOS代码块执行后会再次跑Android逻辑,导致交易处理混乱。 - 错误的列表操作:手动调用
purchaseDetailsList.clear()属于越权操作,框架内部的交易列表应自行维护,强行清空会导致状态不一致。
修复方案
修改后的_onPurchaseUpdate函数如下:
Future<void> _onPurchaseUpdate(List<PurchaseDetails> purchaseDetailsList) async { for (final purchaseDetails in purchaseDetailsList) { switch (purchaseDetails.status) { case PurchaseStatus.canceled: await InAppPurchase.instance.completePurchase(purchaseDetails); paymentState.value = PaymentState.failed; errorMessage = "Purchase Cancelled"; update(); Future.delayed(const Duration(seconds: 1), () { paymentState.value = PaymentState.init; }); break; case PurchaseStatus.purchased: try { if (Platform.isIOS) { final String? iosPurchaseToken = purchaseDetails.verificationData.localVerificationData; if (iosPurchaseToken?.isNotEmpty == true) { print("IOS >> $iosPurchaseToken"); final params = CheckPurchaseTokenValidityParams( deviceType: 'IOS', token: iosPurchaseToken!, price: _purchasePrice, ); final result = await checkPurchaseTokenValidityUseCase(params); result.fold( (failure) { errorMessage = failure.toString(); paymentState.value = PaymentState.failed; }, (isValid) { _subscriptionDetailsData = isValid; SharedPrefUtility.setisPremium(); paymentState.value = PaymentState.success; _navigateToWelcomeIfNeeded(); }, ); } } else if (Platform.isAndroid) { final purchaseData = json.decode(purchaseDetails.verificationData.localVerificationData); final subscriptionID = purchaseData?["productId"]; final packageName = purchaseData?["packageName"]; final purchaseToken = purchaseData?["purchaseToken"]; if (purchaseData?["acknowledged"] == false) { final androidAddition = InAppPurchase.instance.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>(); await androidAddition.consumePurchase(purchaseDetails); } final params = CheckPurchaseTokenValidityParams( deviceType: "ANDROID", token: purchaseToken, price: _purchasePrice, packageName: packageName, subscriptionID: subscriptionID, isUpgraded: _isSubscribed, ); final result = await checkPurchaseTokenValidityUseCase(params); result.fold( (failure) { errorMessage = failure.toString(); paymentState.value = PaymentState.failed; }, (isValid) { _subscriptionDetailsData = isValid; SharedPrefUtility.setisPremium(); paymentState.value = PaymentState.success; _navigateToWelcomeIfNeeded(); }, ); } // 统一完成交易,避免重复返回 await InAppPurchase.instance.completePurchase(purchaseDetails); update(); } catch (e) { errorMessage = e.toString(); paymentState.value = PaymentState.failed; update(); } break; case PurchaseStatus.error: errorMessage = purchaseDetails.error.toString(); paymentState.value = PaymentState.failed; update(); // 错误交易也需完成,防止重复推送 await InAppPurchase.instance.completePurchase(purchaseDetails); break; case PurchaseStatus.restored: print("Restore Purchased Called"); // 处理恢复的交易 if (Platform.isIOS) { final iosPurchaseToken = purchaseDetails.verificationData.localVerificationData; if (iosPurchaseToken?.isNotEmpty == true) { final params = CheckPurchaseTokenValidityParams( deviceType: 'IOS', token: iosPurchaseToken!, price: _purchasePrice, ); final result = await checkPurchaseTokenValidityUseCase(params); result.fold( (failure) => errorMessage = failure.toString(), (isValid) { _subscriptionDetailsData = isValid; SharedPrefUtility.setisPremium(); paymentState.value = PaymentState.success; _navigateToWelcomeIfNeeded(); }, ); } } await InAppPurchase.instance.completePurchase(purchaseDetails); update(); break; } } } // 提取重复导航逻辑 void _navigateToWelcomeIfNeeded() { if (!isNavigated && !SharedPrefUtility.getIsVisitedToWelcomeScreen()) { locator.get<NavigationService>().pushNamed( AppRoutes.welcomePlusUser, arguments: const <String, dynamic>{"isFirst": false}, ); SharedPrefUtility.setIsVisitedToWelcomeScreen(); isNavigated = true; update(); } }
关键修复点说明
- 统一交易完成流程:所有交易状态(取消、购买、错误、恢复)都调用
completePurchase,确保App Store将交易标记为已处理,不会重复返回。 - 严格区分跨平台逻辑:用
Platform.isIOS/Platform.isAndroid隔离两端代码,避免Android逻辑在iOS端执行。 - 完善恢复交易处理:在
restored分支中添加验证和完成步骤,确保恢复的交易被正确收尾。 - 移除错误的列表清空操作:删除
purchaseDetailsList.clear(),交由框架自行维护交易列表。 - 提取重复逻辑:将导航逻辑抽为单独函数,提升代码可读性和可维护性。
内容的提问来源于stack exchange,提问作者Mrudang
相关产品推荐
相关产品推荐

