How to adapt your web-based payment app to Web Payments and provide a better user experience for customers.


![Eiji Kitamura](https://web.dev/images/authors/agektmr.jpg) Eiji Kitamura [X](https://twitter.com/agektmr) [GitHub](https://github.com/agektmr) [Mastodon](https://infosec.exchange/@agektmr) [Bluesky](https://bsky.app/profile/agektmr.com)

<br />

Once [the payment app is registered](https://web.dev/articles/registering-a-web-based-payment-app), you
are ready to accept payment requests from merchants. This post explains how to
orchestrate a payment transaction from a service worker during runtime (i.e.
when a window is displayed and the user is interacting with it).
![Orchestrating payment transactions with a service worker](https://web.dev/static/articles/orchestrating-payment-transactions/image/orchestrating-payment-tra-dd17687ea595a.png) Orchestrating payment transactions with a service worker

"Runtime payment parameter changes" refer to a set of events that allows the
merchant and payment handler to exchange messages while the user is interacting
with the payment handler. Learn more in [Handling optional payment information
with a service worker](https://web.dev/articles/handling-optional-payment-information).

## Receive a payment request event from the merchant

When a customer chooses to pay with your web-based payment app and [the merchant
invokes
`PaymentRequest.show()`](https://web.dev/articles/life-of-a-payment-transaction),
your service worker will receive a `paymentrequest` event. Add an event listener
to the service worker to capture the event and prepare for the next action.

\[payment handler\] service-worker.js:

    ...
    let payment_request_event;
    let resolver;
    let client;

    // `self` is the global object in service worker
    self.addEventListener('paymentrequest', async e => {
      if (payment_request_event) {
        // If there's an ongoing payment transaction, reject it.
        resolver.reject();
      }
      // Preserve the event for future use
      payment_request_event = e;
    ...

> [!CAUTION]
> **Caution:** Remember that a service worker is a single instance across different tabs and windows in the same browser. Global variables are shared across all tabs and windows.

The preserved `PaymentRequestEvent` contains important information about this
transaction:

| Property name | Description |
|---|---|
| `topOrigin` | A string that indicates the origin of the top-level web page (usually the payee merchant). Use this to identify the merchant origin. |
| `paymentRequestOrigin` | A string that indicates the origin of the invoker. This can be the same as `topOrigin` when the merchant invokes the Payment Request API directly, but may be different if the API is invoked from within an iframe by a third party such as a payment gateway. |
| `paymentRequestId` | The `id` property of the `https://w3c.github.io/payment-request#paymentdetailsinit-dictionary` provided to the Payment Request API. If the merchant omits, the browser will provide an auto-generated ID. |
| `methodData` | The payment-method-specific data provided by the merchant as part of `https://w3c.github.io/payment-request#dom-paymentmethoddata`. Use this to determine the payment transaction details. |
| `total` | The total amount provided by the merchant as part of `https://w3c.github.io/payment-request#dom-paymentdetailsinit`. Use this to construct a UI to let the customer know the total amount to pay. |
| `instrumentKey` | The instrument key selected by the user. This reflects the `https://w3c.github.io/payment-handler#paymentinstruments-interface` you provided in advance. An empty string indicates that the user did not specify any instruments. |

## Open the payment handler window to display the web-based payment app frontend

When a `paymentrequest` event is received, the payment app can open a payment
handler window by calling `PaymentRequestEvent.openWindow()`. The payment
handler window will present the customers your payment app's interface where
they can authenticate, choose shipping address and options, and authorize the
payment. We'll cover how to write the frontend code in *Handling payments on the
payment frontend* (coming soon).
Checkout flow with a web-based payment app.

Pass a preserved promise to `PaymentRequestEvent.respondWith()` so that you can
resolve it with a payment result in the future.

\[payment handler\] service-worker.js:

    ...
    self.addEventListener('paymentrequest', async e => {
    ...
      // Retain a promise for future resolution
      // Polyfill for PromiseResolver is provided below.
      resolver = new PromiseResolver();

      // Pass a promise that resolves when payment is done.
      e.respondWith(resolver.promise);
      // Open the checkout page.
      try {
        // Open the window and preserve the client
        client = await e.openWindow(checkoutURL);
        if (!client) {
          // Reject if the window fails to open
          throw 'Failed to open window';
        }
      } catch (err) {
        // Reject the promise on failure
        resolver.reject(err);
      };
    });
    ...

You can use a convenient `PromiseResolver` polyfill to resolve a promise at
arbitrary timing.

    class PromiseResolver {
      constructor() {
        this.promise_ = new Promise((resolve, reject) => {
          this.resolve_ = resolve;
          this.reject_ = reject;
        })
      }
      get promise() { return this.promise_ }
      get resolve() { return this.resolve_ }
      get reject() { return this.reject_ }
    }

> [!NOTE]
> **Note:** For security reasons, the frontend page opened in the payment handler window must have valid HTTPS certificates and no [mixed content](https://web.dev/articles/what-is-mixed-content); otherwise the payment request will be cancelled by Chrome. Learn more at [Debugging a web-based payment app](https://web.dev/articles/registering-a-web-based-payment-app#debugging_a_web-based_payment_app).

## Exchange information with the frontend

The payment app's service worker can exchange messages with the payment app's
frontend through `ServiceWorkerController.postMessage()`. To receive messages
from the frontend, listen to `message` events.

\[payment handler\] service-worker.js:

    // Define a convenient `postMessage()` method
    const postMessage = (type, contents = {}) => {
      if (client) client.postMessage({ type, ...contents });
    }

### Receive the ready signal from the frontend

Once the payment handler window is opened, the service worker should wait for a
ready-state signal from the payment app frontend. The service worker can pass
important information to the frontend when it is ready.

\[payment handler\] frontend:

    navigator.serviceWorker.controller.postMessage({
      type: 'WINDOW_IS_READY'
    });

\[payment handler\] service-worker.js:

    ...
    // Received a message from the frontend
    self.addEventListener('message', async e => {
      let details;
      try {
        switch (e.data.type) {
          // `WINDOW_IS_READY` is a frontend's ready state signal
          case 'WINDOW_IS_READY':
            const { total } = payment_request_event;
    ...

### Pass the transaction details to the frontend

Now send the payment details back. In this case you're only sending the total of
the payment request, but you can pass more details if you like.

\[payment handler\] service-worker.js:

    ...
            // Pass the payment details to the frontend
            postMessage('PAYMENT_IS_READY', { total });
            break;
    ...

\[payment handler\] frontend:

    let total;

    navigator.serviceWorker.addEventListener('message', async e => {
      switch (e.data.type) {
          case 'PAYMENT_IS_READY':
            ({ total } = e.data);
            // Update the UI
            renderHTML(total);
            break;
    ...

## Return the customer's payment credentials

When the customer authorizes the payment, the frontend can send a post message
to the service worker to proceed. You can resolve the promise passed to
`PaymentRequestEvent.respondWith()` to send the result back to the merchant.
Pass a
[`PaymentHandlerResponse`](https://w3c.github.io/payment-handler/#dom-paymenthandlerresponse)
object.

| Property name | Description |
|---|---|
| `methodName` | The payment method identifier used to make payment. |
| `details` | The payment method specific data that provides necessary information for the merchant to process payment. |

\[payment handler\] frontend:

      const paymentMethod = ...

      postMessage('PAYMENT_AUTHORIZED', {
        paymentMethod,              // Payment method identifier
      });

\[payment handler\] service-worker.js:

    ...
    // Received a message from the frontend
    self.addEventListener('message', async e => {
      let details;
      try {
        switch (e.data.type) {
          ...
          case 'PAYMENT_AUTHORIZED':
            // Resolve the payment request event promise
            // with a payment response object
            const response = {
              methodName: e.data.paymentMethod,
              details: { id: 'put payment credential here' },
            }
            resolver.resolve(response);
            // Don't forget to initialize.
            payment_request_event = null;
            break;
          ...

## Cancel the payment transaction

To allow the customer to cancel the transaction, the frontend can send a post
message to the service worker to do so. The service worker can then resolve the
promise passed to `PaymentRequestEvent.respondWith()` with `null` to indicate to
the merchant that the transaction has been cancelled.

\[payment handler\] frontend:

      postMessage('CANCEL_PAYMENT');

\[payment handler\] service-worker.js:

    ...
    // Received a message from the frontend
    self.addEventListener('message', async e => {
      let details;
      try {
        switch (e.data.type) {
          ...
          case 'CANCEL_PAYMENT':
            // Resolve the payment request event promise
            // with null
            resolver.resolve(null);
            // Don't forget to initialize.
            payment_request_event = null;
            break;
          ...

## Sample code

All sample codes you saw in this document were excerpts from the following:
[Web-based Payment Handler
Demo](https://github.com/GoogleChromeLabs/web-identity-demos/tree/main/paymenthandler-demo)

## Next steps

In this article, we learned how to orchestrate a payment transaction from a
service worker. The next step is to learn how to add some more advanced features
to the service worker.

- [Handling optional payment information with a service worker](https://web.dev/articles/handling-optional-payment-information)