Upgrade from Checkout Sheet Kit
Checkout Kit v4 is the successor to Checkout Sheet Kit. Three things changed at once: the product renamed from Checkout Sheet Kit to Checkout Kit, the checkout lifecycle gained live state updates and changed event payloads, and the three platform repositories consolidated into the Shopify/checkout-kit monorepo. Under the hood, Checkout Kit speaks UCP's Embedded Checkout Protocol to checkout, and web pixels now fire inside checkout the way they do on your online store instead of being relayed to your app. This guide walks through the upgrade one step at a time; throughout, Checkout Sheet Kit means v3 and Checkout Kit means v4.
Anchor to Why upgradeWhy upgrade
Checkout Kit v4 changes these APIs and behaviors:
- Aligned presentation callbacks. SwiftUI, the Android Kotlin builder, and React Native's per-present handlers use
onStart,onUpdate,onComplete,onFail, andonDismiss. - Live checkout state.
updateevents carry a fullCheckoutsnapshot so you can observe and respond to buyer-visible changes while checkout is open. v3 web pixel events exposed some checkout data, but didn't provide a dedicated checkout state update event. - A checkout snapshot on completion.
completedelivers the wholeCheckout, not a trimmedorderDetailspayload. - Link policy. Choose whether Checkout Kit opens a link, your app handles it, or neither opens it.
- Buyer privacy by default. Event payloads are redacted unless the merchant grants your app an access scope, so a default integration can't leak buyer PII.
- Simpler error handling. Checkout handles recovery internally. For terminal failures, use a stable error code instead of branching on Android exception subclasses or Swift error enum cases.
Anchor to What changed at a glanceWhat changed at a glance
| Area | Checkout Sheet Kit (v3) | Checkout Kit (v4) |
|---|---|---|
| Launch | present(...) with a delegate (iOS) or event processor (Android) | ShopifyCheckout.present(...) on iOS and Android; callbacks move to a delegate, listener, or builder that gains the new event methods |
| Lifecycle events | Completion, failure, and cancel/close callbacks | New start and update events; revised completion, failure, and dismissal APIs |
| Checkout state updates | No dedicated update event | update events carrying a full, deduplicated Checkout snapshot |
| Links | Link-click callback | Explicit default, handled, or cancel link policy |
| Theming | ColorScheme (light, dark, automatic, web) | Appearance (.app(colorScheme) or .storefront()) |
| Error handling | Recovery hooks, Android exception subclasses, and Swift error enum cases | Checkout handles recovery internally; apps handle terminal failures using an error code |
| Web pixels | Pixel events relayed to your app | No app-side pixel callbacks; pixels fire inside checkout |
| Buyer PII | Delivered unconditionally on completion | Redacted by default. Restored on the complete event only with a merchant-granted access scope |
Anchor to RequirementsRequirements
- An app using Checkout Sheet Kit v3 on iOS, Android, or React Native.
- iOS 15.0+ with Swift 6 tooling, Android SDK 23+, or React Native 0.77+ with the New Architecture enabled (React Native also raises the floors to iOS 15 and Android SDK 24).
- Authentication as before: Checkout Kit still doesn't create carts or authenticate buyers, so keep attaching buyer context to the cart before presenting (Authenticate checkouts). Multipass is still supported for stores that already use it. New integrations should connect a third-party identity provider.
- A valid
checkoutUrlfrom Build a mobile storefront or a cart permalink.
Anchor to Step 1: Update dependencies and importsStep 1: Update dependencies and imports
v4 ships from the unified monorepo. Update your dependency coordinates and imports — everything your app consumes comes from the kit package itself.
Update dependencies
Swift
// Package.swift — before
.package(url: "https://github.com/Shopify/checkout-sheet-kit-swift", from: "3.8.0")
// Target dependency: "ShopifyCheckoutSheetKit"
// Package.swift — after
.package(url: "https://github.com/Shopify/checkout-kit", from: "4.0.0")
// Target dependency: "ShopifyCheckoutKit"
// Imports — before
import ShopifyCheckoutSheetKit
// Imports — after
import ShopifyCheckoutKitKotlin
// build.gradle — before
implementation("com.shopify:checkout-sheet-kit:3.6.3")
// build.gradle — after
implementation("com.shopify:checkout-kit:4.0.0")
// Imports — before
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit
// Imports — after
import com.shopify.checkoutkit.checkout.ShopifyCheckoutReact Native
// npm install @shopify/checkout-kit-react-native
// Imports — before
import { ShopifyCheckoutSheetProvider, useShopifyCheckoutSheet } from '@shopify/checkout-sheet-kit';
// Imports — after
import { ShopifyCheckoutProvider, useShopifyCheckout } from '@shopify/checkout-kit-react-native';On Android, the package namespace changes from com.shopify.checkoutsheetkit to com.shopify.checkoutkit, so a project-wide find and replace on the package prefix handles most of the import churn. Native iOS apps use Swift Package Manager; CocoaPods remains only for the React Native SDK's native pods.
React Native requires the New Architecture in v4. The kit verifies at startup that the JavaScript package and the bundled native module agree on the event contract, and throws if they've drifted, so always update the iOS and Android pods together with the npm package.
React Native requires the New Architecture in v4. The kit verifies at startup that the JavaScript package and the bundled native module agree on the event contract, and throws if they've drifted, so always update the iOS and Android pods together with the npm package.
Anchor to Step 2: Rename entry points and configurationStep 2: Rename entry points and configuration
On Swift and Android, the ShopifyCheckoutSheetKit entry point becomes ShopifyCheckout, the same type as the SwiftUI view, while the Swift module becomes ShopifyCheckoutKit. On React Native, the provider and hook drop the word "Sheet". Replace the configuration's top-level colorScheme with appearance.
Rename entry points and configuration
Swift
// Before
ShopifyCheckoutSheetKit.configure { config in
config.colorScheme = .automatic
}
// After
ShopifyCheckout.configure { config in
config.appearance = .app(.automatic())
config.logLevel = .warn
}Kotlin
// Before
ShopifyCheckoutSheetKit.configure { config ->
config.colorScheme = ColorScheme.Automatic()
}
// After
ShopifyCheckout.configure { config ->
config.appearance = CheckoutAppearance.App(ColorScheme.Automatic())
// Native sheet chrome: corner radius, max width, scrim, drag handle, snap points
config.sheet = CheckoutSheetOptions()
config.logLevel = LogLevel.WARN
}React Native
// Before
import {ColorScheme} from '@shopify/checkout-sheet-kit';
<ShopifyCheckoutSheetProvider configuration={{ colorScheme: ColorScheme.automatic }}>
{children}
</ShopifyCheckoutSheetProvider>
// After — the provider and configuration field rename
import {CheckoutAppearanceType, CheckoutColorScheme} from '@shopify/checkout-kit-react-native';
<ShopifyCheckoutProvider configuration={{
appearance: {type: CheckoutAppearanceType.app, colorScheme: CheckoutColorScheme.automatic},
}}>
{children}
</ShopifyCheckoutProvider>The v4 Swift Configuration is a Sendable value type, and configure, present, preload, and clearPreload are main-actor isolated. On Android, the version accessor renames from version to the constant VERSION. On iOS, add LSApplicationQueriesSchemes entries to your Info.plist so checkout can deep link into wallet and banking apps.
Anchor to Step 3: Update how you launch checkoutStep 3: Update how you launch checkout
Launching is still "hand checkout a URL". Register callbacks on a delegate, listener, or builder at presentation. On Android, the dismissible return type renames from CheckoutSheetKitDialog to CheckoutHandle. The new name describes a handle to the presentation rather than a specific dialog UI; programmatic dismissal remains available.
Launch checkout
Swift
// Before
let controller = ShopifyCheckoutSheetKit.present(checkout: url, from: self, delegate: self)
// After — same shape; the delegate gains the new event methods (Step 4)
let controller = ShopifyCheckout.present(checkout: url, from: self, delegate: self)Swift (SwiftUI)
// Before
CheckoutSheet(checkout: url)
.colorScheme(.automatic)
// After — the view renames and appearance becomes a modifier
ShopifyCheckout(checkout: url)
.appearance(.app(.automatic()))Kotlin
// Before — present() returns a dismissible CheckoutSheetKitDialog
val dialog: CheckoutSheetKitDialog? =
ShopifyCheckoutSheetKit.present(checkoutUrl, activity, checkoutEventProcessor)
// After — builder callbacks; present() returns a CheckoutHandle
val handle: CheckoutHandle? = ShopifyCheckout.present(checkoutUrl, activity) {
onComplete { event -> /* checkout completed */ }
onFail { event -> /* checkout failed */ }
onDismiss { /* buyer closed checkout */ }
}
// Close checkout from your own UI when you need to
handle?.dismiss()
// Java-friendly alternative: extend DefaultCheckoutListener and pass it
// as the third argument to inherit permission/file-chooser/geolocation defaults.React Native
// Before — global listeners registered once
shopifyCheckout.addEventListener('completed', (event) => { /* ... */ });
shopifyCheckout.present(checkoutUrl);
// After — callbacks are scoped to this presentation
shopifyCheckout.present(checkoutUrl, {
onDismiss: () => { /* checkout closed */ },
onFail: ({error}) => { /* checkout failed */ },
});On Android, the Kotlin builder is the primary surface; from Java, extend DefaultCheckoutListener (the rename of DefaultCheckoutEventProcessor) so you inherit the file chooser, permission, and geolocation defaults. On React Native, the global addEventListener model is removed: register lifecycle callbacks on each present call. They stay active until checkout is dismissed, including after a failure: a terminal failure closes checkout itself and onDismiss follows onFail. There's nothing to tear down manually.
Anchor to Step 4: Adopt the event modelStep 4: Adopt the event model
This is where most of the upgrade effort goes. v4 adds start and update events and revises the completion, failure, and dismissal callbacks. Update your handlers to use these lifecycle events:
start— checkout is ready for the buyer. Events that arrive before your presentation callbacks are bound — including during a background preload — aren't replayed, sostartisn't guaranteed when you present a preloaded checkout. Don't make it your only "checkout opened" signal.update— checkout state changed. Carries a fullCheckoutsnapshot, not a diff; line item, totals, message, and delivery changes all arrive here, with identical consecutive snapshots suppressed.complete— checkout finished successfully, with the final snapshot. Completion isn't deduplicated, so make what you do here idempotent for the checkout or order.fail— checkout can't continue. Checkout closes itself afterwards anddismissfollows. Ifcompletealready fired, the order stands: a laterfaildoesn't reverse it.dismiss— the presentation closed, whether the buyer closed it, a failure closed it, or the buyer dismissed the confirmation page. No outcome implied.
The callback payloads change too. start, update, and complete carry a Checkout snapshot, fail carries an error, and dismiss has no payload. Update the code that reads these events as you migrate your handlers.
Handle completion and live checkout state
Swift
// Before
func checkoutDidComplete(event: CheckoutCompletedEvent) {
recordOrder(event.orderDetails.id)
}
// After — same delegate, new payload
func checkoutDidComplete(_ event: CheckoutCompleteEvent) {
// event.checkout is the full snapshot: id, status, line items, totals, order
recordOrder(event.checkout)
}
func checkoutDidUpdate(_ event: CheckoutUpdateEvent) {
// For example, update the total shown in your app when it changes in checkout.
handleCheckoutUpdate(event.checkout)
}
// SwiftUI equivalents: .onComplete { event in ... } and .onUpdate { event in ... }Kotlin
// Before
override fun onCheckoutCompleted(event: CheckoutCompletedEvent) {
recordOrder(event.orderDetails.id)
}
// After — builder callbacks at presentation
ShopifyCheckout.present(checkoutUrl, activity) {
onComplete { event ->
recordOrder(event.checkout)
}
onUpdate { event ->
handleCheckoutUpdate(event.checkout)
}
}React Native
// Before
shopifyCheckout.addEventListener('completed', (event) => {
recordOrder(event.orderDetails.id);
});
// After — handle lifecycle events for this presentation
shopifyCheckout.present(checkoutUrl, {
onUpdate: ({checkout}) => {
handleCheckoutUpdate(checkout);
},
onComplete: ({checkout}) => {
recordOrder(checkout);
},
onDismiss: () => { /* checkout closed */ },
onFail: ({error}) => { /* checkout failed */ },
});In v3, iOS checkoutDidCancel and Android onCheckoutCanceled could fire when the buyer closed checkout after it completed. In v4, checkoutDidDismiss and onCheckoutDismissed describe closure without implying abandonment. React Native's v3 'close' event becomes onDismiss for consistency. Use complete to tell when checkout finishes successfully, and don't reset cart state on dismissal alone.
In v3, iOS checkoutDidCancel and Android onCheckoutCanceled could fire when the buyer closed checkout after it completed. In v4, checkoutDidDismiss and onCheckoutDismissed describe closure without implying abandonment. React Native's v3 'close' event becomes onDismiss for consistency. Use complete to tell when checkout finishes successfully, and don't reset cart state on dismissal alone.
Anchor to Web pixel callbacks are removedWeb pixel callbacks are removed
Checkout Sheet Kit relayed web pixel events to your app. Checkout Kit doesn't: pixels now fire inside checkout, exactly as they do on your online store. What to change depends on what you used the relay for:
- Syncing analytics platforms. Remove your app's web pixel event handlers and any client-side forwarding of checkout pixel events to analytics platforms. Configure pixels in checkout as you do for your online store.
- In-app logic such as custom thank-you pages or progress UI. Use lifecycle events instead:
startfor readiness when it's received,updatefor changes during checkout, andcompletefor successful checkout, each carrying the fullCheckout.
Anchor to Event payloads redact buyer PII by defaultEvent payloads redact buyer PII by default
Checkout Sheet Kit handed the host app the buyer's contact details, addresses, and order confirmation URL unconditionally. v4 inverts that default: every event payload is redacted before it leaves checkout. Line items, quantities, totals, and currency are always present; the order ID and name (only once the order exists, so never on start or update), discounts (when any apply), and the chosen fulfillment method (when one is selected) are optional — the buyer block, payment block, fulfillment destinations, and the confirmation permalink are stripped.
PII is restored only when both conditions hold: the merchant has granted your app the read_checkout_kit_enhanced_buyer_events access scope on their shop, and the event is complete — no other event ever carries PII. If your integration read buyer name, email, phone, or address from the completion event, plan for that data to be absent and read it server-side from order data instead, or request the scope per shop and handle the redacted shape gracefully either way, because a merchant can decline or revoke it.
Anchor to Step 5: Update error handlingStep 5: Update error handling
v4 removes the client-side recovery hooks (isRecoverable, shouldRecoverFromError, and Android's preRecoveryActions). Recoverability is decided inside checkout: recoverable validation and payment errors stay checkout state the buyer resolves in place, and when checkout reports an unrecoverable error the kit drives the single failure callback. A terminal failure closes checkout itself, then fires the dismiss callback. To retry, record the intent in the failure handler and present a fresh checkout URL after dismissal; presenting again directly from the failure handler is ignored while checkout is still closing.
Error types also flatten into one error value per platform with a shared set of string error codes (such as cart_expired, http_error, network_error, and sdk_error) and a first-class HTTP status code, replacing v3's per-platform exception hierarchies and enum cases.
Handle checkout failure
Swift
// Before
func shouldRecoverFromError(error: CheckoutError) -> Bool {
return error.isRecoverable
}
func checkoutDidFail(error: CheckoutError) { /* ... */ }
// After — only the failure callback remains; match on the error code.
// Checkout closes itself after a terminal failure, then calls checkoutDidDismiss().
func checkoutDidFail(_ event: CheckoutFailureEvent) {
retryWithFreshCart = event.error.code == .cartExpired
if !retryWithFreshCart { showCheckoutUnavailable() }
}
func checkoutDidDismiss() {
// Consume the flag: every presentation ends with a dismissal, including the retry.
let shouldRetry = retryWithFreshCart
retryWithFreshCart = false
if shouldRetry { refreshCartAndPresentAgain() }
}Kotlin
// Before — exception subclasses with a recoverability flag
override fun onCheckoutFailed(error: CheckoutException) {
if (error.isRecoverable) { /* ... */ }
}
// After — the event wraps one exception type; match on the error code.
// Checkout closes itself after a terminal failure, then calls onDismiss.
ShopifyCheckout.present(checkoutUrl, activity) {
onFail { event ->
retryWithFreshCart = event.error.code == CheckoutErrorCode.CART_EXPIRED
if (!retryWithFreshCart) showCheckoutUnavailable()
}
onDismiss {
// Consume the flag: every presentation ends with a dismissal, including the retry.
val shouldRetry = retryWithFreshCart
retryWithFreshCart = false
if (shouldRetry) refreshCartAndPresentAgain()
}
}React Native
// After — onFail is part of the per-present callbacks.
// Checkout closes itself after a terminal failure, then calls onDismiss.
let retryWithFreshCart = false;
shopifyCheckout.present(checkoutUrl, {
onFail: ({error}) => {
retryWithFreshCart = error.code === 'cart_expired';
if (!retryWithFreshCart) showCheckoutUnavailable();
},
onDismiss: () => {
if (retryWithFreshCart) refreshCartAndPresentAgain();
},
});Anchor to Step 6: Move theming to AppearanceStep 6: Move theming to Appearance
v3's ColorScheme had four cases, with web meaning "use the storefront's branding". v4 reshapes this into a two-option Appearance: .app(colorScheme) themes checkout to match your app (light, dark, or automatic), and .storefront() uses the merchant's storefront branding — the equivalent of legacy web, and the new default.
Update theming
Swift
// Before
config.colorScheme = .web
// After — storefront branding is the .web equivalent
config.appearance = .storefront()
// Or theme checkout to match your app
config.appearance = .app(.automatic())Kotlin
// Before
config.colorScheme = ColorScheme.Web()
// After — storefront branding is the Web() equivalent
config.appearance = CheckoutAppearance.Storefront()
// Or theme checkout to match your app
config.appearance = CheckoutAppearance.App(ColorScheme.Automatic())React Native
// Before — match your app's light color scheme
import {ColorScheme} from '@shopify/checkout-sheet-kit';
configuration={{
colorScheme: ColorScheme.light,
colors: {ios: {backgroundColor: '#f0f0e8'}},
}}
// After — v3's ColorScheme.light becomes an app appearance
import {CheckoutAppearanceType, CheckoutColorScheme} from '@shopify/checkout-kit-react-native';
configuration={{
appearance: {
type: CheckoutAppearanceType.app,
colorScheme: CheckoutColorScheme.light,
colors: {ios: {webViewBackgroundColor: '#f0f0e8'}},
},
}}On React Native, move platform-specific color overrides from the top-level colors field into appearance.colors. The v4 color keys match the native SDKs: for example, use webViewBackgroundColor instead of backgroundColor. On Android, native sheet chrome (corner radius, maximum width, scrim color, drag handle, snap points, and dismissal behavior) moves to CheckoutSheetOptions on the configuration. On iOS, the SwiftUI title localization key renames from shopify_checkout_sheet_title to shopify_checkout_kit_title.
Anchor to Step 7: Move link-click handling to the link policyStep 7: Move link-click handling to the link policy
In v4, you can explicitly choose whether Checkout Kit opens a link, your app handles it, or neither does. Checkout Kit opens SDK-handled web links in an in-app browser where available. On iOS and Android, return an action from the link callback:
- Default: let the kit open the link, as it does when you implement nothing.
- Handled: your app routed the link itself; the kit does nothing.
- Cancel: don't open the link.
Intercept link taps
Swift
// Before
func checkoutDidClickLink(url: URL) {
UIApplication.shared.open(url)
}
// After — UIKit delegate; SwiftUI uses .onLinkClick { link in ... }
func checkoutAction(for link: CheckoutLink) -> CheckoutLinkAction {
if link.url.host == "help.example.com" {
presentInAppBrowser(link.url)
return .handled
}
return .default
}Kotlin
// Before
override fun onCheckoutLinkClicked(uri: Uri) {
startActivity(Intent(Intent.ACTION_VIEW, uri))
}
// After — return a policy instead of opening the link yourself
ShopifyCheckout.present(checkoutUrl, activity) {
onLinkClick { link ->
if (link.url.host == "help.example.com") {
presentInAppBrowser(link.url)
CheckoutLinkAction.Handled
} else {
CheckoutLinkAction.Default
}
}
}If your v3 link callback did nothing beyond opening links, then you can remove it if Checkout Kit's default handling meets your needs. Keep a callback if you need custom routing, want to block links, or need to log taps; return the default action after logging (.default on iOS, CheckoutLinkAction.Default on Android) to preserve Checkout Kit's default handling. On React Native, onLinkClick only notifies you; choose the policy with the separate linkAction option ('default', 'handled', or 'cancel').
Anchor to Step 8: Review preloadingStep 8: Review preloading
The preload workflow stays the same: preload a URL, then present the same URL to reuse the warmed session, and call clearPreload() when the cart changes. In v4, you can observe preload state (idle, loading, ready, or failed with a reason; an expired preload returns to idle with an expired reason on every platform) on all three platforms:
- On iOS,
preload(checkout:)returns an observableCheckoutPreload. - On Android,
ShopifyCheckout.preload(...)returns aCheckoutPreloadwith astateproperty and an optionalPreloadStateListener. - On React Native,
preload(...)returns a subscription with astateproperty and supports anonStateChangecallback.
Preload failures never fire the checkout failure callback. A failed preload falls back to a cold load at presentation time; completion and unrecoverable errors invalidate the preload automatically.
A preloaded session is reused only when the presented URL matches the preloaded one exactly, including query parameters — see Preload checkout.
Anchor to Reference: v3 to v4 renamesReference: v3 to v4 renames
| Concept | v3 | v4 |
|---|---|---|
| Swift module | ShopifyCheckoutSheetKit | ShopifyCheckoutKit |
| Entry point (Swift and Android) | ShopifyCheckoutSheetKit | ShopifyCheckout |
| SwiftUI view | CheckoutSheet | ShopifyCheckout (the same type as the entry point) |
| Clear a preload | invalidate() | clearPreload() |
| Android package | com.shopify.checkoutsheetkit | com.shopify.checkoutkit.checkout (entry point and public types) |
| Android handler base | DefaultCheckoutEventProcessor | DefaultCheckoutListener |
| React Native package | @shopify/checkout-sheet-kit | @shopify/checkout-kit-react-native |
| React Native provider / hook | ShopifyCheckoutSheetProvider / useShopifyCheckoutSheet() | ShopifyCheckoutProvider / useShopifyCheckout() |
| Dismissal | checkoutDidCancel / onCheckoutCanceled / 'close' | checkoutDidDismiss / onCheckoutDismissed / onDismiss |
| Completion | checkoutDidComplete(CheckoutCompletedEvent) / onCheckoutCompleted / 'completed' | complete event with a Checkout snapshot (checkoutDidComplete(CheckoutCompleteEvent) / onComplete) |
| Link clicks | checkoutDidClickLink / onCheckoutLinkClicked | Link policy: checkoutAction(for:) / onLinkClick returning default, handled, or cancel |
| Web pixel callbacks | checkoutDidEmitWebPixelEvent / onWebPixelEvent / 'pixel' | Removed; pixels fire inside checkout |
| Theming | colorScheme (and top-level colors on React Native) | appearance (appearance.colors for React Native color overrides) |