---
name: storekit
description: "StoreKit 2 in-app purchases and subscriptions: SubscriptionStoreView, StoreView and ProductView paywalls, consumable, non-consumable and auto-renewable purchase flows, Product and Transaction APIs, entitlement verification, subscription status and renewal, offer codes, introductory, promotional and win-back offers, configuration file testing, Family Sharing, Ask to Buy, refunds, billing retry. Use when building or reviewing purchase, paywall, entitlement or subscription code. Not for App Store review (app-store-review) or ASO (app-store-optimization)."
metadata:
  source: multi-agent-pipeline
---

# StoreKit 2

StoreKit 2 is the Swift-native purchase API: `Product` describes what you
sell, `Transaction` records what the user owns, and SwiftUI supplies
`PurchaseAction`, `StoreView`, `ProductView` and `SubscriptionStoreView` to
sell it. This skill targets iOS 26 and names the release where each API
appeared, because older deployment targets change what you may call.

The original In-App Purchase API (`SKProduct`, `SKPaymentQueue`,
`SKProductsRequest`) stays out of new code. Use it only when an old OS
version forces you to.

Two review rules shape everything below:

- Separate "the preferred SwiftUI path" from "an invalid API". For a custom
  SwiftUI buy button, `PurchaseAction` is preferred; calling
  `product.purchase(options:)` directly is still correct for flows that need
  low-level control. Never call the direct call wrong in itself.
- This file carries the rules and short snippets. Complete code for each core
  flow is in [references/core-patterns.md](references/core-patterns.md).

Where neighbouring work goes:

| Topic | Skill |
|-------|-------|
| Submission readiness, privacy manifests, ATT, metadata, screenshots, rejection audits | `app-store-review` |
| Keywords, ranking, screenshot captions, conversion strategy | `app-store-optimization` |

In a file that imports SwiftUI, the bare name `Transaction` clashes with
SwiftUI's animation transaction when used as a type. Write
`StoreKit.Transaction` there.

## Implementation Review Minimums

When you review a paywall, a purchase manager or an entitlement gate, say each
of these out loud, even when the code already gets it right:

1. Standard SwiftUI paywalls use `StoreView`, `ProductView` or
   `SubscriptionStoreView`; custom SwiftUI buy buttons use `PurchaseAction`.
2. `Transaction.updates` is iterated from app launch. That is the only place
   the app hears about purchases made on another device, Family Sharing
   changes, renewals, Ask to Buy approvals, refunds, revocations and
   transactions left unfinished last time.
3. When `Transaction.currentEntitlements` comes up, 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."
4. Every `VerificationResult` is checked before access is granted.
5. Content is delivered or the entitlement persisted first, and only then is
   `transaction.finish()` called.
6. A pending purchase or a cancelled one never unlocks anything. A pending Ask
   to Buy purchase unlocks when the approved, verified transaction arrives on
   the launch-time listener.
7. Refunded or revoked transactions are excluded from entitlement state, and
   entitlements are recomputed when a refund or revocation arrives on
   `Transaction.updates`.
8. A subscription paywall shows a restore path and links to Terms of Service
   and the Privacy Policy.

## Product Types

| Kind | `Product.ProductType` | Behaviour | Example |
|------|----------------------|-----------|---------|
| Consumable | `.consumable` | used up, can be bought again | hint packs, credits |
| Non-consumable | `.nonConsumable` | bought once, kept forever | pro unlock |
| Auto-renewable | `.autoRenewable` | billed on a schedule until cancelled | monthly plan |
| Non-renewing | `.nonRenewing` | access for a fixed period, no renewal | season pass |

## Loading Products

Keep identifiers in one place and fetch them together:

```swift
import StoreKit

enum StoreItem {
    static let proUnlock = "com.example.recipes.pro"
    static let monthly = "com.example.recipes.monthly"
    static let annual = "com.example.recipes.annual"
    static let everything = [proUnlock, monthly, annual]
}

func loadCatalog() async throws -> [Product] {
    try await Product.products(for: StoreItem.everything)
}
```

## Purchase Flow

Pick the entry point by how much control you need:

| Need | Use | Since |
|------|-----|-------|
| Standard paywall | `StoreView`, `ProductView`, `SubscriptionStoreView`; they buy, restore and show policy links | iOS 17 |
| Custom SwiftUI button | `@Environment(\.purchase)` (`PurchaseAction`) | iOS 17 |
| Low-level custom flow | `product.purchase(options:)` | iOS 15 |
| UIKit or AppKit confirmation sheet | `product.purchase(confirmIn:options:)` | iOS 17 |

