Skip to content
Mobile
Skill

/storekit

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,

From plugin
swift-ios-skills
1.3k86 skills1 MCP

Claude Code

$ npx -y skills add dpearson2699/swift-ios-skills --skill storekit --agent claude-code

SKILL.md

storekit.SKILL.md
name: storekit
description: "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."

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](#product-types)
  • [Loading Products](#loading-products)
  • [Purchase Flow](#purchase-flow)
  • [Transaction.updates Listener](#transactionupdates-listener)
  • [Entitlement Checking](#entitlement-checking)
  • [SubscriptionStoreView (iOS 17+)](#subscriptionstoreview-ios-17)
  • [StoreView (iOS 17+)](#storeview-ios-17)
  • [Subscription Status Checking](#subscription-status-checking)
  • [Restore Purchases](#restore-purchases)
  • [App Transaction (App Purchase Verification)](#app-transaction-app-purchase-verification)
  • [Purchase Options](#purchase-options)
  • [SwiftUI Purchase Callbacks](#swiftui-purchase-callbacks)
  • [Common Mistakes](#common-mistakes)
  • [Review Checklist](#review-checklist)
  • [References](#references)

Product Types

| Type | Enum Case | Behavior | |---|---|---| | **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 |

Loading Products

Define product IDs as constants. Fetch products with `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)")
}

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.

@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.

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

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.

@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 == .
Read more
@dpearson2699
Ships withswift-ios-skills

86 agent skills optimized for iOS 26+ development with Swift 6.3 and modern Apple frameworks.

1.3k86 skills1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
1.3k
Star history of dpearson2699/swift-ios-skills on GitHub, a chart by star-history.com

Other skills on swift-ios-skills.

alarmkit

alarmkit

Implement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple…

app-clips

app-clips

Build iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store…

More skills from other plugins.

deep-code-review

deep-code-review

In-depth design-focused code review - understands codebase context before evaluating PR changes, posts structured feedback to…

expo1d53k
@expo@expoInstall Skill
modlens

modlens

Plug-in vision for text-only models. Hard rule: when a file path or URL with an image extension (.png, .jpg, .jpeg, .webp, .gif,…

modlens2w4.6k
@liustack@liustackInstall Skill
appllama-app-design-skill

appllama-app-design-sk…

Build native-feeling, benchmark-quality mobile app screens (Expo / React Native). Use when designing or implementing any mobile…

appllama2mo3.9k
@appllama@appllamaInstall Skill

Questions about storekit.

Which coding agents can use storekit?
storekit installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.