如何在飞行模式/离线状态下处理StoreKit 2内购逻辑?
解决StoreKit 2离线状态下非消耗型内购功能失效问题
问题背景
为iOS应用添加首个非消耗型内购时,发现StoreKit 2在离线/飞行模式下无法正常工作。原本通过store.purchasedItems.isEmpty判断是否展示高级功能,但该逻辑在离线时完全失效——核心原因是Transaction.currentEntitlements需要在线向App Store完成验证,离线状态下无法获取已购项目列表,导致purchasedItems为空。
解决方案:本地持久化+StoreKit验证结合
可以用@AppStorage(或UserDefaults)存储购买状态的本地缓存,同时保留StoreKit的在线验证逻辑,确保离线时能读取本地状态,在线时同步最新的官方验证结果。
具体实现步骤
- 添加本地状态存储
在Store类中加入@AppStorage变量,用来缓存用户的购买状态:
import Foundation import StoreKit import SwiftUI // 需导入SwiftUI才能使用@AppStorage typealias Transaction = StoreKit.Transaction public enum StoreError: Error { case failedVerification } class Store: ObservableObject { @Published private(set) var items: [Product] = [] @Published private(set) var purchasedItems: [Product] = [] // 新增:本地缓存购买状态(替换为你的内购ID对应的标识) @AppStorage("hasPurchasedPremium") private var hasPurchasedPremium = false var updateListenerTask: Task<Void, Error>? = nil // 原有init、deinit等代码保持不变 }
- 在关键时机更新本地状态
需要在以下场景同步更新hasPurchasedPremium:
- 购买成功后
- 启动时StoreKit验证完成后
- 交易状态更新时
修改对应方法:
// 修改purchase方法,购买成功后同步本地状态 func purchase(_ product: Product) async throws -> Transaction? { let result = try await product.purchase() switch result { case .success(let verification): let transaction = try checkVerified(verification) await updateCustomerProductStatus() // 匹配你的内购ID,更新本地状态 if transaction.productID == "你的内购ID" { hasPurchasedPremium = true } await transaction.finish() return transaction case .userCancelled, .pending: return nil default: return nil } } // 修改updateCustomerProductStatus方法,验证后同步本地状态 @MainActor func updateCustomerProductStatus() async { var purchasedItems: [Product] = [] var localPurchaseStatus = false for await result in Transaction.currentEntitlements { do { let transaction = try checkVerified(result) // 跳过已撤销的内购 guard transaction.revocationDate == nil else { continue } if let product = items.first(where: { $0.id == transaction.productID }) { purchasedItems.append(product) if product.id == "你的内购ID" { localPurchaseStatus = true } } } catch { print("验证失败: \(error)") } } self.purchasedItems = purchasedItems hasPurchasedPremium = localPurchaseStatus } // 修改listenForTransactions方法,交易更新时同步本地状态 func listenForTransactions() -> Task<Void, Error> { return Task.detached { for await result in Transaction.updates { do { let transaction = try self.checkVerified(result) await self.updateCustomerProductStatus() // 处理撤销场景 if transaction.productID == "你的内购ID" { await MainActor.run { self.hasPurchasedPremium = transaction.revocationDate == nil } } await transaction.finish() } catch { print("交易处理失败: \(error)") } } } }
- 修改功能判断逻辑
原有用store.purchasedItems.isEmpty的地方,改为优先判断本地缓存,同时保留在线验证的兜底:
// 在UI中判断是否展示高级功能 if store.hasPurchasedPremium || !store.purchasedItems.isEmpty { // 展示高级功能 } else { // 展示购买入口 }
注意事项
- 防篡改:本地缓存仅用于离线场景,在线时必须以StoreKit的
Transaction.currentEntitlements验证结果为准,避免用户通过修改本地数据获取权限。 - 恢复购买:添加恢复购买方法,支持用户切换设备或重装APP后恢复权限:
func restorePurchases() async { await updateCustomerProductStatus() }
- 撤销处理:如果用户内购被退款或撤销,
Transaction.revocationDate会有值,需同步将本地状态设为false。
内容的提问来源于stack exchange,提问作者Alexnnd
相关产品推荐
相关产品推荐

