refactor(pricing): update pricing section layout and simplify pricing tier logic; add hidden property for conditional display

This commit is contained in:
sharath 2025-06-25 22:36:56 +00:00
parent 4d4faca12a
commit 29c9fa78db
No known key found for this signature in database
4 changed files with 152 additions and 269 deletions

View File

@ -24,7 +24,9 @@ export default function Home() {
{/* <FeatureSection /> */}
{/* <GrowthSection /> */}
<OpenSourceSection />
<PricingSection />
<div className='flex flex-col items-center px-4'>
<PricingSection />
</div>
{/* <TestimonialSection /> */}
{/* <FAQSection /> */}
<CTASection />

View File

@ -141,8 +141,9 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
</div>
{/* Plans Comparison */}
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} />
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} insideDialog={true} />
<div className="mt-20"></div>
{/* Manage Subscription Button */}
<Button
onClick={handleManageSubscription}
@ -178,7 +179,7 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
</div>
{/* Plans Comparison */}
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} />
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} insideDialog={true} />
{/* Manage Subscription Button */}
<Button

View File

@ -5,16 +5,8 @@ import type { PricingTier } from '@/lib/home';
import { siteConfig } from '@/lib/home';
import { cn } from '@/lib/utils';
import { motion } from 'motion/react';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import { CheckIcon } from 'lucide-react';
import Link from 'next/link';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import {
getSubscription,
@ -26,7 +18,6 @@ import { toast } from 'sonner';
import { isLocalMode } from '@/lib/config';
// Constants
const DEFAULT_SELECTED_PLAN = '6 hours';
export const SUBSCRIPTION_PLANS = {
FREE: 'free',
PRO: 'base',
@ -53,16 +44,6 @@ interface PriceDisplayProps {
isCompact?: boolean;
}
interface CustomPriceDisplayProps {
price: string;
}
interface UpgradePlan {
hours: string;
price: string;
stripePriceId: string;
}
interface PricingTierProps {
tier: PricingTier;
isCompact?: boolean;
@ -142,24 +123,6 @@ function PriceDisplay({ price, isCompact }: PriceDisplayProps) {
);
}
function CustomPriceDisplay({ price }: CustomPriceDisplayProps) {
return (
<motion.span
key={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] }}
>
{price}
</motion.span>
);
}
function PricingTier({
tier,
isCompact = false,
@ -173,37 +136,7 @@ function PricingTier({
returnUrl,
insideDialog = false,
}: PricingTierProps) {
const [localSelectedPlan, setLocalSelectedPlan] = useState(
selectedPlan || DEFAULT_SELECTED_PLAN,
);
const hasInitialized = useRef(false);
// Auto-select the correct plan only on initial load
useEffect(() => {
if (
!hasInitialized.current &&
tier.name === 'Custom' &&
tier.upgradePlans &&
currentSubscription?.price_id
) {
const matchingPlan = tier.upgradePlans.find(
(plan) => plan.stripePriceId === currentSubscription.price_id,
);
if (matchingPlan) {
setLocalSelectedPlan(matchingPlan.hours);
}
hasInitialized.current = true;
}
}, [currentSubscription, tier.name, tier.upgradePlans]);
// Only refetch when plan is selected
const handlePlanSelect = (value: string) => {
setLocalSelectedPlan(value);
if (tier.name === 'Custom' && onSubscriptionUpdate) {
onSubscriptionUpdate();
}
};
// Auto-select the correct plan only on initial load - simplified since no more Custom tier
const handleSubscribe = async (planStripePriceId: string) => {
if (!isAuthenticated) {
window.location.href = '/auth';
@ -215,22 +148,11 @@ function PricingTier({
}
try {
// For custom tier, get the selected plan's stripePriceId
let finalPriceId = planStripePriceId;
if (tier.name === 'Custom' && tier.upgradePlans) {
const selectedPlan = tier.upgradePlans.find(
(plan) => plan.hours === localSelectedPlan,
);
if (selectedPlan?.stripePriceId) {
finalPriceId = selectedPlan.stripePriceId;
}
}
onPlanSelect?.(finalPriceId);
onPlanSelect?.(planStripePriceId);
const response: CreateCheckoutSessionResponse =
await createCheckoutSession({
price_id: finalPriceId,
price_id: planStripePriceId,
success_url: returnUrl,
cancel_url: returnUrl,
});
@ -295,76 +217,12 @@ function PricingTier({
}
};
const getPriceValue = (
tier: (typeof siteConfig.cloudPricingItems)[0],
selectedHours?: string,
): string => {
if (tier.upgradePlans && selectedHours) {
const plan = tier.upgradePlans.find(
(plan) => plan.hours === selectedHours,
);
if (plan) {
return plan.price;
}
}
return tier.price;
};
const getDisplayedHours = (
tier: (typeof siteConfig.cloudPricingItems)[0],
) => {
if (tier.name === 'Custom' && localSelectedPlan) {
return localSelectedPlan;
}
return tier.hours;
};
const getSelectedPlanPriceId = (
tier: (typeof siteConfig.cloudPricingItems)[0],
): string => {
if (tier.name === 'Custom' && tier.upgradePlans) {
const selectedPlan = tier.upgradePlans.find(
(plan) => plan.hours === localSelectedPlan,
);
return selectedPlan?.stripePriceId || tier.stripePriceId;
}
return tier.stripePriceId;
};
const getSelectedPlanPrice = (
tier: (typeof siteConfig.cloudPricingItems)[0],
): string => {
if (tier.name === 'Custom' && tier.upgradePlans) {
const selectedPlan = tier.upgradePlans.find(
(plan) => plan.hours === localSelectedPlan,
);
return selectedPlan?.price || tier.price;
}
return tier.price;
};
const tierPriceId = getSelectedPlanPriceId(tier);
const tierPriceId = tier.stripePriceId;
const isCurrentActivePlan =
isAuthenticated &&
// For custom tier, check if the selected plan matches the current subscription
(tier.name === 'Custom'
? tier.upgradePlans?.some(
(plan) =>
plan.hours === localSelectedPlan &&
plan.stripePriceId === currentSubscription?.price_id,
)
: currentSubscription?.price_id === tierPriceId);
isAuthenticated && currentSubscription?.price_id === tierPriceId;
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
const isScheduledTargetPlan =
isScheduled &&
// For custom tier, check if the selected plan matches the scheduled subscription
(tier.name === 'Custom'
? tier.upgradePlans?.some(
(plan) =>
plan.hours === localSelectedPlan &&
plan.stripePriceId === currentSubscription?.scheduled_price_id,
)
: currentSubscription?.scheduled_price_id === tierPriceId);
isScheduled && currentSubscription?.scheduled_price_id === tierPriceId;
const isPlanLoading = isLoading[tierPriceId];
let buttonText = isAuthenticated ? 'Select Plan' : 'Try Free';
@ -411,41 +269,15 @@ function PricingTier({
</span>
);
} else {
// For custom tier, find the current plan in upgradePlans
const currentTier =
tier.name === 'Custom' && tier.upgradePlans
? tier.upgradePlans.find(
(p) => p.stripePriceId === currentSubscription?.price_id,
)
: siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription?.price_id,
);
// Find the highest active plan from upgradePlans
const highestActivePlan = siteConfig.cloudPricingItems.reduce(
(highest, item) => {
if (item.upgradePlans) {
const activePlan = item.upgradePlans.find(
(p) => p.stripePriceId === currentSubscription?.price_id,
);
if (activePlan) {
const activeAmount =
parseFloat(activePlan.price.replace(/[^\d.]/g, '') || '0') *
100;
const highestAmount =
parseFloat(highest?.price?.replace(/[^\d.]/g, '') || '0') * 100;
return activeAmount > highestAmount ? activePlan : highest;
}
}
return highest;
},
null as { price: string; hours: string; stripePriceId: string } | null,
// Find the current tier
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription?.price_id,
);
const currentPriceString = currentSubscription
? highestActivePlan?.price || currentTier?.price || '$0'
? currentTier?.price || '$0'
: '$0';
const selectedPriceString = getSelectedPlanPrice(tier);
const selectedPriceString = tier.price;
const currentAmount =
currentPriceString === '$0'
? 0
@ -501,14 +333,20 @@ function PricingTier({
return (
<div
className={cn(
'rounded-xl flex flex-col relative h-fit min-h-[400px] min-[650px]:h-full min-[900px]:h-fit',
'rounded-xl flex flex-col relative',
insideDialog
? 'min-h-[420px]'
: 'h-full min-h-[480px]',
tier.isPopular && !insideDialog
? 'md:shadow-[0px_61px_24px_-10px_rgba(0,0,0,0.01),0px_34px_20px_-8px_rgba(0,0,0,0.05),0px_15px_15px_-6px_rgba(0,0,0,0.09),0px_4px_8px_-2px_rgba(0,0,0,0.10),0px_0px_0px_1px_rgba(0,0,0,0.08)] bg-accent'
: 'bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border',
!insideDialog && ringClass,
)}
>
<div className="flex flex-col gap-4 p-4">
<div className={cn(
"flex flex-col gap-3",
insideDialog ? "p-3" : "p-4"
)}>
<p className="text-sm flex items-center gap-2">
{tier.name}
{tier.isPopular && (
@ -519,54 +357,20 @@ function PricingTier({
{isAuthenticated && statusBadge}
</p>
<div className="flex items-baseline mt-2">
{tier.name === 'Custom' ? (
<CustomPriceDisplay
price={getPriceValue(tier, localSelectedPlan)}
/>
) : (
<PriceDisplay price={tier.price} />
)}
<PriceDisplay price={tier.price} isCompact={insideDialog} />
<span className="ml-2">{tier.price !== '$0' ? '/month' : ''}</span>
</div>
<p className="text-sm mt-2">{tier.description}</p>
{tier.name === 'Custom' && tier.upgradePlans ? (
<div className="w-full space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Customize your monthly usage
</p>
<Select value={localSelectedPlan} onValueChange={handlePlanSelect}>
<SelectTrigger className="w-full bg-white dark:bg-background">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{tier.upgradePlans.map((plan) => (
<SelectItem
key={plan.hours}
value={plan.hours}
className={
localSelectedPlan === plan.hours
? 'font-medium bg-primary/5'
: ''
}
>
{plan.hours} - {plan.price}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary/10 border-primary/20 text-primary w-fit">
{localSelectedPlan}/month
</div>
</div>
) : (
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary/10 border-primary/20 text-primary w-fit">
{getDisplayedHours(tier)}/month
</div>
)}
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary/10 border-primary/20 text-primary w-fit">
{tier.hours}/month
</div>
</div>
<div className="p-4 flex-grow">
<div className={cn(
"flex-grow",
insideDialog ? "px-3 pb-2" : "px-4 pb-3"
)}>
{tier.features && tier.features.length > 0 && (
<ul className="space-y-3">
{tier.features.map((feature) => (
@ -581,14 +385,17 @@ function PricingTier({
)}
</div>
<div className="mt-auto p-4">
<div className={cn(
"mt-auto",
insideDialog ? "px-3 pt-1 pb-3" : "px-4 pt-2 pb-4"
)}>
<Button
onClick={() => handleSubscribe(tierPriceId)}
disabled={buttonDisabled}
variant={buttonVariant || 'default'}
className={cn(
'w-full font-medium transition-all duration-200',
isCompact ? 'h-7 rounded-md text-xs' : 'h-10 rounded-full text-sm',
isCompact || insideDialog ? 'h-8 rounded-md text-xs' : 'h-10 rounded-full text-sm',
buttonClassName,
isPlanLoading && 'animate-pulse',
)}
@ -712,14 +519,19 @@ export function PricingSection({
{deploymentType === 'cloud' && (
<div className={cn(
"grid gap-4 w-full max-w-6xl mx-auto",
"grid gap-4 w-full mx-auto",
{
"px-6": !insideDialog
"px-6 max-w-7xl": !insideDialog,
"max-w-5xl": insideDialog
},
(!hideFree || siteConfig.cloudPricingItems.filter((tier) => !hideFree || tier.price !== '$0').length > 2)
? "min-[650px]:grid-cols-2 min-[900px]:grid-cols-3" : "min-[650px]:grid-cols-2"
insideDialog
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-4"
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
!insideDialog && "grid-rows-1 items-stretch"
)}>
{siteConfig.cloudPricingItems.filter((tier) => !hideFree || tier.price !== '$0').map((tier) => (
{siteConfig.cloudPricingItems
.filter((tier) => !tier.hidden && (!hideFree || tier.price !== '$0'))
.map((tier) => (
<PricingTier
key={tier.name}
tier={tier}

View File

@ -46,6 +46,7 @@ export interface PricingTier {
features: string[];
stripePriceId: string;
upgradePlans: UpgradePlan[];
hidden?: boolean; // Optional property to hide plans from display while keeping them in code
}
export const siteConfig = {
@ -117,7 +118,7 @@ export const siteConfig = {
upgradePlans: [],
},
{
name: 'Pro',
name: 'Plus',
price: '$20',
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
@ -133,47 +134,114 @@ export const siteConfig = {
upgradePlans: [],
},
{
name: 'Custom',
name: 'Pro',
price: '$50',
description: 'Everything in Pro, plus:',
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '6 hours',
features: ['Suited to your needs'],
upgradePlans: [
{
hours: '6 hours',
price: '$50',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
},
{
hours: '12 hours',
price: '$100',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
},
{
hours: '25 hours',
price: '$200',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
},
{
hours: '50 hours',
price: '$400',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
},
{
hours: '125 hours',
price: '$800',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
},
{
hours: '200 hours',
price: '$1000',
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
},
features: [
'6 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
upgradePlans: [],
},
{
name: 'Business',
price: '$100',
description: 'Everything in Pro, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '12 hours',
features: [
'12 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Ultra',
price: '$200',
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-primary text-white dark:text-black',
isPopular: false,
hours: '25 hours',
features: [
'25 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
upgradePlans: [],
},
{
name: 'Enterprise',
price: '$400',
description: 'Everything in Ultra, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '50 hours',
features: [
'50 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
'Custom integrations',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Scale',
price: '$800',
description: 'Everything in Enterprise, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '125 hours',
features: [
'125 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
'Custom integrations',
'Dedicated account manager',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Premium',
price: '$1000',
description: 'Everything in Scale, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '200 hours',
features: [
'200 hours',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
'Custom integrations',
'Dedicated account manager',
'Custom SLA',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
upgradePlans: [],
hidden: true,
},
],
companyShowcase: {