From bec4494084f01e0e5ec198059b5d2c8eb874dcbd Mon Sep 17 00:00:00 2001 From: sharath <29162020+tnfssc@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:00:56 +0000 Subject: [PATCH] feat(pricing): add yearly subscription tiers and enhance billing period toggle functionality --- backend/services/billing.py | 18 +- backend/utils/config.py | 61 +++++ .../(personalAccount)/settings/layout.tsx | 2 +- .../home/sections/pricing-section.tsx | 211 ++++++++++++++++-- frontend/src/lib/config.ts | 66 ++++++ frontend/src/lib/home.tsx | 49 +++- 6 files changed, 374 insertions(+), 33 deletions(-) diff --git a/backend/services/billing.py b/backend/services/billing.py index 087b84bf..1547fedf 100644 --- a/backend/services/billing.py +++ b/backend/services/billing.py @@ -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) diff --git a/backend/utils/config.py b/backend/utils/config.py index d1f45e98..bca8d582 100644 --- a/backend/utils/config.py +++ b/backend/utils/config.py @@ -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 diff --git a/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx b/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx index ef0664cf..dbdf652b 100644 --- a/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx +++ b/frontend/src/app/(dashboard)/(personalAccount)/settings/layout.tsx @@ -21,7 +21,7 @@ export default function PersonalAccountSettingsPage({ <>
-
+
{tier.features.map((feature) => (
  • -
    +
    {feature} @@ -423,13 +548,45 @@ export function PricingSection({ const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>( 'cloud', ); - const [planLoadingStates, setPlanLoadingStates] = useState>({}); 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>({}); + + // 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' && ( + + )} + {deploymentType === 'cloud' && (
    @@ -524,6 +688,7 @@ export function PricingSection({ isAuthenticated={isAuthenticated} returnUrl={returnUrl} insideDialog={insideDialog} + billingPeriod={billingPeriod} /> ))}
    diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index 3a245d29..0ee80e9c 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -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 diff --git a/frontend/src/lib/home.tsx b/frontend/src/lib/home.tsx index 3a58267c..c717148e 100644 --- a/frontend/src/lib/home.tsx +++ b/frontend/src/lib/home.tsx @@ -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, },