@omnikit-js/ui 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -19,6 +19,31 @@ A comprehensive SaaS billing component for Next.js applications with Stripe inte
19
19
  npm install @omnikit-js/ui
20
20
  ```
21
21
 
22
+ **Important**: The component requires Tailwind CSS to be configured in your project.
23
+
24
+ ### Setup Tailwind CSS
25
+
26
+ 1. Install Tailwind CSS if you haven't already:
27
+ ```bash
28
+ npm install -D tailwindcss postcss autoprefixer
29
+ npx tailwindcss init -p
30
+ ```
31
+
32
+ 2. Import the component's CSS in your main CSS file or `layout.js`:
33
+ ```css
34
+ @import '@omnikit-js/ui/styles.css';
35
+ ```
36
+
37
+ Or in your `app/globals.css`:
38
+ ```css
39
+ @tailwind base;
40
+ @tailwind components;
41
+ @tailwind utilities;
42
+
43
+ /* Import the component styles */
44
+ @import '@omnikit-js/ui/styles.css';
45
+ ```
46
+
22
47
  ## Prerequisites
23
48
 
24
49
  1. A Stripe account with API keys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnikit-js/ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",
@@ -13,7 +13,8 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
- "src"
16
+ "src",
17
+ "styles.css"
17
18
  ],
18
19
  "scripts": {
19
20
  "build": "rollup -c",
@@ -35,9 +36,9 @@
35
36
  "access": "public"
36
37
  },
37
38
  "peerDependencies": {
39
+ "next": "^14.0.0 || ^15.0.0",
38
40
  "react": "^18.0.0 || ^19.0.0",
39
- "react-dom": "^18.0.0 || ^19.0.0",
40
- "next": "^14.0.0 || ^15.0.0"
41
+ "react-dom": "^18.0.0 || ^19.0.0"
41
42
  },
42
43
  "dependencies": {
43
44
  "@radix-ui/react-avatar": "^1.1.10",
@@ -52,6 +53,7 @@
52
53
  "@radix-ui/react-slot": "^1.2.3",
53
54
  "@radix-ui/react-switch": "^1.2.5",
54
55
  "@radix-ui/react-tabs": "^1.1.12",
56
+ "@radix-ui/react-tooltip": "^1.2.7",
55
57
  "@stripe/react-stripe-js": "^3.7.0",
56
58
  "@stripe/stripe-js": "^7.4.0",
57
59
  "class-variance-authority": "^0.7.1",
@@ -70,9 +72,9 @@
70
72
  "@rollup/plugin-replace": "^6.0.2",
71
73
  "@rollup/plugin-terser": "^0.4.4",
72
74
  "@rollup/plugin-typescript": "^11.1.6",
75
+ "@types/node": "^20",
73
76
  "@types/react": "^18.3.3",
74
77
  "@types/react-dom": "^18.3.0",
75
- "@types/node": "^20",
76
78
  "autoprefixer": "^10.4.19",
77
79
  "postcss": "^8.4.38",
78
80
  "react": "^18.3.1",
@@ -11,6 +11,7 @@ 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
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';
@@ -48,7 +49,8 @@ async function setDefaultPaymentMethod(paymentMethodId: string, apiUrl: string,
48
49
  headers: {
49
50
  'x-api-key': apiKey,
50
51
  'Content-Type': 'application/json'
51
- }
52
+ },
53
+ body: JSON.stringify({})
52
54
  });
53
55
 
54
56
  if (!response.ok) {
@@ -64,7 +66,8 @@ async function deletePaymentMethod(paymentMethodId: string, apiUrl: string, apiK
64
66
  headers: {
65
67
  'x-api-key': apiKey,
66
68
  'Content-Type': 'application/json'
67
- }
69
+ },
70
+ body: JSON.stringify({})
68
71
  });
69
72
 
70
73
  if (!response.ok) {
@@ -144,9 +147,27 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
144
147
  const [isLoading, setIsLoading] = useState(false);
145
148
  const [addPaymentModalOpen, setAddPaymentModalOpen] = useState(false);
146
149
  const [subscribeModalOpen, setSubscribeModalOpen] = useState(false);
150
+ const [deletePaymentModalOpen, setDeletePaymentModalOpen] = useState(false);
147
151
  const [selectedPlan, setSelectedPlan] = useState<any>(null);
152
+ const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);
148
153
  const [activeTab, setActiveTab] = useState('overview');
149
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
+ }
150
171
 
151
172
  const currentSubscription = customerData?.subscriptions?.[0];
152
173
  const currentPlanItem = currentSubscription?.items?.[0];
@@ -206,19 +227,24 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
206
227
  }
207
228
  };
208
229
 
209
- const handleDeletePayment = async (paymentMethodId: string) => {
210
- if (!confirm('Are you sure you want to delete this payment method?')) {
211
- return;
212
- }
213
-
214
- setIsLoading(true);
230
+ const handleDeletePayment = (paymentMethodId: string) => {
231
+ setSelectedPaymentMethodId(paymentMethodId);
232
+ setDeletePaymentModalOpen(true);
233
+ };
234
+
235
+ const confirmDeletePayment = async () => {
236
+ if (!selectedPaymentMethodId) return;
237
+
215
238
  try {
216
- await deletePaymentMethod(paymentMethodId, apiUrl, apiKey);
239
+ setIsLoading(true);
240
+ await deletePaymentMethod(selectedPaymentMethodId, apiUrl, apiKey);
217
241
  router.refresh();
218
242
  } catch (error) {
219
- console.error('Failed to delete payment method:', error);
243
+ console.error('Error deleting payment method:', error);
220
244
  } finally {
221
245
  setIsLoading(false);
246
+ setDeletePaymentModalOpen(false);
247
+ setSelectedPaymentMethodId(null);
222
248
  }
223
249
  };
224
250
 
@@ -393,7 +419,7 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
393
419
  <CardDescription>Your resource usage for the current billing period</CardDescription>
394
420
  </CardHeader>
395
421
  <CardContent className="space-y-6">
396
- {usage?.data?.map((item: any) => {
422
+ {usage?.data?.filter((item: any) => typeof item.limit === 'number' && item.limit > 0).map((item: any) => {
397
423
  const percentage = typeof item.limit === 'number'
398
424
  ? Math.min((item.current_usage / item.limit) * 100, 100)
399
425
  : 0;
@@ -465,7 +491,7 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
465
491
  </TabsContent>
466
492
 
467
493
  <TabsContent value="plans" className="space-y-4">
468
- <div className="grid gap-6 md:grid-cols-3">
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'}`}>
469
495
  {pricingData?.products?.map((product: any) => {
470
496
  const price = product.prices?.[0];
471
497
  const features = product.features || [];
@@ -493,44 +519,55 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
493
519
  };
494
520
 
495
521
  return (
496
- <Card key={product.id} className={`relative overflow-hidden ${isCurrentPlan ? "border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
522
+ <Card key={product.id} className={`relative overflow-hidden h-full flex flex-col ${isCurrentPlan ? "border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
497
523
  {product.metadata?.popular === 'true' && !isCurrentPlan && (
498
524
  <div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
499
525
  POPULAR
500
526
  </div>
501
527
  )}
502
- <CardHeader className="text-center pb-8 pt-6">
503
- <CardTitle className="text-xl font-semibold">{product.name}</CardTitle>
504
- <CardDescription className="mt-2">{product.description}</CardDescription>
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>
505
531
  </CardHeader>
506
- <CardContent className="text-center pb-8">
507
- <div className="mb-8">
532
+ <CardContent className="text-center pb-4 flex-1">
533
+ <div className="mb-4">
508
534
  <div className="flex items-baseline justify-center">
509
- <span className="text-5xl font-bold tracking-tight">
510
- {parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(0)}`}
535
+ <span className="text-3xl font-bold tracking-tight">
536
+ {parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(2).replace(/\.00$/, '')}`}
511
537
  </span>
512
538
  {parseFloat(price?.unit_amount) > 0 && (
513
- <span className="ml-1 text-lg text-muted-foreground">/month</span>
539
+ <span className="ml-1 text-sm text-muted-foreground">/month</span>
514
540
  )}
515
541
  </div>
516
- {isCurrentPlan && (
542
+ {isCurrentPlan ? (
517
543
  <Badge variant="secondary" className="mt-2">Current Plan</Badge>
544
+ ) : (
545
+ <div className="mt-2 h-6"></div>
518
546
  )}
519
547
  </div>
520
- <div className="space-y-4">
548
+ <div className="space-y-3 flex-1 flex flex-col justify-between min-h-[300px]">
521
549
  {features.map((feature: any) => (
522
- <div key={feature.id} className="border-t pt-4">
523
- <div className="text-2xl font-semibold text-foreground">
524
- {feature.value} {feature.units}
525
- </div>
526
- <div className="text-sm text-muted-foreground mt-1">
527
- {feature.description}
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>
528
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>
529
566
  </div>
530
567
  ))}
531
568
  </div>
532
569
  </CardContent>
533
- <CardFooter className="pt-4">
570
+ <CardFooter className="pt-3">
534
571
  <Button
535
572
  className="w-full"
536
573
  variant={isCurrentPlan ? "outline" : product.metadata?.popular === 'true' ? "default" : "secondary"}
@@ -624,6 +661,26 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
624
661
  onSuccess={handleSubscriptionCreated}
625
662
  createSubscription={handleCreateSubscription}
626
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>
627
684
  </>
628
685
  );
629
686
  }
@@ -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,22 @@ export default function SubscriptionConfirmModal({
45
46
  }).format(amount / 100);
46
47
  };
47
48
 
49
+ const formatFeatureValue = (value: any) => {
50
+ // Handle string boolean values
51
+ if (value === 'true' || value === true) {
52
+ return '✓'
53
+ }
54
+ if (value === 'false' || value === false) {
55
+ return '✗'
56
+ }
57
+ // Handle numeric values (both string and number)
58
+ const numValue = typeof value === 'string' ? parseFloat(value) : value
59
+ if (!isNaN(numValue) && numValue >= 1000) {
60
+ return new Intl.NumberFormat('en-US').format(numValue)
61
+ }
62
+ return value
63
+ };
64
+
48
65
  const handleConfirm = async () => {
49
66
  setIsSubscribing(true);
50
67
  setError(null);
@@ -120,7 +137,18 @@ export default function SubscriptionConfirmModal({
120
137
  {plan.features?.map((feature: any) => (
121
138
  <li key={feature.id} className="flex items-start text-sm">
122
139
  <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>
140
+ <TooltipProvider>
141
+ <Tooltip>
142
+ <TooltipTrigger asChild>
143
+ <span className="cursor-help">
144
+ {formatFeatureValue(feature.value)} <span className="text-sm text-muted-foreground font-normal">{feature.units}</span> - {feature.name || feature.description}
145
+ </span>
146
+ </TooltipTrigger>
147
+ <TooltipContent>
148
+ <p>{feature.description}</p>
149
+ </TooltipContent>
150
+ </Tooltip>
151
+ </TooltipProvider>
124
152
  </li>
125
153
  ))}
126
154
  </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 }
package/styles.css ADDED
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;