Skip to content

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.

  • 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 }) or syncCart(cartId) has resolved) before the checkout button becomes interactive.
  • Your storefront provides two functions (imported from lib/cart.ts in 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.
  1. In your checkout button handler, call await window.xcotton.sp.resolveSP().
  2. Navigate to checkout only after it resolves.
  3. 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 retry resolveSP(). That case does not mean the quote or the fee line failed.
  4. Guard against duplicate clicks while the request is in flight.
  5. Do not read the switch state yourself or call addSpFee() directly. The panel manages the switch state internally, and resolveSP() handles all branches. Call addSpFee() / removeSpFee() only when your storefront draws its own toggle.

resolveSP() returns the action result, and your storefront decides whether to keep navigating:

ResultMeaning
{ 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, and context.reason names 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, call getQuote() first. It rejects with SP_COVERAGE_LIMIT_EXCEEDED and carries the amount, limit, and currency.
  • Use declineSpFee() for an explicit decline (clears the fee and writes the refusal marker). In checkout mode it keeps the customer’s switch intent. Do not chain resolveSP() after a decline.
components/checkout-button.tsx
'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
}

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.

ActionExpected result
Toggle the panel switchThe cart lines do not change
Turn the switch on, then click checkoutThe fee line is added, then the browser navigates to checkout
Turn the switch off, then click checkoutThe fee line is removed, then the browser navigates to checkout
Click checkout again before the first request settlesOnly one resolveSP() call is sent
Toggle the switch, then click checkout before the switch update settlesresolveSP() 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 checkoutAn 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 rangeresolveSP() rejects with a specific reason. A message is shown and navigation is blocked
SymptomCause and fix
Error [XC] SP is not readyThrown 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 checkoutVerify 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 limitedShopify 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