跳转到内容

SDK 参考

本页列出 Headless SDK 的全部公开接口、类型定义与错误信息。你可以直接下载 xcotton-types.ts(内含 window.xcotton 与 window.xcottonConfig 的全局类型声明)。完整的接入步骤见 快速开始。

全局对象 / 命名空间就绪时机
window.xcottonConfig由站点在加载 sdk.js 之前声明
window.xcottonsdk.js 加载并执行完毕后
window.xcotton.ppinit() 解析完成,且店铺后台已开启 PP
window.xcotton.sp首次带 cartId 的购物车同步完成(由 init({ cartId }) 或 syncCart() 触发),且店铺后台已开启 SP

SDK 的启动配置对象,必须在加载 sdk.js 之前声明。

interface XcottonConfig {
shop: string
country: string
currency: string
language: string
publicAccessToken: string
spWidgetSelector?: string
ppWidgetSelector?: string
}
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',
}
字段必填说明
shop是Shopify 店铺域名,例如 example.myshopify.com
country是当前市场的国家代码(如 US),必须与购物车买家国家及 Shopify Markets 一致
currency是当前市场实际结算使用的币种代码(如 USD),必须与购物车币种一致
language是界面语言代码(如 en),仅影响面板文案,不改变结算币种
publicAccessToken是Shopify Storefront API Public Access Token
spWidgetSelector否SP 容器的 CSS 选择器;首次匹配后保持至节点移除,全部面板共享状态与报价
ppWidgetSelector否PP 面板容器的 CSS 选择器

currency 同时是 SDK 的币种基准:购物车、报价与写入行的币种都必须与它一致。站点切换市场时,应先准备属于新市场的购物车,再在新页面加载前声明新的 country / currency 并重新加载 SDK。运行中修改 window.xcottonConfig 不会改变当前会话。

初始化 SDK,登记 onCartChange 回调与可选的 adapter。

interface XcottonApi {
init(opts?: XcottonInitOptions): Promise<void>
syncCart(input?: string | Cart): Promise<Cart>
isXcottonError(value: unknown): value is XcottonError
sp?: SpApi
pp?: PpApi
}
interface XcottonInitOptions {
cartId?: string
adapter?: XcottonCartAdapter
onCartChange?: (cart: Cart) => unknown
}
const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
await sdk.init({
cartId: 'gid://shopify/Cart/your-cart-id',
onCartChange() {
void reloadCartView().catch(console.error)
},
})
参数说明
cartId可选。页面启动时若已有购物车可直接传入,init 会顺带完成首次同步
adapter可选。覆盖默认的 Storefront 购物车读写实现,可只覆盖部分方法,详见 XcottonCartAdapter
onCartChange可选。在 SDK 写车得到确认后、以及每次 syncCart 结束时触发,参数为已确认的 Cart;没有已确认快照时不触发。详细语义见 购物车同步
  • 一次性入口:init 只执行一次,不承担刷新、补绑、换车或重新配置。初始化进行中、成功后、环境初始化失败后或首次同步失败后再次调用都会被拒绝:进行中为 SDK_INIT_IN_PROGRESS,其余为 SDK_ALREADY_INITIALIZED(context.phase 为 ready、environment_failed、sync_failed 或 busy)。重复调用不会复用原 Promise,也不作为幂等成功。
  • 绑定、刷新与换车:init 不是第二个购物车入口。首次无车初始化成功后,用 syncCart(cartId) 完成首次绑定;刷新与换车也走 syncCart,晚到的 cartId 也不通过重复 init 绑定。
  • 无车初始化:尚未创建购物车时也可调用 init(PP 商品页报价不依赖购物车)。
  • 两阶段恢复:环境初始化失败(含 PP 应启用但初始化失败)时 SDK 不保留半初始化能力,修正失败原因后重新加载页面,不能用 syncCart 绕过;环境已就绪但首次读车或业务同步失败时,用 syncCart(cartId) 从购物车真实状态恢复,不重放已执行的写入,也不要再次 init。
  • 首次同步被占用:首次同步与另一项在途购物车操作冲突时,init 以 OPERATION_IN_PROGRESS 拒绝;此后再调用 init 会得到 SDK_ALREADY_INITIALIZED(context.phase 为 busy)。等当前操作结束后用 syncCart(cartId) 绑定,不要再次 init。
  • 失败处理:init 失败时 Promise 会 reject,错误为 XcottonError(环境阶段为 SDK_INIT_FAILED,首次同步阶段为 SDK_INIT_SYNC_FAILED,首次同步被占用为 OPERATION_IN_PROGRESS);message 以 [XC] 开头并带有失败模块、阶段与具体原因,详见错误处理。

判断任意值是否为 SDK 抛出的 XcottonError。SDK 公开方法(包括未初始化时的失败)使用该结构;站点函数或脚本加载异常不在此列。守卫不依赖 instanceof,跨打包边界或不同执行上下文同样有效。

const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
try {
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
} catch (error) {
if (sdk.isXcottonError(error)) {
console.warn(error.code, error.context, error.recovery.action)
} else {
console.error(error)
}
}
  • 参数:任意值。
  • 返回:value is XcottonError。为 true 时可安全读取 code / message / context / recovery / cause。
  • 不要用 instanceof XcottonError 判断:远程模块与站点可能运行在不同打包或 realm 中。

购物车同步的唯一入口:首次绑定、切换购物车与同车刷新都调用它。SDK 自行判断本次是首次绑定、换车还是同车重读,站点不需要维护第二份绑定身份或同步队列。

