storekit

storekit

热门

使用 StoreKit 2 实现、审查或改进应用内购买和订阅。适用于使用 SubscriptionStoreView 或 ProductView 构建付费墙、使用 Product 和 Transaction API 处理交易、验证权益、处理购买流程(消耗型、非消耗型、自动续期)、实现优惠码或促销/赢回/ introductory 优惠、管理订阅状态和续期状态、使用配置文件设置 StoreKit 测试,或集成家庭共享、请求购买、退款处理和账单重试逻辑。

931Star
0Fork
更新于 2026/7/26
SKILL.md
readonly只读
name
storekit
description

使用 StoreKit 2 实现、审查或改进应用内购买和订阅。适用于使用 SubscriptionStoreView 或 ProductView 构建付费墙、使用 Product 和 Transaction API 处理交易、验证权益、处理购买流程(消耗型、非消耗型、自动续期)、实现优惠码或促销/赢回/ introductory 优惠、管理订阅状态和续期状态、使用配置文件设置 StoreKit 测试,或集成家庭共享、请求购买、退款处理和账单重试逻辑。

StoreKit 2 应用内购买和订阅

使用 StoreKit 2 实现应用内购买、订阅、付费墙和 StoreKit 测试。使用基于 Swift 的现代 ProductTransactionPurchaseActionStoreViewSubscriptionStoreView API。除非需要支持旧版操作系统,否则避免使用原始的应用内购买 API(SKProductSKPaymentQueue)。

StoreKit 视图会自动发起购买。对于自定义控件,在 SwiftUI 中使用 PurchaseAction,在 UIKit/AppKit 中使用 purchase(confirmIn:options:),在 watchOS 中使用 product.purchase(options:)

目录

产品类型

类型 枚举值 行为
消耗型 .consumable 一次性使用,可重复购买(宝石、金币)
非消耗型 .nonConsumable 永久购买一次(高级解锁)
自动续期 .autoRenewable 自动续费的定期计费
非续期 .nonRenewing 限时访问,不自动续费

加载产品

将产品 ID 定义为常量。使用 Product.products(for:) 获取产品。

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

购买流程

对于标准付费墙,优先使用 StoreKit 视图,因为它们会自动处理购买、恢复购买和显示政策控件。对于自定义 SwiftUI 购买按钮,优先使用环境中的 PurchaseAction。在 watchOS 上使用直接的 product.purchase(options:),在 UIKit 或 AppKit 中使用 purchase(confirmIn:options:)。始终处理每个 PurchaseResult,在访问前验证,持久化交付,然后完成。

@Environment(\.purchase) private var purchase

func purchaseProduct(_ product: Product) async throws {
    let result = try await purchase(product, 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:
        // 请求购买或延迟批准:显示待处理 UI,暂不解锁。
        showPendingApprovalMessage()
    @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 监听器

在应用启动时启动,而不是在付费墙出现时。捕获来自其他设备的购买、家庭共享更改、续期、请求购买批准、退款、撤销以及 Apple 在启动后立即发出的未完成交易。保持任务在应用生命周期内持续。

@main
struct MyApp: App {
    private let transactionListener: Task<Void, Never>

    init() {
        transactionListener = Self.listenForTransactions()
    }

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

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

权益检查

Transaction.currentEntitlements 会发出非消耗型、活跃或宽限期内的自动续期订阅,以及最新的非续期订阅交易(包括已完成的)。它排除消耗型以及已退款或已撤销的产品。单独跟踪消耗型的交付,并在授予访问权限前对非续期订阅应用应用的过期策略。

@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 {
                if transaction.productType == .nonRenewing,
                   transaction.expirationDate.map({ $0 <= .now }) ?? true {
                    continue
                }
                purchased.insert(transaction.productID)
            }
        }
        purchasedProductIDs = purchased
    }
}

SwiftUI .currentEntitlementTask 修饰符

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(.some(.verified(let transaction))) where transaction.revocationDate == nil:
                PremiumContentView()
            case .success:
                PaywallView()
            }
        }
        .currentEntitlementTask(for: ProductID.premium) { state in
            self.state = state
        }
    }
}

SubscriptionStoreView (iOS 17+)

用于订阅付费墙的内置 SwiftUI 视图。自动处理产品加载、购买 UI 和恢复购买。

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(.success(.verified(let transaction))) = result {
            await deliverContent(for: transaction)
            await transaction.finish()
        }
    }

自定义营销内容

使用 SubscriptionStoreView 控件样式 中的容器背景和头部模式。

分层布局

使用 SubscriptionOptionGroupSubscriptionOptionSectionSubscriptionPeriodGroupSet 组织 iOS 18+ 选项;参见 订阅组管理

StoreView (iOS 17+)

展示多个产品,包含本地化名称、价格和购买按钮。

StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
    .productViewStyle(.large)
    .storeButton(.visible, for: .restorePurchases)
    .onInAppPurchaseCompletion { product, result in
        if case .success(.success(.verified(let transaction))) = result {
            await deliverContent(for: transaction)
            await transaction.finish()
        }
    }

