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,114 @@
1
+ ---
2
+ name: recur-quickstart
3
+ description: 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 串接", "金流設定".
4
+ ---
5
+
6
+ # Recur Quickstart
7
+
8
+ You are helping a developer integrate Recur, Taiwan's subscription payment platform (similar to Stripe Billing).
9
+
10
+ ## Step 1: Install SDK
11
+
12
+ ```bash
13
+ pnpm add recur-tw
14
+ # or
15
+ npm install recur-tw
16
+ ```
17
+
18
+ ## Step 2: Get API Keys
19
+
20
+ API keys are available in the Recur dashboard at `app.recur.tw` → Settings → Developers.
21
+
22
+ **Key formats:**
23
+ - `pk_test_xxx` - Publishable key (frontend, safe to expose)
24
+ - `sk_test_xxx` - Secret key (backend only, never expose)
25
+ - `pk_live_xxx` / `sk_live_xxx` - Production keys
26
+
27
+ **Environment variables to set:**
28
+ ```bash
29
+ RECUR_PUBLISHABLE_KEY=pk_test_xxx
30
+ RECUR_SECRET_KEY=sk_test_xxx
31
+ ```
32
+
33
+ ## Step 3: Add Provider (React)
34
+
35
+ Wrap your app with `RecurProvider`:
36
+
37
+ ```tsx
38
+ import { RecurProvider } from 'recur-tw'
39
+
40
+ export default function App({ children }) {
41
+ return (
42
+ <RecurProvider
43
+ config={{
44
+ publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY,
45
+ }}
46
+ >
47
+ {children}
48
+ </RecurProvider>
49
+ )
50
+ }
51
+ ```
52
+
53
+ ## Step 4: Create Your First Checkout
54
+
55
+ ```tsx
56
+ import { useRecur } from 'recur-tw'
57
+
58
+ function PricingButton({ productId }: { productId: string }) {
59
+ const { checkout } = useRecur()
60
+
61
+ const handleCheckout = async () => {
62
+ await checkout({
63
+ productId,
64
+ onPaymentComplete: (subscription) => {
65
+ console.log('Payment successful!', subscription)
66
+ },
67
+ onPaymentFailed: (error) => {
68
+ console.error('Payment failed:', error)
69
+ },
70
+ })
71
+ }
72
+
73
+ return <button onClick={handleCheckout}>Subscribe</button>
74
+ }
75
+ ```
76
+
77
+ ## Step 5: Set Up Webhooks
78
+
79
+ Create a webhook endpoint to receive payment notifications. See the `recur-webhooks` skill for detailed instructions.
80
+
81
+ ## Quick Verification Checklist
82
+
83
+ - [ ] SDK installed (`pnpm list recur-tw`)
84
+ - [ ] Environment variables set
85
+ - [ ] RecurProvider wrapping app
86
+ - [ ] Test checkout works in sandbox
87
+ - [ ] Webhook endpoint configured
88
+
89
+ ## Common Issues
90
+
91
+ ### "Invalid API key"
92
+ - Check key format: must start with `pk_test_`, `sk_test_`, `pk_live_`, or `sk_live_`
93
+ - Ensure using publishable key for frontend, secret key for backend
94
+
95
+ ### "Product not found"
96
+ - Verify product exists in Recur dashboard
97
+ - Check you're using correct environment (sandbox vs production)
98
+
99
+ ### Checkout not appearing
100
+ - Ensure `RecurProvider` wraps your app
101
+ - Check browser console for errors
102
+ - Verify publishable key is correct
103
+
104
+ ## Next Steps
105
+
106
+ - `/recur-checkout` - Learn checkout flow options
107
+ - `/recur-webhooks` - Set up payment notifications
108
+ - `/recur-entitlements` - Implement access control
109
+
110
+ ## Resources
111
+
112
+ - [Recur Documentation](https://recur.tw/docs)
113
+ - [SDK on npm](https://www.npmjs.com/package/recur-tw)
114
+ - [API Reference](https://recur.tw/docs/api)
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Check Recur environment setup
3
+
4
+ echo "🔍 Checking Recur environment..."
5
+ echo ""
6
+
7
+ # Check for recur-tw package
8
+ if [ -f "package.json" ]; then
9
+ if grep -q '"recur-tw"' package.json; then
10
+ VERSION=$(grep '"recur-tw"' package.json | head -1 | sed 's/.*: "\(.*\)".*/\1/')
11
+ echo "✅ recur-tw installed: $VERSION"
12
+ else
13
+ echo "❌ recur-tw not found in package.json"
14
+ echo " Run: pnpm add recur-tw"
15
+ fi
16
+ else
17
+ echo "⚠️ No package.json found in current directory"
18
+ fi
19
+
20
+ echo ""
21
+
22
+ # Check environment variables
23
+ check_env() {
24
+ local var_name=$1
25
+ local var_value=$(printenv "$var_name")
26
+
27
+ if [ -n "$var_value" ]; then
28
+ # Mask the value, show first 12 chars
29
+ local masked="${var_value:0:12}..."
30
+ echo "✅ $var_name: $masked"
31
+ else
32
+ echo "❌ $var_name: not set"
33
+ fi
34
+ }
35
+
36
+ echo "Environment Variables:"
37
+ check_env "RECUR_PUBLISHABLE_KEY"
38
+ check_env "NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY"
39
+ check_env "RECUR_SECRET_KEY"
40
+ check_env "RECUR_WEBHOOK_SECRET"
41
+
42
+ echo ""
43
+
44
+ # Check .env files
45
+ echo "Configuration Files:"
46
+ for file in .env .env.local .env.development .env.production; do
47
+ if [ -f "$file" ]; then
48
+ if grep -q "RECUR" "$file"; then
49
+ echo "✅ $file contains RECUR variables"
50
+ else
51
+ echo "⚠️ $file exists but no RECUR variables"
52
+ fi
53
+ fi
54
+ done
55
+
56
+ echo ""
57
+ echo "Done! 🎉"
@@ -0,0 +1,349 @@
1
+ ---
2
+ name: recur-webhooks
3
+ description: 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".
4
+ ---
5
+
6
+ # Recur Webhook Integration
7
+
8
+ You are helping implement Recur webhooks to receive real-time payment and subscription events.
9
+
10
+ ## Webhook Events
11
+
12
+ ### Core Events (Most Common)
13
+
14
+ | Event | When Fired |
15
+ |-------|------------|
16
+ | `checkout.completed` | Payment successful, subscription/order created |
17
+ | `subscription.activated` | Subscription is now active |
18
+ | `subscription.cancelled` | Subscription was cancelled |
19
+ | `subscription.renewed` | Recurring payment successful |
20
+ | `subscription.past_due` | Payment failed, subscription at risk |
21
+ | `order.paid` | One-time purchase completed |
22
+ | `refund.created` | Refund initiated |
23
+
24
+ ### All Supported Events
25
+
26
+ ```typescript
27
+ type WebhookEventType =
28
+ // Checkout
29
+ | 'checkout.created'
30
+ | 'checkout.completed'
31
+ // Orders
32
+ | 'order.paid'
33
+ | 'order.payment_failed'
34
+ // Subscription Lifecycle
35
+ | 'subscription.created'
36
+ | 'subscription.activated'
37
+ | 'subscription.cancelled'
38
+ | 'subscription.expired'
39
+ | 'subscription.trial_ending'
40
+ // Subscription Changes
41
+ | 'subscription.upgraded'
42
+ | 'subscription.downgraded'
43
+ | 'subscription.renewed'
44
+ | 'subscription.past_due'
45
+ // Scheduled Changes
46
+ | 'subscription.schedule_created'
47
+ | 'subscription.schedule_executed'
48
+ | 'subscription.schedule_cancelled'
49
+ // Invoices
50
+ | 'invoice.created'
51
+ | 'invoice.paid'
52
+ | 'invoice.payment_failed'
53
+ // Customer
54
+ | 'customer.created'
55
+ | 'customer.updated'
56
+ // Product
57
+ | 'product.created'
58
+ | 'product.updated'
59
+ // Refunds
60
+ | 'refund.created'
61
+ | 'refund.succeeded'
62
+ | 'refund.failed'
63
+ ```
64
+
65
+ ## Webhook Handler Implementation
66
+
67
+ ### Next.js App Router
68
+
69
+ ```typescript
70
+ // app/api/webhooks/recur/route.ts
71
+ import { NextRequest, NextResponse } from 'next/server'
72
+ import crypto from 'crypto'
73
+
74
+ const WEBHOOK_SECRET = process.env.RECUR_WEBHOOK_SECRET!
75
+
76
+ function verifySignature(payload: string, signature: string): boolean {
77
+ const expected = crypto
78
+ .createHmac('sha256', WEBHOOK_SECRET)
79
+ .update(payload)
80
+ .digest('hex')
81
+ return crypto.timingSafeEqual(
82
+ Buffer.from(signature),
83
+ Buffer.from(expected)
84
+ )
85
+ }
86
+
87
+ export async function POST(request: NextRequest) {
88
+ const payload = await request.text()
89
+ const signature = request.headers.get('x-recur-signature')
90
+
91
+ // Verify signature
92
+ if (!signature || !verifySignature(payload, signature)) {
93
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
94
+ }
95
+
96
+ const event = JSON.parse(payload)
97
+
98
+ // Handle events
99
+ switch (event.type) {
100
+ case 'checkout.completed':
101
+ await handleCheckoutCompleted(event.data)
102
+ break
103
+
104
+ case 'subscription.activated':
105
+ await handleSubscriptionActivated(event.data)
106
+ break
107
+
108
+ case 'subscription.cancelled':
109
+ await handleSubscriptionCancelled(event.data)
110
+ break
111
+
112
+ case 'subscription.renewed':
113
+ await handleSubscriptionRenewed(event.data)
114
+ break
115
+
116
+ case 'subscription.past_due':
117
+ await handleSubscriptionPastDue(event.data)
118
+ break
119
+
120
+ case 'refund.created':
121
+ await handleRefundCreated(event.data)
122
+ break
123
+
124
+ default:
125
+ console.log(`Unhandled event type: ${event.type}`)
126
+ }
127
+
128
+ return NextResponse.json({ received: true })
129
+ }
130
+
131
+ // Event handlers
132
+ async function handleCheckoutCompleted(data: any) {
133
+ const { customerId, subscriptionId, orderId, productId, amount } = data
134
+
135
+ // Update your database
136
+ // Grant access to the user
137
+ // Send confirmation email
138
+ }
139
+
140
+ async function handleSubscriptionActivated(data: any) {
141
+ const { subscriptionId, customerId, productId, status } = data
142
+
143
+ // Update user's subscription status in your database
144
+ // Enable premium features
145
+ }
146
+
147
+ async function handleSubscriptionCancelled(data: any) {
148
+ const { subscriptionId, customerId, cancelledAt, accessUntil } = data
149
+
150
+ // Mark subscription as cancelled
151
+ // User still has access until accessUntil date
152
+ // Send cancellation confirmation email
153
+ }
154
+
155
+ async function handleSubscriptionRenewed(data: any) {
156
+ const { subscriptionId, customerId, amount, nextBillingDate } = data
157
+
158
+ // Update billing records
159
+ // Extend access period
160
+ }
161
+
162
+ async function handleSubscriptionPastDue(data: any) {
163
+ const { subscriptionId, customerId, failureReason } = data
164
+
165
+ // Notify user of payment failure
166
+ // Consider sending dunning emails
167
+ // May want to restrict access after grace period
168
+ }
169
+
170
+ async function handleRefundCreated(data: any) {
171
+ const { refundId, orderId, amount, reason } = data
172
+
173
+ // Update order status
174
+ // Adjust user credits/access
175
+ // Send refund notification
176
+ }
177
+ ```
178
+
179
+ ### Express.js
180
+
181
+ ```typescript
182
+ import express from 'express'
183
+ import crypto from 'crypto'
184
+
185
+ const app = express()
186
+
187
+ // Important: Use raw body for signature verification
188
+ app.post(
189
+ '/api/webhooks/recur',
190
+ express.raw({ type: 'application/json' }),
191
+ (req, res) => {
192
+ const payload = req.body.toString()
193
+ const signature = req.headers['x-recur-signature'] as string
194
+
195
+ // Verify signature
196
+ const expected = crypto
197
+ .createHmac('sha256', process.env.RECUR_WEBHOOK_SECRET!)
198
+ .update(payload)
199
+ .digest('hex')
200
+
201
+ if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
202
+ return res.status(401).json({ error: 'Invalid signature' })
203
+ }
204
+
205
+ const event = JSON.parse(payload)
206
+
207
+ // Handle event...
208
+ console.log('Received event:', event.type)
209
+
210
+ res.json({ received: true })
211
+ }
212
+ )
213
+ ```
214
+
215
+ ## Event Payload Structure
216
+
217
+ ```typescript
218
+ interface WebhookEvent {
219
+ id: string // Event ID (for idempotency)
220
+ type: string // Event type
221
+ timestamp: string // ISO 8601 timestamp
222
+ data: {
223
+ // Varies by event type
224
+ customerId?: string
225
+ customerEmail?: string
226
+ subscriptionId?: string
227
+ orderId?: string
228
+ productId?: string
229
+ amount?: number
230
+ currency?: string
231
+ // ... more fields depending on event
232
+ }
233
+ }
234
+ ```
235
+
236
+ ## Webhook Configuration
237
+
238
+ 1. Go to **Recur Dashboard** → **Settings** → **Webhooks**
239
+ 2. Click **Add Endpoint**
240
+ 3. Enter your endpoint URL (e.g., `https://yourapp.com/api/webhooks/recur`)
241
+ 4. Select events to receive
242
+ 5. Copy the **Webhook Secret** to your environment variables
243
+
244
+ ## Testing Webhooks Locally
245
+
246
+ ### Using ngrok
247
+
248
+ ```bash
249
+ # Start ngrok tunnel
250
+ ngrok http 3000
251
+
252
+ # Use the ngrok URL in Recur dashboard
253
+ # https://xxxx.ngrok.io/api/webhooks/recur
254
+ ```
255
+
256
+ ### Using Recur CLI (if available)
257
+
258
+ ```bash
259
+ # Forward webhooks to local server
260
+ recur webhooks forward --to localhost:3000/api/webhooks/recur
261
+ ```
262
+
263
+ ## Best Practices
264
+
265
+ ### 1. Always Verify Signatures
266
+
267
+ Never trust webhook payloads without verifying the signature.
268
+
269
+ ### 2. Handle Idempotency
270
+
271
+ Webhooks may be delivered multiple times. Use the event `id` to deduplicate:
272
+
273
+ ```typescript
274
+ async function handleEvent(event: WebhookEvent) {
275
+ // Check if already processed
276
+ const existing = await db.webhookEvent.findUnique({
277
+ where: { eventId: event.id }
278
+ })
279
+
280
+ if (existing) {
281
+ console.log('Event already processed:', event.id)
282
+ return
283
+ }
284
+
285
+ // Process event...
286
+
287
+ // Mark as processed
288
+ await db.webhookEvent.create({
289
+ data: { eventId: event.id, processedAt: new Date() }
290
+ })
291
+ }
292
+ ```
293
+
294
+ ### 3. Return 200 Quickly
295
+
296
+ Process events asynchronously to avoid timeouts:
297
+
298
+ ```typescript
299
+ export async function POST(request: NextRequest) {
300
+ // Verify and parse...
301
+
302
+ // Queue for async processing
303
+ await queue.add('process-webhook', event)
304
+
305
+ // Return immediately
306
+ return NextResponse.json({ received: true })
307
+ }
308
+ ```
309
+
310
+ ### 4. Handle Retries Gracefully
311
+
312
+ Recur retries failed webhook deliveries. Ensure your handler is idempotent.
313
+
314
+ ### 5. Log Everything
315
+
316
+ ```typescript
317
+ console.log('Webhook received:', {
318
+ type: event.type,
319
+ id: event.id,
320
+ timestamp: event.timestamp,
321
+ })
322
+ ```
323
+
324
+ ## Debugging Webhooks
325
+
326
+ ### Check Webhook Logs
327
+
328
+ In Recur Dashboard → Webhooks → Click endpoint → View delivery logs
329
+
330
+ ### Common Issues
331
+
332
+ **401 Unauthorized**
333
+ - Check webhook secret is correct
334
+ - Ensure using raw body for signature verification
335
+ - Verify signature algorithm (HMAC SHA-256)
336
+
337
+ **Timeout (no response)**
338
+ - Return 200 before processing
339
+ - Use async processing for heavy operations
340
+
341
+ **Missing events**
342
+ - Check event types are selected in dashboard
343
+ - Verify endpoint URL is correct and accessible
344
+
345
+ ## Related Skills
346
+
347
+ - `/recur-quickstart` - Initial SDK setup
348
+ - `/recur-checkout` - Implement payment flows
349
+ - `/recur-entitlements` - Check subscription access after webhook
@@ -0,0 +1,111 @@
1
+ #!/bin/bash
2
+ # Send a test webhook to your local endpoint
3
+ #
4
+ # Usage:
5
+ # ./test-webhook.sh [endpoint] [event_type]
6
+ #
7
+ # Example:
8
+ # ./test-webhook.sh http://localhost:3000/api/webhooks/recur checkout.completed
9
+
10
+ ENDPOINT="${1:-http://localhost:3000/api/webhooks/recur}"
11
+ EVENT_TYPE="${2:-checkout.completed}"
12
+ SECRET="${RECUR_WEBHOOK_SECRET:-test_secret}"
13
+
14
+ # Generate timestamp
15
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
16
+
17
+ # Create payload based on event type
18
+ case $EVENT_TYPE in
19
+ "checkout.completed")
20
+ PAYLOAD=$(cat <<EOF
21
+ {
22
+ "id": "evt_test_$(date +%s)",
23
+ "type": "checkout.completed",
24
+ "timestamp": "$TIMESTAMP",
25
+ "data": {
26
+ "checkoutId": "chk_test_123",
27
+ "customerId": "cus_test_456",
28
+ "customerEmail": "test@example.com",
29
+ "subscriptionId": "sub_test_789",
30
+ "productId": "prod_test_abc",
31
+ "amount": 29900,
32
+ "currency": "TWD"
33
+ }
34
+ }
35
+ EOF
36
+ )
37
+ ;;
38
+ "subscription.activated")
39
+ PAYLOAD=$(cat <<EOF
40
+ {
41
+ "id": "evt_test_$(date +%s)",
42
+ "type": "subscription.activated",
43
+ "timestamp": "$TIMESTAMP",
44
+ "data": {
45
+ "subscriptionId": "sub_test_789",
46
+ "customerId": "cus_test_456",
47
+ "productId": "prod_test_abc",
48
+ "status": "ACTIVE",
49
+ "currentPeriodStart": "$TIMESTAMP",
50
+ "currentPeriodEnd": "$(date -u -v+1m +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d '+1 month' +"%Y-%m-%dT%H:%M:%SZ")"
51
+ }
52
+ }
53
+ EOF
54
+ )
55
+ ;;
56
+ "subscription.cancelled")
57
+ PAYLOAD=$(cat <<EOF
58
+ {
59
+ "id": "evt_test_$(date +%s)",
60
+ "type": "subscription.cancelled",
61
+ "timestamp": "$TIMESTAMP",
62
+ "data": {
63
+ "subscriptionId": "sub_test_789",
64
+ "customerId": "cus_test_456",
65
+ "cancelledAt": "$TIMESTAMP",
66
+ "accessUntil": "$(date -u -v+1m +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -d '+1 month' +"%Y-%m-%dT%H:%M:%SZ")"
67
+ }
68
+ }
69
+ EOF
70
+ )
71
+ ;;
72
+ *)
73
+ PAYLOAD=$(cat <<EOF
74
+ {
75
+ "id": "evt_test_$(date +%s)",
76
+ "type": "$EVENT_TYPE",
77
+ "timestamp": "$TIMESTAMP",
78
+ "data": {}
79
+ }
80
+ EOF
81
+ )
82
+ ;;
83
+ esac
84
+
85
+ # Calculate signature
86
+ SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
87
+
88
+ echo "📤 Sending test webhook..."
89
+ echo "Endpoint: $ENDPOINT"
90
+ echo "Event: $EVENT_TYPE"
91
+ echo "Signature: ${SIGNATURE:0:20}..."
92
+ echo ""
93
+
94
+ # Send request
95
+ RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
96
+ -H "Content-Type: application/json" \
97
+ -H "x-recur-signature: $SIGNATURE" \
98
+ -d "$PAYLOAD")
99
+
100
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
101
+ BODY=$(echo "$RESPONSE" | sed '$d')
102
+
103
+ echo "Response: $HTTP_CODE"
104
+ echo "$BODY"
105
+ echo ""
106
+
107
+ if [ "$HTTP_CODE" = "200" ]; then
108
+ echo "✅ Webhook delivered successfully!"
109
+ else
110
+ echo "❌ Webhook delivery failed"
111
+ fi
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Verify Recur webhook signature
3
+ *
4
+ * Usage:
5
+ * npx tsx verify-signature.ts <payload> <signature> <secret>
6
+ *
7
+ * Example:
8
+ * npx tsx verify-signature.ts '{"type":"checkout.completed"}' 'abc123...' 'whsec_xxx'
9
+ */
10
+
11
+ import crypto from 'crypto'
12
+
13
+ function verifySignature(payload: string, signature: string, secret: string): boolean {
14
+ const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
15
+
16
+ try {
17
+ return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ function main() {
24
+ const args = process.argv.slice(2)
25
+
26
+ if (args.length < 3) {
27
+ console.log('Usage: npx tsx verify-signature.ts <payload> <signature> <secret>')
28
+ console.log('')
29
+ console.log('Example:')
30
+ console.log(
31
+ " npx tsx verify-signature.ts '{\"type\":\"test\"}' 'abc123' 'whsec_xxx'"
32
+ )
33
+ process.exit(1)
34
+ }
35
+
36
+ const [payload, signature, secret] = args
37
+
38
+ console.log('Payload:', payload.substring(0, 50) + '...')
39
+ console.log('Signature:', signature.substring(0, 20) + '...')
40
+ console.log('')
41
+
42
+ const isValid = verifySignature(payload, signature, secret)
43
+
44
+ if (isValid) {
45
+ console.log('✅ Signature is VALID')
46
+ } else {
47
+ console.log('❌ Signature is INVALID')
48
+
49
+ // Show expected signature for debugging
50
+ const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
51
+ console.log('')
52
+ console.log('Expected:', expected)
53
+ console.log('Received:', signature)
54
+ }
55
+
56
+ process.exit(isValid ? 0 : 1)
57
+ }
58
+
59
+ main()