Orchestrating payment transactions with a service worker

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

Once the payment app is registered, 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
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.

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(), 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;
…

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 PaymentDetailsInit 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 PaymentMethodData. Use this to determine the payment transaction details.
total The total amount provided by the merchant as part of 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 instrumentKey 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_ }
}

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 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 working sample app:

https://paymenthandler-demo.glitch.me

[payment handler] service worker

[payment handler] frontend

To try it out:

  1. Go to https://paymentrequest-demo.glitch.me/.
  2. Go to the bottom of the page.
  3. Press Add a payment button.
  4. Enter https://paymenthandler-demo.glitch.me to the Payment Method Identifier field.
  5. Press Pay button next to the field.

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.