单个产品的 ProductView

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)

订阅状态检查

func checkSubscriptionActive(groupID: String) async throws -> Bool {
    let statuses = try await Product.SubscriptionInfo.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
}

续期状态

状态 含义
.subscribed 订阅活跃
.expired 订阅已过期
.inBillingRetryPeriod 支付失败,Apple 正在重试
.inGracePeriod 支付失败,但宽限期内仍可访问
.revoked Apple 已退款或撤销订阅

恢复购买

StoreKit 2 通过 Transaction.currentEntitlements 处理恢复。添加恢复按钮或显式调用 AppStore.sync()

func restorePurchases() async throws {
    try await AppStore.sync()
    await StoreManager.shared.updateEntitlements()
}

在商店视图上:.storeButton(.visible, for: .restorePurchases)

应用交易(应用购买验证)

验证应用安装的合法性。用于商业模式变更或检测篡改安装(iOS 16+)。

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 { /* 无法获取应用交易 */ }
}

购买选项

// 用于服务器端对账的应用账户令牌
try await product.purchase(options: [.appAccountToken(UUID())])

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

// 在沙盒中模拟请求购买
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])

SwiftUI 购买回调

.onInAppPurchaseStart { product in
    await analytics.trackPurchaseStarted(product.id)
}
.onInAppPurchaseCompletion { product, result in
    if case .success(.success(.verified(let transaction))) = result {
        await deliverContent(for: transaction)
        await transaction.finish()
    }
}
.inAppPurchaseOptions { product in
    [.appAccountToken(userAccountToken)]
}

常见错误

1. 未在应用启动时启动 Transaction.updates

// 错误:没有监听器——错过续期、退款、请求购买批准
@main struct MyApp: App {
    var body: some Scene { WindowGroup { ContentView() } }
}
// 正确:在 App init 中启动监听器(参见上面的 Transaction.updates 部分)

2. 忘记调用 transaction.finish()

// 错误:从未完成——永远出现在未完成队列中
let transaction = try checkVerified(verification)
unlockFeature(transaction.productID)

// 正确:先持久化交付,然后完成。如果交付失败,暂不完成。
let transaction = try checkVerified(verification)
try await recordDelivery(transaction)
await transaction.finish()

3. 忽略验证结果

// 错误:使用未验证的交易——安全风险
let transaction = verification.unsafePayloadValue

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

4. 在新的 StoreKit 2 代码中使用原始的应用内购买 API

// 避免:原始的应用内购买 API
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)

// 推荐:StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()

5. 未检查 revocationDate

// 错误:对已退款购买授予访问权限
if case .verified(let transaction) = result {
    purchased.insert(transaction.productID)
}

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

6. 硬编码价格

// 错误:对其他货币和地区不正确
Text("Buy Premium for $4.99")

// 正确:使用 Product 的本地化价格
Text("Buy \(product.displayName) for \(product.displayPrice)")

7. 未处理 .pending 购买结果

// 错误:静默丢弃待处理的请求购买
default: break

// 正确:说明批准待处理;仅在 Transaction.updates 后解锁
case .pending:
    showPendingApprovalMessage()

8. 仅在启动时检查一次权益

// 错误:只检查一次,从不更新
func appDidFinish() { Task { await updateEntitlements() } }

// 正确:在 Transaction.updates 和返回前台时重新检查
// Transaction.updates 监听器处理会话中的更改。
// 同时在内容视图上使用 .task { await storeManager.updateEntitlements() }。

9. 缺少恢复购买按钮

// 错误:没有恢复选项——有被 App Store 拒绝的风险
SubscriptionStoreView(groupID: "group_id")

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

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

// 错误:没有条款或隐私政策
SubscriptionStoreView(groupID: "group_id")

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

审查清单

  • [ ] Transaction.updates 监听器在应用启动时于 App init 中启动
  • [ ] 所有交易在授予访问权限前已验证
  • [ ] 仅在持久化内容交付后调用 transaction.finish()
  • [ ] 排除已撤销/已退款的交易,并更新权益状态
  • [ ] .pending 结果显示请求购买/延迟批准反馈
  • [ ] 恢复购买按钮在付费墙和商店视图上可见
  • [ ] 订阅视图上包含服务条款和隐私政策链接
  • [ ] 使用 product.displayPrice 显示价格,绝不硬编码
  • [ ] 订阅条款(价格、时长、续期)清晰显示
  • [ ] 免费试用期后明确显示定价
  • [ ] 除非需要支持旧版操作系统,否则不使用原始的应用内购买 API(SKProductSKPaymentQueue
  • [ ] 产品 ID 定义为常量,而非分散的字符串
  • [ ] StoreKit 测试涵盖促销优惠、赢回、优惠码、请求购买、续期、退款和撤销
  • [ ] 在 Transaction.updates 和应用进入前台时重新检查权益
  • [ ] 如果适用,服务器端验证使用 jwsRepresentation
  • [ ] 消耗型及时交付并完成
  • [ ] 当跨并发边界共享时,交易观察者类型和产品模型类型是 Sendable

参考资料