recur-tw 0.9.13 → 0.10.1

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 CHANGED
@@ -1,896 +1,384 @@
1
- # Recur SDK (Taiwan)
1
+ # Recur SDK
2
2
 
3
- A React & Vanilla JS SDK for embedding subscription checkout flows with PAYUNi payment integration.
4
-
5
- **專為台灣市場設計** - 透過 PAYUNi 金流服務處理訂閱式付款。
3
+ 台灣訂閱制金流 SDK - 透過 PAYUNi 金流處理訂閱式與一次性付款。
6
4
 
7
- Taiwan-specific subscription checkout SDK with embedded payment forms powered by Web Components.
5
+ A React & Vanilla JS SDK for embedding subscription checkout flows with PAYUNi payment integration.
8
6
 
9
- ## 🚀 Features
7
+ ## Features
10
8
 
11
- - **Hosted Checkout** - Zero-backend integration with `<recur-checkout>` Web Component
12
- - **React SDK** - Full React integration with hooks (`useProducts`, `useRecur`)
13
- - **Vanilla JS** - Use with plain HTML/JavaScript (no framework required)
14
- - **Embedded Checkout** - Native payment forms with PAYUNi credit card fields
15
- - **Web Components** - Modern, encapsulated UI components
16
- - **Custom Styling** - Fully customizable with shadcn/ui compatibility
17
- - ✅ **TypeScript** - Full type definitions included
18
- - ✅ **SSR Safe** - Works with Next.js, Remix, and other SSR frameworks
19
- - ✅ **Taiwan-focused** - PAYUNi payment integration
9
+ - **Zero-config 嵌入** - 一行程式碼即可嵌入付款按鈕
10
+ - **React SDK** - 完整 React 整合 (`useProducts`, `useRecur`)
11
+ - **Vanilla JS** - JavaScript,無框架依賴
12
+ - **Web Components** - 現代化、封裝良好的 UI 元件
13
+ - **TypeScript** - 完整型別定義
14
+ - **SSR Safe** - 支援 Next.js、Remix SSR 框架
20
15
 
21
16
  ---
22
17
 
23
- ## 📦 Installation
18
+ ## 快速開始
24
19
 
25
- ### For React Projects
26
-
27
- ```bash
28
- npm install recur-tw
29
- # or
30
- pnpm add recur-tw
31
- # or
32
- yarn add recur-tw
33
- ```
20
+ ### 方式一:Checkout Button(最簡單)
34
21
 
35
- ### For Static HTML/JavaScript
22
+ Gumroad 一樣,只需一個 script 和一個連結:
36
23
 
37
24
  ```html
38
- <!-- Via CDN (unpkg) -->
39
- <script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script>
25
+ <script src="https://unpkg.com/recur-tw/dist/checkout.js"></script>
40
26
 
41
- <!-- Via CDN (jsdelivr) -->
42
- <script src="https://cdn.jsdelivr.net/npm/recur-tw@latest/dist/recur.umd.js"></script>
27
+ <a class="recur-button"
28
+ href="https://recur.tw/buy/prod_xxx"
29
+ data-key="pk_live_xxx">
30
+ 訂閱方案
31
+ </a>
43
32
  ```
44
33
 
45
- ---
46
-
47
- ## 🎯 Quick Start
48
-
49
- ### Option 1: React/Next.js
34
+ 點擊連結會開啟 modal 結帳視窗,完成後觸發事件:
50
35
 
