recur-tw 0.10.8 → 0.11.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 +136 -3
- package/dist/index.d.cts +291 -2
- package/dist/index.d.ts +291 -2
- package/dist/index.js +136 -4
- package/dist/recur.umd.js +1 -1
- 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
|
@@ -2674,16 +2674,27 @@ function toCamelCase(obj) {
|
|
|
2674
2674
|
|
|
2675
2675
|
// package.json
|
|
2676
2676
|
var package_default = {
|
|
2677
|
-
version: "0.
|
|
2677
|
+
version: "0.11.0"};
|
|
2678
2678
|
var SDK_VERSION = package_default.version;
|
|
2679
2679
|
var SDK_TYPE = "react";
|
|
2680
2680
|
var RecurContext = React.createContext(null);
|
|
2681
|
-
|
|
2681
|
+
var CustomerContext = React.createContext(null);
|
|
2682
|
+
function RecurProvider({ children, config: initialConfig = {}, customer: customerIdentifier }) {
|
|
2682
2683
|
const [config, setConfig] = React.useState({
|
|
2683
2684
|
checkoutMode: "embedded",
|
|
2684
2685
|
...initialConfig
|
|
2685
2686
|
});
|
|
2686
2687
|
const [isCheckingOut, setIsCheckingOut] = React.useState(false);
|
|
2688
|
+
const [customerData, setCustomerData] = React.useState({
|
|
2689
|
+
customer: null,
|
|
2690
|
+
subscription: null,
|
|
2691
|
+
entitlements: []
|
|
2692
|
+
});
|
|
2693
|
+
const [customerIsLoading, setCustomerIsLoading] = React.useState(false);
|
|
2694
|
+
const [customerError, setCustomerError] = React.useState(null);
|
|
2695
|
+
const lastSuccessfulCache = React__default.default.useRef(null);
|
|
2696
|
+
const isRefetching = React__default.default.useRef(false);
|
|
2697
|
+
const refetchDebounceTimer = React__default.default.useRef(null);
|
|
2687
2698
|
React__default.default.useEffect(() => {
|
|
2688
2699
|
setConfig({
|
|
2689
2700
|
checkoutMode: "embedded",
|
|
@@ -2693,6 +2704,89 @@ function RecurProvider({ children, config: initialConfig = {} }) {
|
|
|
2693
2704
|
React__default.default.useEffect(() => {
|
|
2694
2705
|
setIsCheckingOut(false);
|
|
2695
2706
|
}, [config.checkoutMode]);
|
|
2707
|
+
const fetchEntitlements = React.useCallback(async () => {
|
|
2708
|
+
if (!config.publishableKey) {
|
|
2709
|
+
console.warn("[Recur SDK] Cannot fetch entitlements: publishableKey is required");
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
if (!customerIdentifier?.email && !customerIdentifier?.externalId && !customerIdentifier?.id) {
|
|
2713
|
+
setCustomerData({ customer: null, subscription: null, entitlements: [] });
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
setCustomerIsLoading(true);
|
|
2717
|
+
setCustomerError(null);
|
|
2718
|
+
try {
|
|
2719
|
+
const baseUrl = config.baseUrl || "https://api.recur.tw";
|
|
2720
|
+
const params = new URLSearchParams();
|
|
2721
|
+
if (customerIdentifier.email) params.set("email", customerIdentifier.email);
|
|
2722
|
+
if (customerIdentifier.externalId) params.set("external_id", customerIdentifier.externalId);
|
|
2723
|
+
if (customerIdentifier.id) params.set("customer_id", customerIdentifier.id);
|
|
2724
|
+
const response = await fetch(`${baseUrl}/v1/customers/entitlements?${params.toString()}`, {
|
|
2725
|
+
method: "GET",
|
|
2726
|
+
headers: {
|
|
2727
|
+
"X-Recur-Publishable-Key": config.publishableKey,
|
|
2728
|
+
"X-Recur-SDK-Type": SDK_TYPE,
|
|
2729
|
+
"X-Recur-SDK-Version": SDK_VERSION
|
|
2730
|
+
}
|
|
2731
|
+
});
|
|
2732
|
+
if (!response.ok) {
|
|
2733
|
+
const errorData = await response.json().catch(() => ({}));
|
|
2734
|
+
throw new Error(errorData.error?.message || "Failed to fetch entitlements");
|
|
2735
|
+
}
|
|
2736
|
+
const rawResult = await response.json();
|
|
2737
|
+
const result = toCamelCase(rawResult);
|
|
2738
|
+
const newData = {
|
|
2739
|
+
customer: result.customer || null,
|
|
2740
|
+
subscription: result.subscription || null,
|
|
2741
|
+
entitlements: result.entitlements || []
|
|
2742
|
+
};
|
|
2743
|
+
setCustomerData(newData);
|
|
2744
|
+
lastSuccessfulCache.current = newData;
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
console.error("[Recur SDK] Failed to fetch entitlements:", error);
|
|
2747
|
+
setCustomerError(error instanceof Error ? error : new Error("Failed to fetch entitlements"));
|
|
2748
|
+
if (lastSuccessfulCache.current) {
|
|
2749
|
+
setCustomerData(lastSuccessfulCache.current);
|
|
2750
|
+
}
|
|
2751
|
+
} finally {
|
|
2752
|
+
setCustomerIsLoading(false);
|
|
2753
|
+
}
|
|
2754
|
+
}, [config.publishableKey, config.baseUrl, customerIdentifier?.email, customerIdentifier?.externalId, customerIdentifier?.id]);
|
|
2755
|
+
React__default.default.useEffect(() => {
|
|
2756
|
+
fetchEntitlements();
|
|
2757
|
+
}, [fetchEntitlements]);
|
|
2758
|
+
const refetch = React.useCallback(async () => {
|
|
2759
|
+
if (isRefetching.current) {
|
|
2760
|
+
console.log("[Recur SDK] Refetch skipped - already in progress");
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (refetchDebounceTimer.current) {
|
|
2764
|
+
clearTimeout(refetchDebounceTimer.current);
|
|
2765
|
+
}
|
|
2766
|
+
isRefetching.current = true;
|
|
2767
|
+
try {
|
|
2768
|
+
await fetchEntitlements();
|
|
2769
|
+
} finally {
|
|
2770
|
+
refetchDebounceTimer.current = setTimeout(() => {
|
|
2771
|
+
isRefetching.current = false;
|
|
2772
|
+
}, 300);
|
|
2773
|
+
}
|
|
2774
|
+
}, [fetchEntitlements]);
|
|
2775
|
+
React__default.default.useEffect(() => {
|
|
2776
|
+
return () => {
|
|
2777
|
+
if (refetchDebounceTimer.current) {
|
|
2778
|
+
clearTimeout(refetchDebounceTimer.current);
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
}, []);
|
|
2782
|
+
const customerContextValue = React.useMemo(() => ({
|
|
2783
|
+
customer: customerData.customer,
|
|
2784
|
+
subscription: customerData.subscription,
|
|
2785
|
+
entitlements: customerData.entitlements,
|
|
2786
|
+
isLoading: customerIsLoading,
|
|
2787
|
+
error: customerError,
|
|
2788
|
+
refetch
|
|
2789
|
+
}), [customerData, customerIsLoading, customerError, refetch]);
|
|
2696
2790
|
const updateConfig = React.useCallback((newConfig) => {
|
|
2697
2791
|
setConfig((prev) => ({ ...prev, ...newConfig }));
|
|
2698
2792
|
}, []);
|
|
@@ -3420,7 +3514,7 @@ function RecurProvider({ children, config: initialConfig = {} }) {
|
|
|
3420
3514
|
}),
|
|
3421
3515
|
[config, checkout, fetchProducts, fetchPlans, isCheckingOut, updateConfig, getCheckoutStatus]
|
|
3422
3516
|
);
|
|
3423
|
-
return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children });
|
|
3517
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RecurContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(CustomerContext.Provider, { value: customerContextValue, children }) });
|
|
3424
3518
|
}
|
|
3425
3519
|
function useRecur() {
|
|
3426
3520
|
const context = React.useContext(RecurContext);
|
|
@@ -3510,8 +3604,47 @@ function useSubscribe(options = {}) {
|
|
|
3510
3604
|
reset
|
|
3511
3605
|
};
|
|
3512
3606
|
}
|
|
3607
|
+
function useCustomer() {
|
|
3608
|
+
const context = React.useContext(CustomerContext);
|
|
3609
|
+
if (!context) {
|
|
3610
|
+
throw new Error("useCustomer must be used within a RecurProvider");
|
|
3611
|
+
}
|
|
3612
|
+
const { customer, subscription, entitlements, isLoading: isLoading2, error, refetch } = context;
|
|
3613
|
+
const check = React.useCallback(
|
|
3614
|
+
(options) => {
|
|
3615
|
+
if (!customer) {
|
|
3616
|
+
return { allowed: false, reason: "no_customer" };
|
|
3617
|
+
}
|
|
3618
|
+
if (entitlements.length === 0) {
|
|
3619
|
+
return { allowed: false, reason: "no_subscription" };
|
|
3620
|
+
}
|
|
3621
|
+
const entitlement = entitlements.find(
|
|
3622
|
+
(e) => e.product === options.product || e.productId === options.product
|
|
3623
|
+
);
|
|
3624
|
+
if (!entitlement) {
|
|
3625
|
+
return { allowed: false, reason: "wrong_product" };
|
|
3626
|
+
}
|
|
3627
|
+
let matchingSubscription;
|
|
3628
|
+
if (subscription && (subscription.product.slug === options.product || subscription.product.id === options.product)) {
|
|
3629
|
+
matchingSubscription = subscription;
|
|
3630
|
+
}
|
|
3631
|
+
return { allowed: true, subscription: matchingSubscription };
|
|
3632
|
+
},
|
|
3633
|
+
[customer, entitlements, subscription]
|
|
3634
|
+
);
|
|
3635
|
+
return {
|
|
3636
|
+
customer,
|
|
3637
|
+
subscription,
|
|
3638
|
+
entitlements,
|
|
3639
|
+
check,
|
|
3640
|
+
refetch,
|
|
3641
|
+
isLoading: isLoading2,
|
|
3642
|
+
error
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3513
3645
|
|
|
3514
3646
|
exports.RecurProvider = RecurProvider;
|
|
3647
|
+
exports.useCustomer = useCustomer;
|
|
3515
3648
|
exports.usePlans = useProducts;
|
|
3516
3649
|
exports.useProducts = useProducts;
|
|
3517
3650
|
exports.useRecur = useRecur;
|
package/dist/index.d.cts
CHANGED
|
@@ -551,6 +551,173 @@ interface UniPaymentInstance {
|
|
|
551
551
|
}) => Promise<any>;
|
|
552
552
|
onUpdate?: (callback: (data: any) => void) => void;
|
|
553
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Customer identifier for entitlements lookup
|
|
556
|
+
* At least one identifier must be provided
|
|
557
|
+
*/
|
|
558
|
+
interface CustomerIdentifier {
|
|
559
|
+
/**
|
|
560
|
+
* Customer's email address
|
|
561
|
+
*/
|
|
562
|
+
email?: string;
|
|
563
|
+
/**
|
|
564
|
+
* Customer's external ID from your system
|
|
565
|
+
*/
|
|
566
|
+
externalId?: string;
|
|
567
|
+
/**
|
|
568
|
+
* Customer's internal ID in Recur
|
|
569
|
+
*/
|
|
570
|
+
id?: string;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Entitlement status derived from subscription status
|
|
574
|
+
*/
|
|
575
|
+
type EntitlementStatus = 'active' | 'trialing' | 'past_due' | 'canceled';
|
|
576
|
+
/**
|
|
577
|
+
* An entitlement represents a customer's access to a product
|
|
578
|
+
* Computed from active subscriptions
|
|
579
|
+
*/
|
|
580
|
+
interface Entitlement {
|
|
581
|
+
/**
|
|
582
|
+
* Product slug (URL-friendly identifier)
|
|
583
|
+
* @example 'pro-plan'
|
|
584
|
+
*/
|
|
585
|
+
product: string;
|
|
586
|
+
/**
|
|
587
|
+
* Product ID
|
|
588
|
+
* @example 'prod_abc123'
|
|
589
|
+
*/
|
|
590
|
+
productId: string;
|
|
591
|
+
/**
|
|
592
|
+
* Current status of the entitlement
|
|
593
|
+
*/
|
|
594
|
+
status: EntitlementStatus;
|
|
595
|
+
/**
|
|
596
|
+
* Associated subscription ID
|
|
597
|
+
* @example 'sub_xyz789'
|
|
598
|
+
*/
|
|
599
|
+
subscriptionId: string;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Options for the check() method
|
|
603
|
+
*/
|
|
604
|
+
interface CheckOptions {
|
|
605
|
+
/**
|
|
606
|
+
* Product slug or ID to check access for
|
|
607
|
+
* @example 'pro-plan' or 'prod_abc123'
|
|
608
|
+
*/
|
|
609
|
+
product: string;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Reason why access was denied
|
|
613
|
+
*/
|
|
614
|
+
type CheckDeniedReason = 'no_customer' | 'no_subscription' | 'expired' | 'wrong_product';
|
|
615
|
+
/**
|
|
616
|
+
* Result of the check() method
|
|
617
|
+
*/
|
|
618
|
+
interface CheckResult {
|
|
619
|
+
/**
|
|
620
|
+
* Whether the customer has access to the product
|
|
621
|
+
*/
|
|
622
|
+
allowed: boolean;
|
|
623
|
+
/**
|
|
624
|
+
* Reason for denial (only present when allowed is false)
|
|
625
|
+
*/
|
|
626
|
+
reason?: CheckDeniedReason;
|
|
627
|
+
/**
|
|
628
|
+
* Associated subscription (only present when allowed is true)
|
|
629
|
+
*/
|
|
630
|
+
subscription?: EntitlementSubscription;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Simplified subscription info returned in check result
|
|
634
|
+
*/
|
|
635
|
+
interface EntitlementSubscription {
|
|
636
|
+
/**
|
|
637
|
+
* Subscription ID
|
|
638
|
+
*/
|
|
639
|
+
id: string;
|
|
640
|
+
/**
|
|
641
|
+
* Subscription status
|
|
642
|
+
*/
|
|
643
|
+
status: string;
|
|
644
|
+
/**
|
|
645
|
+
* Product information
|
|
646
|
+
*/
|
|
647
|
+
product: {
|
|
648
|
+
id: string;
|
|
649
|
+
slug: string;
|
|
650
|
+
name: string;
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
653
|
+
* End of current billing period (ISO 8601)
|
|
654
|
+
*/
|
|
655
|
+
currentPeriodEnd: string;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Customer info returned from entitlements API
|
|
659
|
+
*/
|
|
660
|
+
interface EntitlementCustomer {
|
|
661
|
+
/**
|
|
662
|
+
* Customer ID
|
|
663
|
+
*/
|
|
664
|
+
id: string;
|
|
665
|
+
/**
|
|
666
|
+
* Customer email
|
|
667
|
+
*/
|
|
668
|
+
email: string;
|
|
669
|
+
/**
|
|
670
|
+
* Customer name
|
|
671
|
+
*/
|
|
672
|
+
name?: string | null;
|
|
673
|
+
/**
|
|
674
|
+
* External ID from your system
|
|
675
|
+
*/
|
|
676
|
+
externalId?: string | null;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Return type of the useCustomer hook
|
|
680
|
+
*/
|
|
681
|
+
interface UseCustomerResult {
|
|
682
|
+
/**
|
|
683
|
+
* Customer information (null if not found or loading)
|
|
684
|
+
*/
|
|
685
|
+
customer: EntitlementCustomer | null;
|
|
686
|
+
/**
|
|
687
|
+
* The most recent active subscription (null if none)
|
|
688
|
+
*/
|
|
689
|
+
subscription: EntitlementSubscription | null;
|
|
690
|
+
/**
|
|
691
|
+
* All active entitlements for this customer
|
|
692
|
+
*/
|
|
693
|
+
entitlements: Entitlement[];
|
|
694
|
+
/**
|
|
695
|
+
* Check if customer has access to a specific product
|
|
696
|
+
* Synchronous - reads from local cache
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* const { allowed } = check({ product: 'pro-plan' });
|
|
700
|
+
* if (!allowed) return <UpgradePrompt />;
|
|
701
|
+
*/
|
|
702
|
+
check: (options: CheckOptions) => CheckResult;
|
|
703
|
+
/**
|
|
704
|
+
* Manually refresh entitlements from the API
|
|
705
|
+
* Call this after checkout to update permissions
|
|
706
|
+
*
|
|
707
|
+
* @example
|
|
708
|
+
* await checkout({ productId: 'prod_xxx' });
|
|
709
|
+
* await refetch(); // Update entitlements
|
|
710
|
+
*/
|
|
711
|
+
refetch: () => Promise<void>;
|
|
712
|
+
/**
|
|
713
|
+
* Whether entitlements are currently being fetched
|
|
714
|
+
*/
|
|
715
|
+
isLoading: boolean;
|
|
716
|
+
/**
|
|
717
|
+
* Error if the last fetch failed
|
|
718
|
+
*/
|
|
719
|
+
error: Error | null;
|
|
720
|
+
}
|
|
554
721
|
|
|
555
722
|
/**
|
|
556
723
|
* Recur Payment Form Web Component (混合模式)
|
|
@@ -747,6 +914,21 @@ declare global {
|
|
|
747
914
|
interface RecurProviderProps {
|
|
748
915
|
children: React.ReactNode;
|
|
749
916
|
config?: RecurConfig;
|
|
917
|
+
/**
|
|
918
|
+
* Customer identifier for entitlements lookup
|
|
919
|
+
* When provided, the SDK will fetch the customer's entitlements on mount
|
|
920
|
+
*
|
|
921
|
+
* @example
|
|
922
|
+
* ```tsx
|
|
923
|
+
* <RecurProvider
|
|
924
|
+
* config={{ publishableKey: 'pk_test_xxx' }}
|
|
925
|
+
* customer={{ email: user?.email }}
|
|
926
|
+
* >
|
|
927
|
+
* <App />
|
|
928
|
+
* </RecurProvider>
|
|
929
|
+
* ```
|
|
930
|
+
*/
|
|
931
|
+
customer?: CustomerIdentifier;
|
|
750
932
|
}
|
|
751
933
|
/**
|
|
752
934
|
* RecurProvider
|
|
@@ -760,7 +942,7 @@ interface RecurProviderProps {
|
|
|
760
942
|
* </RecurProvider>
|
|
761
943
|
* ```
|
|
762
944
|
*/
|
|
763
|
-
declare function RecurProvider({ children, config: initialConfig }: RecurProviderProps): react_jsx_runtime.JSX.Element;
|
|
945
|
+
declare function RecurProvider({ children, config: initialConfig, customer: customerIdentifier }: RecurProviderProps): react_jsx_runtime.JSX.Element;
|
|
764
946
|
|
|
765
947
|
/**
|
|
766
948
|
* useRecur Hook
|
|
@@ -946,4 +1128,111 @@ interface UseSubscribeResult {
|
|
|
946
1128
|
*/
|
|
947
1129
|
declare function useSubscribe(options?: UseSubscribeOptions): UseSubscribeResult;
|
|
948
1130
|
|
|
949
|
-
|
|
1131
|
+
/**
|
|
1132
|
+
* useCustomer Hook
|
|
1133
|
+
*
|
|
1134
|
+
* Access customer entitlements and check product access without requiring
|
|
1135
|
+
* your own database or webhook integration. The hook fetches entitlements
|
|
1136
|
+
* from Recur on mount and provides synchronous access via the check() method.
|
|
1137
|
+
*
|
|
1138
|
+
* Must be used within a RecurProvider with a customer identifier:
|
|
1139
|
+
* ```tsx
|
|
1140
|
+
* <RecurProvider
|
|
1141
|
+
* config={{ publishableKey: 'pk_xxx' }}
|
|
1142
|
+
* customer={{ email: user?.email }}
|
|
1143
|
+
* >
|
|
1144
|
+
* <App />
|
|
1145
|
+
* </RecurProvider>
|
|
1146
|
+
* ```
|
|
1147
|
+
*
|
|
1148
|
+
* ## Basic Usage - Permission Check
|
|
1149
|
+
* @example
|
|
1150
|
+
* ```tsx
|
|
1151
|
+
* function PremiumFeature() {
|
|
1152
|
+
* const { check, isLoading } = useCustomer();
|
|
1153
|
+
*
|
|
1154
|
+
* if (isLoading) return <Spinner />;
|
|
1155
|
+
*
|
|
1156
|
+
* const { allowed } = check({ product: 'pro-plan' });
|
|
1157
|
+
*
|
|
1158
|
+
* if (!allowed) {
|
|
1159
|
+
* return <UpgradePrompt />;
|
|
1160
|
+
* }
|
|
1161
|
+
*
|
|
1162
|
+
* return <PremiumContent />;
|
|
1163
|
+
* }
|
|
1164
|
+
* ```
|
|
1165
|
+
*
|
|
1166
|
+
* ## Display Subscription Status
|
|
1167
|
+
* @example
|
|
1168
|
+
* ```tsx
|
|
1169
|
+
* function AccountPage() {
|
|
1170
|
+
* const { customer, subscription, entitlements, isLoading } = useCustomer();
|
|
1171
|
+
*
|
|
1172
|
+
* if (isLoading) return <Spinner />;
|
|
1173
|
+
*
|
|
1174
|
+
* return (
|
|
1175
|
+
* <div>
|
|
1176
|
+
* <h1>Hi, {customer?.name}</h1>
|
|
1177
|
+
* {subscription ? (
|
|
1178
|
+
* <>
|
|
1179
|
+
* <p>Plan: {subscription.product.name}</p>
|
|
1180
|
+
* <p>Renews: {new Date(subscription.currentPeriodEnd).toLocaleDateString()}</p>
|
|
1181
|
+
* </>
|
|
1182
|
+
* ) : (
|
|
1183
|
+
* <p>No active subscription</p>
|
|
1184
|
+
* )}
|
|
1185
|
+
* </div>
|
|
1186
|
+
* );
|
|
1187
|
+
* }
|
|
1188
|
+
* ```
|
|
1189
|
+
*
|
|
1190
|
+
* ## Refresh After Checkout
|
|
1191
|
+
* @example
|
|
1192
|
+
* ```tsx
|
|
1193
|
+
* function CheckoutButton({ productId }) {
|
|
1194
|
+
* const { checkout } = useRecur();
|
|
1195
|
+
* const { refetch } = useCustomer();
|
|
1196
|
+
*
|
|
1197
|
+
* const handleCheckout = async () => {
|
|
1198
|
+
* await checkout({
|
|
1199
|
+
* productId,
|
|
1200
|
+
* onPaymentComplete: async () => {
|
|
1201
|
+
* await refetch(); // Update entitlements after purchase
|
|
1202
|
+
* },
|
|
1203
|
+
* });
|
|
1204
|
+
* };
|
|
1205
|
+
*
|
|
1206
|
+
* return <button onClick={handleCheckout}>Upgrade</button>;
|
|
1207
|
+
* }
|
|
1208
|
+
* ```
|
|
1209
|
+
*
|
|
1210
|
+
* ## Security Note
|
|
1211
|
+
*
|
|
1212
|
+
* The check() method reads from a local cache and can be bypassed client-side.
|
|
1213
|
+
* Use it only for UI gating. For secure access control, always verify
|
|
1214
|
+
* permissions on your backend using the Server SDK:
|
|
1215
|
+
*
|
|
1216
|
+
* ```typescript
|
|
1217
|
+
* // Backend API route
|
|
1218
|
+
* const { allowed } = await recur.entitlements.check({
|
|
1219
|
+
* product: 'pro-plan',
|
|
1220
|
+
* customer: { email: user.email },
|
|
1221
|
+
* });
|
|
1222
|
+
* if (!allowed) return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
1223
|
+
* ```
|
|
1224
|
+
*
|
|
1225
|
+
* @returns {UseCustomerResult} Object containing:
|
|
1226
|
+
* - `customer` - Customer info (null if not found)
|
|
1227
|
+
* - `subscription` - Most recent active subscription (null if none)
|
|
1228
|
+
* - `entitlements` - Array of all active entitlements
|
|
1229
|
+
* - `check(options)` - Synchronous permission check
|
|
1230
|
+
* - `refetch()` - Manual refresh (debounced)
|
|
1231
|
+
* - `isLoading` - Whether data is being fetched
|
|
1232
|
+
* - `error` - Error from last fetch attempt
|
|
1233
|
+
*
|
|
1234
|
+
* @throws {Error} If used outside of RecurProvider
|
|
1235
|
+
*/
|
|
1236
|
+
declare function useCustomer(): UseCustomerResult;
|
|
1237
|
+
|
|
1238
|
+
export { type CheckDeniedReason, type CheckOptions, type CheckResult, type CheckoutError, type CheckoutOptions, type CheckoutResult, type CustomerIdentifier, type Entitlement, type EntitlementCustomer, type EntitlementStatus, type EntitlementSubscription, type Plan, type PlansResult, type Product, type ProductMetadata, type ProductsResult, type RecurConfig, type RecurContextValue, RecurProvider, type RecurProviderProps, type SubscriptionResult, type UseCustomerResult, type UseProductsOptions as UsePlansOptions, type UseProductsResult as UsePlansResult, type UseProductsOptions, type UseProductsResult, type UseSubscribeOptions, type UseSubscribeResult, useCustomer, useProducts as usePlans, useProducts, useRecur, useSubscribe };
|