recur-tw
Version:
React & Vanilla JS SDK for embedding subscription checkout flows (Taiwan / PAYUNi)
535 lines (411 loc) • 11.3 kB
Markdown
# 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.