Checkout 适配模式
在 checkout 模式下,顾客拨动面板开关时不会立即改动购物车,SDK 仅记录顾客的选择并刷新报价。当顾客点击结账按钮时,站点调用 resolveSP(),由 SDK 按当前开关意愿一次性写入或移除费用行,完成后再跳转到结账页。
window.xcotton.sp.getMode()返回'checkout'。- 已按 快速开始 完成初始化,且在结账按钮可点击前已完成当前购物车的同步(即
init({ cartId })或syncCart(cartId)已解析完成)。 - 站点提供以下两个函数(示例代码从
lib/cart.ts导入):getCheckoutUrl():返回当前购物车的结账跳转地址。showCheckoutError(error):在界面上提示错误,并将顾客留在当前页。
- 在结账按钮的点击处理函数中调用
await window.xcotton.sp.resolveSP()。 - 仅在解析成功后跳转到结账页。
- 若 Promise 拒绝(reject),切勿跳转:在页面展示错误提示并让顾客留在当前页。拒绝表示 SDK 无法确认费用行已达到意愿要求,强行跳转会让结账页的保障状态与顾客的选择不符。若
code是OPERATION_IN_PROGRESS,说明拨动开关的处理或其他购物车操作仍在进行,等它结束后重试resolveSP();这种情况不代表报价或费用行失败。 - 在请求处理期间锁定按钮,忽略重复点击。
- 请勿自行读取开关状态再去调用
addSpFee()——开关状态由面板内部维护,resolveSP()已按开关意愿处理所有分支。只有站点自绘开关时才需要直接调用addSpFee()/removeSpFee()。
resolveSP() 返回动作结果,站点据此决定是否继续导航:
| 返回 | 含义 |
|---|---|
{ action: 'added', extId } | 本次写入或调整了费用行 |
{ action: 'removed' } | 本次移除了费用行 |
{ action: 'skipped', protection: 'included', extId } | 费用行已符合意愿(必要标记仍可能写入) |
{ action: 'skipped', protection: 'excluded' } | 顾客不要保障且车上没有费用行 |
- 意愿为开但当前订单不可保时 Promise 会拒绝,
code为SP_NOT_AVAILABLE,context.reason说明具体原因(包括金额超出可保区间或保额上限)。不要跳转,也不要把它当作「顾客不要保障」。需要区分是否超过保额上限时,先调用getQuote():超额时它以SP_COVERAGE_LIMIT_EXCEEDED拒绝并带出金额、额度与币种。 - 本次明确拒绝请使用
declineSpFee()(清费 + 拒绝标记);checkout模式下它保留顾客拨过的开关意愿。拒绝完成后不要再串接resolveSP()。
'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() 为可选辅助函数,演示如何调用 getQuote() 读取报价。是否把价格渲染到结账按钮文案由站点决定。报价结构详见 SpQuote。
| 操作 | 预期结果 |
|---|---|
| 拨动面板开关 | 购物车行不发生变化 |
| 打开开关后点击结账 | 写入费用行,随后跳转至结账页 |
| 关闭开关后点击结账 | 移除费用行,随后跳转至结账页 |
| 首次结账请求完成前再次点击结账 | 仅发出一次 resolveSP() 请求 |
| 拨动开关后立即点击结账(开关处理尚未结束) | resolveSP() 以 OPERATION_IN_PROGRESS 拒绝;等开关处理结束后重试,期间不跳转 |
| 关闭开关后,模拟移除失败再点击结账 | 页面展示错误提示且不跳转;费用行是否已移除以错误的 writeOutcome 为准,刷新购物车后重试 |
| 意愿为开但订单不可保 | resolveSP() 拒绝并给出具体原因,页面展示提示且不跳转 |
| 现象 | 原因与处理方法 |
|---|---|
报错 [XC] SP is not ready | 示例在 window.xcotton.sp 为空时抛出。sp 在购物车同步期间 SP 初始化成功后挂载;同步失败时也可能已挂载,因此结账按钮应以 init / syncCart 的 Promise 成功为准,不能只看 sp 是否存在 |
报错 [XC] Another SDK operation is already in progress (activeOperation=sp.toggle, …) | 拨动开关的处理或其他购物车操作尚未结束。等它结束再重试 resolveSP(),不要跳转 |
| 点击结账后购物车中仍无费用行 | 确认传给 syncCart 的是 Storefront 购物车 ID(形如 gid://shopify/Cart/...),且站点每次改车后均调用了 syncCart(cart.id) |
报错 [XC] Cart requests are rate limited | 触发 Shopify 接口限流。SDK 在冷却期内(按 adapter 返回的等待时间,默认 30 秒)会拒绝购物车请求,请稍后重试 |
报错 [XC] cart currency does not match the configured market (…) | 购物车币种与 window.xcottonConfig.currency 不一致,结账前同步被拒绝 |