Meet StoreKit 2
App Store & Distribution 进阶 28m

认识 StoreKit 2

Meet StoreKit 2

2021年6月10日

在 Apple 官方观看视频

一句话判断

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)")
    }
}

最佳实践

  1. 新项目直接用 StoreKit 2,旧项目渐进迁移。StoreKit 1 和 StoreKit 2 可以共存,但不要在同一购买流程中混用两者。
  2. 每笔交易的 finish() 必须在验证和解锁完成后调用。调用过早会导致用户付了钱但没拿到内容;调用过晚会导致交易反复出现在更新队列中。
  3. 服务端也要升级到 Server API V2。V2 的签名通知比 V1 的纯 JSON 通知安全得多,JWS 验证防止了通知伪造。

还有什么值得关注

  • StoreKit 2 支持了家庭共享(Family Sharing),Transaction 中可以直接检查是否来自家庭成员。
  • Product.SubscriptionInfo.Status 提供了订阅状态的完整快照(活跃、过期、宽限期、计费重试期)。
  • Xcode 13 的 StoreKit Testing 支持在模拟器中完整测试内购流程,不需要沙盒账号。
WWDC 2021