npm package: @ss-dev/agnostic-checkout
Description: Provider-agnostic, embeddable React checkout component with dynamic theming and plugin architecture
Latest version: 1.2.0

# ⚡ Agnostic Checkout SDK

A provider-agnostic, embeddable React checkout component with dynamic theming, plugin architecture, and full TypeScript support.

---

## Features

- 🛒 **Embeddable checkout component** — drop into any React app
- 🎨 **Theme system** — 5 built-in presets (`light`, `dark`, `minimal`, `corporate`, `neon`)
- 🎯 **Brand Auto Theme** — generate a full theme from a single brand color
- 🔌 **Plugin architecture** — extend checkout behavior without modifying core code
- 🏷️ **Discount Plugin** — coupon input, apply/remove with event-driven updates
- ✏️ **Cart Edit Plugin** — quantity stepper, item removal with configurable limits
- 🌍 **i18n** — built-in locales (`en`, `es`, `pt-BR`, `auto`)
- 💱 **Multi-currency** — 25+ currencies with automatic formatting
- 📱 **Responsive** — mobile-first layout with collapsible order summary
- 🔒 **Provider-agnostic** — works with any payment backend

---

## Installation

```bash
npm install @ss-dev/agnostic-checkout
```

---

## Quick Start

```tsx
import { Checkout } from "@ss-dev/agnostic-checkout";

function App() {
  const items = [
    {
      id: "item_1",
      name: "Pro Plan",
      unitPrice: 49.00,
      quantity: 1,
      total: 49.00,
      image: "https://example.com/pro-plan.png",
    },
  ];

  const totals = {
    subtotal: 49.00,
    taxes: 4.90,
    total: 53.90,
  };

  const paymentMethods = [
    {
      id: "pm_card",
      label: "Credit Card",
      type: "card",
    },
  ];

  return (
    <Checkout
      licenseKey="your_license_key" // Required for plugins
      items={items}
      totals={totals}
      paymentMethods={paymentMethods}
      provider={yourPaymentProvider}
      currency="USD"
      locale="en"
    />
  );
}
```

> [!NOTE]
> While the SDK core is free to use, premium features like **Plugins** and **Themes** require a valid license key.

| Prop | Description |
|---|---|
| `items` | Array of cart items to display |
| `totals` | Object with subtotal, taxes, shipping, discount, and total |
| `paymentMethods` | Available payment methods |
| `provider` | Your `PaymentProvider` implementation |
| `currency` | Currency code for formatting (`"USD"`, `"EUR"`, etc.) |
| `locale` | Language for UI text (`"en"`, `"es"`, `"pt-BR"`, `"auto"`) |

---

## Theming

The checkout uses CSS variables internally and supports three theming strategies with the following priority chain:

```
customTheme (overrides)
       ↓
brandColor (auto-generated theme)
       ↓
theme preset ("light" | "dark" | ...)
```

### Preset Themes

Choose from 5 built-in themes:

```tsx
<Checkout theme="light" />
<Checkout theme="dark" />
<Checkout theme="minimal" />
<Checkout theme="corporate" />
<Checkout theme="neon" />
```

### Brand Auto Theme

Generate a complete theme from a single brand color. The SDK automatically derives background, surface, text, border, and accent colors:

```tsx
<Checkout brandColor="#6366F1" />
```

### Custom Theme Overrides

Override specific tokens on top of any base theme:

```tsx
<Checkout
  theme="light"
  customTheme={{
    colors: {
      primary: "#ff0055",
      success: "#00cc88",
    },
    radius: "1rem",
  }}
/>
```

Combine with `brandColor`:

```tsx
<Checkout
  brandColor="#6366F1"
  customTheme={{
    radius: "20px",
    fontFamily: "'Poppins', sans-serif",
  }}
/>
```

### Theme Tokens

| Token | CSS Variable | Description |
|---|---|---|
| `colors.primary` | `--color-primary` | Buttons, links, active states |
| `colors.background` | `--color-background` | Page background |
| `colors.surface` | `--color-surface` | Card/panel backgrounds |
| `colors.text` | `--color-text` | Primary text color |
| `colors.border` | `--color-border` | Borders and dividers |
| `colors.success` | `--color-success` | Success states, discounts |
| `colors.error` | `--color-error` | Error states, remove actions |
| `radius` | `--theme-radius` | Border radius for cards/buttons |
| `fontFamily` | `--theme-font` | Font family stack |

