SDK 参考
本页列出 Headless SDK 的全部公开接口、类型定义与错误信息。你可以直接下载 xcotton-types.ts(内含 window.xcotton 与 window.xcottonConfig 的全局类型声明)。完整的接入步骤见 快速开始。
| 全局对象 / 命名空间 | 就绪时机 |
|---|---|
window.xcottonConfig | 由站点在加载 sdk.js 之前声明 |
window.xcotton | sdk.js 加载并执行完毕后 |
window.xcotton.pp | init() 解析完成,且店铺后台已开启 PP |
window.xcotton.sp | 首次带 cartId 的购物车同步完成(由 init({ cartId }) 或 syncCart() 触发),且店铺后台已开启 SP |
window.xcottonConfig
Section titled “window.xcottonConfig”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 不会改变当前会话。
init(options)
Section titled “init(options)”初始化 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.xcottonif (!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]开头并带有失败模块、阶段与具体原因,详见错误处理。
isXcottonError(value)
Section titled “isXcottonError(value)”判断任意值是否为 SDK 抛出的 XcottonError。SDK 公开方法(包括未初始化时的失败)使用该结构;站点函数或脚本加载异常不在此列。守卫不依赖 instanceof,跨打包边界或不同执行上下文同样有效。
const sdk = window.xcottonif (!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 中。
syncCart(cartId?)
Section titled “syncCart(cartId?)”购物车同步的唯一入口:首次绑定、切换购物车与同车刷新都调用它。SDK 自行判断本次是首次绑定、换车还是同车重读,站点不需要维护第二份绑定身份或同步队列。
const sdk = window.xcottonif (!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回调中调用该方法,否则会导致无限循环。
XcottonCartAdapter
Section titled “XcottonCartAdapter”可选适配器。当购物车读写必须经过站点自有 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 |
addLines | merchandiseId 为完整的 ProductVariant GID;quantity 省略时默认为 1 |
updateLines | id 为当前快照中的 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 errorSDK 用 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?.spif (!sp) throw new Error('[XC] SP is not ready')| 方法 | 作用 | 是否写购物车 | 典型适用模式 |
|---|---|---|---|
addSpFee() | 立即写入费用行;已有正确费用时幂等成功 | 是 | checkout+ 含保护、自定义开关 |
removeSpFee() | 关闭意愿并移除费用行;本来没有费用行不算失败 | 是 | 自定义开关、自定义「不含保护」 |
declineSpFee() | 本次明确拒绝:清费并写拒绝标记 | 是 | checkout+ 不含保护 |
resolveSP() | 按顾客开关意愿对齐费用行,返回动作结果 | 是 | cart、checkout |
getQuote() | 读取当前购物车的 SP 报价;不可保时拒绝 | 否 | 全部模式 |
getMode() | 读取当前实际生效的模式 | 否 | 全部模式 |
showModal() | 打开 SP 保障说明弹窗(需要已挂载面板) | 否 | 全部模式 |
以上方法在三种适配模式下均可调用(resolveSP() 在 checkout+ 下会拒绝),「典型适用模式」代表各自的设计使用场景。
sp.addSpFee()
Section titled “sp.addSpFee()”明确要求立即写入保障费用,包括 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负责通知确认后的购物车事实。
sp.removeSpFee()
Section titled “sp.removeSpFee()”关闭保障意愿并立即移除费用行。
const sp = window.xcotton!.sp!const { removed } = await sp.removeSpFee()removed: true表示本次确认移除了费用行;removed: false表示经确认本来没有费用行,两种都是正常完成。- 即使没有费用行,仍会按模式完成关闭意愿与必要标记;必要标记写入失败时 Promise 拒绝。
- 与
declineSpFee()的区别:removeSpFee()是「关掉保障」的状态操作,不等于顾客本次明确选择「不含保护」。
sp.declineSpFee()
Section titled “sp.declineSpFee()”表达本次明确不购买:清除费用行并写入拒绝确认标记。
const sp = window.xcotton!.sp!const { declined } = await sp.declineSpFee()// declined 恒为 true;失败时 Promise 拒绝| 适配模式 | 意愿处理 |
|---|---|
cart | 关闭开关意愿并持久化,避免下一次同步把已拒绝的费用加回来 |
checkout | 保留顾客拨过的开关意愿,不因本次拒绝持久化为关闭 |
checkout+ | 不读、不写、不续期开关意愿缓存;本次选择只由两个按钮表达 |
- 清费与必要标记已成功时,无关的再次报价失败不会把本次拒绝改成失败。
- 本次拒绝完成后不要再串接
resolveSP():checkout保留了原意愿,后者会重新按意愿加入保障。
sp.resolveSP()
Section titled “sp.resolveSP()”按顾客意愿对齐费用行,并返回动作结果;站点据此决定是否继续导航。
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()。
sp.getMode()
Section titled “sp.getMode()”同步返回客户经理(AM)在后台配置的适配模式。返回值 'checkoutPlus' 即对应文档中的 checkout+ 模式。
sp.getQuote()
Section titled “sp.getQuote()”读取当前购物车的 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.showModal()
Section titled “sp.showModal()”打开 SP 保障说明弹窗,复用当前已挂载的面板。
- 必须在至少一个 SP 面板已挂载且仍连接页面时调用;否则拒绝
SP_PANEL_REQUIRED,recovery.action为MOUNT_SP_PANEL。 - Promise 在弹窗完成渲染后解析,不等待顾客关闭,也不代表顾客已购买。
- 打开本身不改意愿、不写车;多个面板共用同一弹窗,最后一个面板卸载后弹窗关闭。
- 弹窗按钮走内部意愿入口:
cart表达意愿并写车,checkout只表达意愿;checkout+不展示该意愿按钮。
SpQuote
Section titled “SpQuote”interface SpQuote { extId: number totalPrice: string totalPriceInt: number currencyCode: string currencySymbol: string}| 字段 | 说明 |
|---|---|
extId | SP 规格 ID;成功报价恒为有效值,0 不再作为成功返回 |
totalPrice | 费用展示串(主货币单位)。< 10000 分保留 2 位小数;>= 10000 分四舍五入为整数元,会丢失分位 |
totalPriceInt | 费用精确金额,固定为「主货币单位 × 100」的整数(与币种小数位无关);金额计算与比较必须使用该字段 |
currencyCode | 币种代码(如 'USD'、'CAD'),币种身份以它为准 |
currencySymbol | 币种符号(如 '$'),仅用于展示;CAD 与 USD 同为 $ |
SpResolveResult
Section titled “SpResolveResult”| 分支 | 含义 |
|---|---|
{ 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.xcottonif (!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 合并后的已有行 |
protection | added 主商品与保障均已确认;skipped 本次没有选择保障,主商品与同步已完成 |
加购组合的执行顺序:
- 锁定商品上下文;已有选择直接使用,未选且后台允许弹窗时先弹窗(取消仍继续加主商品),随后把
selection.mainLineAttributes交给回调。 - 调用一次
addMainProduct(attributes, context)。 - 用回调返回的
cartId同步购物车,并重读确认mainLineId的规格与本次上下文一致。 - 若有选择,按返回的
mainLineId写入保障行。
回调与失败约定:
- 回调只写主商品。不要在回调内再次调用 SDK(包括
init、syncCart、pp.addToCart),也不要读页面当前规格或重置 PDP 报价。 - SDK 不重放回调、不回滚已成功的主商品。回调最多执行一次。
- 成功只返回
added/skipped;失败一律 rejectXcottonError。主商品已确认、后续同步或保障失败时为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 不会因冲突或超时重放回调。
商品上下文 PPProductContext
Section titled “商品上下文 PPProductContext”loadPlans(variantId) 在报价的同时把该商品设为全局当前上下文,并返回 { context, options }。context 是 SDK 管理的商品上下文句柄:
- 省略
context时,调用开始时捕获全局当前上下文;异步等待后不会漂移到新商品。 - 显式传入
context时,本次调用只属于该商品。句柄无效(不是 SDK 返回的对象)以INVALID_ARGUMENTreject;句柄过期(切换过规格、离开过商品页,或 A→B→A 后回到原规格)以PP_CONTEXT_CHANGEDreject,不会回退到全局上下文。 - 不要手工构造句柄,也不要长期缓存后跨商品复用。
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.loadPlans(variantId, variantDetail?)
Section titled “pp.loadPlans(variantId, variantDetail?)”为商品页当前选中的商品规格请求报价并渲染 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_CHANGEDreject,不会返回空方案冒充正常完成。 -
同规格重新报价保留仍有效的选择;顾客主动反选后,重新报价不会被
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 具体错误,不会吞成空方案。
pp.canBePopup(context?)
Section titled “pp.canBePopup(context?)”只查询当前商品是否满足后台「加购时自动弹窗」条件,不弹窗、不改选择、不写车。
- 配置不允许或没有可用方案时返回
false;true只代表查询当时满足条件,随后主动弹窗仍会独立检查上下文。 - 等待同规格最新报价后再判断。
- 报价或配置读取失败会 reject 具体错误,不会伪装成
false。 - 等待期间换规格或离开商品页会以
PP_CONTEXT_CHANGEDreject。
pp.getSelectedPlan(context?)
Section titled “pp.getSelectedPlan(context?)”只读返回当前规格下已成形的选择;没有选择或顾客已取消时返回 null。
- 不自动选方案、不弹窗、不写车。
- 等待同规格最新报价,不会返回上一轮价格对应的过期方案。
- 上下文失效(换规格 / 离页 / A→B→A)以
PP_CONTEXT_CHANGEDreject;null只表示「确认没有选择」,不能用来判断失败。
pp.setSelectedPlan(selection, context?)
Section titled “pp.setSelectedPlan(selection, context?)”设置或清空当前商品的选择,并同步 PP 面板;不弹窗、不加购、不写车。
- 传入
options中的原始候选:SDK 先校验候选属于目标商品、且仍对应当前报价,再提交。站点不要自行拼装方案或关联属性。 - 传入
null:清空选择(等价于顾客反选)。后续同规格重新报价不会用默认方案重新选中。 - 重复设置同一个有效候选、重复清空都正常成功;幂等不绕过有效性校验。
- 失败原因是具体错误:方案归属不符或方案来自上一轮报价时 reject
INVALID_ARGUMENT(context.path指向selection的具体字段,例如selection.mainVariantId、selection.plan.variantId或方案内容字段);上下文失效时 rejectPP_CONTEXT_CHANGED。 - 购买流程或选择弹窗进行中时 reject
PP_SELECTION_IN_PROGRESS,不会改写正在确认的选择。
const variantId = 'gid://shopify/ProductVariant/1234567890'const index = 0const { 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": "…" }]}pp.showModal(context?)
Section titled “pp.showModal(context?)”锁定调用时的商品上下文并等待同规格最新报价,随后主动打开方案选择弹窗(不受后台自动弹窗配置限制)。
- 顾客确认后更新 SDK 当前选择与 PDP 面板,返回独立
PPSelection;不加购、不写车。 - 顾客取消、关闭或按 Escape 返回
null,并清空 PDP 当前选择(替代此前「取消保留打开前选择」的约定)。本次取消不会记录「拒绝」:PDP 仍未选中时,下次加购继续按后台配置判断是否弹窗。 - 没有可用方案时 reject
PP_NO_OPTIONS,不返回null。 - 等待期间换规格或离页以
PP_CONTEXT_CHANGEDreject;弹窗打开后任何重新报价都会关闭弹窗并以PP_CONTEXT_CHANGEDreject,不会让顾客确认过期价格,也不会清空后来上下文的选择。 - 选择互斥从等待报价开始,到确认、取消或失败结束:进行中再次调用
showModal()或setSelectedPlan()会立即 rejectPP_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 内部组合流程不会被自身锁拒绝。
pp.removeProtection({ mainLineId })
Section titled “pp.removeProtection({ mainLineId })”按主商品行取消保障,返回 { status: 'removed' };目标行本来就没有保障与关联标记时返回 { status: 'already_unprotected' }。mainLineId 为主商品行的 CartLine.id(不是保障行 ID)。主商品保留,SDK 只清理该行 _PlanID、保留站点其他属性,并按同组剩余主行重算保障数量。
已确认取消后,SDK 按需恢复该主行的补购候选。候选刷新失败不推翻 removed;此时 getCartQuotes() 拒绝,待服务恢复后调用 syncCart() 重新同步。
pp.offerProtection({ mainLineId })
Section titled “pp.offerProtection({ mainLineId })”购物车行补购编排,返回 { status: 'added' } 或 { status: 'cancelled' }。
- 依次检查购物车补购开关、主商品行与当前有效补购报价;开关关闭或该行没有候选时以
PP_NO_OPTIONS拒绝,不当作顾客取消。 - 只有一个方案也弹窗,由顾客明确确认后才复用
addProtection写入;顾客取消返回{ status: 'cancelled' }且零写入。 - 已有有效保障时以
PP_ALREADY_PROTECTED拒绝(只依据当前购物车中的实际保障行;残留_PlanID不算已保障)。关联歧义或保障数量异常报CART_SNAPSHOT_INCONSISTENT,不归为已保障。 - 选择针对该主行的购物车报价,不切换或清空 PDP 上下文与选择。弹窗等待期间主行事实或报价失效时,旧选择拒绝且不写车;重新同步后再开始补购。
- 与
pp.addToCart共用购买流程锁:整个流程(含写入确认阶段)内其它选择/购买入口报PP_SELECTION_IN_PROGRESS。
pp.getCartQuotes()
Section titled “pp.getCartQuotes()”由 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()后再重新读取。
pp.getRelations()
Section titled “pp.getRelations()”读取当前购物车中已加保障的主商品行及其对应的保障行。每条已加保的主商品行对应数组中的一项,未加保的主商品行不会出现。由于同规格、同方案的多条主商品行会共用同一条保障行,不同项的 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) |
PPOfferProtectionResult | added 保障已写入并确认;cancelled 顾客取消,零写入 |
PPAddToCartResult.protection | added 主商品与保障均完成;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.xcottonif (!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) }}常见错误信息
Section titled “常见错误信息”| 错误 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() instead | checkout+(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 内部抛出的错误,而是本系列文档示例代码中用于防御性检查的报错。