Skip to content

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.

  • 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() and showCheckoutError(error), as described in Checkout mode.
  1. Render two checkout buttons: wire “Checkout with protection” to onProtectionCheckoutClick(true) and “Checkout without protection” to onProtectionCheckoutClick(false).
  2. Call await window.xcotton.sp.addSpFee() (with protection) or await window.xcotton.sp.declineSpFee() (without protection) from the handler.
  3. 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 with SP_NOT_AVAILABLE, and context.reason names 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, call getQuote() first. It rejects with SP_COVERAGE_LIMIT_EXCEEDED and carries the amount, limit, and currency.
    • declineSpFee() resolves { declined: true }.
  4. 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.
  5. 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.

components/protection-checkout-button.tsx
'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>
</>
)
}

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.

ActionExpected 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 rangeaddSpFee() 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 failureThe Promise rejects, an error is shown, and navigation is blocked
Click the second button before the first request settlesThe second click is ignored. Only the first request runs
SymptomCause and fix
Navigation never happens and addSpFee() reports SP_NOT_AVAILABLEThe 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 checkoutA 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 readywindow.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