Storekit

by dpearson26998d90fd121a26No license1.1K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

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.

Instructions onlySoftware Development
AI-generated overview

Guides implementing, reviewing, and testing StoreKit 2 in-app purchases and subscriptions in Apple apps.

What it does
This skill provides reference instructions for building in-app purchases and subscriptions with StoreKit 2, covering product loading, purchase flows, transaction listeners, entitlement checks, paywall views, subscription status, restore purchases, and app transaction verification. It includes code patterns, a list of common mistakes, and a review checklist, plus two reference documents on App Review guidelines and advanced StoreKit topics. It produces guidance and code examples rather than executable artifacts.
When to use it
Use it when writing or reviewing Swift code for in-app purchases, subscriptions, paywalls, offer codes, promotional or win-back offers, Family Sharing, Ask to Buy, refunds, or billing retry. It is also relevant when setting up StoreKit testing configuration or checking subscription renewal state.
Requirements
No scripts are shipped; it is instructions and reference documents only. Working with the code examples assumes an Apple platform development environment with Swift and StoreKit 2, and the reference links point to external Apple documentation.

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.

Source and attribution

Source:dpearson2699/swift-ios-skillsinskills/storekitat commit8d90fd1

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from dpearson2699/swift-ios-skills

Widgetkit

dpearson2699

Guides implementing, reviewing, and improving WidgetKit widgets and controls for iOS, iPadOS, watchOS, and CarPlay.

Software Development1.1Kupdated 2 months ago

Weatherkit

dpearson2699

Guides iOS developers in fetching WeatherKit forecasts, alerts, and attribution using WeatherService.

Software Development1.1Kupdated 2 months ago

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.

Awaiting classification1.1Kupdated 2 months ago

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.

Awaiting classification1.1Kupdated 2 months ago

Tabletopkit

dpearson2699

Guides building multiplayer spatial board games on visionOS with Apple's TabletopKit and RealityKit.

Software Development1.1Kupdated 2 months ago

Swiftui Webkit

dpearson2699

Guides embedding and controlling web content in SwiftUI apps with WebKit for SwiftUI on iOS 26 and later.

Software Development1.1Kupdated 2 months ago