recur-tw 0.0.3-beta.3 → 0.0.3-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +416 -309
- package/dist/index.cjs +1418 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -14
- package/dist/index.d.ts +169 -14
- package/dist/index.js +1418 -273
- package/dist/index.js.map +1 -1
- package/dist/recur.umd.js +786 -17
- package/dist/recur.umd.js.map +1 -1
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
# Recur SDK (Taiwan)
|
|
2
2
|
|
|
3
|
-
A React & Vanilla JS SDK for embedding subscription checkout flows
|
|
3
|
+
A React & Vanilla JS SDK for embedding subscription checkout flows with PAYUNi payment integration.
|
|
4
4
|
|
|
5
5
|
**專為台灣市場設計** - 使用 PAYUNi 支付網關處理訂閱式付款。
|
|
6
6
|
|
|
7
|
-
Taiwan-specific subscription checkout SDK.
|
|
7
|
+
Taiwan-specific subscription checkout SDK with embedded payment forms powered by Web Components.
|
|
8
8
|
|
|
9
9
|
## 🚀 Features
|
|
10
10
|
|
|
11
|
-
- ✅ **React SDK** - Full React integration with hooks
|
|
11
|
+
- ✅ **React SDK** - Full React integration with hooks (`usePlans`, `useRecur`)
|
|
12
12
|
- ✅ **Vanilla JS** - Use with plain HTML/JavaScript (no framework required)
|
|
13
|
-
- ✅ **
|
|
13
|
+
- ✅ **Embedded Checkout** - Native payment forms with PAYUNi credit card fields
|
|
14
|
+
- ✅ **Web Components** - Modern, encapsulated UI components
|
|
15
|
+
- ✅ **Custom Styling** - Fully customizable with shadcn/ui compatibility
|
|
14
16
|
- ✅ **TypeScript** - Full type definitions included
|
|
15
|
-
- ✅ **
|
|
17
|
+
- ✅ **SSR Safe** - Works with Next.js, Remix, and other SSR frameworks
|
|
16
18
|
- ✅ **Taiwan-focused** - PAYUNi payment integration
|
|
17
19
|
|
|
18
20
|
---
|
|
@@ -43,7 +45,61 @@ yarn add recur-tw
|
|
|
43
45
|
|
|
44
46
|
## 🎯 Quick Start
|
|
45
47
|
|
|
46
|
-
### Option 1:
|
|
48
|
+
### Option 1: React/Next.js
|
|
49
|
+
|
|
50
|
+
Full framework integration with React hooks and embedded checkout:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
'use client';
|
|
54
|
+
|
|
55
|
+
import { RecurProvider, usePlans, useRecur } from 'recur-tw';
|
|
56
|
+
|
|
57
|
+
// 1. Wrap your app with RecurProvider
|
|
58
|
+
export default function App() {
|
|
59
|
+
return (
|
|
60
|
+
<RecurProvider
|
|
61
|
+
config={{
|
|
62
|
+
publishableKey: 'pk_test_xxx',
|
|
63
|
+
containerElementId: 'recur-payment-container' // For embedded checkout
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<SubscriptionPage />
|
|
67
|
+
</RecurProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Fetch plans and checkout
|
|
72
|
+
function SubscriptionPage() {
|
|
73
|
+
const { data: plans, isLoading } = usePlans();
|
|
74
|
+
const { checkout, isCheckingOut } = useRecur();
|
|
75
|
+
|
|
76
|
+
if (isLoading) return <div>Loading plans...</div>;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
{plans?.map((plan) => (
|
|
81
|
+
<button
|
|
82
|
+
key={plan.id}
|
|
83
|
+
onClick={() => checkout({
|
|
84
|
+
planId: plan.id,
|
|
85
|
+
customerEmail: 'user@example.com',
|
|
86
|
+
customerName: 'John Doe',
|
|
87
|
+
customerPhone: '+886912345678',
|
|
88
|
+
})}
|
|
89
|
+
disabled={isCheckingOut}
|
|
90
|
+
>
|
|
91
|
+
Subscribe to {plan.name} - NT${plan.price}
|
|
92
|
+
</button>
|
|
93
|
+
))}
|
|
94
|
+
|
|
95
|
+
{/* Embedded payment form will appear here */}
|
|
96
|
+
<div id="recur-payment-container"></div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Option 2: Vanilla JavaScript (Static HTML)
|
|
47
103
|
|
|
48
104
|
Perfect for landing pages, marketing sites, or any static HTML:
|
|
49
105
|
|
|
@@ -66,7 +122,7 @@ Perfect for landing pages, marketing sites, or any static HTML:
|
|
|
66
122
|
planId: 'plan_xxx',
|
|
67
123
|
customerName: '王小明',
|
|
68
124
|
customerEmail: 'user@example.com',
|
|
69
|
-
mode: '
|
|
125
|
+
mode: 'redirect' // 'redirect' or 'embedded'
|
|
70
126
|
});
|
|
71
127
|
});
|
|
72
128
|
</script>
|
|
@@ -74,375 +130,370 @@ Perfect for landing pages, marketing sites, or any static HTML:
|
|
|
74
130
|
</html>
|
|
75
131
|
```
|
|
76
132
|
|
|
77
|
-
[See full Vanilla JS examples →](./examples/)
|
|
78
|
-
|
|
79
|
-
### Option 2: React/Next.js
|
|
80
|
-
|
|
81
|
-
Full framework integration with React hooks:
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
import { RecurProvider, useRecur } from 'recur-tw';
|
|
85
|
-
|
|
86
|
-
// 1. Wrap your app with RecurProvider
|
|
87
|
-
export default function App() {
|
|
88
|
-
return (
|
|
89
|
-
<RecurProvider config={{ publishableKey: 'pk_test_xxx' }}>
|
|
90
|
-
<YourApp />
|
|
91
|
-
</RecurProvider>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 2. Use checkout in your components
|
|
96
|
-
function CheckoutButton() {
|
|
97
|
-
const { checkout, isCheckingOut } = useRecur();
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<button onClick={() => checkout({ planId: 'pro' })} disabled={isCheckingOut}>
|
|
101
|
-
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
102
|
-
</button>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
133
|
---
|
|
108
134
|
|
|
109
135
|
## 🔑 Get Your API Key
|
|
110
136
|
|
|
111
|
-
1. Go to your
|
|
112
|
-
2. Navigate to **API Keys**
|
|
113
|
-
3. Click **Create
|
|
114
|
-
4. Copy your `pk_test_...` key
|
|
137
|
+
1. Go to your Recur dashboard
|
|
138
|
+
2. Navigate to **Settings** → **API Keys**
|
|
139
|
+
3. Click **Create Publishable Key**
|
|
140
|
+
4. Copy your `pk_test_...` or `pk_live_...` key
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
> ⚠️ **Never** use your secret key (`sk_*`) in client-side code!
|
|
117
143
|
|
|
118
144
|
---
|
|
119
145
|
|
|
120
146
|
## 📖 Documentation
|
|
121
147
|
|
|
122
|
-
###
|
|
148
|
+
### Checkout Modes
|
|
123
149
|
|
|
124
|
-
####
|
|
150
|
+
#### Embedded Mode (Recommended)
|
|
125
151
|
|
|
126
|
-
|
|
127
|
-
```javascript
|
|
128
|
-
await recur.checkout({
|
|
129
|
-
planId: 'plan_xxx',
|
|
130
|
-
mode: 'modal',
|
|
131
|
-
onClose: () => console.log('Modal closed')
|
|
132
|
-
});
|
|
133
|
-
```
|
|
152
|
+
Embed the full payment form directly in your page with PAYUNi credit card fields:
|
|
134
153
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await recur.checkout({
|
|
138
|
-
planId: 'plan_xxx',
|
|
139
|
-
mode: 'iframe',
|
|
140
|
-
container: '#checkout-container' // CSS selector or HTMLElement
|
|
141
|
-
});
|
|
142
|
-
```
|
|
154
|
+
```tsx
|
|
155
|
+
const { checkout } = useRecur();
|
|
143
156
|
|
|
144
|
-
|
|
145
|
-
```javascript
|
|
146
|
-
await recur.checkout({
|
|
157
|
+
await checkout({
|
|
147
158
|
planId: 'plan_xxx',
|
|
148
|
-
|
|
159
|
+
customerEmail: 'user@example.com',
|
|
160
|
+
customerName: 'John Doe',
|
|
161
|
+
customerPhone: '+886912345678',
|
|
162
|
+
// mode: 'embedded' is default when containerElementId is configured
|
|
149
163
|
});
|
|
150
164
|
```
|
|
151
165
|
|
|
152
|
-
|
|
166
|
+
**Features:**
|
|
167
|
+
- ✅ Native PAYUNi credit card input fields
|
|
168
|
+
- ✅ No redirects - complete flow in your app
|
|
169
|
+
- ✅ Fully customizable styling
|
|
170
|
+
- ✅ Web Components based (modern, encapsulated)
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
// Initialize
|
|
156
|
-
const recur = RecurCheckout.init({
|
|
157
|
-
publishableKey: string, // Required
|
|
158
|
-
baseUrl?: string // Optional, defaults to current origin
|
|
159
|
-
});
|
|
172
|
+
#### Redirect Mode
|
|
160
173
|
|
|
161
|
-
|
|
162
|
-
await recur.checkout({
|
|
163
|
-
planId: string, // Required
|
|
164
|
-
customerName?: string,
|
|
165
|
-
customerEmail?: string,
|
|
166
|
-
customerPhone?: string,
|
|
167
|
-
mode?: 'modal' | 'iframe' | 'redirect', // Default: 'redirect'
|
|
168
|
-
container?: string | HTMLElement, // Required for iframe mode
|
|
169
|
-
onSuccess?: (result) => void,
|
|
170
|
-
onError?: (error) => void,
|
|
171
|
-
onClose?: () => void
|
|
172
|
-
});
|
|
174
|
+
Redirect to a full-page checkout:
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
```tsx
|
|
177
|
+
await checkout({
|
|
178
|
+
planId: 'plan_xxx',
|
|
179
|
+
mode: 'redirect',
|
|
180
|
+
successUrl: 'https://yoursite.com/success',
|
|
181
|
+
cancelUrl: 'https://yoursite.com/cancel',
|
|
182
|
+
});
|
|
176
183
|
```
|
|
177
184
|
|
|
178
|
-
[See complete examples →](./examples/)
|
|
179
|
-
|
|
180
185
|
---
|
|
181
186
|
|
|
182
|
-
###
|
|
187
|
+
### React Hooks
|
|
183
188
|
|
|
184
|
-
####
|
|
189
|
+
#### `usePlans()`
|
|
190
|
+
|
|
191
|
+
Fetch available subscription plans:
|
|
185
192
|
|
|
186
193
|
```tsx
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
import { usePlans } from 'recur-tw';
|
|
195
|
+
|
|
196
|
+
function PlansPage() {
|
|
197
|
+
const { data: plans, isLoading, error } = usePlans();
|
|
198
|
+
|
|
199
|
+
if (isLoading) return <div>Loading...</div>;
|
|
200
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
189
201
|
|
|
190
|
-
export default function RootLayout({ children }) {
|
|
191
202
|
return (
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
203
|
+
<div>
|
|
204
|
+
{plans?.map((plan) => (
|
|
205
|
+
<div key={plan.id}>
|
|
206
|
+
<h3>{plan.name}</h3>
|
|
207
|
+
<p>NT${plan.price} / {plan.billingPeriod}</p>
|
|
208
|
+
{plan.trialDays > 0 && <p>🎉 {plan.trialDays} days free trial</p>}
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
199
212
|
);
|
|
200
213
|
}
|
|
201
214
|
```
|
|
202
215
|
|
|
203
|
-
|
|
216
|
+
#### `useRecur()`
|
|
204
217
|
|
|
205
|
-
|
|
206
|
-
// components/pricing-button.tsx
|
|
207
|
-
'use client';
|
|
218
|
+
Access checkout functionality:
|
|
208
219
|
|
|
220
|
+
```tsx
|
|
209
221
|
import { useRecur } from 'recur-tw';
|
|
210
222
|
|
|
211
|
-
|
|
212
|
-
const { checkout, isCheckingOut } = useRecur();
|
|
223
|
+
function CheckoutButton({ planId }: { planId: string }) {
|
|
224
|
+
const { checkout, isCheckingOut, config, updateConfig } = useRecur();
|
|
213
225
|
|
|
214
226
|
return (
|
|
215
|
-
<
|
|
216
|
-
onClick={
|
|
217
|
-
await checkout({ planId });
|
|
218
|
-
}}
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => checkout({ planId })}
|
|
219
229
|
disabled={isCheckingOut}
|
|
220
230
|
>
|
|
221
231
|
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
222
|
-
</
|
|
232
|
+
</button>
|
|
223
233
|
);
|
|
224
234
|
}
|
|
225
235
|
```
|
|
226
236
|
|
|
227
|
-
|
|
237
|
+
**Returns:**
|
|
238
|
+
- `checkout(options)` - Initiate checkout flow
|
|
239
|
+
- `isCheckingOut` - Boolean indicating if checkout is in progress
|
|
240
|
+
- `config` - Current SDK configuration
|
|
241
|
+
- `updateConfig(newConfig)` - Update configuration dynamically
|
|
242
|
+
- `fetchPlans()` - Manually fetch plans
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### Configuration
|
|
228
247
|
|
|
229
|
-
|
|
248
|
+
#### RecurProvider Props
|
|
230
249
|
|
|
231
250
|
```tsx
|
|
232
251
|
interface RecurConfig {
|
|
233
|
-
//
|
|
234
|
-
|
|
252
|
+
// Required: Your publishable API key
|
|
253
|
+
publishableKey: string;
|
|
235
254
|
|
|
236
|
-
//
|
|
255
|
+
// Optional: API base URL (defaults to production)
|
|
237
256
|
baseUrl?: string;
|
|
238
257
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// Success callback URL
|
|
243
|
-
successUrl?: string;
|
|
244
|
-
|
|
245
|
-
// Cancel callback URL
|
|
246
|
-
cancelUrl?: string;
|
|
258
|
+
// Optional: Container element ID for embedded checkout
|
|
259
|
+
containerElementId?: string;
|
|
247
260
|
}
|
|
248
261
|
```
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
### Basic Checkout
|
|
263
|
+
Example:
|
|
253
264
|
|
|
254
265
|
```tsx
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
<RecurProvider
|
|
267
|
+
config={{
|
|
268
|
+
publishableKey: process.env.NEXT_PUBLIC_RECUR_KEY!,
|
|
269
|
+
baseUrl: 'https://api.recur.tw', // optional
|
|
270
|
+
containerElementId: 'recur-payment-container',
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
{children}
|
|
274
|
+
</RecurProvider>
|
|
260
275
|
```
|
|
261
276
|
|
|
262
|
-
|
|
277
|
+
#### Checkout Options
|
|
263
278
|
|
|
264
279
|
```tsx
|
|
265
|
-
|
|
280
|
+
interface CheckoutOptions {
|
|
281
|
+
// Required: Plan ID to subscribe to
|
|
282
|
+
planId: string;
|
|
266
283
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
customerName
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
284
|
+
// Customer information
|
|
285
|
+
customerEmail?: string;
|
|
286
|
+
customerName?: string;
|
|
287
|
+
customerPhone?: string;
|
|
288
|
+
|
|
289
|
+
// Checkout mode
|
|
290
|
+
mode?: 'embedded' | 'redirect'; // Default: 'embedded' if containerElementId is set
|
|
291
|
+
|
|
292
|
+
// Redirect URLs (for redirect mode)
|
|
293
|
+
successUrl?: string;
|
|
294
|
+
cancelUrl?: string;
|
|
295
|
+
|
|
296
|
+
// Callbacks
|
|
297
|
+
onPaymentComplete?: (subscription: Subscription) => void;
|
|
298
|
+
onError?: (error: CheckoutError) => void;
|
|
299
|
+
}
|
|
273
300
|
```
|
|
274
301
|
|
|
275
|
-
|
|
302
|
+
---
|
|
276
303
|
|
|
277
|
-
|
|
278
|
-
const { checkout } = useRecur();
|
|
304
|
+
### Custom Styling
|
|
279
305
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
306
|
+
The SDK uses Web Components with customizable styles. You can override PAYUNi input styles to match your design system.
|
|
307
|
+
|
|
308
|
+
#### Default Styling (shadcn/ui compatible)
|
|
309
|
+
|
|
310
|
+
The SDK automatically applies shadcn-compatible focus rings:
|
|
311
|
+
|
|
312
|
+
```css
|
|
313
|
+
.form-input-focus {
|
|
314
|
+
border-color: var(--ring, hsl(215 16% 47%)) !important;
|
|
315
|
+
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent) !important;
|
|
316
|
+
}
|
|
291
317
|
```
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
#### Custom Styling
|
|
320
|
+
|
|
321
|
+
Override styles by providing custom CSS:
|
|
294
322
|
|
|
295
323
|
```tsx
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
324
|
+
const paymentForm = document.createElement('recur-payment-form');
|
|
325
|
+
paymentForm.setAttribute('custom-styles', `
|
|
326
|
+
/* Custom focus style */
|
|
327
|
+
.form-input-focus {
|
|
328
|
+
border-color: #3b82f6 !important;
|
|
329
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* Adjust container heights */
|
|
333
|
+
#recur-payment-container-card-no,
|
|
334
|
+
#recur-payment-container-card-exp,
|
|
335
|
+
#recur-payment-container-card-cvc {
|
|
336
|
+
height: 40px !important;
|
|
337
|
+
}
|
|
338
|
+
`);
|
|
339
|
+
```
|
|
300
340
|
|
|
301
|
-
|
|
302
|
-
const { checkout } = useRecur();
|
|
341
|
+
The custom styles are injected into the Light DOM, allowing them to affect PAYUNi's iframe elements while keeping other UI styles encapsulated.
|
|
303
342
|
|
|
304
|
-
|
|
305
|
-
planId: 'pro-monthly',
|
|
306
|
-
onPaymentComplete: (subscription) => {
|
|
307
|
-
console.log('Payment completed!', subscription);
|
|
308
|
-
// Refresh page or update UI
|
|
309
|
-
},
|
|
310
|
-
onPaymentCancel: () => {
|
|
311
|
-
console.log('Payment cancelled');
|
|
312
|
-
},
|
|
313
|
-
});
|
|
314
|
-
```
|
|
343
|
+
---
|
|
315
344
|
|
|
316
|
-
###
|
|
345
|
+
### Advanced Examples
|
|
346
|
+
|
|
347
|
+
#### Embedded Checkout with Error Handling
|
|
317
348
|
|
|
318
349
|
```tsx
|
|
319
350
|
'use client';
|
|
320
351
|
|
|
321
352
|
import { useState } from 'react';
|
|
322
|
-
import { useRecur } from '
|
|
323
|
-
import { Button } from '@/components/ui/button';
|
|
324
|
-
import { Input } from '@/components/ui/input';
|
|
353
|
+
import { useRecur } from 'recur-tw';
|
|
325
354
|
|
|
326
|
-
export function
|
|
355
|
+
export function CheckoutForm({ planId }: { planId: string }) {
|
|
327
356
|
const { checkout, isCheckingOut } = useRecur();
|
|
328
|
-
const [
|
|
329
|
-
const [name, setName] = useState('');
|
|
357
|
+
const [error, setError] = useState<string | null>(null);
|
|
330
358
|
|
|
331
|
-
const
|
|
332
|
-
|
|
359
|
+
const handleCheckout = async () => {
|
|
360
|
+
setError(null);
|
|
333
361
|
|
|
334
362
|
await checkout({
|
|
335
363
|
planId,
|
|
336
|
-
customerEmail:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
364
|
+
customerEmail: 'user@example.com',
|
|
365
|
+
onPaymentComplete: (subscription) => {
|
|
366
|
+
console.log('Payment successful!', subscription);
|
|
367
|
+
// Redirect to success page or show confirmation
|
|
340
368
|
},
|
|
341
|
-
onError: (
|
|
342
|
-
|
|
369
|
+
onError: (err) => {
|
|
370
|
+
setError(err.message);
|
|
343
371
|
},
|
|
344
372
|
});
|
|
345
373
|
};
|
|
346
374
|
|
|
347
375
|
return (
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
placeholder="Email"
|
|
359
|
-
value={email}
|
|
360
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
361
|
-
required
|
|
362
|
-
/>
|
|
363
|
-
<Button type="submit" disabled={isCheckingOut}>
|
|
364
|
-
{isCheckingOut ? 'Processing...' : 'Subscribe'}
|
|
365
|
-
</Button>
|
|
366
|
-
</form>
|
|
376
|
+
<div>
|
|
377
|
+
<button onClick={handleCheckout} disabled={isCheckingOut}>
|
|
378
|
+
{isCheckingOut ? 'Processing...' : 'Subscribe Now'}
|
|
379
|
+
</button>
|
|
380
|
+
|
|
381
|
+
{error && <div className="error">{error}</div>}
|
|
382
|
+
|
|
383
|
+
{/* Payment form will appear here */}
|
|
384
|
+
<div id="recur-payment-container"></div>
|
|
385
|
+
</div>
|
|
367
386
|
);
|
|
368
387
|
}
|
|
369
388
|
```
|
|
370
389
|
|
|
371
|
-
|
|
390
|
+
#### Dynamic Plan Selection
|
|
372
391
|
|
|
373
392
|
```tsx
|
|
374
|
-
|
|
375
|
-
const { checkout } = useRecur();
|
|
393
|
+
'use client';
|
|
376
394
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
organizationId: 'different-org-id',
|
|
380
|
-
});
|
|
381
|
-
```
|
|
395
|
+
import { useState } from 'react';
|
|
396
|
+
import { usePlans, useRecur } from 'recur-tw';
|
|
382
397
|
|
|
383
|
-
|
|
398
|
+
export function PricingTable() {
|
|
399
|
+
const { data: plans } = usePlans();
|
|
400
|
+
const { checkout, isCheckingOut } = useRecur();
|
|
401
|
+
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
|
|
384
402
|
|
|
385
|
-
|
|
386
|
-
|
|
403
|
+
const handleSelectPlan = async (planId: string) => {
|
|
404
|
+
setSelectedPlan(planId);
|
|
387
405
|
|
|
388
|
-
|
|
389
|
-
|
|
406
|
+
await checkout({
|
|
407
|
+
planId,
|
|
408
|
+
customerEmail: 'user@example.com',
|
|
409
|
+
customerName: 'John Doe',
|
|
410
|
+
});
|
|
411
|
+
};
|
|
390
412
|
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
return (
|
|
414
|
+
<div>
|
|
415
|
+
{!selectedPlan ? (
|
|
416
|
+
// Plan selection
|
|
417
|
+
<div className="plans-grid">
|
|
418
|
+
{plans?.map((plan) => (
|
|
419
|
+
<div key={plan.id} className="plan-card">
|
|
420
|
+
<h3>{plan.name}</h3>
|
|
421
|
+
<p className="price">NT${plan.price} / {plan.billingPeriod}</p>
|
|
422
|
+
{plan.trialDays > 0 && (
|
|
423
|
+
<p className="trial">🎉 {plan.trialDays} day free trial</p>
|
|
424
|
+
)}
|
|
425
|
+
<button onClick={() => handleSelectPlan(plan.id)}>
|
|
426
|
+
Select Plan
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
))}
|
|
430
|
+
</div>
|
|
431
|
+
) : (
|
|
432
|
+
// Payment form
|
|
433
|
+
<div>
|
|
434
|
+
<button onClick={() => setSelectedPlan(null)}>← Back to plans</button>
|
|
435
|
+
<div id="recur-payment-container"></div>
|
|
436
|
+
</div>
|
|
437
|
+
)}
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
393
441
|
```
|
|
394
442
|
|
|
395
|
-
|
|
443
|
+
#### Update Config Dynamically
|
|
396
444
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
Returns a `RecurContextValue` object with the following properties:
|
|
445
|
+
```tsx
|
|
446
|
+
const { config, updateConfig } = useRecur();
|
|
400
447
|
|
|
401
|
-
|
|
448
|
+
// Switch to different API endpoint
|
|
449
|
+
updateConfig({
|
|
450
|
+
baseUrl: 'https://staging-api.recur.tw'
|
|
451
|
+
});
|
|
402
452
|
|
|
403
|
-
|
|
453
|
+
// Update publishable key
|
|
454
|
+
updateConfig({
|
|
455
|
+
publishableKey: 'pk_live_xxx'
|
|
456
|
+
});
|
|
457
|
+
```
|
|
404
458
|
|
|
405
|
-
|
|
406
|
-
- `planId` (required): The ID of the subscription plan
|
|
407
|
-
- `customerName` (optional): Customer's name
|
|
408
|
-
- `customerEmail` (optional): Customer's email
|
|
409
|
-
- `customerPhone` (optional): Customer's phone number
|
|
410
|
-
- `organizationId` (optional): Override the organization ID
|
|
411
|
-
- `onSuccess` (optional): Callback when checkout is initiated successfully
|
|
412
|
-
- `onError` (optional): Callback when checkout fails
|
|
413
|
-
- `onPaymentComplete` (optional): Callback when payment is completed (popup mode only)
|
|
414
|
-
- `onPaymentCancel` (optional): Callback when payment is cancelled (popup mode only)
|
|
459
|
+
---
|
|
415
460
|
|
|
416
|
-
|
|
461
|
+
## Web Components Architecture
|
|
417
462
|
|
|
418
|
-
|
|
463
|
+
The SDK uses modern Web Components for better encapsulation and reusability:
|
|
419
464
|
|
|
420
|
-
|
|
465
|
+
### RecurPaymentForm Component
|
|
421
466
|
|
|
422
|
-
|
|
467
|
+
The embedded payment form is a custom element:
|
|
423
468
|
|
|
424
|
-
|
|
469
|
+
```html
|
|
470
|
+
<recur-payment-form
|
|
471
|
+
container-id="recur-payment-container"
|
|
472
|
+
custom-styles="..."
|
|
473
|
+
></recur-payment-form>
|
|
474
|
+
```
|
|
425
475
|
|
|
426
|
-
|
|
476
|
+
**Features:**
|
|
477
|
+
- Shadow DOM for style isolation
|
|
478
|
+
- Light DOM for PAYUNi iframe integration
|
|
479
|
+
- Custom events for form submission
|
|
480
|
+
- Attribute-based configuration
|
|
427
481
|
|
|
428
|
-
|
|
482
|
+
**Events:**
|
|
483
|
+
- `submit` - Fired when user submits payment
|
|
484
|
+
- `error` - Fired when an error occurs
|
|
429
485
|
|
|
430
|
-
|
|
486
|
+
**Custom Styling:**
|
|
487
|
+
Styles are injected into Light DOM to affect PAYUNi iframes:
|
|
431
488
|
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
console.error('Error message:', error.message);
|
|
438
|
-
console.error('Error details:', error.details);
|
|
439
|
-
},
|
|
440
|
-
});
|
|
489
|
+
```javascript
|
|
490
|
+
paymentForm.setAttribute('custom-styles', `
|
|
491
|
+
.form-input-focus { /* PAYUNi focus styles */ }
|
|
492
|
+
#container-card-no { /* Card number field styles */ }
|
|
493
|
+
`);
|
|
441
494
|
```
|
|
442
495
|
|
|
443
|
-
|
|
444
|
-
- `CHECKOUT_FAILED`: Failed to initiate checkout
|
|
445
|
-
- `CHECKOUT_ERROR`: General checkout error
|
|
496
|
+
---
|
|
446
497
|
|
|
447
498
|
## TypeScript Support
|
|
448
499
|
|
|
@@ -451,84 +502,140 @@ The SDK is written in TypeScript and provides full type definitions:
|
|
|
451
502
|
```tsx
|
|
452
503
|
import type {
|
|
453
504
|
RecurConfig,
|
|
505
|
+
RecurContextValue,
|
|
454
506
|
CheckoutOptions,
|
|
455
507
|
CheckoutResult,
|
|
456
508
|
CheckoutError,
|
|
509
|
+
Plan,
|
|
510
|
+
PlansResult,
|
|
511
|
+
Subscription,
|
|
457
512
|
SubscriptionResult,
|
|
458
513
|
} from 'recur-tw';
|
|
459
514
|
```
|
|
460
515
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
### Server-Side Organization Detection
|
|
516
|
+
### Type Definitions
|
|
464
517
|
|
|
465
518
|
```tsx
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
519
|
+
interface Plan {
|
|
520
|
+
id: string;
|
|
521
|
+
name: string;
|
|
522
|
+
price: number;
|
|
523
|
+
billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'CUSTOM';
|
|
524
|
+
trialDays: number;
|
|
525
|
+
description?: string;
|
|
526
|
+
features?: Record<string, any>;
|
|
527
|
+
}
|
|
469
528
|
|
|
470
|
-
|
|
471
|
-
|
|
529
|
+
interface Subscription {
|
|
530
|
+
id: string;
|
|
531
|
+
status: string;
|
|
532
|
+
planId: string;
|
|
533
|
+
amount: number;
|
|
534
|
+
billingPeriod: string;
|
|
535
|
+
currentPeriodStart: string;
|
|
536
|
+
currentPeriodEnd: string;
|
|
537
|
+
}
|
|
472
538
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
{children}
|
|
478
|
-
</RecurProvider>
|
|
479
|
-
</body>
|
|
480
|
-
</html>
|
|
481
|
-
);
|
|
539
|
+
interface CheckoutError {
|
|
540
|
+
code: string;
|
|
541
|
+
message: string;
|
|
542
|
+
details?: any;
|
|
482
543
|
}
|
|
483
544
|
```
|
|
484
545
|
|
|
485
|
-
|
|
546
|
+
---
|
|
486
547
|
|
|
487
|
-
|
|
488
|
-
// Let each checkout specify its own organization
|
|
489
|
-
<RecurProvider>
|
|
490
|
-
<App />
|
|
491
|
-
</RecurProvider>
|
|
548
|
+
## Error Handling
|
|
492
549
|
|
|
493
|
-
|
|
494
|
-
const { checkout } = useRecur();
|
|
550
|
+
All errors are caught and passed to callbacks:
|
|
495
551
|
|
|
496
|
-
|
|
497
|
-
await checkout({
|
|
552
|
+
```tsx
|
|
553
|
+
await checkout({
|
|
554
|
+
planId: 'pro-monthly',
|
|
555
|
+
onError: (error) => {
|
|
556
|
+
console.error('Error code:', error.code);
|
|
557
|
+
console.error('Error message:', error.message);
|
|
498
558
|
|
|
499
|
-
//
|
|
500
|
-
|
|
559
|
+
// Handle specific errors
|
|
560
|
+
switch (error.code) {
|
|
561
|
+
case 'CHECKOUT_ERROR':
|
|
562
|
+
// Handle checkout initialization error
|
|
563
|
+
break;
|
|
564
|
+
case 'PAYMENT_FAILED':
|
|
565
|
+
// Handle payment failure
|
|
566
|
+
break;
|
|
567
|
+
default:
|
|
568
|
+
// Handle unknown error
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
});
|
|
501
572
|
```
|
|
502
573
|
|
|
503
|
-
|
|
574
|
+
Common error codes:
|
|
575
|
+
- `CHECKOUT_ERROR` - Failed to initialize checkout
|
|
576
|
+
- `PAYMENT_FAILED` - Payment processing failed
|
|
577
|
+
- `INVALID_PLAN` - Plan ID not found
|
|
578
|
+
- `API_ERROR` - API request failed
|
|
504
579
|
|
|
505
|
-
|
|
506
|
-
import { useRecur } from '@/lib/recur';
|
|
507
|
-
import { toast } from 'sonner';
|
|
580
|
+
---
|
|
508
581
|
|
|
509
|
-
|
|
510
|
-
const { checkout, isCheckingOut } = useRecur();
|
|
582
|
+
## Browser Support
|
|
511
583
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
toast.success('Redirecting to payment...');
|
|
517
|
-
},
|
|
518
|
-
onError: (error) => {
|
|
519
|
-
toast.error(error.message);
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
};
|
|
584
|
+
- Chrome/Edge 90+
|
|
585
|
+
- Firefox 88+
|
|
586
|
+
- Safari 14+
|
|
587
|
+
- Mobile browsers with Web Components support
|
|
523
588
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
589
|
+
The SDK uses modern Web Components (Custom Elements) which are widely supported in modern browsers.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Examples
|
|
594
|
+
|
|
595
|
+
Check out the `/examples` directory for complete working examples:
|
|
596
|
+
|
|
597
|
+
- **Next.js Basic** - Full Next.js integration with App Router
|
|
598
|
+
- **Vanilla JS** - Static HTML examples
|
|
599
|
+
- **React SPA** - Create React App example
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Migration Guide
|
|
604
|
+
|
|
605
|
+
### From v0.0.2 to v0.0.3
|
|
606
|
+
|
|
607
|
+
**Breaking Changes:**
|
|
608
|
+
- `organizationId` → `publishableKey` in config
|
|
609
|
+
- Checkout now uses Web Components instead of innerHTML
|
|
610
|
+
- New `usePlans()` hook for fetching plans
|
|
611
|
+
- Embedded mode is now the default when `containerElementId` is set
|
|
612
|
+
|
|
613
|
+
**Migration:**
|
|
614
|
+
|
|
615
|
+
```tsx
|
|
616
|
+
// Before (v0.0.2)
|
|
617
|
+
<RecurProvider config={{ organizationId: 'org_xxx' }}>
|
|
618
|
+
|
|
619
|
+
// After (v0.0.3)
|
|
620
|
+
<RecurProvider config={{ publishableKey: 'pk_test_xxx' }}>
|
|
530
621
|
```
|
|
531
622
|
|
|
623
|
+
---
|
|
624
|
+
|
|
625
|
+
## Contributing
|
|
626
|
+
|
|
627
|
+
This is part of the Recur project. Please see the main repository for contribution guidelines.
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
532
631
|
## License
|
|
533
632
|
|
|
534
|
-
|
|
633
|
+
MIT
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Support
|
|
638
|
+
|
|
639
|
+
- Documentation: https://github.com/kaikhq/recur.tw
|
|
640
|
+
- Issues: https://github.com/kaikhq/recur.tw/issues
|
|
641
|
+
- Email: support@recur.tw
|