---

## Plugin System

Plugins extend checkout functionality through a **slot-based architecture**. Plugins inject UI into predefined areas and communicate via events — they never modify state directly.

> [!IMPORTANT]
> A valid **Pro Plan** license key is required for plugins to be displayed.

```tsx
import { createDiscountPlugin, createCartEditPlugin } from "@ss-dev/agnostic-checkout/plugins";

<Checkout
  plugins={[
    createDiscountPlugin(handleEvent),
    createCartEditPlugin(handleEvent, { allowRemove: true }),
  ]}
  onEvent={handleEvent}
/>
```

Plugins emit events. Your `onEvent` handler processes them and updates `items`/`totals` externally:

```tsx
const handleEvent = (event) => {
  switch (event.type) {
    case "COUPON_APPLY_REQUESTED":
      // Validate event.code, update items/totals
      break;
    case "ITEM_REMOVE_REQUESTED":
      // Remove item by event.itemId
      break;
    case "ITEM_QUANTITY_UPDATE_REQUESTED":
      // Update quantity for event.itemId to event.quantity
      break;
  }
};
```

---

## Discount Plugin

Adds a coupon input field to the order summary. Users can type a discount code, apply it, and remove it via a trash icon.

```tsx
import { createDiscountPlugin } from "@ss-dev/agnostic-checkout/plugins";

const plugins = [
  createDiscountPlugin(handleEvent),
];
```

**Events emitted:**

| Event | Payload | Description |
|---|---|---|
| `COUPON_APPLY_REQUESTED` | `{ code: string }` | User submitted a coupon code |
| `COUPON_REMOVE_REQUESTED` | — | User clicked the remove button |

The plugin does **not** calculate discounts. Your `onEvent` handler validates the code and updates `items`/`totals` with the new discount values.

---

## Cart Edit Plugin

Allows users to modify item quantities and/or remove items from the cart.

```tsx
import { createCartEditPlugin } from "@ss-dev/agnostic-checkout/plugins";

const plugins = [
  createCartEditPlugin(handleEvent, {
    allowRemove: true,
    allowQuantityEdit: true,
    minQuantity: 1,
    maxQuantity: 10,
  }),
];
```

### Options

| Option | Type | Default | Description |
|---|---|---|---|
| `allowRemove` | `boolean` | `false` | Show remove button per item |
| `allowQuantityEdit` | `boolean` | `false` | Show quantity stepper per item |
| `minQuantity` | `number` | `1` | Minimum allowed quantity |
| `maxQuantity` | `number` | `99` | Maximum allowed quantity |

**Events emitted:**

| Event | Payload | Description |
|---|---|---|
| `ITEM_QUANTITY_UPDATE_REQUESTED` | `{ itemId, quantity }` | User changed item quantity |
| `ITEM_REMOVE_REQUESTED` | `{ itemId }` | User clicked remove |

---

## Payment Provider

The checkout is payment-agnostic. Implement the `PaymentProvider` interface to connect any backend:

```tsx
interface PaymentProvider {
  initialize?(config: unknown): Promise<void>;
  createPayment(data: unknown): Promise<PaymentResult>;
  confirmPayment?(data: unknown): Promise<PaymentResult>;
}
```

```tsx
class StripeProvider implements PaymentProvider {
  async createPayment(data) {
    const response = await fetch("/api/payments", {
      method: "POST",
      body: JSON.stringify(data),
    });
    return { status: "success" };
  }
}

<Checkout provider={new StripeProvider()} />
```

### Action Flow (`requires_action`)

The checkout supports an intrinsic action lifecycle directly managed by the state machine to handle things like 3D Secure, OTP validation, or redirect-based payments. 

When your `PaymentProvider` returns `{ status: "requires_action", action: { type: "otp" } }`, the SDK will **automatically**:
1. Enter a strict "requires_action" internal step.
2. Dim the entire screen and open a full-screen, unskippable Action Overlay modal.
3. Automatically lock background scrolling and trap focus to the modal.
4. Emit the `ACTION_STARTED` event with the action payload to your `onEvent` listener.
5. Provide built-in timeout protections (a safety countdown out-of-the-box if the integration halts).
6. Forward the resolution back into the global checkout flow once completed (`resume({ status: 'success' })`), emitting `ACTION_COMPLETED`.

To render your custom Action UI component inside this secure overlay context, provide a `renderAction` callback to the `<Checkout />`:

