Checkout+ 适配模式
在 checkout+ 模式下,面板应配置为不带开关,仅展示报价。顾客通过点击不同的结账按钮来决定是否投保:点击「含保护结账」调用 addSpFee() 写入费用行,点击「不含保护结账」调用 declineSpFee() 移除费用行并记录顾客放弃保护。两个按钮均由站点负责渲染。该模式没有开关意愿:不读写开关缓存,弹窗也不展示意愿按钮。
window.xcotton.sp.getMode()返回'checkoutPlus'。- 已按 快速开始 完成初始化,且在结账按钮可点击前已完成当前购物车的同步。
- 站点提供
getCheckoutUrl()和showCheckoutError(error),职责与 Checkout 适配模式 相同。
- 在页面渲染两个结账按钮:「含保护结账」调用
onProtectionCheckoutClick(true),「不含保护结账」调用onProtectionCheckoutClick(false)。 - 在点击处理函数中调用
await window.xcotton.sp.addSpFee()(含保护)或await window.xcotton.sp.declineSpFee()(不含保护)。 - 只有 Promise 成功后才跳转:
addSpFee()成功返回{ added: true, extId };当前订单不可保(例如购物车为空、金额不在可保区间或超过保额上限)时以SP_NOT_AVAILABLE拒绝,context.reason说明具体原因。不要跳转,提示顾客该订单不适用保障,并让顾客改用「不含保护结账」。需要区分是否超过保额上限时,先调用getQuote():超额时它以SP_COVERAGE_LIMIT_EXCEEDED拒绝并带出金额、额度与币种。declineSpFee()成功返回{ declined: true }。
- Promise 拒绝时不得跳转:网络失败或写车失败意味着结账页的保障状态与顾客选择可能不符,应展示错误并留在当前页。若
code是OPERATION_IN_PROGRESS,说明其他购物车操作仍在进行,等它结束后重试,不要把它当作写车失败。 - 两个按钮共用同一个互斥标记(如
protectionCheckoutPending),防止写入与移除操作并发执行。
注意:「不含保护结账」按钮同样必须经过处理函数,不能直接跳转。declineSpFee() 会同时移除购物车中残留的费用行并记录「顾客放弃保护」;若直接跳转,顾客此前点击「含保护结账」后留下的费用行将无法被清除。
checkout+ 模式没有开关意愿,不要调用 resolveSP()(会被拒绝并指引改用两个写入口),也不要从占位意愿推断本次选择。
'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() 为可选辅助函数,演示如何调用 getQuote() 读取保障价格;是否把它渲染到「含保护结账」按钮由站点决定。
| 操作 | 预期结果 |
|---|---|
| 点击「含保护结账」 | 费用行写入购物车,随后跳转至结账页 |
| 订单金额超出可保范围后点击「含保护结账」 | addSpFee() 以 SP_NOT_AVAILABLE 拒绝,展示提示且不跳转 |
| 先点击「含保护结账」,返回购物车后再点击「不含保护结账」 | 购物车中的费用行被移除,随后跳转至结账页 |
| 模拟一次写车失败 | Promise 拒绝,页面展示错误且不跳转 |
| 第一个按钮请求未结束前点击另一个按钮 | 忽略第二次点击,仅执行第一个请求 |
| 现象 | 原因与处理方法 |
|---|---|
始终不跳转,addSpFee() 报 SP_NOT_AVAILABLE | 当前购物车无匹配的 SP 报价(例如购物车为空、金额不在可保区间或超过保额上限)。可先调用 getQuote():它成功即代表报价可用,超保额时以 SP_COVERAGE_LIMIT_EXCEEDED 拒绝 |
调用 resolveSP() 被拒绝,报 INVALID_ARGUMENT(reason: checkoutPlus has no switch intent) | checkout+ 没有开关意愿。两个按钮应分别调用 addSpFee() 与 declineSpFee() |
| 跳转结账后购物车中仍无费用行 | 按钮未调用处理函数便直接跳转,或没有等待 Promise。请确保两个按钮均通过 onProtectionCheckoutClick 处理 |
报错 [XC] SP is not ready | window.xcotton.sp 在购物车同步期间 SP 初始化成功后挂载;同步失败时也可能已挂载。请在 init / syncCart 成功后启用按钮,不要只看 sp 是否存在 |