.agents/skills/recur-entitlements/SKILL.md
Implement access control and permission checking with Recur entitlements API. Use when building paywalls, checking subscription status, gating premium features, or when user mentions "paywall", "權限檢查", "entitlements", "access control", "premium features".
npx skillsauth add flsteven87/three_kingdoms_strategy recur-entitlementsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are helping implement access control using Recur's entitlements system. Entitlements let you check if a customer has access to your products (subscriptions or one-time purchases).
This project uses FastAPI (Python) + React (TypeScript). All examples match this stack.
import { RecurProvider, useCustomer } from 'recur-tw'
// 1. Wrap app with provider and identify customer
function App() {
const { user } = useAuth()
return (
<RecurProvider
config={{ publishableKey: import.meta.env.VITE_RECUR_PUBLISHABLE_KEY }}
customer={{ email: user.email }}
>
<Routes />
</RecurProvider>
)
}
// 2. Check access anywhere in your app
function PremiumFeature() {
const { check, isLoading } = useCustomer()
if (isLoading) return <div>載入中...</div>
const { allowed } = check('pro-plan')
if (!allowed) {
return <UpgradePrompt />
}
return <PremiumContent />
}
Warning: Client-side checks can be bypassed. Always verify on the backend for sensitive operations.
Identify customers using one of these methods:
// By email (most common)
<RecurProvider customer={{ email: user.email }}>
// By your system's user ID
<RecurProvider customer={{ externalId: user.id }}>
// By Recur customer ID
<RecurProvider customer={{ id: 'cus_xxx' }}>
Fast, uses cached data. Good for UI rendering.
const { check } = useCustomer()
// Check by product slug
const { allowed, entitlement } = check('pro-plan')
// Check by product ID
const { allowed } = check('prod_xxx')
if (allowed) {
// User has access
// entitlement contains details like status, expiresAt
}
Fetches fresh data from API. Use for critical operations.
const { check } = useCustomer()
// Real-time check — good for:
// - Before processing important actions
// - After checkout to confirm access
// - When cached data might be stale
const { allowed, entitlement } = await check('pro-plan', { live: true })
const { refetch } = useCustomer()
// After checkout completion
onPaymentComplete: async () => {
await refetch() // Refresh entitlements
navigate('/dashboard')
}
interface Entitlement {
product: string // Product slug
productId: string // Product ID
status: EntitlementStatus
source: 'subscription' | 'order' // How they got access
sourceId: string // Subscription/Order ID
grantedAt: string // When access was granted
expiresAt: string | null // When access expires (null = permanent)
}
type EntitlementStatus =
| 'active' // Subscription active
| 'trialing' // In trial period
| 'past_due' // Payment failed, in grace period
| 'canceled' // Cancelled but access until period end
| 'purchased' // One-time purchase (permanent)
# src/services/entitlement_service.py
import httpx
import logging
from src.core.config import get_settings
logger = logging.getLogger(__name__)
class EntitlementService:
"""Check Recur entitlements server-side."""
def __init__(self):
self._settings = get_settings()
async def check_access(
self,
product: str,
*,
email: str | None = None,
external_id: str | None = None,
) -> bool:
"""Check if a customer has access to a product."""
params: dict[str, str] = {"product": product}
if email:
params["email"] = email
elif external_id:
params["externalId"] = external_id
else:
raise ValueError("Must provide email or external_id")
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.recur.tw/v1/customers/entitlements",
params=params,
headers={
"X-Recur-Secret-Key": self._settings.recur_secret_key,
},
)
response.raise_for_status()
data = response.json()
return data.get("allowed", False)
async def get_entitlements(
self,
*,
email: str | None = None,
external_id: str | None = None,
) -> list[dict]:
"""Get all entitlements for a customer."""
params: dict[str, str] = {}
if email:
params["email"] = email
elif external_id:
params["externalId"] = external_id
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.recur.tw/v1/customers/entitlements",
params=params,
headers={
"X-Recur-Secret-Key": self._settings.recur_secret_key,
},
)
response.raise_for_status()
data = response.json()
return data.get("entitlements", [])
# src/core/dependencies.py
from fastapi import Depends, HTTPException
from src.services.entitlement_service import EntitlementService
async def require_subscription(
product: str,
user_email: str,
entitlement_service: EntitlementService = Depends(get_entitlement_service),
) -> None:
"""FastAPI dependency that requires an active subscription."""
has_access = await entitlement_service.check_access(
product,
email=user_email,
)
if not has_access:
raise HTTPException(
status_code=403,
detail={
"error": "subscription_required",
"message": "此功能需要訂閱才能使用",
"product": product,
},
)
# Usage in endpoint
@router.get("/premium-data")
async def get_premium_data(
current_user: User = Depends(get_current_user),
_: None = Depends(lambda: require_subscription("pro-plan", current_user.email)),
):
return {"data": "premium content"}
interface PaywallProps {
readonly children: React.ReactNode
readonly product: string
readonly fallback?: React.ReactNode
}
function Paywall({ children, product, fallback }: PaywallProps) {
const { check, isLoading } = useCustomer()
if (isLoading) return <div>載入中...</div>
const { allowed } = check(product)
if (!allowed) {
return fallback || <UpgradePrompt product={product} />
}
return <>{children}</>
}
// Usage
<Paywall product="pro-plan">
<PremiumDashboard />
</Paywall>
function useFeature(featureProduct: string) {
const { check, isLoading } = useCustomer()
if (isLoading) {
return { enabled: false, loading: true }
}
const { allowed, entitlement } = check(featureProduct)
return {
enabled: allowed,
loading: false,
entitlement,
isTrial: entitlement?.status === 'trialing',
isPastDue: entitlement?.status === 'past_due',
isCanceled: entitlement?.status === 'canceled',
}
}
// Usage
function MyComponent() {
const { enabled, isTrial } = useFeature('pro-plan')
if (!enabled) return <UpgradeButton />
return (
<>
{isTrial && <TrialBanner />}
<ProFeature />
</>
)
}
function PricingGate() {
const { check } = useCustomer()
const hasEnterprise = check('enterprise-plan').allowed
const hasPro = check('pro-plan').allowed
if (hasEnterprise) return <EnterpriseDashboard />
if (hasPro) return <ProDashboard />
return <FreeDashboard />
}
const { allowed, entitlement } = check('pro-plan')
if (allowed && entitlement?.status === 'past_due') {
// Show warning but allow access during grace period
return (
<>
<PaymentFailedBanner />
<PremiumContent />
</>
)
}
const { entitlement } = check('pro-plan')
if (entitlement?.status === 'canceled') {
// User cancelled but still has access until period end
return (
<>
<ResubscribeBanner expiresAt={entitlement.expiresAt} />
<PremiumContent />
</>
)
}
const { entitlement } = check('pro-plan')
if (entitlement?.status === 'trialing' && entitlement.expiresAt) {
const trialEnds = new Date(entitlement.expiresAt)
const daysLeft = Math.ceil((trialEnds.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
return <TrialBanner daysLeft={daysLeft} />
}
When allowed is false, check the denial reason:
const { allowed, denial } = check('pro-plan')
if (!allowed) {
switch (denial?.reason) {
case 'no_customer':
return <CreateAccountPrompt />
case 'no_entitlement':
return <SubscribePrompt />
case 'expired':
return <RenewPrompt />
case 'insufficient_balance':
return <BuyCreditsPrompt />
default:
return <GenericUpgradePrompt />
}
}
active, trialing, past_due, canceled, purchasedrefetch() to update UI after purchase/recur-quickstart - Initial SDK setup/recur-checkout - Implement purchase flows/recur-webhooks - Sync entitlements with webhooks (FastAPI handler)/recur-portal - Customer self-service managementdevelopment
Set up and handle Recur webhook events for payment notifications. Use when implementing webhook handlers, verifying signatures, handling subscription events, or when user mentions "webhook", "付款通知", "訂閱事件", "payment notification".
development
Quick setup guide for Recur payment integration. Use when starting a new Recur integration, setting up API keys, installing the SDK, or when user mentions "integrate Recur", "setup Recur", "Recur 串接", "金流設定".
development
Implement Customer Portal for subscription self-service. Use when building account pages, letting customers manage subscriptions, update payment methods, view billing history, or when user mentions "customer portal", "帳戶管理", "訂閱管理", "更新付款方式", "self-service".
data-ai
List all available Recur skills and how to use them. Use when user asks "what can Recur do", "Recur skills", "Recur 有什麼功能", "help with Recur", "如何使用 Recur skills".