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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
The command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
File Explorer
4 files
Showing SKILL.md
SKILL.md
Source instructions · Read-only preview
name
storekit
description
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. 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.
StoreKit views initiate purchases automatically. For custom controls, use
PurchaseAction in SwiftUI, purchase(confirmIn:options:) in UIKit/AppKit, and
product.purchase(options:) on watchOS.
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:) for watchOS, and use purchase(confirmIn:options:)
for UIKit or AppKit confirmation. Always handle every PurchaseResult, verify
before access, deliver durably, then finish.
@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.
Transaction.currentEntitlements emits non-consumables, active or grace-period
auto-renewable subscriptions, and the latest non-renewing subscription
transaction—including finished ones. It excludes consumables and refunded or
revoked products. Track consumable fulfillment separately, and apply the app's
expiration policy to non-renewing subscriptions before granting access.
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.
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