Flutter痛点 — Android端的Flutter in_app_purchase:购买成功(收据已收到),但purchaseStream报错——verifyIAP从未被调用

移动开发 2026-07-09

我们在Flutter上使用 in_app_purchase: ^3.2.3 (in_app_purchase_android: 0.4.0+10)。在Android上,当用户完成购买时:

  • Google Play处理正确(已收到测试收据邮件、生成订单ID,在沙箱中显示 “Test card, always approves”)
  • 应用会短暂显示 “购买未完成。如果您取消,请再试一次。”
  • 档位保持不变
  • 我们后端的Cloud Function (verifyIAP) 从未被调用 —— 通过GCP日志确认

在第二次尝试时:“You already own this item” —— 这表明Google Play认为购买已完成但尚未确认。

我们的购买流处理程序:

void _onPurchaseUpdate(List<PurchaseDetails> purchases) {
  for (final purchase in purchases) {
    if (purchase.productID != _pendingProductId) {
      if (purchase.status != PurchaseStatus.pending) {
        _iap.completePurchase(purchase);
      }
      continue;
    }

    switch (purchase.status) {
      case PurchaseStatus.purchased:
      case PurchaseStatus.restored:
        _handlePurchased(purchase);
      case PurchaseStatus.canceled:
        _iap.completePurchase(purchase);
        _resolvePending(IAPResult.cancelled);
      case PurchaseStatus.error:
        _iap.completePurchase(purchase);
        _resolvePending(IAPResult.storeError);
      case PurchaseStatus.pending:
        break;
    }
  }
}

错误信息 “Purchase didn't complete. If you cancelled, just try again.” 对应到 IAPResult.storeError

问题: 在Android上,完成的Google Play购买是否可能以 PurchaseStatus.error 的形式到达 purchaseStream,而不是 PurchaseStatus.purchased?如果可以,需满足哪些条件?或者是否存在其他机制,在到达我们的处理程序之前就会使流事件被丢弃?

我们已排除的情况:

  • didChangeAppLifecycleState 取消路径受 if (Platform.isAndroid) return; 保护
  • 30秒超时未触发(错误很快就出现)
  • _pendingProductId 不匹配是可能的,但不清楚为何在流交付时会为null
  • iOS使用相同代码也能正常工作

非常感谢任何帮助!

解决方案

我们终于解决了……

两种机制:

1/ didChangeAppLifecycleState 在计费覆盖层被取消时触发(这是你的bug)

Play的计费覆盖层会短暂暂停应用。如果你的生命周期观察者采用iOS风格的取消逻辑,它会在 purchaseStream 传送 purchased 事件之前把 _pendingProductId 清空为null。事件到达时没有匹配项,因此被丢弃。修复:

void didChangeAppLifecycleState(AppLifecycleState state) {

if (Platform.isAndroid) return;

// iOS-only cancellation logic below...

}

2/ 真正的购买可能以 PurchaseStatus.error 的形式到达

itemAlreadyOwned 被传递为 PurchaseStatus.error,并带有一个 空的 productID。如果你的不匹配检查先检查 productID == _pendingProductId,空字符串会导致它失败,事件在到达错误处理程序之前会被静默丢弃。在进入不匹配分支之前,请检查 itemAlreadyOwned +空的productID,然后调用 restorePurchases() —— 未确认的令牌将以携带实际产品ID的 PurchaseStatus.restored 出现。

iOS对这两种情况都不受影响。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章