iOS quickstart (XizorKit)
Install XizorKit via Swift Package Manager, configure the SDK, make purchases, check entitlements, and present paywalls.
XizorKit is the native Swift SDK: StoreKit 2 purchases, server-verified entitlements, and server-hosted paywalls — each paywall is served as a self-contained web page that the SDK renders in a WKWebView (presented from SwiftUI or UIKit), while purchases, restore, and analytics stay native via a bridge. It supports iOS 17+, macOS 14+, tvOS 17+, watchOS 10+, and visionOS 1+, and is built with strict concurrency.
Install
XizorKit currently lives inside the Xizor monorepo at github.com/valewnrt/xizor, in the sdks/swift directory (a dedicated xizor-swift repository with tagged releases is planned — this page will be updated when it ships). Because Swift Package Manager resolves URL dependencies from a repository root, add it as a local package for now:
- Clone the repository:
git clone https://github.com/valewnrt/xizor.git - In Xcode: File → Add Package Dependencies… → Add Local… and select the
sdks/swiftfolder (it containsPackage.swiftfor theXizorKitlibrary). - Add the
XizorKitproduct to your app target.
Alternatively, reference it from another Package.swift via .package(path: "../xizor/sdks/swift").
Configure
Call Xizor.configure(apiKey:) once, as early as possible — your publishable key (xz_pub_…) is in the dashboard under your app's settings and is safe to embed in the app:
import SwiftUI
import XizorKit
@main
struct MyApp: App {
init() {
Xizor.configure(apiKey: "xz_pub_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
configure accepts optional parameters:
| Parameter | Default | Purpose |
|---|---|---|
apiBaseURL | https://api.xizor.dev | Override for testing |
logLevel | .warn | XizorLogLevel verbosity |
salvageDelay | 2500 ms | Wait before double-checking a reported purchase cancel (StoreKit sometimes mis-reports) |
legacyAppAccountToken | nil | Async provider of an existing appAccountToken when migrating from another SDK |
On configure, the SDK starts observing StoreKit transaction updates (renewals, refunds, Ask to Buy) and performs an initial entitlement sync. The published currentEntitlements property makes the whole SDK usable as a SwiftUI ObservableObject.
Identify users and the appAccountToken
Xizor identifies each install with an appAccountToken — a UUID generated on-device, persisted across launches, and attached to every StoreKit purchase as Transaction.appAccountToken. Because Apple embeds it in the signed transaction, it cryptographically ties App Store purchases (and every later server notification: renewals, refunds, billing retry…) back to the right subscriber, even across reinstalls and restores. The SDK registers it with the backend on launch (identify()); you never manage it yourself.
Anonymous users work out of the box. To attach your own user id after login, hand the token to your backend and call POST /v1/subscribers/alias there with your secret key — existing subscriber histories merge automatically (details):
// After login — bind this install to "user_42" via your backend:
let token = await Xizor.shared.appAccountToken
try await myAPI.linkSubscriber(token: token, userID: "user_42")
// backend: POST https://api.xizor.dev/v1/subscribers/alias (secret key)
Purchases and entitlements
// Purchase — verification and entitlement computation happen server-side.
let result = try await Xizor.shared.purchase(productID: "com.example.pro.yearly")
switch result {
case .success(let entitlements): print("unlocked:", entitlements)
case .cancelled: break // user backed out — not an error
case .pending: break // e.g. Ask to Buy — entitlement arrives later
}
// Gate features:
if Xizor.shared.hasEntitlement("pro") { unlockPro() }
// Fetch fresh entitlements from the backend:
let entitlements = try await Xizor.shared.entitlements()
// Restore:
try await Xizor.shared.restorePurchases()
In SwiftUI, observe currentEntitlements directly:
struct ProGate: View {
@ObservedObject var xizor = Xizor.shared
var body: some View {
if xizor.currentEntitlements.contains(where: { $0.identifier == "pro" && $0.isActive }) {
ProContent()
} else {
LockedContent()
}
}
}
Each XizorEntitlement carries identifier, isActive, productId, expiresAt, willRenew, isTrial, and state (subscribed, expired, in_grace_period, in_billing_retry, revoked, paused). Only subscribed and in_grace_period are active.
The SDK sends the signed StoreKit 2 transaction (jwsRepresentation) to POST /v1/transactions, where the signature chain is verified against Apple's root certificates — entitlements are never computed on-device.
Present a paywall
Paywalls are fetched by placement (e.g. "onboarding") — the dashboard decides which paywall a placement shows. Two ways to present:
Imperative (UIKit presentation, awaits the outcome):
let result = await Xizor.shared.presentPaywall(placement: "onboarding")
if case .purchased = result { unlockPro() }
SwiftUI with XizorPaywallView — a self-loading paywall view for fullScreenCover or sheet:
.fullScreenCover(isPresented: $showPaywall) {
XizorPaywallView(placement: "onboarding") { result in
if case .purchased = result { unlockPro() }
showPaywall = false
}
}
Both return/report a XizorPaywallResult: .purchased(entitlements:), .restored(entitlements:), or .dismissed. Purchase buttons, restore, dismiss, product selection, and funnel events (paywall_view, purchase_started, purchase_completed, paywall_dismiss…) are all handled by the renderer automatically.
Paywall documents can trigger custom actions (e.g. "continue with limited version"). Handle them via the closure or a delegate:
XizorPaywallView(placement: "onboarding", onCustomAction: { name, payload in
if name == "skip" { showPaywall = false }
})
// …or globally:
Xizor.shared.paywallDelegate = self // XizorPaywallDelegate
You can also pass customVariables: to fill {{custom.*}} placeholders in paywall text. Product variables ({{product.price}}, {{trial.days}}, …) are filled from StoreKit automatically.
Paywall fetches are ETag-cached: the SDK revalidates on every presentation, so an edit published in the dashboard (or by your AI agent) is live at the very next presentation — no app update, no waiting.
Track custom events
Xizor.shared.track("onboarding_finished", properties: ["step": .int(4)])
Events appear in the dashboard and feed the analytics and paywall funnels.
Next steps
- Apple setup — required for server notifications and sandbox testing.
- Paywalls — placements, versions, and the document schema.
- MCP server — let your agent edit the paywalls you just wired up.