storekit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

StoreKit 2 In-App Purchases and Subscriptions

StoreKit 2 应用内购买与订阅

Implement in-app purchases, subscriptions, and paywalls using StoreKit 2 on iOS 26+. Use only the modern
Product
,
Transaction
,
StoreView
, and
SubscriptionStoreView
APIs. Never use the deprecated original StoreKit (
SKProduct
,
SKPaymentQueue
,
SKStoreReviewController
).
在 iOS 26+ 平台上使用 StoreKit 2 实现应用内购买、订阅和支付墙功能。仅允许使用现代化的
Product
Transaction
StoreView
SubscriptionStoreView
API,禁止使用已弃用的旧版 StoreKit 接口(
SKProduct
SKPaymentQueue
SKStoreReviewController
)。

Product Types

产品类型

TypeEnum CaseBehavior
Consumable
.consumable
Used once, can be repurchased (gems, coins)
Non-consumable
.nonConsumable
Purchased once permanently (premium unlock)
Auto-renewable
.autoRenewable
Recurring billing with automatic renewal
Non-renewing
.nonRenewing
Time-limited access without automatic renewal
类型枚举值行为特性
消耗型
.consumable
单次使用,可重复购买(如宝石、金币)
非消耗型
.nonConsumable
一次购买永久生效(如解锁高级功能)
自动续费型
.autoRenewable
定期扣费,自动续费
非续费型
.nonRenewing
限时访问权限,不会自动续费

Loading Products

加载产品

Define product IDs as constants. Fetch products with
Product.products(for:)
.
swift
import StoreKit

enum ProductID {
    static let premium = "com.myapp.premium"
    static let gems100 = "com.myapp.gems100"
    static let monthlyPlan = "com.myapp.monthly"
    static let yearlyPlan = "com.myapp.yearly"
    static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
}

let products = try await Product.products(for: ProductID.all)
for product in products {
    print("\(product.displayName): \(product.displayPrice)")
}
将产品ID定义为常量,通过
Product.products(for:)
方法获取产品信息。
swift
import StoreKit

enum ProductID {
    static let premium = "com.myapp.premium"
    static let gems100 = "com.myapp.gems100"
    static let monthlyPlan = "com.myapp.monthly"
    static let yearlyPlan = "com.myapp.yearly"
    static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
}

let products = try await Product.products(for: ProductID.all)
for product in products {
    print("\(product.displayName): \(product.displayPrice)")
}

Purchase Flow

购买流程

Call
product.purchase(options:)
and handle all three
PurchaseResult
cases. Always verify and finish transactions.
swift
func purchase(_ product: Product) async throws {
    let result = try await product.purchase(options: [
        .appAccountToken(userAccountToken)
    ])
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        await deliverContent(for: transaction)
        await transaction.finish()
    case .userCancelled:
        break
    case .pending:
        // Ask to Buy or deferred approval -- do not unlock content yet
        break
    @unknown default:
        break
    }
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .verified(let value): return value
    case .unverified(_, let error): throw error
    }
}
调用
product.purchase(options:)
方法,处理
PurchaseResult
的所有三种返回情况,始终记得验证并完成交易。
swift
func purchase(_ product: Product) async throws {
    let result = try await product.purchase(options: [
        .appAccountToken(userAccountToken)
    ])
    switch result {
    case .success(let verification):
        let transaction = try checkVerified(verification)
        await deliverContent(for: transaction)
        await transaction.finish()
    case .userCancelled:
        break
    case .pending:
        // 购买请求待审批或 deferred 状态 -- 暂不解锁内容
        break
    @unknown default:
        break
    }
}

func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
    switch result {
    case .verified(let value): return value
    case .unverified(_, let error): throw error
    }
}

Transaction.updates Listener

Transaction.updates 监听器

Start at app launch. Catches purchases from other devices, Family Sharing changes, renewals, Ask to Buy approvals, refunds, and revocations.
swift
@main
struct MyApp: App {
    private var transactionListener: Task<Void, Error>?

    init() {
        transactionListener = listenForTransactions()
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }

    func listenForTransactions() -> Task<Void, Error> {
        Task.detached {
            for await result in Transaction.updates {
                guard case .verified(let transaction) = result else { continue }
                await StoreManager.shared.updateEntitlements()
                await transaction.finish()
            }
        }
    }
}
在应用启动时启动监听器,可捕获其他设备的购买行为、家庭共享变更、续费、购买请求审批、退款、权限撤销等事件。
swift
@main
struct MyApp: App {
    private var transactionListener: Task<Void, Error>?

