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.
Instalación
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
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 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.
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
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:).
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) privatevar purchase
funcpurchaseProduct(_product: Product) asyncthrows {
let result =tryawait 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:
breakcase .pending:
// Ask to Buy or deferred approval: show pending UI, no unlock yet.
showPendingApprovalMessage()
@unknowndefault:
break
}
}
funccheckVerified<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.
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.
structPremiumGatedView: View {
@Stateprivatevar state: EntitlementTaskState<VerificationResult<Transaction>?> = .loading
var body: someView {
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 inself.state = state
}
}
}
SubscriptionStoreView (iOS 17+)
Built-in SwiftUI view for subscription paywalls. Handles product loading,
purchase UI, and restore purchases automatically.
SubscriptionOptionGroup, SubscriptionOptionSection, and
SubscriptionPeriodGroupSet are iOS 18+ helper views for organizing options
inside SubscriptionStoreView.
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+).
funcverifyAppPurchase() async {
do {
let result =tryawaitAppTransaction.shared
switch result {
case .verified(let appTransaction):
let originalVersion = appTransaction.originalAppVersion
let purchaseDate = appTransaction.originalPurchaseDate
// Migration logic for users who paid before subscription modelcase .unverified:
// Potentially tampered -- restrict features as appropriatebreak
}
} catch { /* Could not retrieve app transaction */ }
}
Purchase Options
// App account token for server-side reconciliationtryawait product.purchase(options: [.appAccountToken(UUID())])
// Consumable quantitytryawait product.purchase(options: [.quantity(5)])
// Simulate Ask to Buy in sandboxtryawait product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
SwiftUI Purchase Callbacks
.onInAppPurchaseStart { product inawait analytics.trackPurchaseStarted(product.id)
}
.onInAppPurchaseCompletion { product, result inifcase .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@mainstructMyApp: App {
var body: someScene { WindowGroup { ContentView() } }
}
// CORRECT: Start listener in App init (see Transaction.updates section above)
2. Forgetting transaction.finish()
// WRONG: Never finished -- reappears in unfinished queue foreverlet transaction =try checkVerified(verification)
unlockFeature(transaction.productID)
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.let transaction =try checkVerified(verification)
tryawait recordDelivery(transaction)
await transaction.finish()
3. Ignoring verification result
// WRONG: Using unverified transaction -- security risklet transaction = verification.unsafePayloadValue
// CORRECT: Verify before usinglet transaction =try checkVerified(verification)
4. Using original In-App Purchase APIs in new StoreKit 2 code
// AVOID: Original In-App Purchase APIslet request =SKProductsRequest(productIdentifiers: ["com.app.premium"])
SKPaymentQueue.default().add(payment)
// PREFERRED: StoreKit 2let products =tryawaitProduct.products(for: ["com.app.premium"])
let result =tryawait product.purchase()
// 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
// WRONG: Silently drops pending Ask to Buydefault: break// CORRECT: Explain approval is pending; unlock only after Transaction.updatescase .pending:
showPendingApprovalMessage()
8. Checking entitlements only once at launch
// WRONG: Check once, never updatefuncappDidFinish() { 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 riskSubscriptionStoreView(groupID: "group_id")
// CORRECTSubscriptionStoreView(groupID: "group_id")
.storeButton(.visible, for: .restorePurchases)
10. Subscription views without policy links
// 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 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