Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

Storekit

  • 2.9k installs
  • 944 repo stars
  • Updated July 15, 2026
  • dpearson2699/swift-ios-skills

A StoreKit 2 skill for implementing iOS in-app purchases, subscriptions, paywalls, entitlement verification, and sandbox testing with modern Swift APIs.

About

StoreKit 2 In-App Purchases and Subscriptions guides iOS developers through implementing paywalls, transaction flows, and entitlement verification using modern Swift APIs on iOS 26+. The skill targets Product, Transaction, PurchaseAction, StoreView, and SubscriptionStoreView rather than legacy SKProduct and SKPaymentQueue unless older OS support demands it. Workflows cover loading products with Product.products(for:), handling PurchaseResult cases including pending Ask to Buy, verifying VerificationResult before granting access, and finishing transactions only after durable delivery. It stresses starting a Transaction.updates listener at app launch to catch renewals, Family Sharing, refunds, and cross-device purchases. Entitlement checks use Transaction.currentEntitlements for non-consumables and active subscriptions while excluding consumable history and revoked transactions. SubscriptionStoreView and StoreView provide built-in paywall UI with restore purchases and policy links. Review checklists flag common mistakes like missing transaction.finish(), hardcoded prices, and entitlement checks only at launch. Use when building or reviewing consumable, non-consumable, auto-renewable.

  • Prefer StoreView, ProductView, and SubscriptionStoreView for standard paywalls
  • Start Transaction.updates at app launch for renewals, refunds, and Ask to Buy
  • Verify every VerificationResult before granting access, then call transaction.finish()
  • Transaction.currentEntitlements covers non-consumables and active subscriptions, not consumables
  • Review checklist covers restore purchases, policy links, and localized displayPrice

Storekit by the numbers

  • 2,876 all-time installs (skills.sh)
  • +119 installs in the week ending Jul 29, 2026 (Skillselion tracking)
  • Ranked #55 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Security screen: MEDIUM risk (skills.sh audit)
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
At a glance

storekit capabilities & compatibility

Capabilities
load products with product.products(for:) and lo · handle purchaseresult success, usercancelled, an · start transaction.updates listener at app launch · check entitlements via transaction.currententitl · build paywalls with subscriptionstoreview, store · restore purchases with appstore.sync() and visib
Platforms
macOS
Runs
Runs locally
Pricing
Free
From the docs

What storekit says it does

Transaction.currentEntitlements covers non-consumables, active or grace-period auto-renewable subscriptions, and non-renewing subscriptions; it does not include consumable purchase or delivery history
SKILL.md
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill storekit

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.9k
repo stars944
Security audit2 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/swift-ios-skills

How do you implement StoreKit 2 purchases without missing renewals, unverified transactions, revoked entitlements, or App Store rejection risks?

Implement and review StoreKit 2 paywalls, purchases, entitlements, and subscription flows on iOS using Product, Transaction, and StoreView APIs.

Who is it for?

iOS developers adding consumable, non-consumable, auto-renewable, or non-renewing purchases with StoreKit 2 on iOS 17+.

Skip if: Skip when you only need App Store metadata review or keyword optimization without touching purchase code.

When should I use this skill?

Use when implementing paywalls, processing transactions, verifying entitlements, handling offer codes, or reviewing StoreKit purchase flows.

What you get

Reviewed paywall code with launch-time Transaction.updates, verified entitlements, proper finish() timing, and SubscriptionStoreView policy controls.

  • paywall UI
  • transaction handlers
  • entitlement verification logic

Files

SKILL.mdMarkdownGitHub ↗

StoreKit 2 In-App Purchases and Subscriptions

Implement in-app purchases, subscriptions, paywalls, and StoreKit testing using StoreKit 2 on iOS 26+. 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.

When reviewing StoreKit code, explicitly separate "preferred SwiftUI path" from "invalid API": PurchaseAction is the preferred custom SwiftUI button path, but direct product.purchase(options:) is still valid for lower-level custom StoreKit flows.

Contents

Implementation Review Minimums

When reviewing a paywall, purchase manager, or entitlement gate, include these points explicitly:

  • Standard SwiftUI paywalls should prefer StoreView, ProductView, or

SubscriptionStoreView; custom SwiftUI buy buttons should prefer PurchaseAction; direct product.purchase(options:) is valid for lower-level custom StoreKit flows.

  • Transaction.updates must start at app launch because it catches purchases

from other devices, Family Sharing changes, renewals, Ask to Buy approvals, refunds, revocations, and unfinished transactions.

  • Include this entitlement-scope sentence verbatim when reviewing

Transaction.currentEntitlements: "It covers non-consumables, active or grace-period auto-renewable subscriptions, and non-renewing subscriptions; it does not include consumable purchase or delivery history."

  • Verify every VerificationResult before granting access. Deliver or persist

the entitlement first, then call transaction.finish().

  • Pending purchases and user cancellations never unlock content; pending Ask to

Buy approvals unlock only after a verified transaction arrives through the launch-time listener.

  • Exclude refunded or revoked transactions from active entitlement state and

