接入延保险
延保险(PP)按单件商品计价。接入分为前后两段:在商品详情页加载报价并供顾客选择方案(此阶段无需购物车);当顾客点击「加入购物车」时,调用 pp.addToCart(),由 SDK 完成读取选择、写入主商品、同步购物车与写入保障行。
完整交互流程如下:
- 商品页报价:调用
loadPlans(variantId),SDK 请求报价并在 PP 面板中渲染保障方案。 - 顾客点击加购:站点传入只写主商品的回调,不再需要重复传
variantId。 - SDK 读取选择:使用
loadPlans建立的商品上下文(或显式传入的context);未选方案且允许弹窗时,SDK 先弹出方案选择窗,取消后仍继续加主商品。 - 写入主商品:SDK 调用一次
addMainProduct(attributes, context),attributes就是主商品行属性,站点原样写入。 - 同步并写入保障: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_CHANGEDreject,旧规格上的已选方案与上下文句柄同时失效;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' },})- 固定点击时的商品:进入处理函数后立即读取
variantId与数量,之后页面再改规格也不影响本次加购。 - 调用一次
pp.addToCart():传入只写主商品的回调;context可省略(使用loadPlans建立的当前商品上下文),也可显式传入报价返回的句柄。回调内不要调用 SDK、不要读页面当前规格,也不要重置 PDP 报价。 - 透传行属性:回调收到的
attributes即主商品行属性(AttributeInput[]),直接作为主商品的行属性写入即可。该属性用于区分「带保障」与「不带保障」的同款商品,防止被 Shopify 合并为同一行。 - 按结果收尾:成功只返回
protection: 'added'或'skipped',两者都带cartId/mainLineId。'skipped':顾客没选方案且后台不弹窗,或顾客取消了本次选择。主商品已加入并完成同步,无需任何补偿。- 失败一律 reject
XcottonError:主商品已写入、但同步或保障写入失败时为PP_WRITE_CONFIRMATION_FAILED,context 含stage/mainProduct/protection/writeOutcome。不要重加主商品:先刷新购物车确认主行是否存在,再提示部分成功或引导顾客在购物车中补加保障。
- 失败与拒绝:主商品回调抛错时同样 reject,但按
context.mainProduct标明主商品结果,通常为unknown;回调最多执行一次,SDK 不重放、不回滚。
返回的 mainLineId 是主商品行 ID,不是保障行 ID。保障行由 SDK 按该主行维护;需要读取对应关系时调用 getRelations(),不要把保障行 ID 当作 mainLineId 传给购物车行操作。
不必再自行读取选择与调用 addProtection(),也不必核对 selection.mainVariantId、手工同步或重新报价——这些都已在 pp.addToCart() 内部完成。只有自建加购界面时才需要直接使用这些原语:canBePopup() 查询是否满足自动弹窗条件,getSelectedPlan() 只读当前选择,setSelectedPlan() 提交或清空选择,规则见 SDK 参考。
手动链(等价写法)
Section titled “手动链(等价写法)”不使用便利封装时,按同一顺序自行组合原子方法即可:
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();失败阶段(主商品、同步、保障)由站点自行捕获并提示。两种写法都使用同一套主行、规格与方案校验;便利封装在整段流程内持有购物车权限,手动链的每一步分别取得权限,因此不要在步骤之间插入其他写车操作。
'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> )}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> )}<script setup lang="ts">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) }}
function onClick() { void onAddToCartClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Add to cart</button></template>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> )}<script setup lang="ts">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) }}
function onClick() { void onAddToCartClick().catch(console.error)}</script>
<template> <button type="button" @click="onClick">Add to cart</button></template><script lang="ts"> 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) } }
function onClick() { void onAddToCartClick().catch(console.error) }</script>
<button type="button" onclick={onClick}>Add to cart</button>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 提到币种不一致 | 购物车或规格详情与初始化币种不一致。对齐市场后再同步;若主商品已成功,不要重加 |