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