Skip to content

StoreKit 2 Basics (Swift)

A SwiftUI guide for coding agents. Also covers in-app purchase swift, storekit 2 basics, product purchase flow, transaction updates stream, currententitlements, storekit configuration file.

StoreKit 2 is the Swift-native purchase API: fetch Product values, run purchase(), verify the signed transaction, and read Transaction.currentEntitlements for what the user owns.

Project Setup

Add the in-app purchase capability in Signing and Capabilities. Then create a StoreKit configuration file (File > New > File), local-only or synced with App Store Connect, and select it in the scheme's run options. That lets Xcode test purchases with no network.

Fetching and Buying

Product.products(for:) fetches by identifier. Each Product carries displayName and displayPrice, ready to render. purchase() returns success, pending, or userCancelled.

import StoreKit

@MainActor final class Store: ObservableObject {
    @Published private(set) var products: [Product] = []
    @Published private(set) var activeTransactions: Set<StoreKit.Transaction> = []

    func fetchProducts() async {
        products = (try? await Product.products(for: ["123456789", "987654321"])) ?? []
    }

    func purchase(_ product: Product) async throws {
        let result = try await product.purchase()
        switch result {
        case .success(let verificationResult):
            if let transaction = try? verificationResult.payloadValue {
                activeTransactions.insert(transaction)
                await transaction.finish()
            }
        case .userCancelled, .pending:
            break
        @unknown default:
            break
        }
    }
}

A success wraps the transaction in VerificationResult; payloadValue unwraps it or throws when the App Store signature does not check out. Unlock the purchased feature first, then call finish(). Finishing before unlocking can lose a paid purchase.

Pending Transactions

With ask to buy, the transaction arrives later, after a parent approves. Observe Transaction.updates from app launch so no transaction is missed.

init() {
    updates = Task {
        for await update in StoreKit.Transaction.updates {
            if let transaction = try? update.payloadValue {
                await fetchActiveTransactions()
                await transaction.finish()
            }
        }
    }
}

deinit { updates?.cancel() }

Entitlements Replace Restore

Transaction.currentEntitlements streams every active subscription and non-refunded purchase, including ones made on another device. Refreshing it whenever the scene becomes active removes the need for a restore-purchases flow.

func fetchActiveTransactions() async {
    var active: Set<StoreKit.Transaction> = []
    for await entitlement in StoreKit.Transaction.currentEntitlements {
        if let transaction = try? entitlement.payloadValue {
            active.insert(transaction)
        }
    }
    activeTransactions = active
}

// In the App: .task(id: scenePhase) { if scenePhase == .active { await store.fetchActiveTransactions() } }

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-mastering-storekit2" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems