UNPKG

@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
# @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**