create-solostack 1.0.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/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/cli.js +13 -0
- package/package.json +45 -0
- package/src/constants.js +94 -0
- package/src/generators/auth.js +595 -0
- package/src/generators/base.js +592 -0
- package/src/generators/database.js +365 -0
- package/src/generators/emails.js +404 -0
- package/src/generators/payments.js +541 -0
- package/src/generators/ui.js +368 -0
- package/src/index.js +374 -0
- package/src/utils/files.js +81 -0
- package/src/utils/git.js +69 -0
- package/src/utils/logger.js +62 -0
- package/src/utils/packages.js +75 -0
- package/src/utils/validate.js +17 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { writeFile, ensureDir } from '../utils/files.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates Stripe payment integration
|
|
6
|
+
* @param {string} projectPath - Path where the project is located
|
|
7
|
+
* @param {string} paymentProvider - Payment provider type
|
|
8
|
+
*/
|
|
9
|
+
export async function generatePayments(projectPath, paymentProvider) {
|
|
10
|
+
// Create Stripe lib file
|
|
11
|
+
const stripeLib = `import Stripe from 'stripe';
|
|
12
|
+
|
|
13
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
14
|
+
throw new Error('STRIPE_SECRET_KEY is not set');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
18
|
+
apiVersion: '2024-12-18.acacia',
|
|
19
|
+
typescript: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const PLANS = {
|
|
23
|
+
FREE: {
|
|
24
|
+
name: 'Free',
|
|
25
|
+
price: 0,
|
|
26
|
+
priceId: null,
|
|
27
|
+
features: [
|
|
28
|
+
'Basic features',
|
|
29
|
+
'Community support',
|
|
30
|
+
'Limited usage',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
PRO: {
|
|
34
|
+
name: 'Pro',
|
|
35
|
+
price: 9,
|
|
36
|
+
priceId: process.env.STRIPE_PRO_PRICE_ID,
|
|
37
|
+
features: [
|
|
38
|
+
'All free features',
|
|
39
|
+
'Priority support',
|
|
40
|
+
'Unlimited usage',
|
|
41
|
+
'Advanced analytics',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
ENTERPRISE: {
|
|
45
|
+
name: 'Enterprise',
|
|
46
|
+
price: 29,
|
|
47
|
+
priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID,
|
|
48
|
+
features: [
|
|
49
|
+
'All pro features',
|
|
50
|
+
'Dedicated support',
|
|
51
|
+
'Custom integrations',
|
|
52
|
+
'SLA guarantee',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
await writeFile(path.join(projectPath, 'src/lib/stripe.ts'), stripeLib);
|
|
59
|
+
|
|
60
|
+
// Create webhook handler
|
|
61
|
+
await ensureDir(path.join(projectPath, 'src/app/api/webhooks/stripe'));
|
|
62
|
+
|
|
63
|
+
const webhookHandler = `import { headers } from 'next/headers';
|
|
64
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
65
|
+
import Stripe from 'stripe';
|
|
66
|
+
import { stripe } from '@/lib/stripe';
|
|
67
|
+
import { db } from '@/lib/db';
|
|
68
|
+
|
|
69
|
+
export async function POST(req: NextRequest) {
|
|
70
|
+
const body = await req.text();
|
|
71
|
+
const signature = (await headers()).get('stripe-signature');
|
|
72
|
+
|
|
73
|
+
if (!signature) {
|
|
74
|
+
return NextResponse.json(
|
|
75
|
+
{ error: 'No signature' },
|
|
76
|
+
{ status: 400 }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let event: Stripe.Event;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
event = stripe.webhooks.constructEvent(
|
|
84
|
+
body,
|
|
85
|
+
signature,
|
|
86
|
+
process.env.STRIPE_WEBHOOK_SECRET!
|
|
87
|
+
);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Webhook signature verification failed:', error);
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Invalid signature' },
|
|
92
|
+
{ status: 400 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Check if we've already processed this event (idempotency)
|
|
98
|
+
const existingEvent = await db.stripeEvent.findUnique({
|
|
99
|
+
where: { eventId: event.id },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (existingEvent?.processed) {
|
|
103
|
+
console.log(\`Event \${event.id} already processed, skipping\`);
|
|
104
|
+
return NextResponse.json({ received: true, skipped: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create or update event record
|
|
108
|
+
await db.stripeEvent.upsert({
|
|
109
|
+
where: { eventId: event.id },
|
|
110
|
+
create: {
|
|
111
|
+
eventId: event.id,
|
|
112
|
+
type: event.type,
|
|
113
|
+
processed: false,
|
|
114
|
+
},
|
|
115
|
+
update: {
|
|
116
|
+
type: event.type,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
switch (event.type) {
|
|
122
|
+
case 'checkout.session.completed': {
|
|
123
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
124
|
+
|
|
125
|
+
if (session.mode === 'subscription') {
|
|
126
|
+
const subscriptionId = session.subscription as string;
|
|
127
|
+
const userId = session.metadata?.userId;
|
|
128
|
+
|
|
129
|
+
if (!userId) {
|
|
130
|
+
throw new Error('No userId in metadata');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
134
|
+
|
|
135
|
+
await db.subscription.create({
|
|
136
|
+
data: {
|
|
137
|
+
userId,
|
|
138
|
+
stripeSubscriptionId: subscription.id,
|
|
139
|
+
stripePriceId: subscription.items.data[0].price.id,
|
|
140
|
+
status: subscription.status.toUpperCase() as any,
|
|
141
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
142
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await db.user.update({
|
|
147
|
+
where: { id: userId },
|
|
148
|
+
data: { stripeCustomerId: subscription.customer as string },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
case 'customer.subscription.updated': {
|
|
155
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
156
|
+
|
|
157
|
+
await db.subscription.update({
|
|
158
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
159
|
+
data: {
|
|
160
|
+
status: subscription.status.toUpperCase() as any,
|
|
161
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
162
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
163
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'customer.subscription.deleted': {
|
|
170
|
+
const subscription = event.data.object as Stripe.Subscription;
|
|
171
|
+
|
|
172
|
+
await db.subscription.update({
|
|
173
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
174
|
+
data: {
|
|
175
|
+
status: 'CANCELED',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'payment_intent.succeeded': {
|
|
182
|
+
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
|
183
|
+
const userId = paymentIntent.metadata?.userId;
|
|
184
|
+
|
|
185
|
+
if (userId) {
|
|
186
|
+
await db.payment.create({
|
|
187
|
+
data: {
|
|
188
|
+
userId,
|
|
189
|
+
stripePaymentId: paymentIntent.id,
|
|
190
|
+
amount: paymentIntent.amount,
|
|
191
|
+
currency: paymentIntent.currency,
|
|
192
|
+
status: paymentIntent.status,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'payment_intent.payment_failed': {
|
|
200
|
+
const paymentIntent = event.data.object as Stripe.PaymentIntent;
|
|
201
|
+
console.error('Payment failed:', paymentIntent.id);
|
|
202
|
+
// TODO: Send email notification
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Mark event as processed
|
|
208
|
+
await db.stripeEvent.update({
|
|
209
|
+
where: { eventId: event.id },
|
|
210
|
+
data: { processed: true },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return NextResponse.json({ received: true, processed: true });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Webhook handler error:', error);
|
|
216
|
+
|
|
217
|
+
// Don't mark as processed on error, allow retry
|
|
218
|
+
try {
|
|
219
|
+
await db.stripeEvent.update({
|
|
220
|
+
where: { eventId: event.id },
|
|
221
|
+
data: { processed: false },
|
|
222
|
+
});
|
|
223
|
+
} catch (dbError) {
|
|
224
|
+
console.error('Failed to update event status:', dbError);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return NextResponse.json(
|
|
228
|
+
{ error: 'Webhook handler failed', message: error instanceof Error ? error.message : 'Unknown error' },
|
|
229
|
+
{ status: 500 }
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
await writeFile(
|
|
236
|
+
path.join(projectPath, 'src/app/api/webhooks/stripe/route.ts'),
|
|
237
|
+
webhookHandler
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Create checkout API route
|
|
241
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/checkout'));
|
|
242
|
+
|
|
243
|
+
const checkoutApi = `import { NextRequest, NextResponse } from 'next/server';
|
|
244
|
+
import { auth } from '@/lib/auth';
|
|
245
|
+
import { stripe } from '@/lib/stripe';
|
|
246
|
+
|
|
247
|
+
export async function POST(req: NextRequest) {
|
|
248
|
+
try {
|
|
249
|
+
const session = await auth();
|
|
250
|
+
|
|
251
|
+
if (!session?.user) {
|
|
252
|
+
return NextResponse.json(
|
|
253
|
+
{ error: 'Unauthorized' },
|
|
254
|
+
{ status: 401 }
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { priceId } = await req.json();
|
|
259
|
+
|
|
260
|
+
if (!priceId) {
|
|
261
|
+
return NextResponse.json(
|
|
262
|
+
{ error: 'Price ID is required' },
|
|
263
|
+
{ status: 400 }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const checkoutSession = await stripe.checkout.sessions.create({
|
|
268
|
+
mode: 'subscription',
|
|
269
|
+
payment_method_types: ['card'],
|
|
270
|
+
line_items: [
|
|
271
|
+
{
|
|
272
|
+
price: priceId,
|
|
273
|
+
quantity: 1,
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
success_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?success=true\`,
|
|
277
|
+
cancel_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?canceled=true\`,
|
|
278
|
+
metadata: {
|
|
279
|
+
userId: session.user.id,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return NextResponse.json({ url: checkoutSession.url });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Checkout error:', error);
|
|
286
|
+
return NextResponse.json(
|
|
287
|
+
{ error: 'Failed to create checkout session' },
|
|
288
|
+
{ status: 500 }
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
await writeFile(
|
|
295
|
+
path.join(projectPath, 'src/app/api/stripe/checkout/route.ts'),
|
|
296
|
+
checkoutApi
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Create customer portal API route
|
|
300
|
+
await ensureDir(path.join(projectPath, 'src/app/api/stripe/portal'));
|
|
301
|
+
|
|
302
|
+
const portalApi = `import { NextRequest, NextResponse } from 'next/server';
|
|
303
|
+
import { auth } from '@/lib/auth';
|
|
304
|
+
import { stripe } from '@/lib/stripe';
|
|
305
|
+
import { db } from '@/lib/db';
|
|
306
|
+
|
|
307
|
+
export async function POST(req: NextRequest) {
|
|
308
|
+
try {
|
|
309
|
+
const session = await auth();
|
|
310
|
+
|
|
311
|
+
if (!session?.user) {
|
|
312
|
+
return NextResponse.json(
|
|
313
|
+
{ error: 'Unauthorized' },
|
|
314
|
+
{ status: 401 }
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const user = await db.user.findUnique({
|
|
319
|
+
where: { id: session.user.id },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
if (!user?.stripeCustomerId) {
|
|
323
|
+
return NextResponse.json(
|
|
324
|
+
{ error: 'No subscription found' },
|
|
325
|
+
{ status: 400 }
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const portalSession = await stripe.billingPortal.sessions.create({
|
|
330
|
+
customer: user.stripeCustomerId,
|
|
331
|
+
return_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing\`,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return NextResponse.json({ url: portalSession.url });
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Portal error:', error);
|
|
337
|
+
return NextResponse.json(
|
|
338
|
+
{ error: 'Failed to create portal session' },
|
|
339
|
+
{ status: 500 }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
await writeFile(
|
|
346
|
+
path.join(projectPath, 'src/app/api/stripe/portal/route.ts'),
|
|
347
|
+
portalApi
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Create billing page
|
|
351
|
+
await ensureDir(path.join(projectPath, 'src/app/dashboard/billing'));
|
|
352
|
+
|
|
353
|
+
const billingPage = `import { auth } from '@/lib/auth';
|
|
354
|
+
import { db } from '@/lib/db';
|
|
355
|
+
import { redirect } from 'next/navigation';
|
|
356
|
+
import { PLANS } from '@/lib/stripe';
|
|
357
|
+
import { SubscriptionCard } from '@/components/subscription-card';
|
|
358
|
+
|
|
359
|
+
export default async function BillingPage() {
|
|
360
|
+
const session = await auth();
|
|
361
|
+
|
|
362
|
+
if (!session?.user) {
|
|
363
|
+
redirect('/login');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const user = await db.user.findUnique({
|
|
367
|
+
where: { id: session.user.id },
|
|
368
|
+
include: { subscription: true },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<div className="container mx-auto px-4 py-8">
|
|
373
|
+
<h1 className="text-3xl font-bold mb-8">Billing & Subscription</h1>
|
|
374
|
+
|
|
375
|
+
{user?.subscription ? (
|
|
376
|
+
<div className="mb-8 rounded-lg border p-6">
|
|
377
|
+
<h2 className="text-xl font-semibold mb-4">Current Subscription</h2>
|
|
378
|
+
<div className="grid gap-4">
|
|
379
|
+
<div>
|
|
380
|
+
<p className="text-sm text-muted-foreground">Status</p>
|
|
381
|
+
<p className="font-medium capitalize">{user.subscription.status.toLowerCase()}</p>
|
|
382
|
+
</div>
|
|
383
|
+
<div>
|
|
384
|
+
<p className="text-sm text-muted-foreground">Current Period</p>
|
|
385
|
+
<p className="font-medium">
|
|
386
|
+
{new Date(user.subscription.currentPeriodStart).toLocaleDateString()} -{' '}
|
|
387
|
+
{new Date(user.subscription.currentPeriodEnd).toLocaleDateString()}
|
|
388
|
+
</p>
|
|
389
|
+
</div>
|
|
390
|
+
{user.subscription.cancelAtPeriodEnd && (
|
|
391
|
+
<div className="rounded-md bg-yellow-50 p-4">
|
|
392
|
+
<p className="text-sm text-yellow-800">
|
|
393
|
+
Your subscription will be canceled at the end of the current billing period.
|
|
394
|
+
</p>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
<form action="/api/stripe/portal" method="POST" className="mt-4">
|
|
399
|
+
<button
|
|
400
|
+
type="submit"
|
|
401
|
+
className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500"
|
|
402
|
+
>
|
|
403
|
+
Manage Subscription
|
|
404
|
+
</button>
|
|
405
|
+
</form>
|
|
406
|
+
</div>
|
|
407
|
+
) : (
|
|
408
|
+
<div className="mb-8">
|
|
409
|
+
<p className="text-muted-foreground mb-4">
|
|
410
|
+
You don't have an active subscription. Choose a plan below to get started.
|
|
411
|
+
</p>
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
|
|
415
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
416
|
+
{Object.entries(PLANS).map(([key, plan]) => (
|
|
417
|
+
<SubscriptionCard
|
|
418
|
+
key={key}
|
|
419
|
+
name={plan.name}
|
|
420
|
+
price={plan.price}
|
|
421
|
+
priceId={plan.priceId}
|
|
422
|
+
features={plan.features}
|
|
423
|
+
currentPlan={user?.subscription?.stripePriceId === plan.priceId}
|
|
424
|
+
/>
|
|
425
|
+
))}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
`;
|
|
431
|
+
|
|
432
|
+
await writeFile(
|
|
433
|
+
path.join(projectPath, 'src/app/dashboard/billing/page.tsx'),
|
|
434
|
+
billingPage
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Create SubscriptionCard component
|
|
438
|
+
const subscriptionCard = `'use client';
|
|
439
|
+
|
|
440
|
+
import { useRouter } from 'next/navigation';
|
|
441
|
+
import { useState } from 'react';
|
|
442
|
+
|
|
443
|
+
interface SubscriptionCardProps {
|
|
444
|
+
name: string;
|
|
445
|
+
price: number;
|
|
446
|
+
priceId: string | null;
|
|
447
|
+
features: string[];
|
|
448
|
+
currentPlan: boolean;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function SubscriptionCard({
|
|
452
|
+
name,
|
|
453
|
+
price,
|
|
454
|
+
priceId,
|
|
455
|
+
features,
|
|
456
|
+
currentPlan,
|
|
457
|
+
}: SubscriptionCardProps) {
|
|
458
|
+
const router = useRouter();
|
|
459
|
+
const [loading, setLoading] = useState(false);
|
|
460
|
+
|
|
461
|
+
const handleSubscribe = async () => {
|
|
462
|
+
if (!priceId) return;
|
|
463
|
+
|
|
464
|
+
setLoading(true);
|
|
465
|
+
try {
|
|
466
|
+
const res = await fetch('/api/stripe/checkout', {
|
|
467
|
+
method: 'POST',
|
|
468
|
+
headers: { 'Content-Type': 'application/json' },
|
|
469
|
+
body: JSON.stringify({ priceId }),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const { url } = await res.json();
|
|
473
|
+
if (url) {
|
|
474
|
+
window.location.href = url;
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error('Checkout error:', error);
|
|
478
|
+
setLoading(false);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div className="rounded-lg border p-6 flex flex-col">
|
|
484
|
+
<h3 className="text-2xl font-bold mb-2">{name}</h3>
|
|
485
|
+
<div className="mb-4">
|
|
486
|
+
<span className="text-4xl font-bold">\${price}</span>
|
|
487
|
+
<span className="text-muted-foreground">/month</span>
|
|
488
|
+
</div>
|
|
489
|
+
<ul className="space-y-2 mb-6 flex-grow">
|
|
490
|
+
{features.map((feature, i) => (
|
|
491
|
+
<li key={i} className="flex items-start">
|
|
492
|
+
<svg
|
|
493
|
+
className="h-5 w-5 text-green-500 mr-2 flex-shrink-0"
|
|
494
|
+
fill="none"
|
|
495
|
+
stroke="currentColor"
|
|
496
|
+
viewBox="0 0 24 24"
|
|
497
|
+
>
|
|
498
|
+
<path
|
|
499
|
+
strokeLinecap="round"
|
|
500
|
+
strokeLinejoin="round"
|
|
501
|
+
strokeWidth={2}
|
|
502
|
+
d="M5 13l4 4L19 7"
|
|
503
|
+
/>
|
|
504
|
+
</svg>
|
|
505
|
+
<span className="text-sm">{feature}</span>
|
|
506
|
+
</li>
|
|
507
|
+
))}
|
|
508
|
+
</ul>
|
|
509
|
+
{currentPlan ? (
|
|
510
|
+
<button
|
|
511
|
+
disabled
|
|
512
|
+
className="w-full rounded-md bg-gray-300 px-4 py-2 text-sm font-semibold text-gray-500 cursor-not-allowed"
|
|
513
|
+
>
|
|
514
|
+
Current Plan
|
|
515
|
+
</button>
|
|
516
|
+
) : priceId ? (
|
|
517
|
+
<button
|
|
518
|
+
onClick={handleSubscribe}
|
|
519
|
+
disabled={loading}
|
|
520
|
+
className="w-full rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 disabled:opacity-50"
|
|
521
|
+
>
|
|
522
|
+
{loading ? 'Loading...' : 'Subscribe'}
|
|
523
|
+
</button>
|
|
524
|
+
) : (
|
|
525
|
+
<button
|
|
526
|
+
disabled
|
|
527
|
+
className="w-full rounded-md bg-gray-200 px-4 py-2 text-sm font-semibold text-gray-700"
|
|
528
|
+
>
|
|
529
|
+
Free Plan
|
|
530
|
+
</button>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
`;
|
|
536
|
+
|
|
537
|
+
await writeFile(
|
|
538
|
+
path.join(projectPath, 'src/components/subscription-card.tsx'),
|
|
539
|
+
subscriptionCard
|
|
540
|
+
);
|
|
541
|
+
}
|