@girardmedia/bootspring 1.2.0 → 2.0.3
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.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
# Stripe Payment Patterns
|
|
2
|
-
|
|
3
|
-
Battle-tested patterns for Stripe integration in Next.js.
|
|
4
|
-
|
|
5
|
-
## Stripe Client Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/stripe.ts
|
|
9
|
-
import Stripe from 'stripe'
|
|
10
|
-
|
|
11
|
-
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
|
12
|
-
apiVersion: '2024-12-18.acacia',
|
|
13
|
-
typescript: true
|
|
14
|
-
})
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
## One-Time Payment Checkout
|
|
18
|
-
|
|
19
|
-
```typescript
|
|
20
|
-
// app/api/checkout/route.ts
|
|
21
|
-
import { stripe } from '@/lib/stripe'
|
|
22
|
-
import { auth } from '@/lib/auth'
|
|
23
|
-
import { NextResponse } from 'next/server'
|
|
24
|
-
|
|
25
|
-
export async function POST(req: Request) {
|
|
26
|
-
const { userId } = await auth()
|
|
27
|
-
if (!userId) {
|
|
28
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const { priceId, quantity = 1 } = await req.json()
|
|
32
|
-
|
|
33
|
-
const session = await stripe.checkout.sessions.create({
|
|
34
|
-
mode: 'payment',
|
|
35
|
-
payment_method_types: ['card'],
|
|
36
|
-
line_items: [{ price: priceId, quantity }],
|
|
37
|
-
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
38
|
-
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
|
|
39
|
-
metadata: { userId }
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return NextResponse.json({ url: session.url })
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
## Subscription Checkout
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
// app/api/subscribe/route.ts
|
|
50
|
-
import { stripe } from '@/lib/stripe'
|
|
51
|
-
import { auth } from '@/lib/auth'
|
|
52
|
-
import { prisma } from '@/lib/db'
|
|
53
|
-
|
|
54
|
-
export async function POST(req: Request) {
|
|
55
|
-
const { userId } = await auth()
|
|
56
|
-
if (!userId) {
|
|
57
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { priceId } = await req.json()
|
|
61
|
-
|
|
62
|
-
// Get or create Stripe customer
|
|
63
|
-
const user = await prisma.user.findUnique({ where: { id: userId } })
|
|
64
|
-
let customerId = user?.stripeCustomerId
|
|
65
|
-
|
|
66
|
-
if (!customerId) {
|
|
67
|
-
const customer = await stripe.customers.create({
|
|
68
|
-
email: user?.email,
|
|
69
|
-
metadata: { userId }
|
|
70
|
-
})
|
|
71
|
-
customerId = customer.id
|
|
72
|
-
|
|
73
|
-
await prisma.user.update({
|
|
74
|
-
where: { id: userId },
|
|
75
|
-
data: { stripeCustomerId: customerId }
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const session = await stripe.checkout.sessions.create({
|
|
80
|
-
mode: 'subscription',
|
|
81
|
-
customer: customerId,
|
|
82
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
83
|
-
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
|
|
84
|
-
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
|
|
85
|
-
subscription_data: {
|
|
86
|
-
metadata: { userId }
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return NextResponse.json({ url: session.url })
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Webhook Handler
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// app/api/webhooks/stripe/route.ts
|
|
98
|
-
import { stripe } from '@/lib/stripe'
|
|
99
|
-
import { prisma } from '@/lib/db'
|
|
100
|
-
import { headers } from 'next/headers'
|
|
101
|
-
import Stripe from 'stripe'
|
|
102
|
-
|
|
103
|
-
export async function POST(req: Request) {
|
|
104
|
-
const body = await req.text()
|
|
105
|
-
const headersList = await headers()
|
|
106
|
-
const signature = headersList.get('stripe-signature')!
|
|
107
|
-
|
|
108
|
-
let event: Stripe.Event
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
event = stripe.webhooks.constructEvent(
|
|
112
|
-
body,
|
|
113
|
-
signature,
|
|
114
|
-
process.env.STRIPE_WEBHOOK_SECRET!
|
|
115
|
-
)
|
|
116
|
-
} catch (err) {
|
|
117
|
-
console.error('Webhook signature verification failed')
|
|
118
|
-
return new Response('Webhook Error', { status: 400 })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
switch (event.type) {
|
|
122
|
-
case 'checkout.session.completed': {
|
|
123
|
-
const session = event.data.object as Stripe.Checkout.Session
|
|
124
|
-
const userId = session.metadata?.userId
|
|
125
|
-
|
|
126
|
-
if (session.mode === 'subscription') {
|
|
127
|
-
await prisma.user.update({
|
|
128
|
-
where: { id: userId },
|
|
129
|
-
data: {
|
|
130
|
-
subscriptionId: session.subscription as string,
|
|
131
|
-
subscriptionStatus: 'active'
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
} else if (session.mode === 'payment') {
|
|
135
|
-
// Handle one-time payment (e.g., add credits)
|
|
136
|
-
await prisma.user.update({
|
|
137
|
-
where: { id: userId },
|
|
138
|
-
data: { credits: { increment: 100 } }
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
break
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
case 'customer.subscription.updated':
|
|
145
|
-
case 'customer.subscription.deleted': {
|
|
146
|
-
const subscription = event.data.object as Stripe.Subscription
|
|
147
|
-
const userId = subscription.metadata.userId
|
|
148
|
-
|
|
149
|
-
await prisma.user.update({
|
|
150
|
-
where: { id: userId },
|
|
151
|
-
data: {
|
|
152
|
-
subscriptionStatus: subscription.status,
|
|
153
|
-
currentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
break
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
case 'invoice.payment_failed': {
|
|
160
|
-
const invoice = event.data.object as Stripe.Invoice
|
|
161
|
-
// Send email notification, update status, etc.
|
|
162
|
-
break
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return new Response('OK', { status: 200 })
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## Client-Side Checkout Button
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
// components/checkout-button.tsx
|
|
174
|
-
'use client'
|
|
175
|
-
|
|
176
|
-
import { useState } from 'react'
|
|
177
|
-
|
|
178
|
-
export function CheckoutButton({
|
|
179
|
-
priceId,
|
|
180
|
-
mode = 'payment'
|
|
181
|
-
}: {
|
|
182
|
-
priceId: string
|
|
183
|
-
mode?: 'payment' | 'subscription'
|
|
184
|
-
}) {
|
|
185
|
-
const [loading, setLoading] = useState(false)
|
|
186
|
-
|
|
187
|
-
const handleCheckout = async () => {
|
|
188
|
-
setLoading(true)
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const endpoint = mode === 'subscription' ? '/api/subscribe' : '/api/checkout'
|
|
192
|
-
const res = await fetch(endpoint, {
|
|
193
|
-
method: 'POST',
|
|
194
|
-
headers: { 'Content-Type': 'application/json' },
|
|
195
|
-
body: JSON.stringify({ priceId })
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
const { url, error } = await res.json()
|
|
199
|
-
|
|
200
|
-
if (error) {
|
|
201
|
-
alert(error)
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
window.location.href = url
|
|
206
|
-
} catch (error) {
|
|
207
|
-
console.error('Checkout error:', error)
|
|
208
|
-
alert('Something went wrong')
|
|
209
|
-
} finally {
|
|
210
|
-
setLoading(false)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return (
|
|
215
|
-
<button
|
|
216
|
-
onClick={handleCheckout}
|
|
217
|
-
disabled={loading}
|
|
218
|
-
className="px-6 py-3 bg-blue-600 text-white rounded-lg disabled:opacity-50"
|
|
219
|
-
>
|
|
220
|
-
{loading ? 'Loading...' : mode === 'subscription' ? 'Subscribe' : 'Buy Now'}
|
|
221
|
-
</button>
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
## Customer Portal (Manage Subscription)
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// app/api/billing/portal/route.ts
|
|
230
|
-
import { stripe } from '@/lib/stripe'
|
|
231
|
-
import { auth } from '@/lib/auth'
|
|
232
|
-
import { prisma } from '@/lib/db'
|
|
233
|
-
|
|
234
|
-
export async function POST() {
|
|
235
|
-
const { userId } = await auth()
|
|
236
|
-
if (!userId) {
|
|
237
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const user = await prisma.user.findUnique({ where: { id: userId } })
|
|
241
|
-
if (!user?.stripeCustomerId) {
|
|
242
|
-
return NextResponse.json({ error: 'No billing account' }, { status: 400 })
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const session = await stripe.billingPortal.sessions.create({
|
|
246
|
-
customer: user.stripeCustomerId,
|
|
247
|
-
return_url: `${process.env.NEXT_PUBLIC_URL}/settings/billing`
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
return NextResponse.json({ url: session.url })
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Environment Variables
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
# .env.local
|
|
258
|
-
STRIPE_SECRET_KEY=sk_test_...
|
|
259
|
-
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
260
|
-
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
## Testing Webhooks Locally
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
# Install Stripe CLI
|
|
267
|
-
brew install stripe/stripe-cli/stripe
|
|
268
|
-
|
|
269
|
-
# Login and forward webhooks
|
|
270
|
-
stripe login
|
|
271
|
-
stripe listen --forward-to localhost:3000/api/webhooks/stripe
|
|
272
|
-
```
|
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
# Subscription Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for managing subscriptions with Stripe.
|
|
4
|
-
|
|
5
|
-
## Subscription Plans
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/plans.ts
|
|
9
|
-
export const PLANS = {
|
|
10
|
-
free: {
|
|
11
|
-
name: 'Free',
|
|
12
|
-
priceId: null,
|
|
13
|
-
limits: {
|
|
14
|
-
projects: 1,
|
|
15
|
-
storage: 100, // MB
|
|
16
|
-
teamMembers: 1
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
pro: {
|
|
20
|
-
name: 'Pro',
|
|
21
|
-
priceId: process.env.STRIPE_PRO_PRICE_ID!,
|
|
22
|
-
limits: {
|
|
23
|
-
projects: 10,
|
|
24
|
-
storage: 5000,
|
|
25
|
-
teamMembers: 5
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
enterprise: {
|
|
29
|
-
name: 'Enterprise',
|
|
30
|
-
priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID!,
|
|
31
|
-
limits: {
|
|
32
|
-
projects: -1, // unlimited
|
|
33
|
-
storage: -1,
|
|
34
|
-
teamMembers: -1
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
} as const
|
|
38
|
-
|
|
39
|
-
export type PlanId = keyof typeof PLANS
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Create Subscription
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// lib/billing.ts
|
|
46
|
-
import Stripe from 'stripe'
|
|
47
|
-
import { prisma } from '@/lib/db'
|
|
48
|
-
import { PLANS, PlanId } from './plans'
|
|
49
|
-
|
|
50
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
51
|
-
|
|
52
|
-
export async function createSubscription(userId: string, planId: PlanId) {
|
|
53
|
-
const user = await prisma.user.findUnique({
|
|
54
|
-
where: { id: userId },
|
|
55
|
-
include: { subscription: true }
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
if (!user) throw new Error('User not found')
|
|
59
|
-
|
|
60
|
-
const plan = PLANS[planId]
|
|
61
|
-
if (!plan.priceId) throw new Error('Cannot subscribe to free plan')
|
|
62
|
-
|
|
63
|
-
// Get or create Stripe customer
|
|
64
|
-
let customerId = user.stripeCustomerId
|
|
65
|
-
|
|
66
|
-
if (!customerId) {
|
|
67
|
-
const customer = await stripe.customers.create({
|
|
68
|
-
email: user.email,
|
|
69
|
-
metadata: { userId: user.id }
|
|
70
|
-
})
|
|
71
|
-
customerId = customer.id
|
|
72
|
-
|
|
73
|
-
await prisma.user.update({
|
|
74
|
-
where: { id: userId },
|
|
75
|
-
data: { stripeCustomerId: customerId }
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Create checkout session
|
|
80
|
-
const session = await stripe.checkout.sessions.create({
|
|
81
|
-
customer: customerId,
|
|
82
|
-
mode: 'subscription',
|
|
83
|
-
payment_method_types: ['card'],
|
|
84
|
-
line_items: [{ price: plan.priceId, quantity: 1 }],
|
|
85
|
-
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?success=true`,
|
|
86
|
-
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing?canceled=true`,
|
|
87
|
-
metadata: { userId, planId }
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return session.url
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Handle Subscription Webhook
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
// app/api/webhooks/stripe/route.ts
|
|
98
|
-
import { headers } from 'next/headers'
|
|
99
|
-
import Stripe from 'stripe'
|
|
100
|
-
import { prisma } from '@/lib/db'
|
|
101
|
-
|
|
102
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
103
|
-
|
|
104
|
-
export async function POST(request: Request) {
|
|
105
|
-
const body = await request.text()
|
|
106
|
-
const signature = headers().get('stripe-signature')!
|
|
107
|
-
|
|
108
|
-
let event: Stripe.Event
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
event = stripe.webhooks.constructEvent(
|
|
112
|
-
body,
|
|
113
|
-
signature,
|
|
114
|
-
process.env.STRIPE_WEBHOOK_SECRET!
|
|
115
|
-
)
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return Response.json({ error: 'Invalid signature' }, { status: 400 })
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
switch (event.type) {
|
|
121
|
-
case 'customer.subscription.created':
|
|
122
|
-
case 'customer.subscription.updated': {
|
|
123
|
-
const subscription = event.data.object as Stripe.Subscription
|
|
124
|
-
await handleSubscriptionChange(subscription)
|
|
125
|
-
break
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
case 'customer.subscription.deleted': {
|
|
129
|
-
const subscription = event.data.object as Stripe.Subscription
|
|
130
|
-
await handleSubscriptionCanceled(subscription)
|
|
131
|
-
break
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return Response.json({ received: true })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function handleSubscriptionChange(subscription: Stripe.Subscription) {
|
|
139
|
-
const customerId = subscription.customer as string
|
|
140
|
-
|
|
141
|
-
const user = await prisma.user.findFirst({
|
|
142
|
-
where: { stripeCustomerId: customerId }
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
if (!user) return
|
|
146
|
-
|
|
147
|
-
await prisma.subscription.upsert({
|
|
148
|
-
where: { userId: user.id },
|
|
149
|
-
create: {
|
|
150
|
-
userId: user.id,
|
|
151
|
-
stripeSubscriptionId: subscription.id,
|
|
152
|
-
stripePriceId: subscription.items.data[0].price.id,
|
|
153
|
-
status: subscription.status,
|
|
154
|
-
currentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
|
155
|
-
},
|
|
156
|
-
update: {
|
|
157
|
-
stripeSubscriptionId: subscription.id,
|
|
158
|
-
stripePriceId: subscription.items.data[0].price.id,
|
|
159
|
-
status: subscription.status,
|
|
160
|
-
currentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
|
|
166
|
-
const customerId = subscription.customer as string
|
|
167
|
-
|
|
168
|
-
const user = await prisma.user.findFirst({
|
|
169
|
-
where: { stripeCustomerId: customerId }
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
if (!user) return
|
|
173
|
-
|
|
174
|
-
await prisma.subscription.update({
|
|
175
|
-
where: { userId: user.id },
|
|
176
|
-
data: { status: 'canceled' }
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Get User Plan
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
// lib/billing.ts
|
|
185
|
-
export async function getUserPlan(userId: string): Promise<PlanId> {
|
|
186
|
-
const subscription = await prisma.subscription.findUnique({
|
|
187
|
-
where: { userId }
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
if (!subscription || subscription.status !== 'active') {
|
|
191
|
-
return 'free'
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Find plan by price ID
|
|
195
|
-
for (const [planId, plan] of Object.entries(PLANS)) {
|
|
196
|
-
if (plan.priceId === subscription.stripePriceId) {
|
|
197
|
-
return planId as PlanId
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return 'free'
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function checkLimit(
|
|
205
|
-
userId: string,
|
|
206
|
-
resource: 'projects' | 'storage' | 'teamMembers',
|
|
207
|
-
currentCount: number
|
|
208
|
-
): Promise<boolean> {
|
|
209
|
-
const planId = await getUserPlan(userId)
|
|
210
|
-
const limit = PLANS[planId].limits[resource]
|
|
211
|
-
|
|
212
|
-
// -1 means unlimited
|
|
213
|
-
if (limit === -1) return true
|
|
214
|
-
|
|
215
|
-
return currentCount < limit
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## Cancel Subscription
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
// lib/billing.ts
|
|
223
|
-
export async function cancelSubscription(userId: string) {
|
|
224
|
-
const subscription = await prisma.subscription.findUnique({
|
|
225
|
-
where: { userId }
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
if (!subscription?.stripeSubscriptionId) {
|
|
229
|
-
throw new Error('No active subscription')
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Cancel at period end (user keeps access until then)
|
|
233
|
-
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
|
234
|
-
cancel_at_period_end: true
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
await prisma.subscription.update({
|
|
238
|
-
where: { userId },
|
|
239
|
-
data: { cancelAtPeriodEnd: true }
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export async function reactivateSubscription(userId: string) {
|
|
244
|
-
const subscription = await prisma.subscription.findUnique({
|
|
245
|
-
where: { userId }
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
if (!subscription?.stripeSubscriptionId) {
|
|
249
|
-
throw new Error('No subscription found')
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
|
253
|
-
cancel_at_period_end: false
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
await prisma.subscription.update({
|
|
257
|
-
where: { userId },
|
|
258
|
-
data: { cancelAtPeriodEnd: false }
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
## Change Plan
|
|
264
|
-
|
|
265
|
-
```typescript
|
|
266
|
-
// lib/billing.ts
|
|
267
|
-
export async function changePlan(userId: string, newPlanId: PlanId) {
|
|
268
|
-
const subscription = await prisma.subscription.findUnique({
|
|
269
|
-
where: { userId }
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
if (!subscription?.stripeSubscriptionId) {
|
|
273
|
-
throw new Error('No active subscription')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const newPlan = PLANS[newPlanId]
|
|
277
|
-
if (!newPlan.priceId) {
|
|
278
|
-
throw new Error('Cannot switch to free plan, cancel instead')
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const stripeSubscription = await stripe.subscriptions.retrieve(
|
|
282
|
-
subscription.stripeSubscriptionId
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
await stripe.subscriptions.update(subscription.stripeSubscriptionId, {
|
|
286
|
-
items: [{
|
|
287
|
-
id: stripeSubscription.items.data[0].id,
|
|
288
|
-
price: newPlan.priceId
|
|
289
|
-
}],
|
|
290
|
-
proration_behavior: 'create_prorations'
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
## When to Use
|
|
296
|
-
|
|
297
|
-
- SaaS billing
|
|
298
|
-
- Tiered pricing
|
|
299
|
-
- Usage limits
|
|
300
|
-
- Plan management
|