Embedded payments (Web Payment Module)

Embedded payments are a form of external payment that allows a third-party payment user interface (UI) to be rendered from within a client application.

Scripts for rendering an embedded payment UI are supplied by the external payment provider via Web Payment Module. Vista's systems do not receive the full card details from the embedded payment UI, which is handled by the external payment provider, removing related security concerns.

Note:

  • Only a single embedded payment can be made against an order.
  • An embedded payment can be combined with a member balance payment and/or one or more gift card payments.
  • An embedded payment cannot be combined with a redirect payment .
  • An embedded payment can be created before other payments have been added, but must be completed as the final stage of an order, after any other payments have been added.
  • The order will be automatically completed after the embedded payment is completed.
NOTE

For access to React-based embedded payment UI reference code, talk to your local Vista representative.

Embedded payment stages

The embedded payment API flow includes a combination of Digital Platform, JavaScript, and Web Payment Module interaction.

Create embedded payment
Load embedded payment UI scripts
Render embedded payment UI
Patron completes or cancels payment
Send token to Web Payment Module
Confirm payment completion
Confirm order completion

Create embedded payment

An embedded payment can be initiated for an in-progress order by calling the CreateEmbeddedPayment endpoint with the webPaymentMethodId of the configured embedded payment method.

Copy
Copied
{
  "webPaymentMethodId": 2,
  "redirectReturnUrl": "https://example.com/order/payment",
  "languageTag": null
}

Note:

  • The order must have a non-zero remaining balance to pay. For orders with zero remaining balance to pay, use the CreateOrderCompletion endpoint to complete the order directly.
  • For standard and gift shop orders, the customer's details must be set prior to creating an embedded payment.
  • If the webPaymentMethodId is unknown, use the GetWebPaymentMethods endpoint to load the list of available payment methods for the order. See Retrieving configured payment methods for more info.
  • Set the redirectReturnUrl to the client application's payment page (i.e., window.location.href ) so that the payment connector will return to the application after redirecting for Strong Customer Authentication (e.g., 3DS), if required.
  • For multilingual applications, set the languageTag to the patron's preferred language, e.g., "en-GB" .
  • If a gift card payment or member balance payment is made after an embedded payment is created, the embedded payment must be re-created, and the embedded payment UI must be re-rendered.

A clientData object will be returned containing the details required to load and render the embedded payment UI:

Copy
Copied
{
  "embeddedPayment": {
    "id": "string",
    "type": "Embedded",
    "value": 0.1,
    "status": "Pending"
  },
  "clientData": {
    "connectorParametersJson": "string",
    "completePaymentUrl": "https://example.com",
    "sdkScriptUrls": ["string"],
    "connectorScriptUrls": ["string"],
    "connectorFunctionName": "string",
    "isPaymentButtonIncluded": true
  },
  "updatedOrderExpiresAt": "2019-08-24T14:15:22Z"
}
NOTE

If your application automatically cancels expired orders, the updatedOrderExpiresAt value should be used to update the order's expiry time. Your application should also track that an embedded payment has been created to avoid cancelling the order prematurely.

Load embedded payment UI scripts

The client application must load the embedded payment scripts using the sdkScriptUrls and connectorScriptUrls values returned in the clientData object.

You may load these scripts by adding <script> tags to the <head> of the HTML document, for example:

Copy
Copied
/**
 * Loads the scripts required to render an embedded payment UI.
 *
 * @param clientData The `clientData` object returned by the CreateEmbeddedPayment endpoint.
 * @returns A promise that resolves when all scripts have been loaded
 */
async function loadEmbeddedPaymentScripts(clientData) {
  const scriptUrls = [
    ...clientData.sdkScriptUrls,
    ...clientData.connectorScriptUrls,
  ];

  return await Promise.all(
    scriptUrls.map((scriptUrl) => loadScript(scriptUrl)),
  );
}

/**
 * Cache each loadScript promise to ensure that scripts aren't unnecessarily re-loaded.
 */
const loadScriptPromiseCache = new Map();

/**
 * Load a script into the DOM via a promise.
 *
 * @param url The URL of the script to load.
 * @returns A promise that resolves when the script has been loaded.
 */
