@plandalf/react
v1.1.6
Published
React checkout components for Plandalf Checkout — an embedded and popup checkout alternative to Stripe Checkout and SamCart, with a built-in billing portal.
Maintainers
Keywords
Readme
@plandalf/react
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/reactprovides 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
npm install @plandalf/react
# or
yarn add @plandalf/react
# or
pnpm add @plandalf/react🚀 Quick Start
Offer Popup Checkout (Recommended)
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
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.
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)
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:
import React, { useState } from 'react';
import { OfferPopupEmbed } from '@plandalf/react';
function CustomerCheckoutExample() {
const [isOpen, setIsOpen] = useState(false);
const customerInfo = {
email: "[email protected]",
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)
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 purchasesmedium: Balanced size, good for most use cases (default)large: Full-featured size, ideal for complex checkouts
<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:
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:
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
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
# 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:interactiveLocal Development
Build the package:
npm run buildLink locally using symlinks:
# In your test project ln -s /path/to/@plandalf/react node_modules/@plandalf/reactConfigure Next.js (if using):
// 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:
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:
<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→OfferPopupEmbedNumiStandardEmbed→OfferStandardEmbedNumiFullScreenEmbedNew→OfferFullScreenEmbed
You can migrate at your own pace by switching imports to the new Offer* names.
🤝 Contributing
We welcome contributions! Please:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Made with ❤️ by the Plandalf team