const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
  • 输入可为 Storefront 购物车 GID(形如 gid://shopify/Cart/...)或满足 SDK 字段契约的完整 Cart。省略时同步当前已绑定的购物车;尚未绑车时无参调用拒绝。完整 Cart 先校验身份、币种和分页完整性,再被采用。
  • Promise 成功返回必要业务同步完成后的独立 Cart。V4 请求或保障数量调整失败时拒绝,不能放行结账;失败时仍可能通知已确认的购物车事实,通知次数不代表操作次数。
  • 在首次绑定或切换到新的 cartId 时,SDK 会按当前适配模式整理费用行,详见 首次同步购物车时。
  • window.xcotton.sp 仅在带 cartId 的首次同步完成后挂载。
  • 并发与合并:公开的 syncCart 调用按顺序执行;相邻且入参相同的并发调用会合并为一次同步并共享结果。与另一项已持有购物车权限的内部操作重叠时,以 OPERATION_IN_PROGRESS 拒绝。已知同车外部变更使旧事实与旧流程失效;忙时请求另一条车不改变当前绑定,也不表示当前车已变化。等待原操作结束后再决定是否同步。
  • 币种守卫:同步到的购物车总价币种必须与初始化时的 currency 一致,不一致会 reject,不会带着错误币种继续报价或写车。
  • 切勿在 onCartChange 回调中调用该方法,否则会导致无限循环。

可选适配器。当购物车读写必须经过站点自有 API 或 BFF 时实现。支持按需覆盖部分方法,未实现的方法将自动使用内置的 Storefront 默认实现。

interface XcottonCartAdapter {
getCart?(cartId: string): Promise<Cart | null>
addLines?(cartId: string, lines: CartLineInput[]): Promise<Cart>
updateLines?(cartId: string, lines: CartLineUpdateInput[]): Promise<Cart>
removeLines?(cartId: string, lineIds: string[]): Promise<Cart>
updateAttributes?(cartId: string, attributes: AttributeInput[]): Promise<Cart>
}
interface CartLineInput {
merchandiseId: string
quantity?: number | null
attributes?: AttributeInput[] | null
}
interface CartLineUpdateInput {
id: string
quantity?: number | null
attributes?: AttributeInput[] | null
}
interface AttributeInput {
key: string
value: string
}
方法实现要点
getCart仅在确认购物车不存在时返回 null;网络故障、鉴权失败或限流错误必须 reject
addLinesmerchandiseId 为完整的 ProductVariant GID;quantity 省略时默认为 1
updateLinesid 为当前快照中的 CartLine.id;quantity 与 attributes 至少传一项;quantity: 0 表示删除
removeLines删除购物车行的首选方法
updateAttributes始终接收完整的购物车属性数组,[] 表示清空

所有写入方法成功后都必须返回操作后的完整 Cart。行属性 attributes 省略或传 null 表示保留原属性,传 [] 表示清空,传非空数组表示整体替换。完整契约与校验规则见 使用自己的购物车 API。

币种守卫:默认与自定义 adapter 使用同一套规则。SDK 会校验 cart.cost.totalAmount.currencyCode 与初始化时的 currency 一致,也会校验各金额字段之间的币种一致;不一致时在报价或写车前就 reject。自定义 adapter 不要自行换算或替换币种。

限流元数据:触发 Shopify 限流时,请 reject 一个保留以下字段的 Error,SDK 会据此进入冷却并拒绝冷却期内的购物车请求:

const error = new Error('Cart API throttled')
Object.assign(error, { status: 429, code: 'THROTTLED', retryAfterMs: 2000 })
throw error

SDK 用 status: 429、code: 'THROTTLED' 或 message 中的 429 / THROTTLED 识别限流;缺少 retryAfterMs 时按 30 秒冷却。三个信号都没有时,错误按普通 adapter 失败处理,不会进入冷却。不要吞掉限流错误后返回 null,也不要自行重试。

默认 adapter、自定义 adapter 与 onCartChange 共用同一个 Cart 类型(即 Storefront API Cart 的字段子集)。完整类型定义见 xcotton-types.ts。

字段路径说明
id购物车 GID
lines.nodes全部购物车行列表(必须拉取所有分页)
lines.pageInfo分页信息;返回给 SDK 时 hasNextPage 必须为 false
lines.nodes[i].id购物车行 ID。同一规格的多行各自拥有独立 ID,更新行属性后 ID 可能变化
lines.nodes[i].__typename'CartLine' 或 'ComponentizableCartLine'(组合商品按父行金额计价)
lines.nodes[i].cost.amountPerQuantity单件价格
lines.nodes[i].cost.subtotalAmount折扣前的行小计金额
lines.nodes[i].cost.totalAmount行应付金额(必须保留 0 元,切勿用其他金额代替)
lines.nodes[i].merchandise商品规格(ProductVariant):含 GID、SKU、标题与 requiresShipping
lines.nodes[i].merchandise.product所属商品:含 id、title、handle、vendor、productType、isGiftCard
attributes购物车属性列表。读取时 value 可能为 null,写入时必须为字符串
cost.totalAmount购物车总价(MoneyV2:amount 为十进制字符串,currencyCode 为币种)
discountCodes折扣码列表及其是否生效(applicable)

注:Cart 不包含商品图片、checkoutUrl 等纯视图字段;站点渲染界面需要这些字段时,请使用站点自身的查询结果。

window.xcotton.sp 在带 cartId 的首次同步完成后挂载;若店铺未开启 SP,该属性始终为 undefined。

interface SpApi {
addSpFee(): Promise<{ added: true; extId: number }>
removeSpFee(): Promise<{ removed: boolean }>
declineSpFee(): Promise<{ declined: true }>
resolveSP(): Promise<SpResolveResult>
getQuote(): Promise<SpQuote>
getMode(): 'cart' | 'checkout' | 'checkoutPlus'
showModal(): Promise<void>
}
const sp = window.xcotton?.sp
if (!sp) throw new Error('[XC] SP is not ready')
方法作用是否写购物车典型适用模式
addSpFee()立即写入费用行;已有正确费用时幂等成功是checkout+ 含保护、自定义开关
removeSpFee()关闭意愿并移除费用行;本来没有费用行不算失败是自定义开关、自定义「不含保护」
declineSpFee()本次明确拒绝:清费并写拒绝标记是checkout+ 不含保护
resolveSP()按顾客开关意愿对齐费用行,返回动作结果是cart、checkout
getQuote()读取当前购物车的 SP 报价;不可保时拒绝否全部模式
getMode()读取当前实际生效的模式否全部模式
showModal()打开 SP 保障说明弹窗(需要已挂载面板)否全部模式

以上方法在三种适配模式下均可调用(resolveSP() 在 checkout+ 下会拒绝),「典型适用模式」代表各自的设计使用场景。

明确要求立即写入保障费用,包括 checkout+ 的「含保护结账」入口。SDK 按当前报价对齐费用行,已存在正确费用时幂等成功,不重复加行。

const sp = window.xcotton!.sp!
const { added, extId } = await sp.addSpFee()
// added 恒为 true;extId 是最终确认的 SP 规格 ID(不是购物车行 ID),不会为 0
  • 当前订单不可保(空车、金额超出可保范围等)或写车失败时 Promise 拒绝,不再返回 added: false。请按错误的 code / recovery 处理,不要仅凭 Promise 成功判断。
  • 返回值不附带完整 Cart;onCartChange 负责通知确认后的购物车事实。

关闭保障意愿并立即移除费用行。

const sp = window.xcotton!.sp!
const { removed } = await sp.removeSpFee()
  • removed: true 表示本次确认移除了费用行;removed: false 表示经确认本来没有费用行,两种都是正常完成。
  • 即使没有费用行,仍会按模式完成关闭意愿与必要标记;必要标记写入失败时 Promise 拒绝。
  • 与 declineSpFee() 的区别:removeSpFee() 是「关掉保障」的状态操作,不等于顾客本次明确选择「不含保护」。

表达本次明确不购买:清除费用行并写入拒绝确认标记。

const sp = window.xcotton!.sp!
const { declined } = await sp.declineSpFee()
// declined 恒为 true;失败时 Promise 拒绝
适配模式意愿处理
cart关闭开关意愿并持久化,避免下一次同步把已拒绝的费用加回来
checkout保留顾客拨过的开关意愿,不因本次拒绝持久化为关闭
checkout+不读、不写、不续期开关意愿缓存;本次选择只由两个按钮表达
  • 清费与必要标记已成功时,无关的再次报价失败不会把本次拒绝改成失败。
  • 本次拒绝完成后不要再串接 resolveSP():checkout 保留了原意愿,后者会重新按意愿加入保障。

按顾客意愿对齐费用行,并返回动作结果;站点据此决定是否继续导航。

type SpResolveResult =
| { action: 'added'; extId: number }
| { action: 'removed' }
| { action: 'skipped'; protection: 'included'; extId: number }
| { action: 'skipped'; protection: 'excluded' }
const sp = window.xcotton!.sp!
const result = await sp.resolveSP()
// added、removed,以及费用行已经符合意愿的 skipped,都继续结账。调用拒绝时不会执行到这里。
if (result.action === 'added' || result.action === 'removed' || result.action === 'skipped') {
window.location.href = '/checkout'
}
  • added:本次写入或调整了费用行,extId 为最终确认的 SP 规格 ID。
  • removed:本次移除了费用行。
  • skipped / included:费用行已符合意愿(必要确认标记仍可能写入,不代表零请求)。
  • skipped / excluded:意愿为不含保障且车上本来没有费用行。
  • 意愿为开但当前不可保时抛具体业务错误(例如 SP_NOT_AVAILABLE),不会当作 skipped;读取、报价、写入或确认失败均拒绝。
  • checkout+ 下没有开关意愿,调用会被拒绝(INVALID_ARGUMENT);请改调 addSpFee() 或 declineSpFee()。

同步返回客户经理(AM)在后台配置的适配模式。返回值 'checkoutPlus' 即对应文档中的 checkout+ 模式。

读取当前购物车的 SP 报价,不修改购物车属性或购物车行,也不占用购买流程的写权限。读取期间购物车会话或已知事实失效时拒绝,不能返回旧车报价。返回 SpQuote:

{
"extId": 45678901234,
"totalPrice": "1.99",
"totalPriceInt": 199,
"currencyCode": "USD",
"currencySymbol": "$"
}
  • 成功返回即表示当前报价可用,extId 一定有效;不可保通过细分业务错误拒绝(例如 SP_NOT_AVAILABLE,context.reason 给出具体原因),不再用 extId: 0 或 inRange 表示。
  • 合法的零元报价仍是有效结果,必须有有效 extId 和数值为 0 的 totalPriceInt。缺失、非有限、负数或非整数的精确金额会拒绝,不以 0 代替;不可保与零元报价不是同一件事。
  • 报价快照不保证价格永久有效,写费时 SDK 仍按当前购物车状态确认。

打开 SP 保障说明弹窗,复用当前已挂载的面板。

  • 必须在至少一个 SP 面板已挂载且仍连接页面时调用;否则拒绝 SP_PANEL_REQUIRED,recovery.action 为 MOUNT_SP_PANEL。
  • Promise 在弹窗完成渲染后解析,不等待顾客关闭,也不代表顾客已购买。
  • 打开本身不改意愿、不写车;多个面板共用同一弹窗,最后一个面板卸载后弹窗关闭。
  • 弹窗按钮走内部意愿入口:cart 表达意愿并写车,checkout 只表达意愿;checkout+ 不展示该意愿按钮。
interface SpQuote {
extId: number
totalPrice: string
totalPriceInt: number
currencyCode: string
currencySymbol: string
}
字段说明
extIdSP 规格 ID;成功报价恒为有效值,0 不再作为成功返回
totalPrice费用展示串(主货币单位)。< 10000 分保留 2 位小数;>= 10000 分四舍五入为整数元,会丢失分位
totalPriceInt费用精确金额,固定为「主货币单位 × 100」的整数(与币种小数位无关);金额计算与比较必须使用该字段
currencyCode币种代码(如 'USD'、'CAD'),币种身份以它为准
currencySymbol币种符号(如 '$'),仅用于展示;CAD 与 USD 同为 $
分支含义
{ action: 'added', extId }已写入或调整费用行,extId 为最终 SP 规格 ID
{ action: 'removed' }已移除费用行
{ action: 'skipped', protection: 'included', extId }已有费用行符合顾客意愿
{ action: 'skipped', protection: 'excluded' }顾客不要保障且车上没有费用行

window.xcotton.pp 在 init() 完成后挂载,无需等待购物车绑定。若店铺未开启 PP,该属性始终为 undefined。

商品页的便利加购由 pp.addToCart 覆盖;以下方法作为原子原语保留,供自定义加购界面或站点自绘购物车按钮使用,也可组合出与便利封装等价的流程。

interface PpApi {
loadPlans(
variantId: string | number,
variantDetail?: PPVariantDetail,
): Promise<PPPlansResult>
loadPlans(variantId: null, variantDetail?: PPVariantDetail): Promise<null>
canBePopup(context?: PPProductContext): Promise<boolean>
getSelectedPlan(context?: PPProductContext): Promise<PPSelection | null>
setSelectedPlan(
selection: PPSelection | null,
context?: PPProductContext,
): Promise<void>
showModal(context?: PPProductContext): Promise<PPSelection | null>
addProtection(params: {
mainLineId: string
selection: PPSelection
}): Promise<{ added: true }>
removeProtection(params: {
mainLineId: string
}): Promise<PPRemoveProtectionResult>
getCartQuotes(): Promise<PPCartQuotesResult>
getRelations(): Promise<PPRelation[]>
offerProtection(params: {
mainLineId: string
}): Promise<PPOfferProtectionResult>
addToCart(options: PPAddToCartOptions): Promise<PPAddToCartResult>
}
方法作用是否需要已同步的购物车
loadPlans()为当前商品规格请求报价,并渲染 PP 面板否
canBePopup()查询是否满足后台自动弹窗条件(只读)否
getSelectedPlan()读取当前选择(只读,不弹窗)否
setSelectedPlan()设置或清空当前选择并同步面板否
showModal()主动打开方案选择弹窗否
addProtection()为指定主商品行写入明确方案的保障是
removeProtection()按主商品行取消保障是
getCartQuotes()读取当前购物车中仍可补加保障的主商品行报价是
getRelations()读取当前已加保的主商品行与对应保障行的关系是
offerProtection()购物车补购编排:校验后弹窗并写入保障是
addToCart()PDP 便利加购:选择、主商品回调、同步与保障否(回调可创建购物车)

商品级报价、选择与弹窗方法不会创建或绑定购物车;保障方法需要已绑定的购物车;addToCart() 可绑定其主商品回调新建的购物车。

pp.addToCart({ context?, addMainProduct })

Section titled “pp.addToCart({ context?, addMainProduct })”

商品页加购的便利封装:SDK 锁定本次商品上下文并取得选择,调用一次站点的主商品写入回调,随后自动同步购物车并写入保障行。与分步组合(getSelectedPlan() → 写主商品 → syncCart() → addProtection())完成相同业务,站点可按现有加购流程二选一。

context 省略时使用调用开始时仍有效的商品页上下文。首页、列表页等没有 loadPlans 上下文的位置不要调用本方法,改为写主商品后调用 syncCart(),保障入口留给购物车。缺少上下文会以 INVALID_ARGUMENT(path=context)拒绝,并给出普通加购路径的恢复指引。

interface PPAddToCartOptions {
context?: PPProductContext
addMainProduct: (
attributes: AttributeInput[],
context: PPProductContext,
) => Promise<{ cartId: string; mainLineId: string }>
}
type PPAddToCartResult = { cartId: string; mainLineId: string } & {
protection: 'added' | 'skipped'
}
declare function addMainProduct(
variantId: string,
quantity: number,
attributes: AttributeInput[],
): Promise<{ cart: { id: string }; mainLine: { id: string } }>
const sdk = window.xcotton
if (!sdk?.pp) throw new Error('[XC] Product Protection is not enabled')
const result = await sdk.pp.addToCart({
addMainProduct: async (attributes, context) => {
const { cart, mainLine } = await addMainProduct(
context.mainVariantId,
1,
attributes,
)
return { cartId: cart.id, mainLineId: mainLine.id }
},
})
字段说明
context可选。省略时使用调用开始时由 loadPlans 建立的全局商品上下文;显式传入则校验句柄有效性与代次。无上下文或已失效会 reject
addMainProduct只写主商品的回调,接收本次要写的主商品行属性(未选方案时为空数组)与本次锁定的上下文。SDK 只调用一次
cartId结果中的购物车 ID,取自回调返回值(可以是回调新建的购物车)
mainLineId结果中的主商品行 ID(CartLine.id),可能是新建行,也可能是 Shopify 合并后的已有行
protectionadded 主商品与保障均已确认;skipped 本次没有选择保障,主商品与同步已完成

加购组合的执行顺序:

  1. 锁定商品上下文;已有选择直接使用,未选且后台允许弹窗时先弹窗(取消仍继续加主商品),随后把 selection.mainLineAttributes 交给回调。
  2. 调用一次 addMainProduct(attributes, context)。
  3. 用回调返回的 cartId 同步购物车,并重读确认 mainLineId 的规格与本次上下文一致。
  4. 若有选择,按返回的 mainLineId 写入保障行。

回调与失败约定:

  • 回调只写主商品。不要在回调内再次调用 SDK(包括 init、syncCart、pp.addToCart),也不要读页面当前规格或重置 PDP 报价。
  • SDK 不重放回调、不回滚已成功的主商品。回调最多执行一次。
  • 成功只返回 added / skipped;失败一律 reject XcottonError。主商品已确认、后续同步或保障失败时为 PP_WRITE_CONFIRMATION_FAILED,context 含 stage、mainProduct、protection、writeOutcome;此时主商品保留,不要诱导顾客重复加购,按恢复指引刷新购物车或补加保障。
  • 回调执行前若操作权限已失效,SDK 不调用回调,mainProduct 与 writeOutcome 均为 not_sent。回调已执行但未能确认结果时,按可用的错误元数据表达;没有足够证据则为 unknown,不能据此断言主商品已写入或未写入。
  • pp.addToCart 与 pp.offerProtection 共用购买流程锁;流程进行中其它选择/购买入口报 PP_SELECTION_IN_PROGRESS。

购买流程从选择开始到主商品回调、同步和保障确认结束,共享购物车操作权限。期间 SP 写入与外部 syncCart 冲突会报忙;不要在 addMainProduct 回调内重入这些 SDK 入口。SDK 不会因冲突或超时重放回调。

loadPlans(variantId) 在报价的同时把该商品设为全局当前上下文,并返回 { context, options }。context 是 SDK 管理的商品上下文句柄:

  • 省略 context 时,调用开始时捕获全局当前上下文;异步等待后不会漂移到新商品。
  • 显式传入 context 时,本次调用只属于该商品。句柄无效(不是 SDK 返回的对象)以 INVALID_ARGUMENT reject;句柄过期(切换过规格、离开过商品页,或 A→B→A 后回到原规格)以 PP_CONTEXT_CHANGED reject,不会回退到全局上下文。
  • 不要手工构造句柄,也不要长期缓存后跨商品复用。
const variantId = 'gid://shopify/ProductVariant/1234567890'
const { context, options } = await window.xcotton!.pp!.loadPlans(variantId)
await window.xcotton!.pp!.canBePopup(context)
await window.xcotton!.pp!.getSelectedPlan(context)
// 普通单商品页面可以省略 context。
await window.xcotton!.pp!.canBePopup()

所有方法返回的对象均为独立快照,修改返回对象不会影响 SDK 内部状态。

为商品页当前选中的商品规格请求报价并渲染 PP 面板,返回 { context, options }:

  • context:SDK 管理的商品上下文句柄,可传给选择相关方法;也可以省略,默认使用全局当前上下文。

  • options:PPSelection[],每项包含方案与 mainLineAttributes,可直接提交给 setSelectedPlan,也供自建加购界面读取方案名、价格与行属性。

  • variantId:主商品规格 ID(字符串或数字)。SDK 会按初始化时配置的市场查询商品归属与价格。

  • 传入 null 表示离开商品上下文:SDK 会取消进行中的报价、关闭已打开的方案弹窗、清空 PP 面板与当前选择,并返回 null。

  • 成功但该商品没有可用方案时返回 { context, options: [] },上下文仍然有效(此时 canBePopup() 为 false、getSelectedPlan() 为 null、showModal() 抛 PP_NO_OPTIONS)。

  • 发起新的调用会自动取消尚未完成的旧报价;被后续上下文替代的旧调用会以 PP_CONTEXT_CHANGED reject,不会返回空方案冒充正常完成。

  • 同规格重新报价保留仍有效的选择;顾客主动反选后,重新报价不会被 hasSelectedDefault 重新选中。

  • variantDetail(可选):当站点已持有当前市场下的规格详情时传入,可省去 SDK 内部的一次 Storefront 查询。

interface PPVariantDetail {
product: { id: string }
title: string
sku?: string | null
price: { amount: string; currencyCode: string }
}
字段说明
product.id规格所属商品的 GID,SDK 按它取可用方案
title规格标题
sku可选,商品 SKU
price.amount当前市场价,Storefront 的十进制字符串(如 '29.99'),不要传「分」
price.currencyCode价格币种,必须与初始化时的 currency 一致

SDK 按现有报价协议把 amount 转成固定 ×100 的整数并校验币种;传入与当前市场不符的价格或币种会直接导致报价错误。报价请求失败、后端拒绝或响应缺少方案身份都会 reject 具体错误,不会吞成空方案。

只查询当前商品是否满足后台「加购时自动弹窗」条件,不弹窗、不改选择、不写车。

  • 配置不允许或没有可用方案时返回 false;true 只代表查询当时满足条件,随后主动弹窗仍会独立检查上下文。
  • 等待同规格最新报价后再判断。
  • 报价或配置读取失败会 reject 具体错误,不会伪装成 false。
  • 等待期间换规格或离开商品页会以 PP_CONTEXT_CHANGED reject。

只读返回当前规格下已成形的选择;没有选择或顾客已取消时返回 null。

  • 不自动选方案、不弹窗、不写车。
  • 等待同规格最新报价,不会返回上一轮价格对应的过期方案。
  • 上下文失效(换规格 / 离页 / A→B→A)以 PP_CONTEXT_CHANGED reject;null 只表示「确认没有选择」,不能用来判断失败。

设置或清空当前商品的选择,并同步 PP 面板;不弹窗、不加购、不写车。

  • 传入 options 中的原始候选:SDK 先校验候选属于目标商品、且仍对应当前报价,再提交。站点不要自行拼装方案或关联属性。
  • 传入 null:清空选择(等价于顾客反选)。后续同规格重新报价不会用默认方案重新选中。
  • 重复设置同一个有效候选、重复清空都正常成功;幂等不绕过有效性校验。
  • 失败原因是具体错误:方案归属不符或方案来自上一轮报价时 reject INVALID_ARGUMENT(context.path 指向 selection 的具体字段,例如 selection.mainVariantId、selection.plan.variantId 或方案内容字段);上下文失效时 reject PP_CONTEXT_CHANGED。
  • 购买流程或选择弹窗进行中时 reject PP_SELECTION_IN_PROGRESS,不会改写正在确认的选择。
const variantId = 'gid://shopify/ProductVariant/1234567890'
const index = 0
const { context, options } = await window.xcotton!.pp!.loadPlans(variantId)
// 顾客选中某项时,提交 SDK 提供的原始候选。
await window.xcotton!.pp!.setSelectedPlan(options[index]!, context)
const selection = await window.xcotton!.pp!.getSelectedPlan(context)
// 顾客取消选中:清空;下次仍按后台配置判断是否自动弹窗。
await window.xcotton!.pp!.setSelectedPlan(null, context)

返回的 selection 是独立快照:

{
"mainVariantId": "gid://shopify/ProductVariant/1234567890",
"plan": { "variantId": 45678901234, "name": "1 Year", "priceText": "$4.99" },
"mainLineAttributes": [{ "key": "_PlanID", "value": "…" }]
}

锁定调用时的商品上下文并等待同规格最新报价,随后主动打开方案选择弹窗(不受后台自动弹窗配置限制)。

  • 顾客确认后更新 SDK 当前选择与 PDP 面板,返回独立 PPSelection;不加购、不写车。
  • 顾客取消、关闭或按 Escape 返回 null,并清空 PDP 当前选择(替代此前「取消保留打开前选择」的约定)。本次取消不会记录「拒绝」:PDP 仍未选中时,下次加购继续按后台配置判断是否弹窗。
  • 没有可用方案时 reject PP_NO_OPTIONS,不返回 null。
  • 等待期间换规格或离页以 PP_CONTEXT_CHANGED reject;弹窗打开后任何重新报价都会关闭弹窗并以 PP_CONTEXT_CHANGED reject,不会让顾客确认过期价格,也不会清空后来上下文的选择。
  • 选择互斥从等待报价开始,到确认、取消或失败结束:进行中再次调用 showModal() 或 setSelectedPlan() 会立即 reject PP_SELECTION_IN_PROGRESS,不排队、不叠窗、不取消已有操作。

顾客在弹窗中确认的方案会留在面板状态里,随后的 pp.addToCart 会解析到同一个选择,站点不需要自行保存选择或重写主商品属性。

pp.addProtection({ mainLineId, selection })

Section titled “pp.addProtection({ mainLineId, selection })”

为指定的主商品行写入明确方案的保障,成功返回 { added: true }。

  • mainLineId:目标主商品行的 CartLine.id(切勿传入商品规格 ID)。
  • selection:必填,来自 loadPlans 的 options 或 getCartQuotes 的有效候选。缺省以 INVALID_ARGUMENT 拒绝;规格必须匹配目标主行,写入前还会校验方案有效性。不要自行拼造方案或继续提交已失效的选项。
  • 不弹窗、不借用 PDP 或隐藏候选;已有同一方案时幂等成功,数量按同规格、同 Plan ID 的共享组从真实购物车重算。主行已保其他方案时以 PP_ALREADY_PROTECTED 拒绝;换方案先调用 removeProtection()。
  • 主行不存在或指向保障行 → CART_LINE_NOT_FOUND;方案或主行身份失效、写入或确认失败分别给具体错误。
  • 购买流程进行中调用会报 PP_SELECTION_IN_PROGRESS;SDK 内部组合流程不会被自身锁拒绝。

按主商品行取消保障,返回 { status: 'removed' };目标行本来就没有保障与关联标记时返回 { status: 'already_unprotected' }。mainLineId 为主商品行的 CartLine.id(不是保障行 ID)。主商品保留,SDK 只清理该行 _PlanID、保留站点其他属性,并按同组剩余主行重算保障数量。

已确认取消后,SDK 按需恢复该主行的补购候选。候选刷新失败不推翻 removed;此时 getCartQuotes() 拒绝,待服务恢复后调用 syncCart() 重新同步。

购物车行补购编排,返回 { status: 'added' } 或 { status: 'cancelled' }。

  • 依次检查购物车补购开关、主商品行与当前有效补购报价;开关关闭或该行没有候选时以 PP_NO_OPTIONS 拒绝,不当作顾客取消。
  • 只有一个方案也弹窗,由顾客明确确认后才复用 addProtection 写入;顾客取消返回 { status: 'cancelled' } 且零写入。
  • 已有有效保障时以 PP_ALREADY_PROTECTED 拒绝(只依据当前购物车中的实际保障行;残留 _PlanID 不算已保障)。关联歧义或保障数量异常报 CART_SNAPSHOT_INCONSISTENT,不归为已保障。
  • 选择针对该主行的购物车报价,不切换或清空 PDP 上下文与选择。弹窗等待期间主行事实或报价失效时,旧选择拒绝且不写车;重新同步后再开始补购。
  • 与 pp.addToCart 共用购买流程锁:整个流程(含写入确认阶段)内其它选择/购买入口报 PP_SELECTION_IN_PROGRESS。

由 getCartOffers 改名而来,返回最近一次购物车同步计算出的 PP 补购报价:当前购物车中仍可补加保障的主商品行。该方法为只读内存读取,不发起网络请求。

{
"enabled": true,
"quotes": [
{
"mainLineId": "gid://shopify/CartLine/abc123",
"quantity": 1,
"options": [
{
"mainVariantId": "gid://shopify/ProductVariant/1234567890",
"plan": { "variantId": 45678901234, "name": "1 Year", "priceText": "$4.99" },
"mainLineAttributes": [{ "key": "_PlanID", "value": "…" }]
}
]
}
]
}
  • enabled 只表示后台「购物车加购保险按钮」开关已确认开启。关闭时返回 { enabled: false, quotes: [] };开启但没有候选返回 { enabled: true, quotes: [] },两种空结果不能混用。
  • 未初始化以 SDK_NOT_INITIALIZED 拒绝,未绑车以 CART_NOT_BOUND 拒绝;尚未同步、同步进行中、快照失效都以 PP_CART_QUOTES_NOT_READY 拒绝,context.reason 分别为 not_synced、sync_in_progress、stale。最近一次同步失败以 PP_CART_QUOTES_SYNC_FAILED 拒绝。不要用空数组表示失败。
  • 每项的 options 与商品页报价同构,是可直接提交给 addProtection 的完整 PPSelection。站点自己写车后,先等待 syncCart() 完成再读取,旧代次的结果不能当作当前报价。
  • 读取被拒绝时,组件先撤下上一轮候选再提示错误,不能保留仍可点击的「Add protection」按钮;服务恢复并完成 syncCart() 后再重新读取。

读取当前购物车中已加保障的主商品行及其对应的保障行。每条已加保的主商品行对应数组中的一项,未加保的主商品行不会出现。由于同规格、同方案的多条主商品行会共用同一条保障行,不同项的 protectionLineId 可能相同。

[
{
"mainLineId": "gid://shopify/CartLine/abc123",
"protectionLineId": "gid://shopify/CartLine/def456"
}
]

若需在购物车商品行上提供「移除保障」操作,可用 mainLineId 调用 removeProtection({ mainLineId });protectionLineId 只用于展示关联关系。未绑车时以 CART_NOT_BOUND 拒绝;读取失败以 CART_SYNC_FAILED 或 adapter 的具体错误拒绝;同一保障组匹配到多条保障行时以 CART_SNAPSHOT_INCONSISTENT 拒绝。确认没有关系时返回 [],不要用空数组表示失败。

interface PPProductContext {
readonly mainVariantId: string
}
interface PPPlansResult {
context: PPProductContext
options: PPSelection[]
}
interface PPSelection {
mainVariantId: string
plan: PPPlanItem
mainLineAttributes: AttributeInput[]
}
interface PPPlanItem {
variantId: string | number
name?: string
priceInt?: number
priceText?: string
properties?: Record<string, string>
[key: string]: unknown
}
interface PPCartQuote {
mainLineId: string
quantity: number
options: PPSelection[]
}
type PPCartQuotesResult =
| { enabled: false; quotes: [] }
| { enabled: true; quotes: PPCartQuote[] }
interface PPRelation {
mainLineId: string
protectionLineId: string
}
type PPRemoveProtectionResult =
| { status: 'removed' }
| { status: 'already_unprotected' }
type PPOfferProtectionResult = { status: 'added' } | { status: 'cancelled' }
interface PPAddToCartOptions {
context?: PPProductContext
addMainProduct: (
attributes: AttributeInput[],
context: PPProductContext,
) => Promise<{ cartId: string; mainLineId: string }>
}
type PPAddToCartResult = { cartId: string; mainLineId: string } & {
protection: 'added' | 'skipped'
}
类型 / 字段说明
PPProductContext.mainVariantId上下文句柄对应的主商品规格 GID;句柄由 SDK 返回,不能手工构造
PPPlansResult.options当前报价的完整方案快照,每项可直接提交给 setSelectedPlan
PPSelection.mainVariantId该选择所属的主商品规格 GID;写主商品前应与点击时固定的目标规格核对
PPSelection.plan选中的保障方案对象
PPSelection.mainLineAttributes主商品属性数组(Storefront AttributeInput[]);组合加购时由站点原样透传,直接调用 addProtection 时 SDK 补写关联标记
PPPlanItem.variantId保障方案对应的规格 ID
PPPlanItem.name方案名称(如 '1 Year')。不同商品的方案名可能重复,请勿用它判断关联关系
PPPlanItem.priceInt方案价格,按现有协议固定 ×100 的整数
PPPlanItem.priceText格式化后的价格展示文本(如 '$4.99')
PPCartQuote.mainLineId可补加保障的主商品行 CartLine.id
PPCartQuote.quantity该主商品行的数量
PPCartQuote.options该主商品行可选的 PPSelection 列表,可直接提交给 addProtection
PPRelation.mainLineId已加保障的主商品行 CartLine.id
PPRelation.protectionLineId关联保障行的 CartLine.id(用于展示;取消保障传 mainLineId)
PPOfferProtectionResultadded 保障已写入并确认;cancelled 顾客取消,零写入
PPAddToCartResult.protectionadded 主商品与保障均完成;skipped 本次未选择保障

SDK 公开方法失败统一为 XcottonError(name === 'XcottonError'),未初始化时也是 SDK_NOT_INITIALIZED。站点函数、sdk.js 加载失败或 SDK 尚未加载时的普通异常不属于此契约:

interface XcottonError {
name: 'XcottonError'
code: XcottonErrorCode
operation: string
message: string
context: { code: XcottonErrorCode } & Record<string, unknown>
recovery: { action: XcottonRecoveryAction; message: string }
cause?: unknown
}
  • code 是程序分支依据;context.code 与它一致,按 code 收窄后可读取该错误专有的定位字段。
  • message 以 [XC] 开头并自带具体原因,只用于日志与展示,不要用文案做分支。
  • 只使用实际返回的 code、context 与可用的 cause 判断原因。上游没有提供的细分原因或写入结果,不要自行推断;cause 可能缺省。
  • recovery.action 是建议的恢复动作(如 SYNC_CART、WAIT_AND_RETRY、CORRECT_ARGUMENT),recovery.message 是可直接展示给开发者的英文指引。
  • 写入失败看 context.writeOutcome:not_sent 未发送、not_applied 明确拒绝且未生效、partial 已部分写入、unknown 写入结果无法确认。partial 与 unknown 都不要自动重试写入,应先核对购物车。
  • 用 window.xcotton.isXcottonError(error) 收窄;不要依赖 instanceof 或错误文案。
const sdk = window.xcotton
if (!sdk) throw new Error('[XC] sdk.js has not loaded yet')
try {
await sdk.syncCart('gid://shopify/Cart/your-cart-id')
} catch (error) {
if (!sdk.isXcottonError(error)) throw error
switch (error.code) {
case 'SDK_NOT_INITIALIZED':
await sdk.init()
break
case 'ADAPTER_RATE_LIMITED':
// 按 error.context.retryAfterMs 等待后再重试
break
default:
console.error(error.code, error.message, error.recovery.action)
}
}
错误 code 或 message原因处理方法
[XC] SDK initialization failed (module=…, stage=…, shop=…): …初始化失败;module / stage 指明失败阶段,冒号后为具体原因按 module、stage、causeCode 与原因定位;常见检查项是 shop、publicAccessToken、市场参数和后台配置。修正后重新加载页面;仍失败时联系客户经理(AM)
[XC] SDK not initialized. Call xcotton.init() before using SDK operations.在 init() 完成前调用了购物车或保障方法先 await init() 完成
[XC] syncCart requires a cartId before the first synchronization (…)无参调用 syncCart 时尚未绑定任何购物车先 syncCart(cartId) 完成首次绑定
[XC] Cart operation requires a bound cart; obtain a cart and call syncCart(cartId) first.在首次同步完成前调用了购物车相关操作(如 addProtection)先取得购物车并调用 syncCart(cartId);不要重复 init
[XC] cart currency does not match the configured market (…)购物车总价币种与初始化时的 currency 不一致按当前市场传 currency,并确认购物车属于同一市场
[XC] Cart session changed (…)操作执行期间切换了绑定的购物车针对新购物车重新发起操作
[XC] Cart requests are rate limited during … (retryAfterMs=…)触发 Shopify 限流,SDK 在冷却期内拒绝请求(按 adapter 提供的等待时间,未提供时默认 30 秒)等待冷却期结束后重试
[XC] pp.addToCart rejected an invalid argument (path=addMainProduct, …)调用 pp.addToCart 时未传主商品写入回调传入只写主商品、返回 { cartId, mainLineId } 的回调
[XC] pp.addToCart rejected an invalid argument (path=context, …)调用位置没有商品页上下文(例如首页或列表页快捷加购)先对目标变体调用 pp.loadPlans,或改用普通加购并调用 syncCart
PP_WRITE_CONFIRMATION_FAILED购买流程在主商品、同步或保障阶段失败;不表示主商品必然已写入结合 context.stage、mainProduct、writeOutcome 与 recovery 处理;已确认写入勿重加,结果未知先核对购物车
PP_ALREADY_PROTECTED购物车主行已有实际保障行(pp.offerProtection,或 pp.addProtection 传入另一方案)刷新购物车并隐藏补购入口;换方案先调用 removeProtection
PP_SELECTION_IN_PROGRESS另一 PP 选择/购买流程仍在进行等待现有流程结束后再调用
[XC] addProtection rejected an invalid argument (path=mainLineId, …)调用 addProtection 时未传 mainLineId传入目标主商品行的 CartLine.id
[XC] addProtection rejected an invalid argument (path=selection, …)未传 selection,或方案不属于该主商品行使用 loadPlans / getCartQuotes 返回的原始 option;不要重加已成功的主商品
[XC] removeProtection rejected an invalid argument (path=mainLineId, …)调用 removeProtection 时未传 mainLineId,或传入了保障行传入目标主商品行的 CartLine.id
[XC] sp.resolveSP rejected an invalid argument (path=spMode, …): checkoutPlus has no switch intent; call addSpFee() or declineSpFee() insteadcheckout+(checkoutPlus)没有开关意愿,不能调用 resolveSP()两个结账按钮分别调 addSpFee() 与 declineSpFee();不要从占位意愿推断本次选择
[XC] line.id: duplicate or missing cart line待更新或删除的行 ID 不在当前购物车中,或在单次请求中重复检查自定义 adapter 返回的购物车数据是否完整
[XC] updateLines: quantity or attributes required调用更新行时既未传数量也未传属性检查自定义 adapter 的 updateLines 调用与实现
[XC] Invalid MoneyV2 amount金额字段不是合法的十进制字符串保留 Storefront 原生的 amount 字符串
[XC] …currencyCode: inconsistent currency(完整路径如 cart.cost.totalAmount.currencyCode)adapter 返回的某处金额与购物车总价币种不一致让所有金额使用同一币种,不要自行换算

注:[XC] sdk.js has not loaded yet 与 [XC] SP is not ready 并非 SDK 内部抛出的错误,而是本系列文档示例代码中用于防御性检查的报错。