Skip to content

Shopify’s Checkout Sheet Kit makes it simple to perform a checkout inside your Swift native app.

License

Notifications You must be signed in to change notification settings

Shopify/checkout-sheet-kit-swift

Repository files navigation

Shopify Checkout Sheet Kit - Swift

GitHub license Swift Package Manager compatible Tests GitHub Release image

Shopify Checkout Sheet Kit is a Swift Package library that enables Swift apps to provide the world’s highest converting, customizable, one-page checkout within the app. The presented experience is a fully-featured checkout that preserves all of the store customizations: Checkout UI extensions, Functions, branding, and more. It also provides platform idiomatic defaults such as support for light and dark mode, and convenient developer APIs to embed, customize, and follow the lifecycle of the checkout experience. Check out our blog to learn how and why we built the Checkout Sheet Kit.

Requirements

  • Swift 5.7+
  • iOS SDK 13.0+
  • The SDK is not compatible with checkout.liquid. The Shopify Store must be migrated for extensibility

Getting Started

The SDK is an open-source Swift Package library. As a quick start, see sample projects or use one of the following ways to integrate the SDK into your project:

Package.swift

dependencies: [
  .package(url: "https://github.com/Shopify/checkout-sheet-kit-swift", from: "3")
]

Xcode

  1. Open your Xcode project
  2. Navigate to File > Add Package Dependencies...
  3. Enter https://github.com/Shopify/checkout-sheet-kit-swift into the search box
  4. Click Add Package

For more details on managing Swift Package dependencies in Xcode, please see Apple's documentation.

CocoaPods

pod "ShopifyCheckoutSheetKit", "~> 3"

For more information on CocoaPods, please see their getting started guide.

Programmatic Usage

Once the SDK has been added as a dependency, you can import the library:

import ShopifyCheckoutSheetKit

To present a checkout to the buyer, your application must first obtain a checkout URL. The most common way is to use the Storefront GraphQL API to assemble a cart (via cartCreate and related update mutations) and query the checkoutUrl. You can use any GraphQL client to accomplish this and we recommend Shopify's Mobile Buy SDK for iOS to simplify the development workflow:

import Buy

let client = Graph.Client(
  shopDomain: "yourshop.myshopify.com",
  apiKey: "<storefront access token>"
)

let query = Storefront.buildQuery { $0
  .cart(id: "myCartId") { $0
    .checkoutUrl()
  }
}

let task = client.queryGraphWith(query) { response, error in
  let checkoutURL = response?.cart.checkoutUrl
}
task.resume()

The checkoutURL object is a standard web checkout URL that can be opened in any browser. To present a native checkout sheet in your application, provide the checkoutURL alongside optional runtime configuration settings to the present(checkout:) function provided by the SDK:

import UIKit
import ShopifyCheckoutSheetKit

class MyViewController: UIViewController {
  func presentCheckout() {
    let checkoutURL: URL = // from cart object
    ShopifyCheckoutSheetKit.present(checkout: checkoutURL, from: self, delegate: self)
  }
}

SwiftUI Usage

import SwiftUI
import ShopifyCheckoutSheetKit

struct ContentView: View {
  @State var isPresented = false
  @State var checkoutURL: URL?

  var body: some View {
    Button("Checkout") {
      isPresented = true
    }
    .sheet(isPresented: $isPresented) {
      if let url = checkoutURL {
        CheckoutSheet(url: url)
           /// Configuration
           .title("Checkout")
           .colorScheme(.automatic)
           .tintColor(.blue)
           .backgroundColor(.white)

           /// Lifecycle events
           .onCancel {
             isPresented = false
           }
           .onComplete { event in
             handleCompletedEvent(event)
           }
           .onFail { error in
             handleError(error)
           }
           .onPixelEvent { event in
             handlePixelEvent(event)
           }
           .onLinkClick { url in
              if UIApplication.shared.canOpenURL(url) {
                UIApplication.shared.open(url)
              }
           }
           .edgesIgnoringSafeArea(.all)
      }
    }
  }
}

