Monitor checkout lifecycle
When a buyer moves through checkout, your app needs to respond: mirror live totals, record the order on completion, surface failures, and clean up when the buyer leaves. Checkout Kit delivers all of this through five lifecycle events with the same meaning on every platform, each expressed idiomatically — a delegate on UIKit, view modifiers in SwiftUI, builder callbacks on Android, and per-present callbacks on React Native.
Anchor to RequirementsRequirements
- Checkout Kit installed in your app.
- A valid
checkoutUrlfrom Build a mobile storefront or a cart permalink.
Anchor to The five lifecycle eventsThe five lifecycle events
| Event | When it fires | Payload |
|---|---|---|
start | When checkout becomes ready for the buyer. Events that arrive before your presentation callbacks are bound — including during a background preload — aren't replayed, so start isn't guaranteed when you present a preloaded checkout. | The initial Checkout snapshot |
update | Whenever checkout state changes: line items, totals, messages, or delivery. Consecutive identical snapshots are suppressed. | A full Checkout snapshot, not a diff |
complete | When the buyer places the order. Completion isn't deduplicated, so make what you do here idempotent for the checkout or order. | The final Checkout snapshot |
fail | When checkout can't continue. Checkout closes itself afterwards and dismiss follows. Can fire before start. complete is authoritative: if a fail arrives after it — for example the web content process is killed while the confirmation page is still open — the order was still placed, so don't undo what you recorded on complete. | An error with a stable code |
dismiss | When the checkout presentation closes, whatever the outcome: the buyer closed it, a terminal failure closed it (fail fires first), or the buyer dismissed the confirmation page after completing. It doesn't fire when your app dismisses checkout programmatically, so put cleanup next to your dismiss call too, and keep it idempotent. | None |
Each update carries the complete current state, so you never need to accumulate diffs: replace your copy of the checkout with the new snapshot. Recoverable problems — a validation message, a declined payment the buyer can retry — are checkout state, not failures; they arrive as update events while the buyer resolves them in checkout.
Anchor to Register lifecycle callbacksRegister lifecycle callbacks
Register lifecycle callbacks
Swift
extension MyViewController: CheckoutDelegate {
func checkoutDidStart(_ event: CheckoutStartEvent) {
// Checkout is ready for the buyer.
}
func checkoutDidUpdate(_ event: CheckoutUpdateEvent) {
// Full snapshot: mirror totals or line items into your UI.
renderTotals(event.checkout.totals)
}
func checkoutDidComplete(_ event: CheckoutCompleteEvent) {
// The buyer placed the order. Record it and reset cart state.
recordOrder(event.checkout)
}
func checkoutDidFail(_ event: CheckoutFailureEvent) {
// Checkout can't continue and closes itself; checkoutDidDismiss() follows.
handleCheckoutError(event.error)
}
func checkoutDidDismiss() {
// The surface closed. Don't reset cart state here — completion
// is signaled only by checkoutDidComplete.
}
}Swift (SwiftUI)
ShopifyCheckout(checkout: url)
.onStart { event in
// Checkout is ready for the buyer.
}
.onUpdate { event in
renderTotals(event.checkout.totals)
}
.onComplete { event in
recordOrder(event.checkout)
showCheckout = false
}
.onFail { event in
handleCheckoutError(event.error)
showCheckout = false
}
.onDismiss {
showCheckout = false
}Kotlin
ShopifyCheckout.present(checkoutUrl, activity) {
onStart { event ->
// Checkout is ready for the buyer.
}
onUpdate { event ->
// Full snapshot: mirror totals or line items into your UI.
renderTotals(event.checkout.totals)
}
onComplete { event ->
// The buyer placed the order. Record it and reset cart state.
recordOrder(event.checkout)
}
onFail { event ->
// Checkout can't continue and closes itself; onDismiss follows.
handleCheckoutError(event.error)
}
onDismiss {
// The surface closed. Completion is signaled only by onComplete.
}
}Kotlin (listener)
// Java-friendly alternative: extend DefaultCheckoutListener so you
// inherit the permission, file chooser, and geolocation defaults.
val listener = object : DefaultCheckoutListener() {
override fun onCheckoutStarted(event: CheckoutStartEvent) { }
override fun onCheckoutUpdated(event: CheckoutUpdateEvent) { renderTotals(event.checkout.totals) }
override fun onCheckoutCompleted(event: CheckoutCompleteEvent) { recordOrder(event.checkout) }
override fun onCheckoutFailed(event: CheckoutFailureEvent) { handleCheckoutError(event.error) }
override fun onCheckoutDismissed() { }
}
ShopifyCheckout.present(checkoutUrl, activity, listener)React Native
const shopifyCheckout = useShopifyCheckout();
shopifyCheckout.present(checkoutUrl, {
onStart: ({checkout}) => {
// Checkout is ready for the buyer.
},
onUpdate: ({checkout}) => {
renderTotals(checkout.totals);
},
onComplete: ({checkout}) => {
recordOrder(checkout);
},
onFail: ({error}) => {
// Checkout can't continue and closes itself; onDismiss follows.
handleCheckoutError(error);
},
onDismiss: () => {
// Checkout closed, whatever the outcome.
},
});The dismiss event fires when the buyer closes checkout, whether or not they completed the purchase. Only the complete event signals success — never reset cart state on dismiss alone.
The dismiss event fires when the buyer closes checkout, whether or not they completed the purchase. Only the complete event signals success — never reset cart state on dismiss alone.
When your app presents ShopifyCheckout in its own SwiftUI .sheet, .onDismiss runs after the buyer taps the close button or swipes the sheet down, and after a terminal failure closes checkout. It doesn't run when the buyer taps outside the sheet (for example on iPad) or when your app sets the sheet's isPresented binding to false, because SwiftUI owns that presentation. To handle every dismissal, run your cleanup from the sheet's own onDismiss parameter, such as .sheet(isPresented: $isPresented, onDismiss: refreshCart), and keep it safe to run twice. Add .interactiveDismissDisabled() to ShopifyCheckout to stop dismissal by tapping outside the sheet; this also turns off swipe-down.
Anchor to Handle failuresHandle failures
The failure event carries an error with a stable, machine-readable code — the same set of codes on every platform (string values on iOS and React Native, a CheckoutErrorCode enum on Android), such as cart_expired, cart_completed, invalid_cart, storefront_password_required, http_error, network_error, and sdk_error. Recoverability is decided inside checkout: by the time fail fires, checkout can't continue. Checkout closes itself and dismiss follows. To retry, record the intent in the failure handler and present a fresh checkoutUrl after dismiss; presenting again from the failure handler is ignored while checkout is still closing. The exception is a failure before any presentation opens, such as an unsupported System WebView or a non-HTTPS URL on Android: only fail fires, present() returns null, and there's no dismiss to wait for, so handle those in the failure callback directly.
Anchor to Respond to link tapsRespond to link taps
When the buyer taps a link inside checkout — terms of service, a policy page, an external app — Checkout Kit opens it for you by default (a Safari view controller or UIApplication.open on iOS, Custom Tabs or an intent on Android). To intercept links, return a link action from the link callback:
- Open: let the kit open the link (the default).
- Handled: your app routed the link itself; the kit does nothing.
- Cancel: don't open the link.
Respond to link taps
Swift
// UIKit: implement checkoutAction(for:) on the delegate.
func checkoutAction(for link: CheckoutLink) -> CheckoutLinkAction {
if link.url.host == "help.example.com" {
presentInAppBrowser(link.url)
return .handled
}
return .default
}
// SwiftUI equivalent:
// ShopifyCheckout(checkout: url).onLinkClick { link in .default }Kotlin
ShopifyCheckout.present(checkoutUrl, activity) {
onLinkClick { link ->
if (link.url.host == "help.example.com") {
presentInAppBrowser(link.url)
CheckoutLinkAction.Handled
} else {
CheckoutLinkAction.Default
}
}
}Anchor to Handle file uploads, geolocation, and permissions on AndroidHandle file uploads, geolocation, and permissions on Android
iOS handles file uploads, geolocation, and permissions automatically. React Native handles geolocation prompts on both platforms; on React Native Android, file inputs and other permission requests fall back to the Android defaults (no file chooser, permission requests denied) unless you add a native integration. On Android, handle them in the presentation builder (or override the same methods on DefaultCheckoutListener):
Android-specific callbacks
On Android 11+, opening external links requires a queries element in your AndroidManifest.xml declaring which apps your app interacts with. See the Android queries documentation.
On Android 11+, opening external links requires a queries element in your AndroidManifest.xml declaring which apps your app interacts with. See the Android queries documentation.
Anchor to The Checkout snapshotThe Checkout snapshot
The start, update, and complete events all carry the same Checkout type: the identifier and status, currency, lineItems, totals, discounts, messages, fulfillment, links, and — on completion — the order confirmation. Buyer and payment details are redacted by default; see Privacy compliance for what event payloads contain and how the enhanced buyer events scope changes that.
Checkout Kit doesn't relay web pixel analytics events. Derive journey signal from the lifecycle events — start, the update stream, and complete — and keep conversion analytics server-side. If you're migrating an integration that consumed pixel events, see Upgrade from Checkout Sheet Kit.