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:
- Before invoking StoreKit or a delegate, Nuxie persists the exact app, environment, customer, signed release, Experience, Placement, Product, and one stable completion ID.
- A direct
.purchasedcallback and a matching StoreKit update both attempt to claim that ID. - The winner durably captures Journey and analytics completion. The loser observes “already completed” and does nothing.
- 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.