@omnikit-js/ui 0.5.4 → 0.6.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.
Files changed (48) hide show
  1. package/README.md +100 -255
  2. package/dist/components/client/index.d.mts +7 -0
  3. package/dist/components/client/index.mjs +6 -0
  4. package/dist/components/client/index.mjs.map +1 -0
  5. package/dist/components/server/index.d.mts +2 -0
  6. package/dist/components/server/index.mjs +201 -0
  7. package/dist/components/server/index.mjs.map +1 -0
  8. package/dist/index-D-5etDTV.d.mts +80 -0
  9. package/dist/index.d.mts +48 -0
  10. package/dist/index.mjs +204 -0
  11. package/dist/index.mjs.map +1 -0
  12. package/package.json +41 -77
  13. package/LICENSE +0 -21
  14. package/dist/styles.css +0 -2486
  15. package/dist/styles.isolated.css +0 -2643
  16. package/dist/styles.prefixed.css +0 -248
  17. package/src/components/BillingClient.tsx +0 -686
  18. package/src/components/BillingServer.tsx +0 -129
  19. package/src/components/PaymentMethodForm.tsx +0 -125
  20. package/src/components/SubscriptionConfirmModal.tsx +0 -213
  21. package/src/components/theme-provider.tsx +0 -11
  22. package/src/components/ui/alert.tsx +0 -59
  23. package/src/components/ui/avatar.tsx +0 -53
  24. package/src/components/ui/badge.tsx +0 -46
  25. package/src/components/ui/button.tsx +0 -59
  26. package/src/components/ui/card-brand-icon.tsx +0 -88
  27. package/src/components/ui/card.tsx +0 -92
  28. package/src/components/ui/chart.tsx +0 -353
  29. package/src/components/ui/dialog.tsx +0 -122
  30. package/src/components/ui/dropdown-menu.tsx +0 -257
  31. package/src/components/ui/input.tsx +0 -21
  32. package/src/components/ui/label.tsx +0 -24
  33. package/src/components/ui/navigation-menu.tsx +0 -168
  34. package/src/components/ui/progress.tsx +0 -31
  35. package/src/components/ui/radio-group.tsx +0 -44
  36. package/src/components/ui/select.tsx +0 -185
  37. package/src/components/ui/separator.tsx +0 -28
  38. package/src/components/ui/switch.tsx +0 -31
  39. package/src/components/ui/tabs.tsx +0 -66
  40. package/src/components/ui/textarea.tsx +0 -18
  41. package/src/components/ui/tooltip.tsx +0 -30
  42. package/src/index.ts +0 -13
  43. package/src/lib/utils.ts +0 -6
  44. package/src/styles/globals.css +0 -1
  45. package/src/styles/isolated.css +0 -316
  46. package/src/styles/main.css +0 -95
  47. package/src/styles/prefixed-main.css +0 -95
  48. package/src/styles/prefixed.css +0 -1