51
- Full framework integration with React hooks and embedded checkout:
52
-
53
- ```tsx
54
- 'use client';
55
-
56
- import { RecurProvider, useProducts, useRecur } from 'recur-tw';
57
-
58
- // 1. Wrap your app with RecurProvider
59
- export default function App() {
60
- return (
61
- <RecurProvider
62
- config={{
63
- publishableKey: 'pk_test_xxx',
64
- containerElementId: 'recur-payment-container' // For embedded checkout
65
- }}
66
- >
67
- <ProductsPage />
68
- </RecurProvider>
69
- );
70
- }
71
-
72
- // 2. Fetch products and checkout
73
- function ProductsPage() {
74
- // Fetch all products (or filter by type: 'SUBSCRIPTION', 'ONE_TIME', etc.)
75
- const { data: products, isLoading } = useProducts();
76
- const { checkout, isCheckingOut } = useRecur();
77
-
78
- if (isLoading) return <div>Loading products...</div>;
79
-
80
- return (
81
- <div>
82
- {products?.map((product) => (
83
- <button
84
- key={product.id}
85
- onClick={() => checkout({
86
- planId: product.id,
87
- customerEmail: 'user@example.com',
88
- customerName: 'John Doe',
89
- })}
90
- disabled={isCheckingOut}
91
- >
92
- {/* product.type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION' */}
93
- {product.type === 'SUBSCRIPTION' ? 'Subscribe to' : 'Buy'} {product.name} - NT${product.price}
94
- </button>
95
- ))}
96
-
97
- {/* Embedded payment form will appear here */}
98
- <div id="recur-payment-container"></div>
99
- </div>
100
- );
101
- }
102
- ```
103
-
104
- ### Option 2: Vanilla JavaScript (Static HTML)
105
-
106
- Perfect for landing pages, marketing sites, or any static HTML:
107
-
108
- ```html
109
- <!DOCTYPE html>
110
- <html>
111
- <body>
112
- <button id="checkout-btn">訂閱方案</button>
113
-
114
- <script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script>
115
- <script>
116
- // Initialize SDK
117
- const recur = RecurCheckout.init({
118
- publishableKey: 'pk_test_your_key_here'
119
- });
120
-
121
- // Checkout on button click
122
- document.getElementById('checkout-btn').addEventListener('click', async () => {
123
- await recur.checkout({
124
- planId: 'plan_xxx',
125
- customerName: '王小明',
126
- customerEmail: 'user@example.com',
127
- mode: 'redirect' // 'redirect' or 'embedded'
128
- });
129
- });
130
- </script>
131
- </body>
132
- </html>
133
- ```
134
-
135
- ### Option 3: Hosted Checkout (Zero Backend)
36
+ ```javascript
37
+ window.addEventListener('recur:success', (e) => {
38
+ console.log('付款成功!', e.detail);
39
+ });
136
40
 
137
- **NEW!** The simplest integration - redirect to Recur's hosted checkout page:
41
+ window.addEventListener('recur:error', (e) => {
42
+ console.error('付款失敗', e.detail);
43
+ });
138
44
 
139
- ```html
140
- <!DOCTYPE html>
141
- <html>
142
- <body>
143
- <!-- Drop-in checkout button - no JavaScript required! -->
144
- <recur-checkout
145
- publishable-key="pk_test_xxx"
146
- product-id="prod_xxx"
147
- success-url="https://yoursite.com/success"
148
- cancel-url="https://yoursite.com/cancel">
149
- 訂閱方案
150
- </recur-checkout>
151
-
152
- <script src="https://unpkg.com/recur-tw@latest/dist/recur.umd.js"></script>
153
- </body>
154
- </html>
45
+ window.addEventListener('recur:close', () => {
46
+ console.log('使用者關閉視窗');
47
+ });
155
48
  ```
156
49
 
157
- Or use the JavaScript API:
50
+ **JavaScript API:**
158
51
 
159
52
  ```javascript
