Skip to main content

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.


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, and onDismiss.
  • Live checkout state. update events carry a full Checkout snapshot 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. complete delivers the whole Checkout, not a trimmed orderDetails payload.
  • 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

AreaCheckout Sheet Kit (v3)Checkout Kit (v4)
Launchpresent(...) 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 eventsCompletion, failure, and cancel/close callbacksNew start and update events; revised completion, failure, and dismissal APIs
Checkout state updatesNo dedicated update eventupdate events carrying a full, deduplicated Checkout snapshot
LinksLink-click callbackExplicit default, handled, or cancel link policy
ThemingColorScheme (light, dark, automatic, web)Appearance (.app(colorScheme) or .storefront())
Error handlingRecovery hooks, Android exception subclasses, and Swift error enum casesCheckout handles recovery internally; apps handle terminal failures using an error code
Web pixelsPixel events relayed to your appNo app-side pixel callbacks; pixels fire inside checkout
Buyer PIIDelivered unconditionally on completionRedacted by default. Restored on the complete event only with a merchant-granted access scope

  • 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 checkoutUrl from 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

// 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 ShopifyCheckoutKit
// 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.ShopifyCheckout
// 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.

Caution

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

// Before
ShopifyCheckoutSheetKit.configure { config in
config.colorScheme = .automatic
}

// After
ShopifyCheckout.configure { config in
config.appearance = .app(.automatic())
config.logLevel = .warn
}
// 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
}
// 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

// 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)
// Before
CheckoutSheet(checkout: url)
.colorScheme(.automatic)

// After — the view renames and appearance becomes a modifier
ShopifyCheckout(checkout: url)
.appearance(.app(.automatic()))
// 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.
// 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, so start isn't guaranteed when you present a preloaded checkout. Don't make it your only "checkout opened" signal.
  • update — checkout state changed. Carries a full Checkout snapshot, 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 and dismiss follows. If complete already fired, the order stands: a later fail doesn'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

// 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 ... }
// 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)
}
}
// 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 */ },
});
Note

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: start for readiness when it's received, update for changes during checkout, and complete for successful checkout, each carrying the full Checkout.

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

// 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() }
}
// 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()
}
}
// 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

// 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())
// 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())
// 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.


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

// 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
}
// 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 observable CheckoutPreload.
  • On Android, ShopifyCheckout.preload(...) returns a CheckoutPreload with a state property and an optional PreloadStateListener.
  • On React Native, preload(...) returns a subscription with a state property and supports an onStateChange callback.

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

Conceptv3v4
Swift moduleShopifyCheckoutSheetKitShopifyCheckoutKit
Entry point (Swift and Android)ShopifyCheckoutSheetKitShopifyCheckout
SwiftUI viewCheckoutSheetShopifyCheckout (the same type as the entry point)
Clear a preloadinvalidate()clearPreload()
Android packagecom.shopify.checkoutsheetkitcom.shopify.checkoutkit.checkout (entry point and public types)
Android handler baseDefaultCheckoutEventProcessorDefaultCheckoutListener
React Native package@shopify/checkout-sheet-kit@shopify/checkout-kit-react-native
React Native provider / hookShopifyCheckoutSheetProvider / useShopifyCheckoutSheet()ShopifyCheckoutProvider / useShopifyCheckout()
DismissalcheckoutDidCancel / onCheckoutCanceled / 'close'checkoutDidDismiss / onCheckoutDismissed / onDismiss
CompletioncheckoutDidComplete(CheckoutCompletedEvent) / onCheckoutCompleted / 'completed'complete event with a Checkout snapshot (checkoutDidComplete(CheckoutCompleteEvent) / onComplete)
Link clickscheckoutDidClickLink / onCheckoutLinkClickedLink policy: checkoutAction(for:) / onLinkClick returning default, handled, or cancel
Web pixel callbackscheckoutDidEmitWebPixelEvent / onWebPixelEvent / 'pixel'Removed; pixels fire inside checkout
ThemingcolorScheme (and top-level colors on React Native)appearance (appearance.colors for React Native color overrides)


Was this page helpful?