---
title: "Purchases"
description: "Use native StoreKit or connect RevenueCat, Superwall, and custom checkout."
search:
  tags: ["sdk", "reference", "ios"]
  keywords: ["Swift", "StoreKit", "Xcode", "RevenueCat", "Superwall", "restorePurchases"]
---

Nuxie uses StoreKit to purchase and restore products shown in an Experience. If your app already uses RevenueCat, Superwall, or another billing provider, connect it with a purchase delegate.

<span id="native-storekit-is-the-default" />

## Native StoreKit

```swift
// nuxie-doc-snippet: native-storekit-setup
import Nuxie

let config = NuxieConfiguration(apiKey: "your_api_key")
try NuxieSDK.shared.setup(with: config)
```

With the default `.full` mode, Nuxie records, synchronizes, and finishes StoreKit transactions. Checkout can finish before server synchronization. Boolean and unlimited access can update from a verified purchase; quota and credit balances wait for the server.

<span id="explicit-storekit-finishing-ownership" />

## Transaction finishing

| Mode | Transaction finishing |
| --- | --- |
| `.full` | Without a purchase delegate, Nuxie synchronizes and finishes native StoreKit transactions. |
| `.observer` | Your app or provider finishes transactions. Without a purchase delegate, Nuxie saves and sends the signed native purchase so Feature access can update. |

Choose `.observer` when another SDK owns transaction finishing. Adding a purchase delegate does not change this setting, but delegated checkout reports only an outcome. Nuxie does not save, send, or finish the delegated transaction.

<span id="outcome-only-purchase-delegate" />

## Purchase delegate

Implement `NuxiePurchaseDelegate` to connect an existing checkout and restore flow:

```swift
// nuxie-doc-snippet: purchase-delegate-contract
import Nuxie

public protocol NuxiePurchaseDelegate: AnyObject, Sendable {
    func purchase(product: StoreProduct) async -> PurchaseResult
    func restorePurchases() async -> RestoreResult
}
```

The purchase receives the `StoreProduct` used to display the offer. Preserve that product and its purchase options so checkout uses the displayed price and terms.

| Operation | Results |
| --- | --- |
| Purchase | `.purchased`, `.pending`, `.cancelled`, `.failed(Error)` |
| Restore | `.restored`, `.noPurchases`, `.failed(Error)` |

Return the outcome only. The delegate does not return receipts or transaction IDs. A `.purchased` outcome can complete the Journey, but it does not convert the paywall's goal or change the customer's Features.

### Custom StoreKit example

This example maps StoreKit checkout and restore results to purchase delegate outcomes. Your app is responsible for transaction finishing. Nuxie receives only the outcome and does not save or send the signed purchase for Feature access:

```swift
// nuxie-doc-snippet: custom-storekit-checkout
import Nuxie
import StoreKit

enum PurchaseError: Error {
    case productUnavailable
    case unknown
}

final class AppPurchaseDelegate: NuxiePurchaseDelegate {
    func purchase(product: StoreProduct) async -> PurchaseResult {
        guard let rawProduct = product.rawProduct else {
            return .failed(PurchaseError.productUnavailable)
        }

        do {
            switch try await rawProduct.purchase(
                options: product.storeKitPurchaseOptions
            ) {
            case .success(let verification):
                switch verification {
                case .verified: return .purchased
                case .unverified(_, let error): return .failed(error)
                }
            case .pending: return .pending
            case .userCancelled: return .cancelled
            @unknown default: return .failed(PurchaseError.unknown)
            }
        } catch {
            return .failed(error)
        }
    }

    func restorePurchases() async -> RestoreResult {
        do {
            try await AppStore.sync()
            for await result in Transaction.currentEntitlements {
                guard case .verified(let transaction) = result,
                      transaction.revocationDate == nil,
                      !transaction.isUpgraded else { continue }
                return .restored
            }
            return .noPurchases
        } catch {
            return .failed(error)
        }
    }
}

let config = NuxieConfiguration(apiKey: "your_api_key")
config.purchaseDelegate = AppPurchaseDelegate()
try NuxieSDK.shared.setup(with: config)
```

Use `product.rawProduct` and `product.storeKitPurchaseOptions`. The options preserve the selected billing plan and introductory-offer eligibility. If your checkout cannot honor them, return a failure.

### Product fields

The delegate receives the product and offer shown in the Experience:

| Field | Description |
| --- | --- |
| `productId` | Nuxie Product ID. |
| `storeProductId` | App Store product identifier. |
| `placementId` | Placement selected in the Experience. |
| `name`, `description`, `price` | Customer-facing product text and formatted price. |
| `productType` | `.consumable`, `.nonConsumable`, `.autoRenewable`, or `.nonRenewing`. |
| `period`, `periodCount`, `periodLabel` | Subscription duration and display label, where applicable. |
| `renewalPrice`, `renewalPeriod` | Price and period after any introductory offer. |
| `billingPlan` | `.default`, `.upFront`, or `.monthly`. |
| `commitmentPrice`, `commitmentPeriod` | Display information for the selected commitment. |
| `introductoryTerms` | Optional introductory price, period, period count, cycles, payment mode, and trial text. |
| `introductoryOfferEligibilityJWS` | Optional eligibility value for the selected introductory offer. |
| `rawProduct` | The StoreKit `Product`, when available. |
| `storeKitPurchaseOptions` | Purchase options to pass to StoreKit for this selection. |

