StoreKit 2下如何可靠检测订阅的过期与续订状态?
作为长期用StoreKit 2做订阅逻辑的开发者,我完全理解你遇到的困惑——一开始我也踩过只靠Transaction.updates的坑。咱们一步步拆解你的问题,再给你一套可靠的实现方案。
先理清几个核心误解(帮你踩坑)
首先得明确StoreKit 2里几个容易混淆的概念,这是你问题的根源:
- 取消自动续订 ≠ 订阅立即过期:用户取消自动续订后,当前订阅周期内的权益是完全有效的,只有当当前周期自然结束时,订阅才会真正过期、权益终止。StoreKit不会在取消时立刻收回权益,也不会在过期时主动发送交易事件(因为没有新的交易产生)。
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。
最后总结
- 别再只依赖
Transaction.updates维护权益状态,它覆盖不了过期场景; - 核心判断逻辑必须基于
Product.SubscriptionInfo.Status; - 结合“主动查询(关键节点+定期)+ 被动监听(交易变更)”的模式,才能让
isSubscribed和用户实际权益100%同步。
按照这个方案实现,你就不会再遇到用户取消订阅过期后还能使用付费功能的问题了!




