← 返回藏书阁

StoreKit 2 (SK2)

wiki/ai/concepts/StoreKit-2.md
分类:ai / concepts · 更新:2026-06-14 00:33

StoreKit 2 (SK2)

Apple 于 iOS 15 (2021) 推出的现代 IAP (In-App Purchase) 框架。
基于 Swift async/await,替代 StoreKit 1 的 delegate/callback 模式。
Apple Commerce API 配合实现完整客户端+服务端支付方案。

为什么用 SK2

对比维度StoreKit 1StoreKit 2
语言Objective-C / SwiftSwift only
异步模型delegate / completion handlerasync/await
交易验证需要 App Receipt + 服务端 verifyReceipt本地 JWS 签名,Transaction 自带验证
订阅状态SKProduct / SKPaymentQueueProduct.SubscriptionInfo / SubscriptionStatus
最低版本iOS 3+iOS 15+ / macOS 12+
测试Sandbox + StoreKit ConfigStoreKit 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

  1. File → New → File → StoreKit Configuration File
  2. 在 ASC (App Store Connect) 中同步产品,或手动添加
  3. Scheme → Options → StoreKit Configuration 选择该文件
  4. 运行 App → 直接测试购买流程,不需要 Sandbox 账号

优势: 完全本地,秒级响应,可模拟各种场景(退款、订阅过期、家庭共享等)。

Sandbox / TestFlight

  • 需要创建 Sandbox 测试账号
  • 测试生产环境行为
  • 交易时间加速(1个月 → 5分钟等)

服务端通知集成

SK2 交易使用 JWS (JSON Web Signature) 签名,可以直接发送到服务端验证:

// 获取 JWS 编码的交易令牌,发送给服务端
let jwsRepresentation = transaction.jsonRepresentation  // String (JWS)
// POST 到你的服务端,服务端用 [[Apple Commerce API]] 验证

常见陷阱

  1. 必须调用 transaction.finish() — 否则系统无限重试,用户无法重新购买
  2. 订阅状态 ≠ 交易存在 — 用 Transaction.currentEntitlements 判断权益,不要自己维护数据库状态
  3. JWS 验证不是可选的 — 即使 SK2 自动验证了,服务端仍应独立验证
  4. Transaction.updates 是 AsyncSequence — 需要在合适的生命周期创建 Task,避免内存泄漏
  5. consumable 不在 currentEntitlements — 消耗品需要自己管理消耗状态
  6. 测试环境时间线不同 — Sandbox 订阅周期大幅缩短(月→5min),不要假设真实时间
  7. 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

参考资源