Tip

To help optimize and deliver the best experience, the SDK also provides a preloading API which can be used to initialize the checkout session ahead of time.

Configuration

The SDK provides a way to customize the presented checkout experience via the ShopifyCheckoutSheetKit.configuration object.

colorScheme

By default, the SDK will match the user's device color appearance. This behavior can be customized via the colorScheme property:

// [Default] Automatically toggle idiomatic light and dark themes based on device preference (`UITraitCollection`)
ShopifyCheckoutSheetKit.configuration.colorScheme = .automatic

// Force idiomatic light color scheme
ShopifyCheckoutSheetKit.configuration.colorScheme = .light

// Force idiomatic dark color scheme
ShopifyCheckoutSheetKit.configuration.colorScheme = .dark

// Force web theme, as rendered by a mobile browser
ShopifyCheckoutSheetKit.configuration.colorScheme = .web

tintColor

If the checkout session is not ready and being initialized, a progress bar is shown and can be customized via the tintColor property:

// Use a custom UI color
ShopifyCheckoutSheetKit.configuration.tintColor = UIColor(red: 0.09, green: 0.45, blue: 0.69, alpha: 1.00)

// Use a system color
ShopifyCheckoutSheetKit.configuration.tintColor = .systemBlue

Note: use preloading to optimize and deliver an instant buyer experience.

backgroundColor

While the checkout session is being initialized, the background color of the view can be customized via the backgroundColor property:

// Use a custom UI color
ShopifyCheckoutSheetKit.configuration.backgroundColor = UIColor(red: 0.09, green: 0.45, blue: 0.69, alpha: 1.00)

// Use a system color
ShopifyCheckoutSheetKit.configuration.backgroundColor = .systemBackground

title

By default, the Checkout Sheet Kit will look for a shopify_checkout_sheet_title key in a Localizable.xcstrings file to set the sheet title, otherwise it will fallback to "Checkout" across all locales.

The title of the sheet can be customized by either setting a value for the shopify_checkout_sheet_title key in the Localizable.xcstrings file for your application or by configuring the title property of the ShopifyCheckoutSheetKit.configuration object manually.

// Hardcoded title, applicable to all languages
ShopifyCheckoutSheetKit.configuration.title = "Custom title"

Here is an example of a Localizable.xcstrings containing translations for 2 locales - en and fr.

{
  "sourceLanguage": "en",
  "strings": {
    "shopify_checkout_sheet_title": {
      "extractionState": "manual",
      "localizations": {
        "en": {
          "stringUnit": {
            "state": "translated",
            "value": "Checkout"
          }
        },
        "fr": {
          "stringUnit": {
            "state": "translated",
            "value": "Caisse"
          }
        }
      }
    }
  }
}

SwiftUI Configuration

Similarly, configuration modifiers are available to set the configuration of your checkout when using SwiftUI:

CheckoutSheet(checkout: checkoutURL)
  .title("Checkout")
  .colorScheme(.automatic)
  .tintColor(.blue)
  .backgroundColor(.black)

Note

Note that if the values of your SwiftUI configuration are variable and you are using preload(), you will need to call preload() each time your variables change to ensure that the checkout cache has been invalidated, for checkout to be loaded with the new configuration.

Preloading

Initializing a checkout session requires communicating with Shopify servers, thus depending on the network quality and bandwidth available to the buyer can result in undesirable waiting time for the buyer. To help optimize and deliver the best experience, the SDK provides a preloading "hint" that allows developers to signal that the checkout session should be initialized in the background, ahead of time.

Preloading is an advanced feature that can be toggled via a runtime flag:

ShopifyCheckoutSheetKit.configure {
  $0.preloading.enabled = false // defaults to true
}

Once enabled, preloading a checkout is as simple as calling preload(checkoutUrl) with a valid checkoutUrl.

