post-purchase-ui-extension
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePost-Purchase UI Extension
售后UI扩展
Component catalog, lifecycle contract, and sandbox rules for (post-purchase upsell surface, npm , package in maintenance — no newer version exists; the modern checkout-extensions SDK has no post-purchase target as of writing).
@shopify/post-purchase-ui-extensions-react0.13.5<s-*>@shopify/post-purchase-ui-extensions-react0.13.5<s-*>⚠️ MANDATORY: Validate with tsc (do not skip)
⚠️ 强制要求:使用tsc验证(请勿跳过)
Run after writing or editing any post-purchase JSX:
bash
cd extensions/<your-extension>
npx tsc --noEmitTypeScript resolves types automatically via the bundled at . If types fail twice on the same artifact, stop and surface the error to the user.
.d.tsnode_modules/@shopify/post-purchase-ui-extensions-react/build/ts/index.d.tsNEVER call , , or for this SDK. The Shopify Dev MCP doesn't index . The validator's enum value covers the modern web-component SDK only and rejects every post-purchase component (, , …) as "not a Polaris web component."
validate_component_codeblocksvalidate_graphql_codeblockssearch_docs_chunks@shopify/post-purchase-ui-extensions-reactpolaris-checkout-extensions@shopify/ui-extensionsBlockStackButton编写或编辑任何售后JSX代码后,请运行:
bash
cd extensions/<your-extension>
npx tsc --noEmitTypeScript会通过 中的捆绑 文件自动解析类型。如果同一代码片段两次类型验证失败,请停止操作并向用户展示错误信息。
node_modules/@shopify/post-purchase-ui-extensions-react/build/ts/index.d.ts.d.ts请勿为此SDK调用 、 或 。 Shopify Dev MCP未索引 。验证器的 枚举值仅适用于现代 Web组件SDK,会将所有售后组件(、 等)判定为“非Polaris Web组件”而拒绝。
validate_component_codeblocksvalidate_graphql_codeblockssearch_docs_chunks@shopify/post-purchase-ui-extensions-reactpolaris-checkout-extensions@shopify/ui-extensionsBlockStackButton⚠️ Skip if a different surface
⚠️ 若为其他场景请跳过
- Admin App Home markup → use
<s-*>instead.shopify-polaris-app-home - Modern checkout extensions (,
@shopify/ui-extensions-reactweb components) → use<s-*>instead.shopify-polaris-checkout-extensions - Customer account extensions → use instead.
shopify-polaris-customer-account-extensions
- Admin App Home 标记 → 改用
<s-*>。shopify-polaris-app-home - 现代结账扩展(、
@shopify/ui-extensions-reactWeb组件)→ 改用<s-*>。shopify-polaris-checkout-extensions - 客户账户扩展 → 改用 。
shopify-polaris-customer-account-extensions
Doc lookup (WebFetch only)
文档查询(仅支持WebFetch)
The MCP doesn't index this SDK — use WebFetch:
- Component props: (lowercase — PascalCase URLs return 404, e.g.
https://shopify.dev/docs/api/checkout-extensions/post-purchase/components/<name>works,/components/blockstackdoes not)/components/BlockStack - Lifecycle, ,
useExtensionInput,Changeset,InputData:ChangesetErrorCodehttps://shopify.dev/docs/api/checkout-extensions/post-purchase/api - End-to-end tutorials: and
https://shopify.dev/docs/apps/build/checkout/product-offers/build-a-post-purchase-offerhttps://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription - UX guidance (only when working on copy, layout, or offer framing — not for API/prop questions): and
https://shopify.dev/docs/apps/build/checkout/product-offers/ux-for-post-purchase-product-offershttps://shopify.dev/docs/apps/build/checkout/product-offers/ux-for-post-purchase-subscriptions
If a component, prop, lifecycle field, or error code is missing from the Component Catalog or Lifecycle Contract below, WebFetch the canonical reference.
MCP未索引此SDK,请使用WebFetch:
- 组件属性:(需小写——大驼峰URL会返回404,例如
https://shopify.dev/docs/api/checkout-extensions/post-purchase/components/<name>可用,/components/blockstack不可用)/components/BlockStack - 生命周期、、
useExtensionInput、Changeset、InputData:ChangesetErrorCodehttps://shopify.dev/docs/api/checkout-extensions/post-purchase/api - 端到端教程:和
https://shopify.dev/docs/apps/build/checkout/product-offers/build-a-post-purchase-offerhttps://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription - UX指南(仅在处理文案、布局或优惠框架时使用——不适用于API/属性问题):和
https://shopify.dev/docs/apps/build/checkout/product-offers/ux-for-post-purchase-product-offershttps://shopify.dev/docs/apps/build/checkout/product-offers/ux-for-post-purchase-subscriptions
如果组件、属性、生命周期字段或错误代码未在下方的【组件目录】或【生命周期约定】中列出,请通过WebFetch查询官方参考文档。
Rules
规则
- Two extension points, two phases. is a data-prefetch hook;
extend("Checkout::PostPurchase::ShouldRender", …)is the React mount. Render runs only if ShouldRender returnedrender("Checkout::PostPurchase::Render", App). See Lifecycle Contract.{ render: true } - in ShouldRender;
storage.update(data)in Render. Storage is the only hand-off between the two phases — they run in separate JS contexts.storage.initialData - takes a signed JWT string, not a Changeset object. Sign the changeset on your backend with the app's API secret, return the token to the extension, then call
applyChangeset. Never sign client-side.await applyChangeset(token) - Always call , including on error paths. Documented behavior:
done()"indicates that the extension has finished running" and "redirects customers to the Order status page." Build-guide code samples call it in both accept and decline branches. Operational rule (not stated in shopify.dev): if an accept handler throws or rejects beforedone()runs, the buyer is stuck on a blank screen — wrap accept handlers in try/finally to guarantee the call.done() - Treat as potentially called more than once per checkout. The shopify.dev pages do not document call frequency. Operational observation in production: it can fire on payment-page load and again after the buyer clicks Pay. Make the handler idempotent (backend dedupe of identical fetches keyed by
ShouldRender).referenceId - Sandbox: no DOM, no CSS, no , no external scripts. All visual customization happens through component props. There is no
window, no<style>, no inlineclassName. Spacing comes from prop tokens — but the scale differs per component (see Spacing scales).style={…} - /
.jsxextension required in every import. The Shopify CLI bundler does not auto-resolve..jsfails;import { X } from "./foo"works.import { X } from "./foo.jsx" - Only import what the SDK re-exports from . The runtime bundles its own React — importing additional React entry points causes duplicate-React errors.
"react",useState, etc. work because they pass through.useEffect - Validate with , not the MCP. See MANDATORY block above.
tsc
- 两个扩展点,两个阶段。是数据预获取钩子;
extend("Checkout::PostPurchase::ShouldRender", …)是React挂载方法。仅当ShouldRender返回render("Checkout::PostPurchase::Render", App)时,Render才会执行。详见【生命周期约定】。{ render: true } - 在ShouldRender中使用 ;在Render中使用
storage.update(data)。Storage是两个阶段之间唯一的数据传递方式——它们运行在独立的JS上下文环境中。storage.initialData - 接受签名后的JWT字符串,而非Changeset对象。在后端使用应用的API密钥对changeset进行签名,将令牌返回给扩展,然后调用
applyChangeset。切勿在客户端进行签名操作。await applyChangeset(token) - 始终调用 ,包括错误路径。文档说明:
done()表示“扩展已完成运行”并“将客户重定向到订单状态页面”。构建指南中的代码示例在接受和拒绝分支中均调用了该方法。操作规则(shopify.dev未明确说明):如果接受处理程序在调用done()之前抛出错误或拒绝,买家会停留在空白页面——请将接受处理程序包裹在try/finally中以确保调用done()。done() - 将ShouldRender视为可能在每次结账时被多次调用。shopify.dev页面未记录调用频率。生产环境中的操作观察:它可能在支付页面加载时触发,也可能在买家点击“支付”后再次触发。请确保处理程序具有幂等性(后端通过 对相同请求进行去重)。
referenceId - 沙箱限制:无DOM、无CSS、无 、无外部脚本。所有视觉自定义均通过组件属性实现。不支持
window、<style>或内联className。间距通过属性令牌设置——但不同组件的间距尺度不同(详见【间距尺度】)。style={…} - 每个导入必须使用 /
.jsx扩展名。Shopify CLI打包器不会自动解析扩展名。.js会失败;import { X } from "./foo"可正常工作。import { X } from "./foo.jsx" - 仅导入SDK从 中重新导出的内容。运行时会捆绑自身的React——导入额外的React入口点会导致重复React错误。
react、useState等可正常使用,因为它们是通过SDK传递的。useEffect - 使用tsc而非MCP进行验证。详见上方【强制要求:使用tsc验证(请勿跳过)】部分。
Common Patterns
常见模式
Generic SDK patterns. Repo-specific architecture (layouts/templates/components, normalize functions, config/token systems) lives in the consuming repo's , not here.
architecture.md通用SDK模式。仓库特定的架构(布局/模板/组件、规范化函数、配置/令牌系统)位于消费仓库的 中,不在本文档范围内。
architecture.mdBoilerplate entry point
样板文件入口
The two-phase contract — every post-purchase extension starts with this skeleton.
jsx
import { extend, render } from "@shopify/post-purchase-ui-extensions-react";
extend("Checkout::PostPurchase::ShouldRender", async ({ inputData, storage }) => {
const data = await fetchOffer(inputData); // your backend
if (!data) return { render: false };
await storage.update(data);
return { render: true };
});
render("Checkout::PostPurchase::Render", App);
function App({ storage, applyChangeset, done }) {
const offer = storage.initialData;
return <BlockStack spacing="loose">{/* … */}</BlockStack>;
}两阶段约定——所有售后扩展均以此框架开始。
jsx
import { extend, render } from "@shopify/post-purchase-ui-extensions-react";
extend("Checkout::PostPurchase::ShouldRender", async ({ inputData, storage }) => {
const data = await fetchOffer(inputData); // 你的后端接口
if (!data) return { render: false };
await storage.update(data);
return { render: true };
});
render("Checkout::PostPurchase::Render", App);
function App({ storage, applyChangeset, done }) {
const offer = storage.initialData;
return <BlockStack spacing="loose">{/* … */}</BlockStack>;
}Loading state on accept Button
接受按钮的加载状态
Buttonloadingdone()jsx
const [loading, setLoading] = useState(false);
<Button
loading={loading}
loadingLabel="Processing"
onPress={async () => {
setLoading(true);
const token = await signChangeset(variantId);
await applyChangeset(token);
done();
}}
>
Add to order
</Button>Buttonloadingdone()jsx
const [loading, setLoading] = useState(false);
<Button
loading={loading}
loadingLabel="Processing"
onPress={async () => {
setLoading(true);
const token = await signChangeset(variantId);
await applyChangeset(token);
done();
}}
>
添加到订单
</Button>Image with locked aspect ratio
锁定宽高比的图片
Use + to prevent layout shift and align cards in a grid when source images have varying intrinsic ratios.
aspectRatiofit="cover"Tilesjsx
<Image source={url} description={alt} aspectRatio={1} fit="cover" />使用 + 防止布局偏移,并在 网格中对齐卡片,避免源图片固有比例不同导致的布局问题。
aspectRatiofit="cover"Tilesjsx
<Image source={url} description={alt} aspectRatio={1} fit="cover" />Heading semantics via HeadingGroup
通过HeadingGroup实现标题语义化
Heading levels are derived from nesting depth — never set manually unless you need to override visuals.
HeadingGroupleveljsx
<HeadingGroup>
<Heading>Section title</Heading>
<HeadingGroup>
<Heading>Subsection title</Heading>
</HeadingGroup>
</HeadingGroup>标题级别由 的嵌套深度决定——除非需要覆盖视觉样式,否则请勿手动设置 。
HeadingGroupleveljsx
<HeadingGroup>
<Heading>章节标题</Heading>
<HeadingGroup>
<Heading>子章节标题</Heading>
</HeadingGroup>
</HeadingGroup>Lifecycle Contract
生命周期约定
Extension points
扩展点
| Point | String | Purpose |
|---|---|---|
| ShouldRender | | Data prefetch. Decide whether to render. |
| Render | | Mount the React tree. |
| 名称 | 字符串标识 | 用途 |
|---|---|---|
| ShouldRender | | 数据预获取。决定是否渲染扩展。 |
| Render | | 挂载React树。 |
ShouldRender API
ShouldRender API
ts
(api: PostPurchaseShouldRenderApi) => { render: boolean } | Promise<{ render: boolean }>apiinputData: InputDatastorageversionlocaleextensionPoint- — persist data for the Render phase. Returns a Promise —
storage.update(data: any): Promise<void>it before returning.await - Return to mount,
{ render: true }to skip silently. May return synchronously or as a{ render: false }.Promise
ts
(api: PostPurchaseShouldRenderApi) => { render: boolean } | Promise<{ render: boolean }>apiinputData: InputDatastorageversionlocaleextensionPoint- — 为Render阶段持久化数据。返回Promise——返回前需使用
storage.update(data: any): Promise<void>。await - 返回 以挂载扩展,返回
{ render: true }则静默跳过。可同步返回或返回{ render: false }。Promise
Render API
Render API
ts
render("Checkout::PostPurchase::Render", (api: PostPurchaseRenderApi) => ReactElement)| Field | Type | Use |
|---|---|---|
| | Same shape as ShouldRender. |
| | Data written by ShouldRender via |
| | Preview cost impact without applying. Pass either a raw |
| | Apply the order edit and charge the buyer. The |
| | Navigate to thank-you page. Always call this, success or error. |
| from | Available alongside |
ts
render("Checkout::PostPurchase::Render", (api: PostPurchaseRenderApi) => ReactElement)| 字段 | 类型 | 用途 |
|---|---|---|
| | 与ShouldRender中的结构相同。 |
| | ShouldRender阶段通过 |
| `(changeset: Readonly<Changeset> \ | string) => Promise<CalculateChangesetResult>` |
| | 应用订单修改并向买家收费。 |
| | 导航到感谢页。无论成功或失败,都必须调用此方法。 |
| 来自 | 与 |
InputData
InputData
| Field | Type |
|---|---|
| |
| |
| |
| |
| |
| |
LineItemproductquantitytotalPriceSetsellingPlanId?ProductidtitlevariantmetafieldsMetafield.valuestring | numbervalueType'integer' | 'string' | 'json_string'| 字段 | 类型 |
|---|---|
| |
| |
| |
| |
| |
| |
LineItemproductquantitytotalPriceSetsellingPlanId?ProductidtitlevariantmetafieldsMetafield.valuestring | numbervalueType'integer' | 'string' | 'json_string'Changeset shape
Changeset结构
ts
Changeset { changes: Changes }
Changes = (AddVariantChange | AddShippingLineChange | SetMetafieldChange | AddSubscriptionChange)[]ts
Changeset { changes: Changes }
Changes = (AddVariantChange | AddShippingLineChange | SetMetafieldChange | AddSubscriptionChange)[]ApplyChangesetOptions
ApplyChangesetOptions
| Option | Type | Use |
|---|---|---|
| | Set when changes include |
| 选项 | 类型 | 用途 |
|---|---|---|
| | 当修改包含 |
ChangesetErrorCode
ChangesetErrorCode
payment_requiredinsufficient_inventorychangeset_already_appliedunsupported_payment_methodinvalid_requestserver_errorbuyer_consent_requiredsubscription_vaulting_errorsubscription_contract_creation_errorsubscription_no_shipping_address_errorsubscription_limit_errororder_released_errorpayment_requiredinsufficient_inventorychangeset_already_appliedunsupported_payment_methodinvalid_requestserver_errorbuyer_consent_requiredsubscription_vaulting_errorsubscription_contract_creation_errorsubscription_no_shipping_address_errorsubscription_limit_errororder_released_errorComponent Catalog
组件目录
29 components total, served by 28 doc URLs under (lowercase). shares the page rather than having its own URL — when looking it up, fetch .
/components/<name>FormLayoutGroupformlayout/components/formlayoutAll importable from .
@shopify/post-purchase-ui-extensions-react共29个组件,对应 (小写)下的28个文档URL。 共享 页面,无独立URL——查询时请获取 。
/components/<name>FormLayoutGroupformlayout/components/formlayout所有组件均可从 导入。
@shopify/post-purchase-ui-extensions-reactSpacing scales
间距尺度
There is no single "spacing scale" — three different scales coexist. Match the literal exactly to the consumer's :
.d.ts| Scale | Allowed values | Used by |
|---|---|---|
| Stack scale | | |
Stack scale + | | |
| Compact scale | | |
| View padding scale | | |
xtightxlooseextraTightextraLoose没有统一的“间距尺度”——存在三种不同的尺度。请严格匹配消费者 文件中的字面量:
.d.ts| 尺度 | 允许值 | 适用组件 |
|---|---|---|
| Stack尺度 | `'xtight' \ | 'tight' \ |
Stack尺度 + | `'none' \ | 'xtight' \ |
| Compact尺度 | `'none' \ | 'tight' \ |
| View内边距尺度 | `'extraTight' \ | 'tight' \ |
xtightxlooseextraTightextraLooseLayout & Structure
布局与结构
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Vertical stack | |
| Horizontal row | |
| Pin first/last child to intrinsic size, fill middle | |
| Equal-size grid, wraps and stacks responsively | |
| Multi-section page scaffold with media-queried sizes | |
| Generic container that does NOT stretch | |
| Visual divider | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 垂直堆叠容器 | |
| 水平行容器 | |
| 将首尾子元素固定为固有尺寸,中间子元素填充剩余空间 | |
| 等尺寸网格,可响应式换行堆叠 | |
| 多章节页面脚手架,支持媒体查询尺寸 | |
| 通用容器,不会自动拉伸 | |
| 视觉分隔线 | |
Typography
排版
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Section title | |
| Increments heading level for nested children | No props. Wrap children that contain their own |
| Inline styled text | |
| Block-level paragraph | |
| Vertical spacing wrapper for text elements | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 章节标题 | `level?: 1 \ |
| 为嵌套子元素递增标题级别 | 无属性。包裹包含自身 |
| 内联样式文本 | `size?: 'small' \ |
| 块级段落 | `size?: 'small' \ |
| 文本元素的垂直间距包装器 | `spacing?: 'none' \ |
Actions
操作组件
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Primary action | |
| Inline-stacked buttons with auto-spacing | No props. Wraps two or more |
| Navigation | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 主要操作按钮 | |
| 内联堆叠按钮,自动设置间距 | 无属性。包裹两个或多个 |
| 导航链接 | |
Forms
表单组件
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Form wrapper with implicit-submit-on-Enter | |
| Vertical-stacked field layout | No props. Children stack on the block axis. |
| Inline-grouped fields within a | No props. Fields appear side-by-side with equal spacing. Lives in the same |
| Single-line input | |
| Dropdown | |
| Boolean toggle | |
| Single radio button | |
| Subscription consent checkbox | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 表单包装器,支持按Enter键隐式提交 | |
| 垂直堆叠的字段布局 | 无属性。子元素在块轴方向堆叠。 |
| | 无属性。字段并排显示,间距均等。与 |
| 单行输入框 | |
| 下拉选择器 | |
| 布尔切换按钮 | |
| 单选按钮 | |
| 订阅同意复选框 | |
Feedback & Status
反馈与状态组件
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Status / system message | |
| Promotional offer header | |
| Loading indicator | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 状态/系统消息 | |
| 促销优惠头部 | |
| 加载指示器 | `size?: 'small' \ |
Media
媒体组件
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Responsive image | |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 响应式图片 | |
Accessibility
无障碍组件
| Component | Purpose | Key Props / Gotchas |
|---|---|---|
| Hide children from a11y tree but show visually | No props. Use for purely decorative or duplicated content. |
| Hide visually but keep available to screen readers | No props. Use for screen-reader-only labels. |
| 组件 | 用途 | 关键属性 / 注意事项 |
|---|---|---|
| 从无障碍树中隐藏子元素,但保持视觉可见 | 无属性。用于纯装饰或重复内容。 |
| 视觉上隐藏,但对屏幕阅读器可见 | 无属性。用于仅屏幕阅读器可见的标签。 |
| ", |