# Payment Request API

> new PaymentRequest(methodData, details, options)、canMakePayment()、show() 与 complete()，W3C 规范定义的每个异常，以及不支持时的托管结账回退方案。

`PaymentRequest` 把结账面板交给浏览器：页面列出接受的支付方式、商品明细与总额、需要的联系人字段，浏览器从已存的卡片、Apple Pay、Google Pay 或已安装的支付处理程序收集凭据，以 `PaymentResponse` 返回。页面不渲染卡片表单，除非所选方式主动返回，页面也接触不到原始卡号。

桌面 Chrome 60 与 Android 上的 Chrome 53 发布了该构造函数，Edge 15 与 Safari 11.1 跟进，Android WebView 在 136 加入（BCD `api.PaymentRequest`）。Safari 只实现一种支付方式：`https://apple.com/apple-pay`。Firefox 55 的实现藏在 `dom.payments.request.enabled` 与 `dom.payments.request.supportedRegions` 之后，没有任何版本默认开启。

## 语法

```js
new PaymentRequest(methodData, details)
new PaymentRequest(methodData, details, options)

request.canMakePayment()
request.show()
request.show(detailsPromise)
request.abort()

response.complete()
response.complete(result)
response.retry(errorFields)
```

`canMakePayment()` 返回 `Promise<boolean>`，不显示任何 UI。`show()` 返回 `Promise<PaymentResponse>`，用户授权付款后兑现；`abort()` 与 `complete()` 返回 `Promise<undefined>`，`retry()` 返回的 Promise 在用户改正被标记的字段后兑现。构造函数带 `[SecureContext]`，并要求 `payment` Permissions Policy，其默认允许列表为 `'self'`；跨源 iframe 需要 `allow="payment"`。请求对象一次性使用：`show()` 调用过一次后 `[[state]]` 离开 `"created"`，再次 `show()` 或之后的 `canMakePayment()` 都会被拒绝。

## 参数

构造函数接受两个必填字典和一个可选字典。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `methodData` | `sequence<PaymentMethodData>` | 是 | 每种接受的支付方式一项。`supportedMethods` 是支付方式标识符，要么是 `https://google.com/pay`、`https://apple.com/apple-pay` 这样的 URL，要么是 `secure-payment-confirmation` 这样的标准化字符串；`data` 是该方式自己的规范定义的对象（商户 id、允许的卡组织）。列表不能为空，也不能重复同一标识符。 |
| `details` | `PaymentDetailsInit` | 是 | `total`（`PaymentItem`：`label`、`amount`）必填；`amount` 是 `PaymentCurrencyAmount`，含 ISO 4217 `currency` 代码和 `"29.99"` 这样的十进制字符串 `value`。可选成员：`id`（省略时生成 UUID）、`displayItems`、`shippingOptions`（`id`、`label`、`amount`、`selected`），以及按支付方式改动总额的 `modifiers`（`supportedMethods`、`total`、`additionalDisplayItems`、`data`）。 |
| `options` | `PaymentOptions` | 否 | `requestPayerName`、`requestPayerEmail`、`requestPayerPhone`、`requestShipping`（布尔，默认 `false`）与 `shippingType`（`"shipping"`、`"delivery"` 或 `"pickup"`，默认 `"shipping"`，只影响面板上的文案）。 |

`show(detailsPromise)` 接受可选的 `Promise<PaymentDetailsUpdate>`，让面板在服务端算完购物车之前就能打开。`complete(result)` 接受 `"success"`、`"fail"` 或 `"unknown"`（默认值）。`retry(errorFields)` 接受 `PaymentValidationErrors` 字典，含 `error`、`payer`（`name`、`email`、`phone`）、`shippingAddress`（按地址字段键入的 `AddressErrors`）与 `paymentMethod`。

## 异常

规范定义了下列失败情形。Chrome 附带固定消息的地方引自 `components/payments/core/error_strings.cc`。