ShopifyCheckoutSheetKit.preload(checkout: checkoutURL)

Setting enabled to false will cause all calls to the preload function to be ignored. This allows the application to selectively toggle preloading behavior as a remote feature flag or dynamically in response to client conditions — e.g. when data saver functionality is enabled by the user.

ShopifyCheckoutSheetKit.preloading.enabled = false
ShopifyCheckoutSheetKit.preload(checkout: checkoutURL) // no-op

Important considerations

  1. Initiating preload results in background network requests and additional CPU/memory utilization for the client, and should be used when there is a high likelihood that the buyer will soon request to checkout—e.g. when the buyer navigates to the cart overview or a similar app-specific experience.
  2. A preloaded checkout session reflects the cart contents at the time when preload is called. If the cart is updated after preload is called, the application needs to call preload again to reflect the updated checkout session.
  3. Calling preload(checkoutUrl) is a hint, not a guarantee: the library may debounce or ignore calls to this API depending on various conditions; the preload may not complete before present(checkoutUrl) is called, in which case the buyer may still see a spinner while the checkout session is finalized.

Flash Sales

It is important to note that during Flash Sales or periods of high amounts of traffic, buyers may be entered into a queue system.

Calls to preload which result in a buyer being enqueued will be rejected. This means that a buyer will never enter the queue without their knowledge.

When to preload

Calling preload() each time an item is added to a buyer's cart can put significant strain on Shopify systems, which in return can result in rejected requests. Rejected requests will not result in a visual error shown to users, but will degrade the experience since they will need to load checkout from scratch.

Instead, a better approach is to call preload() when you have a strong enough signal that the buyer intends to check out. In some cases this might mean a buyer has navigated to a "cart" screen.

Cache invalidation

Should you wish to manually clear the preload cache, there is a ShopifyCheckoutSheetKit.invalidate() helper function to do so.

Lifecycle management for preloaded checkout

Preloading renders a checkout in a background webview, which is brought to foreground when ShopifyCheckoutSheetKit.present() is called. The content of preloaded checkout reflects the state of the cart when preload() was initially called. If the cart is mutated after preload() is called, the application is responsible for invalidating the preloaded checkout to ensure that up-to-date checkout content is displayed to the buyer:

  1. To update preloaded contents: call preload() once again
  2. To invalidate/disable preloaded content: toggle ShopifyCheckoutSheetKit.preloading.enabled

The library will automatically invalidate/abort preload under following conditions:

  • Request results in network error or non 2XX server response code
  • The checkout has successfully completed, as indicated by the server response
  • When ShopifyCheckoutSheetKit.Configuration object is updated by the application (e.g., theming changes)

A preloaded checkout is not automatically invalidated when checkout sheet is closed. For example, if a buyer loads the checkout and then exits, the preloaded checkout is retained and should be updated when cart contents change.

Additional considerations for preloaded checkout

  1. Preloading is a hint, not a guarantee: the library may debounce or ignore calls depending on various conditions; the preload may not complete before present(checkout:) is called, in which case the buyer may still see a progress bar while the checkout session is finalized.
  2. Preloading results in background network requests and additional CPU/memory utilization for the client and should be used responsibly. For example, conditionally based on state of the client and when there is a high likelihood that the buyer will soon request to checkout.

Monitoring the lifecycle of a checkout session

You can use the ShopifyCheckoutSheetKitDelegate protocol to register callbacks for key lifecycle events during the checkout session:

extension MyViewController: ShopifyCheckoutSheetKitDelegate {
  func checkoutDidComplete(event: CheckoutCompletedEvent) {
    // Called when the checkout was completed successfully by the buyer.
    // Use this to update UI, reset cart state, etc.
  }

  func checkoutDidCancel() {
    // Called when the checkout was canceled by the buyer.
    // Use this to call `dismiss(animated:)`, etc.
  }

