suna/frontend/src/components/billing/PlanComparison.tsx

235 lines
7.7 KiB
TypeScript
Raw Normal View History

2025-04-14 08:32:08 +08:00
'use client';
import { createClient } from "@/lib/supabase/client";
import { useEffect, useState } from "react";
2025-04-16 13:41:55 +08:00
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import { setupNewSubscription } from "@/lib/actions/billing";
import { SubmitButton } from "@/components/ui/submit-button";
import { Button } from "@/components/ui/button";
2025-04-14 08:32:08 +08:00
2025-04-14 08:47:15 +08:00
export const SUBSCRIPTION_PLANS = {
2025-04-14 08:32:08 +08:00
FREE: 'price_1RDQbOG6l1KZGqIrgrYzMbnL',
2025-04-16 13:41:55 +08:00
BASIC: 'price_1RC2PYG6l1KZGqIrpbzFB9Lp',
2025-04-14 08:32:08 +08:00
PRO: 'price_1RDQWqG6l1KZGqIrChli4Ys4'
} as const;
2025-04-16 13:41:55 +08:00
// Custom pricing data with cloud prices
const cloudPricingItems = [
{
name: "Free",
price: "$0",
description: "For individual use and exploration",
buttonText: "Get Started",
buttonColor: "bg-primary text-white",
isPopular: false,
hours: "1 hour",
features: [
"1 hour usage per month",
"Basic features",
"Community support",
"Single user",
"Standard response time",
"Public templates only",
],
},
{
name: "Basic",
price: "$10",
description: "For professionals and small teams",
buttonText: "Upgrade Now",
buttonColor: "bg-black text-white dark:bg-white dark:text-black",
isPopular: true,
hours: "10 hours",
features: [
"10 hours usage per month",
"Priority support",
"Advanced features",
"3 team members",
"Custom integrations",
"API access",
],
2025-04-14 08:32:08 +08:00
},
2025-04-16 13:41:55 +08:00
{
name: "Pro",
price: "$50",
description: "For organizations with complex needs",
buttonText: "Upgrade Now",
buttonColor: "bg-primary text-white",
isPopular: false,
hours: "100 hours",
features: [
"100 hours usage per month",
"Dedicated support",
"SSO & advanced security",
"10 team members",
"Service level agreement",
"Custom AI model training",
],
2025-04-14 08:32:08 +08:00
},
2025-04-16 13:41:55 +08:00
];
2025-04-14 08:32:08 +08:00
interface PlanComparisonProps {
accountId?: string | null;
returnUrl?: string;
2025-04-16 13:41:55 +08:00
isManaged?: boolean;
2025-04-14 08:32:08 +08:00
onPlanSelect?: (planId: string) => void;
className?: string;
}
2025-04-16 13:41:55 +08:00
// Price display animation component
const PriceDisplay = ({ tier }: { tier: typeof cloudPricingItems[number] }) => {
return (
<motion.span
key={tier.price}
className="text-4xl font-semibold"
initial={{
opacity: 0,
x: 10,
filter: "blur(5px)",
}}
animate={{ opacity: 1, x: 0, filter: "blur(0px)" }}
transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}
>
{tier.price}
</motion.span>
);
};
2025-04-14 08:32:08 +08:00
export function PlanComparison({
accountId,
returnUrl = typeof window !== 'undefined' ? window.location.href : '',
isManaged = true,
onPlanSelect,
className = ""
}: PlanComparisonProps) {
const [currentPlanId, setCurrentPlanId] = useState<string | undefined>();
useEffect(() => {
async function fetchCurrentPlan() {
if (accountId) {
const supabase = createClient();
const { data } = await supabase
.schema('basejump')
.from('billing_subscriptions')
.select('price_id')
.eq('account_id', accountId)
.eq('status', 'active')
.single();
2025-04-14 08:47:15 +08:00
setCurrentPlanId(data?.price_id || SUBSCRIPTION_PLANS.FREE);
} else {
setCurrentPlanId(SUBSCRIPTION_PLANS.FREE);
2025-04-14 08:32:08 +08:00
}
}
fetchCurrentPlan();
}, [accountId]);
return (
2025-04-16 13:41:55 +08:00
<div className={cn("grid min-[650px]:grid-cols-2 min-[900px]:grid-cols-3 gap-4 w-full max-w-6xl mx-auto", className)}>
{cloudPricingItems.map((tier) => {
const isCurrentPlan = currentPlanId === SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS];
2025-04-14 08:32:08 +08:00
return (
2025-04-16 13:41:55 +08:00
<div
key={tier.name}
className={cn(
"rounded-xl bg-background border border-border p-6 flex flex-col gap-6",
isCurrentPlan && "ring-2 ring-primary"
2025-04-14 08:32:08 +08:00
)}
2025-04-16 13:41:55 +08:00
>
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-lg font-medium">{tier.name}</h3>
{tier.isPopular && (
<span className="bg-primary text-primary-foreground text-xs font-medium px-2 py-0.5 rounded-full">
Popular
</span>
)}
{isCurrentPlan && (
<span className="bg-secondary text-secondary-foreground text-xs font-medium px-2 py-0.5 rounded-full">
Current Plan
</span>
)}
2025-04-14 08:32:08 +08:00
</div>
2025-04-16 13:41:55 +08:00
<div className="flex items-baseline">
<PriceDisplay tier={tier} />
<span className="text-muted-foreground ml-2">
{tier.price !== "$0" ? "/month" : ""}
</span>
2025-04-14 08:32:08 +08:00
</div>
2025-04-16 13:41:55 +08:00
<p className="text-muted-foreground">{tier.description}</p>
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium bg-secondary/10 text-secondary">
{tier.hours}/month
2025-04-14 08:32:08 +08:00
</div>
</div>
{!isCurrentPlan && accountId && (
2025-04-16 13:41:55 +08:00
<form className="mt-2">
2025-04-14 08:32:08 +08:00
<input type="hidden" name="accountId" value={accountId} />
<input type="hidden" name="returnUrl" value={returnUrl} />
2025-04-16 13:41:55 +08:00
<input type="hidden" name="planId" value={SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS]} />
2025-04-14 08:32:08 +08:00
{isManaged ? (
2025-04-16 13:41:55 +08:00
<SubmitButton
pendingText="Loading..."
2025-04-14 08:32:08 +08:00
formAction={setupNewSubscription}
2025-04-16 13:41:55 +08:00
className={cn(
"w-full h-10 rounded-full font-medium transition-colors",
tier.buttonColor
2025-04-14 08:32:08 +08:00
)}
2025-04-16 13:41:55 +08:00
>
{tier.buttonText}
2025-04-14 08:32:08 +08:00
</SubmitButton>
) : (
2025-04-16 13:41:55 +08:00
<Button
className={cn(
"w-full h-10 rounded-full font-medium transition-colors",
tier.buttonColor
2025-04-14 08:32:08 +08:00
)}
2025-04-16 13:41:55 +08:00
onClick={() => onPlanSelect?.(SUBSCRIPTION_PLANS[tier.name.toUpperCase() as keyof typeof SUBSCRIPTION_PLANS])}
>
{tier.buttonText}
2025-04-14 08:32:08 +08:00
</Button>
)}
</form>
)}
2025-04-16 13:41:55 +08:00
<div className="space-y-4">
{tier.name !== "Free" && (
<p className="text-sm text-muted-foreground">
Everything in {tier.name === "Basic" ? "Free" : "Basic"} +
</p>
)}
<ul className="space-y-3">
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-muted-foreground">
<div className="size-5 rounded-full bg-primary/10 flex items-center justify-center">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-primary"
>
<path
d="M2.5 6L5 8.5L9.5 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<span className="text-sm">{feature}</span>
</li>
))}
</ul>
</div>
2025-04-14 08:32:08 +08:00
</div>
);
})}
</div>
);
}