Whatever the entry point, handle every `PurchaseResult` case, verify before
granting, persist the delivery, then finish. In a review, recommend
`PurchaseAction` for SwiftUI buttons and leave direct `purchase(options:)` in
place where the flow genuinely needs it; do not flag it as a bug. The full
flow with the `verified(_:)` helper and `.pending` handling is at
[core-patterns.md#buying-with-purchaseaction](references/core-patterns.md#buying-with-purchaseaction).

## Transaction.updates Listener

- Start it when the app launches, never when a paywall appears.
- It delivers other-device purchases, Family Sharing changes, renewals, Ask
  to Buy approvals, refunds and revocations.
- Right after launch it also delivers, once, every transaction that was never
  finished.
- Keep the task alive for the whole life of the app.

In a review, list those event kinds by name. The listener wired into
`App.init` is at
[core-patterns.md#launch-time-transaction-listener](references/core-patterns.md#launch-time-transaction-listener).

## Entitlement Checking

- `Transaction.currentEntitlements` covers non-consumables, active or
  grace-period auto-renewable subscriptions, and non-renewing subscriptions.
- It does not contain consumables or their delivery history; record consumable
  fulfilment in your own ledger, on device or on a server.
- It leaves out refunded and revoked transactions, but still check
  `revocationDate` whenever you process a transaction.
- `Transaction.unfinished` is the sweep for unfinished consumables and
  recovery.
- For one product, `Transaction.currentEntitlements(for:)` (iOS 18.4) replaces
  `Transaction.currentEntitlement(for:)`, deprecated in iOS 18.4.

In a review the verbatim scope sentence from the minimums is required; a code
sample or a revocation check alone does not replace it. The full
`refresh()` and the `.currentEntitlementTask` gate are at
[core-patterns.md#computing-entitlements](references/core-patterns.md#computing-entitlements).

## Store Views

`SubscriptionStoreView(groupID:)` is a complete subscription paywall: it
loads products, runs the purchase sheet and can show restore and policy
controls. `StoreView(ids:)` sells several products; `ProductView(id:)` shows
one. All three are iOS 17 and all accept `.onInAppPurchaseCompletion` and
`.storeButton(.visible, for: .restorePurchases)`.

```swift
import StoreKit
import SwiftUI

struct PlanPicker: View {
    let group: String
    let terms: URL
    let privacy: URL

    var body: some View {
        SubscriptionStoreView(groupID: group)
            .storeButton(.visible, for: .restorePurchases)
            .subscriptionStorePolicyDestination(url: terms, for: .termsOfService)
            .subscriptionStorePolicyDestination(url: privacy, for: .privacyPolicy)
    }
}
```

Complete examples are at
[core-patterns.md#subscription-paywall](references/core-patterns.md#subscription-paywall)
and [core-patterns.md#product-shelves-storeview-and-productview](references/core-patterns.md#product-shelves-storeview-and-productview).
Control styles, grouped layouts, offers and backgrounds are in
[references/storekit-advanced.md](references/storekit-advanced.md).

## Subscription Status Checking

Read status with `Product.SubscriptionInfo.status(for: groupID)`. Count
`.subscribed` and `.inGracePeriod` as active.

| `RenewalState` | Meaning |
|----------------|---------|
| `.subscribed` | active |
| `.expired` | ended |
| `.inBillingRetryPeriod` | a charge failed and Apple keeps retrying |
| `.inGracePeriod` | a charge failed, but access continues for the grace window |
| `.revoked` | refunded or revoked by Apple |

The helper is at
[core-patterns.md#is-a-plan-active](references/core-patterns.md#is-a-plan-active);
what to grant in each state, expiration reasons, grace period and billing retry
are in [references/storekit-advanced.md](references/storekit-advanced.md).

## Restore Purchases

In StoreKit 2 there is no separate restore step for data: whatever the user
owns shows up in `Transaction.currentEntitlements`. Still offer a restore
control. On a store view use `.storeButton(.visible, for: .restorePurchases)`;
in custom UI call `AppStore.sync()` and recompute:

```swift
func restoreTapped(using store: EntitlementStore) async throws {
    try await AppStore.sync()
    await store.refresh()
}
```

## App Transaction

`AppTransaction.shared` (iOS 16) proves the app install itself is genuine.
Use it when the business model changes, such as moving from a paid app to a
subscription while honouring earlier buyers, and to spot tampered installs.
From the verified value read `originalAppVersion` and `originalPurchaseDate`;
on `.unverified`, limit features. Full code at
[core-patterns.md#verifying-the-install-with-apptransaction](references/core-patterns.md#verifying-the-install-with-apptransaction).

## Purchase Options

| Option | Purpose |
|--------|---------|
| `.appAccountToken(uuid)` | ties the purchase to your account ID for server reconciliation |
| `.quantity(n)` | buys several of a consumable |
| `.simulatesAskToBuyInSandbox(true)` | forces the Ask to Buy flow in sandbox |

## SwiftUI Purchase Callbacks

Store views take `.onInAppPurchaseStart`, `.onInAppPurchaseCompletion` and
`.inAppPurchaseOptions`. In the completion handler: verify, deliver, finish.
Example at
[core-patterns.md#store-view-callbacks](references/core-patterns.md#store-view-callbacks).

## Common Mistakes

Each has a wrong and a right version in
[core-patterns.md#ten-mistakes-fixed](references/core-patterns.md#ten-mistakes-fixed).

1. No `Transaction.updates` listener at launch.
2. Never calling `transaction.finish()`.
3. Ignoring the verification result.
4. `SKProduct` or `SKPaymentQueue` in new StoreKit 2 code.
5. Not checking `revocationDate`.
6. Prices typed into strings instead of `product.displayPrice`.
7. No handling for `.pending`.
8. Checking entitlements once at launch and never again.
9. No restore purchases control.
10. Subscription screens without Terms and Privacy links.

## Old API to Current API

These replacements are regression facts; flag the left column in review.

| Stale code | Current code | Why |
|------------|--------------|-----|
| `Product.SubscriptionInfo.Status.status(for:)` | `Product.SubscriptionInfo.status(for:)` | the former does not exist |
| `transaction.offerType`, `transaction.offerID` | `transaction.offer?.type`, `transaction.offer?.id` | deprecated in iOS 17.2 |
| `.promotionalOffer(offerID:signature:)` and `.promotionalOffer(offerID:keyID:nonce:signature:timestamp:)` | `Product.PurchaseOption.promotionalOffer(_:compactJWS:)` | deprecated in iOS 26; the new call returns an array of options |
| `.subscriptionPromotionalOffer(offer:signature:)` on store views | `.subscriptionPromotionalOffer(offer:compactJWS:)` (iOS 26) | deprecated in iOS 26 |
| `Transaction.currentEntitlement(for:)` | `Transaction.currentEntitlements(for:)` | deprecated in iOS 18.4 |
| Showing every `winBackOffers` entry | only entries whose IDs are in the verified `renewalInfo.eligibleWinBackOfferIDs` | the raw list is not per user |
| `.purchaseDate(_:renewalBehavior: .default)` | `.purchaseDate(_:renewalBehavior:)` with `.renewUntilNow` or `.cancelImmediately`, from `StoreKitTest` | there is no `.default`; the option is test-only |

## Review Checklist

- [ ] `Transaction.updates` listener starts in the `App` initializer
- [ ] Every transaction is verified before access
- [ ] `transaction.finish()` runs only after durable delivery
- [ ] Revoked and refunded transactions are excluded and entitlements refreshed
- [ ] `.pending` shows Ask to Buy or deferred-approval feedback
- [ ] Restore purchases is visible on the paywall and store views
- [ ] Subscription screens link to Terms of Service and the Privacy Policy
- [ ] Prices come from `product.displayPrice`
- [ ] Price, period and renewal terms are clear before purchase
- [ ] Free-trial copy states what is charged after the trial
- [ ] No `SKProduct` or `SKPaymentQueue` unless an old OS needs them
- [ ] Product IDs live in constants
- [ ] StoreKit tests cover promotional, win-back and code offers, Ask to Buy, renewals, refunds and revocations
- [ ] Entitlements are recomputed on `Transaction.updates` and when the app returns to the foreground
- [ ] Server validation, where used, receives `jwsRepresentation`
- [ ] Consumables are delivered and finished promptly
- [ ] Types that cross concurrency domains (observers, product models) are `Sendable`

## References

- [references/core-patterns.md](references/core-patterns.md): load when writing
  or reviewing a core flow.
- [references/storekit-advanced.md](references/storekit-advanced.md): offers,
  test setup, subscription edge cases.
- [references/app-review-guidelines.md](references/app-review-guidelines.md):
  before submission or when a payment rejection needs diagnosing (Guideline
  3.1.1, subscription disclosure, avoiding rejections).
- Apple: [Choosing a StoreKit API](https://developer.apple.com/documentation/storekit/choosing-a-storekit-api-for-in-app-purchases),
  [Transaction.updates](https://developer.apple.com/documentation/storekit/transaction/updates),
  [Transaction.currentEntitlements](https://developer.apple.com/documentation/storekit/transaction/currententitlements),
  [SubscriptionStoreView](https://developer.apple.com/documentation/storekit/subscriptionstoreview),
  [PurchaseAction](https://developer.apple.com/documentation/storekit/purchaseaction).
