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 App 中使用 WebKit for SwiftUI 嵌入與控制網頁內容。

Software Development1.1K2 個月前更新