@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.0",
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
- price_id: priceId,
88
- payment_method_id: paymentMethodId
88
+ priceId: priceId,
89
+ paymentMethodId: paymentMethodId
89
90
  })
90
91
  });
91
92
 
92
- if (!response.ok) {
93
- const errorData = await response.json();
94
- throw new Error(errorData.error || 'Failed to create subscription');
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
- <span>{item.feature_details.description}</span>
387
- <span className="font-medium">
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-destructive' : ''}`} />
398
- <p className="text-xs text-muted-foreground">
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="grid gap-4 md:grid-cols-3">
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
- <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
- )}
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
- <CardDescription>{product.description}</CardDescription>
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="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>}
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
- <ul className="space-y-2">
531
+ <div className="space-y-2">
444
532
  {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>
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
- </ul>
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
- {isCurrentPlan ? "Current Plan" : parseFloat(price?.unit_amount) === 0 ? "Get Started" : "Upgrade"}
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
- <span>{feature.value} {feature.units} - {feature.description}</span>
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 }