Initialize the SDK as early as possible in your app’s lifecycle. On iOS, call configure() early, then bootstrap() to fetch products and restore entitlements. On Android, configure() is synchronous and should be called in Application.onCreate(), then call bootstrap() to fetch products and restore entitlements:
Copy
@mainstruct YourApp: App { var body: some Scene { WindowGroup { ContentView() .task { ZeroSettle.shared.configure(.init( publishableKey: "your_live_key" )) try? await ZeroSettle.shared.bootstrap(userId: Auth.currentUser.id) } .zeroSettleHandler() } }}
#if DEBUGlet publishableKey = "your_test_key" // Sandbox - no real charges#elselet publishableKey = "your_live_key" // Live - real payments#endifZeroSettle.shared.configure(.init(publishableKey: publishableKey))
On iOS, checkoutSheet provides the best conversion rates because the user stays in your app. On Android, use ZeroSettle.purchase() which opens a Custom Tab for the web checkout:
Copy
struct PurchaseButton: View { @State private var selectedProduct: Product? let product: ZSProduct var body: some View { Button("Buy \(product.displayName) — \(product.webPrice.formatted)") { selectedProduct = product } .checkoutSheet( item: $selectedProduct, userId: Auth.currentUser.id ) { result in switch result { case .success(let transaction): unlockProduct(transaction.productId) case .failure(let error): if let sheetError = error as? PaymentSheetError, sheetError == .cancelled { // User cancelled — don't show an error return } showError(error) } } }}
Preloading and warmUp() are iOS-specific APIs. On Android, products are cached in the ZeroSettle.products StateFlow after calling bootstrap() or fetchProducts(). No additional preloading step is needed.
The .checkoutSheet() modifier automatically preloads the PaymentIntent and WKWebView before presenting the sheet, so the checkout is fully rendered the moment it slides up.PaymentIntent results are cached for 5 minutes. This means re-opens after dismiss skip the API call entirely. The cache is invalidated automatically after a successful payment.For the fastest first open, call warmUp() right after fetching products:
Copy
.task { let catalog = try await ZeroSettle.shared.fetchProducts() // Pre-cache the PaymentIntent while the user browses if let first = catalog.products.first { await CheckoutSheet.warmUp(productId: first.id, userId: currentUser.id) }}
For the standalone CheckoutSheet init or UIKit’s present method, you can preload manually:
ZeroSettle uses specific error types for different scenarios:iOS:
Error Type
When It’s Thrown
ZSError
SDK configuration and general errors
PaymentSheetError
Payment sheet presentation and payment errors
StoreKitPurchaseError
StoreKit purchase errors
CheckoutError
Legacy checkout view errors (deprecated)
Android:
Error Type
When It’s Thrown
ZSError.NotConfigured
SDK not configured before use
ZSError.ProductNotFound
Product ID not found in catalog
ZSError.Cancelled
User dismissed the checkout
ZSError.CheckoutFailed
Payment failed (with CheckoutFailure reason)
Copy
// checkoutSheet handles errors via its completion handler.checkoutSheet(item: $selectedProduct, userId: userId) { result in switch result { case .success(let transaction): unlockProduct(transaction.productId) case .failure(let error): if let zsError = error as? ZSError { switch zsError { case .notConfigured: fatalError("SDK not configured") case .productNotFound(let id): print("Product \(id) not found") case .networkError(let underlying): showRetry("Network error: \(underlying.localizedDescription)") default: showError(error.localizedDescription) } } else if let sheetError = error as? PaymentSheetError, sheetError == .cancelled { // User cancelled — don't show an error } else { showError(error.localizedDescription) } }}
On iOS, call bootstrap() after configure() to fetch products and cache them in ZeroSettle.shared.products. On Android, call bootstrap() after configure() to fetch and cache products in the ZeroSettle.products StateFlow. There’s no need for a separate fetch call or custom caching on either platform. Use fetchProducts() only for manual refresh (e.g., pull-to-refresh).
Copy
// Products already fetched by bootstrap() — just use themlet products = ZeroSettle.shared.products
warmUp() and PaymentIntent preloading are iOS-specific APIs. On Android, products are cached in the ZeroSettle.products StateFlow after bootstrap() or fetchProducts() --- no additional warm-up step is needed.
When you call bootstrap(), products are fetched and the first product’s PaymentIntent is warmed up automatically. No separate calls are needed.For manual control (e.g., warming up additional products or refreshing later), fetchProducts() and warmUp() are still available:
Copy
struct HomeView: View { var body: some View { TabView { // ... } .task { // Products already fetched by bootstrap() — warm up additional products let products = ZeroSettle.shared.products for product in products.prefix(3) { await CheckoutSheet.warmUp( productId: product.id, userId: Auth.currentUser.id ) } } }}
warmUp() caches the PaymentIntent for 5 minutes. If the user reaches the paywall within that window, the sheet opens without any network delay.
For critical features, verify entitlements server-side:
Copy
# Client: User claims to have premium# Server: Verify with ZeroSettle API before granting access# Your backend calls ZeroSettlecurl -X GET "https://api.zerosettle.io/v1/iap/entitlements?user_id=<user_id>" \ -H "X-ZeroSettle-Key: <publishable_key>"
All ZeroSettle API endpoints — including the REST API — authenticate with your publishable key (zs_pk_live_... or zs_pk_test_...) via the X-ZeroSettle-Key header. This is the same key you use in the SDK.