Skip to content
Nuxie
Esc
navigateopen⌘Jpreview
On this page

Purchases

Purchase the retained StoreProduct with native StoreKit or an outcome-only delegate

Nuxie resolves a signed Placement to a live App Store product before an Experience is shown. The same retained StoreProduct supplies the price and terms rendered by the paywall and the StoreKit product purchased after the customer taps Buy. There is no second product lookup and no product-ID purchase bridge.

Native StoreKit is the default

No purchase configuration is required for an app that wants Nuxie to own StoreKit:

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

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

The default .full purchase handling mode means Nuxie durably records, synchronizes, and finishes verified native StoreKit transactions. Checkout completion does not wait for Nuxie’s backend. Boolean and unlimited Feature Access can update from authenticated local purchase state; fixed quotas and credits wait for authoritative server state.

Outcome-only purchase delegate

Configure NuxiePurchaseDelegate only when RevenueCat, Superwall, or a custom billing stack must launch checkout or restore. The public contract has two methods:

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

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

PurchaseResult is exactly .purchased, .pending, .cancelled, or .failed(Error). RestoreResult is exactly .restored, .noPurchases, or .failed(Error). Neither result carries a receipt, JWS, transaction identifier, finish closure, provider tag, or compatibility overload.

Custom StoreKit example

This delegate launches StoreKit while Nuxie’s default .full mode retains receipt sync and finish ownership:

// 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)

The delegate must buy product.rawProduct with product.storeKitPurchaseOptions. Those options preserve Nuxie’s deterministic account token, selected Apple billing plan, and any freshly signed introductory-eligibility decision. If the delegate cannot honor the exact retained terms, it must fail instead of substituting another product or price.

One internal completion authority

The callback and StoreKit observer are inputs to one durable transaction pipeline:

  1. Before invoking StoreKit or a delegate, Nuxie persists the exact app, environment, customer, signed release, Experience, Placement, Product, and one stable completion ID.
  2. A direct .purchased callback and a matching StoreKit update both attempt to claim that ID.
  3. The winner durably captures Journey and analytics completion. The loser observes “already completed” and does nothing.
  4. Receipt synchronization and finishing are separately idempotent by StoreKit transaction ID.

The 30-second correlation window for an outcome-only delegate helps decide whether an observed transaction belongs to the active checkout. It does not prevent duplicates by itself; the shared completion claim does. If the callback never returns, StoreKit can still recover the checkout. After the correlation window expires, deterministic account ownership can still route the transaction without reviving stale Experience, Placement, local-grant, or Journey context.

On launch, Nuxie waits for authenticated active Product authority before processing unfinished transactions. An unsigned active Product is native-owned; a Product with a signed Connector Feature Access marker is provider-owned; conflicting Products fail closed. This prevents a cold-start observer from guessing who may sync or finish a transaction.

Explicit StoreKit finishing ownership

NuxieConfiguration.purchaseHandlingMode controls finishing for native-owned StoreKit transactions:

Mode Owner Behavior
.full Nuxie Nuxie durably syncs and finishes verified native transactions.
.observer App/provider Nuxie may observe and sync native transactions, but never calls Transaction.finish().

Configuring a delegate does not silently change this choice. Use purchaseHandlingMode = .observer whenever the app or another SDK owns finishing. A signed provider Connector is the separate receipt authority: provider-owned Products stay out of Nuxie’s native receipt sync and finish path entirely.

RevenueCat

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

// 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)
}

The adapter opens Nuxie’s retained StoreKit product and exact options. RevenueCat observes, posts, and finishes the transaction; restore uses Purchases.restorePurchases(). Until a signed Connector cutover, Nuxie’s observer may also record and synchronize the verified native update, but .observer prevents it from finishing. After cutover, signed provider authority suppresses Nuxie’s native receipt path.

Superwall

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

// 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)
}

The adapter opens the same retained StoreKit product and exact options while Superwall observes and finishes the transaction. Nuxie’s before/after-cutover receipt behavior is the same as RevenueCat’s. Restore uses Superwall.restorePurchases().

Feature Access before and after Connector cutover

Purchase ownership and Feature Access migration are separate decisions:

State Checkout and StoreKit processing App access checks Nuxie result
Before cutover Provider launches, reports, and finishes; Nuxie .observer may also sync a verified native update but never finishes it. Provider SDK The outcome completes the Journey. A separate verified StoreKit update can apply authenticated Boolean/unlimited local grants.
After reviewed Connector cutover Signed provider authority suppresses Nuxie’s native receipt sync and finishing. Nuxie Features The outcome can project immediate Boolean/unlimited local access; Connector state reconciles durable access.

Fixed quotas and credits remain server-authoritative before and after cutover. A delegate outcome cannot invent a balance. On the pre-cutover/native receipt path, the first protected usage can atomically verify one matching unsynchronized StoreKit transaction, grant the server allowance, and consume the requested amount through useFeatureAndWait(). Provider-owned balances arrive through Connector synchronization.

Purchase and restore events

Event Meaning
$purchase_completed The shared completion ID was durably captured for checkout UX.
$purchase_failed Checkout failed.
$purchase_synced A verified native transaction was accepted by Nuxie.
$restore_completed Restore completed and the delegate reported purchases.
$restore_no_purchases Restore completed with no current purchases.
$restore_failed Restore failed.

$purchase_completed is not verified revenue. Revenue and durable entitlement metrics come from verified StoreKit transactions or synchronized provider state.

Platform boundary

This is the final hard-cut iOS API. No legacy product-ID or evidence-bearing delegate mode exists. Android commerce remains a later milestone.

Next steps

Last updated on August 20, 2026

Was this page helpful?