Claude Agent Skill · by Dpearson2699

Storekit

Install Storekit skill for Claude Code from dpearson2699/swift-ios-skills.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skills
Works with Paperclip

How Storekit fits into a Paperclip company.

Storekit drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

E
E-Commerce EmpirePaired

Pre-configured AI company — 20 agents, 9 skills, one-time purchase.

$59$89
Explore pack
Source file
SKILL.md477 lines
Expand
---name: storekitdescription: "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, and paywalls using StoreKit 2 oniOS 26+. Use the modern `Product`, `Transaction`, `StoreView`, and`SubscriptionStoreView` APIs. Avoid the older original StoreKit APIs(`SKProduct`, `SKPaymentQueue`, `SKStoreReviewController`). ## 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:)`. ```swiftimport 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 Call `product.purchase(options:)` and handle all three `PurchaseResult` cases.Always verify and finish transactions. ```swiftfunc purchase(_ product: Product) async throws {    let result = try await product.purchase(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 -- do not unlock content yet        break    @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. Catches purchases from other devices, Family Sharingchanges, renewals, Ask to Buy approvals, refunds, and revocations. ```swift@mainstruct MyApp: App {    private var transactionListener: Task<Void, Error>?     init() {        transactionListener = listenForTransactions()    }     var body: some Scene {        WindowGroup { ContentView() }    }     func listenForTransactions() -> Task<Void, Error> {        Task.detached {            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-consumable purchases and activesubscriptions. Always check `revocationDate`. ```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 {                purchased.insert(transaction.productID)            }        }        purchasedProductIDs = purchased    }}``` ### SwiftUI .currentEntitlementTask Modifier ```swiftstruct 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(let transaction):                if transaction != nil { PremiumContentView() }                else { 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. ```swiftSubscriptionStoreView(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(.verified(let transaction)) = result {            await transaction.finish()        }    }``` ### Custom Marketing Content ```swiftSubscriptionStoreView(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 ```swiftSubscriptionStoreView(groupID: "YOUR_GROUP_ID") {    SubscriptionPeriodGroupSet()}.subscriptionStoreControlStyle(.picker)``` ## StoreView (iOS 17+) Merchandises multiple products with localized names, prices, and purchase buttons. ```swiftStoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)    .productViewStyle(.large)    .storeButton(.visible, for: .restorePurchases)    .onInAppPurchaseCompletion { product, result in        if case .success(.verified(let transaction)) = result {            await transaction.finish()        }    }``` ### ProductView for Individual Products ```swiftProductView(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 ```swiftfunc checkSubscriptionActive(groupID: String) async throws -> Bool {    let statuses = try await Product.SubscriptionInfo.Status.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 | State | Meaning ||---|---|| `.subscribed` | Active subscription || `.expired` | Subscription has expired || `.inBillingRetryPeriod` | Payment failed, Apple is retrying || `.inGracePeriod` | Payment failed but access continues during grace period || `.revoked` | Apple refunded or revoked the subscription | ## Restore Purchases StoreKit 2 handles restoration via `Transaction.currentEntitlements`. Add arestore button or call `AppStore.sync()` explicitly. ```swiftfunc 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 changesor detecting tampered installations (iOS 16+). ```swiftfunc 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    return true  // Return false to cancel}.onInAppPurchaseCompletion { product, result in    if case .success(.verified(let transaction)) = result {        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: Always finish after delivering contentlet transaction = try checkVerified(verification)unlockFeature(transaction.productID)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 legacy original StoreKit APIs ```swift// AVOID: Original StoreKit (legacy)let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])SKPaymentQueue.default().add(payment)SKStoreReviewController.requestReview() // PREFERRED: StoreKit 2let products = try await Product.products(for: ["com.app.premium"])let result = try await product.purchase()try await AppStore.requestReview(in: windowScene)``` ### 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: Inform user purchase is awaiting approvalcase .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 after content delivery- [ ] Revoked transactions excluded from entitlements- [ ] `.pending` purchase result handled for Ask to Buy- [ ] 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 StoreKit APIs (`SKProduct`, `SKPaymentQueue`)- [ ] Product IDs defined as constants, not scattered strings- [ ] StoreKit configuration file set up for testing- [ ] 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](references/app-review-guidelines.md) for IAP rules (Guideline 3.1.1),  subscription display requirements, and rejection prevention.- See [references/storekit-advanced.md](references/storekit-advanced.md) for subscription control styles,  offer management, testing patterns, and advanced subscription handling.