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).
"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).
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:
- Go to https://paymentrequest-demo.glitch.me/.
- Go to the bottom of the page.
- Press Add a payment button.
- Enter
https://paymenthandler-demo.glitch.me
to the Payment Method Identifier field. - 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.