# 權限檢查 (Entitlements) (/guides/entitlements)





概述 [#概述]

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

<Callout type="info">
  前端的 `check()` 是從本地快取讀取，僅用於 UI 顯示控制。敏感操作請在後端使用 Server SDK 驗證。
</Callout>

React SDK [#react-sdk]

設置 Provider [#設置-provider]

使用 `useCustomer` hook 前，必須在 `RecurProvider` 中提供 `customer` 參數：

```tsx title="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 [#使用-usecustomer-hook]

```tsx title="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() 用法 [#check-用法]

<Tabs items="['字串簡寫', '物件形式', '即時檢查']">
  <Tab value="字串簡寫">
    ```tsx
    // 推薦用法 - 簡潔明瞭
    const { allowed } = check("pro-plan");
    ```
  </Tab>

  <Tab value="物件形式">
    ```tsx
    // 物件形式 - 未來可擴展支援 feature/benefit
    const { allowed } = check({ product: "pro-plan" });
    ```
  </Tab>

  <Tab value="即時檢查">
    ```tsx
    // 從 API 取得最新資料（非同步）
    const { allowed } = await check("pro-plan", { live: true });
    ```
  </Tab>
</Tabs>

check() 回傳值 [#check-回傳值]

```typescript
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) [#拒絕原因-reason]

| 值                | 說明                         |
| ---------------- | -------------------------- |
| `no_customer`    | 找不到客戶（未提供 customer 或客戶不存在） |
| `no_entitlement` | 客戶沒有任何權限                   |
| `not_found`      | 客戶有權限，但不包含指定產品             |

權限狀態 (status) [#權限狀態-status]

| 值           | 說明                    |
| ----------- | --------------------- |
| `active`    | 訂閱生效中                 |
| `trialing`  | 試用期間                  |
| `past_due`  | 付款逾期但仍有存取權（僅寬限期啟用時出現） |
| `canceled`  | 已取消但尚未到期              |
| `purchased` | 一次性購買（永久）             |

***

Vanilla JS [#vanilla-js]

使用 Recur.create() 初始化 [#使用-recurcreate-初始化]

```html
<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>
```

存取屬性 [#存取屬性]

```javascript
recur.customer       // 客戶資訊
recur.subscription   // 最近的訂閱
recur.entitlements   // 所有權限陣列
recur.isLoading      // 是否載入中
recur.error          // 錯誤資訊
```

手動重新整理 [#手動重新整理]

```javascript
// 付款完成後更新權限
await recur.refetch();

// 重新檢查
const { allowed } = recur.check("pro-plan");
```

***

常見使用情境 [#常見使用情境]

情境 1：Paywall — 未訂閱時導向結帳 [#情境-1paywall--未訂閱時導向結帳]

```tsx title="components/paywall.tsx"
'use client';
import { useCustomer, useRecur } from 'recur-tw';

function PremiumFeature() {
  const { check, isLoading } = useCustomer();
  const { redirectToCheckout } = useRecur();

  if (isLoading) return <Loading />;

  const { allowed } = check('pro-plan');

  if (!allowed) {
    return (
      <div>
        <p>此功能需要 Pro 方案</p>
        <button onClick={() => redirectToCheckout({
          productSlug: 'pro-plan',
          successUrl: `${window.location.origin}/success`,
          cancelUrl: window.location.href,
        })}>
          升級到 Pro
        </button>
      </div>
    );
  }

  return <PremiumContent />;
}
```

情境 2：UI 權限控制元件 [#情境-2ui-權限控制元件]

```tsx title="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>
```

情境 3：結帳後更新權限 [#情境-3結帳後更新權限]

使用 Hosted Checkout 時，可在 `successUrl` 頁面呼叫 `refetch()` 更新快取：

```tsx title="app/success/page.tsx"
'use client';
import { useCustomer } from 'recur-tw';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

function SuccessPage() {
  const { refetch } = useCustomer();
  const searchParams = useSearchParams();

  useEffect(() => {
    if (searchParams.get('session_id')) {
      refetch(); // 付款完成後更新權限快取
    }
  }, []);

  return <p>付款成功！感謝你的訂閱。</p>;
}
```

情境 4：顯示訂閱狀態 [#情境-4顯示訂閱狀態]

```tsx title="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>
  );
}
```

***

後端驗證 [#後端驗證]

<Callout type="warn">
  前端的 `check()` 可被繞過，敏感操作務必在後端驗證！
</Callout>

```typescript title="app/api/premium/route.ts"
import { Recur } from 'recur-tw/server';

const recur = new Recur(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. **訂閱優先於一次性購買**：如果同一產品同時有訂閱和購買，會以訂閱為主
