本页包含服务端职责(创建支付 session、接收 Webhook)与前端职责(挂载组件、提交支付、处理结果)。全部属性、方法、事件和取值见 Element API 参考。
适用场景
工作流程
1
服务端创建支付 session
商户服务端向 Waffo 创建支付 session,拿到
sessionId。2
前端挂载组件
用
sessionId 创建 Elements 实例并挂载 Payment Element。3
用户支付
钱包等支付方式由组件内按钮直接提交;卡等支付方式由你的支付按钮调用
elements.confirmPayment() 提交。4
处理结果
前端在
payment 事件里更新界面,服务端以 Webhook 为准完成发货。前置条件
- 已开通 Waffo 商户账号,并获取前端使用的 Client API Key。它只能用于浏览器侧,不是服务端签名密钥。
- 结账页通过 HTTPS 提供。本地开发可以使用
http://localhost或http://127.0.0.1,其他 HTTP 页面会导致组件无法启动,并返回SDK_CONNECTION_FAILED。 - 安装
@waffo/payment-sdk,版本需包含 Payment Element。
Step 1: 服务端创建支付 session
在商户服务端为本次支付创建 session,把返回的sessionId 交给前端。金额、币种、商户订单号等信息都在这一步确定,前端拿不到也改不了。session 中可用的支付方式决定了组件能展示哪些支付方式。
Step 2: 初始化 SDK 与 Elements
env必须与sessionId、Client API Key 所属环境一致。- 在 React、Next.js 等框架中,请在客户端挂载后初始化,不要在服务端渲染期间执行。
- 一个结账流程只创建一个 Elements 实例,它下面的所有组件共享同一个 session 和同一把提交锁。
Step 3: 选择接入方式并挂载
Payment Element 提供四种接入方式。它们只是界面组织方式不同,不会增加 session 本身没有的支付方式。- 整体接入
- 分组接入
- 原子化接入
需要用户填写的信息
有些支付方式要求用户填写额外信息,如证件号、地址、收款银行。组件会按 session 中支付方式所属的国家,套用和 Waffo 收银台相同的规则,你不需要额外配置:- 证件号:按国家的格式校验并自动排版,如巴西 CPF 显示为
123.456.789-09。 - 巴西地址:邮编填满 8 位后自动查询并填入州、城市和街道;再次修改邮编时,查到的地址只作为提示,由用户点填写覆盖。邮编填好之前,地址栏先不显示。
- 收款银行:印度显示为下拉框,其他国家显示为带银行 logo 的单选列表。
多个组件并存的规则
- 同一个 session 下的组件展示内容不能重叠,否则
mount()抛出SDK_IFRAME_CREATE_FAILED。 - 分组和支付方式名称不能写在同一个数组里;跨组件时,只有
['card']分组可以与支付方式名称组件同时挂载。 - 相同的选择会返回同一个实例:
['card', 'express']与['express', 'card']相同。数组顺序不影响展示顺序,展示顺序请用paymentMethodOrder。 - 同时挂载了多个需要你提交的组件时,调用
confirmPayment(element)必须指定目标组件。
自定义渲染
自定义渲染适合你想用自己的设计系统绘制按钮和输入框的场景。SDK 在页面中维护一个隐藏的 iframe,负责通信、提交和后续动作,不需要调用mount()。
仅支持单个非卡、非 Apple Pay、非 Google Pay 的支付方式。session 中没有该支付方式时,create() 返回的 Promise 会 reject。
wechat.payMethod 与 state.payMethod 的结构:
userFields是扁平的键值对,key 直接使用requiredFields[].field(如userInfo.userEmail),不要展开成嵌套对象。- 字段校验失败时,
confirmPayment()以INVALID_USER_FIELD拒绝,field指向出错字段;state.errors中也会出现同样的错误。 requiredFields中也可能包含required: false的选填字段。- 示例中的
message是你页面上的错误提示元素。
Step 4: 提交支付
不同支付方式由不同的入口提交。组件通过state.confirmTrigger 告诉你当前选中的方式由谁提交,不要根据支付方式名称自行判断:
confirmPayment 的参数与返回值
PaymentElement
要提交的组件,即
elements.create() 返回的对象。页面上只有一个需要你提交的组件时可省略;同时挂载了多个时必须传。object
仅自定义渲染组件使用,普通组件传入会 reject。
Promise<PaymentResult>
resolve 的值与
payment 事件的 result 相同,字段见 elements.on(‘payment’)。返回的是第一个结果:遇到付款指引时,用户关闭指引就 resolve 为 processing,最终结果只会通过 payment 事件送达。订阅 session
当 session 为订阅类型且使用整体接入时,组件会展示订阅协议,用户勾选后才能提交。未勾选就提交时,confirmPayment() 会 reject,error 事件返回 TERMS_NOT_AGREED。分组接入、原子化接入和自定义渲染不展示协议,请在你的页面上自行展示并校验。
Step 5: 处理支付结果
无论用户从哪个入口提交,结果都通过elements.on('payment') 返回;所有错误都通过 elements.on('error') 返回。请只在 payment 事件里处理结果,confirmPayment() 的 Promise 只用于捕获异常,避免同一个结果处理两次。
processing 之后同一次支付还可能再收到一次 succeeded 或 failed,payment 事件不是只触发一次。result 的完整字段见 elements.on(‘payment’),err 的字段见 elements.on(‘error’)。
前端结果只用于更新界面。请以 Webhook 或订单查询结果作为发货依据。
Step 6: 服务端接收 Webhook
Payment Element 产生的订单与其他集成方式一样,通过 Webhook 通知最终结果。处理方式见 Webhook 概览。会话状态
组件每次读取 session 时,都会先看它的支付状态,只有还能付的 session 才显示支付表单。已经付过、处理中或已结束的 session,组件会换成一块状态提示,并且不能再提交:
不能付的 session 下,
state.availablePayMethods 为空,confirmTrigger 为 'element',所以按 confirmTrigger 显示的支付按钮会自动隐藏。
这意味着:
- 挂载时也可能触发
payment事件。用已经付过的sessionId挂载组件时,你会马上收到succeeded,可以直接按成功处理,例如跳到成功页。 - 整页跳转回来后可以恢复结果。在
redirect事件里保存sessionId,用户回到你的页面后,用它重新创建 Elements 并挂载组件,已支付或已结束的结果会通过payment事件送达。处理中的 session 不会触发事件,请以 Webhook 或订单查询为准。 - 重复提交不会被当成支付失败。用户在已经付过的 session 上再次提交时,组件会重新读取 session,按它的状态返回结果:已支付返回
succeeded,已结束返回failed,等待后续动作时直接打开那笔订单。
监听事件
Payment Element 的事件分两层:组件(PaymentElement)上的事件反映单个组件的状态,Elements 上的事件反映整次支付的结果。
订阅与取消订阅
- 回调函数的返回值会被忽略,不需要返回任何内容;
async函数也不会被等待。 - 回调中抛出的异常只会打印到控制台,不影响其他回调和支付流程。
elements.destroy()会清除所有订阅。- 请在
mount()之前订阅element的事件,避免错过首次ready。
ready 事件
element.on('ready', (state) => {})
组件第一次渲染完成、可以交互时触发。updateSessionId() 或组件自动刷新 session 后会再次触发。它不代表支付成功。
state 与 element.on(‘change’) 相同。
change 事件
element.on('change', (state) => {})
组件状态变化时触发,例如用户切换支付方式、填写表单、开始提交、进入 processing。状态没有实际变化时不会重复触发。通常用它控制你的支付按钮。
state(element.getState() 返回同样的结构,组件就绪前返回 null):
一个整体接入组件在用户选中卡之后的
state 示例:
组件 error 事件
element.on('error', (err) => {})
该组件出错时触发。同一个错误也会出现在 elements.on('error'),两个都监听会收到两次,一般只需监听 elements.on('error');需要区分是哪个组件出错时才使用它。
err 与 elements.on(‘error’) 相同。
payment 事件
elements.on('payment', (result) => {})
产生支付结果时触发。钱包等组件自己提交的支付,和你调用 confirmPayment() 提交的支付,结果都从这里返回。同一次支付可能触发多次:先收到 processing,之后再收到 succeeded 或 failed。挂载时 session 已经支付或已结束,也会立即触发一次,见 会话状态。
result(与 confirmPayment() 的返回值结构相同):
用
'sessionType' in data 区分两种 data:
- 订单数据
- 订阅数据
普通支付 session 返回订单数据:
redirect 事件
elements.on('redirect', ({ url }) => {})
SDK 即将整页跳转到外部支付页面(如银行页面)时触发,当前页面随后会被卸载。此时还没有支付结果,所以不会触发 payment。仅在 handleNextAction 为 true(默认)时出现。
error 事件
elements.on('error', (err) => {})
任意组件出错、SDK 处理后续动作失败或接口返回错误时触发,是所有错误的统一入口。
err:
3DS 与后续动作
默认情况下,SDK 会自动完成支付的后续动作:
弹层右上角始终有关闭按钮。用户关闭弹层时,你会收到
processing;SDK 在后台继续查询订单,拿到最终结果后再触发一次 payment。
关闭付款指引不等于取消订单;关闭 3DS 后用户无法再完成验证,订单会停在处理中,直到超时或失败。
confirmPayment() 返回的是第一个结果:用户关闭弹层时就以 processing 返回,不会等待银行转账这类长时间流程。请持续监听 payment 事件获取后续结果。
收到 redirect 事件时,保存恢复页面所需的信息(如 sessionId、你的订单号)。
redirect 的回调参数只有一个字段 url(string),即 SDK 即将整页跳转到的外部支付页面地址。
如果你希望自己处理后续动作,创建时传入 handleNextAction: false。此时 SDK 不打开弹层也不跳转,而是返回 status: 'requires_action'。动作信息是 JSON 字符串,订单在 data.orderAction,订阅在 data.subscription.subscriptionAction:
外观与语言
通过appearance 设置组件外观,挂载后可以用 elements.updateAppearance() 实时更新,无需重新挂载:
variables 和 layout 之外,还可以用两项做更细的调整:
variables.spacing:组件内的间距,包括快捷按钮之间、手风琴各行之间、表单字段之间。rules:用 CSS 声明单独调整某类组件的样式,如输入框、标签、跳转按钮、支付方式图标,写法参考 Stripe Appearance API。
inputs: 'condensed'(对齐 Stripe 的 appearance.inputs)把卡号、有效期和 CVV 拼成一块,只保留一个「银行卡信息」标题,各字段用框内占位文字说明;持卡人姓名在下方单独一个输入框,与 Stripe Checkout 一致;默认的 'spaced' 每个字段单独带标签。目前只作用于卡表单。
labels: 'floating'(对齐 Stripe 的 appearance.labels)把字段名放进输入框里:没有内容时当作占位文字,聚焦或填写后缩小浮到框的左上角;默认的 'above' 把字段名放在框上方。和 inputs: 'condensed' 一起用时,「银行卡信息」标题和持卡人姓名的标签不再单独显示。目前只作用于卡表单。浮动标签默认使用次要文字色,.Label 规则不会改变它的颜色,要单独调整请用 .Label--floating。
输入框尺寸与字号:输入框文字是 fontSizeBase 的 0.875 倍(传 16px 得到 14px)。要和页面上的其他输入框对齐高度、内边距、字重或字号,请用 .Input 规则。rules 不能直接写 height,可以用上下内边距调整高度:
rules 选择器和布局选项见 Element API 参考。
locale 支持以下语言,未设置时默认 en:
更新 session
用户修改了商品或优惠后,商户服务端更新 session,前端调用updateSessionId() 原地刷新所有组件,无需重新挂载:
updateSessionId()不会替你创建 session,新 session 需要服务端先创建。- 组件还在加载、或上一次更新尚未完成时再次调用,会排队执行:连续调用只有最后一个
sessionId生效,排队的调用在它完成时一起返回。 - 支付已经开始(提交中、等待 3DS 或付款指引、该 session 已有结果)时调用会抛出
SDK_ELEMENTS_BUSY。 - 同一个 session 改价刷新时,用户已填写的卡号、有效期、CVV 和其他字段会保留;换成新的
sessionId时表单会清空。 - 更新进行中也可以挂载新的组件(例如改国家后新出现的钱包),新组件直接使用更新后的 session。
- 某个组件 15 秒内没有响应时,更新以
SDK_CONNECTION_FAILED失败,phase变为error。组件的容器已被页面移除时,该组件会在更新时被自动卸载,需要时请重新mount()。 - 支付已进入
processing、succeeded或failed后,不能用同一个sessionId刷新后重新支付,请换新 session。 - 更新后可用支付方式、金额和自定义渲染的字段可能变化,请在
change事件中重新读取。
清理
unmount()和destroy()只移除前端组件,不会取消或退款后端订单。- 支付处于
processing时不要提前调用destroy(),否则收不到后续的payment事件。
错误处理
错误通过两种方式返回:- 方法抛出:
Elements/PaymentElement的方法抛出的异常,包括confirmPayment()的所有 reject,都带code和message,字段类问题还带field。 error事件:elements.on('error')返回{ code, message, field? }。
code 都可能来自 SDK(SDK_*)、组件或 Waffo 接口。
组件和接口给出的 message 已按 locale 本地化,可以直接展示给用户;SDK 在浏览器本地抛出的错误(如组件未挂载、正在提交、options 用错),message 是英文的开发说明,建议按 code 换成你自己的文案。不要根据 message 文案做业务分支。组件级的 element.on('error') 与 elements.on('error') 会收到相同的错误,一般只需监听后者。
confirmPayment 被拒时的错误
confirmPayment() reject 的是一个 Error 对象(name 为 WaffoSDKError),带以下字段:
常见的错误码和处理方式:
同时触发
error 事件的错误会收到两次,请用同一个提示区替换显示,避免重复提示:
上线检查清单
- 生产环境使用
env: 'prod'和生产环境 Client API Key - 结账页通过 HTTPS 提供
- 如需 Apple Pay,已在 Waffo 完成域名配置,见 Apple Pay 集成
- 以
confirmTrigger决定是否展示你的支付按钮,钱包路径不调用confirmPayment() - 只在
payment事件中处理结果,processing期间保留 Elements 实例 - 发货以 Webhook 或订单查询为准,不只依赖前端结果
- 离开结账流程时调用
elements.destroy()