iOS内购初始化Widget或支付流程时反复触发恢复购买,且重新订阅状态异常排查
我完全理解你现在的头疼——iOS内购的restore方法总在不该触发的时候自动跑起来,取消订阅后重新购买还拿不到预期的purchased状态,这确实很闹心。我结合你的代码和Apple内购的核心机制,一步步帮你拆解问题:
一、为什么restore会自动触发?怎么阻止?
你当前的代码和内购机制的几个细节导致了不必要的restore触发:
1. 主动调用了不必要的restorePurchases()
看你的_initializeState方法,Android端在初始化时主动调用了await InAppPurchase.instance.restorePurchases();——这是完全没必要的!restorePurchases()只应该在用户主动点击“恢复购买”按钮时调用(比如页面上专门加一个“恢复订阅”的入口),初始化时调用会直接触发恢复流程,干扰正常的购买监听。
修改建议:删掉这段Android初始化时的主动恢复代码,只保留用户主动触发恢复操作时的调用。
2. 未处理的事务残留
iOS的SKPaymentQueue机制:如果有未被finishTransaction的事务(比如error、canceled状态的),下次App启动会自动重新上报这些事务,导致你的purchaseStream收到事件,看起来像是触发了restore,但实际是旧事务的重复推送。
你的当前代码中,purchaseStream监听只处理了非canceled和非error的事件,这些状态的事务根本没被finish,导致残留:
// 你当前的监听逻辑:跳过了canceled和error的事务 if (purchaseDetailsList.isNotEmpty && purchaseDetailsList.first.status != PurchaseStatus.canceled && purchaseDetailsList.first.status != PurchaseStatus.error) { _listenToPurchaseUpdated(purchaseDetailsList); }
修复方案:修改监听逻辑,处理所有状态的事务,并强制完成事务:
// 替换你的purchaseStream监听逻辑 _purchaseStreamSubscription = _purchaseStream.listen((purchaseDetailsList) async { await _handleAllPurchaseTransactions(purchaseDetailsList); }, onDone: () { debugPrint('Stream is done'); }, onError: (error) { debugPrint('Stream error: $error'); }); // 新增统一处理所有事务的方法 Future<void> _handleAllPurchaseTransactions(List<PurchaseDetails> purchaseDetailsList) async { for (final purchase in purchaseDetailsList) { // 跳过已处理过的事务,避免重复处理 if (_processedTokens.contains(purchase.purchaseID)) continue; _processedTokens.add(purchase.purchaseID ?? ''); switch (purchase.status) { case PurchaseStatus.pending: // 显示支付中提示 debugPrint('支付中: ${purchase.productID}'); break; case PurchaseStatus.purchased: debugPrint('购买成功: ${purchase.productID}'); await _verifyAndUpdateSubscription(purchase); break; case PurchaseStatus.restored: debugPrint('恢复购买成功: ${purchase.productID}'); await _verifyAndUpdateSubscription(purchase); break; case PurchaseStatus.canceled: debugPrint('支付取消: ${purchase.productID}'); break; case PurchaseStatus.error: debugPrint('支付失败: ${purchase.error}'); break; } // 核心:无论任何状态,只要事务未完成,就强制finish if (purchase.pendingCompletePurchase) { await InAppPurchase.instance.completePurchase(purchase); } } }
3. iOS事务清理时机不彻底
你当前的_clearAllTransactions()方法是对的,但可以优化得更彻底,确保初始化时没有残留事务:
Future<void> _clearAllTransactions() async { if (!Platform.isIOS) return; // 用StoreKit平台的工具类直接清理所有pending事务 final storeKitPlatform = InAppPurchase.instance.getPlatformAddition<InAppPurchaseStoreKitPlatform>(); await storeKitPlatform?.finishAllPendingTransactions(); // 再手动遍历清理一次,双重保险 final queue = SKPaymentQueueWrapper(); final transactions = await queue.transactions(); for (final tx in transactions) { await queue.finishTransaction(tx); debugPrint('清理残留事务: ${tx.payment.productIdentifier}'); } }
二、取消订阅后重新购买返回restored而不是purchased?
这是Apple订阅机制的正常表现,取决于你重新购买的时机:
1. 订阅是否真正过期?
当你取消自动续费时,订阅并不会立即失效——它会持续到当前周期结束(比如你设置的3分钟测试周期)。如果在订阅有效期内重新购买同一订阅,Apple会认为你是在“恢复自动续费”,所以返回restored状态。
只有当订阅完全过期(比如3分钟周期结束后),此时重新购买才会返回purchased状态,这是你预期的结果。
2. Sandbox测试环境的小坑
Sandbox的订阅状态有时会有1-2分钟的延迟,你需要确保:
- 确实等订阅过期后再发起购买(可以通过Sandbox测试账号的订阅记录确认状态)
- 购买时调用的是正常的购买方法(
buySubscriptions),而非restorePurchases()
三、正确区分purchased和restored的场景
| 状态 | 触发场景 |
|---|---|
purchased | 1. 用户首次购买订阅/非消耗品 2. 订阅完全过期后,用户重新购买同一订阅 3. 购买从未买过的新SKU |
restored | 1. 用户主动点击“恢复购买”按钮 2. 订阅有效期内取消自动续费后,重新开启自动续费 3. 设备残留的未完成事务被自动恢复 4. 更换设备后恢复历史购买记录 |
业务建议:对于订阅来说,purchased和restored的核心业务逻辑是一致的(都是用户获得了订阅权限),你只需要做差异化提示即可(比如“订阅已购买”vs“订阅已恢复”),无需对权限授予逻辑做区分。
最终代码调整总结
- 删掉初始化时Android端的
restorePurchases()调用 - 完善所有事务的
completePurchase处理,避免残留 - 确保iOS事务清理逻辑彻底
- 只在用户主动触发“恢复购买”时调用
restorePurchases()
内容来源于stack exchange




