@omnikit-js/ui 0.2.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/dist/client.esm.js +28 -0
  4. package/dist/client.esm.js.map +1 -0
  5. package/dist/client.js +28 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/index.esm.js +28 -0
  8. package/dist/index.esm.js.map +1 -0
  9. package/dist/index.js +28 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/styles.css +3005 -0
  12. package/package.json +83 -0
  13. package/src/components/BillingClient.tsx +549 -0
  14. package/src/components/BillingServer.tsx +127 -0
  15. package/src/components/PaymentMethodForm.tsx +125 -0
  16. package/src/components/SubscriptionConfirmModal.tsx +185 -0
  17. package/src/components/theme-provider.tsx +11 -0
  18. package/src/components/ui/alert.tsx +59 -0
  19. package/src/components/ui/avatar.tsx +53 -0
  20. package/src/components/ui/badge.tsx +46 -0
  21. package/src/components/ui/button.tsx +59 -0
  22. package/src/components/ui/card-brand-icon.tsx +88 -0
  23. package/src/components/ui/card.tsx +92 -0
  24. package/src/components/ui/chart.tsx +353 -0
  25. package/src/components/ui/dialog.tsx +122 -0
  26. package/src/components/ui/dropdown-menu.tsx +257 -0
  27. package/src/components/ui/input.tsx +21 -0
  28. package/src/components/ui/label.tsx +24 -0
  29. package/src/components/ui/navigation-menu.tsx +168 -0
  30. package/src/components/ui/progress.tsx +31 -0
  31. package/src/components/ui/radio-group.tsx +44 -0
  32. package/src/components/ui/select.tsx +185 -0
  33. package/src/components/ui/separator.tsx +28 -0
  34. package/src/components/ui/switch.tsx +31 -0
  35. package/src/components/ui/tabs.tsx +66 -0
  36. package/src/components/ui/textarea.tsx +18 -0
  37. package/src/index.ts +13 -0
  38. package/src/lib/utils.ts +6 -0
  39. package/src/styles/globals.css +3 -0
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@omnikit-js/ui",
3
+ "version": "0.2.0",
4
+ "description": "A SaaS billing component for Next.js with Stripe integration",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "build": "rollup -c",
14
+ "dev": "rollup -c -w",
15
+ "test": "echo \"Error: no test specified\" && exit 1",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "react",
20
+ "nextjs",
21
+ "billing",
22
+ "saas",
23
+ "stripe",
24
+ "component"
25
+ ],
26
+ "author": "OmniKit",
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.0.0 || ^19.0.0",
33
+ "react-dom": "^18.0.0 || ^19.0.0",
34
+ "next": "^14.0.0 || ^15.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@radix-ui/react-avatar": "^1.1.10",
38
+ "@radix-ui/react-dialog": "^1.1.14",
39
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
40
+ "@radix-ui/react-label": "^2.1.7",
41
+ "@radix-ui/react-navigation-menu": "^1.2.13",
42
+ "@radix-ui/react-progress": "^1.1.7",
43
+ "@radix-ui/react-radio-group": "^1.3.7",
44
+ "@radix-ui/react-select": "^2.2.5",
45
+ "@radix-ui/react-separator": "^1.1.7",
46
+ "@radix-ui/react-slot": "^1.2.3",
47
+ "@radix-ui/react-switch": "^1.2.5",
48
+ "@radix-ui/react-tabs": "^1.1.12",
49
+ "@stripe/react-stripe-js": "^3.7.0",
50
+ "@stripe/stripe-js": "^7.4.0",
51
+ "class-variance-authority": "^0.7.1",
52
+ "clsx": "^2.1.1",
53
+ "lucide-react": "^0.525.0",
54
+ "tailwind-merge": "^3.3.1"
55
+ },
56
+ "devDependencies": {
57
+ "@babel/core": "^7.24.5",
58
+ "@babel/preset-env": "^7.24.5",
59
+ "@babel/preset-react": "^7.24.1",
60
+ "@babel/preset-typescript": "^7.27.1",
61
+ "@rollup/plugin-babel": "^6.0.4",
62
+ "@rollup/plugin-commonjs": "^25.0.7",
63
+ "@rollup/plugin-node-resolve": "^15.2.3",
64
+ "@rollup/plugin-replace": "^6.0.2",
65
+ "@rollup/plugin-terser": "^0.4.4",
66
+ "@rollup/plugin-typescript": "^11.1.6",
67
+ "@types/react": "^18.3.3",
68
+ "@types/react-dom": "^18.3.0",
69
+ "@types/node": "^20",
70
+ "autoprefixer": "^10.4.19",
71
+ "postcss": "^8.4.38",
72
+ "react": "^18.3.1",
73
+ "react-dom": "^18.3.1",
74
+ "rollup": "^4.17.2",
75
+ "rollup-plugin-copy": "^3.5.0",
76
+ "rollup-plugin-delete": "^2.0.0",
77
+ "rollup-plugin-dts": "^6.1.1",
78
+ "rollup-plugin-peer-deps-external": "^2.2.4",
79
+ "rollup-plugin-postcss": "^4.0.2",
80
+ "tailwindcss": "^3.4.3",
81
+ "typescript": "^5.4.5"
82
+ }
83
+ }
@@ -0,0 +1,549 @@
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, Loader2 } from "lucide-react";
14
+ import PaymentMethodForm from './PaymentMethodForm';
15
+ import SubscriptionConfirmModal from './SubscriptionConfirmModal';
16
+ import { CardBrandIcon } from './ui/card-brand-icon';
17
+
18
+ interface BillingClientProps {
19
+ customerData: any;
20
+ pricingData: any;
21
+ paymentMethods: any;
22
+ apiUrl: string;
23
+ apiKey: string;
24
+ }
25
+
26
+ const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
27
+
28
+ // Client-side actions
29
+ async function cancelSubscription(subscriptionId: string, apiUrl: string, apiKey: string) {
30
+ const response = await fetch(`${apiUrl}/billing/subscriptions/${subscriptionId}`, {
31
+ method: 'DELETE',
32
+ headers: {
33
+ 'x-api-key': apiKey,
34
+ 'Content-Type': 'application/json'
35
+ }
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error('Failed to cancel subscription');
40
+ }
41
+
42
+ return await response.json();
43
+ }
44
+
45
+ async function setDefaultPaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
46
+ const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}/default`, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'x-api-key': apiKey,
50
+ 'Content-Type': 'application/json'
51
+ }
52
+ });
53
+
54
+ if (!response.ok) {
55
+ throw new Error('Failed to set default payment method');
56
+ }
57
+
58
+ return await response.json();
59
+ }
60
+
61
+ async function deletePaymentMethod(paymentMethodId: string, apiUrl: string, apiKey: string) {
62
+ const response = await fetch(`${apiUrl}/billing/payment-methods/${paymentMethodId}`, {
63
+ method: 'DELETE',
64
+ headers: {
65
+ 'x-api-key': apiKey,
66
+ 'Content-Type': 'application/json'
67
+ }
68
+ });
69
+
70
+ if (!response.ok) {
71
+ throw new Error('Failed to delete payment method');
72
+ }
73
+
74
+ return await response.json();
75
+ }
76
+
77
+ async function createSubscription(userId: string, priceId: string, paymentMethodId: string, apiUrl: string, apiKey: string) {
78
+ try {
79
+ const response = await fetch(`${apiUrl}/billing/subscriptions`, {
80
+ method: 'POST',
81
+ headers: {
82
+ 'x-api-key': apiKey,
83
+ 'Content-Type': 'application/json'
84
+ },
85
+ body: JSON.stringify({
86
+ user_id: userId,
87
+ price_id: priceId,
88
+ payment_method_id: paymentMethodId
89
+ })
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const errorData = await response.json();
94
+ throw new Error(errorData.error || 'Failed to create subscription');
95
+ }
96
+
97
+ return { success: true };
98
+ } catch (error) {
99
+ return {
100
+ success: false,
101
+ error: error instanceof Error ? error.message : 'An error occurred'
102
+ };
103
+ }
104
+ }
105
+
106
+ async function createSetupIntent(userId: string, apiUrl: string, apiKey: string) {
107
+ const response = await fetch(`${apiUrl}/billing/setup-intent`, {
108
+ method: 'POST',
109
+ headers: {
110
+ 'x-api-key': apiKey,
111
+ 'Content-Type': 'application/json'
112
+ },
113
+ body: JSON.stringify({
114
+ user_id: userId
115
+ })
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new Error('Failed to create setup intent');
120
+ }
121
+
122
+ return await response.json();
123
+ }
124
+
125
+ export default function BillingClient({ customerData, pricingData, paymentMethods, apiUrl, apiKey }: BillingClientProps) {
126
+ const [isLoading, setIsLoading] = useState(false);
127
+ const [addPaymentModalOpen, setAddPaymentModalOpen] = useState(false);
128
+ const [subscribeModalOpen, setSubscribeModalOpen] = useState(false);
129
+ const [selectedPlan, setSelectedPlan] = useState<any>(null);
130
+ const [activeTab, setActiveTab] = useState('overview');
131
+ const router = useRouter();
132
+
133
+ const currentSubscription = customerData?.subscriptions?.[0];
134
+ const currentPlanItem = currentSubscription?.items?.[0];
135
+ const usage = customerData?.usage;
136
+ const invoices = customerData?.invoices || [];
137
+ const entitlements = customerData?.entitlements || [];
138
+
139
+ // Find the current plan details from pricing data
140
+ const currentPlan = pricingData?.products?.find((product: any) =>
141
+ product.prices?.some((price: any) => price.id === currentPlanItem?.price_id)
142
+ );
143
+ const currentPrice = currentPlan?.prices?.find((price: any) => price.id === currentPlanItem?.price_id);
144
+
145
+ const formatDate = (date: string | number) => {
146
+ const dateObj = typeof date === 'string' ? new Date(date) : new Date(date * 1000);
147
+ return dateObj.toLocaleDateString('en-US', {
148
+ month: 'long',
149
+ day: 'numeric',
150
+ year: 'numeric'
151
+ });
152
+ };
153
+
154
+ const formatCurrency = (amount: number) => {
155
+ return new Intl.NumberFormat('en-US', {
156
+ style: 'currency',
157
+ currency: 'usd',
158
+ }).format(amount / 100);
159
+ };
160
+
161
+ const handleCancelSubscription = async () => {
162
+ if (!currentSubscription?.id) return;
163
+
164
+ if (!confirm('Are you sure you want to cancel your subscription? You will continue to have access until the end of your billing period.')) {
165
+ return;
166
+ }
167
+
168
+ setIsLoading(true);
169
+ try {
170
+ await cancelSubscription(currentSubscription.id, apiUrl, apiKey);
171
+ router.refresh();
172
+ } catch (error) {
173
+ console.error('Failed to cancel subscription:', error);
174
+ } finally {
175
+ setIsLoading(false);
176
+ }
177
+ };
178
+
179
+ const handleSetDefaultPayment = async (paymentMethodId: string) => {
180
+ setIsLoading(true);
181
+ try {
182
+ await setDefaultPaymentMethod(paymentMethodId, apiUrl, apiKey);
183
+ router.refresh();
184
+ } catch (error) {
185
+ console.error('Failed to set default payment method:', error);
186
+ } finally {
187
+ setIsLoading(false);
188
+ }
189
+ };
190
+
191
+ const handleDeletePayment = async (paymentMethodId: string) => {
192
+ if (!confirm('Are you sure you want to delete this payment method?')) {
193
+ return;
194
+ }
195
+
196
+ setIsLoading(true);
197
+ try {
198
+ await deletePaymentMethod(paymentMethodId, apiUrl, apiKey);
199
+ router.refresh();
200
+ } catch (error) {
201
+ console.error('Failed to delete payment method:', error);
202
+ } finally {
203
+ setIsLoading(false);
204
+ }
205
+ };
206
+
207
+ const handlePaymentMethodAdded = () => {
208
+ setAddPaymentModalOpen(false);
209
+ router.refresh();
210
+ };
211
+
212
+ const handleSubscribeClick = (plan: any) => {
213
+ const price = plan.prices?.[0];
214
+ const isFree = price && parseFloat(price.unit_amount) === 0;
215
+
216
+ if (!isFree && (!paymentMethods?.paymentMethods || paymentMethods.paymentMethods.length === 0)) {
217
+ setActiveTab('overview');
218
+ setAddPaymentModalOpen(true);
219
+ return;
220
+ }
221
+
222
+ setSelectedPlan(plan);
223
+ setSubscribeModalOpen(true);
224
+ };
225
+
226
+ const handleSubscriptionCreated = () => {
227
+ setSubscribeModalOpen(false);
228
+ setSelectedPlan(null);
229
+ router.refresh();
230
+ setActiveTab('overview');
231
+ };
232
+
233
+ const handleCreateSubscription = async (userId: string, priceId: string, paymentMethodId: string) => {
234
+ return await createSubscription(userId, priceId, paymentMethodId, apiUrl, apiKey);
235
+ };
236
+
237
+ const handleCreateSetupIntent = async (userId: string) => {
238
+ return await createSetupIntent(userId, apiUrl, apiKey);
239
+ };
240
+
241
+ return (
242
+ <>
243
+ <div className="space-y-6">
244
+ <div>
245
+ <h3 className="text-lg font-medium">Billing</h3>
246
+ <p className="text-sm text-muted-foreground">
247
+ Manage your subscription and billing information.
248
+ </p>
249
+ </div>
250
+
251
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
252
+ <TabsList>
253
+ <TabsTrigger value="overview">Overview</TabsTrigger>
254
+ <TabsTrigger value="usage">Usage</TabsTrigger>
255
+ <TabsTrigger value="plans">Plans</TabsTrigger>
256
+ <TabsTrigger value="billing-history">Billing History</TabsTrigger>
257
+ </TabsList>
258
+
259
+ <TabsContent value="overview" className="space-y-4">
260
+ <Card>
261
+ <CardHeader>
262
+ <CardTitle>Current Plan</CardTitle>
263
+ <CardDescription>
264
+ {currentPlan ? `You are currently on the ${currentPlan.name}` : 'No active subscription'}
265
+ </CardDescription>
266
+ </CardHeader>
267
+ <CardContent>
268
+ {currentSubscription ? (
269
+ <div className="flex items-center justify-between">
270
+ <div>
271
+ <p className="text-2xl font-bold">
272
+ {currentPrice ? formatCurrency(parseFloat(currentPrice.unit_amount) * 100) : 'Active'}/month
273
+ </p>
274
+ <p className="text-sm text-muted-foreground">
275
+ Next billing date: {formatDate(currentSubscription.current_period_end)}
276
+ </p>
277
+ {currentSubscription.cancel_at_period_end && (
278
+ <p className="text-sm text-destructive mt-2">
279
+ <AlertCircle className="inline h-4 w-4 mr-1" />
280
+ Subscription will cancel at the end of the billing period
281
+ </p>
282
+ )}
283
+ </div>
284
+ <div className="space-x-2">
285
+ <Button variant="outline" onClick={() => setActiveTab('plans')}>
286
+ Change Plan
287
+ </Button>
288
+ {!currentSubscription.cancel_at_period_end && (
289
+ <Button
290
+ variant="outline"
291
+ onClick={handleCancelSubscription}
292
+ disabled={isLoading}
293
+ >
294
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
295
+ Cancel Subscription
296
+ </Button>
297
+ )}
298
+ </div>
299
+ </div>
300
+ ) : (
301
+ <div className="text-center py-6">
302
+ <p className="text-muted-foreground mb-4">You don't have an active subscription</p>
303
+ <Button onClick={() => setActiveTab('plans')}>
304
+ Choose a Plan
305
+ </Button>
306
+ </div>
307
+ )}
308
+ </CardContent>
309
+ </Card>
310
+
311
+ <Card>
312
+ <CardHeader>
313
+ <CardTitle>Payment Methods</CardTitle>
314
+ <CardDescription>Manage your payment information</CardDescription>
315
+ </CardHeader>
316
+ <CardContent className="space-y-4">
317
+ {paymentMethods?.paymentMethods?.length > 0 ? (
318
+ <div className="space-y-3">
319
+ {paymentMethods.paymentMethods.map((method: any) => (
320
+ <div key={method.id} className="flex items-center justify-between p-4 border rounded-lg">
321
+ <div className="flex items-center gap-4">
322
+ <CardBrandIcon brand={method.brand} className="w-16 h-10" />
323
+ <div className="flex-1">
324
+ <p className="font-medium">•••• •••• •••• {method.last4}</p>
325
+ <p className="text-sm text-muted-foreground">
326
+ Expires {method.exp_month}/{method.exp_year}
327
+ </p>
328
+ </div>
329
+ {method.is_default && (
330
+ <Badge variant="secondary">Default</Badge>
331
+ )}
332
+ </div>
333
+ <div className="flex items-center gap-2 ml-4">
334
+ {!method.is_default && (
335
+ <Button
336
+ variant="outline"
337
+ size="sm"
338
+ onClick={() => handleSetDefaultPayment(method.stripe_payment_method_id)}
339
+ disabled={isLoading}
340
+ >
341
+ Set as Default
342
+ </Button>
343
+ )}
344
+ <Button
345
+ variant="outline"
346
+ size="sm"
347
+ onClick={() => handleDeletePayment(method.stripe_payment_method_id)}
348
+ disabled={isLoading}
349
+ >
350
+ Remove
351
+ </Button>
352
+ </div>
353
+ </div>
354
+ ))}
355
+ </div>
356
+ ) : (
357
+ <div className="text-center py-6">
358
+ <p className="text-muted-foreground mb-4">No payment methods on file</p>
359
+ </div>
360
+ )}
361
+ </CardContent>
362
+ <CardFooter>
363
+ <Button className="w-full" onClick={() => setAddPaymentModalOpen(true)}>
364
+ <CreditCard className="mr-2 h-4 w-4" />
365
+ Add Payment Method
366
+ </Button>
367
+ </CardFooter>
368
+ </Card>
369
+ </TabsContent>
370
+
371
+ <TabsContent value="usage" className="space-y-4">
372
+ <Card>
373
+ <CardHeader>
374
+ <CardTitle>Current Usage</CardTitle>
375
+ <CardDescription>Your resource usage for the current billing period</CardDescription>
376
+ </CardHeader>
377
+ <CardContent className="space-y-6">
378
+ {usage?.data?.map((item: any) => {
379
+ const percentage = typeof item.limit === 'number'
380
+ ? Math.min((item.current_usage / item.limit) * 100, 100)
381
+ : 0;
382
+
383
+ return (
384
+ <div key={item.feature_key} className="space-y-3">
385
+ <div className="flex items-center justify-between text-sm">
386
+ <span>{item.feature_details.description}</span>
387
+ <span className="font-medium">
388
+ {typeof item.limit === 'number'
389
+ ? `${item.current_usage.toLocaleString()} / ${item.limit.toLocaleString()}`
390
+ : item.current_usage
391
+ }
392
+ {item.feature_details.units && ` ${item.feature_details.units}`}
393
+ </span>
394
+ </div>
395
+ {typeof item.limit === 'number' && (
396
+ <>
397
+ <Progress value={percentage} className={`h-2 ${!item.within_limit ? 'bg-destructive' : ''}`} />
398
+ <p className="text-xs text-muted-foreground">
399
+ {item.within_limit
400
+ ? `${Math.max(0, item.limit - item.current_usage).toLocaleString()} ${item.feature_details.units || ''} remaining`
401
+ : `${(item.current_usage - item.limit).toLocaleString()} ${item.feature_details.units || ''} over limit`
402
+ }
403
+ </p>
404
+ </>
405
+ )}
406
+ </div>
407
+ );
408
+ })}
409
+ </CardContent>
410
+ <CardFooter>
411
+ <p className="text-sm text-muted-foreground">
412
+ Usage resets on {currentSubscription ? formatDate(currentSubscription.current_period_end) : 'N/A'}.
413
+ Need more resources? Consider upgrading your plan.
414
+ </p>
415
+ </CardFooter>
416
+ </Card>
417
+ </TabsContent>
418
+
419
+ <TabsContent value="plans" className="space-y-4">
420
+ <div className="grid gap-4 md:grid-cols-3">
421
+ {pricingData?.products?.map((product: any) => {
422
+ const price = product.prices?.[0];
423
+ const features = product.features || [];
424
+ const isCurrentPlan = currentPlanItem?.price_id === price?.id;
425
+
426
+ return (
427
+ <Card key={product.id} className={isCurrentPlan ? "border-primary" : ""}>
428
+ <CardHeader>
429
+ <div className="flex items-center justify-between">
430
+ <CardTitle>{product.name}</CardTitle>
431
+ {isCurrentPlan && <Badge>Current</Badge>}
432
+ {product.metadata?.popular === 'true' && !isCurrentPlan && (
433
+ <Badge variant="secondary">Popular</Badge>
434
+ )}
435
+ </div>
436
+ <CardDescription>{product.description}</CardDescription>
437
+ </CardHeader>
438
+ <CardContent>
439
+ <div className="text-3xl font-bold mb-4">
440
+ {parseFloat(price?.unit_amount) === 0 ? 'Free' : formatCurrency(parseFloat(price?.unit_amount) * 100)}
441
+ {parseFloat(price?.unit_amount) > 0 && <span className="text-sm font-normal">/month</span>}
442
+ </div>
443
+ <ul className="space-y-2">
444
+ {features.map((feature: any) => (
445
+ <li key={feature.id} className="flex items-center">
446
+ <Check className="h-4 w-4 mr-2 text-primary" />
447
+ <span className="text-sm">
448
+ {feature.value} {feature.units} - {feature.description}
449
+ </span>
450
+ </li>
451
+ ))}
452
+ </ul>
453
+ </CardContent>
454
+ <CardFooter>
455
+ <Button
456
+ className="w-full"
457
+ variant={isCurrentPlan ? "outline" : "default"}
458
+ disabled={isCurrentPlan}
459
+ onClick={() => !isCurrentPlan && handleSubscribeClick(product)}
460
+ >
461
+ {isCurrentPlan ? "Current Plan" : parseFloat(price?.unit_amount) === 0 ? "Get Started" : "Upgrade"}
462
+ </Button>
463
+ </CardFooter>
464
+ </Card>
465
+ );
466
+ })}
467
+ </div>
468
+ </TabsContent>
469
+
470
+ <TabsContent value="billing-history" className="space-y-4">
471
+ <Card>
472
+ <CardHeader>
473
+ <CardTitle>Billing History</CardTitle>
474
+ <CardDescription>Download your past invoices</CardDescription>
475
+ </CardHeader>
476
+ <CardContent>
477
+ <div className="space-y-4">
478
+ {invoices.length > 0 ? (
479
+ invoices.map((invoice: any) => (
480
+ <div key={invoice.id} className="flex items-center justify-between p-4 border rounded-lg">
481
+ <div className="flex items-center space-x-4">
482
+ <FileText className="h-8 w-8 text-muted-foreground" />
483
+ <div>
484
+ <p className="font-medium">Invoice #{invoice.id.slice(-8).toUpperCase()}</p>
485
+ <p className="text-sm text-muted-foreground">{formatDate(invoice.created)}</p>
486
+ </div>
487
+ </div>
488
+ <div className="flex items-center space-x-4">
489
+ <div className="text-right">
490
+ <p className="font-medium">{formatCurrency(invoice.amount_paid)}</p>
491
+ <p className="text-sm text-muted-foreground capitalize">{invoice.status}</p>
492
+ </div>
493
+ <Button
494
+ variant="outline"
495
+ size="sm"
496
+ onClick={() => window.open(invoice.invoice_pdf || invoice.hosted_invoice_url, '_blank')}
497
+ >
498
+ <FileText className="h-4 w-4 mr-2" />
499
+ Download
500
+ </Button>
501
+ </div>
502
+ </div>
503
+ ))
504
+ ) : (
505
+ <div className="text-center py-12">
506
+ <FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
507
+ <p className="text-muted-foreground">No invoices yet</p>
508
+ </div>
509
+ )}
510
+ </div>
511
+ </CardContent>
512
+ </Card>
513
+ </TabsContent>
514
+ </Tabs>
515
+ </div>
516
+
517
+ <Dialog open={addPaymentModalOpen} onOpenChange={setAddPaymentModalOpen}>
518
+ <DialogContent>
519
+ <DialogHeader>
520
+ <DialogTitle>Add Payment Method</DialogTitle>
521
+ <DialogDescription>
522
+ Add a new payment method to your account. Your card information is securely processed by Stripe.
523
+ </DialogDescription>
524
+ </DialogHeader>
525
+
526
+ <Elements stripe={stripePromise}>
527
+ <PaymentMethodForm
528
+ userId={String(customerData?.id || '')}
529
+ onSuccess={handlePaymentMethodAdded}
530
+ onCancel={() => setAddPaymentModalOpen(false)}
531
+ createSetupIntent={handleCreateSetupIntent}
532
+ />
533
+ </Elements>
534
+ </DialogContent>
535
+ </Dialog>
536
+
537
+ <SubscriptionConfirmModal
538
+ isOpen={subscribeModalOpen}
539
+ onClose={() => setSubscribeModalOpen(false)}
540
+ plan={selectedPlan}
541
+ currentPlan={currentPlan}
542
+ paymentMethods={paymentMethods?.paymentMethods || []}
543
+ userId={String(customerData?.id || '')}
544
+ onSuccess={handleSubscriptionCreated}
545
+ createSubscription={handleCreateSubscription}
546
+ />
547
+ </>
548
+ );
549
+ }