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,
},