async function loadScript(url) {
  // If the script is currently loading, return the cached promise.
  const cachedPromise = loadScriptPromiseCache.get(url);
  if (cachedPromise) {
    return cachedPromise;
  }

  // Skip if the script has already been loaded.
  if (document.querySelector(`script[src="${url}"]`)) {
    return;
  }

  const head = document.head || document.getElementsByTagName('head')[0];
  const script = document.createElement('script');

  script.src = url;
  script.type = 'text/javascript';

  const promise = new Promise((resolve, reject) => {
    script.onload = () => resolve();
    script.onerror = () => reject();
  });

  head.appendChild(script);
  loadScriptPromiseCache.set(url, promise);

  promise.finally(() => {
    loadScriptPromiseCache.delete(url);
  });

  return promise;
}
IMPORTANT

The client application must await the loadEmbeddedPaymentScripts function before rendering the embedded payment UI to ensure that all required scripts have been loaded.

Render embedded payment UI

After all payment scripts have been loaded, use the clientData object returned by the CreateEmbeddedPayment endpoint to render the embedded payment UI in the client application.

First, create a container element in the client application where the embedded payment UI will be rendered:

Copy
Copied
<html>
  <body>
    <div id="embedded-payment"></div>
  </body>
</html>

Then, render the embedded payment UI using the clientData and a reference to the container element:

Copy
Copied
/**
 * Renders the UI for an embedded payment.
 *
 * @param clientData The `clientData` object returned by the CreateEmbeddedPayment endpoint.
 * @param element A reference to the DOM element that the payment UI should be rendered into.
 * @param callbacks Callbacks for handling events from the payment UI.
 * @returns A reference to the rendered payment UI with `unmount()` and `startPayment()` methods.
 */
function renderEmbeddedPaymentUI(clientData, element, callbacks) {
  const serverParams = JSON.parse(clientData.connectorParametersJson);

  const clientParams = {
    // A reference to the DOM element for the payment UI.
    container: element,
    // Callbacks for handling events from the payment UI.
    onPaymentStarted: () => callbacks.onPaymentStarted(),
    onPaymentTokenReceived: (token) =>
      callbacks.onPaymentTokenReceived(clientData.completePaymentUrl, token),
    onPaymentMethodDeclined: (errorMessage) =>
      callbacks.onPaymentMethodDeclined(errorMessage),
    onPaymentButtonStateChanged: ({ isDisabled }) =>
      callbacks.onPaymentButtonStateChanged({ isDisabled }),
    onError: (errorMessage) => callbacks.onError(errorMessage),
  };

  // Return a reference to the rendered payment UI.
  // Use the `unmount()` method to teardown the UI.
  // Use the `startPayment()` method to start when using a custom payment button.
  return window.vista.payments.connector[clientData.connectorFunctionName](
    serverParams,
    clientParams,
  );
}

const embeddedPaymentUI = renderEmbeddedPaymentUI(
  clientData,
  document.getElementById('embedded-payment'),
  {
    onPaymentStarted: () => undefined, // store 'payment started' state
    onPaymentTokenReceived: postPaymentTokenToWPM, // POST the payment token to Web Payment Module
    onPaymentMethodDeclined: (errorMessage) => undefined, // handle payment cancelled or declined
    onPaymentButtonStateChanged: ({ isDisabled }) => undefined, // update custom payment button state
    onError: (error) => undefined, // handle unexpected error
  },
);

Custom payment button

Some payment connectors do not include a 'pay now' button, as indicated by the clientData.isPaymentButtonIncluded property returned by the CreateEmbeddedPayment endpoint.

If the payment connector doesn't include a 'pay now' button, the client application should render a custom button that calls the startPayment() method of the payment UI when clicked:

Copy
Copied
<html>
  <body>
    <div id="embedded-payment"></div>
    <button onclick="onPaymentButtonClick()">Pay now</button>

    <script>
      // Render the embedded payment UI.
      const embeddedPaymentUI = renderEmbeddedPaymentUI(...);

      // Start the embedded payment when the custom button is clicked.
      function onPaymentButtonClick() {
        embeddedPaymentUI.startPayment();
      }
    </script>
  </body>
</html>

Callbacks

The callbacks object passed to the renderEmbeddedPaymentUI function includes several callback methods for handling events from the embedded payment UI:

