Implement or review in-app purchases and subscriptions with StoreKit 2, including paywalls, transactions, entitlement verification, offers, renewal state, testing, Family Sharing, Ask to Buy, refunds, and billing recovery. Route physical-goods checkout to passkit.
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Medium
Suggest reviewing before use
Build, review, and debug digital-goods purchases with modern StoreKit. Preserve the project's deployment target and existing StoreKit abstraction. Use original SKProduct/SKPaymentQueue APIs only when legacy support or an existing migration boundary requires them.
Route physical goods and real-world services to passkit; full submission/privacy/rejection audits to app-store-review; metadata and conversion work to app-store-optimization.
Read only the references required by the request:
PurchaseAction, direct purchases, options, AppTransaction, or durable content delivery, read products, merchandising, and purchases.Transaction.updates, current entitlements, subscription status, revocation, Family Sharing, restore, or reconciliation, read entitlements and subscription state.SKTestSession, renewal controls, and test matrices, read StoreKit Test and Sandbox.Do not load offer, compliance, or subscription material for a narrow consumable/non-consumable task.
VerificationResult before granting access or using signed transaction state.transaction.finish().Transaction.updates listener during app initialization, not when a paywall appears..pending, .userCancelled, errors, refunds/revocations, and recovery as distinct states.AppStore.sync() only from an explicit user restore action because it may prompt for authentication.Prefer StoreKit views for standard merchandising:
ProductView for one product;StoreView for a collection;SubscriptionStoreView for auto-renewable subscriptions in one group.They load localized product information and initiate purchases. Configure visible restore/redeem controls and policy destinations according to the product and platform.
For custom SwiftUI buttons, prefer PurchaseAction. Use purchase(confirmIn:options:) for UIKit/AppKit or lower-level product.purchase(options:) where the platform/custom flow requires it. Don't introduce a parallel purchasing abstraction when the project already centralizes verification and fulfillment correctly.
| Product type | Entitlement responsibility |
|---|---|
| Consumable | Persist delivered balance/history outside current entitlements |
| Non-consumable | Grant durable ownership from verified transaction state |
| Auto-renewable | Reconcile verified entitlement and subscription renewal state |
| Non-renewing | App/server defines and persists the access-expiration policy |
Handle all PurchaseResult cases:
switch try await purchase(product) {
case .success(let result):
let transaction = try verified(result)
try await fulfillment.deliver(transaction)
await transaction.finish()
case .pending:
purchaseState = .pendingApproval
case .userCancelled:
purchaseState = .idle
@unknown default:
purchaseState = .failed
}Never use unsafePayloadValue to unlock content. Never finish before delivery. If durable delivery fails, leave the transaction unfinished and recover it through the lifetime listener or Transaction.unfinished.
Make fulfillment safe when the initiating purchase flow and Transaction.updates observe the same transaction. For consumables, reconcile server/app balance before finish; currentEntitlements doesn't retain consumed quantity.
Start one listener at app launch. It can deliver Ask to Buy approvals, purchases from another device, renewals, refunds, revocations, Family Sharing changes, and unfinished transactions.
Transaction.currentEntitlements represents verified current access for non-consumables, active or grace-period auto-renewable subscriptions, and non-renewing subscription transactions. It excludes consumables and refunded/revoked products. Apply the app's expiration policy to non-renewing subscriptions.
Rebuild a fresh entitlement set so removed or revoked access disappears. Reconcile at launch and on transaction updates; refresh on foreground only where the product needs it, without creating duplicate listeners.
UI hints such as a StoreKit view's entitlement configuration can customize merchandising but aren't access authority. Grant access from verified transaction information.
Use verified subscription status/renewal information when the UI needs more than yes/no access. Treat .subscribed and .inGracePeriod as entitled; distinguish billing retry, expiration, and revocation according to product policy.
StoreKit automatically makes transaction information available after reinstall or on a new device. Derive access proactively from current entitlements and expose a user-initiated Restore Purchases path. Do not call AppStore.sync() automatically at launch.
Offers require both configured offer data and current eligibility. A raw winBackOffers list is not eligibility; compare it with verified renewal information. Keep StoreKit configuration and sandbox coverage for promotional offers, win-back, offer codes, Ask to Buy, renewal transitions, refunds, revocations, and Family Sharing.
When reviewing flawed StoreKit code, name the broken contract:
Transaction.updates observation during app initialization.AppStore.sync() for explicit user action.transaction.offer?.type and .id APIs for the supported SDK.VerificationResult containing a Transaction on success, or an error describing the failure.Transaction.OwnershipType.assigned and Transaction.RevocationType.assignmentRevoked; queries also return transactions assigned to Managed Apple Accounts.Product.ProductType adds subscription Bundles and Suites. Product.SubscriptionInfo.BundledSubscription exposes Bundle merchandising data; Transaction and RenewalInfo gain Bundle and Suite fields.partnerName and partnerId added to Transaction.AdvancedCommerceInfo and RenewalInfo.AdvancedCommerceInfo.SKTestSession.clearTransactions(), storefront changes propagate through Storefront.updates. Known issue: buying a subscription that belongs to a bundle wrongly allows unbundling instead of throwing.#available(iOS 27, *).Sources: https://sosumi.ai/documentation/ios-ipados-release-notes/ios-ipados-27-release-notes
.pending.AppStore.sync() is user initiated.thienqo/ios-skills
6bf8b62
Also appears in
since Oct 29, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.