Recur
開發者指南權限檢查

權限檢查 (Entitlements)

使用 Entitlements API 檢查客戶是否有權限存取特定產品

概述

Recur 提供 Entitlements API 讓你檢查客戶是否有權限存取特定產品。支援 SUBSCRIPTION(訂閱)和 ONE_TIME(一次性購買)兩種產品類型。

前端的 check() 是從本地快取讀取,僅用於 UI 顯示控制。敏感操作請在後端使用 Server SDK 驗證。

React SDK

設置 Provider

使用 useCustomer hook 前,必須在 RecurProvider 中提供 customer 參數:

app/layout.tsx
'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

components/premium-feature.tsx
'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 權限控制元件

components/feature-gate.tsx
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:結帳後更新權限

components/checkout-button.tsx
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:顯示訂閱狀態

components/subscription-status.tsx
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() 可被繞過,敏感操作務必在後端驗證!

app/api/premium/route.ts
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 });
}

重要注意事項

  1. check() 預設是同步的:從本地快取讀取,零延遲
  2. 使用 { live: true } 做即時檢查:會呼叫 API 取得最新資料
  3. 前端檢查僅用於 UI:敏感操作請在後端使用 Server SDK 驗證
  4. ONE_TIME 產品的 expiresAt 為 null:表示永久存取權
  5. 訂閱優先於一次性購買:如果同一產品同時有訂閱和購買,會以訂閱為主

On this page