Skip to content

Commit ca51e75

Browse files
committed
Refactor subscription components and update badge styles for improved clarity
- Updated import path for PaidPlan component to reflect new directory structure. - Changed badge variant from "green" to "success" in SiteConfiguration component for consistency with updated styling. - Removed deprecated PaidPlan component, streamlining subscription management. - Enhanced badge styles in the UI to improve visual consistency across components.
1 parent 11a73e2 commit ca51e75

5 files changed

Lines changed: 209 additions & 23 deletions

File tree

client/src/app/settings/organization/subscription/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Card, CardContent, CardDescription, CardTitle } from "@/components/ui/card";
44
import { Skeleton } from "@/components/ui/skeleton";
5-
import { PaidPlan } from "../../../../components/subscription/PaidPlan";
5+
import { PaidPlan } from "../../../../components/subscription/PaidPlain/PaidPlan";
66
import { useStripeSubscription } from "../../../../lib/subscription/useStripeSubscription";
77
import { NoOrganization } from "../../../../components/NoOrganization";
88
import { TrialPlan } from "../../../../components/subscription/TrialPlan";

client/src/components/SiteSettings/SiteConfiguration.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export function SiteConfiguration({ siteMetadata, disabled = false, onClose }: S
184184
enabledMessage: "Session replay enabled",
185185
disabledMessage: "Session replay disabled",
186186
disabled: sessionReplayDisabled,
187-
badge: <Badge variant="green">Pro</Badge>,
187+
badge: <Badge variant="success">Pro</Badge>,
188188
},
189189
{
190190
id: "webVitals",
@@ -195,7 +195,7 @@ export function SiteConfiguration({ siteMetadata, disabled = false, onClose }: S
195195
enabledMessage: "Web Vitals enabled",
196196
disabledMessage: "Web Vitals disabled",
197197
disabled: webVitalsDisabled,
198-
badge: <Badge variant="green">Standard</Badge>,
198+
badge: <Badge variant="success">Standard</Badge>,
199199
},
200200
{
201201
id: "trackErrors",
@@ -206,7 +206,7 @@ export function SiteConfiguration({ siteMetadata, disabled = false, onClose }: S
206206
enabledMessage: "Error tracking enabled",
207207
disabledMessage: "Error tracking disabled",
208208
disabled: trackErrorsDisabled,
209-
badge: <Badge variant="green">Standard</Badge>,
209+
badge: <Badge variant="success">Standard</Badge>,
210210
},
211211
{
212212
id: "trackOutbound",

client/src/components/subscription/PaidPlan.tsx renamed to client/src/components/subscription/PaidPlain/PaidPlan.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import { Shield } from "lucide-react";
22
import { useState } from "react";
33
import { toast } from "sonner";
44
import { DateTime } from "luxon";
5-
import { Alert } from "../ui/alert";
6-
import { Button } from "../ui/button";
7-
import { Card, CardContent } from "../ui/card";
8-
import { Progress } from "../ui/progress";
9-
import { BACKEND_URL } from "../../lib/const";
10-
import { getStripePrices } from "../../lib/stripe";
11-
import { formatDate } from "../../lib/subscription/planUtils";
12-
import { useStripeSubscription } from "../../lib/subscription/useStripeSubscription";
13-
import { UsageChart } from "../UsageChart";
5+
import { Alert } from "../../ui/alert";
6+
import { Button } from "../../ui/button";
7+
import { Card, CardContent } from "../../ui/card";
8+
import { Progress } from "../../ui/progress";
9+
import { BACKEND_URL } from "../../../lib/const";
10+
import { getStripePrices } from "../../../lib/stripe";
11+
import { formatDate } from "../../../lib/subscription/planUtils";
12+
import { useStripeSubscription } from "../../../lib/subscription/useStripeSubscription";
13+
import { UsageChart } from "../../UsageChart";
1414
import { authClient } from "@/lib/auth";
15+
import { PlanDialog } from "./PlanDialog";
1516

1617
export function PaidPlan() {
1718
const { data: activeSubscription, isLoading, error: subscriptionError, refetch } = useStripeSubscription();
@@ -27,6 +28,7 @@ export function PaidPlan() {
2728

2829
const [isProcessing, setIsProcessing] = useState(false);
2930
const [actionError, setActionError] = useState<string | null>(null);
31+
const [showPlanDialog, setShowPlanDialog] = useState(false);
3032

3133
const eventLimit = activeSubscription?.eventLimit || 0;
3234
const currentUsage = activeSubscription?.monthlyEventCount || 0;
@@ -102,7 +104,7 @@ export function PaidPlan() {
102104
}
103105
};
104106

105-
const handleChangePlan = () => createPortalSession("subscription_update");
107+
const handleChangePlan = () => setShowPlanDialog(true);
106108
const handleViewSubscription = () => createPortalSession();
107109
const handleCancelSubscription = () => createPortalSession("subscription_cancel");
108110

@@ -131,6 +133,11 @@ export function PaidPlan() {
131133
return (
132134
<div className="space-y-6">
133135
{actionError && <Alert variant="destructive">{actionError}</Alert>}
136+
<PlanDialog
137+
open={showPlanDialog}
138+
onOpenChange={setShowPlanDialog}
139+
currentPlanName={activeSubscription?.planName}
140+
/>
134141
<Card>
135142
<CardContent>
136143
<div className="space-y-6 mt-3 p-2">
@@ -146,8 +153,8 @@ export function PaidPlan() {
146153
<p className="text-neutral-400 text-sm">{formatRenewalDate()}</p>
147154
</div>
148155
<div className="space-x-2">
149-
<Button variant="success" onClick={handleChangePlan} disabled={isProcessing}>
150-
{isProcessing ? "Processing..." : "Change Plan"}
156+
<Button variant="success" onClick={handleChangePlan}>
157+
Change Plan
151158
</Button>
152159
<Button variant="outline" onClick={handleViewSubscription} disabled={isProcessing}>
153160
View Details
@@ -174,7 +181,7 @@ export function PaidPlan() {
174181
<p className="text-sm text-amber-700 dark:text-amber-300">
175182
<strong>Usage limit reached!</strong> You've exceeded your plan's event limit.
176183
</p>
177-
<Button variant="success" size="sm" onClick={handleChangePlan} disabled={isProcessing}>
184+
<Button variant="success" size="sm" onClick={handleChangePlan}>
178185
Upgrade Plan
179186
</Button>
180187
</div>
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
2+
import { authClient } from "@/lib/auth";
3+
import { BACKEND_URL } from "@/lib/const";
4+
import { getStripePrices } from "@/lib/stripe";
5+
import { cn } from "@/lib/utils";
6+
import { Loader2 } from "lucide-react";
7+
import { useState } from "react";
8+
import { toast } from "sonner";
9+
import { Badge } from "../../ui/badge";
10+
11+
interface PlanDialogProps {
12+
open: boolean;
13+
onOpenChange: (open: boolean) => void;
14+
currentPlanName?: string;
15+
}
16+
17+
const EVENT_TIERS = [
18+
{ events: 100_000, label: "100k" },
19+
{ events: 250_000, label: "250k" },
20+
{ events: 500_000, label: "500k" },
21+
{ events: 1_000_000, label: "1M" },
22+
{ events: 2_000_000, label: "2M" },
23+
{ events: 5_000_000, label: "5M" },
24+
{ events: 10_000_000, label: "10M" },
25+
];
26+
27+
export function PlanDialog({ open, onOpenChange, currentPlanName }: PlanDialogProps) {
28+
const [isAnnual, setIsAnnual] = useState(false);
29+
const [loadingPriceId, setLoadingPriceId] = useState<string | null>(null);
30+
const stripePrices = getStripePrices();
31+
const { data: activeOrg } = authClient.useActiveOrganization();
32+
33+
const handleCheckout = async (priceId: string, planName: string) => {
34+
if (!activeOrg) {
35+
toast.error("Please select an organization");
36+
return;
37+
}
38+
39+
setLoadingPriceId(priceId);
40+
try {
41+
const baseUrl = window.location.origin;
42+
const successUrl = `${baseUrl}/settings/organization/subscription?session_id={CHECKOUT_SESSION_ID}`;
43+
const cancelUrl = `${baseUrl}/settings/organization/subscription`;
44+
45+
const response = await fetch(`${BACKEND_URL}/stripe/create-checkout-session`, {
46+
method: "POST",
47+
headers: {
48+
"Content-Type": "application/json",
49+
},
50+
credentials: "include",
51+
body: JSON.stringify({
52+
priceId,
53+
successUrl,
54+
cancelUrl,
55+
organizationId: activeOrg.id,
56+
}),
57+
});
58+
59+
const data = await response.json();
60+
61+
if (!response.ok) {
62+
throw new Error(data.error || "Failed to create checkout session.");
63+
}
64+
65+
if (data.checkoutUrl) {
66+
window.location.href = data.checkoutUrl;
67+
} else {
68+
throw new Error("Checkout URL not received.");
69+
}
70+
} catch (error: any) {
71+
toast.error(`Checkout failed: ${error.message}`);
72+
setLoadingPriceId(null);
73+
}
74+
};
75+
76+
const getPriceForTier = (events: number, planType: "standard" | "pro") => {
77+
const suffix = isAnnual ? "-annual" : "";
78+
const planName = `${planType}${events >= 1_000_000 ? events / 1_000_000 + "m" : events / 1_000 + "k"}${suffix}`;
79+
const plan = stripePrices.find(p => p.name === planName);
80+
return plan;
81+
};
82+
83+
const isCurrentPlan = (planName: string) => {
84+
return currentPlanName === planName;
85+
};
86+
87+
return (
88+
<Dialog open={open} onOpenChange={onOpenChange}>
89+
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
90+
<DialogHeader>
91+
<DialogTitle>Choose Your Plan</DialogTitle>
92+
</DialogHeader>
93+
94+
{/* Billing toggle */}
95+
<div className="flex justify-center mb-0">
96+
<div className="flex gap-3 text-sm">
97+
<button
98+
onClick={() => setIsAnnual(false)}
99+
className={cn(
100+
"px-4 py-2 rounded-full transition-colors cursor-pointer",
101+
!isAnnual ? "bg-emerald-500/20 text-emerald-400 font-medium" : "text-neutral-400 hover:text-neutral-200"
102+
)}
103+
>
104+
Monthly
105+
</button>
106+
<button
107+
onClick={() => setIsAnnual(true)}
108+
className={cn(
109+
"px-4 py-2 rounded-full transition-colors cursor-pointer",
110+
isAnnual ? "bg-emerald-500/20 text-emerald-400 font-medium" : "text-neutral-400 hover:text-neutral-200"
111+
)}
112+
>
113+
Annual
114+
<span className="ml-1 text-xs text-emerald-500">-17%</span>
115+
</button>
116+
</div>
117+
</div>
118+
119+
{/* Plan columns */}
120+
<div className="grid grid-cols-2 gap-6">
121+
{[
122+
{ type: "standard" as const, title: "Standard", subtitle: "Core analytics features" },
123+
{ type: "pro" as const, title: "Pro", subtitle: "Advanced features + session replays" },
124+
].map(({ type, title, subtitle }) => (
125+
<div key={type} className="space-y-3">
126+
<div className="text-center mb-4">
127+
<h3 className="text-xl font-bold">{title}</h3>
128+
<p className="text-sm text-neutral-400">{subtitle}</p>
129+
</div>
130+
<div className="space-y-2">
131+
{EVENT_TIERS.map(tier => {
132+
const plan = getPriceForTier(tier.events, type);
133+
if (!plan) return null;
134+
const isCurrent = isCurrentPlan(plan.name);
135+
const isLoading = loadingPriceId === plan.priceId;
136+
137+
return (
138+
<div
139+
key={plan.name}
140+
className={cn(
141+
"flex flex-col gap-2 justify-between p-3 rounded-lg border cursor-pointer",
142+
isCurrent
143+
? "bg-emerald-500/10 border-emerald-500"
144+
: "bg-neutral-800/20 border-neutral-700/50 hover:bg-neutral-800/30",
145+
isLoading && "opacity-50"
146+
)}
147+
onClick={() => handleCheckout(plan.priceId, plan.name)}
148+
>
149+
<div className="flex items-center justify-between w-full">
150+
<Badge variant="success">{type === "pro" ? "Pro" : "Standard"}</Badge>
151+
<div className="text-xs text-neutral-400">
152+
<span className="text-neutral-200 font-semibold text-base">
153+
${isAnnual ? Math.round(plan.price / 12) : plan.price}
154+
</span>{" "}
155+
/ month
156+
</div>
157+
</div>
158+
<div className="text-neutral-100 font-medium flex items-center gap-2">
159+
{tier.label} events <span className="text-neutral-400 text-xs font-normal">/ month</span>
160+
{isLoading && <Loader2 className="w-4 h-4 animate-spin" />}
161+
</div>
162+
</div>
163+
);
164+
})}
165+
</div>
166+
</div>
167+
))}
168+
</div>
169+
170+
{/* Features comparison link */}
171+
<div className="mt-3 text-center">
172+
<a
173+
href="https://www.rybbit.io/pricing"
174+
target="_blank"
175+
rel="noopener noreferrer"
176+
className="text-neutral-200 hover:text-neutral-100 text-sm underline"
177+
>
178+
View detailed feature comparison →
179+
</a>
180+
</div>
181+
</DialogContent>
182+
</Dialog>
183+
);
184+
}

client/src/components/ui/badge.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,12 @@ const badgeVariants = cva(
1717
warning:
1818
"border-transparent bg-yellow-500/10 text-yellow-700 hover:bg-yellow-500/20 dark:bg-yellow-500/20 dark:text-yellow-400 dark:hover:bg-yellow-500/30",
1919
success:
20-
"border-transparent bg-green-500/10 text-green-700 hover:bg-green-500/20 dark:bg-green-500/20 dark:text-green-400 dark:hover:bg-green-500/30",
20+
"border-transparent bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 dark:bg-emerald-500/20 dark:text-emerald-400 dark:hover:bg-emerald-500/30",
2121
info: "border-transparent bg-blue-500/10 text-blue-700 hover:bg-blue-500/20 dark:bg-blue-500/20 dark:text-blue-400 dark:hover:bg-blue-500/30",
2222
outline:
2323
"border-neutral-300 bg-transparent text-neutral-700 hover:bg-neutral-50 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800",
2424
ghost:
2525
"border-transparent bg-transparent text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800",
26-
// Legacy variants for backwards compatibility
27-
green:
28-
"border-transparent bg-green-500/10 text-green-700 hover:bg-green-500/20 dark:bg-green-500/20 dark:text-green-400 dark:hover:bg-green-500/30",
29-
red: "border-transparent bg-red-500/10 text-red-600 hover:bg-red-500/20 dark:bg-red-500/20 dark:text-red-400 dark:hover:bg-red-500/30",
30-
minimal: "border-transparent bg-neutral-200/50 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400",
3126
},
3227
},
3328
defaultVariants: {

0 commit comments

Comments
 (0)