@@ -1,686 +0,0 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import { loadStripe } from '@stripe/stripe-js';
6
- import { Elements } from '@stripe/react-stripe-js';
7
- import { Button } from "./ui/button";
8
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
9
- import { Badge } from "./ui/badge";
10
- import { Progress } from "./ui/progress";
11
- import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
12
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
13
- import { Check, CreditCard, FileText, AlertCircle, AlertTriangle, CheckCircle, Info, Loader2 } from "lucide-react";
14
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
15
- import PaymentMethodForm from './PaymentMethodForm';
16
- import SubscriptionConfirmModal from './SubscriptionConfirmModal';
17
- import { CardBrandIcon } from './ui/card-brand-icon';
18
-
19
- interface BillingClientProps {
20
- customerData: any;
21
- pricingData: any;
22
- paymentMethods: any;
23
- apiUrl: string;
24
- apiKey: string;
25
- }
26
-
27
- const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
28
-
29
- // Client-side actions
30
- async function cancelSubscription(subscriptionId: string, apiUrl: string, apiKey: string) {
31
- const response = await fetch(`${apiUrl}/billing/subscriptions/${subscriptionId}`, {
32
- method: 'DELETE',
33
- headers: {
34
- 'x-api-key': apiKey,
35
- 'Content-Type': 'application/json'
36
- }
37
- });
38
-
39
- if (!response.ok) {
40
- throw new Error('Failed to cancel subscription');
41
- }
42
-
43
- return await response.json();
44
- }
45
-
46
- async function setDefaultPaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
47
- const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}/default`, {
48
- method: 'POST',
49
- headers: {
50
- 'x-api-key': apiKey,
51
- 'Content-Type': 'application/json'
52
- },
53
- body: JSON.stringify({})
54
- });
55
-
56
- if (!response.ok) {
57
- throw new Error('Failed to set default payment method');
58
- }
59
-
60
- return await response.json();
61
- }
62
-
63
- async function deletePaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
64
- const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}`, {
65
- method: 'DELETE',
66
- headers: {
67
- 'x-api-key': apiKey,
68
- 'Content-Type': 'application/json'
69
- },
70
- body: JSON.stringify({})
71
- });
72
-
73
- if (!response.ok) {
74
- throw new Error('Failed to delete payment method');
75
- }
76
-
77
- return await response.json();
78
- }
79
-
80
- async function createSubscription(userId: string, priceId: string, paymentMethodId: string, apiUrl: string, apiKey: string) {
81
- try {
82
- const response = await fetch(`${apiUrl}/billing/subscriptions`, {
83
- method: 'POST',
84
- headers: {
85
- 'x-api-key': apiKey,
86
- 'Content-Type': 'application/json'
87
- },
88
- body: JSON.stringify({
89
- user_id: userId,
90
- priceId: priceId,
91
- paymentMethodId: paymentMethodId
92
- })
93
- });
94
-
95
- const data = await response.json();
96
-
97
- // Check if the response indicates success
98
- if (!response.ok || data.success === false) {
99
- // Handle specific error types
100
- let errorMessage = 'Failed to create subscription';
101
-
102
- if (data.error === 'MISSING_PARAMETERS') {
103
- errorMessage = data.message || 'Missing required parameters. Please check your information and try again.';
104
- } else if (data.message) {
105
- errorMessage = data.message;
106
- } else if (data.error) {
107
- errorMessage = data.error;
108
- }
109
-
110
- return {
111
- success: false,
112
- error: errorMessage,
113
- errorType: data.error
114
- };
115
- }
116
-
117
- return { success: true, data };
118
- } catch (error) {
119
- return {
120
- success: false,
121
- error: error instanceof Error ? error.message : 'An error occurred while creating subscription',
122
- errorType: 'NETWORK_ERROR'
123
- };
124
- }
125
- }
126
-
127
- async function createSetupIntent(userId: string, apiUrl: string, apiKey: string) {
128
- const response = await fetch(`${apiUrl}/billing/setup-intent`, {
129
- method: 'POST',
130
- headers: {
131
- 'x-api-key': apiKey,
132
- 'Content-Type': 'application/json'
133
- },
134
- body: JSON.stringify({
135
- user_id: userId
136
- })
137
- });
138
-
139
- if (!response.ok) {
140
- throw new Error('Failed to create setup intent');
141
- }
142
-
143
- return await response.json();
144
- }
145
-
146
- export default function BillingClient({ customerData, pricingData, paymentMethods, apiUrl, apiKey }: BillingClientProps) {
147
- const [isLoading, setIsLoading] = useState(false);
148
- const [addPaymentModalOpen, setAddPaymentModalOpen] = useState(false);
149
- const [subscribeModalOpen, setSubscribeModalOpen] = useState(false);
150
- const [deletePaymentModalOpen, setDeletePaymentModalOpen] = useState(false);
151
- const [selectedPlan, setSelectedPlan] = useState<any>(null);
152
- const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);
153
- const [activeTab, setActiveTab] = useState('overview');
154
- const router = useRouter();
155
-
156
- const formatFeatureValue = (value: any) => {
157
- // Handle string boolean values
158
- if (value === 'true' || value === true) {
159
- return '✓'
160
- }
161
- if (value === 'false' || value === false) {
162
- return '✗'
163
- }
164
- // Handle numeric values (both string and number)
165
- const numValue = typeof value === 'string' ? parseFloat(value) : value
166
- if (!isNaN(numValue) && numValue >= 1000) {
167
- return new Intl.NumberFormat('en-US').format(numValue)
168
- }
169
- return value
170
- }
171
-
172
- const currentSubscription = customerData?.subscriptions?.[0];
173
- const currentPlanItem = currentSubscription?.items?.[0];
174
- const usage = customerData?.usage;
175
- const invoices = customerData?.invoices || [];
176
- const entitlements = customerData?.entitlements || [];
177
-
178
- // Find the current plan details from pricing data
179
- const currentPlan = pricingData?.products?.find((product: any) =>
180
- product.prices?.some((price: any) => price.id === currentPlanItem?.price_id)
181
- );
182
- const currentPrice = currentPlan?.prices?.find((price: any) => price.id === currentPlanItem?.price_id);
183
-
184
- const formatDate = (date: string | number) => {
185
- const dateObj = typeof date === 'string' ? new Date(date) : new Date(date * 1000);
186
- return dateObj.toLocaleDateString('en-US', {
187
- month: 'long',
188
- day: 'numeric',
189
- year: 'numeric'
190
- });
191
- };
192
-
193
- const formatCurrency = (amount: number) => {
194
- return new Intl.NumberFormat('en-US', {
195
- style: 'currency',
196
- currency: 'usd',
197
- }).format(amount / 100);
198
- };
199
-
200
- const handleCancelSubscription = async () => {
201
- if (!currentSubscription?.id) return;
202
-
203
- if (!confirm('Are you sure you want to cancel your subscription? You will continue to have access until the end of your billing period.')) {
204
- return;
205
- }
206
-
207
- setIsLoading(true);
208
- try {
209
- await cancelSubscription(currentSubscription.id, apiUrl, apiKey);
210
- router.refresh();
211
- } catch (error) {
212
- console.error('Failed to cancel subscription:', error);
213
- } finally {
214
- setIsLoading(false);
215
- }
216
- };
217
-
218
- const handleSetDefaultPayment = async (paymentMethodId: string) => {
219
- setIsLoading(true);
220
- try {
221
- await setDefaultPaymentMethod(paymentMethodId, apiUrl, apiKey);
222
- router.refresh();
223
- } catch (error) {
224
- console.error('Failed to set default payment method:', error);
225
- } finally {
226
- setIsLoading(false);
227
- }
228
- };
229
-
230
- const handleDeletePayment = (paymentMethodId: string) => {
231
- setSelectedPaymentMethodId(paymentMethodId);
232
- setDeletePaymentModalOpen(true);
233
- };
234
-
235
- const confirmDeletePayment = async () => {
236
- if (!selectedPaymentMethodId) return;
237
-
238
- try {
239
- setIsLoading(true);
240
- await deletePaymentMethod(selectedPaymentMethodId, apiUrl, apiKey);
241
- router.refresh();
242
- } catch (error) {
243
- console.error('Error deleting payment method:', error);
244
- } finally {
245
- setIsLoading(false);
246
- setDeletePaymentModalOpen(false);
247
- setSelectedPaymentMethodId(null);
248
- }
249
- };
250
-
251
- const handlePaymentMethodAdded = () => {
252
- setAddPaymentModalOpen(false);
253
- router.refresh();
254
- };
255
-
256
- const handleSubscribeClick = (plan: any) => {
257
- const price = plan.prices?.[0];
258
- const isFree = price && parseFloat(price.unit_amount) === 0;
259
-
260
- if (!isFree && (!paymentMethods?.paymentMethods || paymentMethods.paymentMethods.length === 0)) {
261
- setActiveTab('overview');
262
- setAddPaymentModalOpen(true);
263
- return;
264
- }
265
-
266
- setSelectedPlan(plan);
267
- setSubscribeModalOpen(true);
268
- };
269
-
270
- const handleSubscriptionCreated = () => {
271
- setSubscribeModalOpen(false);
272
- setSelectedPlan(null);
273
- router.refresh();
274
- setActiveTab('overview');
275
- };
276
-
277
- const handleCreateSubscription = async (userId: string, priceId: string, paymentMethodId: string) => {
278
- return await createSubscription(userId, priceId, paymentMethodId, apiUrl, apiKey);
279
- };
280
-
281
- const handleCreateSetupIntent = async (userId: string) => {
282
- return await createSetupIntent(userId, apiUrl, apiKey);
283
- };
284
-
285
- return (
286
- <>
287
- <div className="space-y-6">
288
- <div>
289
- <h3 className="text-lg font-medium">Billing</h3>
290
- <p className="text-sm text-muted-foreground">
291
- Manage your subscription and billing information.
292
- </p>
293
- </div>
294
-
295
- <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
296
- <TabsList>
297
- <TabsTrigger value="overview">Overview</TabsTrigger>
298
- <TabsTrigger value="usage">Usage</TabsTrigger>
299
- <TabsTrigger value="plans">Plans</TabsTrigger>
300
- <TabsTrigger value="billing-history">Billing History</TabsTrigger>
301
- </TabsList>
302
-
303
- <TabsContent value="overview" className="space-y-4">
304
- <Card>
305
- <CardHeader>
306
- <CardTitle>Current Plan</CardTitle>
307
- <CardDescription>
308
- {currentPlan ? `You are currently on the ${currentPlan.name}` : 'No active subscription'}
309
- </CardDescription>
310
- </CardHeader>
311
- <CardContent>
312
- {currentSubscription ? (
313
- <div className="flex items-center justify-between">
314
- <div>
315
- <p className="text-2xl font-bold">
316
- {currentPrice ? formatCurrency(parseFloat(currentPrice.unit_amount) * 100) : 'Active'}/month
317
- </p>
318
- <p className="text-sm text-muted-foreground">
319
- Next billing date: {formatDate(currentSubscription.current_period_end)}
320
- </p>
321
- {currentSubscription.cancel_at_period_end && (
322
- <p className="text-sm text-destructive mt-2">
323
- <AlertCircle className="inline h-4 w-4 mr-1" />
324
- Subscription will cancel at the end of the billing period
325
- </p>
326
- )}
327
- </div>
328
- <div className="space-x-2">
329
- <Button variant="outline" onClick={() => setActiveTab('plans')}>
330
- Change Plan
331
- </Button>
332
- {!currentSubscription.cancel_at_period_end && (
333
- <Button
334
- variant="outline"
335
- onClick={handleCancelSubscription}
336
- disabled={isLoading}
337
- >
338
- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
339
- Cancel Subscription
340
- </Button>
341
- )}
342
- </div>
343
- </div>
344
- ) : (
345
- <div className="text-center py-6">
346
- <p className="text-muted-foreground mb-4">You don't have an active subscription</p>
347
- <Button onClick={() => setActiveTab('plans')}>
348
- Choose a Plan
349
- </Button>
350
- </div>
351
- )}
352
- </CardContent>
353
- </Card>
354
-
355
- <Card>
356
- <CardHeader>
357
- <CardTitle>Payment Methods</CardTitle>
358
- <CardDescription>Manage your payment information</CardDescription>
359
- </CardHeader>
360
- <CardContent className="space-y-4">
361
- {paymentMethods?.paymentMethods?.length > 0 ? (
362
- <div className="space-y-3">
363
- {paymentMethods.paymentMethods.map((method: any) => (
364
- <div key={method.id} className="flex items-center justify-between p-4 border border-border rounded-lg">
365
- <div className="flex items-center gap-4">
366
- <CardBrandIcon brand={method.brand} className="w-16 h-10" />
367
- <div className="flex-1">
368
- <p className="font-medium">•••• •••• •••• {method.last4}</p>
369
- <p className="text-sm text-muted-foreground">
370
- Expires {method.exp_month}/{method.exp_year}
371
- </p>
372
- </div>
373
- {method.is_default && (
374
- <Badge variant="secondary">Default</Badge>
375
- )}
376
- </div>
377
- <div className="flex items-center gap-2 ml-4">
378
- {!method.is_default && (
379
- <Button
380
- variant="outline"
381
- size="sm"
382
- onClick={() => handleSetDefaultPayment(method.stripe_payment_method_id)}
383
- disabled={isLoading}
384
- >
385
- Set as Default
386
- </Button>
387
- )}
388
- <Button
389
- variant="outline"
390
- size="sm"
391
- onClick={() => handleDeletePayment(method.stripe_payment_method_id)}
392
- disabled={isLoading}
393
- >
394
- Remove
395
- </Button>
396
- </div>
397
- </div>
398
- ))}
399
- </div>
400
- ) : (
401
- <div className="text-center py-6">
402
- <p className="text-muted-foreground mb-4">No payment methods on file</p>
403
- </div>
404
- )}
405
- </CardContent>
406
- <CardFooter>
407
- <Button className="w-full" onClick={() => setAddPaymentModalOpen(true)}>
408
- <CreditCard className="mr-2 h-4 w-4" />
409
- Add Payment Method
410
- </Button>
411
- </CardFooter>
412
- </Card>
413
- </TabsContent>
414
-
415
- <TabsContent value="usage" className="space-y-4">
416
- <Card>
417
- <CardHeader>
418
- <CardTitle>Current Usage</CardTitle>
419
- <CardDescription>Your resource usage for the current billing period</CardDescription>
420
- </CardHeader>
421
- <CardContent className="space-y-6">
422
- {usage?.data?.filter((item: any) => typeof item.limit === 'number' && item.limit > 0).map((item: any) => {
423
- const percentage = typeof item.limit === 'number'
424
- ? Math.min((item.current_usage / item.limit) * 100, 100)
425
- : 0;
426
-
427
- const getUsageColor = (percent: number) => {
428
- if (percent >= 90) return 'text-red-600 dark:text-red-400';
429
- if (percent >= 75) return 'text-orange-600 dark:text-orange-400';
430
- if (percent >= 50) return 'text-yellow-600 dark:text-yellow-400';
431
- return 'text-green-600 dark:text-green-400';
432
- };
433
-
434
- const getProgressColor = (percent: number) => {
435
- if (percent >= 90) return 'bg-red-100 dark:bg-red-950';
436
- if (percent >= 75) return 'bg-orange-100 dark:bg-orange-950';
437
- if (percent >= 50) return 'bg-yellow-100 dark:bg-yellow-950';
438
- return 'bg-green-100 dark:bg-green-950';
439
- };
440
-
441
- const getStatusIcon = (percent: number, withinLimit: boolean) => {
442
- if (!withinLimit || percent >= 90) {
443
- return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
444
- }
445
- if (percent >= 75) {
446
- return <AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />;
447
- }
448
- if (percent >= 50) {
449
- return <Info className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />;
450
- }
451
- return <CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />;
452
- };
453
-
454
- return (
455
- <div key={item.feature_key} className="space-y-3">
456
- <div className="flex items-center justify-between text-sm">
457
- <div className="flex items-center gap-2">
458
- {typeof item.limit === 'number' && getStatusIcon(percentage, item.within_limit)}
459
- <span>{item.feature_details.description}</span>
460
- </div>
461
- <span className={`font-medium ${typeof item.limit === 'number' ? getUsageColor(percentage) : ''}`}>
462
- {typeof item.limit === 'number'
463
- ? `${item.current_usage.toLocaleString()} / ${item.limit.toLocaleString()}`
464
- : item.current_usage
465
- }
466
- {item.feature_details.units && ` ${item.feature_details.units}`}
467
- </span>
468
- </div>
469
- {typeof item.limit === 'number' && (
470
- <>
471
- <Progress value={percentage} className={`h-2 ${getProgressColor(percentage)} ${!item.within_limit ? '[&>div]:bg-red-500' : percentage >= 90 ? '[&>div]:bg-red-500' : percentage >= 75 ? '[&>div]:bg-orange-500' : percentage >= 50 ? '[&>div]:bg-yellow-500' : '[&>div]:bg-green-500'}`} />
472
- <p className={`text-xs ${item.within_limit ? percentage >= 90 ? 'text-red-600 dark:text-red-400' : percentage >= 75 ? 'text-orange-600 dark:text-orange-400' : 'text-muted-foreground' : 'text-red-600 dark:text-red-400 font-medium'}`}>
473
- {item.within_limit
474
- ? `${Math.max(0, item.limit - item.current_usage).toLocaleString()} ${item.feature_details.units || ''} remaining`
475
- : `${(item.current_usage - item.limit).toLocaleString()} ${item.feature_details.units || ''} over limit`
476
- }
477
- </p>
478
- </>
479
- )}
480
- </div>
481
- );
482
- })}
483
- </CardContent>
484
- <CardFooter>
485
- <p className="text-sm text-muted-foreground">
486
- Usage resets on {currentSubscription ? formatDate(currentSubscription.current_period_end) : 'N/A'}.
487
- Need more resources? Consider upgrading your plan.
488
- </p>
489
- </CardFooter>
490
- </Card>
491
- </TabsContent>
492
-
493
- <TabsContent value="plans" className="space-y-4">
494
- <div className={`grid gap-4 ${pricingData?.products?.length === 3 ? 'md:grid-cols-3' : 'md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'}`}>
495
- {pricingData?.products?.map((product: any) => {
496
- const price = product.prices?.[0];
497
- const features = product.features || [];
498
- const isCurrentPlan = currentPlanItem?.price_id === price?.id;
499
-
500
- // Determine if this is an upgrade, downgrade, or switch to free
501
- const getButtonText = () => {
502
- if (isCurrentPlan) return "Current Plan";
503
-
504
- const targetPrice = parseFloat(price?.unit_amount || 0);
505
- const currentPlanPrice = parseFloat(currentPrice?.unit_amount || 0);
506
-
507
- // If target is free plan
508
- if (targetPrice === 0) {
509
- return currentPlanPrice > 0 ? "Switch to Free" : "Get Started";
510
- }
511
-
512
- // If current plan is free
513
- if (currentPlanPrice === 0) {
514
- return "Upgrade";
515
- }
516
-
517
- // Compare prices for paid plans
518
- return targetPrice > currentPlanPrice ? "Upgrade" : "Downgrade";
519
- };
520
-
521
- return (
522
- <Card key={product.id} className={`pricing-card relative overflow-hidden h-full flex flex-col ${isCurrentPlan ? "current border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
523
- {product.metadata?.popular === 'true' && !isCurrentPlan && (
524
- <div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
525
- POPULAR
526
- </div>
527
- )}
528
- <CardHeader className="text-center pb-4 pt-4">
529
- <CardTitle className="text-lg font-semibold">{product.name}</CardTitle>
530
- <CardDescription className="mt-1 text-sm">{product.description}</CardDescription>
531
- </CardHeader>
532
- <CardContent className="text-center pb-4 flex-1">
533
- <div className="mb-4">
534
- <div className="flex items-baseline justify-center">
535
- <span className="text-3xl font-bold tracking-tight">
536
- {parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(2).replace(/\.00$/, '')}`}
537
- </span>
538
- {parseFloat(price?.unit_amount) > 0 && (
539
- <span className="ml-1 text-sm text-muted-foreground">/month</span>
540
- )}
541
- </div>
542
- {isCurrentPlan ? (
543
- <Badge variant="secondary" className="mt-2">Current Plan</Badge>
544
- ) : (
545
- <div className="mt-2 h-6"></div>
546
- )}
547
- </div>
548
- <div className="space-y-3 flex-1 flex flex-col justify-between min-h-[300px]">
549
- {features.map((feature: any) => (
550
- <div key={feature.id} className="border-t pt-3 first:border-t-0 first:pt-0 flex-1 flex flex-col justify-center">
551
- <div className="text-lg font-semibold text-foreground">
552
- {formatFeatureValue(feature.value)} <span className="text-sm text-gray-500 font-normal">{feature.units}</span>
553
- </div>
554
- <TooltipProvider>
555
- <Tooltip>
556
- <TooltipTrigger asChild>
557
- <div className="text-xs text-gray-500 mt-1 cursor-help">
558
- {feature.name || feature.description}
559
- </div>
560
- </TooltipTrigger>
561
- <TooltipContent>
562
- <p>{feature.description}</p>
563
- </TooltipContent>
564
- </Tooltip>
565
- </TooltipProvider>
566
- </div>
567
- ))}
568
- </div>
569
- </CardContent>
570
- <CardFooter className="pt-3">
571
- <Button
572
- className="w-full"
573
- variant={isCurrentPlan ? "outline" : product.metadata?.popular === 'true' ? "default" : "secondary"}
574
- size="lg"
575
- disabled={isCurrentPlan}
576
- onClick={() => !isCurrentPlan && handleSubscribeClick(product)}
577
- >
578
- {getButtonText()}
579
- </Button>
580
- </CardFooter>
581
- </Card>
582
- );
583
- })}
584
- </div>
585
- </TabsContent>
586
-
587
- <TabsContent value="billing-history" className="space-y-4">
588
- <Card>
589
- <CardHeader>
590
- <CardTitle>Billing History</CardTitle>
591
- <CardDescription>Download your past invoices</CardDescription>
592
- </CardHeader>
593
- <CardContent>
594
- <div className="space-y-4">
595
- {invoices.length > 0 ? (
596
- invoices.map((invoice: any) => (
597
- <div key={invoice.id} className="flex items-center justify-between p-4 border border-border rounded-lg">
598
- <div className="flex items-center space-x-4">
599
- <FileText className="h-8 w-8 text-muted-foreground" />
600
- <div>
601
- <p className="font-medium">Invoice #{invoice.id.slice(-8).toUpperCase()}</p>
602
- <p className="text-sm text-muted-foreground">{formatDate(invoice.created)}</p>
603
- </div>
604
- </div>
605
- <div className="flex items-center space-x-4">
606
- <div className="text-right">
607
- <p className="font-medium">{formatCurrency(invoice.amount_paid)}</p>
608
- <p className="text-sm text-muted-foreground capitalize">{invoice.status}</p>
609
- </div>
610
- <Button
611
- variant="outline"
612
- size="sm"
613
- onClick={() => window.open(invoice.invoice_pdf || invoice.hosted_invoice_url, '_blank')}
614
- >
615
- <FileText className="h-4 w-4 mr-2" />
616
- Download
617
- </Button>
618
- </div>
619
- </div>
620
- ))
621
- ) : (
622
- <div className="text-center py-12">
623
- <FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
624
- <p className="text-muted-foreground">No invoices yet</p>
625
- </div>
626
- )}
627
- </div>
628
- </CardContent>
629
- </Card>
630
- </TabsContent>
631
- </Tabs>
632
- </div>
633
-
634
- <Dialog open={addPaymentModalOpen} onOpenChange={setAddPaymentModalOpen}>
635
- <DialogContent>
636
- <DialogHeader>
637
- <DialogTitle>Add Payment Method</DialogTitle>
638
- <DialogDescription>
639
- Add a new payment method to your account. Your card information is securely processed by Stripe.
640
- </DialogDescription>
641
- </DialogHeader>
642
-
643
- <Elements stripe={stripePromise}>
644
- <PaymentMethodForm
645
- userId={String(customerData?.id || '')}
646
- onSuccess={handlePaymentMethodAdded}
647
- onCancel={() => setAddPaymentModalOpen(false)}
648
- createSetupIntent={handleCreateSetupIntent}
649
- />
650
- </Elements>
651
- </DialogContent>
652
- </Dialog>
653
-
654
- <SubscriptionConfirmModal
655
- isOpen={subscribeModalOpen}
656
- onClose={() => setSubscribeModalOpen(false)}
657
- plan={selectedPlan}
658
- currentPlan={currentPlan}
659
- paymentMethods={paymentMethods?.paymentMethods || []}
660
- userId={String(customerData?.id || '')}
661
- onSuccess={handleSubscriptionCreated}
662
- createSubscription={handleCreateSubscription}
663
- />
664
-
665
- <Dialog open={deletePaymentModalOpen} onOpenChange={setDeletePaymentModalOpen}>
666
- <DialogContent>
667
- <DialogHeader>
668
- <DialogTitle>Delete Payment Method</DialogTitle>
669
- <DialogDescription>
670
- Are you sure you want to delete this payment method? This action cannot be undone.
671
- </DialogDescription>
672
- </DialogHeader>
673
- <div className="flex justify-end space-x-2 pt-4">
674
- <Button variant="outline" onClick={() => setDeletePaymentModalOpen(false)}>
675
- Cancel
676
- </Button>
677
- <Button variant="destructive" onClick={confirmDeletePayment} disabled={isLoading}>
678
- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
679
- Delete
680
- </Button>
681
- </div>
682
- </DialogContent>
683
- </Dialog>
684
- </>
685
- );
686
- }