StoreKit 2 (SK2)
StoreKit 2 (SK2)
Apple 于 iOS 15 (2021) 推出的现代 IAP (In-App Purchase) 框架。
基于 Swift async/await,替代 StoreKit 1 的 delegate/callback 模式。
与 Apple Commerce API 配合实现完整客户端+服务端支付方案。
为什么用 SK2
| 对比维度 | StoreKit 1 | StoreKit 2 |
| 语言 | Objective-C / Swift | Swift only |
| 异步模型 | delegate / completion handler | async/await |
| 交易验证 | 需要 App Receipt + 服务端 verifyReceipt | 本地 JWS 签名,Transaction 自带验证 |
| 订阅状态 | SKProduct / SKPaymentQueue | Product.SubscriptionInfo / SubscriptionStatus |
| 最低版本 | iOS 3+ | iOS 15+ / macOS 12+ |
| 测试 | Sandbox + StoreKit Config | StoreKit Configuration File (Xcode 本地) |
结论:新项目直接用 SK2,除非需要支持 iOS 14 及以下。
核心类型
Product
// 查询商品
let products = try await Product.products(for: ["com.app.premium_monthly", "com.app.premium_yearly"])
for product in products {
print(product.displayName) // 显示名称
print(product.displayPrice) // 本地化价格字符串 "¥18.00"
print(product.type) // .autoRenewable / .nonRenewable / .consumable / .nonConsumable
}
Transaction
// 监听交易更新(替代 SKPaymentQueue.add(observer))
Task.detached {
for await result in Transaction.updates {
guard case .verified(let transaction) = result else { continue }
// 处理已验证的交易
await deliverContent(transaction)
await transaction.finish() // 必须调用!否则系统会一直重试
}
}
购买流程
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase()
switch result {
case .success(let verification):
// SK2 自动做客户端验证(检查 JWS 签名)
let transaction = try checkVerified(verification)
await deliverContent(transaction)
await transaction.finish()
return transaction
case .userCancelled:
return nil
case .pending:
// 交易需要审批(家长控制 / Ask to Buy)
return nil
@unknown default:
return nil
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified(_, let error):
throw error
case .verified(let safe):
return safe
}
}
获取当前交易历史
// 获取当前用户的所有已购买交易
var transactions: [Transaction] = []
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
transactions.append(transaction)
}
}
订阅管理
查询订阅状态
let product = try await Product.products(for: ["com.app.premium"]).first!
let status = try await product.subscription?.status
for state in status ?? [] {
switch state.state {
case .subscribed:
// 已订阅
case .expired:
// 已过期
case .inGracePeriod:
// 宽限期(Apple 尝试扣款中)
case .inBillingRetryPeriod:
// 账单重试期
case .revoked:
// 已撤销(退款)
default:
break
}
}
订阅优惠
// 介绍性优惠(Introductory Offer)— 自动应用,无需代码
// 促销优惠(Promotional Offer)— 需要服务端签名
let signature = try await fetchPromoOfferSignature(
productID: product.id,
offerID: "promo_50_off"
)
let result = try await product.purchase(
options: [.promotionalOffer(offerID: "promo_50_off", keyID: "ABC123", nonce: signature.nonce, signature: signature.signature, timestamp: signature.timestamp)]
)
退款请求
// iOS 15+ 让用户在 App 内发起退款
let result = try await AppStore.requestRefund(for: transaction.id)
// result: .success / .userCancelled
测试
Xcode StoreKit Configuration File
- File → New → File → StoreKit Configuration File
- 在 ASC (App Store Connect) 中同步产品,或手动添加
- Scheme → Options → StoreKit Configuration 选择该文件
- 运行 App → 直接测试购买流程,不需要 Sandbox 账号
优势: 完全本地,秒级响应,可模拟各种场景(退款、订阅过期、家庭共享等)。
Sandbox / TestFlight
- 需要创建 Sandbox 测试账号
- 测试生产环境行为
- 交易时间加速(1个月 → 5分钟等)
服务端通知集成
SK2 交易使用 JWS (JSON Web Signature) 签名,可以直接发送到服务端验证:
// 获取 JWS 编码的交易令牌,发送给服务端
let jwsRepresentation = transaction.jsonRepresentation // String (JWS)
// POST 到你的服务端,服务端用 [[Apple Commerce API]] 验证
常见陷阱
- 必须调用
transaction.finish()— 否则系统无限重试,用户无法重新购买 - 订阅状态 ≠ 交易存在 — 用
Transaction.currentEntitlements判断权益,不要自己维护数据库状态 - JWS 验证不是可选的 — 即使 SK2 自动验证了,服务端仍应独立验证
Transaction.updates是 AsyncSequence — 需要在合适的生命周期创建 Task,避免内存泄漏- consumable 不在
currentEntitlements中 — 消耗品需要自己管理消耗状态 - 测试环境时间线不同 — Sandbox 订阅周期大幅缩短(月→5min),不要假设真实时间
- SK2 和 SK1 不能混用 — 同一个 App 要么全部 SK2,要么全部 SK1,混用会导致交易丢失
迁移路径 (SK1 → SK2)
SKPaymentQueue.add(observer) → Transaction.updates (AsyncSequence)
SKPaymentQueue.default.add(payment) → product.purchase()
SKReceiptRefreshRequest → 不再需要 App Receipt
verifyReceipt (服务端) → Transaction.jsonRepresentation + 服务端 JWS 验证
SKProduct.subscriptionPeriod → Product.SubscriptionInfo