Checkout+ mode
In checkout+ mode, the panel should be configured without a toggle switch and only display the protection price. Customers choose whether to include coverage by clicking one of two checkout buttons: “Checkout with protection” calls addSpFee() to write the fee line, and “Checkout without protection” calls declineSpFee() to remove it and record the decline. Your storefront renders both buttons. This mode has no switch intent: it never reads or writes the switch cache, and the modal does not show the intent button.
Before you start
Section titled “Before you start”window.xcotton.sp.getMode()returns'checkoutPlus'.- You have initialized the SDK as shown in the Quick start, and the active cart is synchronized before the checkout buttons become interactive.
- Your storefront provides
getCheckoutUrl()andshowCheckoutError(error), as described in Checkout mode.
- Render two checkout buttons: wire “Checkout with protection” to
onProtectionCheckoutClick(true)and “Checkout without protection” toonProtectionCheckoutClick(false). - Call
await window.xcotton.sp.addSpFee()(with protection) orawait window.xcotton.sp.declineSpFee()(without protection) from the handler. - Navigate only after the promise resolves:
addSpFee()resolves{ added: true, extId }. When the current order cannot be protected (for example, the cart is empty, the subtotal is outside the eligible range, or it exceeds the coverage limit) it rejects withSP_NOT_AVAILABLE, andcontext.reasonnames the cause. Do not navigate. Tell the customer this order is not eligible and let them use “Checkout without protection” instead. To tell whether the total exceeds the coverage limit, callgetQuote()first. It rejects withSP_COVERAGE_LIMIT_EXCEEDEDand carries the amount, limit, and currency.declineSpFee()resolves{ declined: true }.
- A rejection blocks navigation: a network or cart write failure means checkout coverage may not match the customer’s choice, so show the error and stay on the page. If the code is
OPERATION_IN_PROGRESS, another cart operation is still running: wait for it to settle and retry, and do not treat it as a write failure. - Share a single in-flight lock (
protectionCheckoutPending) across both buttons so the two calls never run concurrently.
Both buttons must run through the handler. declineSpFee() removes any leftover fee line and records the decline. If “Checkout without protection” navigates directly to the checkout URL, a fee line added during an earlier “Checkout with protection” attempt stays in the cart.
checkout+ has no switch intent. Do not call resolveSP() (it rejects and points you to the two write entries), and do not infer the current choice from the placeholder intent.
Full example
Section titled “Full example”'use client'
import { xcottonInit } from '@/lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '@/lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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}
export function ProtectionCheckoutButton() { return ( <> <button type="button" onClick={() => void onProtectionCheckoutClick(true).catch(console.error) } > Checkout with protection </button> <button type="button" onClick={() => void onProtectionCheckoutClick(false).catch(console.error) } > Checkout </button> </> )}import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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}
export function ProtectionCheckoutButton() { return ( <> <button type="button" onClick={() => void onProtectionCheckoutClick(true).catch(console.error) } > Checkout with protection </button> <button type="button" onClick={() => void onProtectionCheckoutClick(false).catch(console.error) } > Checkout </button> </> )}<script setup lang="ts">import { xcottonInit } from '~/lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '~/lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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="onProtectionCheckoutClick(true)"> Checkout with protection </button> <button type="button" @click="onProtectionCheckoutClick(false)"> Checkout </button></template>import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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}
export function ProtectionCheckoutButton() { return ( <> <button type="button" onClick={() => void onProtectionCheckoutClick(true).catch(console.error) } > Checkout with protection </button> <button type="button" onClick={() => void onProtectionCheckoutClick(false).catch(console.error) } > Checkout </button> </> )}<script setup lang="ts">import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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="onProtectionCheckoutClick(true)"> Checkout with protection </button> <button type="button" @click="onProtectionCheckoutClick(false)"> Checkout </button></template><script lang="ts"> import { xcottonInit } from '$lib/xcotton' import { getCheckoutUrl, showCheckoutError } from '$lib/cart' let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false } }
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={() => onProtectionCheckoutClick(true)} >Checkout with protection</button><button type="button" onclick={() => onProtectionCheckoutClick(false)} >Checkout</button>import { xcottonInit } from '../lib/xcotton'import { getCheckoutUrl, showCheckoutError } from '../lib/cart'let protectionCheckoutPending = false
async function onProtectionCheckoutClick(withProtection: boolean) { if (protectionCheckoutPending) return protectionCheckoutPending = true try { await xcottonInit() const sp = window.xcotton?.sp if (!sp) throw new Error('[XC] SP is not ready') if (withProtection) await sp.addSpFee() else await sp.declineSpFee() window.location.assign(getCheckoutUrl()) } catch (error) { showCheckoutError(error) } finally { protectionCheckoutPending = false }}
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}
export function ProtectionCheckoutButton() { return ( <> <button type="button" onClick={() => void onProtectionCheckoutClick(true).catch(console.error) } > Checkout with protection </button> <button type="button" onClick={() => void onProtectionCheckoutClick(false).catch(console.error) } > Checkout </button> </> )}protectionPrice() in the example is optional. It shows how to read the coverage price with getQuote(). Whether you render that price on the “Checkout with protection” button is your choice.
Verify and troubleshoot
Section titled “Verify and troubleshoot”| Action | Expected result |
|---|---|
| Click “Checkout with protection” | The fee line is written to the cart, then the page navigates |
| Click “Checkout with protection” when the subtotal is out of range | addSpFee() rejects with SP_NOT_AVAILABLE. A message is shown and navigation is blocked |
| Click “with protection”, return to cart, then click “without protection” | The fee line is removed from the cart, then the page navigates |
| Simulate a cart write failure | The Promise rejects, an error is shown, and navigation is blocked |
| Click the second button before the first request settles | The second click is ignored. Only the first request runs |
| Symptom | Cause and fix |
|---|---|
Navigation never happens and addSpFee() reports SP_NOT_AVAILABLE | The cart has no eligible SP quote (for example, the cart is empty, the subtotal is outside the eligible range, or it exceeds the coverage limit). Call getQuote(): a resolved quote means it is available, and an over-limit total rejects with SP_COVERAGE_LIMIT_EXCEEDED |
resolveSP() rejects with INVALID_ARGUMENT (reason: checkoutPlus has no switch intent) | checkout+ has no switch intent. The two buttons must call addSpFee() and declineSpFee() respectively |
| No fee line in the cart after checkout | A button navigated directly without calling the handler, or the code did not await the promise. Route both buttons through onProtectionCheckoutClick |
Error [XC] SP is not ready | window.xcotton.sp is attached once SP initialization succeeds during cart synchronization, and a failed sync can leave it attached. Enable the buttons after init / syncCart succeeds instead of checking for sp |