feat(pricing): add yearly subscription tiers and enhance billing period toggle functionality

This commit is contained in:
sharath 2025-06-26 15:00:56 +00:00
parent 9036e22c4f
commit bec4494084
No known key found for this signature in database
6 changed files with 374 additions and 33 deletions

View File

@ -33,6 +33,14 @@ SUBSCRIPTION_TIERS = {
config.STRIPE_TIER_50_400_ID: {'name': 'tier_50_400', 'minutes': 3000, 'cost': 400}, # 50 hours
config.STRIPE_TIER_125_800_ID: {'name': 'tier_125_800', 'minutes': 7500, 'cost': 800}, # 125 hours
config.STRIPE_TIER_200_1000_ID: {'name': 'tier_200_1000', 'minutes': 12000, 'cost': 1000}, # 200 hours
# Yearly tiers (same usage limits, different billing period)
config.STRIPE_TIER_2_20_YEARLY_ID: {'name': 'tier_2_20_yearly', 'minutes': 120, 'cost': 20}, # 2 hours/month, $204/year
config.STRIPE_TIER_6_50_YEARLY_ID: {'name': 'tier_6_50_yearly', 'minutes': 360, 'cost': 50}, # 6 hours/month, $510/year
config.STRIPE_TIER_12_100_YEARLY_ID: {'name': 'tier_12_100_yearly', 'minutes': 720, 'cost': 100}, # 12 hours/month, $1020/year
config.STRIPE_TIER_25_200_YEARLY_ID: {'name': 'tier_25_200_yearly', 'minutes': 1500, 'cost': 200}, # 25 hours/month, $2040/year
config.STRIPE_TIER_50_400_YEARLY_ID: {'name': 'tier_50_400_yearly', 'minutes': 3000, 'cost': 400}, # 50 hours/month, $4080/year
config.STRIPE_TIER_125_800_YEARLY_ID: {'name': 'tier_125_800_yearly', 'minutes': 7500, 'cost': 800}, # 125 hours/month, $8160/year
config.STRIPE_TIER_200_1000_YEARLY_ID: {'name': 'tier_200_1000_yearly', 'minutes': 12000, 'cost': 1000}, # 200 hours/month, $10200/year
}
# Pydantic models for request/response validation
@ -127,7 +135,15 @@ async def get_user_subscription(user_id: str) -> Optional[Dict]:
config.STRIPE_TIER_25_200_ID,
config.STRIPE_TIER_50_400_ID,
config.STRIPE_TIER_125_800_ID,
config.STRIPE_TIER_200_1000_ID
config.STRIPE_TIER_200_1000_ID,
# Yearly tiers
config.STRIPE_TIER_2_20_YEARLY_ID,
config.STRIPE_TIER_6_50_YEARLY_ID,
config.STRIPE_TIER_12_100_YEARLY_ID,
config.STRIPE_TIER_25_200_YEARLY_ID,
config.STRIPE_TIER_50_400_YEARLY_ID,
config.STRIPE_TIER_125_800_YEARLY_ID,
config.STRIPE_TIER_200_1000_YEARLY_ID
]:
our_subscriptions.append(sub)

View File

