recur-skills 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: recur-checkout
3
+ description: Implement Recur checkout flows including embedded, modal, and redirect modes. Use when adding payment buttons, checkout forms, subscription purchase flows, or when user mentions "checkout", "結帳", "付款按鈕", "embedded checkout".
4
+ ---
5
+
6
+ # Recur Checkout Integration
7
+
8
+ You are helping implement Recur checkout flows. Recur supports multiple checkout modes for different use cases.
9
+
10
+ ## Checkout Modes
11
+
12
+ | Mode | Best For | User Experience |
13
+ |------|----------|-----------------|
14
+ | `embedded` | SPA apps | Form renders inline in your page |
15
+ | `modal` | Quick purchases | Form appears in a dialog overlay |
16
+ | `redirect` | Simple integration | Full page redirect to Recur |
17
+
18
+ ## Basic Implementation
19
+
20
+ ### Using useRecur Hook
21
+
22
+ ```tsx
23
+ import { useRecur } from 'recur-tw'
24
+
25
+ function CheckoutButton({ productId }: { productId: string }) {
26
+ const { checkout, isLoading } = useRecur()
27
+
28
+ const handleClick = async () => {
29
+ await checkout({
30
+ productId,
31
+ // Or use productSlug: 'pro-plan'
32
+
33
+ // Optional: Pre-fill customer info
34
+ customerEmail: 'user@example.com',
35
+ customerName: 'John Doe',
36
+
37
+ // Optional: Link to your user system
38
+ externalCustomerId: 'user_123',
39
+
40
+ // Callbacks
41
+ onPaymentComplete: (result) => {
42
+ console.log('Success!', result)
43
+ // result.id - Subscription/Order ID
44
+ // result.status - 'ACTIVE', 'TRIALING', etc.
45
+ },
46
+ onPaymentFailed: (error) => {
47
+ console.error('Failed:', error)
48
+ return { action: 'retry' } // or 'close' or 'custom'
49
+ },
50
+ onPaymentCancel: () => {
51
+ console.log('User cancelled')
52
+ },
53
+ })
54
+ }
55
+
56
+ return (
57
+ <button onClick={handleClick} disabled={isLoading}>
58
+ {isLoading ? 'Processing...' : 'Subscribe'}
59
+ </button>
60
+ )
61
+ }
62
+ ```
63
+
64
+ ### Using useSubscribe Hook (with state management)
65
+
66
+ ```tsx
67
+ import { useSubscribe } from 'recur-tw'
68
+
69
+ function SubscribeButton({ productId }: { productId: string }) {
70
+ const { subscribe, isLoading, error, subscription } = useSubscribe()
71
+
72
+ const handleClick = () => {
73
+ subscribe({
74
+ productId,
75
+ onPaymentComplete: (sub) => {
76
+ // Subscription created successfully
77
+ router.push('/dashboard')
78
+ },
79
+ })
80
+ }
81
+
82
+ if (subscription) {
83
+ return <p>Subscribed! ID: {subscription.id}</p>
84
+ }
85
+
86
+ return (
87
+ <>
88
+ <button onClick={handleClick} disabled={isLoading}>
89
+ Subscribe
90
+ </button>
91
+ {error && <p className="error">{error.message}</p>}
92
+ </>
93
+ )
94
+ }
95
+ ```
96
+
97
+ ## Embedded Mode Setup
98
+
99
+ For embedded mode, you need a container element:
100
+
101
+ ```tsx
102
+ // In RecurProvider config
103
+ <RecurProvider
104
+ config={{
105
+ publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY,
106
+ checkoutMode: 'embedded',
107
+ containerElementId: 'recur-checkout-container',
108
+ }}
109
+ >
110
+ {children}
111
+ </RecurProvider>
112
+
113
+ // In your checkout page
114
+ function CheckoutPage() {
115
+ return (
116
+ <div>
117
+ <h1>Complete Your Purchase</h1>
118
+ {/* Recur will render the payment form here */}
119
+ <div id="recur-checkout-container" />
120
+ </div>
121
+ )
122
+ }
123
+ ```
124
+
125
+ ## Handling 3D Verification
126
+
127
+ Recur handles 3D Secure automatically. For mobile apps or specific flows:
128
+
129
+ ```tsx
130
+ await checkout({
131
+ productId,
132
+ // These URLs are used when 3D verification requires redirect
133
+ successUrl: 'https://yourapp.com/checkout/success',
134
+ cancelUrl: 'https://yourapp.com/checkout/cancel',
135
+ })
136
+ ```
137
+
138
+ ## Product Types
139
+
140
+ Recur supports different product types:
141
+
142
+ ```tsx
143
+ // Subscription (recurring)
144
+ checkout({ productId: 'prod_subscription_xxx' })
145
+
146
+ // One-time purchase
147
+ checkout({ productId: 'prod_onetime_xxx' })
148
+
149
+ // Credits (prepaid wallet)
150
+ checkout({ productId: 'prod_credits_xxx' })
151
+
152
+ // Donation (variable amount)
153
+ checkout({ productId: 'prod_donation_xxx' })
154
+ ```
155
+
156
+ ## Listing Products
157
+
158
+ ```tsx
159
+ import { useProducts } from 'recur-tw'
160
+
161
+ function PricingPage() {
162
+ const { products, isLoading } = useProducts({
163
+ type: 'SUBSCRIPTION', // Filter by type
164
+ })
165
+
166
+ if (isLoading) return <div>Loading...</div>
167
+
168
+ return (
169
+ <div className="pricing-grid">
170
+ {products.map(product => (
171
+ <PricingCard key={product.id} product={product} />
172
+ ))}
173
+ </div>
174
+ )
175
+ }
176
+ ```
177
+
178
+ ## Payment Failed Handling
179
+
180
+ ```tsx
181
+ onPaymentFailed: (error) => {
182
+ // error.code tells you what went wrong
183
+ switch (error.code) {
184
+ case 'CARD_DECLINED':
185
+ return { action: 'retry' }
186
+ case 'INSUFFICIENT_FUNDS':
187
+ return {
188
+ action: 'custom',
189
+ customTitle: '餘額不足',
190
+ customMessage: '請使用其他付款方式',
191
+ }
192
+ default:
193
+ return { action: 'close' }
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Server-Side Checkout (API)
199
+
200
+ For server-rendered apps or custom flows:
201
+
202
+ ```typescript
203
+ // Create checkout session
204
+ const response = await fetch('https://api.recur.tw/v1/checkouts', {
205
+ method: 'POST',
206
+ headers: {
207
+ 'X-Recur-Secret-Key': process.env.RECUR_SECRET_KEY,
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ body: JSON.stringify({
211
+ productId: 'prod_xxx',
212
+ customerEmail: 'user@example.com',
213
+ successUrl: 'https://yourapp.com/success',
214
+ cancelUrl: 'https://yourapp.com/cancel',
215
+ }),
216
+ })
217
+
218
+ const { checkoutUrl } = await response.json()
219
+ // Redirect user to checkoutUrl
220
+ ```
221
+
222
+ ## Checkout Result Structure
223
+
224
+ ```typescript
225
+ interface CheckoutResult {
226
+ id: string // Subscription or Order ID
227
+ status: string // 'ACTIVE', 'TRIALING', 'PENDING'
228
+ productId: string
229
+ amount: number // In cents (e.g., 29900 = NT$299)
230
+ billingPeriod?: string // 'MONTHLY', 'YEARLY' for subscriptions
231
+ currentPeriodEnd?: string // ISO date
232
+ trialEndsAt?: string // ISO date if trial
233
+ }
234
+ ```
235
+
236
+ ## Best Practices
237
+
238
+ 1. **Always handle all callbacks** - onPaymentComplete, onPaymentFailed, onPaymentCancel
239
+ 2. **Show loading states** - Use isLoading to disable buttons during checkout
240
+ 3. **Pre-fill customer info** - Reduces friction if you already have user data
241
+ 4. **Use externalCustomerId** - Links Recur customers to your user system
242
+ 5. **Test in sandbox first** - Use `pk_test_` keys during development
243
+
244
+ ## Related Skills
245
+
246
+ - `/recur-quickstart` - Initial SDK setup
247
+ - `/recur-webhooks` - Receive payment notifications
248
+ - `/recur-entitlements` - Check subscription access
@@ -0,0 +1,389 @@
1
+ ---
2
+ name: recur-entitlements
3
+ description: 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".
4
+ ---
5
+
6
+ # Recur Entitlements & Access Control
7
+
8
+ 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).
9
+
10
+ ## Quick Start: Client-Side Check
11
+
12
+ ```tsx
13
+ import { RecurProvider, useCustomer } from 'recur-tw'
14
+
15
+ // 1. Wrap app with provider and identify customer
16
+ function App() {
17
+ return (
18
+ <RecurProvider
19
+ config={{ publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY }}
20
+ customer={{ email: 'user@example.com' }}
21
+ >
22
+ <MyApp />
23
+ </RecurProvider>
24
+ )
25
+ }
26
+
27
+ // 2. Check access anywhere in your app
28
+ function PremiumFeature() {
29
+ const { check, isLoading } = useCustomer()
30
+
31
+ if (isLoading) return <div>Loading...</div>
32
+
33
+ const { allowed } = check('pro-plan')
34
+
35
+ if (!allowed) {
36
+ return <UpgradePrompt />
37
+ }
38
+
39
+ return <PremiumContent />
40
+ }
41
+ ```
42
+
43
+ ## Customer Identification
44
+
45
+ Identify customers using one of these methods:
46
+
47
+ ```tsx
48
+ // By email (most common)
49
+ <RecurProvider customer={{ email: 'user@example.com' }}>
50
+
51
+ // By your system's user ID
52
+ <RecurProvider customer={{ externalId: 'user_123' }}>
53
+
54
+ // By Recur customer ID
55
+ <RecurProvider customer={{ id: 'cus_xxx' }}>
56
+ ```
57
+
58
+ ## Checking Access
59
+
60
+ ### Synchronous Check (Cached)
61
+
62
+ Fast, uses cached data. Good for UI rendering.
63
+
64
+ ```tsx
65
+ const { check } = useCustomer()
66
+
67
+ // Check by product slug
68
+ const { allowed, entitlement } = check('pro-plan')
69
+
70
+ // Check by product ID
71
+ const { allowed } = check('prod_xxx')
72
+
73
+ if (allowed) {
74
+ // User has access
75
+ // entitlement contains details like status, expiresAt
76
+ }
77
+ ```
78
+
79
+ ### Async Check (Live)
80
+
81
+ Fetches fresh data from API. Use for critical operations.
82
+
83
+ ```tsx
84
+ const { check } = useCustomer()
85
+
86
+ // Real-time check
87
+ const { allowed, entitlement } = await check('pro-plan', { live: true })
88
+
89
+ // Good for:
90
+ // - Before processing important actions
91
+ // - After checkout to confirm access
92
+ // - When cached data might be stale
93
+ ```
94
+
95
+ ### Manual Refetch
96
+
97
+ ```tsx
98
+ const { refetch } = useCustomer()
99
+
100
+ // After checkout completion
101
+ onPaymentComplete: async () => {
102
+ await refetch() // Refresh entitlements
103
+ router.push('/dashboard')
104
+ }
105
+ ```
106
+
107
+ ## Entitlement Response Structure
108
+
109
+ ```typescript
110
+ interface Entitlement {
111
+ product: string // Product slug
112
+ productId: string // Product ID
113
+ status: EntitlementStatus
114
+ source: 'subscription' | 'order' // How they got access
115
+ sourceId: string // Subscription/Order ID
116
+ grantedAt: string // When access was granted
117
+ expiresAt: string | null // When access expires (null = permanent)
118
+ }
119
+
120
+ type EntitlementStatus =
121
+ | 'active' // Subscription active
122
+ | 'trialing' // In trial period
123
+ | 'past_due' // Payment failed, in grace period
124
+ | 'canceled' // Cancelled but access until period end
125
+ | 'purchased' // One-time purchase (permanent)
126
+ ```
127
+
128
+ ## Server-Side Checking
129
+
130
+ ### Using Server SDK
131
+
132
+ ```typescript
133
+ import { Recur } from 'recur-tw/server'
134
+
135
+ const recur = new Recur(process.env.RECUR_SECRET_KEY!)
136
+
137
+ // In API route or server action
138
+ async function checkAccess(userEmail: string) {
139
+ const { allowed, entitlement } = await recur.entitlements.check({
140
+ product: 'pro-plan',
141
+ customer: { email: userEmail },
142
+ })
143
+
144
+ if (!allowed) {
145
+ throw new Error('Upgrade required')
146
+ }
147
+
148
+ return entitlement
149
+ }
150
+ ```
151
+
152
+ ### Using REST API Directly
153
+
154
+ ```typescript
155
+ // GET /api/v1/customers/entitlements
156
+ const response = await fetch(
157
+ `https://api.recur.tw/v1/customers/entitlements?email=${encodeURIComponent(email)}`,
158
+ {
159
+ headers: {
160
+ 'X-Recur-Secret-Key': process.env.RECUR_SECRET_KEY!,
161
+ },
162
+ }
163
+ )
164
+
165
+ const { customer, subscription, entitlements } = await response.json()
166
+ ```
167
+
168
+ ## Common Patterns
169
+
170
+ ### Paywall Component
171
+
172
+ ```tsx
173
+ function Paywall({
174
+ children,
175
+ product,
176
+ fallback
177
+ }: {
178
+ children: React.ReactNode
179
+ product: string
180
+ fallback?: React.ReactNode
181
+ }) {
182
+ const { check, isLoading } = useCustomer()
183
+
184
+ if (isLoading) {
185
+ return <div>Loading...</div>
186
+ }
187
+
188
+ const { allowed } = check(product)
189
+
190
+ if (!allowed) {
191
+ return fallback || <UpgradePrompt product={product} />
192
+ }
193
+
194
+ return <>{children}</>
195
+ }
196
+
197
+ // Usage
198
+ <Paywall product="pro-plan">
199
+ <PremiumDashboard />
200
+ </Paywall>
201
+ ```
202
+
203
+ ### Feature Flag Style
204
+
205
+ ```tsx
206
+ function useFeature(featureProduct: string) {
207
+ const { check, isLoading } = useCustomer()
208
+
209
+ if (isLoading) {
210
+ return { enabled: false, loading: true }
211
+ }
212
+
213
+ const { allowed, entitlement } = check(featureProduct)
214
+
215
+ return {
216
+ enabled: allowed,
217
+ loading: false,
218
+ entitlement,
219
+ isTrial: entitlement?.status === 'trialing',
220
+ isPastDue: entitlement?.status === 'past_due',
221
+ }
222
+ }
223
+
224
+ // Usage
225
+ function MyComponent() {
226
+ const { enabled, isTrial } = useFeature('pro-plan')
227
+
228
+ if (!enabled) return <UpgradeButton />
229
+
230
+ return (
231
+ <>
232
+ {isTrial && <TrialBanner />}
233
+ <ProFeature />
234
+ </>
235
+ )
236
+ }
237
+ ```
238
+
239
+ ### API Middleware
240
+
241
+ ```typescript
242
+ // middleware/requireSubscription.ts
243
+ import { Recur } from 'recur-tw/server'
244
+
245
+ const recur = new Recur(process.env.RECUR_SECRET_KEY!)
246
+
247
+ export async function requireSubscription(
248
+ req: Request,
249
+ product: string
250
+ ) {
251
+ const userEmail = await getUserEmail(req) // Your auth logic
252
+
253
+ const { allowed, denial } = await recur.entitlements.check({
254
+ product,
255
+ customer: { email: userEmail },
256
+ })
257
+
258
+ if (!allowed) {
259
+ throw new Response(JSON.stringify({
260
+ error: 'Subscription required',
261
+ reason: denial?.reason, // 'no_customer', 'no_entitlement', etc.
262
+ }), {
263
+ status: 403,
264
+ headers: { 'Content-Type': 'application/json' },
265
+ })
266
+ }
267
+ }
268
+
269
+ // Usage in API route
270
+ export async function GET(req: Request) {
271
+ await requireSubscription(req, 'pro-plan')
272
+
273
+ // User has access, continue...
274
+ return Response.json({ data: 'premium content' })
275
+ }
276
+ ```
277
+
278
+ ### Multiple Product Tiers
279
+
280
+ ```tsx
281
+ function PricingGate() {
282
+ const { check } = useCustomer()
283
+
284
+ const hasPro = check('pro-plan').allowed
285
+ const hasEnterprise = check('enterprise-plan').allowed
286
+
287
+ if (hasEnterprise) {
288
+ return <EnterpriseDashboard />
289
+ }
290
+
291
+ if (hasPro) {
292
+ return <ProDashboard />
293
+ }
294
+
295
+ return <FreeDashboard />
296
+ }
297
+ ```
298
+
299
+ ## Handling Edge Cases
300
+
301
+ ### Past Due Subscriptions
302
+
303
+ ```tsx
304
+ const { allowed, entitlement } = check('pro-plan')
305
+
306
+ if (allowed && entitlement?.status === 'past_due') {
307
+ // Show warning but allow access during grace period
308
+ return (
309
+ <>
310
+ <PaymentFailedBanner />
311
+ <PremiumContent />
312
+ </>
313
+ )
314
+ }
315
+ ```
316
+
317
+ ### Trial Subscriptions
318
+
319
+ ```tsx
320
+ const { entitlement } = check('pro-plan')
321
+
322
+ if (entitlement?.status === 'trialing') {
323
+ const trialEnds = new Date(entitlement.expiresAt!)
324
+ const daysLeft = Math.ceil((trialEnds - Date.now()) / (1000 * 60 * 60 * 24))
325
+
326
+ return <TrialBanner daysLeft={daysLeft} />
327
+ }
328
+ ```
329
+
330
+ ### Cancelled but Active
331
+
332
+ ```tsx
333
+ const { entitlement } = check('pro-plan')
334
+
335
+ if (entitlement?.status === 'canceled') {
336
+ // User cancelled but still has access until period end
337
+ return (
338
+ <>
339
+ <ResubscribeBanner expiresAt={entitlement.expiresAt} />
340
+ <PremiumContent />
341
+ </>
342
+ )
343
+ }
344
+ ```
345
+
346
+ ## Denial Reasons
347
+
348
+ When `allowed` is `false`, check the denial reason:
349
+
350
+ ```typescript
351
+ const { allowed, denial } = check('pro-plan')
352
+
353
+ if (!allowed) {
354
+ switch (denial?.reason) {
355
+ case 'no_customer':
356
+ // Customer not found
357
+ return <CreateAccountPrompt />
358
+
359
+ case 'no_entitlement':
360
+ // No subscription to this product
361
+ return <SubscribePrompt />
362
+
363
+ case 'expired':
364
+ // Subscription/access expired
365
+ return <RenewPrompt />
366
+
367
+ case 'insufficient_balance':
368
+ // For credit-based products
369
+ return <BuyCreditsPrompt />
370
+
371
+ default:
372
+ return <GenericUpgradePrompt />
373
+ }
374
+ }
375
+ ```
376
+
377
+ ## Best Practices
378
+
379
+ 1. **Use cached checks for UI** - Fast rendering, good UX
380
+ 2. **Use live checks for actions** - Ensure fresh data for important operations
381
+ 3. **Handle all statuses** - active, trialing, past_due, canceled
382
+ 4. **Refetch after checkout** - Ensure UI updates after purchase
383
+ 5. **Implement graceful degradation** - Show upgrade prompts, not errors
384
+
385
+ ## Related Skills
386
+
387
+ - `/recur-quickstart` - Initial SDK setup
388
+ - `/recur-checkout` - Implement purchase flows
389
+ - `/recur-webhooks` - Sync entitlements with webhooks