create-solostack 1.2.3 ā 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/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/index.js +111 -3
- 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/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import chalk from 'chalk';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { validateProjectName } from './utils/validate.js';
|
|
7
|
-
import { printSuccess, printError } from './utils/logger.js';
|
|
7
|
+
import { printSuccess, printError, printProSuccess } from './utils/logger.js';
|
|
8
8
|
import { ensureDir, writeFile } from './utils/files.js';
|
|
9
|
-
import { installPackages } from './utils/packages.js';
|
|
9
|
+
import { installPackages, installProPackages } from './utils/packages.js';
|
|
10
10
|
import { initGit } from './utils/git.js';
|
|
11
11
|
import { generateBase } from './generators/base.js';
|
|
12
12
|
import { generateDatabase } from './generators/database.js';
|
|
@@ -15,6 +15,14 @@ import { generatePayments } from './generators/payments.js';
|
|
|
15
15
|
import { generateEmails } from './generators/emails.js';
|
|
16
16
|
import { generateSetup } from './generators/setup.js';
|
|
17
17
|
import { generateUI } from './generators/ui.js';
|
|
18
|
+
import { validateLicense, isLicenseKeyFormat } from './utils/license.js';
|
|
19
|
+
// Pro generators
|
|
20
|
+
import { generateOAuth } from './generators/pro/oauth.js';
|
|
21
|
+
import { generateFullDatabase } from './generators/pro/database-full.js';
|
|
22
|
+
import { generateAdvancedStripe } from './generators/pro/stripe-advanced.js';
|
|
23
|
+
import { generateAdvancedEmails } from './generators/pro/emails.js';
|
|
24
|
+
import { generateAdmin } from './generators/pro/admin.js';
|
|
25
|
+
import { generateApiKeys } from './generators/pro/api-keys.js';
|
|
18
26
|
import {
|
|
19
27
|
AUTH_PROVIDERS,
|
|
20
28
|
DATABASES,
|
|
@@ -112,6 +120,68 @@ export async function main() {
|
|
|
112
120
|
|
|
113
121
|
const projectPath = path.join(process.cwd(), projectName);
|
|
114
122
|
|
|
123
|
+
// Pro upgrade prompt
|
|
124
|
+
let hasProLicense = false;
|
|
125
|
+
let licenseData = null;
|
|
126
|
+
|
|
127
|
+
const proAnswer = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
type: 'confirm',
|
|
130
|
+
name: 'wantsPro',
|
|
131
|
+
message: chalk.cyan('š Upgrade to Pro? (Admin panel, OAuth, API keys, advanced features)'),
|
|
132
|
+
default: false,
|
|
133
|
+
}
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
if (proAnswer.wantsPro) {
|
|
137
|
+
let retryLicense = true;
|
|
138
|
+
|
|
139
|
+
while (retryLicense && !hasProLicense) {
|
|
140
|
+
const licenseAnswer = await inquirer.prompt([
|
|
141
|
+
{
|
|
142
|
+
type: 'input',
|
|
143
|
+
name: 'licenseKey',
|
|
144
|
+
message: 'Enter your Pro license key (get one at https://solostack.dev/pro):',
|
|
145
|
+
validate: (input) => {
|
|
146
|
+
if (!input) {
|
|
147
|
+
return 'License key required. Visit https://solostack.dev/pro to purchase.';
|
|
148
|
+
}
|
|
149
|
+
if (!isLicenseKeyFormat(input)) {
|
|
150
|
+
return 'Invalid license key format. Expected: sk_live_XXXXX...';
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
let spinner = ora('Validating Pro license...').start();
|
|
158
|
+
licenseData = await validateLicense(licenseAnswer.licenseKey);
|
|
159
|
+
|
|
160
|
+
if (licenseData.valid) {
|
|
161
|
+
hasProLicense = true;
|
|
162
|
+
spinner.succeed(chalk.green(`Pro license validated! Welcome ${licenseData.email} š`));
|
|
163
|
+
} else {
|
|
164
|
+
spinner.fail(chalk.red('Invalid license key'));
|
|
165
|
+
|
|
166
|
+
const retryAnswer = await inquirer.prompt([
|
|
167
|
+
{
|
|
168
|
+
type: 'confirm',
|
|
169
|
+
name: 'retry',
|
|
170
|
+
message: 'Would you like to try a different license key?',
|
|
171
|
+
default: true,
|
|
172
|
+
}
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
retryLicense = retryAnswer.retry;
|
|
176
|
+
|
|
177
|
+
if (!retryLicense) {
|
|
178
|
+
console.log(chalk.yellow('\nš” Generating free version instead.'));
|
|
179
|
+
console.log(chalk.yellow(' Get Pro at: https://solostack.dev/pro\n'));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
115
185
|
try {
|
|
116
186
|
// Start generation
|
|
117
187
|
console.log(chalk.cyan('\\nāļø Creating your SaaS boilerplate...\\n'));
|
|
@@ -156,6 +226,40 @@ export async function main() {
|
|
|
156
226
|
await generateSetup(projectPath, config);
|
|
157
227
|
spinner.succeed('Added diagnostics page (/setup)');
|
|
158
228
|
|
|
229
|
+
// Pro Features Generation
|
|
230
|
+
if (hasProLicense) {
|
|
231
|
+
console.log(chalk.cyan('\nš Adding Pro features...\n'));
|
|
232
|
+
|
|
233
|
+
spinner = ora('Adding OAuth providers (Google, GitHub)').start();
|
|
234
|
+
await generateOAuth(projectPath);
|
|
235
|
+
spinner.succeed('OAuth providers configured');
|
|
236
|
+
|
|
237
|
+
spinner = ora('Upgrading database schema').start();
|
|
238
|
+
await generateFullDatabase(projectPath);
|
|
239
|
+
spinner.succeed('Full database schema created');
|
|
240
|
+
|
|
241
|
+
spinner = ora('Setting up advanced Stripe integration').start();
|
|
242
|
+
await generateAdvancedStripe(projectPath);
|
|
243
|
+
spinner.succeed('Stripe subscriptions and webhooks configured');
|
|
244
|
+
|
|
245
|
+
spinner = ora('Adding email templates').start();
|
|
246
|
+
await generateAdvancedEmails(projectPath);
|
|
247
|
+
spinner.succeed('Email templates created');
|
|
248
|
+
|
|
249
|
+
spinner = ora('Creating admin panel').start();
|
|
250
|
+
await generateAdmin(projectPath);
|
|
251
|
+
spinner.succeed('Admin panel created');
|
|
252
|
+
|
|
253
|
+
spinner = ora('Setting up API key system').start();
|
|
254
|
+
await generateApiKeys(projectPath);
|
|
255
|
+
spinner.succeed('API key system configured');
|
|
256
|
+
|
|
257
|
+
// Install Pro-specific packages
|
|
258
|
+
spinner = ora('Installing Pro packages...').start();
|
|
259
|
+
await installProPackages(projectPath);
|
|
260
|
+
spinner.succeed('Pro packages installed');
|
|
261
|
+
}
|
|
262
|
+
|
|
159
263
|
// Install dependencies
|
|
160
264
|
spinner = ora('Installing dependencies (this may take a minute...)').start();
|
|
161
265
|
await installPackages(projectPath);
|
|
@@ -169,7 +273,11 @@ export async function main() {
|
|
|
169
273
|
}
|
|
170
274
|
|
|
171
275
|
// Success message
|
|
172
|
-
|
|
276
|
+
if (hasProLicense) {
|
|
277
|
+
printProSuccess(projectName, projectPath);
|
|
278
|
+
} else {
|
|
279
|
+
printSuccess(projectName, projectPath);
|
|
280
|
+
}
|
|
173
281
|
|
|
174
282
|
// Optional setup wizard
|
|
175
283
|
console.log(); // Empty line for spacing
|