    init() {
        transactionListener = listenForTransactions()
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }

    func listenForTransactions() -> Task<Void, Error> {
        Task.detached {
            for await result in Transaction.updates {
                guard case .verified(let transaction) = result else { continue }
                await StoreManager.shared.updateEntitlements()
                await transaction.finish()
            }
        }
    }
}

Entitlement Checking

权限校验

Use
Transaction.currentEntitlements
for non-consumable purchases and active subscriptions. Always check
revocationDate
.
swift
@Observable
@MainActor
class StoreManager {
    static let shared = StoreManager()
    var purchasedProductIDs: Set<String> = []
    var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }

    func updateEntitlements() async {
        var purchased = Set<String>()
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result,
               transaction.revocationDate == nil {
                purchased.insert(transaction.productID)
            }
        }
        purchasedProductIDs = purchased
    }
}
使用
Transaction.currentEntitlements
校验非消耗型购买和有效订阅状态,始终检查
revocationDate
字段。
swift
@Observable
@MainActor
class StoreManager {
    static let shared = StoreManager()
    var purchasedProductIDs: Set<String> = []
    var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }

    func updateEntitlements() async {
        var purchased = Set<String>()
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result,
               transaction.revocationDate == nil {
                purchased.insert(transaction.productID)
            }
        }
        purchasedProductIDs = purchased
    }
}

SwiftUI .currentEntitlementTask Modifier

SwiftUI .currentEntitlementTask 修饰符

swift
struct PremiumGatedView: View {
    @State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading

    var body: some View {
        Group {
            switch state {
            case .loading: ProgressView()
            case .failure: PaywallView()
            case .success(let transaction):
                if transaction != nil { PremiumContentView() }
                else { PaywallView() }
            }
        }
        .currentEntitlementTask(for: ProductID.premium) { state in
            self.state = state
        }
    }
}
swift
struct PremiumGatedView: View {
    @State private var state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading

    var body: some View {
        Group {
            switch state {
            case .loading: ProgressView()
            case .failure: PaywallView()
            case .success(let transaction):
                if transaction != nil { PremiumContentView() }
                else { PaywallView() }
            }
        }
        .currentEntitlementTask(for: ProductID.premium) { state in
            self.state = state
        }
    }
}

SubscriptionStoreView (iOS 17+)

SubscriptionStoreView(iOS 17+)

Built-in SwiftUI view for subscription paywalls. Handles product loading, purchase UI, and restore purchases automatically.
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
    .subscriptionStoreControlStyle(.prominentPicker)
    .subscriptionStoreButtonLabel(.multiline)
    .storeButton(.visible, for: .restorePurchases)
    .storeButton(.visible, for: .redeemCode)
    .subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
    .subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
    .onInAppPurchaseCompletion { product, result in
        if case .success(.verified(let transaction)) = result {
            await transaction.finish()
        }
    }
SwiftUI 内置的订阅支付墙视图,可自动处理产品加载、购买UI和购买恢复功能。
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
    .subscriptionStoreControlStyle(.prominentPicker)
    .subscriptionStoreButtonLabel(.multiline)
    .storeButton(.visible, for: .restorePurchases)
    .storeButton(.visible, for: .redeemCode)
    .subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
    .subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
    .onInAppPurchaseCompletion { product, result in
        if case .success(.verified(let transaction)) = result {
            await transaction.finish()
        }
    }

Custom Marketing Content

自定义营销内容

swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    VStack {
        Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
        Text("Unlock Premium").font(.largeTitle.bold())
        Text("Access all features").foregroundStyle(.secondary)
    }
}
.containerBackground(.blue.gradient, for: .subscriptionStore)
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    VStack {
        Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
        Text("解锁高级版").font(.largeTitle.bold())
        Text("访问全部功能").foregroundStyle(.secondary)
    }
}
.containerBackground(.blue.gradient, for: .subscriptionStore)

Hierarchical Layout

分层布局

swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    SubscriptionPeriodGroupSet()
}
.subscriptionStoreControlStyle(.picker)
swift
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    SubscriptionPeriodGroupSet()
}
.subscriptionStoreControlStyle(.picker)

StoreView (iOS 17+)

StoreView(iOS 17+)