Callback method Description Expected action
onPaymentStarted The payment has been started by the patron. Track that the embedded payment has been started.
onPaymentTokenReceived The payment token has been created successfully. Send token to Web Payment Module.
onPaymentMethodDeclined The payment was cancelled or declined by the connector. Display an appropriate message with a 'try again' button that restarts the payment.
onPaymentButtonStateChanged The payment button state has changed. Update the disabled state of the custom payment button based on the isDisabled prop.
onError An unexpected error has occurred. Display an appropriate error message or restart the payment.
IMPORTANT

The client application should track that an embedded payment has been started when the onPaymentStarted callback is called to confirm payment completion after redirection for Strong Customer Authentication (e.g., 3DS).

Teardown

The embedded payment UI can be torn down by calling the unmount() method of the returned payment UI:

Copy
Copied
embeddedPaymentUI.unmount();

The embedded payment UI should be torn down when:

  • The payment has been completed successfully.
  • The payment has been declined by the connector.
  • An unexpected error has occurred.

Patron completes or cancels payment

The rendered embedded payment UI will allow the patron to complete or cancel the payment.

The onPaymentTokenReceived callback will be called when the patron successfully creates a payment token. The client application must send the token to Web Payment Module to complete the payment.

The onPaymentMethodDeclined callback will be called if the payment is cancelled by the patron or declined by the connector. The client application should display an appropriate message with a 'try again' button that restarts the payment.

Send token to Web Payment Module

Once the onPaymentTokenReceived callback is called, the client application must POST the payment token to Web Payment Module to complete the payment:

Copy
Copied
/**
 * Posts a payment token to Web Payment Module (WPM) to complete an embedded payment.
 *
 * @param completePaymentUrl The `clientData.completePaymentUrl` returned by the CreateEmbeddedPayment endpoint.
 * @param token The token received from the payment UI.
 */
async function postPaymentTokenToWPM(completePaymentUrl, token) {
  try {
    const response = await fetch(completePaymentUrl, {
      method: 'POST',
      body: token,
      headers: new Headers({
        'Content-Type': 'application/x-www-form-urlencoded',
      }),
    });

    // Redirect required for Strong Customer Authentication (e.g., 3DS).
    if (response.status === 302) {
      const text = await response.text();
      document.write(text);
    } else {
      // Token has been sent to WPM, move on to next step.
    }
  } catch {
    throw new Error(
      'Unable to POST embedded payment token to completePaymentUrl.',
    );
  }
}
IMPORTANT

Web Payment Module may return a 302 status code to indicate that a redirect is required for Strong Customer Authentication (e.g., 3DS). The client application must handle this by overwriting the current HTML document with the response body from Web Payment Module to initiate the redirection.

Once Strong Customer Authentication has been completed, the patron will be redirected back to the payment page (i.e., redirectReturnUrl) to confirm payment completion.

Confirm payment completion

The payment will be automatically completed after the token has been sent to WPM and Strong Customer Authentication has been completed (if required).

As payment completion occurs on a background process, poll the GetCurrentEmbeddedPayment endpoint with an exponential backoff between requests until it no longer returns a "Pending" status.

Copy
Copied
{
  "id": "string",
  "type": "Embedded",
  "value": 0.1,
  "status": "Completed"
}

If a "Completed" status is returned, the payment has been completed successfully and the order should be automatically completed.

If a "Declined" status is returned, the payment was declined by the connector. See payment errors for more info.

Confirm order completion

The order will be automatically completed after the embedded payment has been successfully completed.

NOTE

Order completion does not need to be manually requested by the client application when an embedded payment has been completed.

As order completion occurs on a background process, poll the GetOrderCompletion endpoint with an exponential backoff between requests until it no longer returns a "Pending" status.

Copy
Copied
{
  "orderId": "string",
  "orderType": "Standard",
  "status": "Completed",
  "transactionNumber": 0,
  "updatedOrderExpiresAt": null
}

If a "Completed" status is returned, the order has been completed successfully. The client application may now use the GetCompletedOrder endpoint to retrieve the details of the completed order.

If a "Failed" status is returned, a post-payment failure has occurred. See order completion errors for more info.