@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,279 +0,0 @@
|
|
|
1
|
-
# Push Notification Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for web push notifications.
|
|
4
|
-
|
|
5
|
-
## Service Worker Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// public/sw.js
|
|
9
|
-
self.addEventListener('push', (event) => {
|
|
10
|
-
const data = event.data?.json() ?? {}
|
|
11
|
-
|
|
12
|
-
const options = {
|
|
13
|
-
body: data.body,
|
|
14
|
-
icon: data.icon || '/icon-192.png',
|
|
15
|
-
badge: '/badge.png',
|
|
16
|
-
data: { url: data.url },
|
|
17
|
-
actions: data.actions || []
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
event.waitUntil(
|
|
21
|
-
self.registration.showNotification(data.title, options)
|
|
22
|
-
)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
self.addEventListener('notificationclick', (event) => {
|
|
26
|
-
event.notification.close()
|
|
27
|
-
|
|
28
|
-
const url = event.notification.data?.url || '/'
|
|
29
|
-
|
|
30
|
-
event.waitUntil(
|
|
31
|
-
clients.matchAll({ type: 'window' }).then((clientList) => {
|
|
32
|
-
for (const client of clientList) {
|
|
33
|
-
if (client.url === url && 'focus' in client) {
|
|
34
|
-
return client.focus()
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return clients.openWindow(url)
|
|
38
|
-
})
|
|
39
|
-
)
|
|
40
|
-
})
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Push Subscription
|
|
44
|
-
|
|
45
|
-
```tsx
|
|
46
|
-
// hooks/usePushNotifications.ts
|
|
47
|
-
'use client'
|
|
48
|
-
|
|
49
|
-
import { useState, useEffect } from 'react'
|
|
50
|
-
|
|
51
|
-
const VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
|
|
52
|
-
|
|
53
|
-
export function usePushNotifications() {
|
|
54
|
-
const [permission, setPermission] = useState<NotificationPermission>('default')
|
|
55
|
-
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if ('Notification' in window) {
|
|
59
|
-
setPermission(Notification.permission)
|
|
60
|
-
}
|
|
61
|
-
}, [])
|
|
62
|
-
|
|
63
|
-
async function subscribe() {
|
|
64
|
-
try {
|
|
65
|
-
// Request permission
|
|
66
|
-
const perm = await Notification.requestPermission()
|
|
67
|
-
setPermission(perm)
|
|
68
|
-
|
|
69
|
-
if (perm !== 'granted') return null
|
|
70
|
-
|
|
71
|
-
// Register service worker
|
|
72
|
-
const registration = await navigator.serviceWorker.register('/sw.js')
|
|
73
|
-
|
|
74
|
-
// Subscribe to push
|
|
75
|
-
const sub = await registration.pushManager.subscribe({
|
|
76
|
-
userVisibleOnly: true,
|
|
77
|
-
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
setSubscription(sub)
|
|
81
|
-
|
|
82
|
-
// Send subscription to server
|
|
83
|
-
await fetch('/api/push/subscribe', {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: { 'Content-Type': 'application/json' },
|
|
86
|
-
body: JSON.stringify(sub.toJSON())
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
return sub
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.error('Push subscription failed:', error)
|
|
92
|
-
return null
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function unsubscribe() {
|
|
97
|
-
if (!subscription) return
|
|
98
|
-
|
|
99
|
-
await subscription.unsubscribe()
|
|
100
|
-
|
|
101
|
-
await fetch('/api/push/unsubscribe', {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
headers: { 'Content-Type': 'application/json' },
|
|
104
|
-
body: JSON.stringify({ endpoint: subscription.endpoint })
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
setSubscription(null)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
permission,
|
|
112
|
-
subscription,
|
|
113
|
-
subscribe,
|
|
114
|
-
unsubscribe,
|
|
115
|
-
isSupported: 'PushManager' in window
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function urlBase64ToUint8Array(base64String: string) {
|
|
120
|
-
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
|
121
|
-
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
|
122
|
-
const rawData = window.atob(base64)
|
|
123
|
-
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)))
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## Server-Side Push
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
// lib/push.ts
|
|
131
|
-
import webpush from 'web-push'
|
|
132
|
-
import { prisma } from '@/lib/db'
|
|
133
|
-
|
|
134
|
-
webpush.setVapidDetails(
|
|
135
|
-
'mailto:support@myapp.com',
|
|
136
|
-
process.env.VAPID_PUBLIC_KEY!,
|
|
137
|
-
process.env.VAPID_PRIVATE_KEY!
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
interface PushPayload {
|
|
141
|
-
title: string
|
|
142
|
-
body: string
|
|
143
|
-
icon?: string
|
|
144
|
-
url?: string
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export async function sendPushNotification(userId: string, payload: PushPayload) {
|
|
148
|
-
const subscriptions = await prisma.pushSubscription.findMany({
|
|
149
|
-
where: { userId }
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const results = await Promise.allSettled(
|
|
153
|
-
subscriptions.map(async (sub) => {
|
|
154
|
-
try {
|
|
155
|
-
await webpush.sendNotification(
|
|
156
|
-
{
|
|
157
|
-
endpoint: sub.endpoint,
|
|
158
|
-
keys: {
|
|
159
|
-
p256dh: sub.p256dh,
|
|
160
|
-
auth: sub.auth
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
JSON.stringify(payload)
|
|
164
|
-
)
|
|
165
|
-
} catch (error: any) {
|
|
166
|
-
// Remove invalid subscriptions
|
|
167
|
-
if (error.statusCode === 410 || error.statusCode === 404) {
|
|
168
|
-
await prisma.pushSubscription.delete({
|
|
169
|
-
where: { id: sub.id }
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
throw error
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
return results
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Usage
|
|
181
|
-
await sendPushNotification(userId, {
|
|
182
|
-
title: 'New Message',
|
|
183
|
-
body: 'You have a new message from John',
|
|
184
|
-
url: '/messages/123'
|
|
185
|
-
})
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Subscribe API
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// app/api/push/subscribe/route.ts
|
|
192
|
-
import { auth } from '@/auth'
|
|
193
|
-
import { prisma } from '@/lib/db'
|
|
194
|
-
|
|
195
|
-
export async function POST(request: Request) {
|
|
196
|
-
const session = await auth()
|
|
197
|
-
if (!session) {
|
|
198
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const subscription = await request.json()
|
|
202
|
-
|
|
203
|
-
await prisma.pushSubscription.upsert({
|
|
204
|
-
where: { endpoint: subscription.endpoint },
|
|
205
|
-
create: {
|
|
206
|
-
userId: session.user.id,
|
|
207
|
-
endpoint: subscription.endpoint,
|
|
208
|
-
p256dh: subscription.keys.p256dh,
|
|
209
|
-
auth: subscription.keys.auth
|
|
210
|
-
},
|
|
211
|
-
update: {
|
|
212
|
-
p256dh: subscription.keys.p256dh,
|
|
213
|
-
auth: subscription.keys.auth
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
return Response.json({ success: true })
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Notification Preferences
|
|
222
|
-
|
|
223
|
-
```tsx
|
|
224
|
-
// components/NotificationSettings.tsx
|
|
225
|
-
'use client'
|
|
226
|
-
|
|
227
|
-
import { usePushNotifications } from '@/hooks/usePushNotifications'
|
|
228
|
-
|
|
229
|
-
export function NotificationSettings() {
|
|
230
|
-
const { permission, subscription, subscribe, unsubscribe, isSupported } = usePushNotifications()
|
|
231
|
-
|
|
232
|
-
if (!isSupported) {
|
|
233
|
-
return <p>Push notifications are not supported in this browser.</p>
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<div className="space-y-4">
|
|
238
|
-
<div className="flex items-center justify-between">
|
|
239
|
-
<div>
|
|
240
|
-
<p className="font-medium">Push Notifications</p>
|
|
241
|
-
<p className="text-sm text-gray-500">
|
|
242
|
-
{subscription ? 'Enabled' : 'Disabled'}
|
|
243
|
-
</p>
|
|
244
|
-
</div>
|
|
245
|
-
|
|
246
|
-
{subscription ? (
|
|
247
|
-
<button
|
|
248
|
-
onClick={unsubscribe}
|
|
249
|
-
className="rounded bg-red-100 px-4 py-2 text-red-700"
|
|
250
|
-
>
|
|
251
|
-
Disable
|
|
252
|
-
</button>
|
|
253
|
-
) : (
|
|
254
|
-
<button
|
|
255
|
-
onClick={subscribe}
|
|
256
|
-
disabled={permission === 'denied'}
|
|
257
|
-
className="rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
|
|
258
|
-
>
|
|
259
|
-
Enable
|
|
260
|
-
</button>
|
|
261
|
-
)}
|
|
262
|
-
</div>
|
|
263
|
-
|
|
264
|
-
{permission === 'denied' && (
|
|
265
|
-
<p className="text-sm text-red-600">
|
|
266
|
-
Notifications are blocked. Please enable them in your browser settings.
|
|
267
|
-
</p>
|
|
268
|
-
)}
|
|
269
|
-
</div>
|
|
270
|
-
)
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
## When to Use
|
|
275
|
-
|
|
276
|
-
- Real-time updates
|
|
277
|
-
- Message notifications
|
|
278
|
-
- Reminders
|
|
279
|
-
- Marketing engagement
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
# Checkout Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for payment checkout flows with Stripe.
|
|
4
|
-
|
|
5
|
-
## Checkout Session
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/checkout.ts
|
|
9
|
-
import Stripe from 'stripe'
|
|
10
|
-
|
|
11
|
-
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
12
|
-
|
|
13
|
-
export async function createCheckoutSession({
|
|
14
|
-
userId,
|
|
15
|
-
priceId,
|
|
16
|
-
mode = 'subscription',
|
|
17
|
-
successUrl,
|
|
18
|
-
cancelUrl
|
|
19
|
-
}: {
|
|
20
|
-
userId: string
|
|
21
|
-
priceId: string
|
|
22
|
-
mode?: 'subscription' | 'payment'
|
|
23
|
-
successUrl?: string
|
|
24
|
-
cancelUrl?: string
|
|
25
|
-
}) {
|
|
26
|
-
const user = await prisma.user.findUnique({
|
|
27
|
-
where: { id: userId }
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
if (!user) throw new Error('User not found')
|
|
31
|
-
|
|
32
|
-
// Get or create customer
|
|
33
|
-
let customerId = user.stripeCustomerId
|
|
34
|
-
|
|
35
|
-
if (!customerId) {
|
|
36
|
-
const customer = await stripe.customers.create({
|
|
37
|
-
email: user.email,
|
|
38
|
-
name: user.name ?? undefined,
|
|
39
|
-
metadata: { userId }
|
|
40
|
-
})
|
|
41
|
-
customerId = customer.id
|
|
42
|
-
|
|
43
|
-
await prisma.user.update({
|
|
44
|
-
where: { id: userId },
|
|
45
|
-
data: { stripeCustomerId: customerId }
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const session = await stripe.checkout.sessions.create({
|
|
50
|
-
customer: customerId,
|
|
51
|
-
mode,
|
|
52
|
-
payment_method_types: ['card'],
|
|
53
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
54
|
-
success_url: successUrl ?? `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
55
|
-
cancel_url: cancelUrl ?? `${process.env.NEXT_PUBLIC_APP_URL}/checkout/canceled`,
|
|
56
|
-
metadata: { userId },
|
|
57
|
-
allow_promotion_codes: true,
|
|
58
|
-
billing_address_collection: 'auto',
|
|
59
|
-
tax_id_collection: { enabled: true }
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
return session
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Checkout API Route
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// app/api/checkout/route.ts
|
|
70
|
-
import { auth } from '@/auth'
|
|
71
|
-
import { createCheckoutSession } from '@/lib/checkout'
|
|
72
|
-
|
|
73
|
-
export async function POST(request: Request) {
|
|
74
|
-
const session = await auth()
|
|
75
|
-
|
|
76
|
-
if (!session?.user) {
|
|
77
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const { priceId, mode } = await request.json()
|
|
81
|
-
|
|
82
|
-
if (!priceId) {
|
|
83
|
-
return Response.json({ error: 'Price ID required' }, { status: 400 })
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const checkoutSession = await createCheckoutSession({
|
|
88
|
-
userId: session.user.id,
|
|
89
|
-
priceId,
|
|
90
|
-
mode
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
return Response.json({ url: checkoutSession.url })
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error('Checkout error:', error)
|
|
96
|
-
return Response.json({ error: 'Failed to create checkout' }, { status: 500 })
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Checkout Button
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
// components/checkout/CheckoutButton.tsx
|
|
105
|
-
'use client'
|
|
106
|
-
|
|
107
|
-
import { useState } from 'react'
|
|
108
|
-
import { Button } from '@/components/ui/Button'
|
|
109
|
-
|
|
110
|
-
interface Props {
|
|
111
|
-
priceId: string
|
|
112
|
-
mode?: 'subscription' | 'payment'
|
|
113
|
-
children: React.ReactNode
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function CheckoutButton({ priceId, mode = 'subscription', children }: Props) {
|
|
117
|
-
const [loading, setLoading] = useState(false)
|
|
118
|
-
|
|
119
|
-
async function handleCheckout() {
|
|
120
|
-
setLoading(true)
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const response = await fetch('/api/checkout', {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: { 'Content-Type': 'application/json' },
|
|
126
|
-
body: JSON.stringify({ priceId, mode })
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const { url, error } = await response.json()
|
|
130
|
-
|
|
131
|
-
if (error) {
|
|
132
|
-
throw new Error(error)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
window.location.href = url
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error('Checkout failed:', error)
|
|
138
|
-
alert('Failed to start checkout')
|
|
139
|
-
} finally {
|
|
140
|
-
setLoading(false)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<Button onClick={handleCheckout} disabled={loading}>
|
|
146
|
-
{loading ? 'Loading...' : children}
|
|
147
|
-
</Button>
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Success Page
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
// app/checkout/success/page.tsx
|
|
156
|
-
import { stripe } from '@/lib/stripe'
|
|
157
|
-
import { redirect } from 'next/navigation'
|
|
158
|
-
|
|
159
|
-
interface Props {
|
|
160
|
-
searchParams: { session_id?: string }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export default async function CheckoutSuccessPage({ searchParams }: Props) {
|
|
164
|
-
const sessionId = searchParams.session_id
|
|
165
|
-
|
|
166
|
-
if (!sessionId) {
|
|
167
|
-
redirect('/')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const session = await stripe.checkout.sessions.retrieve(sessionId, {
|
|
171
|
-
expand: ['subscription', 'customer']
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
if (session.payment_status !== 'paid') {
|
|
175
|
-
redirect('/checkout/canceled')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<div className="mx-auto max-w-md py-12 text-center">
|
|
180
|
-
<div className="mb-4 text-5xl">✓</div>
|
|
181
|
-
<h1 className="mb-2 text-2xl font-bold">Payment Successful</h1>
|
|
182
|
-
<p className="mb-8 text-gray-600">
|
|
183
|
-
Thank you for your purchase. Your subscription is now active.
|
|
184
|
-
</p>
|
|
185
|
-
<a
|
|
186
|
-
href="/dashboard"
|
|
187
|
-
className="rounded-lg bg-black px-6 py-3 text-white hover:bg-gray-800"
|
|
188
|
-
>
|
|
189
|
-
Go to Dashboard
|
|
190
|
-
</a>
|
|
191
|
-
</div>
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## Embedded Checkout
|
|
197
|
-
|
|
198
|
-
```tsx
|
|
199
|
-
// components/checkout/EmbeddedCheckout.tsx
|
|
200
|
-
'use client'
|
|
201
|
-
|
|
202
|
-
import { useState, useEffect } from 'react'
|
|
203
|
-
import { loadStripe } from '@stripe/stripe-js'
|
|
204
|
-
import {
|
|
205
|
-
EmbeddedCheckout,
|
|
206
|
-
EmbeddedCheckoutProvider
|
|
207
|
-
} from '@stripe/react-stripe-js'
|
|
208
|
-
|
|
209
|
-
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!)
|
|
210
|
-
|
|
211
|
-
interface Props {
|
|
212
|
-
priceId: string
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function CheckoutForm({ priceId }: Props) {
|
|
216
|
-
const [clientSecret, setClientSecret] = useState('')
|
|
217
|
-
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
fetch('/api/checkout/embedded', {
|
|
220
|
-
method: 'POST',
|
|
221
|
-
headers: { 'Content-Type': 'application/json' },
|
|
222
|
-
body: JSON.stringify({ priceId })
|
|
223
|
-
})
|
|
224
|
-
.then(res => res.json())
|
|
225
|
-
.then(data => setClientSecret(data.clientSecret))
|
|
226
|
-
}, [priceId])
|
|
227
|
-
|
|
228
|
-
if (!clientSecret) {
|
|
229
|
-
return <div>Loading...</div>
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return (
|
|
233
|
-
<EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
|
|
234
|
-
<EmbeddedCheckout />
|
|
235
|
-
</EmbeddedCheckoutProvider>
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// API route for embedded checkout
|
|
240
|
-
// app/api/checkout/embedded/route.ts
|
|
241
|
-
export async function POST(request: Request) {
|
|
242
|
-
const session = await auth()
|
|
243
|
-
const { priceId } = await request.json()
|
|
244
|
-
|
|
245
|
-
const checkoutSession = await stripe.checkout.sessions.create({
|
|
246
|
-
ui_mode: 'embedded',
|
|
247
|
-
customer: user.stripeCustomerId,
|
|
248
|
-
mode: 'subscription',
|
|
249
|
-
line_items: [{ price: priceId, quantity: 1 }],
|
|
250
|
-
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/return?session_id={CHECKOUT_SESSION_ID}`
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
return Response.json({ clientSecret: checkoutSession.client_secret })
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## One-Time Payment
|
|
258
|
-
|
|
259
|
-
```typescript
|
|
260
|
-
// lib/checkout.ts
|
|
261
|
-
export async function createPaymentSession({
|
|
262
|
-
userId,
|
|
263
|
-
amount,
|
|
264
|
-
description,
|
|
265
|
-
metadata = {}
|
|
266
|
-
}: {
|
|
267
|
-
userId: string
|
|
268
|
-
amount: number
|
|
269
|
-
description: string
|
|
270
|
-
metadata?: Record<string, string>
|
|
271
|
-
}) {
|
|
272
|
-
const user = await prisma.user.findUnique({
|
|
273
|
-
where: { id: userId }
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
const session = await stripe.checkout.sessions.create({
|
|
277
|
-
customer: user?.stripeCustomerId ?? undefined,
|
|
278
|
-
customer_email: !user?.stripeCustomerId ? user?.email : undefined,
|
|
279
|
-
mode: 'payment',
|
|
280
|
-
payment_method_types: ['card'],
|
|
281
|
-
line_items: [{
|
|
282
|
-
price_data: {
|
|
283
|
-
currency: 'usd',
|
|
284
|
-
product_data: { name: description },
|
|
285
|
-
unit_amount: amount
|
|
286
|
-
},
|
|
287
|
-
quantity: 1
|
|
288
|
-
}],
|
|
289
|
-
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/payment/success`,
|
|
290
|
-
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/payment/canceled`,
|
|
291
|
-
metadata: { userId, ...metadata }
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
return session
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
## When to Use
|
|
299
|
-
|
|
300
|
-
- Subscription signup
|
|
301
|
-
- One-time purchases
|
|
302
|
-
- Upgrade flows
|
|
303
|
-
- E-commerce checkout
|