在上架前联调 IAP 时,我们遇到一个典型但非常迷惑的问题:

  • 用户点击“继续购买”
  • 按钮进入“处理中”
  • 几秒后恢复“继续购买”
  • 没有弹出 App Store 购买确认框

这篇文章记录完整排查路径、关键日志、根因以及可落地修复方案,适合发布前做 StoreKit 风险检查。

1. 问题现象

购买页商品列表能正常加载,点击购买按钮后:

  • purchase() 被调用
  • 无系统弹窗
  • UI 最终回到未购买态

乍看像是“购买流程没发起”,但日志显示并不这么简单。

2. 首轮排查:确认购买链路有没有走通

我们先把购买链路关键节点打全日志:

  • 按钮点击参数
  • purchaseSelectedProduct() 入口和 guard 分支
  • product.purchase() 前后
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

结果显示:

  • purchase() 返回了 .success
  • 交易校验成功、transaction.finish() 也执行了
  • entitledIDs 仍为空

这说明:不是“没发起购买”,而是“购买后权益没被识别”

3. 深挖后发现的关键事实

继续补日志后,出现了几个决定性信号:

  1. Transaction.currentEntitlements 迭代数为 0
  2. Transaction.latest(for: monthly) 有有效记录,但已过期
  3. 更关键的是:购买前 Transaction.unfinished 中存在旧交易
  4. 本次 purchase() 返回的也是旧交易链路中的交易,而非新的交互购买流程

这就解释了为什么“不弹窗”:

  • StoreKit 先返回了可处理的旧交易(unfinished / 历史链路)
  • 你代码把它 finish 掉了
  • 购买请求看起来“成功”,但没有走到用户期望的新购买确认弹窗路径

4. 根因总结

本次问题不是单点,而是两类因素叠加:

  • 未完成交易(unfinished transaction)干扰购买流程
  • 沙盒订阅链路处于过期状态(即使有 latest transaction,也可能不是 active entitlement)

最终表现就是:不弹窗、成功返回旧交易、权益仍为空。

5. 修复策略(实践有效)

5.1 购买前主动清理 unfinished 交易

在启动和点击购买前,扫描 Transaction.unfinished,对 verified transaction 执行 finish(),避免旧交易截胡新购买流程。

5.2 保留并强化诊断日志(上线前建议开启)

关键日志建议保留到上线前:

  • unfinished transaction 快照
  • subscription status(state、renewal info)
  • currentEntitlements 迭代数量
  • latest(for:) 的 transaction/expiration

5.3 权益判断采用“双通道”(可选增强)

若某些环境下 currentEntitlements 短暂为空,可用 latest(for:) 做回退判定(注意仍需校验是否 active)。

6. 上线前 IAP 检查清单

  1. 每次测试前确认 Sandbox 账号订阅状态(是否已过期)
  2. 关键路径打印 unfinished 数量
  3. 测试“首次购买 / 过期后续订 / 恢复购买”三类场景
  4. 检查 currentEntitlementslatest 是否一致
  5. 若出现“不弹窗”,先看是否被旧交易链路命中

7. 结语

“点击购买不弹窗”不一定是 UI 或调用时机问题。
在 StoreKit 2 下,交易状态、unfinished 队列、订阅生命周期都会影响最终表现。

上架前把这些日志和清理动作补齐,能减少大量线上不可复现的 IAP 问题。