160
- const recur = RecurCheckout.init({
161
- publishableKey: 'pk_test_xxx'
162
- });
163
-
164
- // Redirect to hosted checkout
165
- await recur.redirectToCheckout({
53
+ // 程式控制開啟結帳
54
+ Recur.popup({
166
55
  productId: 'prod_xxx',
167
- successUrl: 'https://yoursite.com/success',
168
- cancelUrl: 'https://yoursite.com/cancel',
56
+ key: 'pk_live_xxx',
57
+ email: 'user@example.com',
58
+ name: '王小明'
169
59
  });
170
- ```
171
-
172
- ---
173
60
 
174
- ## 🔑 Get Your API Key
61
+ // 關閉結帳視窗
62
+ Recur.close();
63
+ ```
175
64
 
176
- 1. Go to your Recur dashboard
177
- 2. Navigate to **Settings** → **API Keys**
178
- 3. Click **Create Publishable Key**
179
- 4. Copy your `pk_test_...` or `pk_live_...` key
65
+ **Data 屬性:**
180
66
 
181
- > ⚠️ **Never** use your secret key (`sk_*`) in client-side code!
67
+ | 屬性 | 說明 |
68
+ |------|------|
69
+ | `data-key` | Publishable Key(必填) |
70
+ | `data-email` | 預填 Email |
71
+ | `data-name` | 預填姓名 |
182
72
 
183
73
  ---
184
74
 
185
- ## 📖 Documentation
186
-
187
- ### Checkout Modes
188
-
189
- #### Embedded Mode (Recommended)
75
+ ### 方式二:Floating Widget(浮動按鈕)
190
76
 
191
- Embed the full payment form directly in your page with PAYUNi credit card fields:
192
-
193
- ```tsx
194
- const { checkout } = useRecur();
77
+ Buy Me a Coffee 一樣的浮動按鈕:
195
78
 
196
- await checkout({
197
- planId: 'plan_xxx',
198
- customerEmail: 'user@example.com',
199
- customerName: 'John Doe',
200
- // mode: 'embedded' is default when containerElementId is configured
201
- });
79
+ ```html
80
+ <script
81
+ src="https://unpkg.com/recur-tw/dist/widget.js"
82
+ data-key="pk_live_xxx"
83
+ data-product="prod_xxx"
84
+ data-text="訂閱支持"
85
+ data-color="#667eea"
86
+ data-position="right"
87
+ ></script>
202
88
  ```
203
89
 
204
- **Features:**
205
- - ✅ Native PAYUNi credit card input fields
206
- - ✅ No redirects - complete flow in your app
207
- - ✅ Fully customizable styling
208
- - ✅ Web Components based (modern, encapsulated)
90
+ **Data 屬性:**
209
91
 
210
- #### Redirect Mode
92
+ | 屬性 | 說明 | 預設值 |
93
+ |------|------|--------|
94
+ | `data-key` | Publishable Key(必填) | - |
95
+ | `data-product` | 商品 ID(必填) | - |
96
+ | `data-text` | 按鈕文字 | 訂閱支持 |
97
+ | `data-color` | 按鈕顏色 | #667eea |
98
+ | `data-text-color` | 文字顏色 | #ffffff |
99
+ | `data-position` | 位置 (left/right) | right |
100
+ | `data-x-margin` | 水平邊距 (px) | 18 |
101
+ | `data-y-margin` | 垂直邊距 (px) | 18 |
211
102
 
212
- Redirect to a full-page checkout:
103
+ **JavaScript API:**
213
104
 
214
- ```tsx
215
- await checkout({
216
- planId: 'plan_xxx',
217
- mode: 'redirect',
218
- successUrl: 'https://yoursite.com/success',
219
- cancelUrl: 'https://yoursite.com/cancel',
220
- });
105
+ ```javascript
106
+ RecurWidget.open(); // 開啟結帳視窗
107
+ RecurWidget.close(); // 關閉結帳視窗
108
+ RecurWidget.show(); // 顯示浮動按鈕
109
+ RecurWidget.hide(); // 隱藏浮動按鈕
110
+ RecurWidget.destroy(); // 移除 Widget
221
111
  ```
222
112
 
223
- #### Hosted Checkout Mode (NEW!)
113
+ ---
224
114
 
225
- The simplest integration - no iframe or embedded form needed:
115
+ ### 方式三:Vanilla JavaScript
226
116
 
227
- ```tsx
117
+ 適合需要更多控制的場景:
118
+
119
+ ```html
120
+ <script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
121
+
122
+ <script>
228
123
  const recur = RecurCheckout.init({
229
- publishableKey: 'pk_test_xxx'
124
+ publishableKey: 'pk_live_xxx'
230
125
  });
231
126
 
232
- // Method 1: Redirect to hosted checkout
233
- await recur.redirectToCheckout({
127
+ // 取得商品列表
128
+ const { products } = await recur.fetchProducts();
129
+
130
+ // 開啟結帳(Modal 模式)
131
+ await recur.checkout({
234
132
  productId: 'prod_xxx',
235
- successUrl: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}',
236
- cancelUrl: 'https://yoursite.com/cancel',
237
- customerEmail: 'user@example.com', // optional, pre-fill email
238
- mode: 'SUBSCRIPTION', // PAYMENT, SUBSCRIPTION, or SETUP
133
+ customerEmail: 'user@example.com',
134
+ customerName: '王小明',
135
+ mode: 'modal',
136
+ onPaymentComplete: (result) => {
137
+ console.log('付款成功!', result);
138
+ },
139
+ onError: (error) => {
140
+ console.error('付款失敗', error);
141
+ }
239
142
  });
240
143
 
241
- // Method 2: Get session URL without redirecting
242
- const session = await recur.createCheckoutSession({
144
+ // 或重導到 Hosted Checkout
145
+ await recur.redirectToCheckout({
243
146
  productId: 'prod_xxx',
244
147
  successUrl: 'https://yoursite.com/success',
245
- cancelUrl: 'https://yoursite.com/cancel',
148
+ cancelUrl: 'https://yoursite.com/cancel'
246
149
  });
247
- console.log(session.url); // https://checkout.recur.tw/cs_xxx
248
- console.log(session.expiresAt); // Session expires in 30 minutes
150
+ </script>
249
151
  ```
250
152
 
251
- **Checkout Modes:**
252
- - `PAYMENT` - One-time payment
253
- - `SUBSCRIPTION` - Recurring subscription (default)
254
- - `SETUP` - Save card for future charges
255
-
256
- ---
153
+ **Checkout 模式:**
257
154
 
258
- ### `<recur-checkout>` Web Component
155
+ | 模式 | 說明 |
156
+ |------|------|
157
+ | `modal` | 在 Modal 中開啟付款表單(預設) |
158
+ | `iframe` | 嵌入到指定容器 |
159
+ | `redirect` | 重導到 Hosted Checkout 頁面 |
259
160
 
260
- A drop-in checkout button that requires zero JavaScript:
261
-
262
- ```html
263
- <recur-checkout
264
- publishable-key="pk_test_xxx"
265
- product-id="prod_xxx"
266
- success-url="/success"
267
- cancel-url="/cancel"
268
- customer-email="user@example.com"
269
- mode="SUBSCRIPTION"
270
- button-text="Subscribe Now"
271
- button-style="gradient">
272
- </recur-checkout>
273
- ```
274
-
275
- **Attributes:**
276
-
277
- | Attribute | Required | Description |
278
- |-----------|----------|-------------|
279
- | `publishable-key` | Yes | Your Recur publishable key |
280
- | `product-id` | Yes | The product ID to purchase |
281
- | `success-url` | Yes | Redirect URL after successful payment |
282
- | `cancel-url` | Yes | Redirect URL if user cancels |
283
- | `customer-email` | No | Pre-fill customer email |
284
- | `mode` | No | `PAYMENT`, `SUBSCRIPTION`, or `SETUP` |
285
- | `button-text` | No | Button label (default: button content) |
286
- | `button-style` | No | `primary`, `outline`, or `gradient` |
287
- | `disabled` | No | Disable the button |
288
- | `api-base-url` | No | Custom API URL (for development) |
161
+ ---
289
162
 
290
- **Events:**
163
+ ### 方式四:React / Next.js
291
164
 
292
- ```javascript
293
- document.querySelector('recur-checkout').addEventListener('checkout-started', (e) => {
294
- console.log('Redirecting to:', e.detail.url);
295
- });
296
-
297
- document.querySelector('recur-checkout').addEventListener('checkout-error', (e) => {
298
- console.error('Error:', e.detail.message);
299
- });
165
+ ```bash
166
+ npm install recur-tw
300
167
  ```
301
168
 
302
- ---
303
-
304
- ### React Hooks
305
-
306
- #### `useProducts()`
169
+ ```tsx
170
+ 'use client';
307
171
 
308
- Fetch available products with optional type filtering:
172
+ import { RecurProvider, useProducts, useRecur } from 'recur-tw';
309
173
 
310
- ```tsx
311
- import { useProducts } from 'recur-tw';
174
+ // 1. 包裝 Provider
175
+ export default function App() {
176
+ return (
177
+ <RecurProvider config={{ publishableKey: 'pk_live_xxx' }}>
178
+ <ProductsPage />
179
+ </RecurProvider>
180
+ );
181
+ }
312
182
 
183
+ // 2. 使用 Hooks
313
184
  function ProductsPage() {
314
- // Fetch all products
315
- const { data: products, isLoading, error } = useProducts();
316
-
317
- // Or filter by type
318
- // const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
319
- // const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
185
+ const { data: products, isLoading } = useProducts();
186
+ const { checkout, isCheckingOut } = useRecur();
320
187
 
321
- if (isLoading) return <div>Loading...</div>;
322
- if (error) return <div>Error: {error.message}</div>;
188
+ if (isLoading) return <div>載入中...</div>;
323
189
 
324
190
  return (
325
191
  <div>
326
192
  {products?.map((product) => (
327
- <div key={product.id}>
328
- <span className="badge">
329
- {product.type === 'SUBSCRIPTION' ? '訂閱' : '一次性'}
330
- </span>
331
- <h3>{product.name}</h3>
332
- <p>NT${product.price} {product.billingPeriod && `/ ${product.billingPeriod}`}</p>
333
- {product.trialDays && product.trialDays > 0 && <p>🎉 {product.trialDays} days free trial</p>}
334
- </div>
193
+ <button
194
+ key={product.id}
195
+ onClick={() => checkout({
196
+ productId: product.id,
197
+ customerEmail: 'user@example.com',
198
+ mode: 'modal'
199
+ })}
200
+ disabled={isCheckingOut}
201
+ >
202
+ {product.name} - NT${product.price}
203
+ </button>
335
204
  ))}
336
205
  </div>
337
206
  );
338
207
  }
339
208
  ```
340
209
 
341
- **Product Types:**
342
- - `SUBSCRIPTION` - Recurring subscription products
343
- - `ONE_TIME` - One-time purchase products
344
- - `CREDITS` - Credit/token packages
345
- - `DONATION` - Donation products
346
-
347
- #### `useRecur()`
210
+ **Hooks:**
348
211
 
349
- Access checkout functionality:
212
+ - `useProducts(options?)` - 取得商品列表
213
+ - `useRecur()` - 取得 checkout 函式與狀態
350
214
 
351
215
  ```tsx
352
- import { useRecur } from 'recur-tw';
353
-
354
- function CheckoutButton({ planId }: { planId: string }) {
355
- const { checkout, isCheckingOut, config, updateConfig } = useRecur();
356
-
357
- return (
358
- <button
359
- onClick={() => checkout({ planId })}
360
- disabled={isCheckingOut}
361
- >
362
- {isCheckingOut ? 'Processing...' : 'Subscribe'}
363
- </button>
364
- );
365
- }
216
+ // 篩選商品類型
217
+ const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
218
+ const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
366
219
  ```
367
220
 
368
- **Returns:**
369
- - `checkout(options)` - Initiate checkout flow
370
- - `isCheckingOut` - Boolean indicating if checkout is in progress
371
- - `config` - Current SDK configuration
372
- - `updateConfig(newConfig)` - Update configuration dynamically
373
- - `fetchProducts(options)` - Manually fetch products (optionally filter by type)
374
- - `fetchPlans()` - Fetch subscription products only (backward compat)
375
-
376
221
  ---
377
222
 
378
- ### Configuration
223
+ ### 方式五:`<recur-checkout>` Web Component
379
224
 
380
- #### RecurProvider Props
225
+ JavaScript 的結帳按鈕:
381
226
 
382
- ```tsx
383
- interface RecurConfig {
384
- // Required: Your publishable API key
385
- publishableKey: string;
386
-
387
- // Optional: API base URL (defaults to production)
388
- baseUrl?: string;
389
-
390
- // Optional: Container element ID for embedded checkout
391
- containerElementId?: string;
392
- }
393
- ```
394
-
395
- Example:
396
-
397
- ```tsx
398
- <RecurProvider
399
- config={{
400
- publishableKey: process.env.NEXT_PUBLIC_RECUR_KEY!,
401
- baseUrl: 'https://api.recur.tw', // optional
402
- containerElementId: 'recur-payment-container',
403
- }}
404
- >
405
- {children}
406
- </RecurProvider>
407
- ```
408
-
409
- #### Checkout Options
410
-
411
- ```tsx
412
- interface CheckoutOptions {
413
- // Required: Plan ID to subscribe to
414
- planId: string;
415
-
416
- // Customer information (email is required)
417
- customerEmail: string; // Required: primary customer identifier
418
- customerName?: string;
419
-
420
- // External customer ID - link to your existing users (optional, immutable once set)
421
- externalCustomerId?: string;
422
-
423
- // Checkout mode
424
- mode?: 'embedded' | 'redirect'; // Default: 'embedded' if containerElementId is set
425
-
426
- // Redirect URLs (for redirect mode)
427
- successUrl?: string;
428
- cancelUrl?: string;
227
+ ```html
228
+ <script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
429
229
 
430
- // Callbacks
431
- onPaymentComplete?: (subscription: Subscription) => void;
432
- onError?: (error: CheckoutError) => void;
433
- }
230
+ <recur-checkout
231
+ publishable-key="pk_live_xxx"
232
+ product-id="prod_xxx"
233
+ success-url="/success"
234
+ cancel-url="/cancel"
235
+ button-text="立即訂閱"
236
+ button-style="gradient">
237
+ </recur-checkout>
434
238
  ```
435
239
 
436
240
  ---
437
241
 
438
- ### Customer Identification
242
+ ## Server SDK
439
243
 
440
- The SDK uses email as the primary customer identifier, with optional external ID support.
244
+ 後端驗證與管理:
441
245
 
442
- #### Required Fields
246
+ ```typescript
247
+ import { RecurServer } from 'recur-tw/server';
443
248
 
444
- - `customerEmail` - **Required**. Customer's email address (primary identifier)
445
- - `externalCustomerId` - Optional. Your system's user ID
446
-
447
- #### Using External Customer ID
448
-
449
- The `externalCustomerId` parameter allows you to link Recur subscriptions to your existing user database:
450
-
451
- ```tsx
452
- // React
453
- await checkout({
454
- planId: 'plan_xxx',
455
- customerEmail: 'user@example.com', // Required
456
- customerName: 'John Doe',
457
- externalCustomerId: 'user_12345', // Optional: Your system's user ID
249
+ const recur = new RecurServer({
250
+ secretKey: 'sk_live_xxx'
458
251
  });
459
252
 
460
- // Vanilla JS
461
- await recur.checkout({
462
- planId: 'plan_xxx',
463
- customerEmail: 'user@example.com', // Required
464
- externalCustomerId: 'cus_abc456',
253
+ // 建立 Portal Session(讓客戶管理訂閱)
254
+ const session = await recur.portalSessions.create({
255
+ customerId: 'cus_xxx',
256
+ returnUrl: 'https://yoursite.com/account'
465
257
  });
466
258
 
467
- // Hosted Checkout
468
- await recur.redirectToCheckout({
469
- productId: 'prod_xxx',
470
- successUrl: '/success',
471
- cancelUrl: '/cancel',
472
- customerEmail: 'user@example.com', // Required
473
- externalCustomerId: 'user_12345',
474
- });
259
+ // 重導客戶到 Portal
260
+ res.redirect(session.url);
475
261
  ```
476
262
 
477
- #### Customer Resolution Rules
478
-
479
- When processing a checkout:
480
-
481
- 1. **externalCustomerId provided** - Looks up customer by external ID first
482
- - If found, verifies email matches (throws error if mismatch)
483
- - If not found, checks if email already exists
484
- 2. **Email lookup** - If external ID not found or not provided
485
- - If email exists and external ID was provided → Error (cannot add external ID to existing customer)
486
- - If email exists without external ID → Returns existing customer
487
- 3. **Create new** - Creates a new customer if neither found
488
-
489
- #### Important: External ID is Immutable
490
-
491
- The `externalCustomerId` can only be set when **creating** a customer. Once set:
492
- - It cannot be changed
493
- - It cannot be added to an existing customer
494
- - It cannot be transferred to another customer
495
-
496
- This design ensures data consistency and prevents accidental customer merges.
497
-
498
- #### Retrieving Customer by External ID
499
-
500
- Use the API to retrieve customers by their external ID:
501
-
502
- ```bash
503
- GET /api/v1/subscribers/external/{externalCustomerId}
504
- ```
505
-
506
- #### Best Practices
507
-
508
- - **Always provide email**: Email is required for all checkout operations
509
- - **Set external ID early**: Include `externalCustomerId` in the initial checkout request when creating customers
510
- - **Use consistent IDs**: Use your database primary key or UUID as the external ID
511
- - **Plan ahead**: Decide whether to use external IDs before your first customer signup
512
-
513
263
  ---
514
264
 
515
- ### Custom Styling
516
-
517
- The SDK uses Web Components with customizable styles. You can override PAYUNi input styles to match your design system.
518
-
519
- #### Default Styling (shadcn/ui compatible)
520
-
521
- The SDK automatically applies shadcn-compatible focus rings:
522
-
523
- ```css
524
- .form-input-focus {
525
- border-color: var(--ring, hsl(215 16% 47%)) !important;
526
- box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent) !important;
527
- }
528
- ```
529
-
530
- #### Custom Styling
265
+ ## API Key
531
266
 
532
- Override styles by providing custom CSS:
267
+ 1. 前往 Recur Dashboard
268
+ 2. **Settings** → **API Keys**
269
+ 3. 建立 Publishable Key (`pk_*`) 用於前端
270
+ 4. 建立 Secret Key (`sk_*`) 用於後端
533
271
 
534
- ```tsx
535
- const paymentForm = document.createElement('recur-payment-form');
536
- paymentForm.setAttribute('custom-styles', `
537
- /* Custom focus style */
538
- .form-input-focus {
539
- border-color: #3b82f6 !important;
540
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2) !important;
541
- }
542
-
543
- /* Adjust container heights */
544
- #recur-payment-container-card-no,
545
- #recur-payment-container-card-exp,
546
- #recur-payment-container-card-cvc {
547
- height: 40px !important;
548
- }
549
- `);
550
- ```
551
-
552
- The custom styles are injected into the Light DOM, allowing them to affect PAYUNi's iframe elements while keeping other UI styles encapsulated.
272
+ > **注意:** 永遠不要在前端使用 Secret Key!
553
273
 
554
274
  ---
555
275
 
556
- ### Advanced Examples
557
-
558
- #### Embedded Checkout with Error Handling
559
-
560
- ```tsx
561
- 'use client';
562
-
563
- import { useState } from 'react';
564
- import { useRecur } from 'recur-tw';
565
-
566
- export function CheckoutForm({ planId }: { planId: string }) {
567
- const { checkout, isCheckingOut } = useRecur();
568
- const [error, setError] = useState<string | null>(null);
569
-
570
- const handleCheckout = async () => {
571
- setError(null);
572
-
573
- await checkout({
574
- planId,
575
- customerEmail: 'user@example.com',
576
- onPaymentComplete: (subscription) => {
577
- console.log('Payment successful!', subscription);
578
- // Redirect to success page or show confirmation
579
- },
580
- onError: (err) => {
581
- setError(err.message);
582
- },
583
- });
584
- };
585
-
586
- return (
587
- <div>
588
- <button onClick={handleCheckout} disabled={isCheckingOut}>
589
- {isCheckingOut ? 'Processing...' : 'Subscribe Now'}
590
- </button>
591
-
592
- {error && <div className="error">{error}</div>}
593
-
594
- {/* Payment form will appear here */}
595
- <div id="recur-payment-container"></div>
596
- </div>
597
- );
598
- }
599
- ```
600
-
601
- #### Dynamic Product Selection
602
-
603
- ```tsx
604
- 'use client';
276
+ ## 商品類型
605
277
 
606
- import { useState } from 'react';
607
- import { useProducts, useRecur } from 'recur-tw';
278
+ | 類型 | 說明 |
279
+ |------|------|
280
+ | `SUBSCRIPTION` | 訂閱制商品(週期性扣款) |
281
+ | `ONE_TIME` | 一次性購買 |
282
+ | `CREDITS` | 點數/代幣包 |
283
+ | `DONATION` | 贊助/捐款 |
608
284
 
609
- export function PricingTable() {
610
- // Filter to show only subscription products
611
- const { data: products } = useProducts({ type: 'SUBSCRIPTION' });
612
- const { checkout, isCheckingOut } = useRecur();
613
- const [selectedProduct, setSelectedProduct] = useState<string | null>(null);
614
-
615
- const handleSelectProduct = async (productId: string) => {
616
- setSelectedProduct(productId);
617
-
618
- await checkout({
619
- planId: productId,
620
- customerEmail: 'user@example.com',
621
- customerName: 'John Doe',
622
- });
623
- };
624
-
625
- return (
626
- <div>
627
- {!selectedProduct ? (
628
- // Product selection
629
- <div className="products-grid">
630
- {products?.map((product) => (
631
- <div key={product.id} className="product-card">
632
- <h3>{product.name}</h3>
633
- <p className="price">NT${product.price} / {product.billingPeriod}</p>
634
- {product.trialDays && product.trialDays > 0 && (
635
- <p className="trial">🎉 {product.trialDays} day free trial</p>
636
- )}
637
- <button onClick={() => handleSelectProduct(product.id)}>
638
- Select Product
639
- </button>
640
- </div>
641
- ))}
642
- </div>
643
- ) : (
644
- // Payment form
645
- <div>
646
- <button onClick={() => setSelectedProduct(null)}>← Back to products</button>
647
- <div id="recur-payment-container"></div>
648
- </div>
649
- )}
650
- </div>
651
- );
652
- }
653
- ```
285
+ ---
654
286
 
655
- #### Update Config Dynamically
287
+ ## 事件
656
288
 
657
- ```tsx
658
- const { config, updateConfig } = useRecur();
289
+ 所有嵌入方式都支援相同的事件:
659
290
 
660
- // Switch to different API endpoint
661
- updateConfig({
662
- baseUrl: 'https://staging-api.recur.tw'
291
+ ```javascript
292
+ // 付款成功
293
+ window.addEventListener('recur:success', (e) => {
294
+ console.log(e.detail); // { subscriptionId, orderId, ... }
663
295
  });
664
296
 
665
- // Update publishable key
666
- updateConfig({
667
- publishableKey: 'pk_live_xxx'
297
+ // 付款失敗
298
+ window.addEventListener('recur:error', (e) => {
299
+ console.log(e.detail); // { message, code }
668
300
  });
669
- ```
670
-
671
- ---
672
-
673
- ## Web Components Architecture
674
301
 
675
- The SDK uses modern Web Components for better encapsulation and reusability:
676
-
677
- ### RecurPaymentForm Component
678
-
679
- The embedded payment form is a custom element:
680
-
681
- ```html
682
- <recur-payment-form
683
- container-id="recur-payment-container"
684
- custom-styles="..."
685
- ></recur-payment-form>
686
- ```
687
-
688
- **Features:**
689
- - Shadow DOM for style isolation
690
- - Light DOM for PAYUNi iframe integration
691
- - Custom events for form submission
692
- - Attribute-based configuration
693
-
694
- **Events:**
695
- - `submit` - Fired when user submits payment
696
- - `error` - Fired when an error occurs
697
-
698
- **Custom Styling:**
699
- Styles are injected into Light DOM to affect PAYUNi iframes:
700
-
701
- ```javascript
702
- paymentForm.setAttribute('custom-styles', `
703
- .form-input-focus { /* PAYUNi focus styles */ }
704
- #container-card-no { /* Card number field styles */ }
705
- `);
302
+ // 使用者關閉視窗
303
+ window.addEventListener('recur:close', () => {
304
+ console.log('Closed');
305
+ });
706
306
  ```
707
307
 
708
308
  ---
709
309
 
710
- ## TypeScript Support
711
-
712
- The SDK is written in TypeScript and provides full type definitions:
310
+ ## TypeScript
713
311
 
714
- ```tsx
312
+ ```typescript
715
313
  import type {
716
314
  RecurConfig,
717
- RecurContextValue,
315
+ Product,
316
+ ProductsResult,
718
317
  CheckoutOptions,
719
318
  CheckoutResult,
720
319
  CheckoutError,
721
- Product,
722
- ProductsResult,
723
- FetchProductsOptions,
724
- Subscription,
725
- SubscriptionResult,
726
- // Backward compatibility aliases
727
- Plan, // = Product
728
- PlansResult, // = ProductsResult
320
+ Subscription
729
321
  } from 'recur-tw';
730
322
  ```
731
323
 
732
- ### Type Definitions
733
-
734
- ```tsx
735
- interface Product {
736
- id: string;
737
- name: string;
738
- slug: string;
739
- description: string | null;
740
- // Product type distinguishes between recurring and one-time purchases
741
- type: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
742
- billingPeriod: 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'ONE_TIME' | 'CUSTOM' | null;
743
- price: number;
744
- currency: string;
745
- trialDays: number | null;
746
- metadata: ProductMetadata | null;
747
- productFamily?: string | null;
748
- displayOrder: number;
749
- }
750
-
751
- interface FetchProductsOptions {
752
- type?: 'SUBSCRIPTION' | 'ONE_TIME' | 'CREDITS' | 'DONATION';
753
- }
754
-
755
- // Backward compatibility
756
- type Plan = Product;
757
-
758
- interface Subscription {
759
- id: string;
760
- status: string;
761
- planId: string;
762
- amount: number;
763
- billingPeriod: string;
764
- currentPeriodStart: string;
765
- currentPeriodEnd: string;
766
- }
767
-
768
- interface CheckoutError {
769
- code: string;
770
- message: string;
771
- details?: any;
772
- }
773
- ```
774
-
775
324
  ---
776
325
 
777
- ## Error Handling
326
+ ## CDN 連結
778
327
 
779
- All errors are caught and passed to callbacks:
328
+ | 檔案 | 用途 | CDN |
329
+ |------|------|-----|
330
+ | `checkout.js` | Gumroad 風格按鈕 | `https://unpkg.com/recur-tw/dist/checkout.js` |
331
+ | `widget.js` | 浮動按鈕 Widget | `https://unpkg.com/recur-tw/dist/widget.js` |
332
+ | `recur.umd.js` | Vanilla JS 完整版 | `https://unpkg.com/recur-tw/dist/recur.umd.js` |
780
333
 
781
- ```tsx
782
- await checkout({
783
- planId: 'pro-monthly',
784
- onError: (error) => {
785
- console.error('Error code:', error.code);
786
- console.error('Error message:', error.message);
787
-
788
- // Handle specific errors
789
- switch (error.code) {
790
- case 'CHECKOUT_ERROR':
791
- // Handle checkout initialization error
792
- break;
793
- case 'PAYMENT_FAILED':
794
- // Handle payment failure
795
- break;
796
- default:
797
- // Handle unknown error
798
- }
799
- },
800
- });
334
+ 或使用 jsDelivr:
335
+ ```
336
+ https://cdn.jsdelivr.net/npm/recur-tw/dist/checkout.js
801
337
  ```
802
-
803
- Common error codes:
804
- - `CHECKOUT_ERROR` - Failed to initialize checkout
805
- - `PAYMENT_FAILED` - Payment processing failed
806
- - `INVALID_PLAN` - Plan ID not found
807
- - `API_ERROR` - API request failed
808
338
 
809
339
  ---
810
340
 
811
- ## Browser Support
341
+ ## 瀏覽器支援
812
342
 
813
343
  - Chrome/Edge 90+
814
344
  - Firefox 88+
815
345
  - Safari 14+
816
- - Mobile browsers with Web Components support
817
-
818
- The SDK uses modern Web Components (Custom Elements) which are widely supported in modern browsers.
819
-
820
- ---
821
-
822
- ## Examples
823
-
824
- Check out the `/examples` directory for complete working examples:
825
-
826
- - **Next.js Basic** - Full Next.js integration with App Router
827
- - **Vanilla JS** - Static HTML examples
828
- - **React SPA** - Create React App example
346
+ - 支援 Web Components 的行動瀏覽器
829
347
 
830
348
  ---
831
349
 
832
- ## Migration Guide
833
-
834
- ### From v0.2.0 to v0.3.0
835
-
836
- **Breaking Changes:**
837
- - 移除 `/v1/checkout/init` 端點
838
- - Vanilla JS `createEmbeddedCheckout()` 改用 `/v1/checkouts` API
839
- - 付款執行改用 `/v1/checkouts/:id/pay`
840
-
841
- **New API Endpoints:**
842
- ```
843
- POST /v1/checkouts → 建立 checkout session
844
- GET /v1/checkouts/:id → 取得狀態
845
- POST /v1/checkouts/:id → 刷新 SDK Token
846
- DELETE /v1/checkouts/:id → 取消
847
- POST /v1/checkouts/:id/pay → 執行付款
848
- ```
849
-
850
- **無需程式碼變更** - SDK 內部已自動遷移到新端點。
851
-
852
- ### From v0.0.x to v0.1.0
350
+ ## 範例
853
351
 
854
- **Breaking Changes:**
855
- - `organizationId` → `publishableKey` in config
856
- - Checkout now uses Web Components instead of innerHTML
857
- - New `useProducts()` hook for fetching products (replaces `usePlans()`)
858
- - Embedded mode is now the default when `containerElementId` is set
352
+ 查看 `/examples` 目錄:
859
353
 
860
- **Migration:**
354
+ - `checkout-button-test.html` - Checkout Button 測試
355
+ - `widget-test.html` - Widget 測試
861
356
 
862
- ```tsx
863
- // Before
864
- <RecurProvider config={{ organizationId: 'org_xxx' }}>
865
-
866
- // After
867
- <RecurProvider config={{ publishableKey: 'pk_test_xxx' }}>
357
+ 本地測試:
358
+ ```bash
359
+ cd packages/recur-sdk
360
+ pnpm examples
868
361
  ```
869
362
 
870
363
  ---
871
364
 
872
- ## Contributing
873
-
874
- This is part of the Recur project. Please see the main repository for contribution guidelines.
875
-
876
- ---
877
-
878
365
  ## License
879
366
 
880
- This project is licensed under the [Elastic License 2.0](./LICENSE).
367
+ [Elastic License 2.0](./LICENSE)
881
368
 
882
- **你可以:**
883
- - 在你的應用程式中使用此 SDK
369
+ **可以:**
370
+ - 在你的應用程式中免費使用此 SDK
884
371
  - 修改程式碼供自己使用
372
+ - 分發修改後的版本(需附帶授權條款並標註修改)
885
373
 
886
- **你不可以:**
887
- - 將此 SDK 作為託管服務提供給第三方
374
+ **不可以:**
375
+ - 將此 SDK 作為託管/管理服務提供給第三方
888
376
  - 移除或規避授權機制
377
+ - 移除版權聲明或授權資訊
889
378
 
890
379
  ---
891
380
 
892
381
  ## Support
893
382
 
894
- - Documentation: https://github.com/kaikhq/recur.tw
895
383
  - Issues: https://github.com/kaikhq/recur.tw/issues
896
384
  - Email: support@recur.tw