Install
Terminal · npx$
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-native-skillsWorks 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 packSource file
SKILL.md477 linesExpandCollapse
---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.