---
title: 權限檢查 (Entitlements)
description: 使用 Entitlements API 檢查客戶是否有權限存取特定產品
---

## 概述

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

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

## React SDK

### 設置 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

```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() 用法

  
```tsx
// 推薦用法 - 簡潔明瞭
const { allowed } = check("pro-plan");
```
  
  
```tsx
// 物件形式 - 未來可擴展支援 feature/benefit
const { allowed } = check({ product: "pro-plan" });
```
  
  
```tsx
// 從 API 取得最新資料（非同步）
const { allowed } = await check("pro-plan", { live: true });
```
  

### 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)

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

#### 權限狀態 (status)

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

---

## Vanilla JS

### 使用 Recur.create() 初始化

```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 — 未訂閱時導向結帳

```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 權限控制元件

```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：結帳後更新權限

使用 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：顯示訂閱狀態

```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>
  );
}
```

---

## 後端驗證

  前端的 `check()` 可被繞過，敏感操作務必在後端驗證！

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