Storekit

作者 dpearson26998d90fd121a26无许可证1.1K 个星标收录于 2026年10月8日更新于 2026年10月8日仓库2个月前更新

Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.

AI 生成的概览

指导在 Apple 应用中实现、审查和测试 StoreKit 2 应用内购买与订阅。

功能
该技能提供使用 StoreKit 2 构建应用内购买和订阅的参考说明,涵盖产品加载、购买流程、交易监听、权益校验、付费墙视图、订阅状态、恢复购买以及应用交易验证。它包含代码示例、常见错误清单和审查清单,并附带两份关于 App Review 指南和 StoreKit 进阶主题的参考文档。它产出的是指导内容和代码示例,而非可执行产物。
适用场景
在编写或审查涉及应用内购买、订阅、付费墙、优惠码、促销或挽留优惠、家人共享、购买前询问、退款或账单重试的 Swift 代码时使用。设置 StoreKit 测试配置或检查订阅续订状态时也适用。
运行要求
不附带脚本,仅为说明和参考文档。使用其中的代码示例需要具备 Swift 和 StoreKit 2 的 Apple 平台开发环境,参考链接指向外部 Apple 文档。

StoreKit 2 In-App Purchases and Subscriptions

Implement in-app purchases, subscriptions, paywalls, and StoreKit testing using StoreKit 2. Use the modern Swift-based Product, Transaction, PurchaseAction, StoreView, and SubscriptionStoreView APIs. Avoid original In-App Purchase APIs (SKProduct, SKPaymentQueue) unless legacy OS support requires them.

StoreKit views initiate purchases automatically. For custom controls, use PurchaseAction in SwiftUI, purchase(confirmIn:options:) in UIKit/AppKit, and product.purchase(options:) on watchOS.

Contents

Product Types

TypeEnum CaseBehavior
Consumable.consumableUsed once, can be repurchased (gems, coins)
Non-consumable.nonConsumablePurchased once permanently (premium unlock)
Auto-renewable.autoRenewableRecurring billing with automatic renewal
Non-renewing.nonRenewingTime-limited access without automatic renewal

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

Purchase Flow

Prefer StoreKit views for standard paywalls because they initiate purchases, restore purchases, and display policy controls. For custom SwiftUI purchase buttons, prefer PurchaseAction from the environment. Use direct product.purchase(options:) for watchOS, and use purchase(confirmIn:options:) for UIKit or AppKit confirmation. Always handle every PurchaseResult, verify before access, deliver durably, then finish.

swift
@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:        // Ask to Buy or deferred approval: show pending UI, no unlock yet.        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 Listener

Start at app launch, not when a paywall appears. Catches purchases from other devices, Family Sharing changes, renewals, Ask to Buy approvals, refunds, revocations, and unfinished transactions Apple emits once immediately after launch. Keep the task retained for the app lifetime.

swift
@mainstruct 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()            }        }    }}

Entitlement Checking

Transaction.currentEntitlements emits non-consumables, active or grace-period auto-renewable subscriptions, and the latest non-renewing subscription transaction—including finished ones. It excludes consumables and refunded or revoked products. Track consumable fulfillment separately, and apply the app's expiration policy to non-renewing subscriptions before granting access.

swift
@Observable@MainActorclass 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 Modifier

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

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

Custom Marketing Content

Use the container background and header patterns in SubscriptionStoreView Control Styles [blocked].

Hierarchical Layout

Use SubscriptionOptionGroup, SubscriptionOptionSection, or SubscriptionPeriodGroupSet to organize iOS 18+ options; see Subscription Group Management [blocked].

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

ProductView for Individual Products

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(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
.subscribedActive subscription
.expiredSubscription has expired
.inBillingRetryPeriodPayment failed, Apple is retrying
.inGracePeriodPayment failed but access continues during grace period
.revokedApple refunded or revoked the subscription

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)

App Transaction (App Purchase Verification)

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 */ }}

Purchase Options

swift
// App account token for server-side reconciliationtry await product.purchase(options: [.appAccountToken(UUID())])
// Consumable quantitytry await product.purchase(options: [.quantity(5)])
// Simulate Ask to Buy in sandboxtry await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])

SwiftUI Purchase Callbacks

swift
.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)]}

Common Mistakes

1. Not starting Transaction.updates at app launch

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)

