PAYATHON 2026

iOS StoreKit 2 如何在另一台设备上获知 pending 购买?

支付小周

结论

StoreKit 2 没有公开接口,可以向同一 Family Sharing 群组内的其他设备广播“某笔购买刚进入 pending”。设备 B 不能仅凭 Transaction.updates 或 SubscriptionStatus.updates 获知设备 A 的这次待处理购买。

Apple 对 Product.PurchaseResult.pending 的说明是,购买仍需要用户采取行动。如果之后购买成功,StoreKit 才会通过 Transaction.updates 交付对应交易。因此,进入 pending 时没有收到交易更新,符合这一机制。Apple:Product.PurchaseResult.pending

如果希望此时同步禁用 A、B 的购买入口,应用需要自行记录和同步购买意图,并能够识别哪些设备或用户属于同一个业务范围。

为什么两个监听都没有触发?

这几个接口关注的状态不同:

  • Product.PurchaseResult.pending 是当前购买调用的返回结果,表示购买尚待处理。
  • Transaction.updates 提供交易创建或更新事件,包括在其他设备上完成的购买,以及获批后完成的 Ask to Buy 购买。
  • SubscriptionStatus.updates 关注订阅状态变化,不是购买请求的进度通知。

因此,如果 pending 购买没有引起订阅状态变化,订阅状态监听就不会因此触发。相关完整类型是 Product.SubscriptionInfo.Status。Apple:Transaction.updates、Apple:订阅状态 updates

Transaction.unfinished 也不是 pending 购买列表。这里的 unfinished 指应用尚未调用 finish()、仍需处理交付的交易,不能用来查询另一台设备上尚待批准的购买请求。Apple:Transaction.unfinished

如何同步禁用两台设备的购买入口?

可以在应用层实现,具体方式取决于现有账户体系。

1. 明确设备之间的关联方式

如果两台设备登录同一个应用账户,可以按账户同步状态。如果是两个家庭成员的不同账户,则需要应用自己的家庭账户、成员邀请或其他明确的关联机制。

仅知道两台设备处于 Apple 的 Family Sharing 群组,不足以让应用后端自动关联它们。不要假设 StoreKit 会返回可供业务锁使用的家庭成员列表或统一家庭标识。

Family Sharing 负责让家庭成员获得符合条件的已购产品权益。Apple 还说明,家庭成员对应交易的生成可能存在延迟,因此不能依赖它实现购买前的即时互斥。Apple:Explore Family Sharing for In-App Purchases

2. 在发起购买前协调,在 pending 后同步

可以按以下顺序处理:

  1. 设备 A 请求后端检查现有权益及冲突的购买请求。
  2. 后端原子地创建购买意图,避免 A、B 同时通过检查。
  3. A 调用 StoreKit 发起购买。
  4. 如果返回 .pending,A 持久化该状态并上传后端。
  5. B 通过应用自己的状态订阅、推送后重新查询,或打开购买页时查询,更新购买入口。

如果只在收到 .pending 后才写入后端,A、B 仍可能在写入前同时发起购买。购买前协调可以缩小这个竞争窗口。

状态中可以包含应用生成的请求 ID、产品 ID、关联账户、互斥产品范围及创建时间。应用请求 ID 应与 StoreKit 交易 ID 分开管理。

3. 分开处理购买状态和使用权益

pending 可以用于暂时禁用冲突产品的购买按钮,但不能据此授予订阅或终身使用权,也不应移除用户已经拥有的有效权益。

购买完成后,应验证交易、交付权益,再调用 finish()。同时,在应用启动时建立 Transaction.updates 监听,处理稍后完成的购买。

Swift 中如何处理 pending?

下面的函数展示了购买结果的分支处理。recordPending 和 deliver 是应用自行提供的回调,分别负责保存待处理状态和交付已验证交易对应的权益。

import StoreKit

func handlePurchaseResult(
    _ result: Product.PurchaseResult,
    productID: String,
    recordPending: (String) async throws -> Void,
    deliver: (Transaction) async throws -> Void
) async throws {
    switch result {
    case .pending:
        try await recordPending(productID)

    case .success(let verification):
        switch verification {
        case .verified(let transaction):
            try await deliver(transaction)
            await transaction.finish()

        case .unverified(_, let error):
            throw error
        }

    case .userCancelled:
        break

    @unknown default:
        break
    }
}

调用方把 product.purchase() 的结果传给这个函数即可。如果使用 onInAppPurchaseCompletion,也可以对其成功返回的 Product.PurchaseResult 使用相同的分支逻辑。

recordPending 应先保存本地状态,再安排上传和失败重试,避免网络错误导致状态丢失。对于购买前创建的业务记录,调用方还需要在取消、抛错等路径中妥善更新。

上述代码只展示结果处理。通过 Transaction.updates 收到的交易也要经过验证,并复用可重复执行而不会重复交付的权益处理逻辑。Apple:Product.PurchaseResult

如何避免永久锁定或误判?

不要假设每个 pending 请求最终都会向应用返回取消或拒绝事件。业务记录需要提供超时后的提示、重新检查权益及人工恢复入口。

业务超时只代表应用允许重新尝试,不代表原来的 Apple 购买请求已经取消。原请求仍可能稍后成功,因此解除限制时应提示潜在的重复购买风险,并继续处理迟到的交易。

应用启动、回到前台或打开购买页时,可以结合 Transaction.currentEntitlements 重建当前有效权益。它用于查询已有权益,不能查询 pending 购买。Apple:Transaction.currentEntitlements

对于“订阅与终身使用权互斥”,还需要制定明确的产品规则,例如用户已有终身权益时隐藏订阅入口。禁用界面和后端协调能减少冲突,但无法保证阻止所有已经发起、离线或应用外完成的购买。

如果应用没有自己的跨设备账户关联和同步能力,就只能在设备 A 本地展示 pending 状态,并在设备 B 获得实际交易或共享权益后更新界面;无法仅凭 Family Sharing 实现提前同步禁用。

备注:内容仅供参考。