```tsx
<Checkout
  renderAction={({ action, resume }) => {
    if (action.type === "otp") return <OtpValidator onSubmit={() => resume({ status: "success" })} />;
    if (action.type === "redirect") {
      window.location.href = action.url;
      return <p>Redirecting to your bank...</p>;
    }
  }}
  onEvent={(event) => {
    if (event.type === "ACTION_STARTED") console.log(event.action);
    if (event.type === "ACTION_COMPLETED") console.log(event.result);
  }}
/>
```

---

## API Reference

### `<Checkout />` Props

| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| `items` | `CheckoutItem[]` | ✅ | — | Cart items |
| `totals` | `CheckoutTotals` | ✅ | — | Order totals |
| `paymentMethods` | `PaymentMethod[]` | ✅ | — | Available payment methods |
| `provider` | `PaymentProvider` | ✅ | — | Payment provider implementation |
| `currency` | `SupportedCurrency` | ✅ | — | Currency code |
| `locale` | `CheckoutLocale` | — | `"es"` | UI language |
| `theme` | `CheckoutThemeName \| Partial<CheckoutTheme>` | — | `"light"` | Theme preset or partial theme object |
| `brandColor` | `string` | — | — | Auto-generate theme from hex color |
| `customTheme` | `Partial<CheckoutTheme>` | — | — | Override specific theme tokens |
| `onEvent` | `EventHandler` | — | — | Callback for checkout events |
| `plugins` | `CheckoutPlugin[]` | — | `[]` | Plugins to extend functionality |
| `messages` | `Partial<CheckoutMessages>` | — | — | Override default UI text |
| `formatters` | `CheckoutFormatters` | — | — | Custom currency formatter |
| `devTools` | `boolean` | — | `false` | Show dev tools panel |
| `licenseKey` | `string` | — | — | License key to enable premium plugins |
| `initialState` | `PaymentResult` | — | — | Start in a specific state |
| `renderAction` | `(props) => ReactNode` | — | — | **Required** to display UI for `requires_action` states. If omitted, the overlay will not render. |

### `CheckoutItem`

```tsx
interface CheckoutItem {
  id: string;
  name: string;
  quantity: number;
  unitPrice: number;
  total: number;
  image?: string;
  discount?: {
    type: "percentage" | "fixed";
    value: number;
    label?: string;
  };
}
```

### `CheckoutTotals`

```tsx
interface CheckoutTotals {
  subtotal: number;
  discount?: number;
  taxes?: number;
  shipping?: number;
  total: number;
}
```

### Events

| Event | Description |
|---|---|
| `CHECKOUT_VIEWED` | Checkout component mounted |
| `STEP_CHANGED` | User navigated to a new step |
| `PAYMENT_METHOD_SELECTED` | User selected a payment method |
| `PAYMENT_SUBMITTED` | Payment form submitted |
| `PAYMENT_SUCCESS` | Payment completed successfully |
| `PAYMENT_ERROR` | Payment failed |
| `ACTION_STARTED` | The action overlay was triggered and opened natively |
| `ACTION_COMPLETED` | An action was successfully resolved by `resume()` |
| `COUPON_APPLY_REQUESTED` | Coupon code submitted (Discount Plugin) |
| `COUPON_REMOVE_REQUESTED` | Coupon removed (Discount Plugin) |
| `ITEM_QUANTITY_UPDATE_REQUESTED` | Quantity changed (Cart Edit Plugin) |
| `ITEM_REMOVE_REQUESTED` | Item removed (Cart Edit Plugin) |

---

## Internationalization

Built-in support for three locales plus automatic detection:

```tsx
<Checkout locale="en" />     // English
<Checkout locale="es" />     // Spanish
<Checkout locale="pt-BR" />  // Portuguese (Brazil)
<Checkout locale="auto" />   // Auto-detect from browser
```

Override specific messages:

```tsx
<Checkout
  messages={{
    payNow: "Complete Purchase",
    continueToPay: "Proceed to Payment",
  }}
/>
```

Custom currency formatter:

```tsx
<Checkout
  formatters={{
    currency: (amount) => `US$ ${amount.toFixed(2)}`,
  }}
/>
```

---

## Demo

Check out the interactive demo with the **Settings toolbar** to test theme switching, locales, and more:

[Agnostic Checkout Demo](https://agnostic-checkout.netlify.app/)

---

## License

MIT