认识 StoreKit 2
Meet StoreKit 2
2021年6月10日
一句话判断
StoreKit 2 是 IAP(In-App Purchase)开发体验的全面翻新 —— 用 async/await + Swift 原生 API 替代了 delegate 回调地狱,用 JWS 签名的事务替代了不可读的收据(Receipt),整个内购模块的代码量能砍掉三分之二。
这场 Session 讲了什么
StoreKit 2 是苹果对 In-App Purchase 框架的完全重写。基于 Swift 5.5 的 async/await 和新类型系统,重新设计了所有内购相关的 API。
核心变化:Product 类型替代了 SKProduct,用 async 方法获取产品信息;Transaction 替代了 SKPaymentTransaction,每笔交易都带 JWS 签名,可以在本地验证真伪;Product.purchase() 方法用一个调用完成整个购买流程(展示系统对话框、处理支付、返回结果);订阅状态通过 Transaction.currentEntitlement 直接查询,不再需要解析收据。
还介绍了 App Store Server API(服务端 API)和 App Store Server Notifications V2,让服务端内购验证也变得更简单。
值得深挖的点
JWS 签名的 Transaction 验证。每笔 StoreKit 2 的交易都是一个 JWS(JSON Web Signature),包含交易详情和苹果的签名。你可以在本地用苹果的根证书验证签名,不需要发送到苹果服务器验证收据。这大幅简化了验证流程:以前需要把 Base64 编码的收据 POST 到苹果服务器、解析返回的 JSON、检查各种字段;现在直接 try Transaction.verify(signedTransaction) 一行搞定。
Transaction 的监听和更新。Transaction.updates 是一个 AsyncSequence,实时推送交易状态变更(购买完成、订阅续期、退款、家庭共享等)。这替代了旧的 SKPaymentQueue.addTransactionObserver 模式。你只需要一个 for await 循环就能处理所有交易更新。
代码片段
获取产品信息并购买:
import StoreKit
// 获取产品列表
func loadProducts() async throws -> [Product] {
let productIDs = ["com.app.premium_monthly", "com.app.premium_yearly"]
return try await Product.products(for: productIDs)
}
// 购买产品(一行完成整个流程)
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
// 验证交易签名
let transaction = try checkVerified(verification)
// 解锁内容
unlockPremium(transaction)
// 完成交易(必须调用,否则会一直出现在队列中)
await transaction.finish()
return transaction
case .userCancelled:
print("用户取消购买")
return nil
case .pending:
print("交易待处理(如需要家长批准)")
return nil
@unknown default:
return nil
}
}
// 验证交易的 JWS 签名
func checkVerified(_ result: VerificationResult<Transaction>) throws -> Transaction {
switch result {
case .unverified:
throw StoreError.failedVerification
case .verified(let transaction):
return transaction
}
}
监听交易更新:
import StoreKit
// 在 app 启动时开始监听交易
func listenForTransactions() -> Task<Void, Never> {
Task.detached {
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
// 处理交易更新(续期、退款等)
self.updateSubscriptionStatus(transaction)
await transaction.finish()
} catch {
print("交易验证失败: \(error)")
}
}
}
}
查询订阅状态:
import StoreKit
func checkSubscriptionStatus() async {
let productID = "com.app.premium_monthly"
// 获取当前有效的权益
guard let result = await Transaction.currentEntitlement(for: productID) else {
print("没有有效订阅")
return
}
do {
let transaction = try checkVerified(result)
// 检查订阅是否仍在有效期内
if transaction.expirationDate ?? Date.distantPast > Date.now {
print("订阅有效,到期时间: \(transaction.expirationDate!)")
} else {
print("订阅已过期")
}
} catch {
print("验证失败: \(error)")
}
}
最佳实践
- 新项目直接用 StoreKit 2,旧项目渐进迁移。StoreKit 1 和 StoreKit 2 可以共存,但不要在同一购买流程中混用两者。
- 每笔交易的
finish()必须在验证和解锁完成后调用。调用过早会导致用户付了钱但没拿到内容;调用过晚会导致交易反复出现在更新队列中。 - 服务端也要升级到 Server API V2。V2 的签名通知比 V1 的纯 JSON 通知安全得多,JWS 验证防止了通知伪造。
还有什么值得关注
- StoreKit 2 支持了家庭共享(Family Sharing),
Transaction中可以直接检查是否来自家庭成员。 Product.SubscriptionInfo.Status提供了订阅状态的完整快照(活跃、过期、宽限期、计费重试期)。- Xcode 13 的 StoreKit Testing 支持在模拟器中完整测试内购流程,不需要沙盒账号。