# Client-sdk

It is a solution for collecting and handling payment sources in secure way.

With SDK you can create a payment form widget as an independent part or insert use inside your form.

The SDK supports methods for customization of widget by your needs (styling, form fields, etc)

## Other information 

To work with the widget you will need public_key or access_token ([see Authentication](https://docs.paydock.com/#authentication))

Also you will need added gateway ([see API Reference by gateway](https://docs.paydock.com/#gateways))



## Get started

The Client SDK ships in JavaScript ES6 (EcmaScript 2015) in three different
formats (CJS, ESM and UMD) along with respective TypeScript declarations. Below,
we exemplify how to import each format.

### Download from CDN

```html
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
<script>
    var widget = new paydock.HtmlWidget('#tag', 'publicKey', 'gatewayId');
</script>
```


For browser environments, you can import the Client SDK directly from our CDN to
your project's HTML. To accomplish this, include the Client SDK in your page
using one and only of the two script tags below. After this step you will be
able to access the Client SDK features via the global variable `paydock`.

For production we recommend using the compressed version (`.min.js`) since
it will result in faster loading times for your end users.

- *Compressed version for production: `https://widget.paydock.com/sdk/latest/widget.umd.min.js`*

- *Full version for development and debug: `https://widget.paydock.com/sdk/latest/widget.umd.js`*

You may download the production version of the Client SDK scripts [here][min],
and, the development version [here][max].

[min]: https://widget.paydock.com/sdk/latest/widget.umd.min.js
[max]: https://widget.paydock.com/sdk/latest/widget.umd.js

For more advanced use-cases, the library has [UMD](https://github.com/umdjs/umd)
format that can be used in RequireJS, Webpack, etc.


### With package manager

```cjs
// module import - CommonJS/Node projects ✅
const paydock = require('@paydock/client-sdk')
const api = new paydock.Api('publicKey');
```

```mjs
// named import - ESM projects or TypeScript projects ✅
import { HtmlWidget } from '@paydock/client-sdk'
const widget = new HtmlWidget('#selector', 'publicKey', 'gatewayId');
```

```mjs
// namespaced import - ESM projects or TypeScript projects ✅
import * as Paydock from '@paydock/client-sdk'
const widget = new Paydock.HtmlWidget('#selector', 'publicKey', 'gatewayId');
```

```js
// default import - Not officially supported . They are handled differently by different tools / settings!
 ❌
import paydock from '@paydock/client-sdk'
>>> "Uncaught SyntaxError: The requested module does not provide an export named 'default'"
```


Our NPM package is compatible with all package managers (e.g., `npm`, `yarn`,
`pnpm`, `bun`). Using `npm` the following command would add the Client SDK as a
production dependency.

```bash
npm install @paydock/client-sdk
```

After installation is complete, if you are developing in NodeJS environments or
using tools that expect your JavaScript code to be in CJS format (e.g., Jest,
Karma, RequireJS, Webpack), you can import the Client SDK using CommonJS modules.
For these environments the UMD format (`@paydock/client-sdk/bundles/widget.umd.js`)
can also be used as a fallback. Alternatively, in case you are developing in
projects that have access to modern bundlers such as Vite or others (e.g., SPA
libs or SSR Metaframeworks), you can import the Client SDK features using ESM
through named imports or namespaced imports.


## Widget

You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#widget-simple-example)

A payment form where it is possible to enter card data/bank accounts and then receive a one-time
token for charges, subscriptions etc. This form can be customized, you can customize the fields and set styles.
It is possible in real-time to monitor the actions of user with widget and get information about payment-source using events.

## Widget simple example

### Container

```html
<div id="widget"></div>
```

You must create a container for the widget. Inside this tag, the widget will be initialized

### Initialization

```javascript
var widget = new paydock.HtmlWidget('#widget', 'publicKey');
widget.load();
```

```javascript
// ES2015 | TypeScript

import { HtmlWidget } from '@paydock/client-sdk';

var widget = new HtmlWidget('#widget', 'publicKey');
widget.load();
```

Then write only need 2 lines of code in js to initialize widget

### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 100%;height: 300px;}</style>
</head>
<body>
    <form id="paymentForm">
        <div id="widget"></div>
        <input name="payment_source_token" id="payment_source_token" type="hidden">
    </form>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
    <script>
        var widget = new paydock.HtmlWidget('#widget', 'publicKey');
        widget.onFinishInsert('input[name="payment_source_token"]', 'payment_source');
        widget.load();
    </script>
</body>
</html>
```

## Widget advanced example

### Customization

```javascript
widget.setStyles({
  background_color: 'rgb(0, 0, 0)',
  border_color: 'yellow',
  text_color: '#FFFFAA',
  button_color: 'rgba(255, 255, 255, 0.9)',
  font_size: '20px'
 });
```

This example shows how you can customize to your needs and design

### Customization from html

```html
<div id="widget"
  widget-style="text-color: #FFFFAA; border-color: #yellow"
  title="Payment form"
  finish-text="Payment resource was successfully accepted"></div>
```

This example shows how you can set style and texts from html

### Settings

```javascript
widget.setRefId('id'); // your unique identifier to identify the data

widget.setFormFields(['phone', 'email']); // add additional fields for form of widget

widget.setSupportedCardIcons(['mastercard', 'visa']); // add icons of supported card types
```

This example shows how you can use a lot of other methods to settings your form

### Error handling

## Overview

Error events are emitted when an error occurs during widget operations. These events provide detailed information about the error, including its category, cause, and contextual details.

## Error Event Structure

### Base Properties

| Property | Type | Description |
|----------|------|-------------|
| `event` | `string` | Always set to `"error"` |
| `purpose` | `string` | Indicates the purpose of the action that triggered the error event (e.g., `"payment_source"`) |
| `message_source` | `string` | Source of the message (e.g., `"widget.paydock"`) |
| `ref_id` | `string` | Reference ID for the operation |
| `widget_id` | `string` | Unique identifier of the widget instance |
| `error` | `object` | Error object containing error information |

### Error Object Properties

The `error` object contains detailed information about the error:

| Property | Type | Description |
|----------|------|-------------|
| `category` | `string` | High-level error classification |
| `cause` | `string` | Specific error cause |
| `retryable` | `boolean` | Indicates if the operation can be retried |
| `details` | `object` | Additional error context |

## Error Categories

| Category | Description |
|----------|-------------|
| `configuration` | Configuration-related errors |
| `identity_access_management` | Authentication and authorization errors |
| `internal` | Internal system errors |
| `process` | Process and operation errors |
| `resource` | Resource-related errors |
| `validation` | Input validation errors |

## Error Causes

| Cause | Category | Description |
|-------|----------|-------------|
| `aborted` | `process` | Operation was aborted |
| `access_forbidden` | `identity` | Access to resource is forbidden |
| `already_exists` | `validation` | Resource already exists |
| `canceled` | `process` | Operation was canceled |
| `invalid_configuration` | `configuration` | Invalid widget configuration |
| `invalid_input` | `validation` | Invalid input provided |
| `not_found` | `resource` | Requested resource not found |
| `not_implemented` | `process` | Requested feature not implemented |
| `rate_limited` | `process` | Too many requests |
| `server_busy` | `process` | Server is too busy to handle request |
| `service_unreachable` | `process` | Unable to reach required service |
| `unauthorized` | `identity` | Authentication required |
| `unknown_error` | `internal` | Unexpected error occurred |
| `unprocessable_entity` | `validation` | Valid input but cannot be processed |

## Error Details Object

| Property | Type | Description |
|----------|------|-------------|
| `cause` | `string` | Matches the top-level error cause |
| `contextId` | `string` | Context identifier (usually matches widget_id) |
| `message` | `string` | Human-readable error message |
| `timestamp` | `string` | ISO 8601 timestamp of when the error occurred |

## Example

```javascript
widget.hideUiErrors(); // hide default UI errors and handle errors by listening to error events with widget.on('error')

widget.on('error', (error) => {
    console.log(error);
    // {
    //     "event": "error",
    //     "purpose": "payment_source",
    //     "message_source": "widget.paydock",
    //     "ref_id": "",
    //     "widget_id": "d4744f30-dcf5-168e-7f78-c8273a3401d4",
    //     "error": {
    //         "category": "process",
    //         "cause": "service_unreachable",
    //         "details": {
    //             "cause": "service_unreachable",
    //             "contextId": "d4744f30-dcf5-168e-7f78-c8273a3401d4",
    //             "message": "The service is not availabe",
    //             "timestamp": "2025-02-13T09:30:21.157Z"
    //         },
    //         "retryable": false
    //     }
    // }
});
```

## Handling Errors (Tips)

When handling errors, consider:

1. Check the `retryable` flag to determine if the operation can be retried
2. Use the `category` for high-level error handling logic
3. Use the `cause` for specific error handling cases
4. The `contextId` can be used for error tracking and debugging
5. The `timestamp` helps with error logging and debugging

### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Title</title>
 <style>iframe {border: 0;width: 100%;height: 400px;}</style>
</head>
<body>
<form id="paymentForm">
    <div id="widget"
        widget-style="text-color: #FFFFAA; border-color: #yellow"
        title="Payment form"
        finish-text="Payment resource was successfully accepted">
    </div>

    <div 
        id="error" 
        style="
            display: none;
            max-width: 600px;
            margin: 16px auto;
            padding: 16px 20px;
            border-radius: 8px;
            background-color: #FEF2F2;
            border: 1px solid #FEE2E2;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
            font-family: system-ui, -apple-system, sans-serif;
            color: #991B1B;
            line-height: 1.5;
            font-size: 14px;
        "
        title="error"
        >
        <div style="display: flex; align-items: flex-start; gap: 12px;">
            <div>
                <h4 style="margin: 0 0 4px 0; font-size: 14px; font-weight: 600;">Access Error</h4>
                <div id="error-message"></div>
            </div>
        </div>
    </div> 
</form>

<script src="https://widget.paydock.com/sdk/latest/widget.umd.js" ></script>
<script>
 var widget = new paydock.HtmlWidget('#widget', 'publicKey', 'gatewayId');

 widget.setSupportedCardIcons(['mastercard', 'visa']);
 widget.setFormFields(['phone', 'email']);
 widget.setRefId('custom-ref-id');
    widget.onFinishInsert('input[name="payment_source_token"]', 'payment_source');

    widget.on('error', ({ error }) => {
        document.getElementById('error-message').textContent = error.details.message;
        document.getElementById('error').style.display = 'block';
    });
 widget.load();
</script>

</body>
</html>
```

## Checkout button

You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#cb_CheckoutButton)
Zipmoney meta parameters description [here](https://www.npmjs.com/package/@paydock/client-sdk#izipmoneymeta)

This widget allows you to turn your button into a full Checkout Button.
As a result, you will be able to receive a one-time token for charges, subscriptions etc. And other data given to the user by the payment gateway.



## Checkout button simple example

### Container

```html
<button type="button" id="button">
    checkout
</button>
```

You must create a button to turn it into checkout-button


### Initialization
```javascript
var button = new paydock.ZipmoneyCheckoutButton('#button', 'publicKey', 'gatewayId');
```

```javascript
// ES2015 | TypeScript


var button = new ZipmoneyCheckoutButton('#button', 'publicKey');
```

Then write only need 1 line of code in js to initialize widget

### Full ZipMoney example

```html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Title</title>
</head>
<body>
<form id="paymentForm">
    <button type="button" id="button">
        <img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTVrrEYxDmq4WXv7hfHygKD9ltnOqv0K6soSAhmbKNllPNYWiLiJA" align="left" style="margin-right:7px;">
    </button>
</form>

<input type="text" name="pst" />

<script src="https://widget.paydock.com/sdk/latest/widget.umd.js" ></script>
<script>
	var button = new paydock.ZipmoneyCheckoutButton('#button', 'publicKey', 'gatewayId');
	button.onFinishInsert('input[name="pst"]', 'payment_source_token');
    button.setMeta("first_name": "Joshua",
       "tokenize": true,
       "last_name": "Wood",
       "email":"joshuawood@hotmail.com.au",
       "gender": "male",
       "charge": {
           "amount": "4",
           "currency":"AUD",
           "shipping_type": "delivery",
           "shipping_address": {
               "first_name": "Joshua",
               "last_name": "Wood",
               "line1": "Suite 660",
               "line2": "822 Ruiz Square",
               "country": "AU",
               "postcode": "3223",
               "city": "Sydney",
               "state": "LA"
           },
           "billing_address": {
               "first_name": "Joshua",
               "last_name": "Wood",
               "line1": "Suite 660",
               "line2": "test",
               "country": "AU",
               "postcode": "3223",
               "city": "Sydney",
               "state": "LA"
           },
           "items": [
               {
                   "name":"ACME Toolbox",
                   "amount":"2",
                   "quantity": 1,
                   "reference":"Fuga consequuntur sint ab magnam"
               },
               {
                   "name":"Device 42",
                   "amount":"2",
                   "quantity": 1,
                   "reference":"Fuga consequuntur sint ab magnam"
               }
           ]
       },

       "statistics": {
           "account_created": "2017-05-05",
           "sales_total_number": "5",
           "sales_total_amount": "4",
           "sales_avg_value": "45",
           "sales_max_value": "400",
           "refunds_total_amount": "21",
           "previous_chargeback": "true",
           "currency": "AUD",
           "last_login": "2017-06-01"
       });

    button.on('finish', function (data) {
           console.log('on:finish', data);
    });
</script>
</body>
</html>
```

## Api
You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#api)

This wrapper helps you to work with paydock api emdpoints

### Get browser details
```javascript
var browserDetails = await new paydock.Api('publicKey').setEnv('env').getBrowserDetails();
```

```javascript
// ES2015 | TypeScript

import { Api } from '@paydock/client-sdk';

var browserDetails = await new paydock.Api('publicKey').setEnv('env').getBrowserDetails();
```

### Initialization
```javascript
var response = await new paydock.Api('publicKey').setEnv('env').charge().preAuth({
      amount: 100,
      currency: 'AUD',
      token: 'token',
    });
```

```javascript
// ES2015 | TypeScript

import { Api } from '@paydock/client-sdk';

var response = await new Api('publicKey').setEnv('env').charge().preAuth({
      amount: 100,
      currency: 'AUD',
      token: 'token',
    });
```

Then write only need 2 lines of code in js to make request

### Initialization full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style></style>
</head>
<body>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
    <script>
         (async function() {
            var response = await new Api('publicKey').setEnv('env').charge().preAuth({
                amount: 100,
                currency: 'AUD',
                token: 'token',
            });
        })();
    </script>
</body>
</html>
```

## Canvas3ds
You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#canvas3d)

This widget provides you to integrate 3d Secure

## Canvas3ds simple example

### Container

```html
<div id="widget"></div>
```

You must create a container for the widget. Inside this tag, the widget will be initialized


### Initialization
```javascript
var canvas3ds = new paydock.Canvas3ds('#widget', 'token');
canvas3ds.load();
```

```javascript
// ES2015 | TypeScript

import { Canvas3ds } from '@paydock/client-sdk';

var list = new Canvas3ds('#widget', 'token');
list.load();
```

Then write only need 2 lines of code in js to initialize widget


### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 40%;height: 300px;}</style>
</head>
<body>
    <div id="widget"></div>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
    <script>
        var canvas3ds = new paydock.Canvas3ds('#widget', 'token');
        canvas3ds.load();
    </script>
</body>
</html>
```


## Canvas3ds advanced example

### Settings

```javascript

canvas3ds.setEnv('sandbox'); // set enviroment

canvas3ds.hide(); // hide widget

canvas3ds.show(); // show widget

canvas3ds.on('chargeAuthSuccess', function (data) {
  console.log(data);
});
canvas3ds.on('chargeAuthReject', function (data) {
  console.log(data);
});
canvas3ds.on('chargeAuthCancelled', function (data) {
  console.log(data);
});
canvas3ds.on('additionalDataCollectSuccess', function (data) {
  console.log(data);
});
canvas3ds.on('additionalDataCollectReject', function (data) {
  console.log(data);
});
canvas3ds.on('chargeAuth', function (data) {
  console.log(data);
});
```

This example shows how you can use a lot of other methods to settings your form

### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 40%;height: 450px;}</style>
</head>
<body>
    <div id="widget3ds"></div>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
    <script>
        var canvas3ds = new paydock.Canvas3ds('#widget3ds', 'token');
        canvas3ds.on('chargeAuthSuccess', function (data) {
            console.log('chargeAuthSuccess', data);
        });
        canvas3ds.on('chargeAuthReject', function (data) {
            console.log('chargeAuthReject', data);
        });
        canvas3ds.load();
    </script>
</body>
</html>
```

### Full example with pre authorization

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 40%;height: 450px;}</style>
</head>
<body>
    <div id="widget"></div>
    <div id="widget3ds"></div>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
    <script>
        (async function () {
            var htmlWidget = new paydock.HtmlWidget('#widget', 'publicKey', 'gatewayId');
            htmlWidget.load();
            var {payment_source} = await htmlWidget.on('finish');
            var preAuthResp = await new paydock.Api('publicKey').setEnv('sandbox').charge().preAuth({
                  amount: 100,
                  currency: 'AUD',
                  token: payment_source,
                });
            var canvas = new paydock.Canvas3ds('#widget3ds', preAuthResp._3ds.token);
            canvas.load();
            var chargeAuthEvent = await canvas.on('chargeAuth');
            console.log('chargeAuthEvent', chargeAuthEvent);
        })()
    </script>
</body>
</html>
```

## Canvas 3ds for Standalone 3ds charges

After you initialized the standalone 3ds charge via `v1/charges/standalone-3ds` API endpoint, you get a token used to initialize the Canvas3ds. All above information regarding setup, loading and initialization still apply.

### Full example

```html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Title</title>
        <style>
            iframe {
                border: 0;
                width: 40%;
                height: 450px;
            }
        </style>
    </head>
    <body>
        <div id="widget3ds"></div>
        <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
        <script>
            var canvas3ds = new paydock.Canvas3ds("#widget3ds", "token");
            canvas3ds.on("chargeAuthSuccess", function (data) {
                console.log(data);
            });
            canvas3ds.on("chargeAuthReject", function (data) {
                console.log(data);
            });
            canvas3ds.on("chargeAuthChallenge", function (data) {
                console.log(data);
            });
            canvas3ds.on("chargeAuthDecoupled", function (data) {
                console.log(data.result.description);
            });
            canvas3ds.on("chargeAuthInfo", function (data) {
                console.log(data.info);
            });
            canvas3ds.on("error", function ({ charge_3ds_id, error }) {
                console.log(error);
            });
            canvas3ds.load();
        </script>
    </body>
</html>
```

- The `chargeAuthSuccess` event is executed both for frictionless flow, or for challenge flow after the customer has correctly authenticated with the bank using whatever challenge imposed.
- The `chargeAuthReject` event is executed when the authorization was rejected or when a timeout was received by the underlying system:
  - A `data.status` of `AuthTimedOut` will be received for timeouts.
  - A `data.status` of `rejected` will be received when the authorization was rejected.
  - A `data.status` of `invalid_event` will be received for unhandled situations.
- The `chargeAuthChallenge` event is sent before starting a challenge flow (i.e. opening an IFrame for the customer to complete a challenge with ther bank). Once the end customer performs the challenge, the Canvas3ds will be able to identify the challenge result and will either produce a `chargeAuthSuccess` or `chargeAuthReject` event.
- The `chargeAuthDecoupled` event is sent when the flow is a decoupled challenge, alongside a `data.result.description` field that you must show to the customer, indicating the method the user must use to authenticate. For example this may happen by having the cardholder authenticating directly with their banking app through biometrics. Once the end customer does this, the Canvas3ds will be able to recognize the challenge result is ready and will either produce a `chargeAuthSuccess` or `chargeAuthReject` event.
- The `error` event is sent if an unexpected issue with the client library occurs. In such scenarios, you should consider the autentication process as interrupted:
  - When getting this event, you will get on `data.error` the full error object.

### Events and Values

| Event Value         | Type                | Description                                                    |
| ------------------- | ------------------- | -------------------------------------------------------------- |
| <code>chargeAuthSuccess</code> | <code>object</code> | Instance of [ChargeEventResponse](#cb_chargeEventResponse) |
| <code>chargeAuthReject</code> | <code>object</code> | Instance of [ChargeEventResponse](#cb_chargeEventResponse) |
| <code>chargeAuthChallenge</code> | <code>object</code> | Instance of [ChargeEventResponse](#cb_chargeEventResponse) |
| <code>chargeAuthDecoupled</code> | <code>object</code> | Instance of [ChargeEventResponse](#cb_chargeEventResponse) |
| <code>chargeAuthInfo</code> | <code>object</code> | Instance of [ChargeEventResponse](#cb_chargeEventResponse) |
| <code>error</code> | <code>object</code> | Instance of [chargeError](#cb_chargeError) |

## Response Values

<a name="cb_chargeEventResponse" id="cb_chargeEventResponse"></a>

### ChargeEventResponse

| Param                           | Type                           | Description                                                                                        |
| ------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------- |
| <code>status</code> | <code>string</code> | status for the event transaction |
| <code>charge_3ds_id</code> | <code>string</code> | Universal unique transaction identifier to identify the transaction |
| <code>info</code> | <code>string</code> | info field for `chargeAuthInfo` event |
| <code>result.description</code> | <code>string</code> [Optional] | field that you must show to the customer, indicating the method the user must use to authenticate during the decoupled challenge flow. |

### ChargeError

<a name="cb_chargeError" id="cb_chargeError"></a>

| Param         | Type                | Description                                                         |
| ------------- | ------------------- | ------------------------------------------------------------------- |
| <code>error</code> | <code>object</code> | error response |
| <code>charge_3ds_id</code> | <code>string</code> | Universal unique transaction identifier to identify the transaction |

## Wallet Buttons
You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#wallet-buttons-simple-example)

Wallet Buttons allow you to easily integrate different E-Wallets into your checkout.
Currently supports ApplePay, Google Pay, Google Pay and Apple Pay via Stripe and Flypay V2 checkout, Paypal Smart Buttons Checkout and Afterpay.

If available in your client environment, you will display a simple button that upon clicking it the user will follow the standard flow for the appropriate Wallet. If not available an event will be raised and no button will be displayed.

## Wallet Buttons simple example

### Container

```html
<div id="widget"></div>
```

You must create a container for the Wallet Buttons. Inside this tag, the button will be initialized.

Before initializing the button, you must perform a POST request to `charges/wallet` from a secure environment like your server. This call will return a token that is required to load the button and securely complete the payment. You can find the documentation to this call in the PayDock API documentation.

### Initialization

For Afterpay wallet, the country code is required:
```javascript
let button = new paydock.WalletButtons(
    "#widget",
    token,
    {
        country: "AU",
    }
);
button.load();
```

```javascript
// ES2015 | TypeScript
import { WalletButtons } from '@paydock/client-sdk';

var button = new WalletButtons(
    '#widget',
    token,
    {
        country: 'AU',
    }
);
button.load();
```

For Flypay v2 wallet, the client_id is required:
```javascript
let button = new paydock.WalletButtons(
    "#widget",
    token,
    {
        client_id: "client_id",
    }
);
button.load();
```

```javascript
// ES2015 | TypeScript
import { WalletButtons } from '@paydock/client-sdk';

var button = new WalletButtons(
    '#widget',
    token,
    {
        client_id: "client_id",
    }
);
button.load();
```

### Setting environment

Current method can change environment. By default environment = sandbox.
Bear in mind that you must set an environment before calling `button.load()`.

```javascript
button.setEnv('sandbox');
```

### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
        let button = new paydock.WalletButtons(
            "#widget",
            token,
            {
                amount_label: "Total",
                country: "DE",
            }
        );
        button.load();
</script>
</html>
```

## Wallet Buttons advanced example

### Checking for button availability

If the customer's browser is not supported, or the customer does not have any card added to their Google Pay or Apple Pay wallets, the button will not load. In this case the callback onUnavailable() will be called. You can define the behavior of this function before loading the button.

```javascript
button.onUnavailable(() => console.log("No wallet buttons available"));
```

### Performing actions when the wallet button is clicked

In some situations you may want to perform some validations or actions when the user clicks on the wallet button, for which you can use this method. Currently supported by Paypal, ApplePay and GooglePay wallets.

```javascript
button.onClick(() => console.log("Perform actions on button click"));
```

### Performing actions when shipping info is updated

In Paypal, ApplePay via MPGS and GooglePay via MPGS integrations after each shipping info update the `onUpdate(data)` will be called with the selected shipping address information, plus selected shipping method when applicable for Paypal, ApplePay and GooglePay. Merchants should handle this callback, recalculate shipping costs in their server by analyzing the new data, and submit a backend to backend request to `POST charges/:id` with the new total amount and shipping amount (you can find the documentation of this call in the PayDock API documentation).

For Paypal integration specifically, if shipping is enabled for the wallet button and different shipping methods were provided in the create wallet charge call, Merchants must ensure that the posted `shipping.amount` to `POST charges/:id` matches the selected shipping option amount (value sent in when initializing the wallet charge). In other words, when providing shipping methods the shipping amount is bound to being one of the provided shipping method amount necessarily. Bear in mind that the total charge amount must include the `shipping.amount`, since it represents the full amount to be charged to the customer.

After analyzing the new shipping information, and making the post with the updated charge and shipping amounts if necessary, the `button.update({ success: true/false })` wallet button method needs to be called to resume the interactions with the customer. Not calling this will result in unexpected behavior.

```javascript
button.onUpdate((data) => {
    console.log("Updating amount via a backend to backend call to POST charges/:id");
     // call `POST charges/:id` to modify charge
     button.update({ success: true });
});
```

For ApplePay via MPGS and GooglePay via MPGS integrations, you can also return a new `amount` and new `shipping_options` in case new options are needed based on the updated shipping data. Before the user authorizes the transaction, you receive redacted address information (address_country, address_city, address_state, address_postcode), and this data can be used to recalculate the new amount and new shipping options.

```javascript
button.onUpdate((data) => {
    console.log("Updating amount via a backend to backend call to POST charges/:id");
     // call `POST charges/:id` to modify charge
     button.update({
        success: true,
        body: {
            amount: 15,
            shipping_options: [
                {
                    id: "NEW-FreeShip",
                    label: "NEW - Free Shipping",
                    detail: "Arrives in 3 to 5 days",
                    amount: "0.00"
                },
                {
                    id: "NEW-FastShip",
                    label: "NEW - Fast Shipping",
                    detail: "Arrives in less than 1 day",
                    amount: "10.00"
                }
            ]
        }
    });
});
```

### Performing actions after the payment is completed

After the payment is completed, the onPaymentSuccessful(data) will be called if the payment was successful. If the payment was not successful, the function onPaymentError(data) will be called. If fraud check is active for the gateway, a fraud body was sent in the wallet charge initialize call and the fraud service left the charge in review, then the onPaymentInReview(data) will be called.
All three callbacks return relevant data according to each one of the scenarios.

>*Note that these callbacks will not be triggered for the Afterpay wallet when Redirect mode is used, that is when the charge is initialized with the success_url and error_url parameters, since the payment completion is done through the Redirect method, and therefore this SDK will not be loaded once the payment is completed at checkout.*

```javascript
button.onPaymentSuccessful((data) => console.log("The payment was successful"));
```

```javascript
button.onPaymentInReview((data) => console.log("The payment is on fraud review"));
```

```javascript
button.onPaymentError((data) => console.log("The payment was not successful"));
```
### Events
The above events can be used in a more generic way via de `on` function, and making use
of the corresponding event names.

```javascript
button.on(EVENT.UNAVAILABLE, () => console.log("No wallet buttons available"));
button.on(EVENT.UPDATE, (data) => console.log("Updating amount via a backend to backend call to POST charges/:id"));
button.on(EVENT.PAYMENT_SUCCESSFUL, (data) => console.log("The payment was successful"));
button.on(EVENT.PAYMENT_ERROR, (data) => console.log("The payment was not successful"));
```

This example shows how to use these functions for **Paypal Smart Checkout Buttons**:
_(Required `meta` fields: - . Optional `meta` fields: `request_shipping`, `pay_later`, `standalone`, `style`)_
### Paypal Smart Checkout Buttons Full example
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.WalletButtons(
        "#widget",
        charge_token,
        {
            request_shipping: true,
            pay_later: true,
            standalone: false,
            style: {
                layout: 'horizontal',
                color: 'blue',
                shape: 'rect',
                label: 'paypal',
            },
        }
    );
    button.setEnv('sandbox');
    button.onUnavailable(() => console.log("No wallet buttons available"));
    button.onUpdate((data) => {
        console.log("Updating amount via a backend to backend call to POST charges/:id");
        // call `POST charges/:id` to modify charge
        button.update({ success: true });
    });
    button.onPaymentSuccessful((data) => console.log("The payment was successful"));
    button.onPaymentError((data) => console.log("The payment was not successful"));
    button.onPaymentInReview((data) => console.log("The payment is on fraud review"));

    // Example 1: Asynchronous onClick handler
    const asyncLogic = async () => {
        // Perform asynchronous logic. Expectation is that a Promise is returned and attached to response via `attachResult`,
        // and resolve or reject of it will dictate how wallet behaves.
    }

    button.onClick(({ data: { attachResult } }) => {
        // Promise is attached to the result. On Paypal, when promise is resolved, the user Journey will continue.
        // If no promise is attached then the Paypal journey will not depend on the promise being resolved or rejected
        attachResult(asyncLogic());
    });

    // Example 2: Synchronous onClick handler
    // button.onClick(({ data: { attachResult } }) => {
    //     // Perform synchronous logic
    //     console.log("Synchronous onClick: Button clicked");
    //     // Optionally return a boolean flag to halt the operation
    //     attachResult(false);
    // });

    button.load();
</script>
</html>
```

This example shows how to use these functions for **Flypay v2 Wallet**.
_(Required `meta` fields: - . Optional `meta` fields: -)_
### Flypay V2 Full example
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.WalletButtons(
		"#widget",
		charge_token,
		{
            access_token: 'TOKEN',
            refresh_token: 'TOKEN',
            client_id: 'CLIENT_ID',
        },
	);
    button.setEnv('sandbox');
    button.onUnavailable((data) => console.log("No wallet buttons available"));
    button.onPaymentSuccessful((data) => console.log("The payment was successful"));
    button.onPaymentError((data) => console.log("The payment was not successful"));
    button.onAuthTokensChanged((data) => console.log('Authentication tokens changed'));
    button.load();
</script>
</html>
```

This example shows how to use these functions for **ApplePay via MPGS** and **GooglePay via MPGS**:

_(Required `meta` fields: `amount_label`, `country`. Optional `meta` fields: `raw_data_initialization`, `request_shipping`, `style.button_type`, `style.button_style`)_
### ApplePay and GooglePay via MPGS Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.WalletButtons(
        "#widget",
        charge_token,
        {
            amount_label: "Total",
            country: 'DE',
            request_shipping: true,
            show_billing_address: true,
            style: {
                google: {
                    button_type: 'buy',
                    button_size_mode: 'static',
                    button_color: 'white',
                },
                apple: {
                    button_type: 'buy',
                    button_style: 'black',
                },
            },
            shipping_options: [
                {
                    id: "FreeShip",
                    label: "Free Shipping",
                    detail: "Arrives in 5 to 7 days",
                    amount: "0.00"
                },
                {
                    id: "FastShip",
                    label: "Fast Shipping",
                    detail: "Arrives in 1 day",
                    amount: "10.00"
                }
            ]
        }
    );
    button.setEnv('sandbox');
    button.onUnavailable(() => console.log("No wallet buttons available"));
    button.onPaymentSuccessful((data) => console.log("The payment was successful"));
    button.onPaymentError((data) => console.log("The payment was not successful"));
    button.onClick(() => console.log("On WalletButton Click"));
    button.onUpdate((data) => {
        console.log("Updating amount via a backend to backend call to POST charges/:id");
        // call `POST charges/:id` to modify charge
        button.update({
            success: true,
            body: {
                amount: 15,
                shipping_options: [
                    {
                        id: "NEW-FreeShip",
                        label: "NEW - Free Shipping",
                        detail: "Arrives in 3 to 5 days",
                        amount: "0.00"
                    },
                    {
                        id: "NEW-FastShip",
                        label: "NEW - Fast Shipping",
                        detail: "Arrives in less than 1 day",
                        amount: "10.00"
                    }
                ]
            }
        });
    });
    button.load();
</script>
</html>
```

Also, for **ApplePay via MPGS** you can initialize the `ApplePayPaymentRequest` with your own values instead of using the default ones. Below you can see an example on how to initialize the `ApplePayPaymentRequest` with the `raw_data_initialization` meta field.

Similarly, for **GooglePay via MPGS** you can initialize the `PaymentMethodSpecification` with your own values instead of using the default ones. Below you can see an example on how to initialize the `PaymentMethodSpecification` with the `raw_data_initialization` meta field.
### ApplePay and GooglePay via MPGS Raw data initialization example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.WalletButtons(
        "#widget",
        charge_token,
        {
            raw_data_initialization: {
                apple: {
                    countryCode: "AU",
                    currencyCode: "AUD",
                    merchantCapabilities: ["supports3DS","supportsCredit","supportsDebit"],
                    supportedNetworks: ["visa","masterCard","amex","discover"],
                    requiredBillingContactFields: ["name","postalAddress"],
                    requiredShippingContactFields:["postalAddress","name","phone","email" ],
                    total: {
                        label: "Total",
                        amount: "10",
                        type: "final",
                    }
                },
                google: {
                    type: "CARD",
                    parameters: {
                        allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
                        allowedCardNetworks: [
                            "AMEX",
                            "DISCOVER",
                            "INTERAC",
                            "JCB",
                            "MASTERCARD",
                            "VISA",
                        ],
                        billingAddressRequired: true,
                    },
                },
            },
            amount_label: "Total",
            country: 'DE',
            request_shipping: true,
            show_billing_address: true,
            style: {
                google: {
                    button_type: 'buy',
                    button_size_mode: 'static',
                    button_color: 'white',
                },
                apple: {
                    button_type: 'buy',
                    button_style: 'black',
                },
            },
            shipping_options: [
                {
                    id: "FreeShip",
                    label: "Free Shipping",
                    detail: "Arrives in 5 to 7 days",
                    amount: "0.00"
                },
                {
                    id: "FastShip",
                    label: "Fast Shipping",
                    detail: "Arrives in 1 day",
                    amount: "10.00"
                }
            ]
        }
    );
    button.setEnv('sandbox');
    button.onUnavailable(() => console.log("No wallet buttons available"));
    button.onPaymentSuccessful((data) => console.log("The payment was successful"));
    button.onPaymentError((data) => console.log("The payment was not successful"));
    button.onUpdate((data) => {
        console.log("Updating amount via a backend to backend call to POST charges/:id");
        // call `POST charges/:id` to modify charge
        button.update({
            success: true,
            body: {
                amount: 15,
                shipping_options: [
                    {
                        id: "NEW-FreeShip",
                        label: "NEW - Free Shipping",
                        detail: "Arrives in 3 to 5 days",
                        amount: "0.00"
                    },
                    {
                        id: "NEW-FastShip",
                        label: "NEW - Fast Shipping",
                        detail: "Arrives in less than 1 day",
                        amount: "10.00"
                    }
                ]
            }
        });
    });
    button.load();
</script>
</html>
```

## Express Wallet Buttons

Express Wallet Buttons allow to integrate with E-Wallets in an "express" operational mode, allowing to show the respective button in product or cart pages.

The general flow to use the widgets is:
1. Configure your gateway and connect it using Paydock API or Dashboard.
2. Create a container in your site
```html
<div id="widget"></div>
```
3. Initialize the specific WalletButtonExpress, providing your Access Token (preferred) or Public Key, plus required and optional meta parameters for the wallet in use. The general format is:
```js
new paydock.{Provider}WalletButtonExpress(
    "#widget",
    accessTokenOrPublicKey,
    gatewayId,
    gatewaySpecificMeta,
);
```
4. (optional) If the screen where the button is rendered allows for cart/amount changes, call `setMeta` method to update the meta information.
5. Handle the `onClick` callback, where you should call your server, initialize the wallet charge via `POST v1/charges/wallet` and return the wallet token.
6. Handle the `onPaymentSuccessful`, `onPaymentError` and `onPaymentInReview` (if fraud is applicable) for payment results.

### Supported Providers
1. [Apple Pay](#apple-pay-wallet-button-express)
2. [Paypal](#paypal-wallet-button-express)

### Apple Pay Wallet Button Express

A full description of the meta parameters for [ApplePayWalletButtonExpress](#ApplePayWalletButtonExpress) meta parameters can be found [here](#ApplePayWalletMeta). Below you will find a fully working html example.

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock ApplePayWalletButtonExpress!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.ApplePayWalletButtonExpress(
        "#widget",
        accessTokenOrPublicKey,
        gatewayId,
        {
            amount_label: 'TOTAL',
            country: 'AU',
            currency: 'AUD',
            amount: 15.5,
            // merchant_capabilities: ['supports3DS', 'supportsEMV', 'supportsCredit', 'supportsDebit'],
            // supported_networks: ['visa', 'masterCard', 'amex', 'chinaUnionPay', 'discover', 'interac', 'jcb', 'privateLabel'],
            // required_billing_contact_fields: ['email', 'name', 'phone', 'postalAddress'], // phone and email do not work according to relevant testing
            // required_shipping_contact_fields: ['email', 'phone'], // Workaround to pull phone and email from shipping contact instead - does not require additional shipping address information
            // supported_countries: ["AU"],
            // style: {
            //     button_type: "buy",
            //     button_style: "black",
            // },
        }
    );

    button.setEnv('sandbox');

    button.onUnavailable(function() {
        console.log("Button not available");
    });

    button.onError(function(error) {
        console.log("On Error Callback", error);
    });

    button.onPaymentSuccessful(function(data) {
        console.log("Payment successful");
        console.log(data);
    });

    button.onPaymentError(function(err) {
        console.log("Payment error");
        console.log(err);
    });

    button.onPaymentInReview(function(data) {
        console.log("The payment is on fraud review");
        console.log(data);
    });

    button.onClick(async (data) => {
        console.log("Button clicked", data);

        const responseData = await fetch('https://your-server-url/initialize-wallet-charge');
        const parsedData = await responseData.json();
        return parsedData.resource.data.token;
    });

    button.onCheckoutClose(() => {
        console.log("Checkout closed");
    });

    button.load();
</script>
</html>
```

### Apple Pay Wallet Button Express with Shipping

A full description of the meta parameters for [ApplePayWalletButtonExpress](#ApplePayWalletButtonExpress) meta parameters can be found [here](#ApplePayWalletMeta). Below you will find a fully working html example.

```html
<html>
<head>
    <title>Apple Pay Express test page</title>
    <style>
        #inputModal {
            display: none;
            position: fixed;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
            z-index: 1000;
        }
        #inputBox {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            padding: 20px;
            background: white;
            border-radius: 5px;
            box-shadow: 2px 2px 10px rgba(0,0,0,0.5);
        }
    </style>
</head>
<body>
    <table id='paymentTable'>
        <tr>
            <td>Environment:</td>
            <td>
                <select id="environment" name="environment">
                    <option value="sandbox">Sandbox</option>
                    <option value="local">Local</option>
                </select>
            </td>
        </tr>
        <tr>
            <td>Access Token / Public Key:</td>
            <td><input type="text" name="access" id="access" /></td>
        </tr>
        <tr>
            <td>Secret Key:</td>
            <td><input type="text" name="secretKey" id="secretKey" /></td>
        </tr>
        <tr>
            <td>Gateway Id:</td>
            <td><input type="text" name="gateway" id="gateway" /></td>
        </tr>
        <tr>
            <td>Wallet Charge - for apple pay we need to add it here because the apple pay popup does not allow us to
                inject it there but it will be used only on click. So is optional, if not provided it simulates an error
                when creating B2B wallet charge on click:</td>
            <td><input type="text" name="token" id="token" /></td>
        </tr>
        <tr>
            <td>Charge id - Used to update the charge with the shipping data/shipping options</td>
            <td><input type="text" name="chargeId" id="chargeId" /></td>
        </tr>
        <tr>
            <td>Meta:</td>
            <td><textarea id="meta" name="meta" rows="4" cols="50"></textarea></td>
        </tr>
    </table>
    <div id="button">
        <input type="submit" name="event" value="Send" class="button" onclick="return loadButtons()" />
    </div>
    <div id="widget"></div>
</body>

<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js"></script>
<script type=text/javascript>
    // Environment enum with API URLs
    const ENVIRONMENTS = {
        PRODUCTION: {
            name: 'production',
            apiUrl: 'https://api.paydock.com'
        },
        SANDBOX: {
            name: 'sandbox',
            apiUrl: 'https://api-sandox.paydock.com'
        }
    };

    // Function to get API URL based on selected environment
    function getApiUrl(environmentName) {
        const env = Object.values(ENVIRONMENTS).find(env => env.name === environmentName);
        return env ? env.apiUrl : ENVIRONMENTS.SANDBOX.apiUrl;
    }

    // Function to get current base URL from selected environment
    function getBaseUrl() {
        const selectedEnvironment = document.getElementById("environment").value;
        return getApiUrl(selectedEnvironment);
    }

    async function updateCharge(secretKey, chargeId, updateData) {
        try {
            const baseUrl = getBaseUrl();
            const response = await fetch(`${baseUrl}/v1/charges/wallet/${chargeId}`, {
                method: 'PUT',
                headers: {
                    'x-user-secret-key': secretKey,
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(updateData),
            });
            const data = await response.json();
            console.log('Update charge response:', data);
            return data;
        } catch (error) {
            console.error('Error updating charge:', error);
        }
    };

    const shippingOptions = [
            {
                label: "Test 1",
                detail: "This is a test 1 shipping methods",
                amount: 10,
                id: "randomId1",
                date_components_range: {
                    start_date_components: {
                        years: 0,
                        months: 0,
                        days: 5,
                        hours: 0,
                    },
                    end_date_components: {
                        years: 0,
                        months: 0,
                        days: 10,
                        hours: 0,
                    }
                }
            },
            {
                label: "Test 2",
                detail: "This is a test 2 shipping methods",
                amount: 14,
                id: "randomId2",
                date_components_range: {
                    start_date_components: {
                        years: 0,
                        months: 0,
                        days: 6,
                        hours: 0,
                    },
                    end_date_components: {
                        years: 0,
                        months: 0,
                        days: 12,
                        hours: 0,
                    },
                },
            },
        ];

    function loadButtons() {
        const secretKey = document.getElementById("secretKey").value;
        if (!secretKey) {
            alert("Please enter the secret key.");
            return false;
        }

        let button = new paydock.ApplePayWalletButtonExpress(
            "#widget",
            document.getElementById("access").value,
            document.getElementById("gateway").value,
            JSON.parse(document.getElementById("meta").value),
        );

        button.setEnv(document.getElementById("environment").value);

        let charge_id;

        button.onClick(async (data) => {
            const { token, chargeId } = await getUserInput();
            charge_id = chargeId;
            return token;
        });

        const amount = JSON.parse(document.getElementById('meta').value).amount;

        button.onUnavailable(function() {
            console.log("Button unavailable");
        });
        button.onError(function(error) {
            console.log("On Error Callback", error);
        });
        button.onPaymentSuccessful(function(data) {
            console.log("Payment successful");
            console.log(data);
        });
        button.onPaymentError(function(err) {
            console.log("Payment error");
            console.log(err);
        });
        button.onPaymentInReview(function(data) {
            console.log("The payment is on fraud review");
            console.log(data);
        });
        button.onCheckoutClose(() => {
            console.log("Checkout closed");
        });

        button.onShippingAddressChange(async function(data) {
            console.log("Shipping address has been updated", data);

            const defaultOption = shippingOptions[0];

            const updateData = {
                shipping: {
                    ...data.data,
                    amount: defaultOption.amount,
                    method: defaultOption.id,
                    options: shippingOptions,
                },
                amount: amount + defaultOption.amount
            };

            const res = await updateCharge(secretKey, charge_id, updateData);
            return { token: res.resource.data.token };
        });

        button.onShippingOptionsChange(async function(data) {
            console.log("Shipping options have been updated", JSON.stringify(data, null, 2));

            const updateData = {
                shipping: {
                    method: data.data.shipping_option_id,
                    amount: data.data.amount,
                },
                amount: amount + Number(data.data.amount)
            };

            const res = await updateCharge(secretKey, charge_id, updateData);
            return { token: res.resource.data.token };
        });

        button.load()

        document.getElementById("paymentTable").style.display = "none";
        document.getElementById("button").style.display = "none";
        return true;
    }

    function getUserInput(message) {
        return new Promise((resolve, reject) => {
            console.log("Simulating B2B call to generate token...");
            setTimeout(() => {
                const token = document.getElementById("token").value;
                const chargeId = document.getElementById("chargeId").value;
                if (token && chargeId)
                    return resolve({ token, chargeId });
                return reject("No token or Charge Id provided");
            }, 2000);
        });
    }

    document.addEventListener('DOMContentLoaded', () => {
        // Function to get the value of a query parameter by name
        function getQueryParam(name) {
            const urlParams = new URLSearchParams(window.location.search);
            return urlParams.get(name);
        }

        // Function to set input values from URL parameters
        function setInputValues() {
            const meta = {
                apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
                amount_label: 'TOTAL',
                country: 'AU',
                currency: 'AUD',
                amount: 10,
                shipping_editing_mode: 'available', // available, store_pickup
                required_shipping_contact_fields: [
                    'postalAddress',
                    'name',
                    'phone',
                    'email',
                ],
                shipping: {
                    amount: 5,
                    address_line1: "Address Line 1",
                    address_city: "Test Locality",
                    address_state: "NSW",
                    address_country: "Australia",
                    address_country_code: "AU",
                    address_postcode: "3000",
                    contact: {
                        phone: "+61400245562",
                        email: "qa.notifications+appleid@paydock.com",
                        first_name: "QA",
                        last_name: "QA",
                    },
                    options: shippingOptions,
                },
                // merchant_capabilities: ['supports3DS', 'supportsEMV', 'supportsCredit', 'supportsDebit'],
                // supported_networks: ['visa', 'masterCard', 'amex', 'chinaUnionPay', 'discover', 'interac', 'jcb', 'privateLabel'],
                // required_billing_contact_fields: ['email', 'name', 'phone', 'postalAddress', 'phoneticName'],
                // required_shipping_contact_fields: ['email', 'phone' /*, 'name', 'postalAddress', 'phoneticName'*/],
                // supported_countries: [],
                // style: {
                //     button_type: 'continue',
                //     button_style: 'white',
                // },
            };
            document.getElementById('meta').value = JSON.stringify(meta, null, 2);
        }

        setInputValues();
    });
</script>
</html>
```

When supporting shipping, the method `onShippingAddressChange` and `onShippingOptionsChange` are required to update the shipping address and options.

```javascript
button.onShippingAddressChange(async function(data) {
    console.log("Shipping address has been updated", data);

    const updateData = {
        shipping: {
            ...data.data,
            amount: defaultOption.amount,
            method: defaultOption.id,
            options: shippingOptions,
        },
        amount: amount + defaultOption.amount
    };

    const res = await updateCharge(secretKey, charge_id, updateData);
    return { token: res.resource.data.token };
});

button.onShippingOptionsChange(async function(data) {
    console.log("Shipping options have been updated", data);

    const updateData = {
        shipping: {
            method: data.data.shipping_option_id,
            amount: data.data.amount,
        },
        amount: amount + Number(data.data.amount)
    };

    const res = await updateCharge(secretKey, charge_id, updateData);
    return { token: res.resource.data.token };
});
```

The `updateCharge` method is a custom method to update the charge with the shipping data/shipping options. It is not part of the Paydock SDK and it should use the Paydock's public API to update the charge from the merchant's server.

### Supported Cases
#### Injected Shipping Address, non-editable by the customer

This is the case where the shipping address is injected by the merchant and is not editable by the customer. The customer can only select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'store_pickup'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'store_pickup',
    required_shipping_contact_fields: [
        'postalAddress', // At least one of the fields is required so the shipping address is shown at Apple Pay.
        'name',
        'phone',
        'email',
    ],
    shipping: {
        amount: 5,
        address_line1: "Address Line 1",
        address_city: "Test Locality",
        address_state: "NSW",
        address_country: "Australia",
        address_country_code: "AU",
        address_postcode: "3000",
        contact: {
            phone: "+61400245562",
            email: "qa.notifications+appleid@paydock.com",
            first_name: "QA",
            last_name: "QA",
        },
        options: [
            {
                label: "Test 1",
                detail: "This is a test 1 shipping methods",
                amount: 10,
                id: "randomId1",
                date_components_range: {
                    start_date_components: {
                        years: 0,
                        months: 0,
                        days: 5,
                        hours: 0,
                    },
                    end_date_components: {
                        years: 0,
                        months: 0,
                        days: 10,
                        hours: 0,
                    }
                }
            }
        ],
    },
}
```

This is the case where the shipping address is injected by the merchant and is editable by the customer. The customer can edit the shipping address and select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'available'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'available',
    required_shipping_contact_fields: [
        'postalAddress', // At least one of the fields is required so the shipping address is shown at Apple Pay.
        'name',
        'phone',
        'email',
    ],
    shipping: {
        amount: 5,
        address_line1: "Address Line 1",
        address_city: "Test Locality",
        address_state: "NSW",
        address_country: "Australia",
        address_country_code: "AU",
        address_postcode: "3000",
        contact: {
            phone: "+61400245562",
            email: "qa.notifications+appleid@paydock.com",
            first_name: "QA",
            last_name: "QA",
        },
        options: [
            {
                label: "Test 1",
                detail: "This is a test 1 shipping methods",
                amount: 10,
                id: "randomId1",
                date_components_range: {
                    start_date_components: {
                        years: 0,
                        months: 0,
                        days: 5,
                        hours: 0,
                    },
                    end_date_components: {
                        years: 0,
                        months: 0,
                        days: 10,
                        hours: 0,
                    }
                }
            }
        ],
    },
}
```

#### Shipping address editable by the customer

This is the case where the shipping address is not injected by the merchant and is editable by the customer. The customer can edit the shipping address and select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'available'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'available',
    required_shipping_contact_fields: [
        'postalAddress', // At least one of the fields is required so the shipping address is shown at Apple Pay.
        'name',
        'phone',
        'email',
    ],
}
```

When the the customer selects an address, the `onShippingAddressChange` method is called with the shipping address data. If the merchant wants to update the charge with the shipping address, it should update the charge using Paydock's public API Update Charge method with the shipping address data.

It can include the shipping options in the update data, that will be used to show the shipping options in the Apple Pay popup.

```javascript
button.onShippingAddressChange(async function(data) {
    console.log("Shipping address has been updated", data);

    const updateData = {
        shipping: {
            ...data.data,
            amount: defaultOption.amount,
            method: defaultOption.id,
            options: shippingOptions,
        },
        amount: amount + defaultOption.amount
    };
});
```

#### No shipping address

This is the case where no shipping address is required at all in the popup (e.g., digital goods, services, or virtual products, or Shipping Address collected separately by the merchant). The "Send to" UI field will not be shown in the Apple Pay sheet, it will be hidden.

**Important:**
- No shipping address should be provided in the meta object.
- Shipping address could be provided in the initial POST `/v1/charges/wallet` endpoint, if collected previously.

The required meta parameters for this case are:
- `required_shipping_contact_fields`: Only include contact fields if needed (phone, email), but NOT `postalAddress`.

```javascript
meta:  {
  "amount_label": "TOTAL",
  "country": "AU",
  "currency": "AUD",
  "amount": 10,
  "shipping_editing_mode": "available",
  "required_shipping_contact_fields": ["phone", "email"],
  "apple_pay_capabilities": ["credentials_available", "credentials_status_unknown", "credentials_unavailable"]
}
```

### Paypal Wallet Button Express
A full description of the meta parameters for [PaypalWalletButtonExpress](#PaypalWalletButtonExpress) meta parameters can be found [here](#PaypalWalletMeta). Below you will find a fully working html example.

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h2>Payment using PayDock PaypalWalletButtonExpress!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.PaypalWalletButtonExpress(
        "#widget",
        accessTokenOrPublicKey,
        gatewayId,
        {
            amount: 15.5,
            currency: 'AUD',
            pay_later: false,
            standalone: false,
            capture: true,
            // style: {
            //     layout: 'horizontal', // or 'vertical'
            //     color: 'gold', // or 'blue', 'silver', 'black', 'white'
            //     shape: 'rect', // or 'pill', 'sharp'
            //     borderRadius: 5,
            //     height: 40,
            //     disableMaxWidth: false,
            //     label: 'paypal', // or 'checkout', 'buynow', 'pay', 'installment'
            //     tagline: true,
            //     messages: {
            //         layout: 'text', // or 'flex'
            //         logo: {
            //             type: 'primary', // or 'alternative', 'inline', 'none'
            //             position: 'left', // or 'right', 'top'
            //         },
            //         text: {
            //             color: 'black', // or 'white', 'monochrome', 'grayscale'
            //             size: 10, // or 11, 12, 13, 14, 15, 16
            //             align: 'left', // or 'center', 'right'
            //         },
            //         color: 'blue', // or 'black', 'white', 'white-no-border', 'gray', 'monochrome', 'grayscale'
            //         ratio: '1x1', // or '1x4', '8x1', '20x1'
            //     },
            // }
        }
    );

    button.setEnv('sandbox');

    button.onUnavailable(function() {
        console.log("Button not available");
    });

    button.onError(function(error) {
        console.log("On Error Callback", error);
    });

    button.onPaymentSuccessful(function(data) {
        console.log("Payment successful");
        console.log(data);
    });

    button.onPaymentError(function(err) {
        console.log("Payment error");
        console.log(err);
    });

    button.onPaymentInReview(function(data) {
        console.log("The payment is on fraud review");
        console.log(data);
    });

    button.onClick(async (data) => {
        console.log("Button clicked", data);

        const responseData = await fetch('https://your-server-url/initialize-wallet-charge');
        const parsedData = await responseData.json();
        return parsedData.resource.data.token;
    });

    button.onCheckoutClose(() => {
        console.log("Checkout closed");
    });

    button.load();
</script>
</html>
```

## Open Wallet Buttons
You can find description of all methods and parameters [here](https://www.npmjs.com/package/@paydock/client-sdk#open-wallet-buttons-simple-example)

Open Wallet Buttons provide a next-generation approach to integrating E-Wallets into your checkout with improved event handling and more granular control over wallet interactions.

Each wallet type has its own dedicated class with fully typed metadata:
- [ApplePayOpenWalletButton](#ApplePayOpenWalletButton) - for Apple Pay integration
- [GooglePayOpenWalletButton](#GooglePayOpenWalletButton) - for Google Pay integration

On `load()`, each button fetches the service configuration from PayDock and validates that the service type matches the expected wallet. If there is a mismatch (e.g. using an Apple Pay service ID with `GooglePayOpenWalletButton`), an error will be raised via the `onError` callback.

If available in your client environment, you will display a simple button that upon clicking it the user will follow the standard flow for the appropriate Wallet. If not available an event will be raised and no button will be displayed.

## Apple Pay Open Wallet Button

### Container

```html
<div id="widget"></div>
```

You must create a container for the Open Wallet Button. Inside this tag, the button will be initialized.

Before initializing the button, you must configure your Apple Pay wallet service through the PayDock dashboard and obtain the service ID that will be used to load the button configuration.

### Initialization

```javascript
let button = new paydock.ApplePayOpenWalletButton(
    "#widget",
    publicKeyOrAccessToken,
    serviceId,
    {
        amount: 100,
        currency: "AUD",
        country: "AU",
        amount_label: "TOTAL",
        store_name: "My Store",
        apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    }
);
button.load();
```

```javascript
// ES2015 | TypeScript
import { ApplePayOpenWalletButton } from '@paydock/client-sdk';

var button = new ApplePayOpenWalletButton(
    '#widget',
    publicKeyOrAccessToken,
    serviceId,
    {
        amount: 100,
        currency: 'AUD',
        country: 'AU',
        amount_label: 'TOTAL',
        store_name: 'My Store',
    }
);
button.load();
```

### Constructor Parameters

The ApplePayOpenWalletButton constructor accepts the following parameters:

1. **selector** (string): CSS selector for the container element
2. **publicKeyOrAccessToken** (string): Your PayDock public key or access token
3. **serviceId** (string): The Apple Pay service ID configured in PayDock dashboard
4. **meta** (ApplePayOpenWalletMeta): Apple Pay-specific configuration object

> **Note:** Required meta fields (`amount`, `currency`, `country`, `amount_label`, `store_name`) are validated automatically by the `ApplePayOpenWalletButton` class. You do not need to specify them manually.

### Setting environment

Current method can change environment. By default environment = sandbox.
Bear in mind that you must set an environment before calling `button.load()`.

```javascript
button.setEnv('sandbox');
```

### Full Apple Pay example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Apple Pay with Open Wallets</title>
</head>
<body>
    <h2>Payment using PayDock Apple Pay Open Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.ApplePayOpenWalletButton(
        "#widget",
        publicKeyOrAccessToken,
        serviceId,
        {
            amount: 100,
            currency: "AUD",
            country: "AU",
            amount_label: "TOTAL",
            store_name: "My Store",
            request_shipping: true,
            request_payer_name: true,
            request_payer_email: true,
            request_payer_phone: true,
            show_billing_address: true,
            style: {
                button_type: 'buy',
                button_style: 'black'
            },
            shipping_options: [
                {
                    id: "standard",
                    label: "Standard Shipping",
                    detail: "Arrives in 5 to 7 days",
                    amount: 5.00,
                    date_components_range: {
                        start_date_components: {
                            years: 0,
                            months: 0,
                            days: 5,
                            hours: 0,
                        },
                        end_date_components: {
                            years: 0,
                            months: 0,
                            days: 7,
                            hours: 0,
                        }
                    }
                },
                {
                    id: "express",
                    label: "Express Shipping",
                    detail: "Arrives in 1 to 2 days",
                    amount: 15.00,
                    date_components_range: {
                        start_date_components: {
                            years: 0,
                            months: 0,
                            days: 1,
                            hours: 0,
                        },
                        end_date_components: {
                            years: 0,
                            months: 0,
                            days: 2,
                            hours: 0,
                        }
                    }
                }
            ],
            apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable']
        }
    );

    button.setEnv('sandbox');

    button.onUnavailable(({ data }) => {
        console.log("Apple Pay not available:", data);
    });

    button.onClick(() => {
        console.log("Apple Pay button clicked");
    });

    button.onSuccess(({ data }) => {
        console.log("Payment successful:", data);
        processPayment(data.token);
    });

    button.onError(({ data }) => {
        console.error("Payment error:", data);
    });

    button.onCancel(() => {
        console.log("Payment cancelled");
    });

    button.onShippingAddressChange(async ({ data }) => {
        const response = await updateShippingCosts(data);
        return {
            amount: response.newAmount,
            shipping_options: response.shippingOptions
        };
    });

    button.onShippingOptionsChange(async ({ data }) => {
        const response = await updateTotal(data);
        return {
            amount: response.newAmount
        };
    });

    button.load();

    async function updateShippingCosts(addressData) {
        // Your shipping calculation logic based on address
        const baseAmount = 100;
        const updatedShippingOptions = [
            {
                id: "updated-standard",
                label: "Updated Standard Shipping",
                detail: "Based on your location",
                amount: 8.00
            },
            {
                id: "updated-express",
                label: "Updated Express Shipping",
                detail: "Fast delivery to your area",
                amount: 18.00
            }
        ];

        return {
            newAmount: baseAmount + updatedShippingOptions[0].amount,
            shippingOptions: updatedShippingOptions
        };
    }

    async function updateTotal(shippingOption) {
        const baseAmount = 100;
        const shippingAmount = shippingOption.amount;
        return {
            newAmount: baseAmount + shippingAmount
        };
    }

    function processPayment(ottToken) {
        fetch('/api/process-payment', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ott_token: ottToken })
        });
    }
</script>
</html>
```

### Apple Pay with Shipping

For Apple Pay with shipping enabled:
```javascript
let button = new paydock.ApplePayOpenWalletButton(
    "#widget",
    publicKeyOrAccessToken,
    serviceId,
    {
        amount: 100,
        currency: "AUD",
        country: "AU",
        amount_label: "TOTAL",
        store_name: "My Store",
        request_shipping: true,
        shipping_editing_mode: 'available',
        required_shipping_contact_fields: [
            'postalAddress',
            'name',
            'phone',
            'email',
        ],
        shipping_options: [
            {
                id: "standard",
                label: "Standard Shipping",
                detail: "5-7 business days",
                amount: 5.00
            },
            {
                id: "express",
                label: "Express Shipping",
                detail: "1-2 business days",
                amount: 15.00
            }
        ],
        apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable']
    }
);
button.load();
```

When supporting shipping, registering `onShippingAddressChange` and `onShippingOptionsChange` handlers lets you recalculate totals and update shipping options dynamically. If no handler is registered (or the handler throws), the SDK auto-accepts with the current amount and options.

```javascript
button.onShippingAddressChange(async function({ data }) {
    console.log("Shipping address has been updated", data);
    return {
        amount: newAmount,
        shipping_options: updatedShippingOptions
    };
});

button.onShippingOptionsChange(async function({ data }) {
    console.log("Shipping option selected", data);
    return {
        amount: newTotalAmount
    };
});
```

### Supported Shipping Cases

#### Injected Shipping Address, non-editable by the customer

This is the case where the shipping address is injected by the merchant and is not editable by the customer. The customer can only select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'store_pickup'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'store_pickup',
    required_shipping_contact_fields: [
        'postalAddress',
        'name',
        'phone',
        'email',
    ],
    shipping: {
        amount: 5,
        address_line1: "Address Line 1",
        address_city: "Test Locality",
        address_state: "NSW",
        address_country: "Australia",
        address_country_code: "AU",
        address_postcode: "3000",
        contact: {
            phone: "+61400245562",
            email: "test@example.com",
            first_name: "QA",
            last_name: "QA",
        },
        options: [
            {
                label: "Test 1",
                detail: "This is a test 1 shipping methods",
                amount: 10,
                id: "randomId1",
            }
        ],
    },
}
```

#### Injected Shipping Address, editable by the customer

This is the case where the shipping address is injected by the merchant and is editable by the customer. The customer can edit the shipping address and select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'available'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'available',
    required_shipping_contact_fields: [
        'postalAddress',
        'name',
        'phone',
        'email',
    ],
    shipping: {
        amount: 5,
        address_line1: "Address Line 1",
        address_city: "Test Locality",
        address_state: "NSW",
        address_country: "Australia",
        address_country_code: "AU",
        address_postcode: "3000",
        contact: {
            phone: "+61400245562",
            email: "test@example.com",
            first_name: "QA",
            last_name: "QA",
        },
        options: [
            {
                label: "Test 1",
                detail: "This is a test 1 shipping methods",
                amount: 10,
                id: "randomId1",
            }
        ],
    },
}
```

#### Shipping address editable by the customer (no pre-filled address)

This is the case where the shipping address is not injected by the merchant and is editable by the customer. The customer can edit the shipping address and select the shipping option.

The required meta parameters for this case are:
- shipping_editing_mode: 'available'
- required_shipping_contact_fields: ['postalAddress'] <-- At least one of the fields is required so the shipping address is shown at Apple Pay.

```javascript
meta: {
    apple_pay_capabilities: ['credentials_available', 'credentials_status_unknown', 'credentials_unavailable'],
    amount_label: 'TOTAL',
    country: 'AU',
    currency: 'AUD',
    amount: 10,
    shipping_editing_mode: 'available',
    required_shipping_contact_fields: [
        'postalAddress',
        'name',
        'phone',
        'email',
    ],
}
```

#### No shipping address

This is the case where no shipping address is required at all in the popup (e.g., digital goods, services, or virtual products, or Shipping Address collected separately by the merchant). The "Send to" UI field will not be shown in the Apple Pay sheet, it will be hidden.

**Important:**
- No shipping address should be provided in the meta object.
- Shipping address could be provided in the initial POST `/v1/charges/wallet` endpoint, if collected previously.

The required meta parameters for this case are:
- `required_shipping_contact_fields`: Only include contact fields if needed (phone, email), but NOT `postalAddress`.

```javascript
meta: {
    amount_label: "TOTAL",
    country: "AU",
    currency: "AUD",
    amount: 10,
    shipping_editing_mode: "available",
    required_shipping_contact_fields: ["phone", "email"],
    apple_pay_capabilities: ["credentials_available", "credentials_status_unknown", "credentials_unavailable"]
}
```

## Google Pay Open Wallet Button

### Initialization

```javascript
let button = new paydock.GooglePayOpenWalletButton(
    "#widget",
    publicKeyOrAccessToken,
    serviceId,
    {
        amount: 100,
        currency: "AUD",
        country: "AU",
        merchant_name: "Your Store",
    }
);
button.load();
```

```javascript
// ES2015 | TypeScript
import { GooglePayOpenWalletButton } from '@paydock/client-sdk';

var button = new GooglePayOpenWalletButton(
    '#widget',
    publicKeyOrAccessToken,
    serviceId,
    {
        amount: 100,
        currency: 'AUD',
        country: 'AU',
        merchant_name: 'Your Store',
    }
);
button.load();
```

### Constructor Parameters

The GooglePayOpenWalletButton constructor accepts the following parameters:

1. **selector** (string): CSS selector for the container element
2. **publicKeyOrAccessToken** (string): Your PayDock public key or access token
3. **serviceId** (string): The Google Pay service ID configured in PayDock dashboard
4. **meta** (GooglePayOpenWalletMeta): Google Pay-specific configuration object

> **Note:** Required meta fields (`amount`, `currency`, `country`) are validated automatically by the `GooglePayOpenWalletButton` class. You do not need to specify them manually.

### Full Google Pay Example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Google Pay with Open Wallets</title>
</head>
<body>
    <h2>Payment using PayDock Google Pay Open Wallet Button!</h2>
    <div id="widget"></div>
</body>
<script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
<script>
    let button = new paydock.GooglePayOpenWalletButton(
        "#widget",
        publicKeyOrAccessToken,
        serviceId,
        {
            amount: 100,
            currency: "AUD",
            country: "AU",
            amount_label: "Total",
            request_shipping: true,
            show_billing_address: true,
            merchant_name: 'Test Merchant',
            style: {
                button_type: 'buy',
                button_color: 'default',
                button_size_mode: 'fill'
            },
            shipping_options: [
                {
                    id: "standard",
                    label: "Standard Shipping",
                    detail: "Arrives in 5 to 7 days",
                    amount: 5.00,
                    type: "ELECTRONIC"
                },
                {
                    id: "express",
                    label: "Express Shipping",
                    detail: "Arrives in 1 to 2 days",
                    amount: 15.00,
                    type: "PICKUP"
                }
            ]
        }
    );

    button.setEnv('sandbox');

    button.onSuccess(({ data }) => {
        console.log("Payment successful:", data);
        processPayment(data.token);
    });

    button.onShippingAddressChange(async ({ data }) => {
        const response = await updateShippingCosts(data);
        return {
            amount: response.newAmount,
            shipping_options: response.shippingOptions
        };
    });

    button.onShippingOptionsChange(async ({ data }) => {
        const response = await updateTotal(data);
        return {
            amount: response.newAmount
        };
    });

    button.onUnavailable(({ data }) => {
        console.log("Google Pay not available:", data);
    });

    button.onError(({ data }) => {
        console.error("Payment error:", data);
    });

    button.onCancel(() => {
        console.log("Payment cancelled");
    });

    button.onClick(() => {
        console.log("Google Pay button clicked");
    });

    button.load();

    // Helper functions
    async function updateShippingCosts(addressData) {
        const baseAmount = 100;
        const updatedShippingOptions = [
            {
                id: "updated-standard",
                label: "Updated Standard Shipping",
                detail: "Based on your location",
                amount: 8.00,
                type: "ELECTRONIC"
            },
            {
                id: "updated-express",
                label: "Updated Express Shipping",
                detail: "Fast delivery to your area",
                amount: 18.00,
                type: "PICKUP"
            }
        ];

        return {
            newAmount: baseAmount + updatedShippingOptions[0].amount,
            shippingOptions: updatedShippingOptions
        };
    }

    async function updateTotal(shippingOption) {
        const baseAmount = 100;
        const shippingAmount = shippingOption.amount;
        return {
            newAmount: baseAmount + shippingAmount
        };
    }

    function processPayment(ottToken) {
        fetch('/api/process-payment', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ ott_token: ottToken })
        });
    }
</script>
</html>
```

## Common API

Both `ApplePayOpenWalletButton` and `GooglePayOpenWalletButton` share the same event handler API inherited from the base class.

### Checking for button availability

If the customer's browser is not supported, or the customer does not have any card added to their wallet, the button will not load. In this case the callback onUnavailable() will be called. You can define the behavior of this function before loading the button.

```javascript
button.onUnavailable(({ data }) => console.log("No wallet button available", data));
```

### Service type validation

Each button validates that the service configuration matches its expected wallet type. If you use an Apple Pay service ID with `GooglePayOpenWalletButton` (or vice versa), an error will be emitted via `onError`:

```javascript
// This will raise an error if the service ID does not correspond to a Google Pay service
let button = new paydock.GooglePayOpenWalletButton(
    "#widget",
    publicKeyOrAccessToken,
    applePayServiceId, // Wrong! This is an Apple Pay service ID
    meta
);

button.onError(({ data }) => {
    // Error: Service configuration type 'ApplePay' does not match expected wallet type 'google'.
    console.error(data.error.message);
});

button.load();
```

### Performing actions when the wallet button is clicked

You can perform validations or actions when the user clicks on the wallet button. The callback supports both synchronous and asynchronous operations using its return value: return `false` to abort, return a `Promise` to defer the wallet sheet, or throw an error to abort.

```javascript
// Synchronous — continue normally
button.onClick(() => {
    console.log("Perform actions on button click");
});

// Synchronous — return false to abort the payment flow
button.onClick(() => {
    if (!isOrderValid()) return false;
});

// Asynchronous — defer the wallet sheet until the promise resolves
button.onClick(async () => {
    const response = await fetch('/api/validate-order');
    const result = await response.json();
    if (!result.valid) {
        throw new Error('Order validation failed');
    }
});
```

### Handling successful OTT creation

When the One Time Token (OTT) is successfully created, the onSuccess callback will be called with the token data. **This callback is required** - if no handler is provided, an error will be thrown.

```javascript
button.onSuccess(({ data }) => {
    console.log("OTT created successfully:", data.token);
    console.log("Amount:", data.amount);
    console.log("Shipping:", data.shipping);
    console.log("Billing:", data.billing);

    fetch('/api/process-payment', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ott_token: data.token })
    });
});
```

**Important**: The `onSuccess` event handler is mandatory. Not providing one will result in an error.

### Updating meta after initialization

If the screen where the button is rendered allows for cart/amount changes, call `setMeta` method to update the meta information. The `setMeta` method is fully typed for each wallet:

```javascript
// For Apple Pay - accepts ApplePayOpenWalletMeta
applePayButton.setMeta({ ...meta, amount: 29.99, amount_label: 'NEW TOTAL' });

// For Google Pay - accepts GooglePayOpenWalletMeta
googlePayButton.setMeta({ ...meta, amount: 29.99, merchant_name: 'Updated Store' });
```

### Handling errors

Register a callback function to handle errors that occur during wallet operations, including service type mismatches.

```javascript
button.onError(({ data }) => {
    console.error("Open Wallet error:", data.error);
    console.log("Error context:", data.context);

    showErrorMessage("Payment initialization failed. Please try again.");
});
```

### Handling checkout cancellation

When the user cancels or closes the wallet payment interface, you can perform cleanup operations.

```javascript
button.onCancel(() => {
    console.log("Wallet checkout cancelled");
    window.location.href = '/checkout';
});
```

### Cleaning up

Remove the wallet button from the DOM when it is no longer needed:

```javascript
button.destroy();
```

### Events
The above events can be used in a more generic way via the `eventEmitter.subscribe` method internally, but the recommended approach is to use the dedicated event handler methods provided by the button classes.

**Available Event Handler Methods:**
- `onClick(handler)` - Button click events (return `false` to abort, `Promise` to defer)
- `onSuccess(handler)` - **Required** - OTT creation success events
- `onUnavailable(handler)` - Wallet unavailable events (supports Promise pattern)
- `onError(handler)` - Error events (supports Promise pattern)
- `onCancel(handler)` - Checkout cancellation events (supports Promise pattern)
- `onLoaded(handler)` - Button loaded/rendered events
- `onShippingAddressChange(handler)` - **Recommended for shipping** - Address change events (auto-accepted if not registered)
- `onShippingOptionsChange(handler)` - **Recommended for shipping options** - Option change events (auto-accepted if not registered)

**Event Handler Patterns:**

```javascript
// Required handler
button.onSuccess(handler);  // Always required

// Recommended when shipping is enabled (auto-accepted if not registered)
button.onShippingAddressChange(handler);
button.onShippingOptionsChange(handler);

// Optional handlers with Promise support
button.onUnavailable(handler);  // or await button.onUnavailable()
button.onError(handler);        // or await button.onError()
button.onCancel(handler);       // or await button.onCancel()

// Click handler with flow control
button.onClick(handler);  // Return false to abort, or a Promise to defer

// Loaded handler
button.onLoaded(handler);  // Notified when button renders
```

### Apple Pay-Specific Meta Properties

A full description of the [ApplePayOpenWalletMeta](#ApplePayOpenWalletMeta) properties:

**Required:**
- `amount`: The payment amount (number)
- `currency`: The currency code (string, e.g., "AUD")
- `country`: The country code (string, e.g., "AU")
- `amount_label`: Label for the total amount (string)
- `store_name`: Merchant store name (string)

**Optional:**
- `request_shipping?: boolean`: Enable shipping address collection
- `shipping_options?: IApplePayShippingOption[]`: Array of shipping options
- `show_billing_address?: boolean`: Show billing address fields
- `apple_pay_capabilities?: string[]`: Device capabilities
- `merchant_capabilities?: string[]`: Merchant capabilities
- `supported_networks?: string[]`: Supported payment networks
- `required_billing_contact_fields?: string[]`: Required billing contact fields
- `required_shipping_contact_fields?: string[]`: Required shipping contact fields
- `supported_countries?: string[]`: Supported countries
- `shipping_editing_mode?: 'available' | 'store_pickup'`: Shipping address editing mode
- `style?: { button_type?: ApplePayButtonType, button_style?: ApplePayButtonStyle }`: Button styling

### Google Pay-Specific Meta Properties

A full description of the [GooglePayOpenWalletMeta](#GooglePayOpenWalletMeta) properties:

**Required:**
- `amount`: The payment amount (number)
- `currency`: The currency code (string, e.g., "AUD")
- `country`: The country code (string, e.g., "AU")

**Optional:**
- `amount_label?: string`: Label for the total amount
- `merchant_name?: string`: Display name for the merchant
- `request_shipping?: boolean`: Enable shipping address collection
- `shipping_options?: IGooglePayShippingOption[]`: Array of shipping options
- `show_billing_address?: boolean`: Show billing address fields
- `card_config?: GooglePayCardConfig`: Card configuration (auth methods, networks, tokenization)
- `style?: { button_type?: GooglePayButtonType, button_color?: GooglePayButtonColor, button_size_mode?: GooglePayButtonSizeMode }`: Button styling

### Shipping Options Format
```javascript
shipping_options: [
    {
        id: "option_id",           // Unique identifier (string)
        label: "Option Name",      // Display name (string)
        detail: "Description",     // Optional description (string)
        amount: 10.00,            // Shipping cost as number
        date_components_range: {   // Optional: delivery date range (Apple Pay only)
            start_date_components: {
                years: 0,
                months: 0,
                days: 5,
                hours: 0,
            },
            end_date_components: {
                years: 0,
                months: 0,
                days: 10,
                hours: 0,
            }
        }
    }
]
```

**Important**:
- `amount` should be a **number**, not a string
- `date_components_range` is optional but provides delivery estimates (Apple Pay only)
- Updated shipping options returned from event handlers don't require `date_components_range`

### Environment Setup
```javascript
// Always set environment before loading
button.setEnv('sandbox');
button.load();
```

### Error Handling Best Practices
```javascript
button.onError(function({ data }) {
    console.error('Full error object:', data);

    const errorMessage = data.error?.message || 'Unknown error occurred';

    if (data.context?.operation === 'wallet_operation') {
        showWalletError(errorMessage);
    } else {
        showGeneralError(errorMessage);
    }
});
```

# Click To Pay

## Overview

Integrate with Click To Pay using Paydock's Click To Pay widget.
For a full description of the methods and parameters, reference the [README file](https://www.npmjs.com/package/@paydock/client-sdk#ClickToPay).

## Click To Pay simple example

The following section provides an example use case and integration for the widget.

### Create a Container

To integrate the Click To Pay checkout iFrame, create a container in your HTML code. This container serves as the placeholder for the iFrame.

```html
<div id="checkoutIframe"></div>
```

### Initialize the Widget

Use the following code to initialize your widget:

```javascript
var src = new paydock.ClickToPay(
    "#checkoutIframe",
    "service_id",
    "paydock_public_key_or_access_token",
    {}, // meta
);
src.load();
```

```javascript
// ES2015 | TypeScript
import { ClickToPay } from '@paydock/client-sdk';
var src = new ClickToPay(
    "#checkoutIframe",
    "service_id",
    "paydock_public_key_or_access_token",
    {}, // meta
);
src.load();
```

*NOTE:* Paydock recommends that you use a Paydock Access Token instead of a public key for security reasons in production environments. When creating your access token, you must enable the `Secure Remote Commerce` and add a whitelist for the domain of your checkout screen.

### Full example

A full example of the container and the initialized widget is as follows:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 40%;height: 300px;}</style>
</head>
<body>
    <div id="checkoutIframe"></div>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
    <script>
        var src = new paydock.ClickToPay(
            "#checkoutIframe",
            "service_id",
            "paydock_public_key_or_access_token",
            {},
        );
        src.load();
    </script>
</body>
</html>
```

## Customize your Click To Pay Checkout

The following is an advanced example that includes customization. You can use these methods to enhance your checkout experience.

### Settings

```javascript
src.setEnv('sandbox'); // set environment
src.hideCheckout(); // hide checkout iframe
src.showCheckout(); // show checkout iframe
src.on('iframeLoaded', () => {
    console.log("Initial iframe loaded");
});
src.on('checkoutReady', () => {
    console.log("Checkout ready to be used");
});
src.on('checkoutCompleted', (token) => {
    console.log(token);
});
src.on('checkoutError', (error) => {
    console.log(error);
});
```

### Full example

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>iframe {border: 0;width: 40%;height: 450px;}</style>
</head>
<body>
    <div id="checkoutIframe"></div>
    <script src="https://widget.paydock.com/sdk/latest/widget.umd.min.js" ></script>
    <script>
        var src = new paydock.ClickToPay(
            "#checkoutIframe",
            "service_id",
            "paydock_public_key_or_access_token",
            {},
        );
        src.on('iframeLoaded', () => {
            console.log("Initial iframe loaded");
        });
        src.on('checkoutReady', () => {
            console.log("Checkout ready to be used");
        });
        src.on('checkoutCompleted', (token) => {
            console.log(token);
        });
        src.on('checkoutError', (error) => {
            console.log(error);
        });
        src.load();
    </script>
</body>
</html>
```

## Customize your billing address fields

To customize your billing address experience, Paydock uses a flag that manages whether a customer's billing address is mandatory.
The options for this customization are NONE (default option), and POSTAL_COUNTRY or FULL.

```
var src = new paydock.ClickToPay(
    "#checkoutIframe",
    "service_id",
    "paydock_public_key_or_access_token",
    {
        "dpa_transaction_options": {
            "dpa_billing_preference": "FULL"
        }
    },
);
```

The Click To Pay checkout in the example requires the billing address from the customer, which is then returned as a part of the checkout data. The data is then stored and leveraged in the Paydock charge.
You can also provide the billing address at the time of creating the charge. For example, if you have a different method for collecting the billing address, such as outside of the Click To Pay checkout, you can provide it alongside other information at the charge creation step:
1. Disable the billing address in Paydock's Click To Pay widget.
2. Get your One Time Token from the Click To Pay widget alongside other details that may have been collected outside the Click To Pay checkout as the shipping address.
3. Send the billing address when creating the charge.

```
POST v1/charges
{
    "amount": "10.00",
    "currency": "AUD",
    "token": "one_time_token",
    "customer": {
        "payment_source": {
            "gateway_id": "gateway_id",
            "address_line1": "address_line1",
            "address_line2": "address_line2",
            "address_city": "address_city",
            "address_postcode": "address_postcode",
            "address_state": "address_state",
            "address_country": "address_country"
        }
    },
    "shipping": {
        "address_line1": "address_line1",
        "address_line2": "address_line2",
        "address_line3": "address_line3",
        "address_city": "address_city",
        "address_postcode": "address_postcode",
        "address_state": "address_state",
        "address_country": "address_country"
    }
}
```

## How to customize accepted cards

You can send a flag `unaccepted_card_type` to block the usage of a specific card type. The available options are 'DEBIT' and 'CREDIT'.

### Example code

The following example demonstrates how to block the card:

```
var src = new paydock.ClickToPay(
    "#checkoutIframe",
    "service_id",
    "paydock_public_key",
    {
        unaccepted_card_type: 'DEBIT'
    },
);
```

## Personalize the Style

Customize the look and feel of your UI. The following example demonstrates changes in the styling of the buttons.

### Example code

```
var src = new paydock.ClickToPay(
    "#checkoutIframe",
    "service_id",
    "paydock_public_key",
    {},
);
src.setStyles({
    enable_src_popup: true,
    primary_button_color: 'red',
    secondary_button_color: 'red',
    primary_button_text_color: 'red',
    secondary_button_text_color: 'red',
    font_family: 'Arial',
});
```

## Configurations for background loading and improved User Load times

### hold_for_customer_data
When set to `true`, this flag allows you to load the Click to Pay iframe in the background while collecting customer information. This improves the perceived performance by starting the iframe load process early.

```javascript
const clickToPay = new cba.ClickToPay("#checkoutIframe", SERVICE_ID, PUBLIC_KEY, {
  hold_for_customer_data: true,
  dpa_config: {
    dpa_id: DPA_ID,
    // ... other config
  }
});
```

### injectCustomerData(customer)
Once you have collected the customer information, use this method to provide it to the Click to Pay iframe. This will trigger the checkout to continue.

```javascript
clickToPay.injectCustomerData({
  email: "customer@example.com",
  first_name: "John",
  last_name: "Doe",
  phone: "+61400123456"  // Include country code
});
```

### Background Loading Example
Here's a complete example showing how to implement background loading while collecting customer information:

```javascript
// 1. Initialize with hold_for_customer_data
const clickToPay = new cba.ClickToPay("#checkoutIframe", SERVICE_ID, PUBLIC_KEY, {
  hold_for_customer_data: true,
  dpa_config: {
    dpa_id: DPA_ID,
    // ... other config
  }
});

// Hide checkout and show form initially
document.getElementById("checkoutIframe").style.display = 'none';
document.getElementById("customerForm").style.display = 'block';
document.getElementById("loader").style.display = 'none';

// 2. Setup event handlers
clickToPay.on('iframeLoaded', () => {
  // Iframe is now loaded
});

clickToPay.on('checkoutReady', () => {
  // Hide loader, show checkout
  document.getElementById("loader").style.display = 'none';
  document.getElementById("checkoutIframe").style.display = 'block';
  clickToPay.showCheckout();
});

// 3. Start loading in background
clickToPay.load();

// 4. Handle form submission
document.getElementById("submitButton").addEventListener("click", (e) => {
  e.preventDefault();

  const customer = {
    email: document.getElementById("email").value.trim(),
    first_name: document.getElementById("firstName").value.trim(),
    last_name: document.getElementById("lastName").value.trim(),
    phone: document.getElementById("phone").value.trim()
  };

  // Hide form, show loader
  document.getElementById("customerForm").style.display = "none";
  document.getElementById("loader").style.display = "flex";

  try {
    // Inject customer data to continue checkout
    clickToPay.injectCustomerData(customer);
  } catch (error) {
    // Show form again on error
    document.getElementById("customerForm").style.display = "block";
    document.getElementById("loader").style.display = "none";
    alert("An error occurred. Please try again.");
  }
});
```

#### HTML Structure
Here's the required HTML structure for the background loading example:

```html
<!-- Click to Pay iframe container -->
<div id="checkoutIframe"></div>

<!-- Customer information form -->
<form id="customerForm">
  <input type="email" id="email" placeholder="Email" required>
  <input type="text" id="firstName" placeholder="First Name" required>
  <input type="text" id="lastName" placeholder="Last Name" required>
  <input type="tel" id="phone" placeholder="Phone (with country code)" required>
  <button type="submit" id="submitButton">Continue to Checkout</button>
</form>

<!-- Loading indicator -->
<div id="loader" style="display: none;">
  Loading...
</div>
```

# Fraud prevention

The Fraud Prevention module allows you to add security layers to your payment workflows
by integrating with any of our underlying fraud prevention providers.

## Real time user behavior analysis

### Forter

One of Forter's key features is our ability to track the user's real-time behavior on
the site and use it to separate fraudsters from legitimate buyers. To take advantage
of Forter's technology, a JavaScript snippet needs to be placed on EVERY page
of your commerce site beginning with the homepage and up to and including the final
"Thank you for your purchase" page.

The integration is simple and straightforward - you only need to configure event
listeners and then instantiate a FraudPreventionService with your site configuration.

Additional setup is required in case your website uses Content Security Policies (CSP)

#### Forter: Code snippet

```html
<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Real time user behaviour anaylsis - Forter example</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <script src="https://widget.paydock.com/sdk/latest/widget.umd.js" ></script>
</head>

<body>
  <main>
    <h1>Real time user behaviour anaylsis - Forter example</h1>
    <div class="forter-test">
      <h2>Forter Integration</h2>

      <div class="status-card">
        <p>
          <strong>Integration Status:</strong>
          <span data-fraud-prevention="status-indicator" class="status pending">
            Pending
          </span>
        </p>
        <p>
          <strong>Token Value:</strong>
          <code data-fraud-prevention="forter-token">Not available</code>
        </p>
        <p data-fraud-prevention="error-container" style="display: none;">
          <strong>Error Code:</strong>
          <span data-fraud-prevention="error-code" class="error"></span>
        </p>
      </div>
    </div>
  </main>
  <script>
    const { FRAUD_PREVENTION_EVENTS, FraudPreventionService } = window.paydock

    let token = '';
    let errorCode = '';

    const render = () => {
      const statusIndicator = document.querySelector('[data-fraud-prevention="status-indicator"]');
      const tokenValue = document.querySelector('[data-fraud-prevention="forter-token"]');
      const errorContainer = document.querySelector('[data-fraud-prevention="error-container"]');
      const errorCodeElement = document.querySelector('[data-fraud-prevention="error-code"]');

      if (token) {
        statusIndicator.className = 'status success';
        statusIndicator.textContent = 'Active';
        tokenValue.textContent = token;
      } else {
        statusIndicator.className = 'status pending';
        statusIndicator.textContent = 'Pending';
        tokenValue.textContent = 'Not available';
      }

      if (errorCode) {
        errorCodeElement.textContent = errorCode;
        errorContainer.style.display = 'block';
      } else {
        errorContainer.style.display = 'none';
      }
    };

    document.addEventListener(FRAUD_PREVENTION_EVENTS.NAMESPACE, (event) => {
      switch (event.detail.type) {
        case FRAUD_PREVENTION_EVENTS.TYPES.FINTERPRINT_TOKEN_READY: {
          token = event.detail.payload.token;
          break;
        }
        case FRAUD_PREVENTION_EVENTS.TYPES.FINGERPRINT_TOKEN_ERROR: {
          errorCode = event.detail.payload.code;
          break;
        }
        default: {
          throw new Error(
            `${FRAUD_PREVENTION_EVENTS.NAMESPACE} emitted an unsupported event: ${JSON.stringify(event.detail)}.`,
          );
        }
      }

      render();
    });

    const fraudPreventionServiceConfig = {
      environmentId: 'sandbox',
      mode: 'test'
    }

    // Set "csp" to true if your website uses Content Security Policies
    const csp = false;

    new FraudPreventionService(fraudPreventionServiceConfig)
      .withForter({
        siteId: 'example_site_id_or_subsite_id',
        csp,
      });

    // new FraudPreventionService(fraudPreventionServiceConfig)
    //   .withAccessTokenStrategy("eyJhb_access_token_example_...")
    //   .withForter({
    //     providerId: environment.forter.serviceId,
    //     csp,
    //   });

    // new FraudPreventionService(fraudPreventionServiceConfig)
    //   .withPublicKeyStrategy("pk_example_...")
    //   .withForter({
    //     providerId: environment.forter.serviceId,
    //     csp,
    //   });
  </script>
</body>

</html>
```

#### Forter: Content Security Policies

If your site enforces Content Security Policies (CSP), make sure to:

1. Set the `csp` option to `true` when invoking `withForter` on your `FraudPreventionService` instance.
2. Allowlist Forter's domains on `connect-src`, `script-src` and `worker-src` as shown below.

```bash
connect-src https://*.forter.com wss://cdn0.forter.com https://d2o5idwacg3gyw.cloudfront.net https://dz8rit8v72mig.cloudfront.net https://db7q4jg5rkhk8.cloudfront.net https://1.1.1.1 https://d94qwxh6czci4.cloudfront.net https://dr6vcclmzwk74.cloudfront.net https://d6rak4b14t5gp.cloudfront.net https://d3k4bt74u9esq1.cloudfront.net https://d1ezzflfzltk6e.cloudfront.net https://d3nocrch4qti4v.cloudfront.net https://duuytoqss3gu4.cloudfront.net https://df45ay5pw60dy.cloudfront.net
script-src https://*.forter.com https://dlthst9q2beh8.cloudfront.net https://d2nww8zpyj5pk0.cloudfront.net https://d2w2nqfk3z9hdt.cloudfront.net
worker-src blob:
```
