@omnikit-js/ui 0.3.0 → 0.3.1
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 +1 -1
- package/src/components/BillingClient.tsx +117 -37
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ 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
14
|
import PaymentMethodForm from './PaymentMethodForm';
|
|
15
15
|
import SubscriptionConfirmModal from './SubscriptionConfirmModal';
|
|
16
16
|
import { CardBrandIcon } from './ui/card-brand-icon';
|
|
@@ -84,21 +84,39 @@ async function createSubscription(userId: string, priceId: string, paymentMethod
|
|
|
84
84
|
},
|
|
85
85
|
body: JSON.stringify({
|
|
86
86
|
user_id: userId,
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
priceId: priceId,
|
|
88
|
+
paymentMethodId: paymentMethodId
|
|
89
89
|
})
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
|
|
94
|
+
// Check if the response indicates success
|
|
95
|
+
if (!response.ok || data.success === false) {
|
|
96
|
+
// Handle specific error types
|
|
97
|
+
let errorMessage = 'Failed to create subscription';
|
|
98
|
+
|
|
99
|
+
if (data.error === 'MISSING_PARAMETERS') {
|
|
100
|
+
errorMessage = data.message || 'Missing required parameters. Please check your information and try again.';
|
|
101
|
+
} else if (data.message) {
|
|
102
|
+
errorMessage = data.message;
|
|
103
|
+
} else if (data.error) {
|
|
104
|
+
errorMessage = data.error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: errorMessage,
|
|
110
|
+
errorType: data.error
|
|
111
|
+
};
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
return { success: true };
|
|
114
|
+
return { success: true, data };
|
|
98
115
|
} catch (error) {
|
|
99
116
|
return {
|
|
100
117
|
success: false,
|
|
101
|
-
error: error instanceof Error ? error.message : 'An error occurred'
|
|
118
|
+
error: error instanceof Error ? error.message : 'An error occurred while creating subscription',
|
|
119
|
+
errorType: 'NETWORK_ERROR'
|
|
102
120
|
};
|
|
103
121
|
}
|
|
104
122
|
}
|
|
@@ -380,11 +398,41 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
380
398
|
? Math.min((item.current_usage / item.limit) * 100, 100)
|
|
381
399
|
: 0;
|
|
382
400
|
|
|
401
|
+
const getUsageColor = (percent: number) => {
|
|
402
|
+
if (percent >= 90) return 'text-red-600 dark:text-red-400';
|
|
403
|
+
if (percent >= 75) return 'text-orange-600 dark:text-orange-400';
|
|
404
|
+
if (percent >= 50) return 'text-yellow-600 dark:text-yellow-400';
|
|
405
|
+
return 'text-green-600 dark:text-green-400';
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const getProgressColor = (percent: number) => {
|
|
409
|
+
if (percent >= 90) return 'bg-red-100 dark:bg-red-950';
|
|
410
|
+
if (percent >= 75) return 'bg-orange-100 dark:bg-orange-950';
|
|
411
|
+
if (percent >= 50) return 'bg-yellow-100 dark:bg-yellow-950';
|
|
412
|
+
return 'bg-green-100 dark:bg-green-950';
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const getStatusIcon = (percent: number, withinLimit: boolean) => {
|
|
416
|
+
if (!withinLimit || percent >= 90) {
|
|
417
|
+
return <AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400" />;
|
|
418
|
+
}
|
|
419
|
+
if (percent >= 75) {
|
|
420
|
+
return <AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />;
|
|
421
|
+
}
|
|
422
|
+
if (percent >= 50) {
|
|
423
|
+
return <Info className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />;
|
|
424
|
+
}
|
|
425
|
+
return <CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />;
|
|
426
|
+
};
|
|
427
|
+
|
|
383
428
|
return (
|
|
384
429
|
<div key={item.feature_key} className="space-y-3">
|
|
385
430
|
<div className="flex items-center justify-between text-sm">
|
|
386
|
-
<
|
|
387
|
-
|
|
431
|
+
<div className="flex items-center gap-2">
|
|
432
|
+
{typeof item.limit === 'number' && getStatusIcon(percentage, item.within_limit)}
|
|
433
|
+
<span>{item.feature_details.description}</span>
|
|
434
|
+
</div>
|
|
435
|
+
<span className={`font-medium ${typeof item.limit === 'number' ? getUsageColor(percentage) : ''}`}>
|
|
388
436
|
{typeof item.limit === 'number'
|
|
389
437
|
? `${item.current_usage.toLocaleString()} / ${item.limit.toLocaleString()}`
|
|
390
438
|
: item.current_usage
|
|
@@ -394,8 +442,8 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
394
442
|
</div>
|
|
395
443
|
{typeof item.limit === 'number' && (
|
|
396
444
|
<>
|
|
397
|
-
<Progress value={percentage} className={`h-2 ${!item.within_limit ? 'bg-
|
|
398
|
-
<p className=
|
|
445
|
+
<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'}`} />
|
|
446
|
+
<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
447
|
{item.within_limit
|
|
400
448
|
? `${Math.max(0, item.limit - item.current_usage).toLocaleString()} ${item.feature_details.units || ''} remaining`
|
|
401
449
|
: `${(item.current_usage - item.limit).toLocaleString()} ${item.feature_details.units || ''} over limit`
|
|
@@ -417,48 +465,80 @@ export default function BillingClient({ customerData, pricingData, paymentMethod
|
|
|
417
465
|
</TabsContent>
|
|
418
466
|
|
|
419
467
|
<TabsContent value="plans" className="space-y-4">
|
|
420
|
-
<div className="grid gap-
|
|
468
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
421
469
|
{pricingData?.products?.map((product: any) => {
|
|
422
470
|
const price = product.prices?.[0];
|
|
423
471
|
const features = product.features || [];
|
|
424
472
|
const isCurrentPlan = currentPlanItem?.price_id === price?.id;
|
|
425
473
|
|
|
474
|
+
// Determine if this is an upgrade, downgrade, or switch to free
|
|
475
|
+
const getButtonText = () => {
|
|
476
|
+
if (isCurrentPlan) return "Current Plan";
|
|
477
|
+
|
|
478
|
+
const targetPrice = parseFloat(price?.unit_amount || 0);
|
|
479
|
+
const currentPlanPrice = parseFloat(currentPrice?.unit_amount || 0);
|
|
480
|
+
|
|
481
|
+
// If target is free plan
|
|
482
|
+
if (targetPrice === 0) {
|
|
483
|
+
return currentPlanPrice > 0 ? "Switch to Free" : "Get Started";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// If current plan is free
|
|
487
|
+
if (currentPlanPrice === 0) {
|
|
488
|
+
return "Upgrade";
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Compare prices for paid plans
|
|
492
|
+
return targetPrice > currentPlanPrice ? "Upgrade" : "Downgrade";
|
|
493
|
+
};
|
|
494
|
+
|
|
426
495
|
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
|
-
)}
|
|
496
|
+
<Card key={product.id} className={`relative overflow-hidden ${isCurrentPlan ? "border-primary shadow-md" : "border-muted"} ${product.metadata?.popular === 'true' && !isCurrentPlan ? "border-primary/50" : ""}`}>
|
|
497
|
+
{product.metadata?.popular === 'true' && !isCurrentPlan && (
|
|
498
|
+
<div className="absolute top-0 right-0 bg-primary text-primary-foreground text-xs font-medium px-3 py-1 rounded-bl-lg">
|
|
499
|
+
POPULAR
|
|
435
500
|
</div>
|
|
436
|
-
|
|
501
|
+
)}
|
|
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>
|
|
437
505
|
</CardHeader>
|
|
438
|
-
<CardContent>
|
|
439
|
-
<div className="
|
|
440
|
-
|
|
441
|
-
|
|
506
|
+
<CardContent className="text-center pb-8">
|
|
507
|
+
<div className="mb-8">
|
|
508
|
+
<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)}`}
|
|
511
|
+
</span>
|
|
512
|
+
{parseFloat(price?.unit_amount) > 0 && (
|
|
513
|
+
<span className="ml-1 text-lg text-muted-foreground">/month</span>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
{isCurrentPlan && (
|
|
517
|
+
<Badge variant="secondary" className="mt-2">Current Plan</Badge>
|
|
518
|
+
)}
|
|
442
519
|
</div>
|
|
443
|
-
<
|
|
520
|
+
<div className="space-y-4">
|
|
444
521
|
{features.map((feature: any) => (
|
|
445
|
-
<
|
|
446
|
-
<
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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}
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
451
530
|
))}
|
|
452
|
-
</
|
|
531
|
+
</div>
|
|
453
532
|
</CardContent>
|
|
454
|
-
<CardFooter>
|
|
533
|
+
<CardFooter className="pt-4">
|
|
455
534
|
<Button
|
|
456
535
|
className="w-full"
|
|
457
|
-
variant={isCurrentPlan ? "outline" : "default"}
|
|
536
|
+
variant={isCurrentPlan ? "outline" : product.metadata?.popular === 'true' ? "default" : "secondary"}
|
|
537
|
+
size="lg"
|
|
458
538
|
disabled={isCurrentPlan}
|
|
459
539
|
onClick={() => !isCurrentPlan && handleSubscribeClick(product)}
|
|
460
540
|
>
|
|
461
|
-
{
|
|
541
|
+
{getButtonText()}
|
|
462
542
|
</Button>
|
|
463
543
|
</CardFooter>
|
|
464
544
|
</Card>
|