開發者指南權限檢查
權限檢查 (Entitlements)
使用 Entitlements API 檢查客戶是否有權限存取特定產品
概述
Recur 提供 Entitlements API 讓你檢查客戶是否有權限存取特定產品。支援 SUBSCRIPTION(訂閱)和 ONE_TIME(一次性購買)兩種產品類型。
前端的 check() 是從本地快取讀取,僅用於 UI 顯示控制。敏感操作請在後端使用 Server SDK 驗證。
React SDK
設置 Provider
使用 useCustomer hook 前,必須在 RecurProvider 中提供 customer 參數:
'use client';
import { RecurProvider } from 'recur-tw';
export default function Layout({ children }) {
const user = useUser(); // 你的用戶狀態
return (
<RecurProvider
config={{ publishableKey: 'pk_test_xxx' }}
customer={{ email: user?.email }}
>
{children}
</RecurProvider>
);
}使用 useCustomer Hook
'use client';
import { useCustomer } from 'recur-tw';
function PremiumFeature() {
const { check, isLoading } = useCustomer();
if (isLoading) return <Loading />;
// 字串簡寫(推薦)
const { allowed, entitlement } = check("pro-plan");
if (!allowed) {
return <UpgradePrompt />;
}
return <PremiumContent />;
}check() 用法
// 推薦用法 - 簡潔明瞭
const { allowed } = check("pro-plan");// 物件形式 - 未來可擴展支援 feature/benefit
const { allowed } = check({ product: "pro-plan" });// 從 API 取得最新資料(非同步)
const { allowed } = await check("pro-plan", { live: true });check() 回傳值
interface CheckResult {
allowed: boolean; // 是否有權限
reason?: string; // 拒絕原因
entitlement?: { // 授權詳情(allowed 為 true 時)
product: string; // 產品 slug
productId: string; // 產品 ID
status: string; // 狀態
source: string; // 'subscription' | 'order'
sourceId: string; // 訂閱或訂單 ID
grantedAt: string; // 授權開始時間
expiresAt: string | null; // 到期時間(null 為永久)
};
}拒絕原因 (reason)
| 值 | 說明 |
|---|---|
no_customer | 找不到客戶(未提供 customer 或客戶不存在) |
no_entitlement | 客戶沒有任何權限 |
not_found | 客戶有權限,但不包含指定產品 |
權限狀態 (status)
| 值 | 說明 |
|---|---|
active | 訂閱生效中 |
trialing | 試用期間 |
past_due | 付款逾期但仍有存取權 |
canceled | 已取消但尚未到期 |
purchased | 一次性購買(永久) |
Vanilla JS
使用 Recur.create() 初始化
<script src="https://unpkg.com/recur-tw/dist/recur.umd.js"></script>
<script>
async function init() {
// 非同步初始化,會預先載入 entitlements
const recur = await RecurCheckout.create({
publishableKey: 'pk_test_xxx',
customer: { email: 'user@example.com' }
});
// 同步檢查(從快取讀取)
const { allowed, entitlement } = recur.check("pro-plan");
if (allowed) {
console.log('有權限存取!');
console.log('來源:', entitlement.source);
console.log('到期:', entitlement.expiresAt);
}
}
init();
</script>存取屬性
recur.customer // 客戶資訊
recur.subscription // 最近的訂閱
recur.entitlements // 所有權限陣列
recur.isLoading // 是否載入中
recur.error // 錯誤資訊手動重新整理
// 付款完成後更新權限
await recur.refetch();
// 重新檢查
const { allowed } = recur.check("pro-plan");常見使用情境
情境 1:UI 權限控制元件
function FeatureGate({ feature, children, fallback }) {
const { check, isLoading } = useCustomer();
if (isLoading) return null;
const { allowed } = check(feature);
return allowed ? children : fallback;
}
// 使用方式
<FeatureGate feature="pro-plan" fallback={<UpgradeButton />}>
<ProFeature />
</FeatureGate>情境 2:結帳後更新權限
function CheckoutButton({ productId }) {
const { checkout } = useRecur();
const { refetch } = useCustomer();
const handleCheckout = async () => {
await checkout({
productId,
onPaymentComplete: async () => {
await refetch(); // 付款完成後更新權限
},
});
};
return <button onClick={handleCheckout}>購買</button>;
}情境 3:顯示訂閱狀態
function SubscriptionStatus() {
const { check } = useCustomer();
const { allowed, entitlement } = check("pro-plan");
if (!allowed) return <p>尚未訂閱</p>;
const statusLabels = {
active: '生效中',
trialing: '試用中',
past_due: '待付款',
canceled: '已取消',
purchased: '已購買',
};
return (
<div>
<p>狀態:{statusLabels[entitlement.status]}</p>
<p>來源:{entitlement.source === 'subscription' ? '訂閱' : '購買'}</p>
{entitlement.expiresAt && (
<p>到期:{new Date(entitlement.expiresAt).toLocaleDateString('zh-TW')}</p>
)}
</div>
);
}後端驗證
前端的 check() 可被繞過,敏感操作務必在後端驗證!
import { Recur } from 'recur-tw/server';
const recur = new Recur({ secretKey: process.env.RECUR_SECRET_KEY! });
export async function GET(request: Request) {
const user = await getUser(request);
const { allowed } = await recur.entitlements.check({
product: 'pro-plan',
customer: { email: user.email },
});
if (!allowed) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
// 返回敏感資料...
return Response.json({ data: sensitiveData });
}重要注意事項
- check() 預設是同步的:從本地快取讀取,零延遲
- 使用
{ live: true }做即時檢查:會呼叫 API 取得最新資料 - 前端檢查僅用於 UI:敏感操作請在後端使用 Server SDK 驗證
- ONE_TIME 產品的 expiresAt 為 null:表示永久存取權
- 訂閱優先於一次性購買:如果同一產品同時有訂閱和購買,會以訂閱為主