UNPKG

recur-tw

Version:

React & Vanilla JS SDK for embedding subscription checkout flows (Taiwan / PAYUNi)

535 lines (411 loc) 11.3 kB
# Recur SDK (Taiwan) A React & Vanilla JS SDK for embedding subscription checkout flows in your application. **專為台灣市場設計** - 使用 PAYUNi 支付網關處理訂閱式付款。 Taiwan-specific subscription checkout SDK. ## 🚀 Features - ✅ **React SDK** - Full React integration with hooks - ✅ **Vanilla JS** - Use with plain HTML/JavaScript (no framework required) - ✅ **Multiple Modes** - Modal, iframe, or redirect checkout flows - ✅ **TypeScript** - Full type definitions included - ✅ **Secure** - API key authentication - ✅ **Taiwan-focused** - PAYUNi payment integration --- ## 📦 Installation ### For React Projects ```bash npm install recur-tw # or pnpm add recur-tw # or yarn add recur-tw ``` ### For Static HTML/JavaScript ```html <!-- Via CDN (unpkg) --> <script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script> <!-- Via CDN (jsdelivr) --> <script src="https://cdn.jsdelivr.net/npm/recur-tw@latest/dist/recur.umd.js"></script> ``` --- ## 🎯 Quick Start ### Option 1: Vanilla JavaScript (Static HTML) Perfect for landing pages, marketing sites, or any static HTML: ```html <!DOCTYPE html> <html> <body> <button id="checkout-btn">訂閱方案</button> <script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script> <script> // Initialize SDK const recur = RecurCheckout.init({ publishableKey: 'pk_test_your_key_here' }); // Checkout on button click document.getElementById('checkout-btn').addEventListener('click', async () => { await recur.checkout({ planId: 'plan_xxx', customerName: '王小明', customerEmail: 'user@example.com', mode: 'modal' // 'modal', 'iframe', or 'redirect' }); }); </script> </body> </html> ``` [See full Vanilla JS examples →](./examples/) ### Option 2: React/Next.js Full framework integration with React hooks: ```tsx import { RecurProvider, useRecur } from 'recur-tw'; // 1. Wrap your app with RecurProvider export default function App() { return ( <RecurProvider config={{ publishableKey: 'pk_test_xxx' }}> <YourApp /> </RecurProvider> ); } // 2. Use checkout in your components function CheckoutButton() { const { checkout, isCheckingOut } = useRecur(); return ( <button onClick={() => checkout({ planId: 'pro' })} disabled={isCheckingOut}> {isCheckingOut ? 'Processing...' : 'Subscribe'} </button> ); } ``` --- ## 🔑 Get Your API Key 1. Go to your organization settings 2. Navigate to **API Keys** tab 3. Click **Create API Key** 4. Copy your `pk_test_...` key [Learn more about API Keys →](./API_KEYS_GUIDE.md) --- ## 📖 Documentation ### For Vanilla JavaScript Users #### Checkout Modes **Modal Mode** - Open payment in a popup modal: ```javascript await recur.checkout({ planId: 'plan_xxx', mode: 'modal', onClose: () => console.log('Modal closed') }); ``` **iframe Mode** - Embed payment in your page: ```javascript await recur.checkout({ planId: 'plan_xxx', mode: 'iframe', container: '#checkout-container' // CSS selector or HTMLElement }); ``` **Redirect Mode** - Full page redirect (default): ```javascript await recur.checkout({ planId: 'plan_xxx', mode: 'redirect' // or omit mode parameter }); ``` #### API Reference ```javascript // Initialize const recur = RecurCheckout.init({ publishableKey: string, // Required baseUrl?: string // Optional, defaults to current origin }); // Checkout await recur.checkout({ planId: string, // Required customerName?: string, customerEmail?: string, customerPhone?: string, mode?: 'modal' | 'iframe' | 'redirect', // Default: 'redirect' container?: string | HTMLElement, // Required for iframe mode onSuccess?: (result) => void, onError?: (error) => void, onClose?: () => void }); // Close modal or remove iframe manually recur.close(); ``` [See complete examples →](./examples/) --- ### For React/Next.js Users #### 1. Wrap your app with RecurProvider ```tsx // app/layout.tsx or your root component import { RecurProvider } from 'recur-tw'; export default function RootLayout({ children }) { return ( <html> <body> <RecurProvider config={{ organizationId: 'your-org-id' }}> {children} </RecurProvider> </body> </html> ); } ``` ### 2. Use the checkout function in your components ```tsx // components/pricing-button.tsx 'use client'; import { useRecur } from 'recur-tw'; export function PricingButton({ planId }: { planId: string }) { const { checkout, isCheckingOut } = useRecur(); return ( <Button onClick={async () => { await checkout({ planId }); }} disabled={isCheckingOut} > {isCheckingOut ? 'Processing...' : 'Subscribe'} </Button> ); } ``` ## Configuration ### RecurProvider Props ```tsx interface RecurConfig { // Organization ID for the checkout organizationId?: string; // Base URL for API calls (defaults to current origin) baseUrl?: string; // Redirect mode: 'redirect' (default) or 'popup' redirectMode?: 'redirect' | 'popup'; // Success callback URL successUrl?: string; // Cancel callback URL cancelUrl?: string; } ``` ## Usage Examples ### Basic Checkout ```tsx const { checkout } = useRecur(); await checkout({ planId: 'pro-monthly', }); ``` ### Checkout with Customer Information ```tsx const { checkout } = useRecur(); await checkout({ planId: 'pro-monthly', customerName: 'John Doe', customerEmail: 'john@example.com', customerPhone: '+886912345678', }); ``` ### Checkout with Callbacks ```tsx const { checkout } = useRecur(); await checkout({ planId: 'pro-monthly', onSuccess: (result) => { console.log('Checkout initiated:', result.subscription.id); // Show success toast }, onError: (error) => { console.error('Checkout failed:', error.message); // Show error toast }, }); ``` ### Popup Mode ```tsx // In your RecurProvider config <RecurProvider config={{ redirectMode: 'popup' }}> {children} </RecurProvider> // In your component const { checkout } = useRecur(); await checkout({ planId: 'pro-monthly', onPaymentComplete: (subscription) => { console.log('Payment completed!', subscription); // Refresh page or update UI }, onPaymentCancel: () => { console.log('Payment cancelled'); }, }); ``` ### Custom Form ```tsx 'use client'; import { useState } from 'react'; import { useRecur } from '@/lib/recur'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; export function CustomCheckoutForm({ planId }: { planId: string }) { const { checkout, isCheckingOut } = useRecur(); const [email, setEmail] = useState(''); const [name, setName] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await checkout({ planId, customerEmail: email, customerName: name, onSuccess: () => { // Show success message }, onError: (error) => { alert(error.message); }, }); }; return ( <form onSubmit={handleSubmit}> <Input type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} required /> <Input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required /> <Button type="submit" disabled={isCheckingOut}> {isCheckingOut ? 'Processing...' : 'Subscribe'} </Button> </form> ); } ``` ### Dynamic Organization ID ```tsx // Override organization ID per checkout const { checkout } = useRecur(); await checkout({ planId: 'pro-monthly', organizationId: 'different-org-id', }); ``` ### Update Configuration Dynamically ```tsx const { updateConfig } = useRecur(); // Switch to popup mode updateConfig({ redirectMode: 'popup' }); // Update organization ID updateConfig({ organizationId: 'new-org-id' }); ``` ## API Reference ### `useRecur()` Returns a `RecurContextValue` object with the following properties: #### `checkout(options: CheckoutOptions): Promise<void>` Initiates a checkout flow. **Options:** - `planId` (required): The ID of the subscription plan - `customerName` (optional): Customer's name - `customerEmail` (optional): Customer's email - `customerPhone` (optional): Customer's phone number - `organizationId` (optional): Override the organization ID - `onSuccess` (optional): Callback when checkout is initiated successfully - `onError` (optional): Callback when checkout fails - `onPaymentComplete` (optional): Callback when payment is completed (popup mode only) - `onPaymentCancel` (optional): Callback when payment is cancelled (popup mode only) #### `isCheckingOut: boolean` Indicates whether a checkout is currently in progress. #### `config: RecurConfig` Current configuration object. #### `updateConfig(config: Partial<RecurConfig>): void` Updates the configuration. ## Error Handling All checkout errors are caught and passed to the `onError` callback: ```tsx await checkout({ planId: 'pro-monthly', onError: (error) => { console.error('Error code:', error.code); console.error('Error message:', error.message); console.error('Error details:', error.details); }, }); ``` Common error codes: - `CHECKOUT_FAILED`: Failed to initiate checkout - `CHECKOUT_ERROR`: General checkout error ## TypeScript Support The SDK is written in TypeScript and provides full type definitions: ```tsx import type { RecurConfig, CheckoutOptions, CheckoutResult, CheckoutError, SubscriptionResult, } from 'recur-tw'; ``` ## Advanced Usage ### Server-Side Organization Detection ```tsx // app/layout.tsx import { RecurProvider } from '@/lib/recur'; import { getOrganizationIdFromDomain } from '@/lib/utils'; export default async function RootLayout({ children }) { const organizationId = await getOrganizationIdFromDomain(); return ( <html> <body> <RecurProvider config={{ organizationId }}> {children} </RecurProvider> </body> </html> ); } ``` ### Multi-Organization Support ```tsx // Let each checkout specify its own organization <RecurProvider> <App /> </RecurProvider> // In component const { checkout } = useRecur(); // Store A await checkout({ planId: 'plan-a', organizationId: 'store-a' }); // Store B await checkout({ planId: 'plan-b', organizationId: 'store-b' }); ``` ### Integration with Toast Notifications ```tsx import { useRecur } from '@/lib/recur'; import { toast } from 'sonner'; export function CheckoutButton({ planId }: { planId: string }) { const { checkout, isCheckingOut } = useRecur(); const handleCheckout = async () => { await checkout({ planId, onSuccess: (result) => { toast.success('Redirecting to payment...'); }, onError: (error) => { toast.error(error.message); }, }); }; return ( <button onClick={handleCheckout} disabled={isCheckingOut}> {isCheckingOut ? 'Loading...' : 'Subscribe Now'} </button> ); } ``` ## License This SDK is part of the Recur project.