npm package: @plandalf/react
Description: React checkout components for Plandalf Checkout — an embedded and popup checkout alternative to Stripe Checkout and SamCart, with a built-in billing portal.
Author: Plandalf
Latest version: 1.1.6

# @plandalf/react

[![npm version](https://badge.fury.io/js/@plandalf%2Freact.svg)](https://www.npmjs.com/package/@plandalf/react)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A powerful React SDK for seamlessly integrating Plandalf Checkout into your React applications. This library provides beautiful, type-safe checkout components with comprehensive event handling and modern animations.

> Looking for a Stripe Checkout or SamCart-style experience in React? `@plandalf/react` provides popup, inline, and full-screen embeddable checkouts plus a billing portal — a modern alternative focused on developer ergonomics and UX polish.

- Fast to integrate like Stripe Checkout
- Optimized for on-page conversion like SamCart
- Full TypeScript, rich events, and flexible UI sizing

## ✨ Features

- 🚀 **Multiple Embed Types**: Popup, Standard, and Full-screen checkout experiences
- 💪 **Full TypeScript Support**: Complete type safety with comprehensive interfaces
- 🎨 **Beautiful Animations**: Modern, smooth animations with 2025 design standards
- 📱 **Responsive Design**: Mobile-optimized with dynamic sizing
- 🎯 **Comprehensive Events**: Rich event system for complete checkout lifecycle tracking
- ⚡ **High Performance**: Optimized animations using hardware acceleration
- 🔒 **Secure**: Built-in security features and iframe sandboxing
- 🛠 **Developer Friendly**: Easy integration with excellent debugging capabilities

## 🔁 Alternatives & comparison (Stripe Checkout, SamCart)

If you’ve tried other hosted or embeddable checkouts, here’s how `@plandalf/react` compares:

- **Stripe Checkout alternative**: Similar time-to-integrate and reliability with a more flexible on-page embed (popup, inline, and full-screen) and rich client-side events for nuanced UX.
- **SamCart alternative**: Conversion-focused UX with on-page purchase flows without sending users off-site, plus a built-in billing portal component for subscriptions.
- **For React teams**: First-class TypeScript types, idiomatic components, and granular event hooks to orchestrate post-payment flows.

## 📦 Installation

```bash
npm install @plandalf/react
# or
yarn add @plandalf/react
# or
pnpm add @plandalf/react
```

## 🚀 Quick Start

### Offer Popup Checkout (Recommended)

```typescript
import React, { useState } from 'react';
import { OfferPopupEmbed } from '@plandalf/react';

function PopupCheckoutExample() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Start Checkout
      </button>
      
      <OfferPopupEmbed
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        offerId="your-offer-id"
        size="medium" // 'small' | 'medium' | 'large'
        onSuccess={(data) => {
          console.log('Payment successful!', data);
          // Handle success in background
        }}
        onClosed={(data) => {
          console.log('Checkout closed', data);
          setIsOpen(false);
        }}
        onError={(error) => {
          console.error('Checkout error:', error);
        }}
      />
    </>
  );
}
```

### Offer Standard Embed

```typescript
import React from 'react';
import { OfferStandardEmbed } from '@plandalf/react';

function StandardCheckoutExample() {
  return (
    <div style={{ width: '100%', height: '600px' }}>
      <OfferStandardEmbed
        offerId="your-offer-id"
        offer="your-offer-id" // Legacy prop support
        onSuccess={(data) => {
          console.log('Payment completed!', data);
          // Auto-closes after success
        }}
        onError={(error) => {
          console.error('Checkout error:', error);
        }}
      />
    </div>
  );
}
```

### Billing Portal

The SDK also includes a Billing Portal experience powered by a JWT that identifies the customer. The portal is served from `/billing/portal` and supports both inline and popup modes.

```typescript
import React, { useState } from 'react';
import { BillingPortalPopup, BillingPortalEmbed } from '@plandalf/react';

function BillingExamples() {
  const [open, setOpen] = useState(false);
  const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; // HS256 JWT for the customer

  return (
    <>
      <button onClick={() => setOpen(true)}>Open Billing Portal</button>

      <BillingPortalPopup
        isOpen={open}
        onClose={() => setOpen(false)}
        domain="app.example.com" // or "localhost:8002"
        customerToken={token}
        returnUrl="https://your.site/return"
        onClosed={() => setOpen(false)}
      />

      <div style={{ marginTop: 24 }}>
        <BillingPortalEmbed
          domain="app.example.com"
          customerToken={token}
          returnUrl="https://your.site/return"
          dynamicResize
        />
      </div>
    </>
  );
}
```

## 🎯 Advanced Usage

### Comprehensive Event Handling (Offer)

```typescript
import React, { useState } from 'react';
import { OfferPopupEmbed } from '@plandalf/react';

function AdvancedCheckoutExample() {
  const [isOpen, setIsOpen] = useState(false);
  const [checkoutData, setCheckoutData] = useState(null);

  const eventHandlers = {
    onInit: (checkoutId) => {
      console.log('Checkout initialized:', checkoutId);
    },
    onPageChange: (checkoutId, pageId) => {
      console.log('Page changed:', pageId);
    },
    onPaymentInit: (checkoutId) => {
      console.log('Payment process started');
    },
    onSubmit: (checkoutId) => {
      console.log('Payment submitted');
    },
    onSuccess: (data) => {
      console.log('Payment successful!', data);
      setCheckoutData(data);
      // Don't close here - handle in background
    },
    onComplete: (checkout) => {
      console.log('Checkout complete:', checkout);
    },
    onCancel: (data) => {
      console.log('Checkout cancelled:', data);
    },
    onClosed: (data) => {
      console.log('Checkout closed:', data);
      setIsOpen(false);
      
      // Show success message if checkout was completed
      if (checkoutData) {
        alert(`Order completed! ID: ${checkoutData.orderId}`);
      }
    },
    onLineItemChange: (data) => {
      console.log('Cart updated:', data);
    },
    onResize: (data) => {
      console.log('Checkout resized:', data);
    },
    onError: (error) => {
      console.error('Checkout error:', error);
    }
  };

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Advanced Checkout
      </button>
      
      <OfferPopupEmbed
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        offerId="your-offer-id"
        size="large"
        parameters={{
          line_items: JSON.stringify([
            { lookup_key: "premium-plan", quantity: 1, name: "Premium Plan", price: 29.99 }
          ]),
          redirect_url: "https://yoursite.com/success"
        }}
        {...eventHandlers}
      />
    </>
  );
}
```

### Customer Information Pre-filling (Offer)

You can pre-fill customer information to streamline the checkout process:

```typescript
import React, { useState } from 'react';
import { OfferPopupEmbed } from '@plandalf/react';

function CustomerCheckoutExample() {
  const [isOpen, setIsOpen] = useState(false);

  const customerInfo = {
    email: "john.doe@example.com",
    first_name: "John",
    last_name: "Doe",
    phone: "+1-555-0123",
    company: "Acme Corp",
    address: {
      line1: "123 Main St",
      city: "San Francisco",
      state: "CA",
      postal_code: "94105",
      country: "US"
    }
  };

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Checkout with Pre-filled Info
      </button>
      
      <OfferPopupEmbed
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        offerId="your-offer-id"
        customer={customerInfo}
        onSuccess={(data) => {
          console.log('Checkout successful!', data);
        }}
        onClosed={() => setIsOpen(false)}
      />
    </>
  );
}
```

### Cart Integration (Offer)

```typescript
import React, { useState } from 'react';
import { OfferPopupEmbed } from '@plandalf/react';

function CartCheckoutExample() {
  const [isOpen, setIsOpen] = useState(false);
  const [cart, setCart] = useState([
    { lookup_key: "item-1", quantity: 2, name: "Product 1", price: 19.99 },
    { lookup_key: "item-2", quantity: 1, name: "Product 2", price: 39.99 }
  ]);

  const createLineItemsParameter = () => {
    return JSON.stringify(cart.map(item => ({
      lookup_key: item.lookup_key,
      quantity: item.quantity,
      name: item.name,
      price: item.price
    })));
  };

  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Checkout Cart ({cart.length} items)
      </button>
      
      <OfferPopupEmbed
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        offerId="your-offer-id"
        parameters={{
          line_items: createLineItemsParameter(),
          cart_total: cart.reduce((sum, item) => sum + (item.price * item.quantity), 0),
          source: "shopping_cart"
        }}
        onSuccess={(data) => {
          console.log('Cart checkout successful!', data);
          setCart([]); // Clear cart
        }}
        onClosed={() => setIsOpen(false)}
      />
    </>
  );
}
```

## 🎨 Popup Sizing (Offer)

The popup component supports three responsive sizes:

- **`small`**: Compact size, perfect for quick purchases
- **`medium`**: Balanced size, good for most use cases (default)
- **`large`**: Full-featured size, ideal for complex checkouts

```typescript
<OfferPopupEmbed
  size="small"  // Responsive: 560px on desktop, 60vw on tablet, 80vw on mobile
  // or
  size="medium" // Responsive: 900px on desktop, 80vw on smaller screens  
  // or
  size="large"  // Near full-screen with proper margins
/>
```

## 🏗 Legacy Hook Support

For backward compatibility, the legacy `useCheckout` hook is still available:

```typescript
import { useCheckout } from '@plandalf/react';

function LegacyExample() {
  const { checkout, Modal, isOpen, close } = useCheckout("your-offer-id", {
    host: "https://your-checkout-host.com",
    redirect: false
  });

  const handlePurchase = () => {
    const items = [{
      lookup_key: "premium-plan",
      quantity: 1
    }];
    
    checkout(items);
  };

  return (
    <>
      <button onClick={handlePurchase}>Buy Now</button>
      <Modal />
    </>
  );
}
```

## 📚 API Reference

### Components

#### `OfferPopupEmbed`

Modern popup checkout with beautiful animations.

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `isOpen` | `boolean` | ✅ | - | Controls popup visibility |
| `onClose` | `() => void` | ✅ | - | Called when popup should close |
| `offerId` | `string` | ✅ | - | Unique offer identifier |
| `size` | `'small' \| 'medium' \| 'large'` | ❌ | `'medium'` | Popup size preset |
| `width` | `string` | ❌ | - | Custom width (overrides size) |
| `height` | `string` | ❌ | - | Custom height (overrides size) |
| `parameters` | `Record<string, any>` | ❌ | - | Additional checkout parameters |
| `customer` | `CustomerInfo` | ❌ | - | Customer information to pre-fill |
| `domain` | `string` | ❌ | - | Custom checkout domain |

#### `OfferStandardEmbed`

Inline checkout component for seamless integration.

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `offerId` | `string` | ✅ | - | Unique offer identifier |
| `offer` | `string` | ✅ | - | Legacy offer identifier |
| `dynamicResize` | `boolean` | ❌ | `true` | Auto-resize based on content |
| `parameters` | `Record<string, any>` | ❌ | - | Additional checkout parameters |
| `customer` | `CustomerInfo` | ❌ | - | Customer information to pre-fill |
| `domain` | `string` | ❌ | - | Custom checkout domain |

#### `OfferFullScreenEmbed`

Full-screen checkout experience.

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `isOpen` | `boolean` | ✅ | - | Controls full-screen visibility |
| `onClose` | `() => void` | ✅ | - | Called when full-screen should close |
| `offerId` | `string` | ✅ | - | Unique offer identifier |
| `parameters` | `Record<string, any>` | ❌ | - | Additional checkout parameters |
| `customer` | `CustomerInfo` | ❌ | - | Customer information to pre-fill |
| `domain` | `string` | ❌ | - | Custom checkout domain |

#### `BillingPortalPopup`

Popup billing portal launched at `/billing/portal`.

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `isOpen` | `boolean` | ✅ | - | Controls popup visibility |
| `onClose` | `() => void` | ✅ | - | Called when popup should close |
| `customerToken` | `string` | ❌ | - | HS256 JWT identifying the customer (sent as `customer`) |
| `returnUrl` | `string` | ❌ | - | Return URL after portal actions |
| `domain` | `string` | ❌ | - | Custom host/origin (e.g., `app.example.com`) |
| `parameters` | `Record<string, any>` | ❌ | - | Additional query params |
| `size` | `'small' \| 'medium' \| 'large'` | ❌ | `'medium'` | Popup size preset |
| `width` | `string` | ❌ | - | Custom width |
| `height` | `string` | ❌ | - | Custom height |

#### `BillingPortalEmbed`

Inline billing portal with dynamic height.

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `customerToken` | `string` | ❌ | - | HS256 JWT identifying the customer (sent as `customer`) |
| `returnUrl` | `string` | ❌ | - | Return URL after portal actions |
| `domain` | `string` | ❌ | - | Custom host/origin (e.g., `app.example.com`) |
| `parameters` | `Record<string, any>` | ❌ | - | Additional query params |
| `dynamicResize` | `boolean` | ❌ | `true` | Auto-resize based on content |

### Event Callbacks

All Offer embed components support comprehensive event callbacks:

```typescript
interface EmbedEventCallbacks {
  // Core lifecycle events
  onInit?: (checkoutId: string) => void;
  onPageChange?: (checkoutId: string, pageId: string) => void;
  onPaymentInit?: (checkoutId: string) => void;
  onSubmit?: (checkoutId: string) => void;
  onSuccess?: (data: any) => void;
  onComplete?: (checkout: any) => void;
  onCancel?: (data: any) => void;
  onClosed?: (data: any) => void;
  
  // Enhanced interaction events
  onLineItemChange?: (data: any) => void;
  onResize?: (data: any) => void;
  
  // Error handling
  onError?: (error: any) => void;
}
```

### Types

```typescript
interface CheckoutItem {
  lookup_key: string;
  quantity: number;
}

interface CheckoutConfig {
  host?: string;
  redirect?: boolean;
  redirectUrl?: string;
}

interface CustomerInfo {
  email?: string;
  first_name?: string;
  last_name?: string;
  phone?: string;
  company?: string;
  address?: {
    line1?: string;
    line2?: string;
    city?: string;
    state?: string;
    postal_code?: string;
    country?: string;
  };
}
```

## 🛠 Development

### Building

```bash
# Install dependencies
npm install

# Build the package
npm run build

# Watch mode for development
npm run dev

# Run tests
npm test

# Interactive testing
npm run test:interactive
```

### Local Development

1. **Build the package:**
   ```bash
   npm run build
   ```

2. **Link locally using symlinks:**
   ```bash
   # In your test project
   ln -s /path/to/@plandalf/react node_modules/@plandalf/react
   ```

3. **Configure Next.js (if using):**
   ```javascript
   // next.config.js
   module.exports = {
     transpilePackages: ['@plandalf/react'],
     experimental: {
       externalDir: true
     }
   };
   ```

## 🎯 Best Practices

### Success/Close Separation (Offer)

For optimal UX, handle success and close events separately:

```typescript
const [checkoutSuccessful, setCheckoutSuccessful] = useState(false);
const [sessionId, setSessionId] = useState(null);

<OfferPopupEmbed
  onSuccess={(data) => {
    // Handle success immediately - reload entitlements, etc.
    setCheckoutSuccessful(true);
    setSessionId(data.sessionId);
    reloadUserEntitlements(); // Background process
  }}
  onClosed={(data) => {
    // Handle close after success processing is complete
    if (checkoutSuccessful && sessionId) {
      showSuccessMessage(`Checkout ${sessionId} completed!`);
      setCheckoutSuccessful(false);
      setSessionId(null);
    }
    setIsOpen(false);
  }}
/>
```

### Error Handling

Always implement comprehensive error handling:

```typescript
<OfferPopupEmbed
  onError={(error) => {
    console.error('Checkout error:', error);
    // Show user-friendly error message
    showNotification('Checkout failed. Please try again.', 'error');
  }}
  onCancel={(data) => {
    console.log('User cancelled checkout:', data.reason);
    showNotification('Checkout cancelled', 'info');
  }}
/>
```

## ♻️ Backward Compatibility

Legacy component names are still available and re-exported for compatibility:

- `NumiPopupEmbed` → `OfferPopupEmbed`
- `NumiStandardEmbed` → `OfferStandardEmbed`
- `NumiFullScreenEmbedNew` → `OfferFullScreenEmbed`

You can migrate at your own pace by switching imports to the new Offer* names.

## 🤝 Contributing

We welcome contributions! Please:

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 

---

**Made with ❤️ by the Plandalf team**