v154 · payments · secure payment confirmation

When your locale and the dialog disagree

Secure Payment Confirmation shows a browser dialog carrying your merchant name, instrument and amount. You supply that text; the browser supplies the frame around it. If you say the text is French and the dialog is in English, the result is a sentence in two languages — so Chrome 154 refuses instead, with a NotSupportedError.

concepts

  1. Building the request

    Every field of a real secure-payment-confirmation request, constructed live. Including the one that surprises everybody: the data is JSON-serialised, so a getter in there is a TypeError before anything else is checked.

  2. Which of your tags would match

    Canonicalise a list of language tags, find out which ones this browser has data for, and see which one a best-fit match resolves to — all through Intl, which is the same machinery the question is really about.

  3. A checkout that survives the refusal

    The practical shape: check what you can before showing anything, and have somewhere to go when the answer is no. A refusal at the dialog is a refusal in front of the customer.

why it shipped

The dialog is a mixture: the browser's own words around the merchant's. The locale field is how a caller says which language its half is in, so the browser can pick a matching frame. When it cannot — because the browser has no dialog in that language — the previous behaviour was to show the dialog anyway, in whatever language it had, wrapped around text in another.

That is worse than an error. A payment confirmation is exactly the moment a user should not be confused about what they are agreeing to, and a half-translated one invites the reaction the flow is designed to prevent. Refusing gives the caller a chance to supply text the browser can actually frame.

the API

const request = new PaymentRequest([{
  supportedMethods: "secure-payment-confirmation",
  data: {
    rpId: "example.com",
    credentialIds: [credentialId],
    challenge,
    instrument: { displayName: "Visa ····4242", icon: "/card.png" },
    payeeName: "Example Shop",
    locale: ["fr-CA", "fr", "en"],   // ordered by preference
    timeout: 60_000,
  },
}], details);

// The locale is not validated here — it is validated when the dialog opens.
await request.show();   // NotSupportedError if none of them match

enabling it now

Measured on the Chrome 150 used to build these pages: an SPC request constructs successfully with any locale value, including ["zz"] and [] — because the field is not checked at construction. canMakePayment() resolves false here, as it does on any machine with no matching credential, so the dialog is never reached and the refusal cannot be provoked.

new PaymentRequest([{ supportedMethods: "secure-payment-confirmation",
  data: { …, locale: ["zz"] } }], details)   // constructs fine
await request.canMakePayment()                // false — no credential here

So these pages exercise everything up to the dialog and say plainly where they stop. The locale matching itself is demonstrated through Intl, which answers the same question with the same data the browser has.

references