re-check entitlements when refunds or revocations arrive through Transaction.updates.

  • Provide a visible restore purchases path and Terms of Service / Privacy

Policy links on subscription paywalls.

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:).

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:) only for lower-level custom flows, and use purchase(confirmIn:options:) for UIKit or AppKit confirmation. Always handle every PurchaseResult, verify before access, deliver durably, then finish.

Review wording: do not call product.purchase(options:) inherently wrong. Say "prefer PurchaseAction for SwiftUI buttons; keep product.purchase(options:) for lower-level custom flows that need direct StoreKit control."

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

In implementation reviews, name the launch-time coverage explicitly: purchases made on other devices, Family Sharing changes, subscription renewals, Ask to Buy approvals, refunds, revocations, and unfinished transactions.

@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

Use Transaction.currentEntitlements for non-consumables, active or grace period auto-renewable subscriptions, and non-renewing subscriptions. It excludes consumables and consumable delivery history; track consumable fulfillment in your own app or server ledger. It also excludes refunded or revoked transactions. Use Transaction.unfinished for unfinished consumables and recovery sweeps. Always check revocationDate when processing transactions.

In reviews, include this sentence verbatim: "Transaction.currentEntitlements covers non-consumables, active or grace-period auto-renewable subscriptions, and non-renewing subscriptions; it does not include consumable purchase or delivery history." Do not replace this with only a code sample or a revocation check.

@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 {
                purchased.insert(transaction.productID)
            }
        }
        purchasedProductIDs = purchased
    }
}

SwiftUI .currentEntitlementTask Modifier

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.

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

SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    VStack {
        Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
        Text("Unlock Premium").font(.largeTitle.bold())
        Text("Access all features").foregroundStyle(.secondary)
    }
}
.containerBackground(.blue.gradient, for: .subscriptionStore)

Hierarchical Layout

SubscriptionOptionGroup, SubscriptionOptionSection, and SubscriptionPeriodGroupSet are iOS 18+ helper views for organizing options inside SubscriptionStoreView.

SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
    SubscriptionPeriodGroupSet()
}
.subscriptionStoreControlStyle(.picker)

StoreView (iOS 17+)

Merchandises multiple products with localized names, prices, and purchase buttons.

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

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

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.

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+).

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

// App account token for server-side reconciliation
try await product.purchase(options: [.appAccountToken(UUID())])

// Consumable quantity
try await product.purchase(options: [.quantity(5)])

// Simulate Ask to Buy in sandbox
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])

SwiftUI Purchase Callbacks

.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

// 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()

// WRONG: Never finished -- reappears in unfinished queue forever
let 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

// WRONG: Using unverified transaction -- security risk
let transaction = verification.unsafePayloadValue

// CORRECT: Verify before using
let transaction = try checkVerified(verification)

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

// AVOID: Original In-App Purchase APIs
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)

// PREFERRED: StoreKit 2
let products = try await Product.products(for: ["com.app.premium"])
let result = try await product.purchase()

5. Not checking revocationDate

// WRONG: Grants access to refunded purchases
if case .verified(let transaction) = result {
    purchased.insert(transaction.productID)
}

// CORRECT: Skip revoked transactions
if case .verified(let transaction) = result, transaction.revocationDate == nil {
    purchased.insert(transaction.productID)
}

6. Hardcoding prices

// WRONG: Wrong for other currencies and regions
Text("Buy Premium for $4.99")

// CORRECT: Localized price from Product
Text("Buy \(product.displayName) for \(product.displayPrice)")

7. Not handling .pending purchase result

// WRONG: Silently drops pending Ask to Buy
default: break

// CORRECT: Explain approval is pending; unlock only after Transaction.updates
case .pending:
    showPendingApprovalMessage()

8. Checking entitlements only once at launch

// WRONG: Check once, never update
func 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

// WRONG: No restore option -- App Store rejection risk
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(groupID: "group_id")
    .storeButton(.visible, for: .restorePurchases)

10. Subscription views without policy links

// WRONG: No terms or privacy policy
SubscriptionStoreView(groupID: "group_id")

// CORRECT
SubscriptionStoreView(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 for IAP rules (Guideline 3.1.1), subscription display requirements, and rejection prevention.
  • See references/storekit-advanced.md 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.

Related skills

How it compares

Choose storekit for native App Store IAP on iOS; use payment-gateway skills when billing runs through Stripe on web or backend.

FAQ

When should Transaction.updates start?

At app launch in App init, not when a paywall appears, so renewals, Family Sharing, refunds, and unfinished transactions are caught.

What does Transaction.currentEntitlements include?

Non-consumables, active or grace-period auto-renewable subscriptions, and non-renewing subscriptions. It excludes consumable purchase or delivery history.

When is product.purchase(options:) valid?

Prefer PurchaseAction for SwiftUI buttons, but direct product.purchase(options:) remains valid for lower-level custom StoreKit flows needing direct control.

Is Storekit safe to install?

skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Mobile Developmentfrontendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.