Learn Measure Blog Live About

Orchestrating payment transactions with a service worker

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.

Appears in: Web Payments

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;

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 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.
paymentOptions The PaymentOptions object provided to the Payment Request API by the merchant. Indicates whether the merchant is requesting shipping with requestShipping, the type of shipping with shippingType, billing address with requestBillingAddress, payer's email, name, phone respectively with requestPayerEmail, requestPayerName, requestPayerPhone. Use this to determine what information to include in the PaymentHandlerResponse on a payment authorization.
shippingOptions The shippingOptions property of the PaymentDetailsUpdate provided to the Payment Request API. Use this to construct a UI to let the customer select a shipping option.

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);
};
});

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_ }
}

For security reasons, the frontend page opened in the payment handler window must have valid HTTPS certificates and no mixed content; otherwise the payment request will be cancelled by Chrome. Learn more at 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, paymentOptions, shippingOptions } = 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, paymentOptions, shippingOptions
});
break;

[payment handler] frontend:

let paymentOptions;
let total;
let shippingOptions;

navigator.serviceWorker.addEventListener('message', async e => {
switch (e.data.type) {
case 'PAYMENT_IS_READY':
({ total, paymentOptions, shippingOptions } = e.data);
// Update the UI
renderHTML(total, paymentOptions, shippingOptions);
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. If PaymentRequestEvent.paymentOptions.requestBillingAddress === true, append the billing address as a PaymentAddress object as part of this.
payerName If PaymentRequestEvent.paymentOptions.requestPayerName === true, provide the payer's name.
payerEmail If PaymentRequestEvent.paymentOptions.requestPayerEmail === true, provide the payer's email address.
payerPhone If PaymentRequestEvent.paymentOptions.requestPayerPhone === true, provide the payer's phone number.
shippingAddress If PaymentRequestEvent.paymentOptions.requestShipping === true, provide the customer's shipping address with a PaymentAddress object.
shippingOption If PaymentRequestEvent.paymentOptions.requestShipping, provide the identifier of the customer's selected shipping option.

[payment handler] frontend:

  const paymentMethod =
const shippingOptionId =
const shippingAddress =
const contacts =

postMessage('PAYMENT_AUTHORIZED', {
paymentMethod, // Payment method identifier
shippingOptionId, // Shipping option id
shippingAddress, // shipping address object
payerName: contacts.name, // Payer name
payerPhone: contacts.phone, // Payer Phone
payerEmail: contacts.email, // Payer Email
});

[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' },
}
let { paymentOptions } = payment_request_event;
if (paymentOptions.requestBillingAddress) {
response.details.billingAddress = e.data.methodData.billingAddress;
}
if (paymentOptions.requestShipping) {
response.shippingAddress = e.data.shippingAddress;
response.shippingOption = e.data.shippingOptionId;
}
if (paymentOptions.requestPayerEmail) {
response.payerEmail = e.data.payerEmail;
}
if (paymentOptions.requestPayerName) {
response.payerName = e.data.payerName;
}
if (paymentOptions.requestPayerPhone) {
response.payerPhone = e.data.payerPhone;
}
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.

Last updated: Improve article