2. Forgetting transaction.finish()

swift
// WRONG: Never finished -- reappears in unfinished queue foreverlet transaction = try checkVerified(verification)unlockFeature(transaction.productID)
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.let transaction = try checkVerified(verification)try await recordDelivery(transaction)await transaction.finish()

3. Ignoring verification result

swift
// WRONG: Using unverified transaction -- security risklet transaction = verification.unsafePayloadValue
// CORRECT: Verify before usinglet transaction = try checkVerified(verification)

4. Using original In-App Purchase APIs in new StoreKit 2 code

swift
// AVOID: Original In-App Purchase APIslet request = SKProductsRequest(productIdentifiers: ["com.app.premium"])SKPaymentQueue.default().add(payment)
// PREFERRED: StoreKit 2let products = try await Product.products(for: ["com.app.premium"])let result = try await product.purchase()

5. Not checking revocationDate

swift
// WRONG: Grants access to refunded purchasesif case .verified(let transaction) = result {    purchased.insert(transaction.productID)}
// CORRECT: Skip revoked transactionsif case .verified(let transaction) = result, transaction.revocationDate == nil {    purchased.insert(transaction.productID)}

6. Hardcoding prices

swift
// WRONG: Wrong for other currencies and regionsText("Buy Premium for $4.99")
// CORRECT: Localized price from ProductText("Buy \(product.displayName) for \(product.displayPrice)")

7. Not handling .pending purchase result

swift
// WRONG: Silently drops pending Ask to Buydefault: break
// CORRECT: Explain approval is pending; unlock only after Transaction.updatescase .pending:    showPendingApprovalMessage()

8. Checking entitlements only once at launch

swift
// WRONG: Check once, never updatefunc 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.

9. Missing restore purchases button

swift
// WRONG: No restore option -- App Store rejection riskSubscriptionStoreView(groupID: "group_id")
// CORRECTSubscriptionStoreView(groupID: "group_id")    .storeButton(.visible, for: .restorePurchases)

10. Subscription views without policy links

swift
// WRONG: No terms or privacy policySubscriptionStoreView(groupID: "group_id")
// CORRECTSubscriptionStoreView(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 only after durable content delivery
  • Revoked/refunded transactions excluded and entitlement state updated
  • .pending result shows Ask to Buy/deferred-approval feedback
  • 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 In-App Purchase APIs (SKProduct, SKPaymentQueue) unless legacy OS support requires them
  • Product IDs defined as constants, not scattered strings
  • StoreKit tests cover promotional offers, win-back, offer codes, Ask to Buy, renewals, refunds, and revocations
  • 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

References

  • See references/app-review-guidelines.md [blocked] for IAP rules (Guideline 3.1.1), subscription display requirements, and rejection prevention.
  • See references/storekit-advanced.md [blocked] for subscription control styles, offer management, testing patterns, and advanced subscription handling.
  • For submission, privacy, metadata, screenshots, and rejection-risk audits use app-store-review.
  • For keyword, screenshot-caption, ranking, and conversion strategy use app-store-optimization.
  • Official Apple docs: Choosing a StoreKit API, Transaction.updates, Transaction.currentEntitlements, SubscriptionStoreView, and PurchaseAction.

来源与署名

来源:dpearson2699/swift-ios-skills位于skills/storekit提交8d90fd1

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架

更多来自 dpearson2699/swift-ios-skills 的技能

Widgetkit

dpearson2699

指导实现、审查和改进 iOS、iPadOS、watchOS 与 CarPlay 上的 WidgetKit 小组件和控制项。

Software Development1.1K2个月前更新

Weatherkit

dpearson2699

指导 iOS 开发者使用 WeatherService 获取 WeatherKit 预报、警报与署名信息。

Software Development1.1K2个月前更新

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

待分类1.1K2个月前更新

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

待分类1.1K2个月前更新

Tabletopkit

dpearson2699

指导使用 TabletopKit 在 visionOS 上构建多人空间桌游,涵盖棋具、座位、动作与 RealityKit 渲染。

Software Development1.1K2个月前更新

Swiftui Webkit

dpearson2699

指导在 iOS 26 及更高版本的 SwiftUI 应用中使用 WebKit for SwiftUI 嵌入和控制网页内容。

Software Development1.1K2个月前更新