npm package: @paypercut/checkout-js
Description: Lightweight JavaScript SDK for Paypercut Checkout
Author: Paypercut
Latest version: 1.0.21

# Paypercut Checkout JavaScript SDK

A lightweight, framework-agnostic JavaScript SDK for embedding Paypercut Checkout into your web application. Works seamlessly with vanilla JavaScript, TypeScript, React, Vue, Angular, and other modern frameworks.

[![npm version](https://img.shields.io/npm/v/@paypercut/checkout-js.svg)](https://www.npmjs.com/package/@paypercut/checkout-js)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@paypercut/checkout-js)](https://bundlephobia.com/package/@paypercut/checkout-js)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)

---

## Table of Contents

- [Features](#features)
- [How It Works](#how-it-works)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Events](#events)
- [Form Validation for Wallet Payments](#form-validation-for-wallet-payments)
- [Types](#types)
- [Usage Examples](#usage-examples)
  - [Vanilla JavaScript](#vanilla-javascript-cdn)
  - [TypeScript / ESM](#typescript--esm)
  - [React](#react)
- [Security](#security)
- [Troubleshooting](#troubleshooting)
- [Performance Optimization](#performance-optimization)
- [Best Practices](#best-practices)
- [FAQ](#faq)
- [Support](#support)

---

## Installation

### Via NPM

```bash
npm install @paypercut/checkout-js
```

### Via Yarn

```bash
yarn add @paypercut/checkout-js
```

### Via PNPM

```bash
pnpm add @paypercut/checkout-js
```

### Via CDN

```html
<!-- jsDelivr (recommended with SRI) -->
<script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.16/dist/paypercut-checkout.iife.min.js"
        integrity="sha384-..."
        crossorigin="anonymous"></script>

<!-- or UNPKG -->
<script src="https://unpkg.com/@paypercut/checkout-js@1.0.16/dist/paypercut-checkout.iife.min.js"></script>
```

---

## Quick Start

This returns a checkout session ID like `01KB23M9EC960C50G3AH14FTTT`.

### 1. Embed the Checkout

Use the checkout ID to initialize the checkout on your frontend:

```typescript
import { PaypercutCheckout } from '@paypercut/checkout-js';

// Initialize the checkout
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',              // Your checkout session ID from step 1
  containerId: '#checkout'       // CSS selector or HTMLElement
});

// Listen for payment events
checkout.on('success', () => {
  console.log('Payment successful!');
});

checkout.on('error', () => {
  console.error('Payment failed');
});

// Optional: Listen for when iframe finishes loading (useful for modal implementations)
checkout.on('loaded', () => {
  console.log('Checkout loaded and ready');
});

// Render the checkout
checkout.render();
```

That's it! The checkout is now embedded in your page.

### 2. Display Mode

The SDK supports one display mode:

| Mode | When to Use | Configuration |
|------|-------------|---------------|
| **Embedded** | Checkout is part of your page layout | Provide `containerId` |
---

## API Reference

### `PaypercutCheckout(options)`

Creates a new checkout instance.

#### Options

| Option           | Type | Required | Default | Description                                                                                                                |
|------------------|------|----------|---------|----------------------------------------------------------------------------------------------------------------------------|
| `id`             | `string` | Yes | — | Checkout session identifier (e.g., `01KB23M9EC960C50G3AH14FTTT`)                                                           |
| `containerId`    | `string \| HTMLElement` | Yes | — | CSS selector or element where iframe mounts                                                                                |
| `locale`         | `string` | No | `'auto'` | Locale for checkout UI. Options: `'auto'`, `'en'`, `'en-GB'`, `'bg'`, `'bg-BG'`                                            |
| `lang`           | `string` | No | `'auto'` | Locale for checkout UI. Options: `'auto'`, `'en'`, `'en-GB'`, `'bg'`, `'bg-BG'`                                            |
| `ui_mode`        | `'hosted' \| 'embedded'` | No | `'embedded'` | UI mode for checkout display                                                                                               |
| `wallet_options` | `string[]` | No | `['apple_pay', 'google_pay']` | Digital wallet options. Pass `[]` to disable all wallets                                                                   |
| `form_only`      | `boolean` | No | `false` | Show only payment form (no Pay Now button - use external button with `submit()`)                                           |
| `validate_form`      | `boolean` | No | `false` | This indicates that Google Pay/Apple Pay flow to proceed you need to confirm form validation. For EMBEDDED checkouts only) |
| `appearance`     | `object` | No | `undefined` | Appearance configuration for the checkout UI                                                                              |
| `appearance.preset` | `'modal' \| 'inline'` | No | `undefined` | Controls checkout layout preset. `'modal'` shows the order summary (TotalCard) and header inside the iframe — use when rendering in a modal/dialog. `'inline'` hides the TotalCard — use when the merchant renders their own order summary outside the checkout. |

#### Examples

**Basic initialization:**

```typescript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-container'
});
```

**With all options:**

```typescript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-container',
  locale: 'en',                              // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
  lang: 'en',                              // 'auto' | 'en' | 'en-GB' | 'bg' | 'bg-BG'
  ui_mode: 'embedded',                       // 'hosted' | 'embedded'
  wallet_options: ['apple_pay', 'google_pay'], // Can be empty array [] or contain one/both options
  form_only: false,                          // Set true to hide Pay Now button (use external button),
  validate_form: true,            // Set true to require form validation before wallet payment
  appearance: {
    preset: 'modal',              // 'modal' | 'inline' — 'modal' shows TotalCard + header, 'inline' hides them
  },
});
```

**Disable wallet payments:**

```typescript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-container',
  wallet_options: []  // No Apple Pay or Google Pay buttons
});
```

**Only Apple Pay:**

```typescript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-container',
  wallet_options: ['apple_pay']  // Only Apple Pay, no Google Pay
});
```

**Form-only mode (external submit button):**

```typescript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-container',
  form_only: true  // No Pay Now button inside checkout
});

// Use your own button to trigger payment
document.getElementById('my-pay-button').addEventListener('click', () => {
  checkout.submit();
});
```

### Instance Methods

#### `render()`

Mounts and displays the checkout iframe.

```typescript
checkout.render();
```

#### `destroy()`

Destroys the instance and cleans up all event listeners. Call this when you're done with the checkout instance.

```typescript
checkout.destroy();
```

#### `on(event, handler)`

Subscribes to checkout events. Returns an unsubscribe function.

```typescript
const unsubscribe = checkout.on('success', () => {
  console.log('Payment successful!');
});

// Later, to unsubscribe
unsubscribe();
```

#### `once(event, handler)`

Subscribes to a checkout event that automatically unsubscribes after the first emission. Returns an unsubscribe function.

```typescript
checkout.once('loaded', () => {
  console.log('Checkout loaded - this will only fire once');
});
```

#### `off(event, handler)`

Unsubscribes from checkout events.

```typescript
const handler = () => console.log('Payment successful!');
checkout.on('success', handler);
checkout.off('success', handler);
```

#### `isMounted()`

Returns whether the checkout is currently mounted.

```typescript
if (checkout.isMounted()) {
  console.log('Checkout is visible');
}
```

---

## Events

Subscribe to events using the `on()` method. You can use string event names or the `SdkEvent` enum.

### Event Reference

| Event | Enum | Description | Payload |
|-------|------|-------------|---------|
| `loaded` | `SdkEvent.Loaded` | Checkout iframe has finished loading | `void` |
| `success` | `SdkEvent.Success` | Payment completed successfully | `PaymentSuccessPayload` |
| `error` | `SdkEvent.Error` | Terminal failure (tokenize, confirm, or 3DS) | `ApiErrorPayload` |
| `expired` | `SdkEvent.Expired` | Checkout session expired | `void` |
| `threeds_started` | `SdkEvent.ThreeDSStarted` | 3DS challenge flow started | `object` |
| `threeds_complete` | `SdkEvent.ThreeDSComplete` | 3DS challenge completed | `object` |
| `threeds_canceled` | `SdkEvent.ThreeDSCanceled` | 3DS challenge canceled by user | `object` |
| `threeds_error` | `SdkEvent.ThreeDSError` | 3DS challenge error | `ApiErrorPayload` or `object` |

### Usage Examples

**Using string event names:**

```ts
const checkout = PaypercutCheckout({ id: '01KB23M9EC960C50G3AH14FTTT', containerId: '#checkout' });

checkout.on('loaded', () => {
  console.log('Checkout loaded');
});

checkout.on('success', (payload) => {
  // PaymentSuccessPayload
  console.log('Payment successful');
  console.log('Card brand:', payload.payment_method.brand);
  console.log('Last 4:', payload.payment_method.last4);
  console.log('Expiry:', payload.payment_method.exp_month + '/' + payload.payment_method.exp_year);
});

checkout.on('error', (err) => {
  console.error('Payment error:', err.code, err.message);
});

checkout.on('expired', () => {
  console.warn('Checkout session expired');
});
```

**Using SdkEvent enum (recommended for TypeScript):**

```ts
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';

const checkout = PaypercutCheckout({ id: '01KB23M9EC960C50G3AH14FTTT', containerId: '#checkout' });

checkout.on(SdkEvent.Loaded, () => {
  console.log('Checkout loaded');
});

checkout.on(SdkEvent.Success, (payload) => {
  console.log('Payment successful', payload.payment_method);
});

checkout.on(SdkEvent.Error, (err) => {
  console.error('Payment error:', err.code, err.message);
});

checkout.on(SdkEvent.Expired, () => {
  console.warn('Checkout session expired');
});

// 3DS events
checkout.on(SdkEvent.ThreeDSStarted, (ctx) => {
  console.log('3DS challenge started');
});

checkout.on(SdkEvent.ThreeDSComplete, (payload) => {
  console.log('3DS completed');
});

checkout.on(SdkEvent.ThreeDSCanceled, (payload) => {
  console.log('3DS canceled by user');
});

checkout.on(SdkEvent.ThreeDSError, (err) => {
  console.error('3DS error:', err);
});
```

### Success Payload

The `success` event returns a `PaymentSuccessPayload` with the payment method details:

```ts
type PaymentSuccessPayload = {
  payment_method: {
    brand: string;      // e.g., 'visa', 'mastercard', 'amex'
    last4: string;      // Last 4 digits of card number
    exp_month: number;  // Expiration month (1-12)
    exp_year: number;   // Expiration year (e.g., 2030)
  };
};
```

**Example:**

```ts
checkout.on('success', (payload) => {
  // payload:
  // {
  //   payment_method: {
  //     brand: 'visa',
  //     last4: '4242',
  //     exp_month: 12,
  //     exp_year: 2030
  //   }
  // }

  console.log(`Paid with ${payload.payment_method.brand} ending in ${payload.payment_method.last4}`);
  // Output: "Paid with visa ending in 4242"
});
```

### Error Handling

```ts
checkout.on('error', (err) => {
  switch (err.code) {
    case 'card_validation_error':
      // err.errors[] has field-level issues. Messages are localized.
      break;
    case 'card_declined':
      // Optional err.decline_code and user-friendly err.message
      break;
    case 'threeds_error':
    case 'threeds_authentication_failed':
    case 'threeds_canceled':
      // 3DS issue. Some servers use status_code 424 with a detailed message.
      break;
    default:
      // Other terminal errors (e.g., authentication_failed, session_expired)
      break;
  }
});
```

### Notes

- `error`: the SDK forwards a normalized `ApiErrorPayload` when provided by Hosted Checkout.
- `threeds_error`: forwards `payload.error` when available; otherwise the raw message data.
- `three![img.png](img.png)ds_*` non-error events: payload is forwarded as-is from Hosted Checkout (shape may evolve).

---

## Form Validation for Wallet Payments


### How It Works

1. User clicks Apple Pay or Google Pay button in the checkout
2. SDK emits `form_validation` event to your code
3. You validate your form and call either:
   - `completeFormValidation(wallet)` - allows wallet to proceed
   - `failFormValidation(wallet, errors)` - blocks wallet, you show your own errors
4. If validation passes, the wallet payment sheet opens

### Event: `form_validation`

| Property | Type | Description |
|----------|------|-------------|
| `checkoutId` | `string` | The checkout session ID |
| `wallet` | `'apple_pay' \| 'google_pay'` | Which wallet button was clicked |

### Methods

#### `completeFormValidation(wallet)`

Call this when your form is valid. The wallet payment sheet will open.

```typescript
checkout.completeFormValidation('google_pay');
```

#### `failFormValidation(wallet, errors?)`

Call this when your form has errors. The wallet will not open, and you should display your own error messages.

```typescript
checkout.failFormValidation('apple_pay', [
  { code: 'invalid_email', message: 'Please enter a valid email address' },
  { code: 'missing_address', message: 'Shipping address is required' }
]);
```

### Basic Example

```typescript
import { PaypercutCheckout, SdkEvent } from '@paypercut/checkout-js';

const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout',
  wallet_options: ['apple_pay', 'google_pay']
});

// Handle form validation for wallet payments
checkout.on(SdkEvent.FormValidation, ({ wallet, checkoutId }) => {
  // Validate your own form fields
  const email = document.getElementById('email').value;
  const isEmailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  if (isEmailValid) {
    // Form is valid - allow wallet to proceed
    checkout.completeFormValidation(wallet);
  } else {
    // Form has errors - block wallet and show your errors
    document.getElementById('email-error').textContent = 'Please enter a valid email';
    checkout.failFormValidation(wallet, [
      { code: 'invalid_email', message: 'Please enter a valid email' }
    ]);
  }
});

checkout.render();
```

### React Example

```tsx
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance, SdkEvent } from '@paypercut/checkout-js';

export function CheckoutWithForm({ checkoutId }: { checkoutId: string }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const checkoutRef = useRef<CheckoutInstance | null>(null);
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');

  useEffect(() => {
    if (!containerRef.current) return;

    const checkout = PaypercutCheckout({
      id: checkoutId,
      containerId: containerRef.current,
      ui_mode: 'embedded',
      wallet_options: ['apple_pay', 'google_pay'],
      validate_form: true
    });

    // Handle wallet form validation
    checkout.on(SdkEvent.FormValidation, ({ wallet }) => {
      const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

      if (isValid) {
        setEmailError('');
        checkout.completeFormValidation(wallet);
      } else {
        setEmailError('Please enter a valid email address');
        checkout.failFormValidation(wallet, [
          { code: 'invalid_email', message: 'Invalid email' }
        ]);
      }
    });

    checkout.on('success', (payload) => {
      console.log('Payment successful:', payload.payment_method);
    });

    checkout.render();
    checkoutRef.current = checkout;

    return () => checkout.destroy();
  }, [checkoutId, email]);

  return (
    <div>
      {/* Your custom form fields */}
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="your@email.com"
        />
        {emailError && <span style={{ color: 'red' }}>{emailError}</span>}
      </div>

      {/* Checkout iframe */}
      <div ref={containerRef} />
    </div>
  );
}
```

### Timeout Behavior

If you don't respond to the `form_validation` event within 10 seconds, the SDK will:
1. Emit an `error` event with code `form_validation_timeout`
2. Block the wallet payment from starting

This ensures the checkout doesn't hang indefinitely if the handler is not implemented.

### When to Use

Use form validation when you have:
- Custom email/phone fields outside the checkout iframe
- Shipping address forms that must be filled before payment
- Terms & conditions checkboxes that must be accepted
- Any other merchant-side validation requirements

If you don't need to validate anything, you can simply auto-approve:

```typescript
checkout.on('form_validation', ({ wallet }) => {
  // No validation needed - always allow wallet to proceed
  checkout.completeFormValidation(wallet);
});
```

---

## Types

### ApiErrorPayload

The SDK forwards the error payload provided by Hosted Checkout. Common shapes include:

```ts
// Card validation error with per-field details (messages are localized)
type CardField = 'card_number' | 'expiration_date' | 'cvc' | 'cardholder_name';

type CardValidationErrorCode =
  | 'invalid_number' | 'incomplete_number'
  | 'invalid_expiry' | 'incomplete_expiry'
  | 'invalid_cvc' | 'incomplete_cvc'
  | 'required';

type VendorValidationType = 'invalid' | 'incomplete' | 'required';

type CardValidationErrorItem = {
  field: CardField;
  code: CardValidationErrorCode;
  message: string; // localized per current checkout locale
  vendor_type: VendorValidationType;
};

type ApiErrorPayload =
  | {
      checkoutId: string;
      code: 'card_validation_error';
      message: string; // e.g., 'Card details are incomplete or invalid'
      status_code: number; // typically 400
      timestamp: string; // ISO8601
      errors: CardValidationErrorItem[];
    }
  | {
      code:
        | 'threeds_error'
        | 'threeds_authentication_failed'
        | 'threeds_canceled'
        | 'card_declined'
        | 'authentication_failed'
        | 'session_expired'
        | string; // other server codes may appear, e.g. 'permission_denied'
      type?: 'card_error' | string;
      message?: string;
      status_code?: number; // some sources use `status`
      status?: number;      // compatibility with some payloads
      decline_code?: string; // Optional decline reason
      trace_id?: string;
      timestamp?: string; // ISO8601
      [key: string]: any; // passthrough
    };
```

#### Error payload examples

```json
{
  "checkoutId": "01KB23M9EC960C50G3AH14FTTT",
  "code": "card_validation_error",
  "message": "Card details are incomplete or invalid",
  "status_code": 400,
  "timestamp": "2025-01-01T12:00:00.000Z",
  "errors": [
    { "field": "card_number", "code": "invalid_number", "message": "Card number is invalid", "vendor_type": "invalid" },
    { "field": "expiration_date", "code": "incomplete_expiry", "message": "Incomplete date", "vendor_type": "incomplete" }
  ]
}
```

```json
{
  "checkoutId": "01KA8EG3KA2A61YXX4XVD4FYPT",
  "code": "permission_denied",
  "message": "forbidden",
  "status_code": 403,
  "trace_id": "00761104aff14f33adb84d7437d7e320",
  "timestamp": "2025-12-02T09:31:22.779Z"
}
```

```json
{
  "checkoutId": "01KA8EG3KA2A61YXX4XVD4FYPT",
  "code": "threeds_error",
  "type": "card_error",
  "message": "Card authentication failed.",
  "status": 400,
  "timestamp": "2025-12-02T09:31:44.250Z"
}
```

> Note: Some payloads provide `status_code` while others provide `status`. Treat them interchangeably.

#### Error codes explained

- authentication_failed — General authentication failure during confirm or 3DS. Usually recoverable by retrying or using another card.
- card_declined — Issuer declined the card. Optional `decline_code` may be present (e.g., `insufficient_funds`).
- wallet_authentication_canceled — User canceled Apple/Google Pay authentication sheet.
- wallet_not_available — Apple/Google Pay not available on the device/browser.
- card_validation_error — Client-side validation of card fields failed. `errors[]` contains localized, per-field issues.
- threeds_error — 3DS flow encountered an error (e.g., ACS unavailable). Often accompanied by a server message.
- threeds_authentication_failed — 3DS authentication failed (e.g., challenged and consumer failed/denied).
- threeds_canceled — Customer canceled the 3DS challenge.
- session_expired — Checkout session is no longer valid; create a new session.
- permission_denied — Server responded with permission error (e.g., session not allowed). Not enumerated above; SDK forwards unknown codes unchanged.


Notes:
- For 3DS issues, you may receive `threeds_error` with `status_code` 424 and a server-provided message.
- For declines, expect `code: 'card_declined'`, optional `decline_code`, and a user-friendly `message`.
- The validation error `message` fields are already localized based on the checkout’s locale.

### 3DS payloads (representative samples)

The SDK forwards whatever Hosted Checkout sends for 3DS events. Shapes may evolve.

```ts
// threeds_started
{
  type: 'THREEDS_START_FLOW',
  checkoutId: '01KB23M9EC960C50G3AH14FTTT',
  // additional context as provided by Hosted Checkout (e.g., method, acsUrl)
}

// threeds_complete
{
  type: 'THREEDS_COMPLETE',
  checkoutId: '01KB23M9EC960C50G3AH14FTTT',
  // authentication result context (e.g., status: 'Y' | 'A' | 'C' | 'D')
}

// threeds_canceled
{
  type: 'THREEDS_CANCELED',
  checkoutId: '01KB23M9EC960C50G3AH14FTTT',
}

// threeds_error (non-terminal)
// Note: terminal failures will also be emitted on `error` with ApiErrorPayload
{
  type: 'THREEDS_ERROR',
  checkoutId: '01KB23M9EC960C50G3AH14FTTT',
  error?: ApiErrorPayload,
}
```

Tip: Prefer subscribing with the SdkEvent enum for stronger typing.


## Usage Examples

### Vanilla JavaScript (CDN)

```html
<!DOCTYPE html>
<html>
<head>
  <title>Paypercut Checkout</title>
</head>
<body>
  <div id="checkout"></div>

  <script src="https://cdn.jsdelivr.net/npm/@paypercut/checkout-js@1.0.16/dist/paypercut-checkout.iife.min.js"></script>
  <script>
    const checkout = PaypercutCheckout({
      id: '01KB23M9EC960C50G3AH14FTTT',
      containerId: '#checkout',
      locale: 'en',
      ui_mode: 'embedded',
      wallet_options: ['apple_pay', 'google_pay']
    });

    checkout.on('success', function(payload) {
      // payload.payment_method: { brand, last4, exp_month, exp_year }
      alert('Payment successful with ' + payload.payment_method.brand + ' ending in ' + payload.payment_method.last4);
    });

    checkout.on('error', function(error) {
      alert('Payment failed: ' + error.message);
    });

    checkout.render();
  </script>
</body>
</html>
```

### TypeScript / ESM

```typescript
import { PaypercutCheckout } from '@paypercut/checkout-js';

const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout',
  locale: 'auto',
  ui_mode: 'embedded',
  wallet_options: ['apple_pay', 'google_pay'],
  form_only: false
});

checkout.on('success', (payload) => {
  // payload.payment_method: { brand, last4, exp_month, exp_year }
  console.log('Payment successful');
  console.log(`Paid with ${payload.payment_method.brand} **** ${payload.payment_method.last4}`);
  // Redirect to success page
  window.location.href = '/success';
});

checkout.on('error', (error) => {
  console.error('Payment error:', error);
  // Show error message to user
  alert(`Payment failed: ${error.message}`);
});

checkout.render();
```

### React

```tsx
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js';

interface PaymentMethod {
  brand: string;
  last4: string;
  exp_month: number;
  exp_year: number;
}

export function CheckoutComponent({ checkoutId }: { checkoutId: string }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const checkoutRef = useRef<CheckoutInstance | null>(null);
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [paymentMethod, setPaymentMethod] = useState<PaymentMethod | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const checkout = PaypercutCheckout({
      id: checkoutId,
      containerId: containerRef.current,
      locale: 'auto',
      ui_mode: 'embedded',
      wallet_options: ['apple_pay', 'google_pay'],
      form_only: false
    });

    checkout.on('loaded', () => {
      setStatus('idle');
      console.log('Checkout loaded');
    });

    checkout.on('success', (payload) => {
      setStatus('success');
      setPaymentMethod(payload.payment_method);
      console.log('Payment successful:', payload.payment_method);
    });

    checkout.on('error', (error) => {
      setStatus('error');
      console.error('Payment error:', error);
    });

    checkout.render();
    checkoutRef.current = checkout;

    return () => {
      checkout.destroy();
    };
  }, [checkoutId]);

  return (
    <div>
      <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
      {status === 'loading' && <p>Processing payment...</p>}
      {status === 'success' && paymentMethod && (
        <p>✅ Payment successful with {paymentMethod.brand} ending in {paymentMethod.last4}!</p>
      )}
      {status === 'error' && <p>❌ Payment failed. Please try again.</p>}
    </div>
  );
}
```

### Modal-like Implementation

If you want to create a modal-like experience, you can use the `loaded` event to show/hide a loading overlay:

```tsx
import { useEffect, useRef, useState } from 'react';
import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js';

export function ModalCheckout({ checkoutId, onClose }: { checkoutId: string; onClose: () => void }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (!containerRef.current) return;

    const checkout = PaypercutCheckout({
      id: checkoutId,
      containerId: containerRef.current
    });

    checkout.on('loaded', () => {
      setIsLoading(false);
    });

    checkout.on('success', () => {
      console.log('Payment successful');
      onClose();
    });

    checkout.on('error', (error) => {
      console.error('Payment error:', error);
    });

    checkout.render();

    return () => {
      checkout.destroy();
    };
  }, [checkoutId, onClose]);

  return (
    <div style={{
      position: 'fixed',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      backgroundColor: 'rgba(0, 0, 0, 0.5)',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      zIndex: 9999
    }}>
      <div style={{
        position: 'relative',
        width: '90%',
        maxWidth: '500px',
        height: '90%',
        maxHeight: '700px',
        backgroundColor: '#fff',
        borderRadius: '12px',
        overflow: 'hidden'
      }}>
        {isLoading && (
          <div style={{
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            backgroundColor: '#fff',
            zIndex: 1
          }}>
            <p>Loading checkout...</p>
          </div>
        )}
        <div ref={containerRef} style={{ width: '100%', height: '100%' }} />
        <button
          onClick={onClose}
          style={{
            position: 'absolute',
            top: '10px',
            right: '10px',
            zIndex: 2
          }}
        >
          ✕
        </button>
      </div>
    </div>
  );
}
```

## Container Styling

The SDK automatically resizes the iframe to match the checkout content height. To ensure proper display, style your container with `max-width` and `max-height` constraints rather than fixed dimensions.

### Recommended Container Styles

```css
.checkout-container {
  /* Use max-width/max-height to constrain the container */
  max-width: 450px;
  max-height: 600px;
  overflow: hidden;
  overflow-y: auto;
  scroll-behavior: smooth;
}

/* The mount point should be full width with no fixed height */
#checkout-mount {
  width: 100%;
  /* Height is set dynamically by SDK via resize messages */
  overflow: hidden;
  position: relative;
}
```

### Key Points

1. **Avoid fixed `height` or `min-height`** on the container - the SDK will resize the iframe to fit the checkout content
2. **Use `max-height`** if you want to limit the container size and enable scrolling for longer content
3. **Use `max-width`** to constrain the container width (recommended: 375px–450px for optimal checkout display)
4. **Set `overflow-y: auto`** on the container if using `max-height` to enable scrolling

### Example HTML

```html
<div class="checkout-container">
  <div id="checkout-mount"></div>
</div>
```

```javascript
const checkout = PaypercutCheckout({
  id: '01KB23M9EC960C50G3AH14FTTT',
  containerId: '#checkout-mount'
});
checkout.render();
```

---

## Security

### Origin Validation

The SDK automatically validates all postMessage communications against the configured `checkoutHost` origin. This prevents unauthorized messages from being processed.

### Content Security Policy

For enhanced security, configure your Content Security Policy headers:

```
Content-Security-Policy:
  frame-src https://buy.paypercut.io OR something else;
  script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
  connect-src https://buy.paypercut.io  OR something else;
```

### HTTPS Only

Always use HTTPS in production. The SDK is designed for secure communication over HTTPS.

## Troubleshooting

### Checkout not displaying

**Problem:** The iframe doesn't appear after calling `render()`.

**Solutions:**
- Ensure the container element exists in the DOM before calling `render()`
- Check that the `id` is a valid checkout session ID
- Verify there are no CSP errors in the browser console
- Enable debug mode: `debug: true` to see detailed logs

### Events not firing

**Problem:** Event handlers are not being called.

**Solutions:**
- Ensure you subscribe to events before calling `render()`
- Check that the checkout session is active and not expired
- Enable debug mode to see message logs

### TypeScript errors

**Problem:** TypeScript shows type errors when using the SDK.

**Solutions:**
- Ensure you're importing types: `import { PaypercutCheckout, CheckoutInstance } from '@paypercut/checkout-js'`
- Update to the latest version of the SDK
- Check that your `tsconfig.json` includes the SDK's type definitions

## Performance Optimization

### Preconnect to Checkout Host

Add DNS prefetch and preconnect hints to your HTML for faster loading:

```html
<link rel="preconnect" href="https://buy.paypercut.io ">
<link rel="dns-prefetch" href="https://buy.paypercut.io ">
```

### Lazy Loading

Load the SDK only when needed to reduce initial bundle size:

```typescript
async function loadCheckout() {
  const { PaypercutCheckout } = await import('@paypercut/checkout-js');
  return PaypercutCheckout;
}

// Load when user clicks pay button
payButton.addEventListener('click', async () => {
  const PaypercutCheckout = await loadCheckout();
  const checkout = PaypercutCheckout({ id: '01KB23M9EC960C50G3AH14FTTT' });
  checkout.render();
});
```

---

## Best Practices

### 1. Always Verify Payments on Your Backend

Never rely solely on frontend events for payment confirmation. Always verify payments using webhooks on your backend:

```typescript
// ✅ Good: Use frontend events for UI updates only
checkout.on('success', () => {
  // Show success message
  showSuccessMessage();
  // Redirect to order confirmation page
  window.location.href = '/orders/confirmation';
});

// ❌ Bad: Don't grant access based on frontend events alone
checkout.on('success', () => {
  // This can be manipulated by users!
  grantPremiumAccess(userId); // Don't do this!
});
```

### 2. Handle All Event Types

Always handle both success and error events:

```typescript
checkout.on('success', () => {
  // Handle success
  showSuccessMessage();
});

checkout.on('error', () => {
  // Show user-friendly error message
  alert('Payment failed. Please try again.');
});
```

### 3. Clean Up on Component Unmount

Always call `destroy()` when your component unmounts to prevent memory leaks:

```typescript
// React example
useEffect(() => {
  const checkout = PaypercutCheckout({ id: checkoutId });
  checkout.render();

  return () => {
    checkout.destroy(); // Clean up!
  };
}, []);
```

## FAQ For Internal Validation

**Q: How do I handle successful payments?**
Listen to the `success` event and redirect users or update your UI accordingly. Always verify the payment on your backend using webhooks.

**Q: Can I use this with server-side rendering (SSR)?**
Yes, but ensure the SDK is only initialized on the client side. For Next.js, use dynamic imports with `ssr: false`.

**Q: What happens if the user closes the browser during payment?**

**Q: Can I have multiple checkouts on one page?**
Maybe Yes, but only one should be active at a time to avoid user confusion.
If no, we destroy the previous one and create a new one.

**Q: How do I handle errors?**
Listen to the `error` event and display user-friendly error messages. Log errors for debugging.