create-solostack 1.2.3 ā 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +95 -3
- package/src/utils/license.js +83 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/packages.js +14 -0
- package/src/utils/pro-api.js +71 -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,8 @@ 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
|
+
import { generateProFromServer } from './utils/pro-api.js';
|
|
18
20
|
import {
|
|
19
21
|
AUTH_PROVIDERS,
|
|
20
22
|
DATABASES,
|
|
@@ -112,6 +114,69 @@ export async function main() {
|
|
|
112
114
|
|
|
113
115
|
const projectPath = path.join(process.cwd(), projectName);
|
|
114
116
|
|
|
117
|
+
// Pro upgrade prompt
|
|
118
|
+
let hasProLicense = false;
|
|
119
|
+
let licenseData = null;
|
|
120
|
+
|
|
121
|
+
const proAnswer = await inquirer.prompt([
|
|
122
|
+
{
|
|
123
|
+
type: 'confirm',
|
|
124
|
+
name: 'wantsPro',
|
|
125
|
+
message: chalk.cyan('š Upgrade to Pro? (Admin panel, OAuth, API keys, advanced features)'),
|
|
126
|
+
default: false,
|
|
127
|
+
}
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (proAnswer.wantsPro) {
|
|
131
|
+
let retryLicense = true;
|
|
132
|
+
|
|
133
|
+
while (retryLicense && !hasProLicense) {
|
|
134
|
+
const licenseAnswer = await inquirer.prompt([
|
|
135
|
+
{
|
|
136
|
+
type: 'input',
|
|
137
|
+
name: 'licenseKey',
|
|
138
|
+
message: 'Enter your Pro license key (get one at https://solostack.dev/pro):',
|
|
139
|
+
validate: (input) => {
|
|
140
|
+
if (!input) {
|
|
141
|
+
return 'License key required. Visit https://solostack.dev/pro to purchase.';
|
|
142
|
+
}
|
|
143
|
+
if (!isLicenseKeyFormat(input)) {
|
|
144
|
+
return 'Invalid license key format. Expected: sk_live_XXXXX...';
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
let spinner = ora('Validating Pro license...').start();
|
|
152
|
+
licenseData = await validateLicense(licenseAnswer.licenseKey);
|
|
153
|
+
|
|
154
|
+
if (licenseData.valid) {
|
|
155
|
+
hasProLicense = true;
|
|
156
|
+
licenseData.licenseKey = licenseAnswer.licenseKey; // Store key for server-side generation
|
|
157
|
+
spinner.succeed(chalk.green(`Pro license validated! Welcome ${licenseData.email} š`));
|
|
158
|
+
} else {
|
|
159
|
+
spinner.fail(chalk.red('Invalid license key'));
|
|
160
|
+
|
|
161
|
+
const retryAnswer = await inquirer.prompt([
|
|
162
|
+
{
|
|
163
|
+
type: 'confirm',
|
|
164
|
+
name: 'retry',
|
|
165
|
+
message: 'Would you like to try a different license key?',
|
|
166
|
+
default: true,
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
retryLicense = retryAnswer.retry;
|
|
171
|
+
|
|
172
|
+
if (!retryLicense) {
|
|
173
|
+
console.log(chalk.yellow('\nš” Generating free version instead.'));
|
|
174
|
+
console.log(chalk.yellow(' Get Pro at: https://solostack.dev/pro\n'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
115
180
|
try {
|
|
116
181
|
// Start generation
|
|
117
182
|
console.log(chalk.cyan('\\nāļø Creating your SaaS boilerplate...\\n'));
|
|
@@ -156,6 +221,29 @@ export async function main() {
|
|
|
156
221
|
await generateSetup(projectPath, config);
|
|
157
222
|
spinner.succeed('Added diagnostics page (/setup)');
|
|
158
223
|
|
|
224
|
+
// Pro Features Generation (Server-Side)
|
|
225
|
+
if (hasProLicense) {
|
|
226
|
+
console.log(chalk.cyan('\nš Fetching Pro features from server...\n'));
|
|
227
|
+
|
|
228
|
+
spinner = ora('Downloading Pro features (OAuth, Admin, API Keys, Billing...)').start();
|
|
229
|
+
try {
|
|
230
|
+
const fileCount = await generateProFromServer(projectPath, licenseData.licenseKey, config);
|
|
231
|
+
spinner.succeed(`Pro features downloaded (${fileCount} files)`);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
spinner.fail('Failed to download Pro features');
|
|
234
|
+
console.log(chalk.yellow('\nā ļø Error: ' + error.message));
|
|
235
|
+
console.log(chalk.yellow(' Continuing with free version...\n'));
|
|
236
|
+
hasProLicense = false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (hasProLicense) {
|
|
240
|
+
// Install Pro-specific packages
|
|
241
|
+
spinner = ora('Installing Pro packages...').start();
|
|
242
|
+
await installProPackages(projectPath);
|
|
243
|
+
spinner.succeed('Pro packages installed');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
159
247
|
// Install dependencies
|
|
160
248
|
spinner = ora('Installing dependencies (this may take a minute...)').start();
|
|
161
249
|
await installPackages(projectPath);
|
|
@@ -169,7 +257,11 @@ export async function main() {
|
|
|
169
257
|
}
|
|
170
258
|
|
|
171
259
|
// Success message
|
|
172
|
-
|
|
260
|
+
if (hasProLicense) {
|
|
261
|
+
printProSuccess(projectName, projectPath);
|
|
262
|
+
} else {
|
|
263
|
+
printSuccess(projectName, projectPath);
|
|
264
|
+
}
|
|
173
265
|
|
|
174
266
|
// Optional setup wizard
|
|
175
267
|
console.log(); // Empty line for spacing
|