@ -49,6 +49,15 @@ class Configuration:
STRIPE_TIER_125_800_ID_PROD: str = 'price_1RILb3G6l1KZGqIrbJA766tN'
STRIPE_TIER_200_1000_ID_PROD: str = 'price_1RILb3G6l1KZGqIrmauYPOiN'
# Yearly subscription tier IDs - Production (15% discount)
STRIPE_TIER_2_20_YEARLY_ID_PROD: str = 'price_1ReHB5G6l1KZGqIrD70I1xqM'
STRIPE_TIER_6_50_YEARLY_ID_PROD: str = 'price_1ReHAsG6l1KZGqIrlAog487C'
STRIPE_TIER_12_100_YEARLY_ID_PROD: str = 'price_1ReHAWG6l1KZGqIrBHer2PQc'
STRIPE_TIER_25_200_YEARLY_ID_PROD: str = 'price_1ReH9uG6l1KZGqIrsvMLHViC'
STRIPE_TIER_50_400_YEARLY_ID_PROD: str = 'price_1ReH9fG6l1KZGqIrsPtu5KIA'
STRIPE_TIER_125_800_YEARLY_ID_PROD: str = 'price_1ReH9GG6l1KZGqIrfgqaJyat'
STRIPE_TIER_200_1000_YEARLY_ID_PROD: str = 'price_1ReH8qG6l1KZGqIrK1akY90q'
# Subscription tier IDs - Staging
STRIPE_FREE_TIER_ID_STAGING: str = 'price_1RIGvuG6l1KZGqIrw14abxeL'
STRIPE_TIER_2_20_ID_STAGING: str = 'price_1RIGvuG6l1KZGqIrCRu0E4Gi'
@ -59,6 +68,15 @@ class Configuration:
STRIPE_TIER_125_800_ID_STAGING: str = 'price_1RIKNrG6l1KZGqIrjKT0yGvI'
STRIPE_TIER_200_1000_ID_STAGING: str = 'price_1RIKQ2G6l1KZGqIrum9n8SI7'
# Yearly subscription tier IDs - Staging (15% discount)
STRIPE_TIER_2_20_YEARLY_ID_STAGING: str = 'price_1ReGogG6l1KZGqIrEyBTmtPk'
STRIPE_TIER_6_50_YEARLY_ID_STAGING: str = 'price_1ReGoJG6l1KZGqIr0DJWtoOc'
STRIPE_TIER_12_100_YEARLY_ID_STAGING: str = 'price_1ReGnZG6l1KZGqIr0ThLEl5S'
STRIPE_TIER_25_200_YEARLY_ID_STAGING: str = 'price_1ReGmzG6l1KZGqIre31mqoEJ'
STRIPE_TIER_50_400_YEARLY_ID_STAGING: str = 'price_1ReGmgG6l1KZGqIrn5nBc7e5'
STRIPE_TIER_125_800_YEARLY_ID_STAGING: str = 'price_1ReGmMG6l1KZGqIrvE2ycrAX'
STRIPE_TIER_200_1000_YEARLY_ID_STAGING: str = 'price_1ReGlXG6l1KZGqIrlgurP5GU'
# Computed subscription tier IDs based on environment
@property
def STRIPE_FREE_TIER_ID(self) -> str:
@ -108,6 +126,49 @@ class Configuration:
return self.STRIPE_TIER_200_1000_ID_STAGING
return self.STRIPE_TIER_200_1000_ID_PROD
# Yearly tier computed properties
@property
def STRIPE_TIER_2_20_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_2_20_YEARLY_ID_STAGING
return self.STRIPE_TIER_2_20_YEARLY_ID_PROD
@property
def STRIPE_TIER_6_50_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_6_50_YEARLY_ID_STAGING
return self.STRIPE_TIER_6_50_YEARLY_ID_PROD
@property
def STRIPE_TIER_12_100_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_12_100_YEARLY_ID_STAGING
return self.STRIPE_TIER_12_100_YEARLY_ID_PROD
@property
def STRIPE_TIER_25_200_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_25_200_YEARLY_ID_STAGING
return self.STRIPE_TIER_25_200_YEARLY_ID_PROD
@property
def STRIPE_TIER_50_400_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_50_400_YEARLY_ID_STAGING
return self.STRIPE_TIER_50_400_YEARLY_ID_PROD
@property
def STRIPE_TIER_125_800_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_125_800_YEARLY_ID_STAGING
return self.STRIPE_TIER_125_800_YEARLY_ID_PROD
@property
def STRIPE_TIER_200_1000_YEARLY_ID(self) -> str:
if self.ENV_MODE == EnvMode.STAGING:
return self.STRIPE_TIER_200_1000_YEARLY_ID_STAGING
return self.STRIPE_TIER_200_1000_YEARLY_ID_PROD
# LLM API keys
ANTHROPIC_API_KEY: str = None
OPENAI_API_KEY: Optional[str] = None

View File