| 异常 | 条件 |
|---|---|
| `SecurityError` | 构造函数：文档不被允许使用 `payment` Permissions Policy。`show()`：浏览器要求瞬时激活却没有，或对调用限速。 |
| `TypeError` | 构造函数：`methodData` 为空；缺少 `details.total`；某个 `amount.value` 不是合法的十进制金额字符串，或总额为负；两个配送选项共用一个 `id`。Chrome 另加限制：最多 1024 个配送选项、1024 个 modifier，`details.id` 最长 1024 字符。 |
| `RangeError` | 构造函数：支付方式标识符既不是合法 URL 也不是标准化字符串、同一标识符出现两次，或 `currency` 不是格式正确的 ISO 4217 代码。 |
| `InvalidStateError` | `show()`：文档不是 fully active，或 `[[state]]` 不是 `"created"`（Chrome：`Already called show() once`）。`abort()`：没有进行中的 `show()` 或 `retry()`、已有 `abort()` 挂起，或浏览器无法中止当前交互。`canMakePayment()`：`[[state]]` 不是 `"created"` 或文档不是 fully active。`complete()` 与 `retry()`：响应已完成，或有 `retry()` 挂起。 |
| `AbortError` | `show()`：文档不可见、已有另一张支付面板在显示（Chrome：`Another PaymentRequest UI is already showing in a different tab or window.`）、用户关闭面板（Chrome：`User closed the Payment Request UI.`），或页面调用了 `abort()`（Chrome：`The website has aborted the payment`）。`complete()`：面板打开期间文档不再 fully active。 |
| `NotSupportedError` | `show()`：列出的方式没有一个受支持。Chrome 还会在既非 `localhost`、`file://` 也非加密协议的源上拒绝，消息为 `Only localhost, file://, and cryptographic scheme origins allowed.`。 |
| `NotAllowedError` | `canMakePayment()`：浏览器对反复变换支付方式列表的查询施加反指纹配额。 |
| `OperationError` | `show()`：所选支付处理程序报告内部错误，例如被操作系统终止。 |

`canMakePayment()` 兑现为 `false` 不是异常：它表示设备上没有任何列出的方式可用，这就是改显示托管结账页的信号。