  func checkoutDidFail(error: CheckoutError) {
    // Called when the checkout encountered an error and has been aborted. The callback
    // provides a `CheckoutError` enum, with one of the following values:
    // Internal error: exception within the Checkout SDK code
    // You can inspect and log the Erorr and stacktrace to identify the problem.
    case sdkError(underlying: Swift.Error)

    // Issued when the provided checkout URL results in an error related to shop configuration.
    // Note: The SDK only supports stores migrated for extensibility.
    case configurationError(message: String)

    // Unavailable error: checkout cannot be initiated or completed, e.g. due to network or server-side error
    // The provided message describes the error and may be logged and presented to the buyer.
    case checkoutUnavailable(message: String)

    // Expired error: checkout session associated with provided checkoutURL is no longer available.
    // The provided message describes the error and may be logged and presented to the buyer.
    case checkoutExpired(message: String)
  }

  func checkoutDidClickLink(url: URL) {
    // Called when the buyer clicks a link within the checkout experience:
    //  - email address (`mailto:`),
    //  - telephone number (`tel:`),
    //  - web (`http:`)
    // and is being directed outside the application.
  }

  // Issued when the Checkout has emit a standard or custom Web Pixel event.
  // Note that the event must be handled by the consuming app, and will not be sent from inside the checkout.
  // See below for more information.
  func checkoutDidEmitWebPixelEvent(event: PixelEvent) {
    switch event {
      case .standardEvent(let standardEvent):
        recordAnalyticsEvent(standardEvent)
      case .customEvent(let customEvent):
        recordAnalyticsEvent(customEvent)
    }
  }
}

Integrating with Web Pixels, monitoring behavioral data

App developers can use lifecycle events to monitor and log the status of a checkout session.

For behavioural monitoring, Checkout Web Pixel standard and custom events will be relayed back to your application through the checkoutDidEmitWebPixelEvent delegate hook. The responsibility then falls on the application developer to ensure adherence to Apple's privacy policy and local regulations like GDPR and ePrivacy directive before disseminating these events to first-party and third-party systems.

Here's how you might intercept these events:

class MyViewController: UIViewController {
  private func sendEventToAnalytics(event: StandardEvent) {
    // Send standard event to third-party providers
  }

  private func sendEventToAnalytics(event: CustomEvent) {
    // Send custom event to third-party providers
  }

  private func recordAnalyticsEvent(standardEvent: StandardEvent) {
    if hasPermissionToCaptureEvents() {
      sendEventToAnalytics(event: standardEvent)
    }
  }

  private func recordAnalyticsEvent(customEvent: CustomEvent) {
    if hasPermissionToCaptureEvents() {
      sendEventToAnalytics(event: CustomEvent)
    }
  }
}

extension MyViewController: ShopifyCheckoutSheetKitDelegate {
  func checkoutDidEmitWebPixelEvent(event: PixelEvent) {
    switch event {
      case .standardEvent(let standardEvent):
        recordAnalyticsEvent(standardEvent: standardEvent)
      case .customEvent(let customEvent):
        recordAnalyticsEvent(customEvent: customEvent)
    }
  }
}

Note

You may need to augment these events with customer/session information derived from app state. The customData attribute of CustomPixelEvent can take on any shape. As such, this attribute will be returned as a String. Client applications should define a custom data type and deserialize the customData string into that type.

Error handling

In the event of a checkout error occurring, the Checkout Sheet Kit may attempt a retry to recover from the error. Recovery will happen in the background by discarding the failed webview and creating a new "recovery" instance. Recovery will be attempted in the following scenarios:

  • The webview receives a response with a 5XX status code
  • An internal SDK error is emitted

There are some caveats to note when this scenario occurs:

  1. The checkout experience may look different to buyers. Though the sheet kit will attempt to load any checkout customizations for the storefront, there is no guarantee they will show in recovery mode.
  2. The checkoutDidComplete(event:) will be emitted with partial data. Invocations will only receive the order ID via event.orderDetails.id.
  3. checkoutDidEmitWebPixelEvent lifecycle methods will not be emitted.

