Quick start
By the end of this guide, your cart page and cart drawer will render the Shipping Protection (SP) panel. When a customer toggles the switch, the fee line is automatically added to or removed from the cart. In cart mode, your storefront must also call resolveSP() before checkout and block navigation on failure. See Integration modes. The checkout and checkout+ modes differ in how you handle the checkout button. See Integration modes for when each mode writes the fee line.
Before you start
Section titled “Before you start”- Complete the checklist in Before you integrate.
- Confirm with your account manager (AM) that SP is enabled. If your store uses
checkoutorcheckout+integration mode, complete this guide first, then read Integration modes. - Download xcotton-types.ts into your project’s
lib/directory. All examples below import types from that path.
The whole integration at a glance
Section titled “The whole integration at a glance”This minimal, framework-free page runs the entire SP integration in three steps. The sections below show how each piece maps to your framework.
<!-- Step 1: declare the config, then load the SDK --><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script>
<!-- Step 2: the SDK renders the SP panel into this container --><div class="xc-sp-container"></div>
<script type="module"> // Step 3: initialize and sync the page's current cart await window.xcotton.init({ cartId: 'gid://shopify/Cart/your-cart-id', onCartChange: () => reloadCartView(), })
// Then, after every cart write of your own, sync once: // the SDK decides first bind, cart switch, and same-cart refresh // await window.xcotton.syncCart(cart.id)</script>reloadCartView() is your storefront’s own function that re-fetches the cart and updates the UI.
This minimal page is the direct single-entry example: init({ cartId }) also performs the first sync for that cart. The framework examples below use a shared helper: xcottonInit() only assembles the SDK without a cart, and syncXcotton(cartId) handles the first bind, same-cart refreshes, and cart switches. Both forms call init once.
Step 1: Load the SDK
Section titled “Step 1: Load the SDK”Declare window.xcottonConfig before loading sdk.js. The order is strict. In SSR or full-stack frameworks, both scripts must execute before hydration.
window.xcottonConfig fields:
| Field | Required | Description | Example |
|---|---|---|---|
shop | Yes | Shopify store domain | example.myshopify.com |
country | Yes | Active market country code. Must match Shopify Markets | US |
currency | Yes | Active settlement currency for the current market. Must match the cart currency | USD |
language | Yes | UI language code. It sets panel copy and the language for Storefront queries and quotes | en |
publicAccessToken | Yes | Storefront API public access token | — |
spWidgetSelector | No | CSS selector for the SP panel container | .xc-sp-container |
ppWidgetSelector | No | CSS selector for the PP panel container | #xcotton-pp-panel |
The script URL is always https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js with no version segment. The active SDK version is controlled by your store’s back-office configuration, so you never need to change the URL when upgrading.
import Script from 'next/script'
const config = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel',}
export default function RootLayout({ children,}: { children: React.ReactNode}) { return ( <html lang="en"> <body> <Script id="xcotton-config" strategy="beforeInteractive"> {`window.xcottonConfig = ${JSON.stringify(config)}`} </Script> <Script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" strategy="beforeInteractive" crossOrigin="anonymous" /> {children} </body> </html> )}import { Links, Meta, Outlet, Scripts } from '@remix-run/react'
const config = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel',}
export default function App() { return ( <html lang="en"> <head> <Meta /> <Links /> <script dangerouslySetInnerHTML={{ __html: `window.xcottonConfig = ${JSON.stringify(config)}`, }} /> <script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossOrigin="anonymous" /> </head> <body> <Outlet /> <Scripts /> </body> </html> )}<script setup lang="ts">const config = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel',}
useHead({ script: [ { innerHTML: `window.xcottonConfig = ${JSON.stringify(config)}`, tagPosition: 'head', }, { src: 'https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js', tagPosition: 'head', }, ],})</script>
<template> <NuxtPage /></template><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script><script> window.xcottonConfig = { shop: 'example.myshopify.com', country: 'US', currency: 'USD', language: 'en', publicAccessToken: 'your-storefront-public-token', spWidgetSelector: '.xc-sp-container', ppWidgetSelector: '#xcotton-pp-panel', }</script><script src="https://sslstaticus.xcottons.com/shopify-plugins/xmh-idc/headless/sdk.js" crossorigin="anonymous"></script>Step 2: Render the panel container
Section titled “Step 2: Render the panel container”Place an empty container element matching spWidgetSelector on the cart page and inside the cart drawer. Leave it empty. The SDK detects the container and mounts the panel automatically.
Multiple matching containers can coexist, including a temporarily hidden drawer. The SDK creates a panel in each container, sharing the same switch state and quote. Toggling either panel updates the others.
export function CartProtection() { return <div className="xc-sp-container" />}export function CartProtection() { return <div className="xc-sp-container" />}<template><div class="xc-sp-container"></div></template>export function CartProtection() { return <div className="xc-sp-container" />}<template><div class="xc-sp-container"></div></template><div class="xc-sp-container"></div>export function CartProtection() { return <div class="xc-sp-container" />}When the cart drawer closes and reopens, or when client-side navigation returns to the cart page, the SDK automatically restores the panel into the remounted container without firing extra cart or quote requests.
The PP panel container lives on the product page and is rendered by the startup component in Step 3.
Step 3: Initialize and sync the cart
Section titled “Step 3: Initialize and sync the cart”Cart lifecycle management follows two rules:
- At page startup in the shared component example: call
init()once without arguments (the helper wraps this asxcottonInit()) to assemble the SDK only. Do not passcartId. - After every storefront cart write: whenever your code adds items, updates quantities, removes lines, or clears the cart, call
syncCart(cart.id)once (the shared helper wraps this assyncXcotton(cart.id)). It handles the first bind, cart switches, and same-cart refreshes.
Your storefront keeps no binding state. The SDK decides whether this is a first bind, a same-cart refresh, or a cart switch.
First, create a shared helper module so all components interact with the SDK through a single entry point:
import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}import type { XcottonApi } from './xcotton-types'import { reloadCartView } from './cart'
const cartChangeListeners = new Set<() => void>()
export function onXcottonCartChange(listener: () => void): () => void { cartChangeListeners.add(listener) return () => { cartChangeListeners.delete(listener) }}
// Initialize once per page: every component shares one Promise and callback reference.const handleCartChange = () => { void reloadCartView().catch(console.error) // Confirmed cart facts can accompany a business-sync failure; components must read offers and handle errors. for (const listener of cartChangeListeners) listener()}
let initPromise: Promise<void> | undefined
export function xcottonInit(): Promise<void> { const sdk = window.xcotton if (!sdk) return Promise.reject(new Error('[XC] sdk.js has not loaded yet')) if (initPromise) return initPromise // One-shot: repeating this call does not reinitialize; bind a late cartId with syncXcotton(cartId). initPromise = sdk.init({ onCartChange: handleCartChange }) return initPromise}
// Hand storefront writes to the SDK: it decides first bind, cart switch, and same-cart refresh.// Initialize the session first; the storefront keeps no binding state or queue.export async function syncXcotton(cartId: string): Promise<void> { const sdk: XcottonApi | undefined = window.xcotton if (!sdk) throw new Error('[XC] sdk.js has not loaded yet') await xcottonInit() await sdk.syncCart(cartId)}xcottonInit()runs once at page startup and accepts nocartId. It only assembles the SDK. The first bind goes throughsyncXcotton(cartId). The helper caches the single Promise so every component shares the result.initis a one-shot entry point: calling it while initialization is in progress, after success, or after failure rejects explicitly (SDK_INIT_IN_PROGRESS/SDK_ALREADY_INITIALIZED). It never reuses the in-flight Promise as a success, and it cannot replace theadapteroronCartChangeregistered by the first call. If the environment fails to initialize (including PP being enabled but failing to start), the SDK does not keep a half-initialized surface: fix the cause and reload the page.syncXcotton(cartId)first waits forxcottonInit()without a cart ID, then callssdk.syncCart(cartId). It handles the first bind, same-cart refreshes, and cart switches. Call it after every storefront cart write. You do not need to detect cart switches, and a late-arrivingcartIdgoes here too instead of callinginitagain. The SDK runs publicsyncCartcalls in order. Adjacent concurrent calls with the same input merge into one sync and share its result. When a call overlaps an internal cart write that does not go through the public surface, such as a panel switch write, it can reject withOPERATION_IN_PROGRESS. Followrecoveryand retry after the active operation settles. A successfulsyncCartreturns an independentCartafter the required synchronization. Use that Promise to decide whether the business step completed.- Inside
onCartChange, only callreloadCartView(). Never callsyncXcottonorsyncCartinsideonCartChange, or you will trigger an infinite loop (see Keep the cart in sync).
Next, mount the startup component at your application root. It does three things: assembles the SDK once with xcottonInit() without a cart ID. Hands the current cart ID to syncXcotton(cartId), which calls syncCart, when a cart already exists or after the storefront writes the cart. And passes the selected product variant ID to PP pricing (null outside product pages).
'use client'
import { useEffect } from 'react'import { xcottonInit, syncXcotton } from '@/lib/xcotton'import { onCartUpdatedByStorefront } from '@/lib/cart'
export function XcottonStartup({ variant, cartId,}: { variant: string | number | null cartId?: string}) { useEffect(() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) }, []) useEffect(() => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, [cartId]) useEffect(() => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) return () => { void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) } }, [variant]) return <div id="xcotton-pp-panel" hidden={!variant} />}import { useEffect } from 'react'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
export function XcottonStartup({ variant, cartId,}: { variant: string | number | null cartId?: string}) { useEffect(() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) }, []) useEffect(() => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, [cartId]) useEffect(() => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) return () => { void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) } }, [variant]) return <div id="xcotton-pp-panel" hidden={!variant} />}<script setup lang="ts">import { onMounted, onUnmounted, watch } from 'vue'import { xcottonInit, syncXcotton } from '~/lib/xcotton'import { onCartUpdatedByStorefront } from '~/lib/cart'
const props = defineProps<{ variant: string | number | null cartId?: string}>()let cleanup: (() => void) | undefinedonMounted(() => { cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() watch( () => props.cartId, (cartId) => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, { immediate: true }, ) watch( () => props.variant, (variant) => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }, { immediate: true, flush: 'post' }, )})onUnmounted(() => { cleanup?.() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error)})</script>
<template> <div id="xcotton-pp-panel" v-show="variant !== null"></div></template>import { useEffect } from 'react'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
export function XcottonStartup({ variant, cartId,}: { variant: string | number | null cartId?: string}) { useEffect(() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) }, []) useEffect(() => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, [cartId]) useEffect(() => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) return () => { void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) } }, [variant]) return <div id="xcotton-pp-panel" hidden={!variant} />}<script setup lang="ts">import { onMounted, onUnmounted, watch } from 'vue'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
const props = defineProps<{ variant: string | number | null cartId?: string}>()let cleanup: (() => void) | undefinedonMounted(() => { cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() watch( () => props.cartId, (cartId) => { void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }, { immediate: true }, ) watch( () => props.variant, (variant) => { void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }, { immediate: true, flush: 'post' }, )})onUnmounted(() => { cleanup?.() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error)})</script>
<template> <div id="xcotton-pp-panel" v-show="variant !== null"></div></template><script lang="ts"> import { onMount } from 'svelte' import { xcottonInit, syncXcotton } from '$lib/xcotton' import { onCartUpdatedByStorefront } from '$lib/cart'
let { variant, cartId, }: { variant: string | number | null; cartId?: string } = $props() onMount(() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) }) $effect(() => { const current = cartId void (current ? syncXcotton(current) : xcottonInit()).catch(console.error) }) $effect(() => { const current = variant void xcottonInit() .then(() => current === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(current), ) .catch(console.error) return () => { void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) } })</script>
<div id="xcotton-pp-panel" hidden={!variant}></div>import { onMount, onCleanup, createEffect } from 'solid-js'import { xcottonInit, syncXcotton } from '../lib/xcotton'import { onCartUpdatedByStorefront } from '../lib/cart'
export function XcottonStartup(props: { variant: string | number | null cartId?: string}) { onMount(() => { const cleanup = (() => { return onCartUpdatedByStorefront((cart) => syncXcotton(cart.id).catch(console.error), ) })() onCleanup(() => { cleanup() void xcottonInit() .then(() => window.xcotton?.pp?.loadPlans(null)) .catch(console.error) }) }) createEffect(() => { const cartId = props.cartId void (cartId ? syncXcotton(cartId) : xcottonInit()).catch(console.error) }) createEffect(() => { const variant = props.variant void xcottonInit() .then(() => variant === null ? window.xcotton?.pp?.loadPlans(null) : window.xcotton?.pp?.loadPlans(variant), ) .catch(console.error) }) return <div id="xcotton-pp-panel" hidden={!props.variant} />}The startup component keeps observing cartId. Update the prop when an existing cart is restored asynchronously or the active cart changes. The update calls syncXcotton(cartId), and rendering the same ID again adds no requests. Storefront writes still use the separate subscription to syncXcotton(cartId). Do not forward SDK notifications into that subscription.
The example imports two storefront-specific functions from lib/cart.ts that you implement for your app:
| Function | Purpose |
|---|---|
reloadCartView() | Re-queries the cart and updates your UI. Must be read-only and never write to the cart. |
onCartUpdatedByStorefront(handler) | Invokes handler(cart) after your own storefront mutates the cart, returning an unsubscribe function. SDK writes must never fire this event. |
The variant prop is the currently selected variant ID on the product page (string or number) used for PP pricing. See Add product protection. Pass null if your store doesn’t use PP.
Optional: custom SP details button
Section titled “Optional: custom SP details button”To open the SP coverage details modal from your own button outside the panel, call sp.showModal(). The returned Promise resolves as soon as the modal opens, without waiting for the customer to close it. The coverage button inside the modal expresses intent for the active mode: cart mode writes the fee line, checkout mode records intent only, and checkout+ does not show that button.
'use client'
import { xcottonInit } from '@/lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
export function ProtectionDetailsButton() { return ( <button type="button" onClick={() => void onShowSpDetailsClick().catch(console.error)} > Protection details </button> )}import { xcottonInit } from '../lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
export function ProtectionDetailsButton() { return ( <button type="button" onClick={() => void onShowSpDetailsClick().catch(console.error)} > Protection details </button> )}<script setup lang="ts">import { xcottonInit } from '~/lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
function onClick() { void onShowSpDetailsClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Protection details</button></template>import { xcottonInit } from '../lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
export function ProtectionDetailsButton() { return ( <button type="button" onClick={() => void onShowSpDetailsClick().catch(console.error)} > Protection details </button> )}<script setup lang="ts">import { xcottonInit } from '../lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
function onClick() { void onShowSpDetailsClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Protection details</button></template><script lang="ts"> import { xcottonInit } from '$lib/xcotton' async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal() }
function onClick() { void onShowSpDetailsClick().catch(console.error) }</script>
<button type="button" onclick={onClick}>Protection details</button>import { xcottonInit } from '../lib/xcotton'async function onShowSpDetailsClick() { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') await sp.showModal()}
export function ProtectionDetailsButton() { return ( <button type="button" onClick={() => void onShowSpDetailsClick().catch(console.error)} > Protection details </button> )}Verify
Section titled “Verify”These checks use cart mode. In checkout and checkout+, the fee line is written only at checkout. See Integration modes.
Open the cart page and run the following in the browser console:
window.xcotton.sp?.getMode()// 'cart'
await window.xcotton.sp?.getQuote()// { extId: 45678901234, totalPrice: '1.99', totalPriceInt: 199, currencyCode: 'USD', currencySymbol: '$' }Then verify each interaction:
| Action | Expected result |
|---|---|
| Open the cart page | The SP panel renders inside the container |
| Toggle the switch | The fee line is added or removed, and your cart UI refreshes |
Add a product from the storefront and call syncCart(cart.id) | The panel quote updates |
Empty the cart or push the subtotal outside the eligible range, then call syncCart(cart.id) | The fee line is removed automatically |
| Close and reopen the drawer, or navigate back to the cart in a SPA | The panel reappears with no new cart or quote requests in the Network tab |
| Toggle the switch with both the cart page and drawer visible | Both views display the same updated cart |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
Error [XC] sdk.js has not loaded yet | Thrown by the helper module because sdk.js hasn’t executed yet | Ensure both scripts run before hydration and the script URL is reachable |
init() rejects with [XC] SDK initialization failed | SDK environment initialization failed. The parentheses name the module and stage | Verify shop and publicAccessToken. If it persists, send the full error to your account manager (AM). Fix the cause and reload the page. Do not call init again |
init() rejects with code=SDK_INIT_SYNC_FAILED | The environment is ready, but the first cart sync failed | Recover with syncCart(cartId) using the cartId in the error. Do not call init again |
init() rejects with code=OPERATION_IN_PROGRESS | An in-flight operation held the first sync | Wait for the active operation to settle, then recover with syncCart(cartId). Do not call init again |
init() rejects with code=SDK_ALREADY_INITIALIZED | A component called init more than once | Initialize once per page. Route late-arriving cart IDs through syncXcotton(cartId) (or call syncCart(cartId) directly) |
syncCart throws an error starting with [XC] SDK not initialized | syncCart was called before init() finished | Route cart sync calls through syncXcotton after awaiting xcottonInit() |
syncCart() throws [XC] syncCart requires a cartId before the first synchronization | No cart ID was passed and no cart is bound yet | Pass the current cart ID for the first sync |
syncCart throws [XC] cart currency does not match the configured market (…) | The cart currency differs from window.xcottonConfig.currency | Pass the active market’s currency and make sure the cart belongs to that market |
window.xcotton.sp is undefined | The first sync with a cart ID hasn’t finished, or SP is disabled | Await syncCart first. If still undefined, contact your account manager (AM) |
sp is defined, but the panel doesn’t appear | No DOM element matches spWidgetSelector, the container is inside a shadow root, or the current cart does not meet the display conditions (for example, an empty cart or a total outside the eligible range) | Verify the selector with document.querySelectorAll(). Place the container in the light DOM and check that the cart meets the SP display conditions |
| Cart and quote requests fire in an endless loop | syncCart is being called inside onCartChange | Only refresh the UI inside onCartChange |
| Panel price doesn’t match the checkout total | country or currency doesn’t match Shopify Markets | Pass the active market’s values. See Before you integrate |
For the complete list of error messages, see the SDK Reference.
Next steps
Section titled “Next steps”- Keep the cart in sync (required): how
syncCartandonCartChangerelate. - Using
checkoutorcheckout+integration mode: read Integration modes. - Selling PP: read Add product protection.
- Routing cart operations through your own API or BFF: read Use your own cart API.