StoreKit 2:如何通过.subscriptionStatusTask识别订阅组内有效订阅
问题
我用SubscriptionStoreView搭配.onInAppPurchaseCompletion、.subscriptionStatusTask、.manageSubscriptionSheet实现同一订阅组内的月付、年付自动续期订阅(无其他内购类型)。
现在卡在.subscriptionStatusTask的使用上:怎么在这个闭包里准确识别用户实际购买的订阅?之前我通过返回值映射,读取transaction.payloadValue判断,但当月付升级年付后,transaction.payloadValue.productID还是显示月付(推测是月付尚未到期),但我的应用需要根据用户是否购买年付来开放更多功能,仅靠.subscriptionStatusTask该怎么实现?
补充测试现象:月付升级年付后,prodId仍显示月付,但Debug→StoreKit→Manage Transactions里显示月付将于9-7-24到期,年付会续订;取消年付时,提示可使用至2024年9月7日,取消后交易记录显示月付但不再续订。
还有几个疑问:
- 通过
.manageSubscriptionSheet变更订阅时,购买是否即时生效? - 是否需要读取续订信息来提前开放年付功能,用户取消续订则恢复月付权限?
- 还有哪些要点我未考虑到?
附测试代码:
struct ContentViewSO2: View { @State private var paywallShown = false @State private var isPro = false @State private var presentingSubscriptionSheet = false @State private var showManageSubscriptionButton = false @State private var subType = "" var body: some View { VStack(spacing: 75){ Text("Testing Store Paywall and Subscription Status Sheet") .multilineTextAlignment(.center) .foregroundColor(.black) .font(.system(size: 18, weight: .heavy, design: .rounded)) .padding(.vertical, 75) .subscriptionStatusTask(for: "C5FF4E1D") { taskState in if let value = taskState.value { //.. check to see if there was EVER a subscription in order to know whether or not to present the .manageSubscriptionSheet. It seems that if there was never a subscription, the .manageSubscriptionSheet will not appear... it just clocks if value.count > 0 { showManageSubscriptionButton = true } else { showManageSubscriptionButton = false } let _: [()] = value.map { status in //.. transaction = what's currently purchased??????? let statusTransaction = status.transaction let payloadValue = try? status.transaction.payloadValue let prodId = payloadValue?.productID print("product id = \(prodId)") if prodId == "com.HR.test.yearly" { subType = "yearly" } else if prodId == "com.HR.test.monthly" { subType = "monthly" } else { subType = "none" } //.. if there is a value that's not .revoked and not .expired isPro = !value .filter { $0.state != .revoked && $0.state != .expired } .isEmpty //.. for more debugging let renewalState = status.state switch renewalState { case .revoked: print("renewalState = revoked") case .expired: print("renewalState = expired") case .subscribed: print("renewalState = subscribed") case .inBillingRetryPeriod: print("renewalState = inBillingRetryPeriod") case .inGracePeriod: print("renewalState = inGracePeriod") default: print("renewalState = ???") } } } } isPro ? Text("IS PRO FROM .subscriptionStatusTask") .foregroundColor(.white) .background(Color.green) : Text("IS NOT PRO FROM .subscriptionStatusTask") .foregroundColor(.white) .background(Color.red) switch subType { case "monthly": Text("Monthly Subscription") .padding(.horizontal, 25) .foregroundColor(.white) .background(Color.blue) case "yearly": Text("Yearly Subscription") .padding(.horizontal, 25) .foregroundColor(.white) .background(Color.teal) default: Text("No Subscription") .padding(.horizontal, 25) .foregroundColor(.white) .background(Color.red) } //.. check to see if there was EVER a subscription in order to know whether to present the .manageSubscriptionSheet. It seems that if there was never a subscription, the .manageSubscriptionSheet will not appear... it just clocks. If there has not been any subscription EVER, show paywall instead of manage subscription sheet if !showManageSubscriptionButton { Button(action: { paywallShown = true }, label: { Text("Press to test paywall") }) .buttonStyle(.borderedProminent) .sheet(isPresented: $paywallShown) { HRShop() } } else { Button(action: { self.presentingSubscriptionSheet = true }, label: { Text("Show .manageSubScriptionSheet") }) .manageSubscriptionsSheet( isPresented: $presentingSubscriptionSheet, subscriptionGroupID: "C5FF4E1D" ) } } Spacer() } }
解决方案
1. 优先读取latestSubscriptionInfo而非当前交易
用户升级/降级订阅时,当前生效的订阅未到期,但StoreKit已记录下一次续订的产品信息。要从SubscriptionStatus的latestSubscriptionInfo字段获取用户当前的订阅意愿(即将生效的产品),而不是仅依赖当前交易的payloadValue.productID。
修改后的.subscriptionStatusTask逻辑:
.subscriptionStatusTask(for: "C5FF4E1D") { taskState in guard let statuses = taskState.value else { showManageSubscriptionButton = false isPro = false subType = "none" return } showManageSubscriptionButton = !statuses.isEmpty // 筛选有效订阅状态(未过期、未撤销) let activeStatuses = statuses.filter { $0.state != .revoked && $0.state != .expired } isPro = !activeStatuses.isEmpty // 遍历有效状态,优先用latestSubscriptionInfo判断订阅类型 for status in activeStatuses { if let latestInfo = status.latestSubscriptionInfo { let prodId = latestInfo.productID print("Latest subscription product ID: \(prodId)") subType = prodId == "com.HR.test.yearly" ? "yearly" : "monthly" } else { // 无续订计划时,回退到当前交易的产品ID if let payloadValue = try? status.transaction.payloadValue { subType = payloadValue.productID == "com.HR.test.yearly" ? "yearly" : "monthly" } else { subType = "none" } } } // 调试订阅状态 for status in statuses { switch status.state { case .revoked: print("renewalState = revoked") case .expired: print("renewalState = expired") case .subscribed: print("renewalState = subscribed") case .inBillingRetryPeriod: print("renewalState = inBillingRetryPeriod") case .inGracePeriod: print("renewalState = inGracePeriod") default: print("renewalState = ???") } } }
2. 订阅变更的即时性与权限处理
- 即时开放权限:用户通过
.manageSubscriptionSheet完成升级后,latestSubscriptionInfo会立即更新为年付产品ID,可直接据此开放年付功能,无需等待当前月付到期。 - 取消续订后恢复权限:用户取消年付续订时,
latestSubscriptionInfo会变回月付产品ID,此时需将权限降级为月付级别。
3. 需补充的要点
- 状态优先级:存在多个订阅状态时,
latestSubscriptionInfo代表用户当前的订阅意愿,优先级高于当前生效的交易。 - 跨设备同步:
.subscriptionStatusTask会自动同步其他设备的订阅变更,逻辑需依赖StoreKit返回的状态,避免依赖本地缓存。 - 全场景测试:覆盖升级、降级、取消续订、账单重试期、宽限期等所有订阅状态,确保权限判断准确。
- 错误处理:当前代码中
try?会忽略解析错误,实际开发中需添加错误捕获,避免因交易解析失败导致状态判断错误。
内容的提问来源于stack exchange,提问作者KatM
相关产品推荐
相关产品推荐

