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

StoreKit 2下如何可靠检测订阅的过期与续订状态?

StoreKit 2下如何可靠检测订阅的过期与续订状态?

作为长期用StoreKit 2做订阅逻辑的开发者,我完全理解你遇到的困惑——一开始我也踩过只靠Transaction.updates的坑。咱们一步步拆解你的问题,再给你一套可靠的实现方案。


先理清几个核心误解(帮你踩坑)

首先得明确StoreKit 2里几个容易混淆的概念,这是你问题的根源:

  1. 取消自动续订 ≠ 订阅立即过期:用户取消自动续订后,当前订阅周期内的权益是完全有效的,只有当当前周期自然结束时,订阅才会真正过期、权益终止。StoreKit不会在取消时立刻收回权益,也不会在过期时主动发送交易事件(因为没有新的交易产生)。
  2. Transaction.updates的作用边界:这个流只监听交易相关的变更(比如购买成功、续订、退款、恢复购买),订阅过期是一个时间驱动的状态变化,不会生成新的Transaction,所以它不会触发任何事件。

你的问题逐一解答

1. Transaction.updates会在订阅过期时触发事件吗?

不会。它只处理有交易记录产生的场景(比如用户又续订了、申请退款成功了)。订阅过期是当前周期的自然结束,没有对应的交易,所以这个流完全不会有输出。

2. 我应该用Product.SubscriptionInfo.Status代替交易监听吗?

必须的!这才是StoreKit 2官方推荐的、直接获取用户当前订阅权益状态的方式。Transaction记录的是历史交易行为,而SubscriptionInfo直接反映用户当下的权益状态(是否有效、过期时间、自动续订开关等),比单独处理交易靠谱得多。

3. 我是不是搞混了取消和过期的逻辑?

是的。你需要明确两个场景:

  • 取消自动续订:用户只是告诉苹果“以后别自动扣钱了”,但当前已经付费的订阅周期内,权益100%有效,直到周期结束。
  • 订阅立即过期:只有当苹果处理了全额退款(比如用户申请退款并通过),才会触发订阅立即失效,这时候会生成退款交易,Transaction.updates会收到对应的事件。

4. 怎么让isSubscribed和实际权益状态保持同步?

官方推荐的方案是主动查询 + 被动监听结合,双管齐下才能确保状态不脱节:


可靠的实现方案(附代码示例)

1. 核心:主动查询订阅状态的工具函数

不管什么时候,只要你需要知道当前权益状态,就调用这个函数更新isSubscribed

func updateSubscriptionStatus() async {
    // 替换成你自己的订阅产品ID
    guard let subscriptionProduct = try? await Product.products(for: ["com.yourapp.premium"]).first else {
        isSubscribed = false
        return
    }
    
    do {
        let subscriptionInfo = try await subscriptionProduct.subscriptionInfo
        let statuses = try await subscriptionInfo.statuses
        
        // 判断是否有活跃的订阅:状态为active,且过期时间在当前时间之后
        isSubscribed = statuses.contains { status in
            switch status.state {
            case .active, .inBillingRetry, .inGracePeriod:
                // 这几种状态下用户都有权益
                return status.expirationDate > Date()
            default:
                return false
            }
        }
    } catch {
        print("获取订阅状态失败:\(error.localizedDescription)")
        isSubscribed = false
    }
}

这里要注意:除了.active状态,.inBillingRetry(扣费失败重试中)、.inGracePeriod(宽限期内)这两种状态下,用户的权益也是有效的,不能直接判定为未订阅。

2. 被动监听交易变更

当有交易发生时(比如用户刚订阅、退款成功),重新查询状态更新isSubscribed

func setupTransactionListener() {
    Task {
        for await update in Transaction.updates {
            do {
                let transaction = try update.payloadValue
                if case .verified(let verifiedTx) = transaction {
                    // 完成交易(必须调用,否则会重复处理)
                    await verifiedTx.finish()
                    // 交易变更后,重新同步状态
                    await updateSubscriptionStatus()
                }
            } catch {
                print("处理交易更新失败:\(error.localizedDescription)")
            }
        }
    }
}

3. 主动触发状态检查的场景

为了避免isSubscribed和实际状态脱节,你需要在以下关键节点主动调用updateSubscriptionStatus()

  • App启动/冷启动完成时
  • App从后台切换到前台时
  • 用户尝试执行付费操作前(比如点击“解锁高级功能”按钮)
  • 定期检查(比如每5-10分钟一次,根据你的业务需求调整频率,别太频繁就行)

针对你测试场景的说明

你在测试中取消自动续订、等了1-2分钟后,订阅已经过期但isSubscribed还是true——这是因为你只靠Transaction.updates,而它不会触发过期事件。此时你只需要主动调用updateSubscriptionStatus(),它会通过SubscriptionInfo发现订阅已经过期,自动把isSubscribed设为false。


最后总结

  1. 别再只依赖Transaction.updates维护权益状态,它覆盖不了过期场景;
  2. 核心判断逻辑必须基于Product.SubscriptionInfo.Status
  3. 结合“主动查询(关键节点+定期)+ 被动监听(交易变更)”的模式,才能让isSubscribed和用户实际权益100%同步。

按照这个方案实现,你就不会再遇到用户取消订阅过期后还能使用付费功能的问题了!

火山引擎 最新活动