create-solostack 1.2.2 → 1.3.0
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/package.json +1 -1
- package/src/generators/base.js +6 -5
- package/src/generators/pro/admin.js +344 -0
- package/src/generators/pro/api-keys.js +464 -0
- package/src/generators/pro/database-full.js +233 -0
- package/src/generators/pro/emails.js +248 -0
- package/src/generators/pro/oauth.js +217 -0
- package/src/generators/pro/stripe-advanced.js +521 -0
- package/src/generators/setup.js +38 -21
- package/src/index.js +112 -4
- package/src/utils/license.js +83 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/packages.js +14 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Stripe Integration Generator - Pro Feature
|
|
3
|
+
* Generates subscription management, webhooks, and billing page
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateAdvancedStripe(projectPath) {
|
|
10
|
+
// Ensure directories
|
|
11
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/webhook'));
|
|
12
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/create-subscription'));
|
|
13
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/cancel-subscription'));
|
|
14
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/customer-portal'));
|
|
15
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard/billing'));
|
|
16
|
+
|
|
17
|
+
// Generate Stripe utility library
|
|
18
|
+
const stripeLib = `import Stripe from 'stripe';
|
|
19
|
+
|
|
20
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
21
|
+
throw new Error('STRIPE_SECRET_KEY is not set');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
25
|
+
apiVersion: '2024-11-20.acacia',
|
|
26
|
+
typescript: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pricing Plans Configuration
|
|
31
|
+
*/
|
|
32
|
+
export const PLANS = {
|
|
33
|
+
pro: {
|
|
34
|
+
name: 'Pro',
|
|
35
|
+
priceId: process.env.STRIPE_PRO_PRICE_ID || 'price_pro',
|
|
36
|
+
price: 29,
|
|
37
|
+
features: [
|
|
38
|
+
'Unlimited projects',
|
|
39
|
+
'Priority support',
|
|
40
|
+
'API access',
|
|
41
|
+
'Advanced analytics',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
enterprise: {
|
|
45
|
+
name: 'Enterprise',
|
|
46
|
+
priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID || 'price_enterprise',
|
|
47
|
+
price: 99,
|
|
48
|
+
features: [
|
|
49
|
+
'Everything in Pro',
|
|
50
|
+
'Custom integrations',
|
|
51
|
+
'Dedicated account manager',
|
|
52
|
+
'SLA guarantee',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get or create Stripe customer for user
|
|
59
|
+
*/
|
|
60
|
+
export async function getOrCreateCustomer(userId: string, email: string) {
|
|
61
|
+
const { db } = await import('@/lib/db');
|
|
62
|
+
|
|
63
|
+
const user = await db.user.findUnique({
|
|
64
|
+
where: { id: userId },
|
|
65
|
+
select: { stripeCustomerId: true },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (user?.stripeCustomerId) {
|
|
69
|
+
return user.stripeCustomerId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const customer = await stripe.customers.create({
|
|
73
|
+
email,
|
|
74
|
+
metadata: { userId },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await db.user.update({
|
|
78
|
+
where: { id: userId },
|
|
79
|
+
data: { stripeCustomerId: customer.id },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return customer.id;
|
|
83
|
+
}
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
await writeFile(path.join(projectPath, 'src/lib/stripe.ts'), stripeLib);
|
|
87
|
+
|
|
88
|
+
// Generate webhook handler
|
|
89
|
+
const webhookHandler = `import { NextRequest, NextResponse } from 'next/server';
|
|
90
|
+
import { stripe } from '@/lib/stripe';
|
|
91
|
+
import Stripe from 'stripe';
|
|
92
|
+
|
|
93
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
|
94
|
+
|
|
95
|
+
export async function POST(req: NextRequest) {
|
|
96
|
+
const body = await req.text();
|
|
97
|
+
const signature = req.headers.get('stripe-signature')!;
|
|
98
|
+
|
|
99
|
+
let event: Stripe.Event;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
103
|
+
} catch (err: any) {
|
|
104
|
+
console.error('Webhook signature verification failed:', err.message);
|
|
105
|
+
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { db } = await import('@/lib/db');
|
|
109
|
+
|
|
110
|
+
// Check for idempotency
|
|
111
|
+
const existingEvent = await db.stripeEvent.findUnique({
|
|
112
|
+
where: { id: event.id },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (existingEvent?.processed) {
|
|
116
|
+
return NextResponse.json({ received: true, duplicate: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Record the event
|
|
120
|
+
await db.stripeEvent.upsert({
|
|
121
|
+
where: { id: event.id },
|
|
122
|
+
update: {},
|
|
123
|
+
create: { id: event.id, type: event.type },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
case 'customer.subscription.created':
|
|
129
|
+
case 'customer.subscription.updated': {
|
|
130
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
131
|
+
await handleSubscriptionChange(subscription, db);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'customer.subscription.deleted': {
|
|
136
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
137
|
+
await handleSubscriptionDeleted(subscription, db);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'invoice.payment_succeeded': {
|
|
142
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
143
|
+
await handlePaymentSucceeded(invoice, db);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case 'invoice.payment_failed': {
|
|
148
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
149
|
+
await handlePaymentFailed(invoice, db);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Mark event as processed
|
|
155
|
+
await db.stripeEvent.update({
|
|
156
|
+
where: { id: event.id },
|
|
157
|
+
data: { processed: true },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Error processing webhook:', error);
|
|
162
|
+
return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return NextResponse.json({ received: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handleSubscriptionChange(subscription: Stripe.Subscription, db: any) {
|
|
169
|
+
const customerId = subscription.customer as string;
|
|
170
|
+
const user = await db.user.findFirst({
|
|
171
|
+
where: { stripeCustomerId: customerId },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (!user) return;
|
|
175
|
+
|
|
176
|
+
const status = mapSubscriptionStatus(subscription.status);
|
|
177
|
+
|
|
178
|
+
await db.subscription.upsert({
|
|
179
|
+
where: { userId: user.id },
|
|
180
|
+
update: {
|
|
181
|
+
stripeSubscriptionId: subscription.id,
|
|
182
|
+
stripePriceId: subscription.items.data[0].price.id,
|
|
183
|
+
status,
|
|
184
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
185
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
186
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
187
|
+
},
|
|
188
|
+
create: {
|
|
189
|
+
userId: user.id,
|
|
190
|
+
stripeSubscriptionId: subscription.id,
|
|
191
|
+
stripePriceId: subscription.items.data[0].price.id,
|
|
192
|
+
status,
|
|
193
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
194
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
195
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function handleSubscriptionDeleted(subscription: Stripe.Subscription, db: any) {
|
|
201
|
+
await db.subscription.delete({
|
|
202
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
203
|
+
}).catch(() => {}); // Ignore if not found
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function handlePaymentSucceeded(invoice: Stripe.Invoice, db: any) {
|
|
207
|
+
if (!invoice.customer) return;
|
|
208
|
+
|
|
209
|
+
const user = await db.user.findFirst({
|
|
210
|
+
where: { stripeCustomerId: invoice.customer as string },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!user) return;
|
|
214
|
+
|
|
215
|
+
await db.payment.create({
|
|
216
|
+
data: {
|
|
217
|
+
userId: user.id,
|
|
218
|
+
stripePaymentId: invoice.payment_intent as string,
|
|
219
|
+
amount: invoice.amount_paid,
|
|
220
|
+
currency: invoice.currency,
|
|
221
|
+
status: 'succeeded',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handlePaymentFailed(invoice: Stripe.Invoice, db: any) {
|
|
227
|
+
// Send email notification about failed payment
|
|
228
|
+
// await sendPaymentFailedEmail(user.email);
|
|
229
|
+
console.log('Payment failed for invoice:', invoice.id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function mapSubscriptionStatus(status: string) {
|
|
233
|
+
const statusMap: Record<string, string> = {
|
|
234
|
+
active: 'ACTIVE',
|
|
235
|
+
canceled: 'CANCELED',
|
|
236
|
+
past_due: 'PAST_DUE',
|
|
237
|
+
trialing: 'TRIALING',
|
|
238
|
+
incomplete: 'INCOMPLETE',
|
|
239
|
+
};
|
|
240
|
+
return statusMap[status] || 'INCOMPLETE';
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
await writeFile(
|
|
245
|
+
path.join(projectPath, 'src/app/api/stripe/webhook/route.ts'),
|
|
246
|
+
webhookHandler
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Generate create subscription endpoint
|
|
250
|
+
const createSubscription = `import { NextRequest, NextResponse } from 'next/server';
|
|
251
|
+
import { auth } from '@/lib/auth';
|
|
252
|
+
import { stripe, getOrCreateCustomer, PLANS } from '@/lib/stripe';
|
|
253
|
+
|
|
254
|
+
export async function POST(req: NextRequest) {
|
|
255
|
+
try {
|
|
256
|
+
const session = await auth();
|
|
257
|
+
|
|
258
|
+
if (!session?.user?.id) {
|
|
259
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const { priceId } = await req.json();
|
|
263
|
+
|
|
264
|
+
if (!priceId) {
|
|
265
|
+
return NextResponse.json({ error: 'Price ID required' }, { status: 400 });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const customerId = await getOrCreateCustomer(session.user.id, session.user.email!);
|
|
269
|
+
|
|
270
|
+
const checkoutSession = await stripe.checkout.sessions.create({
|
|
271
|
+
customer: customerId,
|
|
272
|
+
mode: 'subscription',
|
|
273
|
+
payment_method_types: ['card'],
|
|
274
|
+
line_items: [
|
|
275
|
+
{
|
|
276
|
+
price: priceId,
|
|
277
|
+
quantity: 1,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
success_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?success=true\`,
|
|
281
|
+
cancel_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?canceled=true\`,
|
|
282
|
+
metadata: {
|
|
283
|
+
userId: session.user.id,
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return NextResponse.json({ url: checkoutSession.url });
|
|
288
|
+
} catch (error: any) {
|
|
289
|
+
console.error('Create subscription error:', error);
|
|
290
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
await writeFile(
|
|
296
|
+
path.join(projectPath, 'src/app/api/stripe/create-subscription/route.ts'),
|
|
297
|
+
createSubscription
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Generate customer portal endpoint
|
|
301
|
+
const customerPortal = `import { NextRequest, NextResponse } from 'next/server';
|
|
302
|
+
import { auth } from '@/lib/auth';
|
|
303
|
+
import { stripe } from '@/lib/stripe';
|
|
304
|
+
import { db } from '@/lib/db';
|
|
305
|
+
|
|
306
|
+
export async function POST(req: NextRequest) {
|
|
307
|
+
try {
|
|
308
|
+
const session = await auth();
|
|
309
|
+
|
|
310
|
+
if (!session?.user?.id) {
|
|
311
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const user = await db.user.findUnique({
|
|
315
|
+
where: { id: session.user.id },
|
|
316
|
+
select: { stripeCustomerId: true },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!user?.stripeCustomerId) {
|
|
320
|
+
return NextResponse.json({ error: 'No billing account found' }, { status: 400 });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const portalSession = await stripe.billingPortal.sessions.create({
|
|
324
|
+
customer: user.stripeCustomerId,
|
|
325
|
+
return_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing\`,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return NextResponse.json({ url: portalSession.url });
|
|
329
|
+
} catch (error: any) {
|
|
330
|
+
console.error('Customer portal error:', error);
|
|
331
|
+
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
`;
|
|
335
|
+
|
|
336
|
+
await writeFile(
|
|
337
|
+
path.join(projectPath, 'src/app/api/stripe/customer-portal/route.ts'),
|
|
338
|
+
customerPortal
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Generate billing page
|
|
342
|
+
const billingPage = `'use client';
|
|
343
|
+
|
|
344
|
+
import { useState, useEffect } from 'react';
|
|
345
|
+
import { useSearchParams } from 'next/navigation';
|
|
346
|
+
import { CreditCard, Check, Loader2, ExternalLink } from 'lucide-react';
|
|
347
|
+
|
|
348
|
+
interface Subscription {
|
|
349
|
+
status: string;
|
|
350
|
+
stripePriceId: string;
|
|
351
|
+
currentPeriodEnd: string;
|
|
352
|
+
cancelAtPeriodEnd: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export default function BillingPage() {
|
|
356
|
+
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
|
357
|
+
const [loading, setLoading] = useState(true);
|
|
358
|
+
const [actionLoading, setActionLoading] = useState(false);
|
|
359
|
+
const searchParams = useSearchParams();
|
|
360
|
+
const success = searchParams.get('success');
|
|
361
|
+
const canceled = searchParams.get('canceled');
|
|
362
|
+
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
fetchSubscription();
|
|
365
|
+
}, []);
|
|
366
|
+
|
|
367
|
+
async function fetchSubscription() {
|
|
368
|
+
try {
|
|
369
|
+
const res = await fetch('/api/user/subscription');
|
|
370
|
+
if (res.ok) {
|
|
371
|
+
const data = await res.json();
|
|
372
|
+
setSubscription(data.subscription);
|
|
373
|
+
}
|
|
374
|
+
} finally {
|
|
375
|
+
setLoading(false);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function handleSubscribe(priceId: string) {
|
|
380
|
+
setActionLoading(true);
|
|
381
|
+
try {
|
|
382
|
+
const res = await fetch('/api/stripe/create-subscription', {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
body: JSON.stringify({ priceId }),
|
|
386
|
+
});
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
if (data.url) {
|
|
389
|
+
window.location.href = data.url;
|
|
390
|
+
}
|
|
391
|
+
} finally {
|
|
392
|
+
setActionLoading(false);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function handleManageBilling() {
|
|
397
|
+
setActionLoading(true);
|
|
398
|
+
try {
|
|
399
|
+
const res = await fetch('/api/stripe/customer-portal', {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
});
|
|
402
|
+
const data = await res.json();
|
|
403
|
+
if (data.url) {
|
|
404
|
+
window.location.href = data.url;
|
|
405
|
+
}
|
|
406
|
+
} finally {
|
|
407
|
+
setActionLoading(false);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (loading) {
|
|
412
|
+
return (
|
|
413
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
414
|
+
<Loader2 className="h-8 w-8 animate-spin text-indigo-600" />
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return (
|
|
420
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
421
|
+
<h1 className="text-3xl font-bold mb-2">Billing</h1>
|
|
422
|
+
<p className="text-gray-600 mb-8">Manage your subscription and billing</p>
|
|
423
|
+
|
|
424
|
+
{success && (
|
|
425
|
+
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
426
|
+
<p className="text-green-800">🎉 Subscription activated successfully!</p>
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{canceled && (
|
|
431
|
+
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
|
432
|
+
<p className="text-yellow-800">Checkout was canceled. Your subscription was not changed.</p>
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
{subscription ? (
|
|
437
|
+
<div className="bg-white rounded-xl border p-6 mb-8">
|
|
438
|
+
<div className="flex items-center justify-between mb-4">
|
|
439
|
+
<div>
|
|
440
|
+
<h2 className="text-xl font-semibold">Current Plan</h2>
|
|
441
|
+
<p className="text-gray-600">
|
|
442
|
+
Status: <span className="font-medium text-green-600">{subscription.status}</span>
|
|
443
|
+
</p>
|
|
444
|
+
</div>
|
|
445
|
+
<CreditCard className="h-8 w-8 text-indigo-600" />
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<p className="text-sm text-gray-500 mb-4">
|
|
449
|
+
Next billing date: {new Date(subscription.currentPeriodEnd).toLocaleDateString()}
|
|
450
|
+
{subscription.cancelAtPeriodEnd && (
|
|
451
|
+
<span className="ml-2 text-amber-600">(Cancels at period end)</span>
|
|
452
|
+
)}
|
|
453
|
+
</p>
|
|
454
|
+
|
|
455
|
+
<button
|
|
456
|
+
onClick={handleManageBilling}
|
|
457
|
+
disabled={actionLoading}
|
|
458
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
|
|
459
|
+
>
|
|
460
|
+
{actionLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
|
|
461
|
+
Manage Billing
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
<div className="grid md:grid-cols-2 gap-6">
|
|
466
|
+
{/* Pro Plan */}
|
|
467
|
+
<div className="bg-white rounded-xl border-2 border-indigo-500 p-6 relative">
|
|
468
|
+
<div className="absolute -top-3 left-4 bg-indigo-500 text-white text-xs px-2 py-1 rounded">
|
|
469
|
+
POPULAR
|
|
470
|
+
</div>
|
|
471
|
+
<h3 className="text-xl font-bold mb-2">Pro</h3>
|
|
472
|
+
<p className="text-3xl font-bold mb-4">$29<span className="text-lg text-gray-500">/mo</span></p>
|
|
473
|
+
<ul className="space-y-2 mb-6">
|
|
474
|
+
{['Unlimited projects', 'Priority support', 'API access', 'Advanced analytics'].map((feature) => (
|
|
475
|
+
<li key={feature} className="flex items-center gap-2 text-sm">
|
|
476
|
+
<Check className="h-4 w-4 text-green-500" />
|
|
477
|
+
{feature}
|
|
478
|
+
</li>
|
|
479
|
+
))}
|
|
480
|
+
</ul>
|
|
481
|
+
<button
|
|
482
|
+
onClick={() => handleSubscribe(process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro')}
|
|
483
|
+
disabled={actionLoading}
|
|
484
|
+
className="w-full py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 disabled:opacity-50"
|
|
485
|
+
>
|
|
486
|
+
{actionLoading ? 'Loading...' : 'Subscribe to Pro'}
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
{/* Enterprise Plan */}
|
|
491
|
+
<div className="bg-white rounded-xl border p-6">
|
|
492
|
+
<h3 className="text-xl font-bold mb-2">Enterprise</h3>
|
|
493
|
+
<p className="text-3xl font-bold mb-4">$99<span className="text-lg text-gray-500">/mo</span></p>
|
|
494
|
+
<ul className="space-y-2 mb-6">
|
|
495
|
+
{['Everything in Pro', 'Custom integrations', 'Dedicated account manager', 'SLA guarantee'].map((feature) => (
|
|
496
|
+
<li key={feature} className="flex items-center gap-2 text-sm">
|
|
497
|
+
<Check className="h-4 w-4 text-green-500" />
|
|
498
|
+
{feature}
|
|
499
|
+
</li>
|
|
500
|
+
))}
|
|
501
|
+
</ul>
|
|
502
|
+
<button
|
|
503
|
+
onClick={() => handleSubscribe(process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PRICE_ID || 'price_enterprise')}
|
|
504
|
+
disabled={actionLoading}
|
|
505
|
+
className="w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
|
|
506
|
+
>
|
|
507
|
+
{actionLoading ? 'Loading...' : 'Subscribe to Enterprise'}
|
|
508
|
+
</button>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
|
|
517
|
+
await writeFile(
|
|
518
|
+
path.join(projectPath, 'src/app/dashboard/billing/page.tsx'),
|
|
519
|
+
billingPage
|
|
520
|
+
);
|
|
521
|
+
}
|
package/src/generators/setup.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
3
|
|
|
4
|
-
export async function generateSetup(projectPath) {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
export async function generateSetup(projectPath, config) {
|
|
5
|
+
const isSupabaseAuth = config?.auth === 'Supabase Auth';
|
|
6
|
+
// Create setup page directory
|
|
7
|
+
await ensureDir(path.join(projectPath, 'src/app/setup'));
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// Generate setup page UI
|
|
10
|
+
const setupPage = `'use client';
|
|
10
11
|
|
|
11
12
|
import { useState, useEffect } from 'react';
|
|
12
|
-
import
|
|
13
|
+
import Link from 'next/link';
|
|
14
|
+
import { CheckCircle, XCircle, Loader2, AlertCircle, RefreshCw, Mail, ArrowLeft } from 'lucide-react';
|
|
13
15
|
|
|
14
16
|
export default function SetupPage() {
|
|
15
17
|
const [data, setData] = useState<any>(null);
|
|
@@ -70,6 +72,15 @@ export default function SetupPage() {
|
|
|
70
72
|
return (
|
|
71
73
|
<div className="min-h-screen bg-gray-50 p-8">
|
|
72
74
|
<div className="mx-auto max-w-4xl space-y-8">
|
|
75
|
+
{/* Back to Home */}
|
|
76
|
+
<Link
|
|
77
|
+
href="/"
|
|
78
|
+
className="inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
|
79
|
+
>
|
|
80
|
+
<ArrowLeft className="h-4 w-4" />
|
|
81
|
+
Back to Home
|
|
82
|
+
</Link>
|
|
83
|
+
|
|
73
84
|
<div className="flex items-center justify-between">
|
|
74
85
|
<div>
|
|
75
86
|
<h1 className="text-3xl font-bold tracking-tight">Setup & Diagnostics</h1>
|
|
@@ -240,14 +251,13 @@ function StatusIcon({ status }: { status: boolean }) {
|
|
|
240
251
|
}
|
|
241
252
|
`;
|
|
242
253
|
|
|
243
|
-
|
|
254
|
+
await writeFile(path.join(projectPath, 'src/app/setup/page.tsx'), setupPage);
|
|
244
255
|
|
|
245
|
-
|
|
246
|
-
|
|
256
|
+
// Create API routes directory
|
|
257
|
+
await ensureDir(path.join(projectPath, 'src/app/api/setup'));
|
|
247
258
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
import { db } from '@/lib/db';
|
|
259
|
+
// Generate Setup API route
|
|
260
|
+
const setupApi = `import { NextResponse } from 'next/server';
|
|
251
261
|
|
|
252
262
|
export async function GET() {
|
|
253
263
|
if (process.env.NODE_ENV !== 'development') {
|
|
@@ -259,12 +269,15 @@ export async function GET() {
|
|
|
259
269
|
connected: false,
|
|
260
270
|
userCount: 0,
|
|
261
271
|
error: null as string | null,
|
|
262
|
-
provider: 'PostgreSQL'
|
|
272
|
+
provider: '${isSupabaseAuth ? 'Supabase' : 'PostgreSQL'}'
|
|
263
273
|
},
|
|
264
274
|
auth: {
|
|
265
275
|
configured: false,
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
type: '${isSupabaseAuth ? 'Supabase Auth' : 'NextAuth.js'}',
|
|
277
|
+
hasSecret: ${isSupabaseAuth ? '!!(process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)' : '!!process.env.NEXTAUTH_SECRET'},
|
|
278
|
+
providers: ${isSupabaseAuth ? `[
|
|
279
|
+
{ name: 'Supabase', configured: !!(process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) }
|
|
280
|
+
]` : `[
|
|
268
281
|
{
|
|
269
282
|
name: 'Google',
|
|
270
283
|
clientId: !!process.env.GOOGLE_CLIENT_ID,
|
|
@@ -277,7 +290,7 @@ export async function GET() {
|
|
|
277
290
|
clientSecret: !!process.env.GITHUB_CLIENT_SECRET,
|
|
278
291
|
configured: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
|
|
279
292
|
}
|
|
280
|
-
]
|
|
293
|
+
]`}
|
|
281
294
|
},
|
|
282
295
|
stripe: {
|
|
283
296
|
configured: false,
|
|
@@ -293,6 +306,10 @@ export async function GET() {
|
|
|
293
306
|
|
|
294
307
|
// Check Database
|
|
295
308
|
try {
|
|
309
|
+
if (!process.env.DATABASE_URL${isSupabaseAuth ? ' && !process.env.NEXT_PUBLIC_SUPABASE_URL' : ''}) {
|
|
310
|
+
throw new Error('DATABASE_URL is not configured in .env');
|
|
311
|
+
}
|
|
312
|
+
const { db } = await import('@/lib/db');
|
|
296
313
|
const userCount = await db.user.count();
|
|
297
314
|
results.database.connected = true;
|
|
298
315
|
results.database.userCount = userCount;
|
|
@@ -315,12 +332,12 @@ export async function GET() {
|
|
|
315
332
|
}
|
|
316
333
|
`;
|
|
317
334
|
|
|
318
|
-
|
|
335
|
+
await writeFile(path.join(projectPath, 'src/app/api/setup/route.ts'), setupApi);
|
|
319
336
|
|
|
320
|
-
|
|
321
|
-
|
|
337
|
+
// Generate Test Email API route
|
|
338
|
+
await ensureDir(path.join(projectPath, 'src/app/api/setup/test-email'));
|
|
322
339
|
|
|
323
|
-
|
|
340
|
+
const testEmailApi = `import { NextResponse, NextRequest } from 'next/server';
|
|
324
341
|
import { Resend } from 'resend';
|
|
325
342
|
|
|
326
343
|
export async function POST(req: NextRequest) {
|
|
@@ -355,5 +372,5 @@ export async function POST(req: NextRequest) {
|
|
|
355
372
|
}
|
|
356
373
|
`;
|
|
357
374
|
|
|
358
|
-
|
|
375
|
+
await writeFile(path.join(projectPath, 'src/app/api/setup/test-email/route.ts'), testEmailApi);
|
|
359
376
|
}
|