跳转到内容

接入延保险

延保险(PP)按单件商品计价。接入分为前后两段:在商品详情页加载报价并供顾客选择方案(此阶段无需购物车);当顾客点击「加入购物车」时,调用 pp.addToCart(),由 SDK 完成读取选择、写入主商品、同步购物车与写入保障行。

完整交互流程如下:

  1. 商品页报价:调用 loadPlans(variantId),SDK 请求报价并在 PP 面板中渲染保障方案。
  2. 顾客点击加购:站点传入只写主商品的回调,不再需要重复传 variantId。
  3. SDK 读取选择:使用 loadPlans 建立的商品上下文(或显式传入的 context);未选方案且允许弹窗时,SDK 先弹出方案选择窗,取消后仍继续加主商品。
  4. 写入主商品:SDK 调用一次 addMainProduct(attributes, context),attributes 就是主商品行属性,站点原样写入。
  5. 同步并写入保障:SDK 用回调返回的 cartId 同步购物车,再按返回的 mainLineId 写入保障行,成功返回 { cartId, mainLineId, protection }。
  • 客户经理(AM)已开启 PP(未开启时 window.xcotton.pp 为 undefined)。
  • 已按 快速开始 完成基础接入。启动组件会在商品页渲染 ppWidgetSelector 对应的容器,并在规格变化时调用 loadPlans。
  • init() 完成后即可访问 window.xcotton.pp;商品报价与方案选择均不依赖购物车。
  • 站点提供以下函数(示例从 lib/cart.ts 导入):
    • getSelectedVariantId():返回点击加购时目标规格的 ID。
    • getSelectedQuantity():返回本次加购数量。
    • addMainProduct(variantId, quantity, attributes):按传入的规格与数量写入主商品,把 attributes 写为主商品行属性,返回 { cartId, mainLineId }。该函数必须使用传入参数,不要在写车时重新读取页面上的当前规格;addToCart 的回调只在站点包装层补规格与数量。
    • showProtectionError(error):展示部分成功或失败提示。
  • 切换规格时:传入新的规格 ID 调用 loadPlans(variantId)。规格 ID 支持字符串或数字,SDK 会按初始化时配置的市场查询商品归属与价格。返回值是 { context, options }:options 是可直接提交给选择接口的方案快照,context 是本次报价的商品上下文句柄(需要明确归属时把它传给 canBePopup / getSelectedPlan / setSelectedPlan / showModal,单商品页面可省略)。
  • 离开商品页时:调用 loadPlans(null)。SDK 会取消进行中的报价请求、关闭已打开的方案弹窗、清空 PP 面板与选择,并返回 null。
  • 快速连续切换规格时:仅最后一次报价生效。被后续上下文替代的旧调用会以 PP_CONTEXT_CHANGED reject,旧规格上的已选方案与上下文句柄同时失效;A→B→A 也不会恢复旧句柄。
  • 报价失败时:请求失败或响应无法解析会 reject 具体错误。后端返回非 0 errCode 时,当前实现按该商品没有可用方案返回 { context, options: [] };不要把这种情况当成网络失败。成功但没有方案同样返回 { context, options: [] }。

若站点已查到当前市场下的规格详情,可作为第二个参数传入,从而省去 SDK 内部的一次 Storefront 查询:

await window.xcotton?.pp?.loadPlans(variantId, {
product: { id: 'gid://shopify/Product/1234567890' },
title: 'Default Title',
sku: 'SKU-001',
price: { amount: '29.99', currencyCode: 'USD' },
})
  1. 固定点击时的商品:进入处理函数后立即读取 variantId 与数量,之后页面再改规格也不影响本次加购。
  2. 调用一次 pp.addToCart():传入只写主商品的回调;context 可省略(使用 loadPlans 建立的当前商品上下文),也可显式传入报价返回的句柄。回调内不要调用 SDK、不要读页面当前规格,也不要重置 PDP 报价。
  3. 透传行属性:回调收到的 attributes 即主商品行属性(AttributeInput[]),直接作为主商品的行属性写入即可。该属性用于区分「带保障」与「不带保障」的同款商品,防止被 Shopify 合并为同一行。
  4. 按结果收尾:成功只返回 protection: 'added' 或 'skipped',两者都带 cartId / mainLineId。
    • 'skipped':顾客没选方案且后台不弹窗,或顾客取消了本次选择。主商品已加入并完成同步,无需任何补偿。
    • 失败一律 reject XcottonError:主商品已写入、但同步或保障写入失败时为 PP_WRITE_CONFIRMATION_FAILED,context 含 stage / mainProduct / protection / writeOutcome。不要重加主商品:先刷新购物车确认主行是否存在,再提示部分成功或引导顾客在购物车中补加保障。
  5. 失败与拒绝:主商品回调抛错时同样 reject,但按 context.mainProduct 标明主商品结果,通常为 unknown;回调最多执行一次,SDK 不重放、不回滚。

