@omnikit-js/ui 0.3.0 → 0.3.2
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnikit-js/ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A SaaS billing component for Next.js with Stripe integration",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"access": "public"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
+
"next": "^14.0.0 || ^15.0.0",
|
|
38
39
|
"react": "^18.0.0 || ^19.0.0",
|
|
39
|
-
"react-dom": "^18.0.0 || ^19.0.0"
|
|
40
|
-
"next": "^14.0.0 || ^15.0.0"
|
|
40
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@radix-ui/react-avatar": "^1.1.10",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"@radix-ui/react-slot": "^1.2.3",
|
|
53
53
|
"@radix-ui/react-switch": "^1.2.5",
|
|
54
54
|
"@radix-ui/react-tabs": "^1.1.12",
|
|
55
|
+
"@radix-ui/react-tooltip": "^1.2.7",
|
|
55
56
|
"@stripe/react-stripe-js": "^3.7.0",
|
|
56
57
|
"@stripe/stripe-js": "^7.4.0",
|
|
57
58
|
"class-variance-authority": "^0.7.1",
|
|
@@ -70,9 +71,9 @@
|
|
|
70
71
|
"@rollup/plugin-replace": "^6.0.2",
|
|
71
72
|
"@rollup/plugin-terser": "^0.4.4",
|
|
72
73
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
74
|
+
"@types/node": "^20",
|
|
73
75
|
"@types/react": "^18.3.3",
|
|
74
76
|
"@types/react-dom": "^18.3.0",
|
|
75
|
-
"@types/node": "^20",
|
|
76
77
|
"autoprefixer": "^10.4.19",
|
|
77
78
|
"postcss": "^8.4.38",
|
|
78
79
|
"react": "^18.3.1",
|
|
@@ -10,7 +10,8 @@ import { Badge } from "./ui/badge";
|
|
|
10
10
|
import { Progress } from "./ui/progress";
|
|
11
11
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
|
|
12
12
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
13
|
-
import { Check, CreditCard, FileText, AlertCircle, Loader2 } from "lucide-react";
|
|
13
|
+
import { Check, CreditCard, FileText, AlertCircle, AlertTriangle, CheckCircle, Info, Loader2 } from "lucide-react";
|
|
14
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
|
14
15
|
import PaymentMethodForm from './PaymentMethodForm';
|
|
15
16
|
import SubscriptionConfirmModal from './SubscriptionConfirmModal';
|
|
16
17
|
import { CardBrandIcon } from './ui/card-brand-icon';
|
|
@@ -84,21 +85,39 @@ async function createSubscription(userId: string, priceId: string, paymentMethod
|
|
|
84
85
|
},
|
|
85
86
|
body: JSON.stringify({
|
|
86
87
|
user_id: userId,
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
priceId: priceId,
|
|
89
|
+
paymentMethodId: paymentMethodId
|
|
89
90
|
})
|
|
90
91
|
});
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
|
|
95
|
+
// Check if the response indicates success
|
|
96
|
+
if (!response.ok || data.success === false) {
|
|
97
|
+
// Handle specific error types
|
|
98
|
+
let errorMessage = 'Failed to create subscription';
|
|
99
|
+
|
|
100
|
+
if (data.error === 'MISSING_PARAMETERS') {
|
|
101
|
+
errorMessage = data.message || 'Missing required parameters. Please check your information and try again.';
|
|
102
|
+
} else if (data.message) {
|
|
103
|
+
errorMessage = data.message;
|
|
104
|
+
} else if (data.error) {
|
|
105
|
+
errorMessage = data.error;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
error: errorMessage,
|
|
111
|
+
errorType: data.error
|
|
112
|
+
};
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
return { success: true };
|
|
115
|
+
return { success: true, data };
|
|
98
116
|
} catch (error) {
|
|
99
117
|
return {
|
|
100
118
|
success: false,
|
|
101
|
-
error: error instanceof Error ? error.message : 'An error occurred'
|
|
119
|
+
error: error instanceof Error ? error.message : 'An error occurred while creating subscription',
|
|
120
|
+
errorType: 'NETWORK_ERROR'
|
|
102
121
|
};
|
|
103
122
|
}
|
|
104
123
|
}
|
|
@@ -129,6 +148,16 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
129
148
|
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
|
130
149
|
const [activeTab, setActiveTab] = useState('overview');
|
|
131
150
|
const router = useRouter();
|
|
151
|
+
|
|
152
|
+
const formatFeatureValue = (value: any) => {
|
|
153
|
+
if (typeof value === 'boolean') {
|
|
154
|
+
return value ? '✓' : '✗'
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === 'number' && value >= 1000) {
|
|
157
|
+
return new Intl.NumberFormat('en-US').format(value)
|
|
158
|
+
}
|
|
159
|
+
return value
|
|
160
|
+
}
|
|
132
161
|
|
|
133
162
|
const currentSubscription = customerData?.subscriptions?.[0];
|
|
134
163
|
const currentPlanItem = currentSubscription?.items?.[0];
|
|
@@ -380,11 +409,41 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
380
409
|
? Math.min((item.current_usage / item.limit) * 100, 100)
|
|
381
410
|
: 0;
|
|
382
411
|
|
|
412
|
+
const getUsageColor = (percent: number) => {
|
|
413
|
+
if (percent >= 90) return 'text-red-600 dark:text-red-400';
|
|
414
|
+
if (percent >= 75) return 'text-orange-600 dark:text-orange-400';
|
|
415
|
+
if (percent >= 50) return 'text-yellow-600 dark:text-yellow-400';
|
|
416
|
+
return 'text-green-600 dark:text-green-400';
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const getProgressColor = (percent: number) => {
|
|
420
|
+
if (percent >= 90) return 'bg-red-100 dark:bg-red-950';
|
|
421
|
+
if (percent >= 75) return 'bg-orange-100 dark:bg-orange-950';
|
|
422
|
+
if (percent >= 50) return 'bg-yellow-100 dark:bg-yellow-950';
|
|
423
|
+
return 'bg-green-100 dark:bg-green-950';
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const getStatusIcon = (percent: number, withinLimit: boolean) => {
|
|
427
|
+
if (!withinLimit || percent >= 90) {
|
|
428
|
+
return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
|
|
429
|
+
}
|
|
430
|
+
if (percent >= 75) {
|
|
431
|
+
return <AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />;
|
|
432
|
+
}
|
|
433
|
+
if (percent >= 50) {
|
|
434
|
+
return <Info className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />;
|
|
435
|
+
}
|
|
436
|
+
return <CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />;
|
|
437
|
+
};
|
|
438
|
+
|
|
383
439
|
return (
|
|
384
440
|
<div key={item.feature_key} className="space-y-3">
|
|
385
441
|
<div className="flex items-center justify-between text-sm">
|
|
386
|
-
<
|
|
387
|
-
|
|
442
|
+
<div className="flex items-center gap-2">
|
|
443
|
+
{typeof item.limit === 'number' && getStatusIcon(percentage, item.within_limit)}
|
|
444
|
+
<span>{item.feature_details.description}</span>
|
|
445
|
+
</div>
|
|
446
|
+
<span className={`font-medium ${typeof item.limit === 'number' ? getUsageColor(percentage) : ''}`}>
|
|
388
447
|
{typeof item.limit === 'number'
|
|
389
448
|
? `${item.current_usage.toLocaleString()} / ${item.limit.toLocaleString()}`
|
|
390
449
|
: item.current_usage
|
|
@@ -394,8 +453,8 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
394
453
|
</div>
|
|
395
454
|
{typeof item.limit === 'number' && (
|
|
396
455
|
<>
|
|
397
|
-
<Progress value={percentage} className={`h-2 ${!item.within_limit ? 'bg-
|
|
398
|
-
<p className=
|
|
456
|
+
<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'}`} />
|
|
457
|
+
<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'}`}>
|
|
399
458
|
{item.within_limit
|
|
400
459
|
? `${Math.max(0, item.limit - item.current_usage).toLocaleString()} ${item.feature_details.units || ''} remaining`
|
|
401
460
|
: `${(item.current_usage - item.limit).toLocaleString()} ${item.feature_details.units || ''} over limit`
|
|
@@ -417,48 +476,89 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
417
476
|
</TabsContent>
|
|
418
477
|
|
|
419
478
|
<TabsContent value="plans" className="space-y-4">
|
|
420
|
-
<div className=
|
|
479
|
+
<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'}`}>
|
|
421
480
|
{pricingData?.products?.map((product: any) => {
|
|
422
481
|
const price = product.prices?.[0];
|
|
423
482
|
const features = product.features || [];
|
|
424
483
|
const isCurrentPlan = currentPlanItem?.price_id === price?.id;
|
|
425
484
|
|
|
485
|
+
// Determine if this is an upgrade, downgrade, or switch to free
|
|
486
|
+
const getButtonText = () => {
|
|
487
|
+
if (isCurrentPlan) return "Current Plan";
|
|
488
|
+
|
|
489
|
+
const targetPrice = parseFloat(price?.unit_amount || 0);
|
|
490
|
+
const currentPlanPrice = parseFloat(currentPrice?.unit_amount || 0);
|
|
491
|
+
|
|
492
|
+
// If target is free plan
|
|
493
|
+
if (targetPrice === 0) {
|
|
494
|
+
return currentPlanPrice > 0 ? "Switch to Free" : "Get Started";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// If current plan is free
|
|
498
|
+
if (currentPlanPrice === 0) {
|
|
499
|
+
return "Upgrade";
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Compare prices for paid plans
|
|
503
|
+
return targetPrice > currentPlanPrice ? "Upgrade" : "Downgrade";
|
|
504
|
+
};
|
|
505
|
+
|
|
426
506
|
return (
|
|
427
|
-
<Card key={product.id} className={isCurrentPlan ? "border-primary" : ""}>
|
|
428
|
-
|
|
429
|
-
<div className="
|
|
430
|
-
|
|
431
|
-
{isCurrentPlan && <Badge>Current</Badge>}
|
|
432
|
-
{product.metadata?.popular === 'true' && !isCurrentPlan && (
|
|
433
|
-
<Badge variant="secondary">Popular</Badge>
|
|
434
|
-
)}
|
|
507
|
+
<Card key={product.id} className={`relative overflow-hidden ${isCurrentPlan ? "border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
|
|
508
|
+
{product.metadata?.popular === 'true' && !isCurrentPlan && (
|
|
509
|
+
<div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
|
|
510
|
+
POPULAR
|
|
435
511
|
</div>
|
|
436
|
-
|
|
512
|
+
)}
|
|
513
|
+
<CardHeader className="text-center pb-4 pt-4">
|
|
514
|
+
<CardTitle className="text-lg font-semibold">{product.name}</CardTitle>
|
|
515
|
+
<CardDescription className="mt-1 text-sm">{product.description}</CardDescription>
|
|
437
516
|
</CardHeader>
|
|
438
|
-
<CardContent>
|
|
439
|
-
<div className="
|
|
440
|
-
|
|
441
|
-
|
|
517
|
+
<CardContent className="text-center pb-4">
|
|
518
|
+
<div className="mb-4">
|
|
519
|
+
<div className="flex items-baseline justify-center">
|
|
520
|
+
<span className="text-3xl font-bold tracking-tight">
|
|
521
|
+
{parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(0)}`}
|
|
522
|
+
</span>
|
|
523
|
+
{parseFloat(price?.unit_amount) > 0 && (
|
|
524
|
+
<span className="ml-1 text-sm text-muted-foreground">/month</span>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
{isCurrentPlan && (
|
|
528
|
+
<Badge variant="secondary" className="mt-2">Current Plan</Badge>
|
|
529
|
+
)}
|
|
442
530
|
</div>
|
|
443
|
-
<
|
|
531
|
+
<div className="space-y-2">
|
|
444
532
|
{features.map((feature: any) => (
|
|
445
|
-
<
|
|
446
|
-
<
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
533
|
+
<div key={feature.id} className="border-t pt-2">
|
|
534
|
+
<div className="text-lg font-semibold text-foreground">
|
|
535
|
+
{formatFeatureValue(feature.value)} {feature.units}
|
|
536
|
+
</div>
|
|
537
|
+
<TooltipProvider>
|
|
538
|
+
<Tooltip>
|
|
539
|
+
<TooltipTrigger asChild>
|
|
540
|
+
<div className="text-xs text-muted-foreground mt-1 cursor-help">
|
|
541
|
+
{feature.name || feature.description}
|
|
542
|
+
</div>
|
|
543
|
+
</TooltipTrigger>
|
|
544
|
+
<TooltipContent>
|
|
545
|
+
<p>{feature.description}</p>
|
|
546
|
+
</TooltipContent>
|
|
547
|
+
</Tooltip>
|
|
548
|
+
</TooltipProvider>
|
|
549
|
+
</div>
|
|
451
550
|
))}
|
|
452
|
-
</
|
|
551
|
+
</div>
|
|
453
552
|
</CardContent>
|
|
454
|
-
<CardFooter>
|
|
553
|
+
<CardFooter className="pt-3">
|
|
455
554
|
<Button
|
|
456
555
|
className="w-full"
|
|
457
|
-
variant={isCurrentPlan ? "outline" : "default"}
|
|
556
|
+
variant={isCurrentPlan ? "outline" : product.metadata?.popular === 'true' ? "default" : "secondary"}
|
|
557
|
+
size="lg"
|
|
458
558
|
disabled={isCurrentPlan}
|
|
459
559
|
onClick={() => !isCurrentPlan && handleSubscribeClick(product)}
|
|
460
560
|
>
|
|
461
|
-
{
|
|
561
|
+
{getButtonText()}
|
|
462
562
|
</Button>
|
|
463
563
|
</CardFooter>
|
|
464
564
|
</Card>
|
|
@@ -6,6 +6,7 @@ import { Button } from './ui/button';
|
|
|
6
6
|
import { Card } from './ui/card';
|
|
7
7
|
import { Check, Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
|
8
8
|
import { Alert, AlertDescription } from './ui/alert';
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
|
9
10
|
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
|
|
10
11
|
import { Label } from './ui/label';
|
|
11
12
|
import { CardBrandIcon } from './ui/card-brand-icon';
|
|
@@ -45,6 +46,16 @@ export default function SubscriptionConfirmModal({
|
|
|
45
46
|
}).format(amount / 100);
|
|
46
47
|
};
|
|
47
48
|
|
|
49
|
+
const formatFeatureValue = (value: any) => {
|
|
50
|
+
if (typeof value === 'boolean') {
|
|
51
|
+
return value ? '✓' : '✗'
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === 'number' && value >= 1000) {
|
|
54
|
+
return new Intl.NumberFormat('en-US').format(value)
|
|
55
|
+
}
|
|
56
|
+
return value
|
|
57
|
+
};
|
|
58
|
+
|
|
48
59
|
const handleConfirm = async () => {
|
|
49
60
|
setIsSubscribing(true);
|
|
50
61
|
setError(null);
|
|
@@ -120,7 +131,18 @@ export default function SubscriptionConfirmModal({
|
|
|
120
131
|
{plan.features?.map((feature: any) => (
|
|
121
132
|
<li key={feature.id} className="flex items-start text-sm">
|
|
122
133
|
<Check className="h-4 w-4 mr-2 text-primary mt-0.5 flex-shrink-0" />
|
|
123
|
-
<
|
|
134
|
+
<TooltipProvider>
|
|
135
|
+
<Tooltip>
|
|
136
|
+
<TooltipTrigger asChild>
|
|
137
|
+
<span className="cursor-help">
|
|
138
|
+
{formatFeatureValue(feature.value)} {feature.units} - {feature.name || feature.description}
|
|
139
|
+
</span>
|
|
140
|
+
</TooltipTrigger>
|
|
141
|
+
<TooltipContent>
|
|
142
|
+
<p>{feature.description}</p>
|
|
143
|
+
</TooltipContent>
|
|
144
|
+
</Tooltip>
|
|
145
|
+
</TooltipProvider>
|
|
124
146
|
</li>
|
|
125
147
|
))}
|
|
126
148
|
</ul>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
const TooltipProvider = TooltipPrimitive.Provider
|
|
9
|
+
|
|
10
|
+
const Tooltip = TooltipPrimitive.Root
|
|
11
|
+
|
|
12
|
+
const TooltipTrigger = TooltipPrimitive.Trigger
|
|
13
|
+
|
|
14
|
+
const TooltipContent = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof TooltipPrimitive.Content>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
|
17
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
18
|
+
<TooltipPrimitive.Content
|
|
19
|
+
ref={ref}
|
|
20
|
+
sideOffset={sideOffset}
|
|
21
|
+
className={cn(
|
|
22
|
+
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
))
|
|
28
|
+
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
|
29
|
+
|
|
30
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|