跳转到内容

为 PWA 创造收入

发布于

完成本指南后,PWA 里的结账按钮会通过 Payment Request API 唤起浏览器自带的支付面板, 收集买家的卡片或钱包信息,把支付响应交给你的服务器,并在拒绝该请求的浏览器中回退到现有表单。 这个 API 是收集支付、地址与联系方式的标准方式;它本身不转账,扣款仍由你的支付处理商完成。

你需要一个通过 HTTPS 提供的页面(普通 HTTP 下该 API 为 undefined)、支付处理商给出的支付方式标识符, 以及一个接收处理商令牌的服务端接口。哪些商店与支付网络允许网页端结账属于政策问题,带日期的来源整理在 支付。

没有该 API 的浏览器中 window.PaymentRequest 不存在,所以渲染原生结账按钮之前先检测。存在时,构造一个 一次性请求并调用 canMakePayment():浏览器能处理你列出的至少一种方式时它解析为 true。下面的 URL 形式 标识符沿用 MDN 示例的写法;你的处理商会给出自己的标识符。

const methodData = [{ supportedMethods: 'https://example.com/pay' }];
const stubDetails = {
total: { label: 'Stub', amount: { currency: 'USD', value: '0.01' } },
};
async function nativeCheckoutAvailable() {
if (!('PaymentRequest' in window)) return false;
try {
return await new PaymentRequest(methodData, stubDetails).canMakePayment();
} catch {
// 用户可能在隐私设置里关闭了该查询,或者浏览器对频繁调用
// 以 DOMException 拒绝。
return false;
}
}

每个页面只调用一次 canMakePayment(),不要每次渲染都调:MDN 注明调用过于频繁时浏览器可能以 DOMException 拒绝该 Promise。传给预检请求的 methodData 要与真实请求完全一致,否则预检回答的是另一个问题。

每次点击都新建一个 PaymentRequest:show() 在每个实例上只能调用一次。在处理函数内同步调用 show()。 规范允许浏览器在页面没有瞬时用户激活时以 SecurityError 拒绝 show(),Chrome 正是这样做的, 因此在点击与 show() 之间 await 一次网络请求会让结账失败。金额还没算好时,把一个 Promise 作为 show() 的参数传入,而不是先 await。

function buildDetails(cart) {
return {
id: cart.orderId,
displayItems: cart.lines.map((line) => ({
label: line.name,
amount: { currency: 'USD', value: line.price },
})),
total: { label: 'Total', amount: { currency: 'USD', value: cart.total } },
};
}
document.querySelector('#checkout').addEventListener('click', async () => {
const request = new PaymentRequest(methodData, buildDetails(currentCart()));
try {
const response = await request.show();
const result = await fetch('/api/charge', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ orderId: currentCart().orderId, details: response.details }),
});
await response.complete(result.ok ? 'success' : 'fail');
} catch (error) {
if (error.name === 'NotSupportedError') {
location.assign('/checkout/form'); // 这种支付方式没有可用的支付应用
} else if (error.name !== 'AbortError') {
throw error; // 买家关闭面板不算错误
}
}
});

response.details 装的是支付方式返回的内容:对 Google Pay 而言是一个支付令牌,由你的后端转交给处理商, 所以扣款发生在服务端,complete() 传入的是服务端的结论。Google Pay 也提供自己的 JavaScript 库(pay.js) 来驱动同一个面板;代价是页面多加载一个脚本,换来的是在没有 Payment Request API 的浏览器里也能渲染按钮。

3. 保留表单路径并显示正确的按钮

Section titled “3. 保留表单路径并显示正确的按钮”

只在第 1 步返回 true 时渲染原生结账按钮;否则渲染指向表单结账的链接。原生面板省去买家在手机上打字, 代价是多出一条需要测试的结账路径。表单是没有该 API 的浏览器(参见 Payment Request API 浏览器支持)以及关闭了面板的买家的回退。

nativeCheckoutAvailable().then((ok) => {
document.querySelector('#checkout').hidden = !ok;
document.querySelector('#checkout-form-link').hidden = ok;
});

面板以 success 关闭时,浏览器收起面板、页面继续;以 fail 关闭时,浏览器显示自己的错误状态。 至此,有浏览器面板的地方用面板,其余地方用你的表单。

← 返回指南总览。