:::observed
关闭 Chrome 的支付面板，`show()` 的 Promise 以 `AbortError: User closed the Payment Request UI.` 拒绝；在同一对象上第二次调用 `show()` 抛出 `InvalidStateError: Already called show() once`。在缺少 `allow="payment"` 的跨源 iframe 内构造请求，抛出 `SecurityError: Must be in a top-level browsing context or an iframe needs to specify allow="payment" explicitly`。同一次页面加载中没有用户手势的第二次 `show()` 被拒绝，消息为 `PaymentRequest.show() calls after the first (per page load) require either transient user activation or delegated payment request capability.`。这些字符串定义在 Chromium 的 [`error_strings.cc`](https://chromium.googlesource.com/chromium/src/+/main/components/payments/core/error_strings.cc) 与 [`payment_request.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/payments/payment_request.cc)（chromium.googlesource.com）。
:::

## 示例

示例以 Google Pay 作为支付方式标识符；换成你的支付服务商的标识符与 `data` 对象即可。每个示例都先检测构造函数，缺失或没有可用方式时回退到托管结账页。

### 仅在 `canMakePayment()` 兑现为 true 时显示钱包按钮

页面加载时构造请求，询问是否有任何列出的方式可用，只在 `true` 时展示原生按钮。结果为 `false` 或构造函数缺失时，保留普通的「继续结账」链接，它跳转到支付服务商的托管页面。

```js
const methodData = [{
  supportedMethods: "https://google.com/pay",
  data: { environment: "PRODUCTION", apiVersion: 2, apiVersionMinor: 0 /* 商户配置 */ },
}];
const details = {
  total: { label: "合计", amount: { currency: "CNY", value: "199.00" } },
};

async function prepareCheckout() {
  const hosted = document.querySelector("#hosted-checkout");
  const wallet = document.querySelector("#wallet-button");

  if (!("PaymentRequest" in window)) {
    hosted.hidden = false;
    return;
  }

  const request = new PaymentRequest(methodData, details);
  let available = false;
  try {
    available = await request.canMakePayment();
  } catch (err) {
    console.warn(`${err.name}: ${err.message}`); // 超出浏览器查询配额时被拒绝
  }

  wallet.hidden = !available;
  hosted.hidden = available;
}

prepareCheckout();
```

保留用来测试的 `request` 对象：`canMakePayment()` 不会消耗它，下一个示例的点击处理函数可以在同一实例上调用 `show()`。

### 服务端扣款后用 `complete()` 关闭面板

`show()` 必须在点击处理函数内运行，瞬时激活才在。把 `response.toJSON()` 发给服务端，服务端一回应就调用 `complete("success")` 或 `complete("fail")`；在此之前面板一直开着，Chrome 超时后会自行关闭，效果等同于无参调用 `complete()`。

```js
const wallet = document.querySelector("#wallet-button");

wallet.addEventListener("click", async () => {
  const request = new PaymentRequest(methodData, details);
  let response;
  try {
    response = await request.show();
  } catch (err) {
    if (err.name === "AbortError") return; // 用户关闭了面板
    location.assign("/checkout/hosted"); // 其他任何拒绝：回退到托管结账页
    return;
  }

  const result = await fetch("/api/charge", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(response.toJSON()),
  });

  await response.complete(result.ok ? "success" : "fail");
  if (result.ok) location.assign("/checkout/thanks");
});
```

`complete("fail")` 显示浏览器的失败状态并关闭面板；接下来该告诉用户怎么办，是页面的责任。

### 配送地址变化时重新计价

设置 `requestShipping: true` 后，面板让用户选地址，并在请求对象上触发 `shippingaddresschange`。在监听器内同步调用 `event.updateWith()`，传入新明细的 Promise；完结之前面板显示加载状态。规范允许浏览器在用户授权前对地址做脱敏，所以用 `country`、`region` 与 `postalCode` 计算运费，不要依赖街道行。

```js
const request = new PaymentRequest(methodData, {
  total: { label: "合计", amount: { currency: "CNY", value: "199.00" } },
  shippingOptions: [
    { id: "ground", label: "普通快递（5 天）", amount: { currency: "CNY", value: "0.00" }, selected: true },
  ],
}, { requestShipping: true });

request.addEventListener("shippingaddresschange", (event) => {
  const { country, postalCode } = event.target.shippingAddress;
  event.updateWith(
    fetch(`/api/shipping?country=${country}&postal=${postalCode}`)
      .then((r) => r.json())
      .then((quote) => ({
        total: { label: "合计", amount: { currency: "CNY", value: quote.total } },
        shippingOptions: quote.options,
      }))
  );
});
```

报价返回 `shippingOptions: []` 表示该地址无法配送；在更新里加上 `error: "该地区暂不支持配送"`，用户就能看到原因。

## 另请参阅

- [Digital Goods API](/zh/reference/capabilities/digital-goods/)，在 Trusted Web Activity 内接入 `PaymentRequest` 的 Play Billing 支付方式
- [Trusted Web Activity（TWA）](/zh/reference/installation/twa/)
- [为 PWA 创造收入](/zh/guides/monetization/)
- [Payment Request API: show() method](https://www.w3.org/TR/payment-request/#show-method)（w3.org）
- [Payment Request API: constructor](https://www.w3.org/TR/payment-request/#constructor)（w3.org）
- [Introducing the Payment Request API for Apple Pay](https://webkit.org/blog/8182/introducing-the-payment-request-api-for-apple-pay/)（webkit.org）
- [How the Payment Request API works](https://web.dev/articles/how-payment-request-api-works)（web.dev）
- [Chromium payments error strings](https://chromium.googlesource.com/chromium/src/+/main/components/payments/core/error_strings.cc)（chromium.googlesource.com）