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 +225 -737
- package/dist/checkout.js +126 -0
- package/dist/recur.umd.js +1 -1
- package/dist/widget.js +132 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,896 +1,384 @@
|
|
|
1
|
-
# Recur SDK
|
|
1
|
+
# Recur SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**專為台灣市場設計** - 透過 PAYUNi 金流服務處理訂閱式付款。
|
|
3
|
+
台灣訂閱制金流 SDK - 透過 PAYUNi 金流處理訂閱式與一次性付款。
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
A React & Vanilla JS SDK for embedding subscription checkout flows with PAYUNi payment integration.
|
|
8
6
|
|
|
9
|
-
##
|
|
7
|
+
## Features
|
|
10
8
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
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
|
-
##
|
|
18
|
+
## 快速開始
|
|
24
19
|
|
|
25
|
-
###
|
|
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
|
-
|
|
22
|
+
像 Gumroad 一樣,只需一個 script 和一個連結:
|
|
36
23
|
|
|
37
24
|
```html
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
41
|
+
window.addEventListener('recur:error', (e) => {
|
|
42
|
+
console.error('付款失敗', e.detail);
|
|
43
|
+
});
|
|
138
44
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
50
|
+
**JavaScript API:**
|
|
158
51
|
|
|
159
52
|
```javascript
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Redirect to hosted checkout
|
|
165
|
-
await recur.redirectToCheckout({
|
|
53
|
+
// 程式控制開啟結帳
|
|
54
|
+
Recur.popup({
|
|
166
55
|
productId: 'prod_xxx',
|
|
167
|
-
|
|
168
|
-
|
|
56
|
+
key: 'pk_live_xxx',
|
|
57
|
+
email: 'user@example.com',
|
|
58
|
+
name: '王小明'
|
|
169
59
|
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
---
|
|
173
60
|
|
|
174
|
-
|
|
61
|
+
// 關閉結帳視窗
|
|
62
|
+
Recur.close();
|
|
63
|
+
```
|
|
175
64
|
|
|
176
|
-
|
|
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
|
-
|
|
67
|
+
| 屬性 | 說明 |
|
|
68
|
+
|------|------|
|
|
69
|
+
| `data-key` | Publishable Key(必填) |
|
|
70
|
+
| `data-email` | 預填 Email |
|
|
71
|
+
| `data-name` | 預填姓名 |
|
|
182
72
|
|
|
183
73
|
---
|
|
184
74
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
### Checkout Modes
|
|
188
|
-
|
|
189
|
-
#### Embedded Mode (Recommended)
|
|
75
|
+
### 方式二:Floating Widget(浮動按鈕)
|
|
190
76
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
const { checkout } = useRecur();
|
|
77
|
+
像 Buy Me a Coffee 一樣的浮動按鈕:
|
|
195
78
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
**JavaScript API:**
|
|
213
104
|
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
105
|
+
```javascript
|
|
106
|
+
RecurWidget.open(); // 開啟結帳視窗
|
|
107
|
+
RecurWidget.close(); // 關閉結帳視窗
|
|
108
|
+
RecurWidget.show(); // 顯示浮動按鈕
|
|
109
|
+
RecurWidget.hide(); // 隱藏浮動按鈕
|
|
110
|
+
RecurWidget.destroy(); // 移除 Widget
|
|
221
111
|
```
|
|
222
112
|
|
|
223
|
-
|
|
113
|
+
---
|
|
224
114
|
|
|
225
|
-
|
|
115
|
+
### 方式三:Vanilla JavaScript
|
|
226
116
|
|
|
227
|
-
|
|
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: '
|
|
124
|
+
publishableKey: 'pk_live_xxx'
|
|
230
125
|
});
|
|
231
126
|
|
|
232
|
-
//
|
|
233
|
-
await recur.
|
|
127
|
+
// 取得商品列表
|
|
128
|
+
const { products } = await recur.fetchProducts();
|
|
129
|
+
|
|
130
|
+
// 開啟結帳(Modal 模式)
|
|
131
|
+
await recur.checkout({
|
|
234
132
|
productId: 'prod_xxx',
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
console.log(session.expiresAt); // Session expires in 30 minutes
|
|
150
|
+
</script>
|
|
249
151
|
```
|
|
250
152
|
|
|
251
|
-
**Checkout
|
|
252
|
-
- `PAYMENT` - One-time payment
|
|
253
|
-
- `SUBSCRIPTION` - Recurring subscription (default)
|
|
254
|
-
- `SETUP` - Save card for future charges
|
|
255
|
-
|
|
256
|
-
---
|
|
153
|
+
**Checkout 模式:**
|
|
257
154
|
|
|
258
|
-
|
|
155
|
+
| 模式 | 說明 |
|
|
156
|
+
|------|------|
|
|
157
|
+
| `modal` | 在 Modal 中開啟付款表單(預設) |
|
|
158
|
+
| `iframe` | 嵌入到指定容器 |
|
|
159
|
+
| `redirect` | 重導到 Hosted Checkout 頁面 |
|
|
259
160
|
|
|
260
|
-
|
|
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
|
-
|
|
163
|
+
### 方式四:React / Next.js
|
|
291
164
|
|
|
292
|
-
```
|
|
293
|
-
|
|
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
|
-
|
|
172
|
+
import { RecurProvider, useProducts, useRecur } from 'recur-tw';
|
|
309
173
|
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
const {
|
|
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
|
|
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
|
-
<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
212
|
+
- `useProducts(options?)` - 取得商品列表
|
|
213
|
+
- `useRecur()` - 取得 checkout 函式與狀態
|
|
350
214
|
|
|
351
215
|
```tsx
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
###
|
|
223
|
+
### 方式五:`<recur-checkout>` Web Component
|
|
379
224
|
|
|
380
|
-
|
|
225
|
+
零 JavaScript 的結帳按鈕:
|
|
381
226
|
|
|
382
|
-
```
|
|
383
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
242
|
+
## Server SDK
|
|
439
243
|
|
|
440
|
-
|
|
244
|
+
後端驗證與管理:
|
|
441
245
|
|
|
442
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
import { RecurServer } from 'recur-tw/server';
|
|
443
248
|
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
//
|
|
461
|
-
await recur.
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
//
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
1. 前往 Recur Dashboard
|
|
268
|
+
2. **Settings** → **API Keys**
|
|
269
|
+
3. 建立 Publishable Key (`pk_*`) 用於前端
|
|
270
|
+
4. 建立 Secret Key (`sk_*`) 用於後端
|
|
533
271
|
|
|
534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
607
|
-
|
|
278
|
+
| 類型 | 說明 |
|
|
279
|
+
|------|------|
|
|
280
|
+
| `SUBSCRIPTION` | 訂閱制商品(週期性扣款) |
|
|
281
|
+
| `ONE_TIME` | 一次性購買 |
|
|
282
|
+
| `CREDITS` | 點數/代幣包 |
|
|
283
|
+
| `DONATION` | 贊助/捐款 |
|
|
608
284
|
|
|
609
|
-
|
|
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
|
-
|
|
287
|
+
## 事件
|
|
656
288
|
|
|
657
|
-
|
|
658
|
-
const { config, updateConfig } = useRecur();
|
|
289
|
+
所有嵌入方式都支援相同的事件:
|
|
659
290
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
291
|
+
```javascript
|
|
292
|
+
// 付款成功
|
|
293
|
+
window.addEventListener('recur:success', (e) => {
|
|
294
|
+
console.log(e.detail); // { subscriptionId, orderId, ... }
|
|
663
295
|
});
|
|
664
296
|
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
|
711
|
-
|
|
712
|
-
The SDK is written in TypeScript and provides full type definitions:
|
|
310
|
+
## TypeScript
|
|
713
311
|
|
|
714
|
-
```
|
|
312
|
+
```typescript
|
|
715
313
|
import type {
|
|
716
314
|
RecurConfig,
|
|
717
|
-
|
|
315
|
+
Product,
|
|
316
|
+
ProductsResult,
|
|
718
317
|
CheckoutOptions,
|
|
719
318
|
CheckoutResult,
|
|
720
319
|
CheckoutError,
|
|
721
|
-
|
|
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
|
-
##
|
|
326
|
+
## CDN 連結
|
|
778
327
|
|
|
779
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
##
|
|
341
|
+
## 瀏覽器支援
|
|
812
342
|
|
|
813
343
|
- Chrome/Edge 90+
|
|
814
344
|
- Firefox 88+
|
|
815
345
|
- Safari 14+
|
|
816
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
354
|
+
- `checkout-button-test.html` - Checkout Button 測試
|
|
355
|
+
- `widget-test.html` - Widget 測試
|
|
861
356
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
367
|
+
[Elastic License 2.0](./LICENSE)
|
|
881
368
|
|
|
882
|
-
|
|
883
|
-
-
|
|
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
|