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

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

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() 更新快取:

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:顯示訂閱狀態

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

Last updated on

On this page