@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnikit-js/ui",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",
@@ -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
- price_id: priceId,
88
- payment_method_id: paymentMethodId
87
+ priceId: priceId,
88
+ paymentMethodId: paymentMethodId
89
89
  })
90
90
  });
91
91
 
92
- if (!response.ok) {
93
- const errorData = await response.json();
94
- throw new Error(errorData.error || 'Failed to create subscription');
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
- <span>{item.feature_details.description}</span>
387
- <span className="font-medium">
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-destructive' : ''}`} />
398
- <p className="text-xs text-muted-foreground">
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-4 md:grid-cols-3">
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
- <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
- )}
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
- <CardDescription>{product.description}</CardDescription>
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="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>}
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
- <ul className="space-y-2">
520
+ <div className="space-y-4">
444
521
  {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>
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
- </ul>
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
- {isCurrentPlan ? "Current Plan" : parseFloat(price?.unit_amount) === 0 ? "Get Started" : "Upgrade"}
541
+ {getButtonText()}
462
542
  </Button>
463
543
  </CardFooter>
464
544
  </Card>