Skip to content

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:

  1. Clone the repository: git clone https://github.com/valewnrt/xizor.git
  2. In Xcode: File → Add Package Dependencies… → Add Local… and select the sdks/swift folder (it contains Package.swift for the XizorKit library).
  3. Add the XizorKit product 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:

ParameterDefaultPurpose
apiBaseURLhttps://api.xizor.devOverride for testing
logLevel.warnXizorLogLevel verbosity
salvageDelay2500 msWait before double-checking a reported purchase cancel (StoreKit sometimes mis-reports)
legacyAppAccountTokennilAsync 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.