返回的 mainLineId 是主商品行 ID,不是保障行 ID。保障行由 SDK 按该主行维护;需要读取对应关系时调用 getRelations(),不要把保障行 ID 当作 mainLineId 传给购物车行操作。

不必再自行读取选择与调用 addProtection(),也不必核对 selection.mainVariantId、手工同步或重新报价——这些都已在 pp.addToCart() 内部完成。只有自建加购界面时才需要直接使用这些原语:canBePopup() 查询是否满足自动弹窗条件,getSelectedPlan() 只读当前选择,setSelectedPlan() 提交或清空选择,规则见 SDK 参考。

不使用便利封装时,按同一顺序自行组合原子方法即可:

const selected = await window.xcotton.pp.getSelectedPlan()
const attributes = selected?.mainLineAttributes ?? []
const { cartId, mainLineId } = await addMainProduct(variantId, quantity, attributes)
await window.xcotton.syncCart(cartId)
if (selected) await window.xcotton.pp.addProtection({ mainLineId, selection: selected })

需要按配置自动弹窗时,用 canBePopup() 判断后调用 showModal();失败阶段(主商品、同步、保障)由站点自行捕获并提示。两种写法都使用同一套主行、规格与方案校验;便利封装在整段流程内持有购物车权限,手动链的每一步分别取得权限,因此不要在步骤之间插入其他写车操作。

components/add-to-cart-button.tsx
'use client'
import { xcottonInit } from '@/lib/xcotton'
import {
getSelectedVariantId,
getSelectedQuantity,
addMainProduct,
showProtectionError,
} from '@/lib/cart'
async function onAddToCartClick() {
// 点击时固定规格与数量;等待报价期间页面可能已改成别的规格。
const variantId = getSelectedVariantId()
const quantity = getSelectedQuantity()
await xcottonInit()
const pp = window.xcotton?.pp
if (!pp) throw new Error('[XC] Product Protection is not enabled')
try {
const result = await pp.addToCart({
addMainProduct: (attributes) =>
addMainProduct(variantId, quantity, attributes),
})
return result
} catch (error) {
// 部分成功如何提示由站点决定;不要重加主商品。
showProtectionError(error)
}
}
export function AddToCartButton() {
return (
<button
type="button"
onClick={() => void onAddToCartClick().catch(console.error)}
>
Add to cart
</button>
)
}
操作预期结果
尚未创建购物车时进入商品页PP 面板正常完成报价并展示方案
快速连续切换商品规格面板仅展示最后一个规格的报价,旧规格上的选择自动失效
离开商品详情页面板清空,已打开的方案弹窗自动关闭
选中方案后点击加入购物车只发出一次主商品写入;主商品行与对应保障行都存在,保障数量等于该规格同方案的主行数量之和
未选方案、后台配置为不弹窗时点击加入购物车protection 为 'skipped',主商品照常加入,本次不写保障行
未选方案、后台配置为弹窗时点击加入购物车先弹出方案选择窗;取消后 protection 为 'skipped',主商品照常加入
店铺未开启 PP 时window.xcotton.pp 不存在;普通加购不经过 PP 入口
模拟保障写入失败reject PP_WRITE_CONFIRMATION_FAILED,主商品只写入一次且保留
无购物车时点击加入购物车回调创建购物车并返回身份;SDK 同步后按需写入保障行
删除主商品行并同步购物车syncCart 后,保障数量重算为同组剩余主行(同规格、同 _PlanID)的数量之和;组内没有这样的主行时移除保障行
单独删除保障行主商品行正常保留
现象原因与处理方法
主商品被加入两次在部分失败后重新调用了 pp.addToCart,或在回调内又自行写了一次主商品。失败时只提示,不重加
主商品回调内调用 SDK 写入口报忙pp.addToCart() 已持有本次购买权限。回调内调用 syncCart 报 OPERATION_IN_PROGRESS,调用其他 PP 选择或购买入口报 PP_SELECTION_IN_PROGRESS。回调只写主商品并返回身份;SDK 负责后续同步和保障写入
带保障与不带保障的同款商品被合并成了同一行写主商品时忽略了回调传入的 attributes,或自行改写成了别的属性
protection 一直是 'skipped'顾客未选方案、后台未开启加购弹窗。可先用 canBePopup() / getSelectedPlan() 排查当前报价与选择
方案报价金额异常检查 variantDetail.price.amount 是否为十进制字符串、currencyCode 是否与当前市场一致;若不确定,请勿传入 variantDetail
报价拒绝或部分成功的 error 提到币种不一致购物车或规格详情与初始化币种不一致。对齐市场后再同步;若主商品已成功,不要重加