@plandalf/react
Version:
React checkout components for Plandalf Checkout β an embedded and popup checkout alternative to Stripe Checkout and SamCart, with a built-in billing portal.
617 lines (502 loc) β’ 17.7 kB
Markdown
[](https://www.npmjs.com/package/@plandalf/react)
[](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
- π **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
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);
}}
/>
</>
);
}
```
```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>
);
}
```
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>
</>
);
}
```
```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}
/>
</>
);
}
```
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)}
/>
</>
);
}
```
```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)}
/>
</>
);
}
```
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
/>
```
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 />
</>
);
}
```
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 |
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;
}
```
```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;
};
}
```
```bash
npm install
npm run build
npm run dev
npm test
npm run test:interactive
```
1. **Build the package:**
```bash
npm run build
```
2. **Link locally using symlinks:**
```bash
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
}
};
```
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);
}}
/>
```
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');
}}
/>
```
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.
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**