Payment Request API:用户同意与安全回退
围绕明确用户激活、安全上下文、取消处理和可用回退使用 PaymentRequest,不收集支付凭据。
BotBrowser Team
Payment Request 可以打开由浏览器控制的支付面板,但 show() 完成并不能证明支付已获授权,也不能证明商户收到资金。BotBrowser 支持重放自有 fixture 并检查可见状态;它不能提供真实支付工具、批准银行交易或处理用户凭据。安全边界应明确:在安全上下文中从明确用户操作发起请求,不把能力检查当成同意,分别处理取消和超时,只把 complete() 用于面板的最终 UI 状态,并在 API 或支付数据不可用时保留普通结账路径。
TL;DR
new PaymentRequest(methodData, details, options)描述支持的方法和交易细节;它不会请求凭据,也不保证某个方法存在。canMakePayment()只是能力提示。结果为true不代表同意、授权、有资金,或保证show()会成功。- 在安全上下文中从可见点击或键盘操作调用
show()。把AbortError、NotAllowedError、超时和不支持的方法当作普通产品状态处理。 - 验证 response 后,用
complete('success')、complete('fail')或complete('unknown')关闭面板。complete()报告 UI 结果,不报告结算。 - 绝不要记录支付凭据、支付 response、卡片数据或完整异常。保留手动表单、发票请求或其他非 PaymentRequest 路径。
目录
分开能力与同意
Payment Request API 允许页面描述支付方法,并请求浏览器中介用户选择。它不是支付处理器的替代品,也不是读取卡片或钱包账户的方法。浏览器负责支付面板,用户决定是否继续。商户仍需要服务器端授权、幂等性、防欺诈控制,以及支付提供方给出的结算结果。
canMakePayment() 可以帮助决定是否显示快速支付入口。它可能根据浏览器策略、已登记方法、隐私保护和实现返回 false、true 或拒绝。结果可能在不同访问之间变化。不要用它隐藏完整结账、推断用户钱包或创建稳定的能力 profile。只在用户选择支付路径时请求,并让结果短期有效。
功能检测同样应保持边界:
const paymentSupported = window.isSecureContext && 'PaymentRequest' in window;
if (!paymentSupported) showCheckoutFallback('unsupported');
构造函数可用不代表每种方法都支持。文档也可能没有焦点、位于受限策略下的嵌入 frame 中,或已经离开短暂用户激活。浏览器更新可能改变可用方法而不改变页面代码。把每次尝试当作新的能力观察,不要把它当作永久的用户属性。
关于相近的同意边界,Web Share 操作与回退说明已完成的交接不代表接收方接受了数据;Web Share 权限边界还说明上下文和近期用户激活同样会限制有效调用。Payment Request 可能把支付 response 暴露给商户,但结算仍由服务器和处理器负责。安全上下文指南介绍强大浏览器功能共同要求的 HTTPS 基线。
构造最小 PaymentRequest
让 method data 和交易细节保持最小。方法标识符告诉浏览器商户可以处理哪类支付方法。details 对象提供显示总额和可选行项目,但不是可信金额。response 返回后,服务器必须重新计算最终金额,因为价格、税、库存和货币不能信任页面提供的值。
const methodData = [
{
supportedMethods: 'https://example.test/pay',
data: { version: 1 },
},
];
const details = {
id: 'order-2026-1042',
total: { label: 'Example order', amount: { currency: 'USD', value: '19.00' } },
displayItems: [{ label: 'Item', amount: { currency: 'USD', value: '19.00' } }],
};
const options = { requestPayerName: false, requestPayerEmail: false };
const request = new PaymentRequest(methodData, details, options);
不要把卡号、钱包 token、秘密或个人标识放进 methodData 或页面 URL。requestPayerEmail 等选项只是数据请求,不证明返回值已验证。只请求订单确实需要的字段,说明原因,并在处理器收到已授权 token 后停止保留这些字段。
让 request 对象只属于一次结账尝试。如果购物车改变,应使用新的 details.id 和服务器订单版本创建新 request。超时或导航后不要复用旧对象。短期操作 token 可以让界面拒绝属于旧购物车的迟到 response。
从用户激活开始 show
request.show() 会打开浏览器支付面板,并返回一个包含 PaymentResponse 的 promise。应直接从可见按钮或键盘激活开始。不要在页面加载、定时器、分析回调中调用,也不要先等待可能消耗短暂激活的无关工作。安全上下文、浏览器策略、iframe 边界或平台规则也可能阻止面板。
let activeAttempt = 0;
async function beginPayment() {
const attempt = ++activeAttempt;
if (!window.isSecureContext || !('PaymentRequest' in window)) {
showCheckoutFallback('unsupported');
return;
}
const request = buildRequest();
showPaymentState('opening');
try {
const response = await request.show();
if (attempt !== activeAttempt) return;
await handlePaymentResponse(response, request, attempt);
} catch (error) {
if (attempt !== activeAttempt) return;
showPaymentState(error?.name === 'AbortError' ? 'cancelled' : 'rejected');
showCheckoutFallback(error?.name === 'AbortError' ? 'cancelled' : 'rejected');
}
}
payButton.addEventListener('click', beginPayment);
面板打开时可以禁用重复激活,但必须保留可见替代路径。取消或拒绝后把焦点还给发起按钮。通过 live region 播报 opening、cancelled 和 fallback,不要暴露 response。用户应在不看到浏览器异常的情况下理解下一步。
嵌入式结账需要明确部署决定。顶层策略或 iframe 属性可能影响浏览器是否允许支付功能,但委托不是绕过权限,也不会替用户授予同意。分别测试同 origin 和嵌入路径。不要假设顶层标签页可用的功能在跨 origin frame 中仍然可用。
验证 response 并完成面板
PaymentResponse 包含方法专属数据、请求的 payer 字段以及 complete() 方法。验证 response 形状后,只通过经过认证的通道把处理器批准的 token 发送到服务器。绝不要把 response 当成收据。处理器可能拒绝、延迟或之后撤销授权。
async function handlePaymentResponse(response, request, attempt) {
showPaymentState('validating');
let outcome = 'unknown';
try {
const result = await postPaymentToken({
methodName: response.methodName,
details: response.details,
orderId: request.id,
attempt,
});
outcome = result.authorized === true ? 'success' : 'fail';
} catch {
outcome = 'unknown';
}
try {
await response.complete(outcome);
} catch {
// The payment sheet may already have closed; keep the server outcome.
}
showPaymentState(outcome);
}
对每个 response 只调用一次 complete()。success 表示页面请求浏览器显示成功完成状态,fail 表示失败尝试,服务器结果不可用时使用 unknown。这些字符串都不能证明扣款、退款或交付。权威状态来自处理器服务器 response 和订单的幂等记录。
如果 response 已关闭、浏览器结束了面板或文档失去相关状态,response.complete() 可能拒绝。不要把拒绝变成第二次扣款尝试。保留服务器结果,显示会创建新订单尝试的重试路径,并避免发送同一 token 两次。
区分取消、拒绝和超时
取消不是卡片失败。用户可以关闭面板、改变想法或拒绝某种方法。AbortError 通常表示取消,但不会透露原因。把 cancelled 与 rejected、unsupported 和 processor-failed 分开。保留购物车并提供手动表单,不要在诊断消息中索要支付凭据。
超时需要明确负责人。页面可以为服务器调用设置应用截止时间,但不能假设定时器证明支付面板做了什么。如果收到 response 后服务器请求超时,标记为 unknown,停止重复提交,并在重试前与处理器核对订单。迟到 callback 不能覆盖新的尝试。
方法错误与浏览器错误不同。缺少方法可能让 show() 在面板出现前拒绝。策略或激活阻止可能产生 NotAllowedError。格式错误的 response 属于验证阶段,处理器拒绝属于服务器阶段。让 UI 和支持记录保留这些阶段,这样用户不会因 iframe 策略问题被要求重新输入卡片。
使用一条有界状态序列:idle、opening、shown、response-received、authorizing、success、fail、unknown、cancelled、rejected、unsupported 和 fallback。每次尝试只记录第一个失败阶段。面板关闭后不要自动重试 show(),也不要从后台清理任务调用 abort(),除非产品明确拥有这个取消动作。
数据边界与回退
支付 response 可能包含敏感的方法 token、payer 资料、账单引用或重放后会成为凭据的不透明值。不要在日志、URL、截图、会话回放、崩溃报告或分析维度中放入 response.details、payer email、卡片片段或完整异常。脱敏也要覆盖 PaymentRequest 调用点之外的集成。
{
"api": "PaymentRequest",
"attempt": "pay-7f31",
"capability": "available",
"outcome": "cancelled",
"methodNamePresent": true,
"paymentDetailsRetained": false,
"settlement": "not-asserted"
}
这条记录足以回答“面板是否打开,以及页面随后显示了什么结果”,但不能回答用户是否拥有某个账户,也不能回答处理器是否扣款。token 应随机、短期有效,诊断导出应过期,任何支持捕获都必须明确并可复核。不要把支付 token 哈希后就当作无害,稳定值仍可能关联多个 session。
设计回退并区分结算。
第一屏就应保留回退。当 PaymentRequest 不存在、canMakePayment() 拒绝、show() 被取消、方法不支持或处理器 response 未知时,保留普通结账表单、发票请求、支付链接或联系流程。回退必须保留购物车、键盘访问、标签、校验以及下一步说明。
不要因为 canMakePayment() 返回 true 就禁用回退。没有服务器权威处理器结果,不要在 complete('success') 后显示“支付完成”。不要把浏览器面板与 Web Share、Credential Management 或钱包自己的应用 UI 混淆。Payment Request 是浏览器中介的结账界面,不是凭据存储,也不会绕过处理器。
在受控 fixture 中使用合成方法标识符和虚假订单金额。断言没有凭据字段被记录、取消后焦点返回、未知服务器结果调用 complete('unknown'),以及手动路径仍可用。真实支付凭据和支付提供方 endpoint 不应进入浏览器自动化 fixture。
使用决策表。
| 观察阶段 | 它能证明什么 | 安全下一步 |
|---|---|---|
| API 不存在或上下文不安全 | 这里不能启动 Payment Request | 保留手动结账或发票回退 |
canMakePayment() 返回 false 或拒绝 | 本次没有有用的能力提示 | 保留主要结账,不推断钱包身份 |
show() 以 AbortError 拒绝 | 面板被取消或关闭 | 保留购物车,记录 cancelled,等待新的动作 |
| 策略或激活阻止 | 浏览器拒绝了本次尝试 | 检查安全上下文、焦点、frame 策略和手势负责人 |
| 收到 response | 浏览器把方法数据返回页面 | 严格验证,只向服务器发送获准 token 数据 |
| 服务器授权被拒 | 处理器没有授权本次尝试 | 调用 complete('fail'),说明回退,避免重复重试 |
| 服务器结果超时 | 结算状态未知 | 条件允许时调用 complete('unknown'),重试前先核对 |
complete('success') 完成 | 面板接受了 UI 完成请求 | 只显示服务器权威的订单状态 |
BotBrowser 的能力和限制。
BotBrowser 受控上下文可以重放同 origin 的 PaymentRequest fixture,使用合成方法,运行能力和用户激活分支,模拟取消或超时,并断言脱敏的可见结果和可用回退。高级浏览器能力文档说明了页面自有边界。BotBrowser 不能提供真实支付工具、批准银行或钱包交易、证明结算,或为测试收集支付凭据。
保留确定性的 fixture 记录:
{
"api": "PaymentRequest",
"secureContext": true,
"userActivation": true,
"canMakePayment": "hint-only",
"show": "cancelled",
"complete": "unknown",
"paymentDetailsRetained": false,
"fallback": "manual-checkout"
}
每次只改变一个变量后重复测试:浏览器版本、顶层 origin、iframe 策略、激活时间、合成方法、服务器超时或取消分支。比较第一个可见结果差异。通过 fixture 只能证明页面状态处理,不能证明支付授权或资金移动。
来源
- W3C Payment Request API
- MDN:Payment Request API
- MDN:PaymentRequest.show()
- MDN:PaymentRequest.canMakePayment()
- MDN:PaymentResponse.complete()
- MDN:安全上下文
- BotBrowser 高级能力
实践结论
只把 PaymentRequest 当作浏览器中介的同意界面。构造最小请求,在安全上下文中从新鲜用户操作调用 show(),把 response 交给服务器验证,并让 complete() 只负责面板状态,不声称已结算。区分取消、拒绝、超时和方法不支持,记录脱敏结果,并保留完整的非 PaymentRequest 结账路径。受控 BotBrowser fixture 可以证明页面可见分支,不能证明真实扣款。
本文属于指南: Web 平台能力面:一致性复核地图