Introductory payment modes are `.freeTrial`, `.payAsYouGo`, and `.payUpFront`. `hasIntroductoryOffer`, `hasFreeTrial`, and `introductoryPaymentMode` help describe the offer. Display helpers include `trialPeriodText`, `trialLabel`, `introOfferLabel`, and `renewalLabel`; `hasTrial` is an alias for `hasFreeTrial`.

### Restore from your app

Call `await NuxieSDK.shared.restorePurchases()` from an app-owned restore button. It returns `.restored`, `.noPurchases`, or `.failed(Error)` using native StoreKit or the configured delegate. A restore button inside an Experience uses the same integration.

Present a distinct result for an empty restore and a failed restore. With native StoreKit and no purchase delegate, read the updated Feature access after restoration. A delegate's restore outcome does not update Nuxie Feature access; keep using your provider's access checks.

### Change checkout at runtime

After setup, `setPurchaseDelegate(_:)` replaces the delegate and `setPurchaseHandlingMode(_:)` changes transaction finishing. Both throw if the SDK is not configured. Pass `nil` to `setPurchaseDelegate(_:)` to return to native checkout. Select the correct finishing mode separately.

## RevenueCat

Copy `Examples/Adapters/NuxieRevenueCatPurchaseDelegate.swift` from the SDK into the app target that already depends on RevenueCat:

```swift
// nuxie-doc-provider-snippet: revenuecat-adapter-configuration
import Nuxie

func configureRevenueCatCheckout(apiKey: String) throws {
    let config = NuxieConfiguration(apiKey: apiKey)
    config.purchaseHandlingMode = .observer
    config.purchaseDelegate = NuxieRevenueCatPurchaseDelegate()
    try NuxieSDK.shared.setup(with: config)
}
```

RevenueCat handles checkout and finishing. Restore calls `Purchases.restorePurchases()`.

## Superwall

Copy `Examples/Adapters/NuxieSuperwallPurchaseDelegate.swift` into the app target that already depends on Superwall:

```swift
// nuxie-doc-provider-snippet: superwall-adapter-configuration
import Nuxie

func configureSuperwallCheckout(apiKey: String) throws {
    let config = NuxieConfiguration(apiKey: apiKey)
    config.purchaseHandlingMode = .observer
    config.purchaseDelegate = NuxieSuperwallPurchaseDelegate()
    try NuxieSDK.shared.setup(with: config)
}
```

Superwall handles checkout and finishing. Restore calls `Superwall.restorePurchases()`.

<span id="feature-access-before-and-after-connector-cutover" />

## Access checks

Choose access checks based on who handles checkout:

| Checkout | Access checks |
| --- | --- |
| Native StoreKit without a purchase delegate | Use Nuxie Feature checks. Nuxie saves and sends the signed purchase in both `.full` and `.observer` modes. In `.observer` mode, your app finishes the transaction. |
| RevenueCat, Superwall, or a custom purchase delegate | Keep using your provider's access checks. Nuxie receives only the checkout outcome, which can complete the Journey but does not grant Feature access. |

RevenueCat and Superwall Connectors feed Automations and server-side Journeys. They do not grant or keep Feature access current today. Support for sending the signed store transaction from delegated checkout without finishing it, and for Connector webhooks that grant and maintain Feature access, is planned.

For native purchases without a purchase delegate, Boolean and unlimited access can update from a verified purchase. The first `useFeatureAndWait()` call can verify a matching purchase, grant its allowance, and deduct usage in one request. Fixed quotas and credits require server-confirmed balances.

<span id="one-internal-completion-authority" />

## Purchase completion

With native StoreKit and no purchase delegate, a checkout callback and StoreKit update can report the same purchase. Nuxie records one completion for the Journey and analytics, then synchronizes the transaction and finishes it in `.full` mode. In `.observer` mode, your app finishes it. If the callback does not return, a StoreKit update can still recover the purchase.

At launch, unfinished native transactions wait until Nuxie has the product configuration needed to determine which integration owns them. A purchase delegate reports completion through its outcome, without sending a transaction to Nuxie.

## Purchase and restore events

| Event | Description |
| --- | --- |
| `$purchase_completed` | Checkout completion was recorded. |
| `$purchase_failed` | Checkout failed. |
| `$purchase_synced` | A verified native transaction was accepted by Nuxie. |
| `$restore_completed` | Restore completed with purchases. |
| `$restore_no_purchases` | Restore completed without current purchases. |
| `$restore_failed` | Restore failed. |

`$purchase_completed` does not establish verified revenue or Feature access. With native StoreKit and no purchase delegate, Nuxie uses the signed transaction to verify revenue and update Feature access. A delegate's outcome does not provide that transaction, and provider Connectors do not grant Feature access today.

<span id="platform-boundary" />
<span id="next-steps" />

Continue with [purchase verification](/docs/monetization/verify-ios-purchases) and [feature access](/docs/ios-sdk/features-and-entitlements). Other frameworks expose their purchase integrations through the corresponding [SDK reference](/docs/reference).