Merchandises multiple products with localized names, prices, and purchase buttons.
swift
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
    .productViewStyle(.large)
    .storeButton(.visible, for: .restorePurchases)
    .onInAppPurchaseCompletion { product, result in
        if case .success(.verified(let transaction)) = result {
            await transaction.finish()
        }
    }
可展示多个产品,自带本地化名称、价格和购买按钮。
swift
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
    .productViewStyle(.large)
    .storeButton(.visible, for: .restorePurchases)
    .onInAppPurchaseCompletion { product, result in
        if case .success(.verified(let transaction)) = result {
            await transaction.finish()
        }
    }

ProductView for Individual Products

单个产品的 ProductView

swift
ProductView(id: ProductID.premium) { iconPhase in
    switch iconPhase {
    case .success(let image): image.resizable().scaledToFit()
    case .loading: ProgressView()
    default: Image(systemName: "star.fill")
    }
}
.productViewStyle(.large)
swift
ProductView(id: ProductID.premium) { iconPhase in
    switch iconPhase {
    case .success(let image): image.resizable().scaledToFit()
    case .loading: ProgressView()
    default: Image(systemName: "star.fill")
    }
}
.productViewStyle(.large)

Subscription Status Checking

订阅状态检查

swift
func checkSubscriptionActive(groupID: String) async throws -> Bool {
    let statuses = try await Product.SubscriptionInfo.Status.status(for: groupID)
    for status in statuses {
        guard case .verified = status.renewalInfo,
              case .verified = status.transaction else { continue }
        if status.state == .subscribed || status.state == .inGracePeriod {
            return true
        }
    }
    return false
}
swift
func checkSubscriptionActive(groupID: String) async throws -> Bool {
    let statuses = try await Product.SubscriptionInfo.Status.status(for: groupID)
    for status in statuses {
        guard case .verified = status.renewalInfo,
              case .verified = status.transaction else { continue }
        if status.state == .subscribed || status.state == .inGracePeriod {
            return true
        }
    }
    return false
}

Renewal States

续费状态说明

StateMeaning
.subscribed
Active subscription
.expired
Subscription has expired
.inBillingRetryPeriod
Payment failed, Apple is retrying
.inGracePeriod
Payment failed but access continues during grace period
.revoked
Apple refunded or revoked the subscription
状态含义
.subscribed
订阅有效
.expired
订阅已过期
.inBillingRetryPeriod
支付失败,苹果正在重试扣款
.inGracePeriod
支付失败,宽限期内仍可正常使用功能
.revoked
苹果已退款或撤销订阅

Restore Purchases

恢复购买

StoreKit 2 handles restoration via
Transaction.currentEntitlements
. Add a restore button or call
AppStore.sync()
explicitly.
swift
func restorePurchases() async throws {
    try await AppStore.sync()
    await StoreManager.shared.updateEntitlements()
}
On store views:
.storeButton(.visible, for: .restorePurchases)
StoreKit 2 通过
Transaction.currentEntitlements
处理恢复逻辑,可添加恢复按钮或显式调用
AppStore.sync()
swift
func restorePurchases() async throws {
    try await AppStore.sync()
    await StoreManager.shared.updateEntitlements()
}
在商店视图上添加:
.storeButton(.visible, for: .restorePurchases)

App Transaction (App Purchase Verification)

App 交易(应用购买验证)

Verify the legitimacy of the app installation. Use for business model changes or detecting tampered installations (iOS 16+).
swift
func verifyAppPurchase() async {
    do {
        let result = try await AppTransaction.shared
        switch result {
        case .verified(let appTransaction):
            let originalVersion = appTransaction.originalAppVersion
            let purchaseDate = appTransaction.originalPurchaseDate
            // Migration logic for users who paid before subscription model
        case .unverified:
            // Potentially tampered -- restrict features as appropriate
            break
        }
    } catch { /* Could not retrieve app transaction */ }
}
验证应用安装的合法性,可用于业务模式变更或检测篡改安装包(iOS 16+)。
swift
func verifyAppPurchase() async {
    do {
        let result = try await AppTransaction.shared
        switch result {
        case .verified(let appTransaction):
            let originalVersion = appTransaction.originalAppVersion
            let purchaseDate = appTransaction.originalPurchaseDate
            // 针对订阅模式推出前付费的老用户做迁移逻辑
        case .unverified:
            // 可能是篡改安装包 -- 按需限制功能
            break
        }
    } catch { /* 无法获取应用交易信息 */ }
}

Purchase Options

购买选项

