You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS内购初始化Widget或支付流程时反复触发恢复购买,且重新订阅状态异常排查

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()

三、正确区分purchasedrestored的场景

状态触发场景
purchased1. 用户首次购买订阅/非消耗品
2. 订阅完全过期后,用户重新购买同一订阅
3. 购买从未买过的新SKU
restored1. 用户主动点击“恢复购买”按钮
2. 订阅有效期内取消自动续费后,重新开启自动续费
3. 设备残留的未完成事务被自动恢复
4. 更换设备后恢复历史购买记录

业务建议:对于订阅来说,purchasedrestored的核心业务逻辑是一致的(都是用户获得了订阅权限),你只需要做差异化提示即可(比如“订阅已购买”vs“订阅已恢复”),无需对权限授予逻辑做区分。

最终代码调整总结

  1. 删掉初始化时Android端的restorePurchases()调用
  2. 完善所有事务的completePurchase处理,避免残留
  3. 确保iOS事务清理逻辑彻底
  4. 只在用户主动触发“恢复购买”时调用restorePurchases()

内容来源于stack exchange

火山引擎 最新活动