checkoutSheet is an embedded checkout experience that presents a native-feeling bottom sheet inside your app. Users can pay with Apple Pay or enter a card — without ever leaving your app. It automatically reads your dashboard configuration to choose the correct checkout mode (webview, Safari VC, or external Safari).
This is the recommended way to accept payments with ZeroSettle.
SwiftUI
Basic Usage
Use the.checkoutSheet() view modifier to present the checkout sheet:
Custom Header
Add a custom header view above the checkout sheet to show product details, branding, or promotional messaging:Item-Based Presentation
ThecheckoutSheet modifier uses an item-based binding by default. Pass an optional ZSProduct? binding — the sheet presents when the binding is non-nil and automatically sets it back to nil on dismiss:
Non-Dismissible Sheet
Passdismissible: false to prevent the user from swiping to dismiss the sheet. This hides the close button and disables interactive dismiss:
Direct View Usage
You can also useCheckoutSheet as a standalone view, for example inside a .sheet():
The SwiftUI-specific APIs above (
checkoutSheet modifier, custom headers, dismissible:) are iOS-only. On Android, use ZeroSettle.purchase() which handles all presentation automatically. See the Android section below.UIKit
Present the payment sheet from any view controller:dismissible: false, or provide a preloaded checkoutURL and transactionId for instant presentation:
Kotlin / Android
On Android,ZeroSettle.purchase() is a single suspend function that handles creating the PaymentIntent, launching the checkout UI, and returning the completed transaction. There is no separate modifier or preloading step.
Presentation API
ThecheckoutSheet modifier uses an item: binding (Binding<ZSProduct?>) that mirrors SwiftUI’s .sheet(item:) pattern. Set the binding to a product to present the sheet, and it automatically resets to nil on dismiss.
This works for both single-product paywalls (set selectedProduct = product on a “Buy” button) and multi-product stores (set selectedProduct from a list selection).
The modifier shares the same caching and preloading behavior described below.
On Android, there is a single
ZeroSettle.purchase() API that handles all presentation. The checkoutSheet modifier is iOS-only.Performance & Caching
Automatic Preloading
The.checkoutSheet() view modifier automatically preloads both the PaymentIntent and the WKWebView before presenting the sheet. When the user taps “Buy”, the checkout is fully rendered the moment it slides up — no manual preloading is needed.
Built-In Caching
The SDK automatically cachesPaymentIntent results (the checkout URL and transaction ID) for 5 minutes. This means:
- Re-opens after dismiss are near-instant — if the user dismisses and re-opens the sheet for the same product, the API call is skipped entirely. Only the WebView needs to reload.
- The cache is shared across all payment sheet instances in your app.
- The cache invalidates automatically after a successful payment, so the next open gets a fresh
PaymentIntent.
checkoutSheet modifier.
Eager Warm-Up
When you provideuserId in configure(), the first product’s PaymentIntent is automatically warmed up. For most apps, this is sufficient and no manual warm-up is needed.
For advanced scenarios — such as warming up multiple products or a specific product the user is likely to buy — call CheckoutSheet.warmUp() manually:
Preloading and warm-up are not available on Android.
ZeroSettle.purchase() creates the PaymentIntent at call time. The checkout UI is presented as soon as the intent is ready.UX Impact
Understanding what’s cached and what must reload helps you set the right expectations:| Scenario | API Call | WebView Load | Perceived Speed |
|---|---|---|---|
| First open (no warm-up) | Network call | Full load | ~1-2s depending on network |
First open (with warmUp()) | Cache hit | Full load | ~0.5-1s (WebView only) |
| Re-open after dismiss (same product) | Cache hit | Full load | ~0.5-1s (WebView only) |
| Re-open after successful payment | Network call | Full load | ~1-2s (cache was invalidated) |
Manual Preloading
For the directCheckoutSheet init or UIKit’s present method, you can preload the PaymentIntent yourself using CheckoutSheet.preload():
preload() also populates the cache, so subsequent calls for the same product + user are instant. warmUp() is just a convenience wrapper around preload().Declarative Preloading (Swift)
For the.checkoutSheet() modifier, you can pass a preload: parameter to automatically preload payment intents:
Swift
| Strategy | Behavior |
|---|---|
.all | Preloads PaymentIntents for all fetched products |
.specified([ZSProduct]) | Preloads only the specified products |
nil (default) | Preloads only the bound product |
This is an iOS-only SwiftUI feature. On Android,
ZeroSettle.purchase() creates the PaymentIntent at call time.Android
On Android, the checkout sheet is presented as aZSPaymentSheetActivity — a full-screen Activity with an embedded WebView. The SDK handles creating and launching this Activity automatically when you call purchase().
Compose
Activity / Fragment
Checkout Modes
The checkout mode is controlled by your remote config (set from the ZeroSettle dashboard). The SDK reads this automatically at runtime:| Mode | Behavior |
|---|---|
WEBVIEW (default) | Launches ZSPaymentSheetActivity — a full-screen Activity with an embedded WebView. This provides the most native-feeling experience. |
CUSTOM_TAB | Opens the checkout page in a Chrome Custom Tab. Shares cookies with Chrome for autofill support. |
EXTERNAL_BROWSER | Opens the checkout page in the user’s default browser. Use this as a fallback if Custom Tabs are unavailable. |
purchase() reads it from the remote config and routes automatically.
Android Error Handling
ZeroSettle.purchase() is a suspend function that throws on failure. Errors are thrown as subtypes of ZSError:
The iOS-specific APIs (
checkoutSheet modifier, warmUp(), preload()) are not available on Android. On Android, use ZeroSettle.purchase() directly — it creates the PaymentIntent and presents the checkout UI in a single call.How It Works
Under the hood,CheckoutSheet:
- Checks the
PaymentIntentcache (5-min TTL) — on a cache hit, skips the network call - If no cache hit, creates a Stripe
PaymentIntentvia the ZeroSettle API and caches the result - Loads the checkout page in a
WKWebViewwith Apple Pay and card entry - Communicates payment status back to your app via a JavaScript bridge
- Verifies the transaction server-side and invalidates the cache for this product
- Returns a
CheckoutTransactionto your completion handler
Error Handling
The completion handler receives aResult<CheckoutTransaction, Error>. Payment-specific errors are of type PaymentSheetError (iOS) or ZSError (Android):
Error Types
iOS (PaymentSheetError)
| Error | Description |
|---|---|
.cancelled | User dismissed the payment sheet |
.notConfigured | SDK hasn’t been configured yet |
.paymentFailed(PaymentFailureDetail) | Payment failed — inspect .kind and .message for details |
.verificationFailed(String) | Transaction completed but server verification failed |
.preloadFailed(APIErrorDetail) | PaymentIntent creation failed before sheet could open |
.userIdRequired | A userId is required for subscriptions and non-consumable products |
PaymentFailureDetail.Kind
| Kind | Description |
|---|---|
.cardDeclined | Payment method was declined |
.networkError | Network connectivity issue |
.serverError | Server returned an error |
.checkoutError | Checkout flow error |
.unknown | Unclassified failure |
Android (ZSError)
| Error | Description |
|---|---|
ZSError.Cancelled | User dismissed the payment sheet |
ZSError.NotConfigured | SDK hasn’t been configured yet — call configure() first |
ZSError.CheckoutFailed(reason) | Checkout failed — inspect reason for details (see CheckoutFailure below) |
ZSError.UserIdRequired(productId) | A userId is required for subscriptions and non-consumable products |
ZSError.TransactionVerificationFailed(detail) | Transaction completed but server-side verification failed |
Android CheckoutFailure reasons
| Reason | Description |
|---|---|
CheckoutFailure.NetworkUnavailable | No network connection |
CheckoutFailure.StripeError(code, message) | Stripe returned an error (e.g., card declined) |
CheckoutFailure.ServerError(statusCode, message) | Server returned a non-2xx response |
CheckoutFailure.ProductNotFound | The product was not found on the server |
CheckoutFailure.MerchantNotOnboarded | The merchant has not completed Stripe onboarding |
CheckoutFailure.Other(message) | An unclassified error occurred |
.cancelled / ZSError.Cancelled is not a real error — it just means the user changed their mind. Don’t show an error message for cancellations.Promotions
If a product has an active promotion, the payment sheet automatically shows the promotional price. TheZSProduct.promotion property contains the details:
- Percent off — e.g., 50% off
- Fixed amount — e.g., $5 off
- Free trial — e.g., 7 days free
Free Trials
PassfreeTrialDays to offer a trial period before the first charge:
The
freeTrialDays parameter defaults to 0 (no trial). When set, the payment sheet displays the trial period and the user is not charged until the trial expires.