swift
// App account token for server-side reconciliation
try await product.purchase(options: [.appAccountToken(UUID())])

// Consumable quantity
try await product.purchase(options: [.quantity(5)])

// Simulate Ask to Buy in sandbox
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
swift
// 用于服务端对账的应用账号令牌
try await product.purchase(options: [.appAccountToken(UUID())])

// 消耗型商品购买数量
try await product.purchase(options: [.quantity(5)])

// 沙箱环境模拟购买请求
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])

SwiftUI Purchase Callbacks

SwiftUI 购买回调

swift
.onInAppPurchaseStart { product in
    return true  // Return false to cancel
}
.onInAppPurchaseCompletion { product, result in
    if case .success(.verified(let transaction)) = result {
        await transaction.finish()
    }
}
.inAppPurchaseOptions { product in
    [.appAccountToken(userAccountToken)]
}
swift
.onInAppPurchaseStart { product in
    return true  // 返回 false 可取消购买
}
.onInAppPurchaseCompletion { product, result in
    if case .success(.verified(let transaction)) = result {
        await transaction.finish()
    }
}
.inAppPurchaseOptions { product in
    [.appAccountToken(userAccountToken)]
}

Common Mistakes

常见错误

1. Not starting Transaction.updates at app launch

1. 应用启动时未启动 Transaction.updates 监听器

swift
// WRONG: No listener -- misses renewals, refunds, Ask to Buy approvals
@main struct MyApp: App {
    var body: some Scene { WindowGroup { ContentView() } }
}
// CORRECT: Start listener in App init (see Transaction.updates section above)
swift
// 错误:没有监听器 -- 会遗漏续费、退款、购买请求审批等事件
@main struct MyApp: App {
    var body: some Scene { WindowGroup { ContentView() } }
}
// 正确:在 App 初始化方法中启动监听器(参考上文 Transaction.updates 章节)

2. Forgetting transaction.finish()

2. 忘记调用 transaction.finish()

swift
// WRONG: Never finished -- reappears in unfinished queue forever
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)

// CORRECT: Always finish after delivering content
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
await transaction.finish()
swift
// 错误:未完成交易 -- 会一直出现在未完成交易队列中
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)

// 正确:交付内容后始终调用 finish 方法
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)
await transaction.finish()

3. Ignoring verification result

3. 忽略验证结果

swift
// WRONG: Using unverified transaction -- security risk
let transaction = verification.unsafePayloadValue

// CORRECT: Verify before using
let transaction = try checkVerified(verification)
swift
// 错误:使用未验证的交易 -- 存在安全风险
let transaction = verification.unsafePayloadValue

// 正确:使用前先验证
let transaction = try checkVerified(verification)

4. Using deprecated original StoreKit APIs

4. 使用已弃用的旧版 StoreKit API

swift
// WRONG: Original StoreKit (deprecated)
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
SKStoreReviewController.requestReview()

// CORRECT: StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
try await AppStore.requestReview(in: windowScene)
swift
// 错误:旧版 StoreKit(已弃用)
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
SKStoreReviewController.requestReview()

// 正确:使用 StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()
try await AppStore.requestReview(in: windowScene)

5. Not checking revocationDate

5. 未检查 revocationDate

swift
// WRONG: Grants access to refunded purchases
if case .verified(let transaction) = result {
    purchased.insert(transaction.productID)
}

// CORRECT: Skip revoked transactions
if case .verified(let transaction) = result, transaction.revocationDate == nil {
    purchased.insert(transaction.productID)
}
swift
// 错误:会给已退款的购买授予权限
if case .verified(let transaction) = result {
    purchased.insert(transaction.productID)
}

// 正确:跳过已撤销的交易
if case .verified(let transaction) = result, transaction.revocationDate == nil {
    purchased.insert(transaction.productID)
}

6. Hardcoding prices

6. 硬编码价格

swift
// WRONG: Wrong for other currencies and regions
Text("Buy Premium for $4.99")

// CORRECT: Localized price from Product
Text("Buy \(product.displayName) for \(product.displayPrice)")
swift
// 错误:不适用于其他货币和地区
Text("购买高级版仅需 $4.99")

// 正确:使用 Product 提供的本地化价格
Text("购买 \(product.displayName) 仅需 \(product.displayPrice)")

7. Not handling .pending purchase result

7. 未处理 .pending 购买结果

swift
// WRONG: Silently drops pending Ask to Buy
default: break

