Skip to main content
Payment Element 把 Waffo 收银台的支付方式列表、卡表单和钱包按钮,以组件的形式嵌入你自己的结账页。用户不离开你的页面就能完成支付;卡号、钱包报文、3DS 验证都在 Waffo 的 iframe 内处理,不经过你的代码。
本页包含服务端职责(创建支付 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 中可用的支付方式决定了组件能展示哪些支付方式。
创建支付 session 的服务端接口尚未在 API 参考中公开,接入前请联系 Waffo TAM 获取接口说明。

Step 2: 初始化 SDK 与 Elements

  • env 必须与 sessionId、Client API Key 所属环境一致。
  • 在 React、Next.js 等框架中,请在客户端挂载后初始化,不要在服务端渲染期间执行。
  • 一个结账流程只创建一个 Elements 实例,它下面的所有组件共享同一个 session 和同一把提交锁。

Step 3: 选择接入方式并挂载

Payment Element 提供四种接入方式。它们只是界面组织方式不同,不会增加 session 本身没有的支付方式。
组件高度随内容自动调整,不需要给容器设固定高度。state 的全部字段见 element.on(‘change’)。

需要用户填写的信息

有些支付方式要求用户填写额外信息,如证件号、地址、收款银行。组件会按 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 告诉你当前选中的方式由谁提交,不要根据支付方式名称自行判断:
不要根据 state.complete 禁用支付按钮。初始状态下没有选中任何支付方式,complete 为 false;按钮一旦被禁用,用户就无法点击触发校验,也就看不到缺少哪些信息。可以根据 state.phase 和 state.loading 防止重复提交。

confirmPayment 的参数与返回值

PaymentElement
要提交的组件,即 elements.create() 返回的对象。页面上只有一个需要你提交的组件时可省略;同时挂载了多个时必须传。
object
仅自定义渲染组件使用,普通组件传入会 reject。
Promise<PaymentResult>
resolve 的值与 payment 事件的 result 相同,字段见 elements.on(‘payment’)。返回的是第一个结果:遇到付款指引时,用户关闭指引就 resolve 为 processing,最终结果只会通过 payment 事件送达。
以下情况会 reject:

订阅 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 返回订单数据:
缺失的字段 SDK 不会补默认值,读取前请判空。一次成功的订单支付结果示例:

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 回调里做页面跳转,否则会打断 SDK 发起的跳转。用户从外部支付页面返回后,可以用保存的 sessionId 重新挂载组件恢复结果(见 会话状态),并以服务端查询或 Webhook 为准。
redirect 的回调参数只有一个字段 url(string),即 SDK 即将整页跳转到的外部支付页面地址。 如果你希望自己处理后续动作,创建时传入 handleNextAction: false。此时 SDK 不打开弹层也不跳转,而是返回 status: 'requires_action'。动作信息是 JSON 字符串,订单在 data.orderAction,订阅在 data.subscription.subscriptionAction:
自行处理后续动作后,SDK 不会再通知最终结果,请通过服务端查询或 Webhook 确认。

外观与语言

通过 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,可以用上下内边距调整高度:
容器较窄时:点击即跳转的支付方式按钮,文字不换行,放不下时显示省略号;按钮宽度不足 96px 时只显示图标。Apple Pay、Google Pay 按钮由钱包方渲染,有自身的最小宽度,请为快捷支付区留出足够宽度。 全部主题变量、间距、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 事件。
在 React 等框架中,请在组件卸载时调用 elements.destroy();sessionId 变化时调用 updateSessionId(),不要重新创建 Elements 实例。

错误处理

错误通过两种方式返回:
  • 方法抛出: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 事件的错误会收到两次,请用同一个提示区替换显示,避免重复提示:
全部错误码见 Element API 参考。

上线检查清单

  • 生产环境使用 env: 'prod' 和生产环境 Client API Key
  • 结账页通过 HTTPS 提供
  • 如需 Apple Pay,已在 Waffo 完成域名配置,见 Apple Pay 集成
  • 以 confirmTrigger 决定是否展示你的支付按钮,钱包路径不调用 confirmPayment()
  • 只在 payment 事件中处理结果,processing 期间保留 Elements 实例
  • 发货以 Webhook 或订单查询为准,不只依赖前端结果
  • 离开结账流程时调用 elements.destroy()