Checkout mode
In checkout mode, toggling the panel switch does not immediately mutate the cart. The SDK records the customer’s preference and updates the displayed quote. When the customer clicks checkout, your handler calls resolveSP(), the SDK adds or removes the fee line in a single step based on the switch intent, and your page then navigates to checkout.
Before you start
Section titled “Before you start”window.xcotton.sp.getMode()returns'checkout'.- You have initialized the SDK as shown in the Quick start, and the active cart is synchronized (
init({ cartId })orsyncCart(cartId)has resolved) before the checkout button becomes interactive. - Your storefront provides two functions (imported from
lib/cart.tsin the example):getCheckoutUrl(): returns the active cart’s checkout URL.showCheckoutError(error): displays an error message in your UI while keeping the customer on the page.
- In your checkout button handler, call
await window.xcotton.sp.resolveSP(). - Navigate to checkout only after it resolves.
- If it rejects, do not navigate. Display the error and keep the customer on the current page. A rejection means the SDK could not confirm that the fee line matches the intent. Navigating anyway would send the customer to checkout with coverage that contradicts their selection. If the code is
OPERATION_IN_PROGRESS, a switch update or another cart operation is still running: wait for it to settle and retryresolveSP(). That case does not mean the quote or the fee line failed. - Guard against duplicate clicks while the request is in flight.
- Do not read the switch state yourself or call
addSpFee()directly. The panel manages the switch state internally, andresolveSP()handles all branches. CalladdSpFee()/removeSpFee()only when your storefront draws its own toggle.
resolveSP() returns the action result, and your storefront decides whether to keep navigating:
| Result | Meaning |
|---|---|
{ action: 'added', extId } | The fee line was written or adjusted |
{ action: 'removed' } | The fee line was removed |
{ action: 'skipped', protection: 'included', extId } | The fee line already matches the intent (markers may still be written) |
{ action: 'skipped', protection: 'excluded' } | The customer wants no protection and no fee line exists |
- When the intent is on but the current order cannot be protected, the promise rejects with
SP_NOT_AVAILABLE, andcontext.reasonnames the cause, including an amount outside the eligible range or above the coverage limit. Do not navigate and do not treat it as “the customer declined”. To tell whether the total exceeds the coverage limit, callgetQuote()first. It rejects withSP_COVERAGE_LIMIT_EXCEEDEDand carries the amount, limit, and currency. - Use
declineSpFee()for an explicit decline (clears the fee and writes the refusal marker). Incheckoutmode it keeps the customer’s switch intent. Do not chainresolveSP()after a decline.
Full example
Section titled “Full example”'use client'
import { xcottonInit } from '@/lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '@/lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
export function CheckoutButton() { return ( <button type="button" onClick={() => void onCheckoutClick().catch(console.error)} > Checkout </button> )}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
export function CheckoutButton() { return ( <button type="button" onClick={() => void onCheckoutClick().catch(console.error)} > Checkout </button> )}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}<script setup lang="ts">import { xcottonInit } from '~/lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '~/lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
function onClick() { void onCheckoutClick().catch(console.error)}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}</script>
<template> <button type="button" @click="onClick">Checkout</button></template>import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
export function CheckoutButton() { return ( <button type="button" onClick={() => void onCheckoutClick().catch(console.error)} > Checkout </button> )}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}<script setup lang="ts">import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
function onClick() { void onCheckoutClick().catch(console.error)}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}</script>
<template> <button type="button" @click="onClick">Checkout</button></template><script lang="ts"> import { xcottonInit } from '$lib/xcotton' import { getCheckoutUrl, showCheckoutError } from '$lib/cart' let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false } }
function onClick() { void onCheckoutClick().catch(console.error) }
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice }</script>
<button type="button" onclick={onClick}>Checkout</button>import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let checkoutPending = false
async function onCheckoutClick() { if (checkoutPending) return checkoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.resolveSP() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { checkoutPending = false }}
export function CheckoutButton() { return ( <button type="button" onClick={() => void onCheckoutClick().catch(console.error)} > Checkout </button> )}
async function protectionPrice() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') const quote = await sp.getQuote() return quote.totalPrice}protectionPrice() in the example is optional. It shows how to read the current quote. Whether you render that price on the checkout button is your choice. For quote fields, see SpQuote.
Verify and troubleshoot
Section titled “Verify and troubleshoot”| Action | Expected result |
|---|---|
| Toggle the panel switch | The cart lines do not change |
| Turn the switch on, then click checkout | The fee line is added, then the browser navigates to checkout |
| Turn the switch off, then click checkout | The fee line is removed, then the browser navigates to checkout |
| Click checkout again before the first request settles | Only one resolveSP() call is sent |
| Toggle the switch, then click checkout before the switch update settles | resolveSP() rejects with OPERATION_IN_PROGRESS. Retry after it settles and do not navigate in between |
| Turn the switch off, simulate a removal failure, and click checkout | An error is shown and navigation is blocked. The error’s writeOutcome records what the SDK confirmed about the removal, so refresh the cart before retrying |
| Intent on, but the order is out of the eligible range | resolveSP() rejects with a specific reason. A message is shown and navigation is blocked |
| Symptom | Cause and fix |
|---|---|
Error [XC] SP is not ready | Thrown by the example when window.xcotton.sp is undefined. sp is attached once SP initialization succeeds during cart synchronization, and a failed sync can leave it attached. Gate the checkout button on the init / syncCart promise instead of the presence of sp |
Error [XC] Another SDK operation is already in progress (activeOperation=sp.toggle, …) | A switch update or another cart operation has not settled. Wait for it, then retry resolveSP(). Do not navigate |
| No fee line in the cart at checkout | Verify that syncCart receives a valid Storefront cart ID (gid://shopify/Cart/...) and that syncCart(cart.id) is called after every storefront cart write |
Error [XC] Cart requests are rate limited | Shopify rate-limited cart operations. During the cooldown window (the adapter’s retry delay, or 30 seconds by default), the SDK rejects cart calls. Retry shortly |
Error [XC] cart currency does not match the configured market (…) | The cart currency differs from window.xcottonConfig.currency, so the pre-checkout sync was rejected |