// CORRECT: Inform user purchase is awaiting approval
case .pending:
    showPendingApprovalMessage()
swift
// 错误:静默忽略待审批的购买请求
default: break

// 正确:告知用户购买正在等待审批
case .pending:
    showPendingApprovalMessage()

8. Checking entitlements only once at launch

8. 仅在启动时校验一次权限

swift
// WRONG: Check once, never update
func appDidFinish() { Task { await updateEntitlements() } }

// CORRECT: Re-check on Transaction.updates AND on foreground return
// Transaction.updates listener handles mid-session changes.
// Also use .task { await storeManager.updateEntitlements() } on content views.
swift
// 错误:仅校验一次,不会更新
func appDidFinish() { Task { await updateEntitlements() } }

// 正确:在 Transaction.updates 触发和应用回到前台时重新校验
// Transaction.updates 监听器会处理会话中的状态变更
// 同时在内容视图上使用 .task { await storeManager.updateEntitlements() }

9. Missing restore purchases button

9. 缺失恢复购买按钮

swift
// WRONG: No restore option -- App Store rejection risk
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(groupID: "group_id")
    .storeButton(.visible, for: .restorePurchases)
swift
// 错误:没有恢复选项 -- 有被 App Store 拒绝的风险
SubscriptionStoreView(groupID: "group_id")

// 正确写法
SubscriptionStoreView(groupID: "group_id")
    .storeButton(.visible, for: .restorePurchases)

10. Subscription views without policy links

10. 订阅视图没有政策链接

swift
// WRONG: No terms or privacy policy
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(groupID: "group_id")
    .subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
    .subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
swift
// 错误:没有服务条款和隐私政策链接
SubscriptionStoreView(groupID: "group_id")

// 正确写法
SubscriptionStoreView(groupID: "group_id")
    .subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
    .subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)

Review Checklist

审核检查清单

  • Transaction.updates
    listener starts at app launch in App init
  • All transactions verified before granting access
  • transaction.finish()
    called after content delivery
  • Revoked transactions excluded from entitlements
  • .pending
    purchase result handled for Ask to Buy
  • Restore purchases button visible on paywall and store views
  • Terms of Service and Privacy Policy links on subscription views
  • Prices shown using
    product.displayPrice
    , never hardcoded
  • Subscription terms (price, duration, renewal) clearly displayed
  • Free trial states post-trial pricing clearly
  • No original StoreKit APIs (
    SKProduct
    ,
    SKPaymentQueue
    )
  • Product IDs defined as constants, not scattered strings
  • StoreKit configuration file set up for testing
  • Entitlements re-checked on Transaction.updates and app foreground
  • Server-side validation uses
    jwsRepresentation
    if applicable
  • Consumables delivered and finished promptly
  • Transaction observer types and product model types are
    Sendable
    when shared across concurrency boundaries
  • Transaction.updates
    监听器在 App 初始化时启动
  • 所有交易在授予权限前都经过验证
  • 内容交付后调用
    transaction.finish()
  • 已撤销的交易不会被计入有效权限
  • 已处理
    .pending
    购买结果应对购买请求场景
  • 支付墙和商店页面显示恢复购买按钮
  • 订阅页面有服务条款和隐私政策链接
  • 价格通过
    product.displayPrice
    展示,没有硬编码
  • 订阅条款(价格、时长、续费规则)清晰展示
  • 免费试用明确说明试用后的定价
  • 没有使用旧版 StoreKit API(
    SKProduct
    SKPaymentQueue
  • 产品ID定义为常量,没有散落的字符串
  • 已配置 StoreKit 测试用的配置文件
  • 在 Transaction.updates 触发和应用回到前台时重新校验权限
  • 服务端验证按需使用
    jwsRepresentation
  • 消耗型商品及时交付并标记交易完成
  • 跨并发边界共享的交易观察者类型和产品模型类型遵循
    Sendable
    协议

Cross-References

交叉参考

  • See
    references/app-review-guidelines.md
    for IAP rules (Guideline 3.1.1), subscription display requirements, and rejection prevention.
  • See
    references/storekit-advanced.md
    for subscription control styles, offer management, testing patterns, and advanced subscription handling.
  • 查看
    references/app-review-guidelines.md
    了解应用内购买规则(指南3.1.1)、订阅展示要求和避免被拒的方法。
  • 查看
    references/storekit-advanced.md
    了解订阅控件样式、优惠管理、测试模式和高级订阅处理方案。