@ -21,7 +21,7 @@ export default function PersonalAccountSettingsPage({
<>
<div className="space-y-6 w-full">
<Separator className="border-subtle dark:border-white/10" />
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0 w-full max-w-6xl mx-auto px-4">
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0 w-full max-w-7xl mx-auto px-4">
<aside className="lg:w-1/4 p-1">
<nav className="flex flex-col space-y-1">
{items.map((item) => (

View File

@ -5,7 +5,7 @@ import type { PricingTier } from '@/lib/home';
import { siteConfig } from '@/lib/home';
import { cn } from '@/lib/utils';
import { motion } from 'motion/react';
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { CheckIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
@ -56,6 +56,7 @@ interface PricingTierProps {
isAuthenticated?: boolean;
returnUrl: string;
insideDialog?: boolean;
billingPeriod?: 'monthly' | 'yearly';
}
// Components
@ -123,6 +124,46 @@ function PriceDisplay({ price, isCompact }: PriceDisplayProps) {
);
}
function BillingPeriodToggle({
billingPeriod,
setBillingPeriod
}: {
billingPeriod: 'monthly' | 'yearly';
setBillingPeriod: (period: 'monthly' | 'yearly') => void;
}) {
return (
<div className="flex items-center justify-center gap-3">
<span className={cn("text-sm font-medium", billingPeriod === 'monthly' ? 'text-foreground' : 'text-muted-foreground')}>
Monthly
</span>
<div
className="relative bg-muted rounded-full p-1 cursor-pointer"
onClick={() => setBillingPeriod(billingPeriod === 'monthly' ? 'yearly' : 'monthly')}
>
<div className="flex">
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
billingPeriod === 'monthly'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground'
)}>
Monthly
</div>
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
billingPeriod === 'yearly'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground'
)}>
Yearly
</div>
</div>
</div>
<span className={cn("text-sm font-medium", billingPeriod === 'yearly' ? 'text-foreground' : 'text-muted-foreground')}>
Yearly
</span>
</div>
);
}
function PricingTier({
tier,
isCompact = false,
@ -135,6 +176,7 @@ function PricingTier({
isAuthenticated = false,
returnUrl,
insideDialog = false,
billingPeriod = 'monthly',
}: PricingTierProps) {
// Auto-select the correct plan only on initial load - simplified since no more Custom tier
const handleSubscribe = async (planStripePriceId: string) => {
@ -217,7 +259,18 @@ function PricingTier({
}
};
const tierPriceId = tier.stripePriceId;
const tierPriceId = billingPeriod === 'yearly' && tier.yearlyStripePriceId
? tier.yearlyStripePriceId
: tier.stripePriceId;
const displayPrice = billingPeriod === 'yearly' && tier.yearlyPrice
? tier.yearlyPrice
: tier.price;
// Find the current tier (moved outside conditional for JSX access)
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription?.price_id || p.yearlyStripePriceId === currentSubscription?.price_id,
);
const isCurrentActivePlan =
isAuthenticated && currentSubscription?.price_id === tierPriceId;
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
@ -269,11 +322,6 @@ function PricingTier({
</span>
);
} else {
// Find the current tier
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription?.price_id,
);
const currentPriceString = currentSubscription
? currentTier?.price || '$0'
: '$0';
@ -287,6 +335,14 @@ function PricingTier({
? 0
: parseFloat(selectedPriceString.replace(/[^\d.]/g, '') || '0') * 100;
// Check if current subscription is monthly and target is yearly for same tier
const currentIsMonthly = currentTier && currentSubscription?.price_id === currentTier.stripePriceId;
const currentIsYearly = currentTier && currentSubscription?.price_id === currentTier.yearlyStripePriceId;
const targetIsMonthly = tier.stripePriceId === tierPriceId;
const targetIsYearly = tier.yearlyStripePriceId === tierPriceId;
const isSameTierDifferentBilling = currentTier && currentTier.name === tier.name &&
((currentIsMonthly && targetIsYearly) || (currentIsYearly && targetIsMonthly));
if (
currentAmount === 0 &&
targetAmount === 0 &&
@ -297,17 +353,49 @@ function PricingTier({
buttonVariant = 'secondary';
buttonClassName = 'bg-primary/5 hover:bg-primary/10 text-primary';
} else {
if (targetAmount > currentAmount) {
buttonText = 'Upgrade';
buttonVariant = tier.buttonColor as ButtonVariant;
buttonClassName =
'bg-primary hover:bg-primary/90 text-primary-foreground';
} else if (targetAmount < currentAmount) {
if (targetAmount > currentAmount || (currentIsMonthly && targetIsYearly && targetAmount >= currentAmount)) {
// Allow upgrade to higher tier OR switch from monthly to yearly at same/higher tier
// But prevent yearly to monthly switches even if target amount is higher
if (currentIsYearly && targetIsMonthly) {
buttonText = '-';
buttonDisabled = true;
buttonVariant = 'secondary';
buttonClassName =
'opacity-50 cursor-not-allowed bg-muted text-muted-foreground';
} else if (currentIsMonthly && targetIsYearly && targetAmount === currentAmount) {
buttonText = 'Switch to Yearly';
buttonVariant = 'default';
buttonClassName = 'bg-green-600 hover:bg-green-700 text-white';
} else {
buttonText = 'Upgrade';
buttonVariant = tier.buttonColor as ButtonVariant;
buttonClassName = 'bg-primary hover:bg-primary/90 text-primary-foreground';
}
} else if (targetAmount < currentAmount && !(currentIsYearly && targetIsMonthly && targetAmount === currentAmount)) {
buttonText = '-';
buttonDisabled = true;
buttonVariant = 'secondary';
buttonClassName =
'opacity-50 cursor-not-allowed bg-muted text-muted-foreground';
} else if (isSameTierDifferentBilling) {
// Allow switching between monthly and yearly for same tier
if (currentIsMonthly && targetIsYearly) {
buttonText = 'Switch to Yearly';
buttonVariant = 'default';
buttonClassName = 'bg-green-600 hover:bg-green-700 text-white';
} else if (currentIsYearly && targetIsMonthly) {
// Prevent downgrade from yearly to monthly
buttonText = '-';
buttonDisabled = true;
buttonVariant = 'secondary';
buttonClassName =
'opacity-50 cursor-not-allowed bg-muted text-muted-foreground';
} else {
buttonText = 'Select Plan';
buttonVariant = tier.buttonColor as ButtonVariant;
buttonClassName =
'bg-primary hover:bg-primary/90 text-primary-foreground';
}
} else {
buttonText = 'Select Plan';
buttonVariant = tier.buttonColor as ButtonVariant;
@ -350,21 +438,58 @@ function PricingTier({
<p className="text-sm flex items-center gap-2">
{tier.name}
{tier.isPopular && (
<span className="bg-gradient-to-b from-secondary/50 from-[1.92%] to-secondary to-[100%] text-white h-6 inline-flex w-fit items-center justify-center px-2 rounded-full text-sm shadow-[0px_6px_6px_-3px_rgba(0,0,0,0.08),0px_3px_3px_-1.5px_rgba(0,0,0,0.08),0px_1px_1px_-0.5px_rgba(0,0,0,0.08),0px_0px_0px_1px_rgba(255,255,255,0.12)_inset,0px_1px_0px_0px_rgba(255,255,255,0.12)_inset]">
<span className="bg-gradient-to-b from-secondary/50 from-[1.92%] to-secondary to-[100%] text-white inline-flex w-fit items-center justify-center px-1.5 py-0.5 rounded-full text-[10px] font-medium shadow-[0px_6px_6px_-3px_rgba(0,0,0,0.08),0px_3px_3px_-1.5px_rgba(0,0,0,0.08),0px_1px_1px_-0.5px_rgba(0,0,0,0.08),0px_0px_0px_1px_rgba(255,255,255,0.12)_inset,0px_1px_0px_0px_rgba(255,255,255,0.12)_inset]">
Popular
</span>
)}
{/* Show upgrade badge for yearly plans when user is on monthly */}
{isAuthenticated && currentSubscription && billingPeriod === 'yearly' &&
currentTier && currentSubscription.price_id === currentTier.stripePriceId &&
tier.yearlyStripePriceId && (currentTier.name === tier.name ||
parseFloat(tier.price.slice(1)) >= parseFloat(currentTier.price.slice(1))) && (
<span className="bg-green-500/10 text-green-700 text-[10px] font-medium px-1.5 py-0.5 rounded-full">
Recommended
</span>
)}
{isAuthenticated && statusBadge}
</p>
<div className="flex items-baseline mt-2">
<PriceDisplay price={tier.price} isCompact={insideDialog} />
<span className="ml-2">{tier.price !== '$0' ? '/month' : ''}</span>
{billingPeriod === 'yearly' && tier.yearlyPrice && displayPrice !== '$0' ? (
<div className="flex flex-col">
<div className="flex items-baseline gap-2">
<PriceDisplay price={`$${Math.round(parseFloat(tier.yearlyPrice.slice(1)) / 12)}`} isCompact={insideDialog} />
{tier.discountPercentage && (
<span className="text-xs line-through text-muted-foreground">
${Math.round(parseFloat(tier.originalYearlyPrice?.slice(1) || '0') / 12)}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-muted-foreground">/month</span>
<span className="text-xs text-muted-foreground">billed yearly</span>
</div>
</div>
) : (
<div className="flex items-baseline">
<PriceDisplay price={displayPrice} isCompact={insideDialog} />
<span className="ml-2">{displayPrice !== '$0' ? '/month' : ''}</span>
</div>
)}
</div>
<p className="text-sm mt-2">{tier.description}</p>
<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.price}/month
</div>
{billingPeriod === 'yearly' && tier.yearlyPrice && tier.discountPercentage ? (
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-green-50 border-green-200 text-green-700 w-fit">
Save ${Math.round((parseFloat(tier.originalYearlyPrice?.slice(1) || '0') - parseFloat(tier.yearlyPrice.slice(1))) / 12)} per 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">
{billingPeriod === 'yearly' && tier.yearlyPrice && displayPrice !== '$0'
? `$${Math.round(parseFloat(tier.yearlyPrice.slice(1)) / 12)}/month (billed yearly)`
: `${displayPrice}/month`
}
</div>
)}
</div>
<div className={cn(
@ -375,7 +500,7 @@ function PricingTier({
<ul className="space-y-3">
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-2">
<div className="size-5 rounded-full border border-primary/20 flex items-center justify-center">
<div className="size-5 min-w-5 rounded-full border border-primary/20 flex items-center justify-center">
<CheckIcon className="size-3 text-primary" />
</div>
<span className="text-sm">{feature}</span>
@ -423,13 +548,45 @@ export function PricingSection({
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
'cloud',
);
const [planLoadingStates, setPlanLoadingStates] = useState<Record<string, boolean>>({});
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError } = useSubscription();
// Derive authentication and subscription status from the hook data
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
const currentSubscription = subscriptionData || null;
// Determine default billing period based on user's current subscription
const getDefaultBillingPeriod = (): 'monthly' | 'yearly' => {
if (!isAuthenticated || !currentSubscription) {
// Default to yearly for non-authenticated users or users without subscription
return 'yearly';
}
// Find current tier to determine if user is on monthly or yearly plan
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription.price_id || p.yearlyStripePriceId === currentSubscription.price_id,
);
if (currentTier) {
// Check if current subscription is yearly
if (currentTier.yearlyStripePriceId === currentSubscription.price_id) {
return 'yearly';
} else if (currentTier.stripePriceId === currentSubscription.price_id) {
return 'monthly';
}
}
// Default to yearly if we can't determine current plan type
return 'yearly';
};
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'yearly'>(getDefaultBillingPeriod());
const [planLoadingStates, setPlanLoadingStates] = useState<Record<string, boolean>>({});
// Update billing period when subscription data changes
useEffect(() => {
setBillingPeriod(getDefaultBillingPeriod());
}, [isAuthenticated, currentSubscription?.price_id]);
const handlePlanSelect = (planId: string) => {
setPlanLoadingStates((prev) => ({ ...prev, [planId]: true }));
};
@ -498,15 +655,22 @@ export function PricingSection({
</>
)}
{deploymentType === 'cloud' && (
<BillingPeriodToggle
billingPeriod={billingPeriod}
setBillingPeriod={setBillingPeriod}
/>
)}
{deploymentType === 'cloud' && (
<div className={cn(
"grid gap-4 w-full mx-auto",
{
"px-6 max-w-7xl": !insideDialog,
"max-w-5xl": insideDialog
"max-w-7xl": insideDialog
},
insideDialog
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-4"
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 2xl:grid-cols-4"
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
!insideDialog && "grid-rows-1 items-stretch"
)}>
@ -524,6 +688,7 @@ export function PricingSection({
isAuthenticated={isAuthenticated}
returnUrl={returnUrl}
insideDialog={insideDialog}
billingPeriod={billingPeriod}
/>
))}
</div>

View File

@ -21,6 +21,14 @@ export interface SubscriptionTiers {
TIER_50_400: SubscriptionTierData;
TIER_125_800: SubscriptionTierData;
TIER_200_1000: SubscriptionTierData;
// Yearly plans with 15% discount
TIER_2_20_YEARLY: SubscriptionTierData;
TIER_6_50_YEARLY: SubscriptionTierData;
TIER_12_100_YEARLY: SubscriptionTierData;
TIER_25_200_YEARLY: SubscriptionTierData;
TIER_50_400_YEARLY: SubscriptionTierData;
TIER_125_800_YEARLY: SubscriptionTierData;
TIER_200_1000_YEARLY: SubscriptionTierData;
}
// Configuration object
@ -64,6 +72,35 @@ const PROD_TIERS: SubscriptionTiers = {
priceId: 'price_1RILb3G6l1KZGqIrmauYPOiN',
name: '200h/$1000',
},
// Yearly plans with 15% discount (12x monthly price with 15% off)
TIER_2_20_YEARLY: {
priceId: 'price_1ReHB5G6l1KZGqIrD70I1xqM',
name: '2h/$204/year',
},
TIER_6_50_YEARLY: {
priceId: 'price_1ReHAsG6l1KZGqIrlAog487C',
name: '6h/$510/year',
},
TIER_12_100_YEARLY: {
priceId: 'price_1ReHAWG6l1KZGqIrBHer2PQc',
name: '12h/$1020/year',
},
TIER_25_200_YEARLY: {
priceId: 'price_1ReH9uG6l1KZGqIrsvMLHViC',
name: '25h/$2040/year',
},
TIER_50_400_YEARLY: {
priceId: 'price_1ReH9fG6l1KZGqIrsPtu5KIA',
name: '50h/$4080/year',
},
TIER_125_800_YEARLY: {
priceId: 'price_1ReH9GG6l1KZGqIrfgqaJyat',
name: '125h/$8160/year',
},
TIER_200_1000_YEARLY: {
priceId: 'price_1ReH8qG6l1KZGqIrK1akY90q',
name: '200h/$10200/year',
},
} as const;
// Staging tier IDs
@ -100,6 +137,35 @@ const STAGING_TIERS: SubscriptionTiers = {
priceId: 'price_1RIKQ2G6l1KZGqIrum9n8SI7',
name: '200h/$1000',
},
// Yearly plans with 15% discount (12x monthly price with 15% off)
TIER_2_20_YEARLY: {
priceId: 'price_1ReGogG6l1KZGqIrEyBTmtPk',
name: '2h/$204/year',
},
TIER_6_50_YEARLY: {
priceId: 'price_1ReGoJG6l1KZGqIr0DJWtoOc',
name: '6h/$510/year',
},
TIER_12_100_YEARLY: {
priceId: 'price_1ReGnZG6l1KZGqIr0ThLEl5S',
name: '12h/$1020/year',
},
TIER_25_200_YEARLY: {
priceId: 'price_1ReGmzG6l1KZGqIre31mqoEJ',
name: '25h/$2040/year',
},
TIER_50_400_YEARLY: {
priceId: 'price_1ReGmgG6l1KZGqIrn5nBc7e5',
name: '50h/$4080/year',
},
TIER_125_800_YEARLY: {
priceId: 'price_1ReGmMG6l1KZGqIrvE2ycrAX',
name: '125h/$8160/year',
},
TIER_200_1000_YEARLY: {
priceId: 'price_1ReGlXG6l1KZGqIrlgurP5GU',
name: '200h/$10200/year',
},
} as const;
// Determine the environment mode from environment variables

View File

@ -39,6 +39,7 @@ interface UpgradePlan {
export interface PricingTier {
name: string;
price: string;
yearlyPrice?: string; // Add yearly price support
description: string;
buttonText: string;
buttonColor: string;
@ -47,8 +48,12 @@ export interface PricingTier {
hours: string;
features: string[];
stripePriceId: string;
yearlyStripePriceId?: string; // Add yearly price ID support
upgradePlans: UpgradePlan[];
hidden?: boolean; // Optional property to hide plans from display while keeping them in code
billingPeriod?: 'monthly' | 'yearly'; // Add billing period support
originalYearlyPrice?: string; // For showing crossed-out price
discountPercentage?: number; // For showing discount badge
}
export const siteConfig = {
@ -117,7 +122,7 @@ export const siteConfig = {
/** @deprecated */
hours: '60 min',
features: [
'$5 of usage',
'$5/month usage',
'Public Projects',
'Basic Model (Limited capabilities)',
],
@ -127,6 +132,9 @@ export const siteConfig = {
{
name: 'Plus',
price: '$20',
yearlyPrice: '$204',
originalYearlyPrice: '$240',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-primary text-white dark:text-black',
@ -134,16 +142,20 @@ export const siteConfig = {
/** @deprecated */
hours: '2 hours',
features: [
'$20 of usage',
'$20/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20_YEARLY.priceId,
upgradePlans: [],
},
{
name: 'Pro',
price: '$50',
yearlyPrice: '$510',
originalYearlyPrice: '$600',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
@ -151,76 +163,92 @@ export const siteConfig = {
/** @deprecated */
hours: '6 hours',
features: [
'$50 of usage',
'$50/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50_YEARLY.priceId,
upgradePlans: [],
},
{
name: 'Business',
price: '$100',
yearlyPrice: '$1020',
originalYearlyPrice: '$1200',
discountPercentage: 15,
description: 'Everything in Pro, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '12 hours',
features: [
'12 hours',
'$100/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100_YEARLY.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Ultra',
price: '$200',
yearlyPrice: '$2040',
originalYearlyPrice: '$2400',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-primary text-white dark:text-black',
isPopular: false,
hours: '25 hours',
features: [
'$200 of usage',
'$200/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200_YEARLY.priceId,
upgradePlans: [],
},
{
name: 'Enterprise',
price: '$400',
yearlyPrice: '$4080',
originalYearlyPrice: '$4800',
discountPercentage: 15,
description: 'Everything in Ultra, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '50 hours',
features: [
'50 hours',
'$400/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
'Custom integrations',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400_YEARLY.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Scale',
price: '$800',
yearlyPrice: '$8160',
originalYearlyPrice: '$9600',
discountPercentage: 15,
description: 'Everything in Enterprise, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '125 hours',
features: [
'125 hours',
'$800/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
@ -228,19 +256,23 @@ export const siteConfig = {
'Dedicated account manager',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800_YEARLY.priceId,
upgradePlans: [],
hidden: true,
},
{
name: 'Premium',
price: '$1000',
yearlyPrice: '$10200',
originalYearlyPrice: '$12000',
discountPercentage: 15,
description: 'Everything in Scale, plus:',
buttonText: 'Try Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '200 hours',
features: [
'200 hours',
'$1000/month usage',
'Private projects',
'Access to intelligent Model (Full Suna)',
'Priority support',
@ -249,6 +281,7 @@ export const siteConfig = {
'Custom SLA',
],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000_YEARLY.priceId,
upgradePlans: [],
hidden: true,
},