Should you wish to opt-out of this fallback experience entirely, you can do so by adding a shouldRecoverFromError(error:) method to your delegate controller. Errors given to the checkoutDidFail(error:) lifecycle method, will contain an isRecoverable property by default indicating whether the request should be retried or not.

func shouldRecoverFromError(error: CheckoutError) {
  return error.isRecoverable // default
}

CheckoutError

Type Description Recommendation
.configurationError(code: .checkoutLiquidNotAvailable) checkout.liquid is not supported. Please migrate to checkout extensibility.
.checkoutUnavailable(message: "Forbidden") Access to checkout is forbidden. This error is unrecoverable.
.checkoutUnavailable(message: "Internal Server Error") An internal server error occurred. This error will be ephemeral. Try again shortly.
.checkoutUnavailable(message: "Storefront password required") Access to checkout is password restricted. We are working on ways to enable the Checkout Sheet Kit for usage with password protected stores.
.checkoutExpired(message: "Checkout already completed") The checkout has already been completed If this is incorrect, create a new cart and open a new checkout URL.
.checkoutExpired(message: "Cart is empty") The cart session has expired. Create a new cart and open a new checkout URL.
.sdkError(underlying:) An error was thrown internally. Please open an issue in this repo with as much detail as possible. URL.

Integrating identity & customer accounts

Buyer-aware checkout experience reduces friction and increases conversion. Depending on the context of the buyer (guest or signed-in), knowledge of buyer preferences, or account/identity system, the application can use one of the following methods to initialize a personalized and contextualized buyer experience.

Cart: buyer bag, identity, and preferences

In addition to specifying the line items, the Cart can include buyer identity (name, email, address, etc.), and delivery and payment preferences: see guide. Included information will be used to present pre-filled and pre-selected choices to the buyer within checkout.

Multipass

Shopify Plus merchants using Classic Customer Accounts can use Multipass (API documentation) to integrate an external identity system and initialize a buyer-aware checkout session.

{
  "email": "<Customer's email address>",
  "created_at": "<Current timestamp in ISO8601 encoding>",
  "remote_ip": "<Client IP address>",
  "return_to": "<Checkout URL obtained from Storefront API>"
}
  1. Follow the Multipass documentation to create a Multipass URL and set return_to to be the obtained checkoutUrl
  2. Provide the Multipass URL to present(checkout:)

Important

The above JSON omits useful customer attributes that should be provided where possible and encryption and signing should be done server-side to ensure Multipass keys are kept secret.

Note

Multipass errors are not "recoverable" (See Error Handling) due to their one-time nature. Failed requests containing multipass URLs will require re-generating new tokens.

Shop Pay

To initialize accelerated Shop Pay checkout, the cart can set a walletPreference to 'shop_pay'. The sign-in state of the buyer is app-local. The buyer will be prompted to sign in to their Shop account on their first checkout, and their sign-in state will be remembered for future checkout sessions.

Customer Account API

We are working on a library to provide buyer sign-in and authentication powered by the new Customer Account API—stay tuned.

Offsite Payments

Certain payment providers finalize transactions by redirecting customers to external banking apps. To enhance the user experience for your buyers, you can set up your storefront to support Universal Links on iOS, allowing customers to be redirected back to your app once the payment is completed.

See the Universal Links guide for information on how to get started with adding support for Offsite Payments in your app.

It is crucial for your app to be configured to handle URL clicks during the checkout process effectively. By default, the kit includes the following delegate method to manage these interactions. This code ensures that external links, such as HTTPS and deep links, are opened correctly by iOS.

public func checkoutDidClickLink(url: URL) {
  if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url)
  }
}

Contributing

We welcome code contributions, feature requests, and reporting of issues. Please see guidelines and instructions.

License

Shopify's Checkout Sheet Kit is provided under an MIT License.

About

Shopify’s Checkout Sheet Kit makes it simple to perform a checkout inside your Swift native app.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks