recur-tw 0.10.8 → 0.12.0
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 +110 -6
- package/dist/index.cjs +255 -20
- package/dist/index.d.cts +451 -2
- package/dist/index.d.ts +451 -2
- package/dist/index.js +255 -21
- package/dist/recur.umd.js +178 -33
- package/dist/server.cjs +174 -1
- package/dist/server.d.cts +245 -1
- package/dist/server.d.ts +245 -1
- package/dist/server.js +174 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -211,6 +211,7 @@ function ProductsPage() {
|
|
|
211
211
|
|
|
212
212
|
- `useProducts(options?)` - 取得商品列表
|
|
213
213
|
- `useRecur()` - 取得 checkout 函式與狀態
|
|
214
|
+
- `useCustomer()` - 檢查客戶權限與訂閱狀態
|
|
214
215
|
|
|
215
216
|
```tsx
|
|
216
217
|
// 篩選商品類型
|
|
@@ -218,6 +219,57 @@ const { data: subscriptions } = useProducts({ type: 'SUBSCRIPTION' });
|
|
|
218
219
|
const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
|
|
219
220
|
```
|
|
220
221
|
|
|
222
|
+
#### 權限檢查(useCustomer)
|
|
223
|
+
|
|
224
|
+
檢查客戶是否有特定產品的訂閱權限,無需自建資料庫或處理 Webhook:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { RecurProvider, useCustomer } from 'recur-tw';
|
|
228
|
+
|
|
229
|
+
// 1. Provider 需傳入 customer 識別資訊
|
|
230
|
+
function App() {
|
|
231
|
+
const user = useAuth(); // 你的認證系統
|
|
232
|
+
return (
|
|
233
|
+
<RecurProvider
|
|
234
|
+
config={{ publishableKey: 'pk_live_xxx' }}
|
|
235
|
+
customer={{ email: user?.email }}
|
|
236
|
+
>
|
|
237
|
+
<PremiumFeature />
|
|
238
|
+
</RecurProvider>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. 使用 check() 檢查權限
|
|
243
|
+
function PremiumFeature() {
|
|
244
|
+
const { check, isLoading } = useCustomer();
|
|
245
|
+
|
|
246
|
+
if (isLoading) return <Spinner />;
|
|
247
|
+
|
|
248
|
+
const { allowed } = check({ product: 'pro-plan' });
|
|
249
|
+
|
|
250
|
+
if (!allowed) {
|
|
251
|
+
return <UpgradePrompt />;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return <PremiumContent />;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 3. 顯示訂閱狀態
|
|
258
|
+
function AccountPage() {
|
|
259
|
+
const { customer, subscription, entitlements } = useCustomer();
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div>
|
|
263
|
+
<p>方案:{subscription?.product.name}</p>
|
|
264
|
+
<p>到期日:{subscription?.currentPeriodEnd}</p>
|
|
265
|
+
<p>擁有權限:{entitlements.map(e => e.product).join(', ')}</p>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
> **安全提醒:** `check()` 從本地快取讀取,可被繞過。僅用於 UI 控制,敏感操作請在後端驗證。
|
|
272
|
+
|
|
221
273
|
---
|
|
222
274
|
|
|
223
275
|
### 方式五:`<recur-checkout>` Web Component
|
|
@@ -244,15 +296,13 @@ const { data: oneTimeProducts } = useProducts({ type: 'ONE_TIME' });
|
|
|
244
296
|
後端驗證與管理:
|
|
245
297
|
|
|
246
298
|
```typescript
|
|
247
|
-
import {
|
|
299
|
+
import { Recur } from 'recur-tw/server';
|
|
248
300
|
|
|
249
|
-
const recur = new
|
|
250
|
-
secretKey: 'sk_live_xxx'
|
|
251
|
-
});
|
|
301
|
+
const recur = new Recur(process.env.RECUR_SECRET_KEY!);
|
|
252
302
|
|
|
253
303
|
// 建立 Portal Session(讓客戶管理訂閱)
|
|
254
|
-
const session = await recur.
|
|
255
|
-
|
|
304
|
+
const session = await recur.portal.sessions.create({
|
|
305
|
+
customer: 'cus_xxx',
|
|
256
306
|
returnUrl: 'https://yoursite.com/account'
|
|
257
307
|
});
|
|
258
308
|
|
|
@@ -260,6 +310,60 @@ const session = await recur.portalSessions.create({
|
|
|
260
310
|
res.redirect(session.url);
|
|
261
311
|
```
|
|
262
312
|
|
|
313
|
+
### 權限檢查(Server-side)
|
|
314
|
+
|
|
315
|
+
在 API 路由中驗證客戶權限,確保安全:
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { Recur } from 'recur-tw/server';
|
|
319
|
+
|
|
320
|
+
const recur = new Recur(process.env.RECUR_SECRET_KEY!);
|
|
321
|
+
|
|
322
|
+
// API 路由範例
|
|
323
|
+
export async function GET(request: Request) {
|
|
324
|
+
const user = await getUser(request);
|
|
325
|
+
|
|
326
|
+
// 檢查單一產品權限
|
|
327
|
+
const { allowed } = await recur.entitlements.check({
|
|
328
|
+
product: 'pro-plan',
|
|
329
|
+
customer: { email: user.email },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!allowed) {
|
|
333
|
+
return Response.json(
|
|
334
|
+
{ error: '請升級到 Pro 方案' },
|
|
335
|
+
{ status: 403 }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return Response.json(protectedData);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 列出所有權限
|
|
343
|
+
const { entitlements } = await recur.entitlements.list({
|
|
344
|
+
customer: { email: 'user@example.com' },
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
console.log(entitlements);
|
|
348
|
+
// [
|
|
349
|
+
// { product: 'pro-plan', status: 'active', subscriptionId: 'sub_xxx' },
|
|
350
|
+
// { product: 'addon-ai', status: 'active', subscriptionId: 'sub_yyy' },
|
|
351
|
+
// ]
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**客戶識別方式(擇一):**
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// 方式一:Email(推薦)
|
|
358
|
+
customer: { email: 'user@example.com' }
|
|
359
|
+
|
|
360
|
+
// 方式二:External ID(你的系統 User ID)
|
|
361
|
+
customer: { externalId: 'usr_12345' }
|
|
362
|
+
|
|
363
|
+
// 方式三:Recur Customer ID
|
|
364
|
+
customer: { id: 'cus_xxx' }
|
|
365
|
+
```
|
|
366
|
+
|
|
263
367
|
---
|
|
264
368
|
|
|
265
369
|
## API Key
|
package/dist/index.cjs
CHANGED
|
@@ -1048,7 +1048,7 @@ var init_payment_form_shadow = __esm({
|
|
|
1048
1048
|
var payment_form_light_default;
|
|
1049
1049
|
var init_payment_form_light = __esm({
|
|
1050
1050
|
"src/components/styles/payment-form-light.css"() {
|
|
1051
|
-
payment_form_light_default = "/**\n * Payment Form - Light DOM Styles\n * These styles are applied to slotted content in the Light DOM\n * Using BEM naming convention with 'recur-' prefix for isolation\n */\n\n/* ============================================\n * Order Summary Styles\n * ============================================ */\n.recur-order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.recur-order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.recur-order-summary__row--total {\n margin-bottom: 0;\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.recur-order-summary__label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.recur-order-summary__value {\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n color: #2d3748;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================\n * Coupon Section Styles\n * ============================================ */\n.recur-coupon {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.recur-coupon__trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n height: 36px;\n background: none;\n border: none;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n color: #718096;\n cursor: pointer;\n transition: color 0.2s;\n}\n\n.recur-coupon__trigger:hover {\n color: #4a5568;\n}\n\n.recur-coupon__trigger-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__input-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n.recur-coupon__input::placeholder {\n text-transform: none;\n}\n\n.recur-coupon__input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__btn {\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s, opacity 0.2s;\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__btn--apply:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-coupon__btn--apply:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.recur-coupon__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n.recur-coupon__remove {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n transition: color 0.2s, background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.recur-coupon__remove:hover {\n color: #6b7280;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.recur-coupon__remove-icon {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n/* Discount Row */\n.recur-order-summary__row--discount {\n color: #16a34a;\n}\n\n.recur-order-summary__row--discount .recur-order-summary__value {\n color: #16a34a;\n font-weight: 500;\n}\n\n/* ============================================\n * Customer Info Styles\n * ============================================ */\n.recur-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.recur-info-row {\n margin-bottom: 12px;\n}\n\n.recur-info-row:last-child {\n margin-bottom: 0;\n}\n\n.recur-info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.recur-info-value {\n display: block;\n font-size: 14px;\n line-height: 20px;\n min-height: 20px;\n color: #2d3748;\n font-weight: 500;\n word-break: break-all;\n}\n\n.recur-form-group {\n margin-bottom: 16px;\n}\n\n.recur-form-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-form-input {\n width: 100%;\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.recur-form-input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-form-input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n/* ============================================\n * Card Fields Styles\n * ============================================ */\n.recur-card-field {\n margin-bottom: 16px;\n}\n\n.recur-card-field-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-payuni-iframe {\n height: 36px;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n touch-action: manipulation;\n}\n\n.recur-card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n max-width: 100%;\n box-sizing: border-box;\n}\n\n.recur-card-field-container {\n position: relative;\n touch-action: manipulation;\n}\n\n.recur-card-skeleton {\n position: absolute;\n top: 0;\n left: 0;\n height: 36px;\n width: 100%;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n z-index: 1;\n pointer-events: none;\n}\n\n.recur-card-skeleton.hidden {\n display: none;\n}\n\n@keyframes recur-skeleton-loading {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* ============================================\n * Submit Button Styles\n * ============================================ */\n.recur-submit-button {\n width: 100%;\n padding: 14px 20px;\n font-size: 16px;\n font-weight: 500;\n color: #ffffff;\n background: #18181b;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n box-sizing: border-box;\n}\n\n.recur-submit-button:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-submit-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.recur-submit-button.loading {\n cursor: wait;\n}\n\n.recur-loading-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n@keyframes recur-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n}\n";
|
|
1051
|
+
payment_form_light_default = "/**\n * Payment Form - Light DOM Styles\n * These styles are applied to slotted content in the Light DOM\n * Using BEM naming convention with 'recur-' prefix for isolation\n */\n\n/* ============================================\n * Order Summary Styles\n * ============================================ */\n.recur-order-summary {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n padding: 16px;\n}\n\n.recur-order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.recur-order-summary__row--total {\n margin-bottom: 0;\n padding-top: 12px;\n border-top: 1px solid #e2e8f0;\n}\n\n.recur-order-summary__label {\n font-size: 14px;\n color: #4a5568;\n}\n\n.recur-order-summary__value {\n font-size: 14px;\n line-height: 20px;\n font-weight: 500;\n color: #2d3748;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.recur-order-summary__total {\n font-size: 18px;\n line-height: 26px;\n font-weight: 700;\n color: #1a202c;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ============================================\n * Coupon Section Styles\n * ============================================ */\n.recur-coupon {\n padding: 12px 0;\n border-top: 1px solid #e2e8f0;\n margin-top: 12px;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n\n.recur-coupon__trigger {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n height: 36px;\n background: none;\n border: none;\n padding: 0;\n font-size: 14px;\n line-height: 1;\n color: #718096;\n cursor: pointer;\n transition: color 0.2s;\n}\n\n.recur-coupon__trigger:hover {\n color: #4a5568;\n}\n\n.recur-coupon__trigger-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__input-row {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__input-wrapper {\n position: relative;\n flex: 1;\n min-width: 0;\n}\n\n.recur-coupon__input {\n width: 100%;\n height: 36px;\n padding: 0 32px 0 12px;\n font-size: 14px;\n text-transform: uppercase;\n background-color: #ffffff;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n outline: none;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n.recur-coupon__input::placeholder {\n text-transform: none;\n}\n\n.recur-coupon__input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n.recur-coupon__input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-coupon__clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: color 0.2s;\n}\n\n.recur-coupon__clear:hover {\n color: #6b7280;\n}\n\n.recur-coupon__clear-icon {\n width: 16px;\n height: 16px;\n}\n\n.recur-coupon__btn {\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n font-weight: 500;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s, opacity 0.2s;\n white-space: nowrap;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.recur-coupon__btn--apply {\n background: #18181b;\n color: white;\n border: none;\n min-width: 52px;\n}\n\n.recur-coupon__btn--apply .recur-coupon__spinner {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__btn--apply:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-coupon__btn--apply:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.recur-coupon__error {\n width: 100%;\n margin-top: 8px;\n font-size: 13px;\n color: #dc2626;\n}\n\n.recur-coupon__applied-wrapper {\n width: 100%;\n}\n\n.recur-coupon__description {\n font-size: 12px;\n color: #6b7280;\n margin: 4px 0 0 22px;\n}\n\n.recur-coupon__bonus {\n display: flex;\n align-items: center;\n gap: 6px;\n background: #dcfce7;\n border-radius: 6px;\n padding: 8px 12px;\n margin-top: 8px;\n font-size: 12px;\n color: #15803d;\n}\n\n.recur-coupon__applied {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n height: 36px;\n}\n\n.recur-coupon__applied-info {\n display: flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n flex: 1;\n}\n\n.recur-coupon__applied-icon {\n width: 16px;\n height: 16px;\n color: #dc2626;\n flex-shrink: 0;\n}\n\n.recur-coupon__applied-name {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n}\n\n.recur-coupon__applied-code {\n font-size: 14px;\n color: #9ca3af;\n white-space: nowrap;\n margin-right: 2px;\n}\n\n.recur-coupon__applied-amount {\n font-size: 14px;\n font-weight: 500;\n color: #dc2626;\n white-space: nowrap;\n font-variant-numeric: tabular-nums;\n flex-shrink: 0;\n margin-left: auto;\n}\n\n.recur-coupon__remove {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: #9ca3af;\n border-radius: 4px;\n transition: color 0.2s, background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n}\n\n.recur-coupon__remove:hover {\n color: #6b7280;\n background: rgba(0, 0, 0, 0.05);\n}\n\n.recur-coupon__remove-icon {\n width: 14px;\n height: 14px;\n}\n\n.recur-coupon__spinner {\n width: 14px;\n height: 14px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n/* Discount Row */\n.recur-order-summary__row--discount {\n color: #16a34a;\n}\n\n.recur-order-summary__row--discount .recur-order-summary__value {\n color: #16a34a;\n font-weight: 500;\n}\n\n/* ============================================\n * Customer Info Styles\n * ============================================ */\n.recur-info-display {\n background: #f7fafc;\n border: 1px solid #e2e8f0;\n border-radius: 6px;\n padding: 12px;\n margin-bottom: 8px;\n}\n\n.recur-info-row {\n margin-bottom: 12px;\n}\n\n.recur-info-row:last-child {\n margin-bottom: 0;\n}\n\n.recur-info-label {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: #718096;\n margin-bottom: 2px;\n}\n\n.recur-info-value {\n display: block;\n font-size: 14px;\n line-height: 20px;\n min-height: 20px;\n color: #2d3748;\n font-weight: 500;\n word-break: break-all;\n}\n\n.recur-form-group {\n margin-bottom: 16px;\n}\n\n.recur-form-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-form-input {\n width: 100%;\n height: 36px;\n padding: 0 12px;\n font-size: 14px;\n border: 1px solid #d1d5db;\n border-radius: 6px;\n transition: border-color 0.2s;\n box-sizing: border-box;\n}\n\n.recur-form-input:focus {\n border-color: var(--ring, hsl(215 16% 47%));\n outline: 0;\n box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring, hsl(215 16% 47%)) 50%, transparent);\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n}\n\n.recur-form-input:hover:not(:focus) {\n border-color: #9ca3af;\n}\n\n/* ============================================\n * Card Fields Styles\n * ============================================ */\n.recur-card-field {\n margin-bottom: 16px;\n}\n\n.recur-card-field-label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #2d3748;\n margin-bottom: 6px;\n}\n\n.recur-payuni-iframe {\n height: 36px;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n touch-action: manipulation;\n}\n\n.recur-card-row {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n column-gap: 16px;\n max-width: 100%;\n box-sizing: border-box;\n}\n\n.recur-card-field-container {\n position: relative;\n touch-action: manipulation;\n}\n\n.recur-card-skeleton {\n position: absolute;\n top: 0;\n left: 0;\n height: 36px;\n width: 100%;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n z-index: 1;\n pointer-events: none;\n}\n\n.recur-card-skeleton.hidden {\n display: none;\n}\n\n@keyframes recur-skeleton-loading {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n/* ============================================\n * Submit Button Styles\n * ============================================ */\n.recur-submit-button {\n width: 100%;\n padding: 14px 20px;\n font-size: 16px;\n font-weight: 500;\n color: #ffffff;\n background: #18181b;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n box-sizing: border-box;\n}\n\n.recur-submit-button:hover:not(:disabled) {\n background: #27272a;\n}\n\n.recur-submit-button:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.recur-submit-button.loading {\n cursor: wait;\n}\n\n.recur-loading-spinner {\n width: 16px;\n height: 16px;\n border: 2px solid currentColor;\n border-top-color: transparent;\n border-radius: 50%;\n animation: recur-spin 0.6s linear infinite;\n}\n\n@keyframes recur-spin {\n to { transform: rotate(360deg); }\n}\n\n/* ============================================\n * Skeleton Loading Styles\n * ============================================ */\n.recur-skeleton {\n display: inline-block;\n background: linear-gradient(90deg, #f0f0f0 0%, #e8e8e8 50%, #f0f0f0 100%);\n background-size: 200% 100%;\n animation: recur-skeleton-loading 1.5s ease-in-out infinite;\n border-radius: 4px;\n}\n\n/* ============================================\n * Test Mode Banner Styles\n * ============================================ */\n.recur-test-mode-banner {\n background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);\n border: 1px solid #f59e0b;\n border-radius: 8px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n\n.recur-test-mode-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 10px 14px;\n cursor: pointer;\n user-select: none;\n transition: background 0.2s;\n}\n\n.recur-test-mode-header:hover {\n background: rgba(245, 158, 11, 0.1);\n}\n\n.recur-test-mode-badge {\n font-size: 13px;\n font-weight: 600;\n color: #92400e;\n}\n\n.recur-test-mode-toggle {\n font-size: 12px;\n color: #b45309;\n}\n\n.recur-test-cards {\n background: rgba(255, 255, 255, 0.7);\n padding: 12px 14px;\n border-top: 1px solid rgba(245, 158, 11, 0.3);\n}\n\n.recur-test-cards-info {\n font-size: 12px;\n color: #78350f;\n margin: 0 0 10px 0;\n}\n\n.recur-test-card {\n background: white;\n border: 1px solid #e5e7eb;\n border-radius: 6px;\n padding: 10px 12px;\n margin-bottom: 8px;\n}\n\n.recur-test-card:last-child {\n margin-bottom: 0;\n}\n\n.recur-test-card-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.recur-test-card-number {\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 14px;\n font-weight: 600;\n color: #1f2937;\n letter-spacing: 0.5px;\n}\n\n.recur-test-card-copy {\n font-size: 11px;\n font-weight: 500;\n color: #3b82f6;\n background: #eff6ff;\n border: 1px solid #bfdbfe;\n border-radius: 4px;\n padding: 3px 8px;\n cursor: pointer;\n transition: all 0.2s;\n}\n\n.recur-test-card-copy:hover {\n background: #dbeafe;\n border-color: #93c5fd;\n}\n\n.recur-test-card-copy.copied {\n background: #dcfce7;\n border-color: #86efac;\n color: #16a34a;\n}\n\n.recur-test-card-details {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n font-size: 11px;\n color: #6b7280;\n}\n\n.recur-test-card-desc {\n color: #374151;\n font-weight: 500;\n}\n\n.recur-test-card-meta {\n color: #9ca3af;\n}\n";
|
|
1052
1052
|
}
|
|
1053
1053
|
});
|
|
1054
1054
|
|
|
@@ -1097,6 +1097,8 @@ var init_payment_form = __esm({
|
|
|
1097
1097
|
__publicField(this, "customStyles");
|
|
1098
1098
|
__publicField(this, "_isInitializing", false);
|
|
1099
1099
|
__publicField(this, "_initializationAborted", false);
|
|
1100
|
+
// Test mode state
|
|
1101
|
+
__publicField(this, "_showTestCardHints", false);
|
|
1100
1102
|
// Coupon state
|
|
1101
1103
|
__publicField(this, "_showCouponInput", false);
|
|
1102
1104
|
__publicField(this, "_couponCode", "");
|
|
@@ -1163,6 +1165,8 @@ var init_payment_form = __esm({
|
|
|
1163
1165
|
<style>${this.getShadowDOMStyles()}</style>
|
|
1164
1166
|
|
|
1165
1167
|
<div class="payment-form-wrapper">
|
|
1168
|
+
<slot name="test-mode-banner"></slot>
|
|
1169
|
+
|
|
1166
1170
|
<div class="form-header">
|
|
1167
1171
|
<h2 class="form-title">訂閱付款</h2>
|
|
1168
1172
|
</div>
|
|
@@ -1199,6 +1203,10 @@ var init_payment_form = __esm({
|
|
|
1199
1203
|
${this.getCustomStyles()}
|
|
1200
1204
|
</style>
|
|
1201
1205
|
|
|
1206
|
+
<div slot="test-mode-banner">
|
|
1207
|
+
${this.renderTestCardHints()}
|
|
1208
|
+
</div>
|
|
1209
|
+
|
|
1202
1210
|
<div slot="order-summary">
|
|
1203
1211
|
${this.renderOrderSummary()}
|
|
1204
1212
|
</div>
|
|
@@ -1235,6 +1243,63 @@ var init_payment_form = __esm({
|
|
|
1235
1243
|
}
|
|
1236
1244
|
}
|
|
1237
1245
|
// ============================================
|
|
1246
|
+
// Test Mode Helpers
|
|
1247
|
+
// ============================================
|
|
1248
|
+
isTestMode() {
|
|
1249
|
+
const publishableKey = this.getAttribute("publishable-key") || "";
|
|
1250
|
+
return publishableKey.startsWith("pk_test_");
|
|
1251
|
+
}
|
|
1252
|
+
renderTestCardHints() {
|
|
1253
|
+
if (!this.isTestMode()) {
|
|
1254
|
+
return litHtml.html``;
|
|
1255
|
+
}
|
|
1256
|
+
const testCards = [
|
|
1257
|
+
{ number: "4147 6310 0000 0001", desc: "VISA \u6E2C\u8A66\u5361\uFF08\u6388\u6B0A\u6210\u529F\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" },
|
|
1258
|
+
{ number: "3560 5110 0000 0001", desc: "JCB \u6E2C\u8A66\u5361\uFF08\u6388\u6B0A\u6210\u529F\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" },
|
|
1259
|
+
{ number: "4147 6310 0000 0002", desc: "VISA \u6E2C\u8A66\u5361\uFF083D\u9A57\u8B49\u5931\u6557\uFF09", expiry: "\u4EFB\u610F\u672A\u4F86\u65E5\u671F", cvc: "\u4EFB\u610F3\u78BC" }
|
|
1260
|
+
];
|
|
1261
|
+
const copyToClipboard = (text, event) => {
|
|
1262
|
+
const btn = event.currentTarget;
|
|
1263
|
+
navigator.clipboard.writeText(text.replace(/\s/g, "")).then(() => {
|
|
1264
|
+
const originalText = btn.textContent;
|
|
1265
|
+
btn.textContent = "\u5DF2\u8907\u88FD!";
|
|
1266
|
+
btn.classList.add("copied");
|
|
1267
|
+
setTimeout(() => {
|
|
1268
|
+
btn.textContent = originalText;
|
|
1269
|
+
btn.classList.remove("copied");
|
|
1270
|
+
}, 1500);
|
|
1271
|
+
});
|
|
1272
|
+
};
|
|
1273
|
+
return litHtml.html`
|
|
1274
|
+
<div class="recur-test-mode-banner">
|
|
1275
|
+
<div class="recur-test-mode-header" @click=${() => {
|
|
1276
|
+
this._showTestCardHints = !this._showTestCardHints;
|
|
1277
|
+
this.renderLightDOM();
|
|
1278
|
+
}}>
|
|
1279
|
+
<span class="recur-test-mode-badge">🧪 測試模式</span>
|
|
1280
|
+
<span class="recur-test-mode-toggle">${this._showTestCardHints ? "\u6536\u5408 \u25B2" : "\u5C55\u958B\u6E2C\u8A66\u5361\u865F \u25BC"}</span>
|
|
1281
|
+
</div>
|
|
1282
|
+
${this._showTestCardHints ? litHtml.html`
|
|
1283
|
+
<div class="recur-test-cards">
|
|
1284
|
+
<p class="recur-test-cards-info">使用以下測試卡號進行測試付款:</p>
|
|
1285
|
+
${testCards.map((card) => litHtml.html`
|
|
1286
|
+
<div class="recur-test-card">
|
|
1287
|
+
<div class="recur-test-card-row">
|
|
1288
|
+
<span class="recur-test-card-number">${card.number}</span>
|
|
1289
|
+
<button type="button" class="recur-test-card-copy" @click=${(e) => copyToClipboard(card.number, e)}>複製</button>
|
|
1290
|
+
</div>
|
|
1291
|
+
<div class="recur-test-card-details">
|
|
1292
|
+
<span class="recur-test-card-desc">${card.desc}</span>
|
|
1293
|
+
<span class="recur-test-card-meta">到期日: ${card.expiry} / CVC: ${card.cvc}</span>
|
|
1294
|
+
</div>
|
|
1295
|
+
</div>
|
|
1296
|
+
`)}
|
|
1297
|
+
</div>
|
|
1298
|
+
` : litHtml.nothing}
|
|
1299
|
+
</div>
|
|
1300
|
+
`;
|
|
1301
|
+
}
|
|
1302
|
+
// ============================================
|
|
1238
1303
|
// Section Renderers (lit-html)
|
|
1239
1304
|
// ============================================
|
|
1240
1305
|
renderOrderSummary() {
|
|
@@ -1648,16 +1713,16 @@ var init_payment_form = __esm({
|
|
|
1648
1713
|
}
|
|
1649
1714
|
this._appliedCoupon = {
|
|
1650
1715
|
code: data.discount.code,
|
|
1651
|
-
couponName: data.discount.
|
|
1652
|
-
discountType: data.discount.
|
|
1653
|
-
discountAmount: data.discount.
|
|
1716
|
+
couponName: data.discount.coupon_name,
|
|
1717
|
+
discountType: data.discount.discount_type,
|
|
1718
|
+
discountAmount: data.discount.discount_amount,
|
|
1654
1719
|
duration: data.discount.duration,
|
|
1655
|
-
durationMonths: data.discount.
|
|
1656
|
-
bonusTrialDays: data.discount.
|
|
1657
|
-
bonusMonths: data.discount.
|
|
1658
|
-
productInterval: data.discount.
|
|
1659
|
-
productIntervalCount: data.discount.
|
|
1660
|
-
productPrice: data.discount.
|
|
1720
|
+
durationMonths: data.discount.duration_months,
|
|
1721
|
+
bonusTrialDays: data.discount.bonus_trial_days,
|
|
1722
|
+
bonusMonths: data.discount.bonus_months,
|
|
1723
|
+
productInterval: data.discount.product_interval,
|
|
1724
|
+
productIntervalCount: data.discount.product_interval_count,
|
|
1725
|
+
productPrice: data.discount.product_price
|
|
1661
1726
|
};
|
|
1662
1727
|
this._showCouponInput = false;
|
|
1663
1728
|
this._couponCode = "";
|
|
@@ -2674,16 +2739,27 @@ function toCamelCase(obj) {
|
|
|
2674
2739
|
|
|
2675
2740
|
// package.json
|
|
2676
2741
|
var package_default = {
|
|
2677
|
-
version: "0.
|
|
2742
|
+
version: "0.12.0"};
|
|
2678
2743
|
var SDK_VERSION = package_default.version;
|
|
2679
2744
|
var SDK_TYPE = "react";
|
|
2680
2745
|
var RecurContext = React.createContext(null);
|
|
2681
|
-
|
|
2746
|
+
var CustomerContext = React.createContext(null);
|
|
2747
|
+
function RecurProvider({ children, config: initialConfig = {}, customer: customerIdentifier }) {
|
|
2682
2748
|
const [config, setConfig] = React.useState({
|
|
2683
2749
|
checkoutMode: "embedded",
|
|
2684
2750
|
...initialConfig
|
|
2685
2751
|
});
|
|
2686
2752
|
const [isCheckingOut, setIsCheckingOut] = React.useState(false);
|
|
2753
|
+
const [customerData, setCustomerData] = React.useState({
|
|
2754
|
+
customer: null,
|
|
2755
|
+
subscription: null,
|
|
2756
|
+
entitlements: []
|
|
2757
|
+
});
|
|
2758
|
+
const [customerIsLoading, setCustomerIsLoading] = React.useState(false);
|
|
2759
|
+
const [customerError, setCustomerError] = React.useState(null);
|
|
2760
|
+
const lastSuccessfulCache = React__default.default.useRef(null);
|
|
2761
|
+
const isRefetching = React__default.default.useRef(false);
|
|
2762
|
+
const refetchDebounceTimer = React__default.default.useRef(null);
|
|
2687
2763
|
React__default.default.useEffect(() => {
|
|
2688
2764
|
setConfig({
|
|
2689
2765
|
checkoutMode: "embedded",
|
|
@@ -2693,6 +2769,89 @@ function RecurProvider({ children, config: initialConfig = {} }) {
|
|
|
2693
2769
|
React__default.default.useEffect(() => {
|
|
2694
2770
|
setIsCheckingOut(false);
|
|
2695
2771
|
}, [config.checkoutMode]);
|
|
2772
|
+
const fetchEntitlements = React.useCallback(async () => {
|
|
2773
|
+
if (!config.publishableKey) {
|
|
2774
|
+
console.warn("[Recur SDK] Cannot fetch entitlements: publishableKey is required");
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
if (!customerIdentifier?.email && !customerIdentifier?.externalId && !customerIdentifier?.id) {
|
|
2778
|
+
setCustomerData({ customer: null, subscription: null, entitlements: [] });
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
setCustomerIsLoading(true);
|
|
2782
|
+
setCustomerError(null);
|
|
2783
|
+
try {
|
|
2784
|
+
const baseUrl = config.baseUrl || "https://api.recur.tw";
|
|
2785
|
+
const params = new URLSearchParams();
|
|
2786
|
+
if (customerIdentifier.email) params.set("email", customerIdentifier.email);
|
|
2787
|
+
if (customerIdentifier.externalId) params.set("external_id", customerIdentifier.externalId);
|
|
2788
|
+
if (customerIdentifier.id) params.set("customer_id", customerIdentifier.id);
|
|
2789
|
+
const response = await fetch(`${baseUrl}/v1/customers/entitlements?${params.toString()}`, {
|
|
2790
|
+
method: "GET",
|
|
2791
|
+
headers: {
|
|
2792
|
+
"X-Recur-Publishable-Key": config.publishableKey,
|
|
2793
|
+
"X-Recur-SDK-Type": SDK_TYPE,
|
|
2794
|
+
"X-Recur-SDK-Version": SDK_VERSION
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
if (!response.ok) {
|
|
2798
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2799
|
+
throw new Error(errorData.error?.message || "Failed to fetch entitlements");
|
|
2800
|
+
}
|
|
2801
|
+
const rawResult = await response.json();
|
|
2802
|
+
const result = toCamelCase(rawResult);
|
|
2803
|
+
const newData = {
|
|
2804
|
+
customer: result.customer || null,
|
|
2805
|
+
subscription: result.subscription || null,
|
|
2806
|
+
entitlements: result.entitlements || []
|
|
2807
|
+
};
|
|
2808
|
+
setCustomerData(newData);
|
|
2809
|
+
lastSuccessfulCache.current = newData;
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
console.error("[Recur SDK] Failed to fetch entitlements:", error);
|
|
2812
|
+
setCustomerError(error instanceof Error ? error : new Error("Failed to fetch entitlements"));
|
|
2813
|
+
if (lastSuccessfulCache.current) {
|
|
2814
|
+
setCustomerData(lastSuccessfulCache.current);
|
|
2815
|
+
}
|
|
2816
|
+
} finally {
|
|
2817
|
+
setCustomerIsLoading(false);
|
|
2818
|
+
}
|
|
2819
|
+
}, [config.publishableKey, config.baseUrl, customerIdentifier?.email, customerIdentifier?.externalId, customerIdentifier?.id]);
|
|
2820
|
+
React__default.default.useEffect(() => {
|
|
2821
|
+
fetchEntitlements();
|
|
2822
|
+
}, [fetchEntitlements]);
|
|
2823
|
+
const refetch = React.useCallback(async () => {
|
|
2824
|
+
if (isRefetching.current) {
|
|
2825
|
+
console.log("[Recur SDK] Refetch skipped - already in progress");
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
if (refetchDebounceTimer.current) {
|
|
2829
|
+
clearTimeout(refetchDebounceTimer.current);
|
|
2830
|
+
}
|
|
2831
|
+
isRefetching.current = true;
|
|
2832
|
+
try {
|
|
2833
|
+
await fetchEntitlements();
|
|
2834
|
+
} finally {
|
|
2835
|
+
refetchDebounceTimer.current = setTimeout(() => {
|
|
2836
|
+
isRefetching.current = false;
|
|
2837
|
+
}, 300);
|
|
2838
|
+
}
|
|
2839
|
+
}, [fetchEntitlements]);
|
|
2840
|
+
React__default.default.useEffect(() => {
|
|
2841
|
+
return () => {
|
|
2842
|
+
if (refetchDebounceTimer.current) {
|
|
2843
|
+
clearTimeout(refetchDebounceTimer.current);
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
}, []);
|
|
2847
|
+
const customerContextValue = React.useMemo(() => ({
|
|
2848
|
+
customer: customerData.customer,
|
|
2849
|
+
subscription: customerData.subscription,
|
|
2850
|
+
entitlements: customerData.entitlements,
|
|
2851
|
+
isLoading: customerIsLoading,
|
|
2852
|
+
error: customerError,
|
|
2853
|
+
refetch
|
|
2854
|
+
}), [customerData, customerIsLoading, customerError, refetch]);
|
|
2696
2855
|
const updateConfig = React.useCallback((newConfig) => {
|
|
2697
2856
|
setConfig((prev) => ({ ...prev, ...newConfig }));
|
|
2698
2857
|
}, []);
|
|
@@ -2894,14 +3053,14 @@ function RecurProvider({ children, config: initialConfig = {} }) {
|
|
|
2894
3053
|
}
|
|
2895
3054
|
console.log("[Recur SDK] Step 1: Creating checkout session...");
|
|
2896
3055
|
const checkoutRequestBody = {
|
|
2897
|
-
|
|
2898
|
-
|
|
3056
|
+
customer_name: options.customerName,
|
|
3057
|
+
customer_email: options.customerEmail
|
|
2899
3058
|
};
|
|
2900
|
-
if (productId) checkoutRequestBody.
|
|
2901
|
-
if (productSlug) checkoutRequestBody.
|
|
2902
|
-
if (options.externalCustomerId) checkoutRequestBody.
|
|
2903
|
-
if (options.successUrl) checkoutRequestBody.
|
|
2904
|
-
if (options.cancelUrl) checkoutRequestBody.
|
|
3059
|
+
if (productId) checkoutRequestBody.product_id = productId;
|
|
3060
|
+
if (productSlug) checkoutRequestBody.product_slug = productSlug;
|
|
3061
|
+
if (options.externalCustomerId) checkoutRequestBody.external_customer_id = options.externalCustomerId;
|
|
3062
|
+
if (options.successUrl) checkoutRequestBody.success_url = options.successUrl;
|
|
3063
|
+
if (options.cancelUrl) checkoutRequestBody.cancel_url = options.cancelUrl;
|
|
2905
3064
|
const checkoutResponse = await fetch(`${baseUrl}/v1/checkouts`, {
|
|
2906
3065
|
method: "POST",
|
|
2907
3066
|
headers,
|
|
@@ -3420,7 +3579,7 @@ function RecurProvider({ children, config: initialConfig = {} }) {
|
|
|
3420
3579
|
}),
|
|
3421
3580
|
[config, checkout, fetchProducts, fetchPlans, isCheckingOut, updateConfig, getCheckoutStatus]
|
|
3422
3581
|
);
|
|
3423
|
-
return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children });
|
|
3582
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(CustomerContext.Provider, { value: customerContextValue, children }) });
|
|
3424
3583
|
}
|
|
3425
3584
|
function useRecur() {
|
|
3426
3585
|
const context = React.useContext(RecurContext);
|
|
@@ -3510,8 +3669,84 @@ function useSubscribe(options = {}) {
|
|
|
3510
3669
|
reset
|
|
3511
3670
|
};
|
|
3512
3671
|
}
|
|
3672
|
+
function useCustomer() {
|
|
3673
|
+
const context = React.useContext(CustomerContext);
|
|
3674
|
+
if (!context) {
|
|
3675
|
+
throw new Error("useCustomer must be used within a RecurProvider");
|
|
3676
|
+
}
|
|
3677
|
+
const { customer, subscription, entitlements, isLoading: isLoading2, error, refetch } = context;
|
|
3678
|
+
const resolveProduct = (target) => {
|
|
3679
|
+
if (typeof target === "string") {
|
|
3680
|
+
return target;
|
|
3681
|
+
}
|
|
3682
|
+
if ("product" in target) {
|
|
3683
|
+
return target.product;
|
|
3684
|
+
}
|
|
3685
|
+
if ("feature" in target) {
|
|
3686
|
+
throw new Error("Feature-based checks are not yet supported. Coming in Phase 2.");
|
|
3687
|
+
}
|
|
3688
|
+
if ("benefit" in target) {
|
|
3689
|
+
throw new Error("Benefit-based checks are not yet supported. Coming in Phase 2.");
|
|
3690
|
+
}
|
|
3691
|
+
throw new Error("Invalid check target");
|
|
3692
|
+
};
|
|
3693
|
+
const checkSync = React.useCallback(
|
|
3694
|
+
(target) => {
|
|
3695
|
+
const product = resolveProduct(target);
|
|
3696
|
+
if (!customer) {
|
|
3697
|
+
return { allowed: false, reason: "no_customer" };
|
|
3698
|
+
}
|
|
3699
|
+
if (entitlements.length === 0) {
|
|
3700
|
+
return { allowed: false, reason: "no_entitlement" };
|
|
3701
|
+
}
|
|
3702
|
+
const matchedEntitlement = entitlements.find(
|
|
3703
|
+
(e) => e.product === product || e.productId === product
|
|
3704
|
+
);
|
|
3705
|
+
if (!matchedEntitlement) {
|
|
3706
|
+
return { allowed: false, reason: "not_found" };
|
|
3707
|
+
}
|
|
3708
|
+
let matchingSubscription;
|
|
3709
|
+
if (subscription && (subscription.product.slug === product || subscription.product.id === product)) {
|
|
3710
|
+
matchingSubscription = subscription;
|
|
3711
|
+
}
|
|
3712
|
+
return {
|
|
3713
|
+
allowed: true,
|
|
3714
|
+
entitlement: matchedEntitlement,
|
|
3715
|
+
// Backward compatibility
|
|
3716
|
+
subscription: matchingSubscription
|
|
3717
|
+
};
|
|
3718
|
+
},
|
|
3719
|
+
[customer, entitlements, subscription]
|
|
3720
|
+
);
|
|
3721
|
+
const checkLive = React.useCallback(
|
|
3722
|
+
async (target) => {
|
|
3723
|
+
await refetch();
|
|
3724
|
+
return checkSync(target);
|
|
3725
|
+
},
|
|
3726
|
+
[refetch, checkSync]
|
|
3727
|
+
);
|
|
3728
|
+
const check = React.useCallback(
|
|
3729
|
+
(target, options) => {
|
|
3730
|
+
if (options?.live) {
|
|
3731
|
+
return checkLive(target);
|
|
3732
|
+
}
|
|
3733
|
+
return checkSync(target);
|
|
3734
|
+
},
|
|
3735
|
+
[checkSync, checkLive]
|
|
3736
|
+
);
|
|
3737
|
+
return {
|
|
3738
|
+
customer,
|
|
3739
|
+
subscription,
|
|
3740
|
+
entitlements,
|
|
3741
|
+
check,
|
|
3742
|
+
refetch,
|
|
3743
|
+
isLoading: isLoading2,
|
|
3744
|
+
error
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3513
3747
|
|
|
3514
3748
|
exports.RecurProvider = RecurProvider;
|
|
3749
|
+
exports.useCustomer = useCustomer;
|
|
3515
3750
|
exports.usePlans = useProducts;
|
|
3516
3751
|
exports.useProducts = useProducts;
|
|
3517
3752
|
exports.useRecur = useRecur;
|