@omnikit-js/ui 0.3.2 → 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 +25 -0
- package/package.json +3 -2
- package/src/components/BillingClient.tsx +60 -23
- package/src/components/SubscriptionConfirmModal.tsx +11 -5
- package/styles.css +3 -0
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.
|
|
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",
|
|
@@ -49,7 +49,8 @@ async function setDefaultPaymentMethod(paymentMethodId: string, apiUrl: string,
|
|
|
49
49
|
headers: {
|
|
50
50
|
'x-api-key': apiKey,
|
|
51
51
|
'Content-Type': 'application/json'
|
|
52
|
-
}
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({})
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
if (!response.ok) {
|
|
@@ -65,7 +66,8 @@ async function deletePaymentMethod(paymentMethodId: string, apiUrl: string, apiK
|
|
|
65
66
|
headers: {
|
|
66
67
|
'x-api-key': apiKey,
|
|
67
68
|
'Content-Type': 'application/json'
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({})
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
if (!response.ok) {
|
|
@@ -145,16 +147,24 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
145
147
|
const [isLoading, setIsLoading] = useState(false);
|
|
146
148
|
const [addPaymentModalOpen, setAddPaymentModalOpen] = useState(false);
|
|
147
149
|
const [subscribeModalOpen, setSubscribeModalOpen] = useState(false);
|
|
150
|
+
const [deletePaymentModalOpen, setDeletePaymentModalOpen] = useState(false);
|
|
148
151
|
const [selectedPlan, setSelectedPlan] = useState<any>(null);
|
|
152
|
+
const [selectedPaymentMethodId, setSelectedPaymentMethodId] = useState<string | null>(null);
|
|
149
153
|
const [activeTab, setActiveTab] = useState('overview');
|
|
150
154
|
const router = useRouter();
|
|
151
155
|
|
|
152
156
|
const formatFeatureValue = (value: any) => {
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
// Handle string boolean values
|
|
158
|
+
if (value === 'true' || value === true) {
|
|
159
|
+
return '✓'
|
|
160
|
+
}
|
|
161
|
+
if (value === 'false' || value === false) {
|
|
162
|
+
return '✗'
|
|
155
163
|
}
|
|
156
|
-
|
|
157
|
-
|
|
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)
|
|
158
168
|
}
|
|
159
169
|
return value
|
|
160
170
|
}
|
|
@@ -217,19 +227,24 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
217
227
|
}
|
|
218
228
|
};
|
|
219
229
|
|
|
220
|
-
const handleDeletePayment =
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
230
|
+
const handleDeletePayment = (paymentMethodId: string) => {
|
|
231
|
+
setSelectedPaymentMethodId(paymentMethodId);
|
|
232
|
+
setDeletePaymentModalOpen(true);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const confirmDeletePayment = async () => {
|
|
236
|
+
if (!selectedPaymentMethodId) return;
|
|
237
|
+
|
|
226
238
|
try {
|
|
227
|
-
|
|
239
|
+
setIsLoading(true);
|
|
240
|
+
await deletePaymentMethod(selectedPaymentMethodId, apiUrl, apiKey);
|
|
228
241
|
router.refresh();
|
|
229
242
|
} catch (error) {
|
|
230
|
-
console.error('
|
|
243
|
+
console.error('Error deleting payment method:', error);
|
|
231
244
|
} finally {
|
|
232
245
|
setIsLoading(false);
|
|
246
|
+
setDeletePaymentModalOpen(false);
|
|
247
|
+
setSelectedPaymentMethodId(null);
|
|
233
248
|
}
|
|
234
249
|
};
|
|
235
250
|
|
|
@@ -404,7 +419,7 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
404
419
|
<CardDescription>Your resource usage for the current billing period</CardDescription>
|
|
405
420
|
</CardHeader>
|
|
406
421
|
<CardContent className="space-y-6">
|
|
407
|
-
{usage?.data?.map((item: any) => {
|
|
422
|
+
{usage?.data?.filter((item: any) => typeof item.limit === 'number' && item.limit > 0).map((item: any) => {
|
|
408
423
|
const percentage = typeof item.limit === 'number'
|
|
409
424
|
? Math.min((item.current_usage / item.limit) * 100, 100)
|
|
410
425
|
: 0;
|
|
@@ -504,7 +519,7 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
504
519
|
};
|
|
505
520
|
|
|
506
521
|
return (
|
|
507
|
-
<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" : ""}`}>
|
|
508
523
|
{product.metadata?.popular === 'true' && !isCurrentPlan && (
|
|
509
524
|
<div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
|
|
510
525
|
POPULAR
|
|
@@ -514,30 +529,32 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
514
529
|
<CardTitle className="text-lg font-semibold">{product.name}</CardTitle>
|
|
515
530
|
<CardDescription className="mt-1 text-sm">{product.description}</CardDescription>
|
|
516
531
|
</CardHeader>
|
|
517
|
-
<CardContent className="text-center pb-4">
|
|
532
|
+
<CardContent className="text-center pb-4 flex-1">
|
|
518
533
|
<div className="mb-4">
|
|
519
534
|
<div className="flex items-baseline justify-center">
|
|
520
535
|
<span className="text-3xl font-bold tracking-tight">
|
|
521
|
-
{parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(
|
|
536
|
+
{parseFloat(price?.unit_amount) === 0 ? 'Free' : `$${parseFloat(price?.unit_amount).toFixed(2).replace(/\.00$/, '')}`}
|
|
522
537
|
</span>
|
|
523
538
|
{parseFloat(price?.unit_amount) > 0 && (
|
|
524
539
|
<span className="ml-1 text-sm text-muted-foreground">/month</span>
|
|
525
540
|
)}
|
|
526
541
|
</div>
|
|
527
|
-
{isCurrentPlan
|
|
542
|
+
{isCurrentPlan ? (
|
|
528
543
|
<Badge variant="secondary" className="mt-2">Current Plan</Badge>
|
|
544
|
+
) : (
|
|
545
|
+
<div className="mt-2 h-6"></div>
|
|
529
546
|
)}
|
|
530
547
|
</div>
|
|
531
|
-
<div className="space-y-
|
|
548
|
+
<div className="space-y-3 flex-1 flex flex-col justify-between min-h-[300px]">
|
|
532
549
|
{features.map((feature: any) => (
|
|
533
|
-
<div key={feature.id} className="border-t pt-
|
|
550
|
+
<div key={feature.id} className="border-t pt-3 first:border-t-0 first:pt-0 flex-1 flex flex-col justify-center">
|
|
534
551
|
<div className="text-lg font-semibold text-foreground">
|
|
535
|
-
{formatFeatureValue(feature.value)} {feature.units}
|
|
552
|
+
{formatFeatureValue(feature.value)} <span className="text-sm text-gray-500 font-normal">{feature.units}</span>
|
|
536
553
|
</div>
|
|
537
554
|
<TooltipProvider>
|
|
538
555
|
<Tooltip>
|
|
539
556
|
<TooltipTrigger asChild>
|
|
540
|
-
<div className="text-xs text-
|
|
557
|
+
<div className="text-xs text-gray-500 mt-1 cursor-help">
|
|
541
558
|
{feature.name || feature.description}
|
|
542
559
|
</div>
|
|
543
560
|
</TooltipTrigger>
|
|
@@ -644,6 +661,26 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
644
661
|
onSuccess={handleSubscriptionCreated}
|
|
645
662
|
createSubscription={handleCreateSubscription}
|
|
646
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>
|
|
647
684
|
</>
|
|
648
685
|
);
|
|
649
686
|
}
|
|
@@ -47,11 +47,17 @@ export default function SubscriptionConfirmModal({
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const formatFeatureValue = (value: any) => {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Handle string boolean values
|
|
51
|
+
if (value === 'true' || value === true) {
|
|
52
|
+
return '✓'
|
|
52
53
|
}
|
|
53
|
-
if (
|
|
54
|
-
return
|
|
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)
|
|
55
61
|
}
|
|
56
62
|
return value
|
|
57
63
|
};
|
|
@@ -135,7 +141,7 @@ export default function SubscriptionConfirmModal({
|
|
|
135
141
|
<Tooltip>
|
|
136
142
|
<TooltipTrigger asChild>
|
|
137
143
|
<span className="cursor-help">
|
|
138
|
-
{formatFeatureValue(feature.value)} {feature.units} - {feature.name || feature.description}
|
|
144
|
+
{formatFeatureValue(feature.value)} <span className="text-sm text-muted-foreground font-normal">{feature.units}</span> - {feature.name || feature.description}
|
|
139
145
|
</span>
|
|
140
146
|
</TooltipTrigger>
|
|
141
147
|
<TooltipContent>
|
package/styles.css
ADDED