mirror of https://github.com/kortix-ai/suna.git
feat(pricing): add yearly subscription tiers and enhance billing period toggle functionality
This commit is contained in:
parent
9036e22c4f
commit
bec4494084
|
@ -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_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_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
|
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
|
# 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_25_200_ID,
|
||||||
config.STRIPE_TIER_50_400_ID,
|
config.STRIPE_TIER_50_400_ID,
|
||||||
config.STRIPE_TIER_125_800_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)
|
our_subscriptions.append(sub)
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,15 @@ class Configuration:
|
||||||
STRIPE_TIER_125_800_ID_PROD: str = 'price_1RILb3G6l1KZGqIrbJA766tN'
|
STRIPE_TIER_125_800_ID_PROD: str = 'price_1RILb3G6l1KZGqIrbJA766tN'
|
||||||
STRIPE_TIER_200_1000_ID_PROD: str = 'price_1RILb3G6l1KZGqIrmauYPOiN'
|
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
|
# Subscription tier IDs - Staging
|
||||||
STRIPE_FREE_TIER_ID_STAGING: str = 'price_1RIGvuG6l1KZGqIrw14abxeL'
|
STRIPE_FREE_TIER_ID_STAGING: str = 'price_1RIGvuG6l1KZGqIrw14abxeL'
|
||||||
STRIPE_TIER_2_20_ID_STAGING: str = 'price_1RIGvuG6l1KZGqIrCRu0E4Gi'
|
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_125_800_ID_STAGING: str = 'price_1RIKNrG6l1KZGqIrjKT0yGvI'
|
||||||
STRIPE_TIER_200_1000_ID_STAGING: str = 'price_1RIKQ2G6l1KZGqIrum9n8SI7'
|
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
|
# Computed subscription tier IDs based on environment
|
||||||
@property
|
@property
|
||||||
def STRIPE_FREE_TIER_ID(self) -> str:
|
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_STAGING
|
||||||
return self.STRIPE_TIER_200_1000_ID_PROD
|
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
|
# LLM API keys
|
||||||
ANTHROPIC_API_KEY: str = None
|
ANTHROPIC_API_KEY: str = None
|
||||||
OPENAI_API_KEY: Optional[str] = None
|
OPENAI_API_KEY: Optional[str] = None
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function PersonalAccountSettingsPage({
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6 w-full">
|
<div className="space-y-6 w-full">
|
||||||
<Separator className="border-subtle dark:border-white/10" />
|
<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">
|
<aside className="lg:w-1/4 p-1">
|
||||||
<nav className="flex flex-col space-y-1">
|
<nav className="flex flex-col space-y-1">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { PricingTier } from '@/lib/home';
|
||||||
import { siteConfig } from '@/lib/home';
|
import { siteConfig } from '@/lib/home';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CheckIcon } from 'lucide-react';
|
import { CheckIcon } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
|
@ -56,6 +56,7 @@ interface PricingTierProps {
|
||||||
isAuthenticated?: boolean;
|
isAuthenticated?: boolean;
|
||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
insideDialog?: boolean;
|
insideDialog?: boolean;
|
||||||
|
billingPeriod?: 'monthly' | 'yearly';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Components
|
// 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({
|
function PricingTier({
|
||||||
tier,
|
tier,
|
||||||
isCompact = false,
|
isCompact = false,
|
||||||
|
@ -135,6 +176,7 @@ function PricingTier({
|
||||||
isAuthenticated = false,
|
isAuthenticated = false,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
insideDialog = false,
|
insideDialog = false,
|
||||||
|
billingPeriod = 'monthly',
|
||||||
}: PricingTierProps) {
|
}: PricingTierProps) {
|
||||||
// Auto-select the correct plan only on initial load - simplified since no more Custom tier
|
// Auto-select the correct plan only on initial load - simplified since no more Custom tier
|
||||||
const handleSubscribe = async (planStripePriceId: string) => {
|
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 =
|
const isCurrentActivePlan =
|
||||||
isAuthenticated && currentSubscription?.price_id === tierPriceId;
|
isAuthenticated && currentSubscription?.price_id === tierPriceId;
|
||||||
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
|
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
|
||||||
|
@ -269,11 +322,6 @@ function PricingTier({
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Find the current tier
|
|
||||||
const currentTier = siteConfig.cloudPricingItems.find(
|
|
||||||
(p) => p.stripePriceId === currentSubscription?.price_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPriceString = currentSubscription
|
const currentPriceString = currentSubscription
|
||||||
? currentTier?.price || '$0'
|
? currentTier?.price || '$0'
|
||||||
: '$0';
|
: '$0';
|
||||||
|
@ -287,6 +335,14 @@ function PricingTier({
|
||||||
? 0
|
? 0
|
||||||
: parseFloat(selectedPriceString.replace(/[^\d.]/g, '') || '0') * 100;
|
: 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 (
|
if (
|
||||||
currentAmount === 0 &&
|
currentAmount === 0 &&
|
||||||
targetAmount === 0 &&
|
targetAmount === 0 &&
|
||||||
|
@ -297,17 +353,49 @@ function PricingTier({
|
||||||
buttonVariant = 'secondary';
|
buttonVariant = 'secondary';
|
||||||
buttonClassName = 'bg-primary/5 hover:bg-primary/10 text-primary';
|
buttonClassName = 'bg-primary/5 hover:bg-primary/10 text-primary';
|
||||||
} else {
|
} else {
|
||||||
if (targetAmount > currentAmount) {
|
if (targetAmount > currentAmount || (currentIsMonthly && targetIsYearly && targetAmount >= currentAmount)) {
|
||||||
buttonText = 'Upgrade';
|
// Allow upgrade to higher tier OR switch from monthly to yearly at same/higher tier
|
||||||
buttonVariant = tier.buttonColor as ButtonVariant;
|
// But prevent yearly to monthly switches even if target amount is higher
|
||||||
buttonClassName =
|
if (currentIsYearly && targetIsMonthly) {
|
||||||
'bg-primary hover:bg-primary/90 text-primary-foreground';
|
|
||||||
} else if (targetAmount < currentAmount) {
|
|
||||||
buttonText = '-';
|
buttonText = '-';
|
||||||
buttonDisabled = true;
|
buttonDisabled = true;
|
||||||
buttonVariant = 'secondary';
|
buttonVariant = 'secondary';
|
||||||
buttonClassName =
|
buttonClassName =
|
||||||
'opacity-50 cursor-not-allowed bg-muted text-muted-foreground';
|
'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 {
|
} else {
|
||||||
buttonText = 'Select Plan';
|
buttonText = 'Select Plan';
|
||||||
buttonVariant = tier.buttonColor as ButtonVariant;
|
buttonVariant = tier.buttonColor as ButtonVariant;
|
||||||
|
@ -350,21 +438,58 @@ function PricingTier({
|
||||||
<p className="text-sm flex items-center gap-2">
|
<p className="text-sm flex items-center gap-2">
|
||||||
{tier.name}
|
{tier.name}
|
||||||
{tier.isPopular && (
|
{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
|
Popular
|
||||||
</span>
|
</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}
|
{isAuthenticated && statusBadge}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline mt-2">
|
<div className="flex items-baseline mt-2">
|
||||||
<PriceDisplay price={tier.price} isCompact={insideDialog} />
|
{billingPeriod === 'yearly' && tier.yearlyPrice && displayPrice !== '$0' ? (
|
||||||
<span className="ml-2">{tier.price !== '$0' ? '/month' : ''}</span>
|
<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>
|
</div>
|
||||||
<p className="text-sm mt-2">{tier.description}</p>
|
<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">
|
{billingPeriod === 'yearly' && tier.yearlyPrice && tier.discountPercentage ? (
|
||||||
{tier.price}/month
|
<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>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
|
@ -375,7 +500,7 @@ function PricingTier({
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{tier.features.map((feature) => (
|
{tier.features.map((feature) => (
|
||||||
<li key={feature} className="flex items-center gap-2">
|
<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" />
|
<CheckIcon className="size-3 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm">{feature}</span>
|
<span className="text-sm">{feature}</span>
|
||||||
|
@ -423,13 +548,45 @@ export function PricingSection({
|
||||||
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
|
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
|
||||||
'cloud',
|
'cloud',
|
||||||
);
|
);
|
||||||
const [planLoadingStates, setPlanLoadingStates] = useState<Record<string, boolean>>({});
|
|
||||||
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError } = useSubscription();
|
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError } = useSubscription();
|
||||||
|
|
||||||
// Derive authentication and subscription status from the hook data
|
// Derive authentication and subscription status from the hook data
|
||||||
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
||||||
const currentSubscription = subscriptionData || 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) => {
|
const handlePlanSelect = (planId: string) => {
|
||||||
setPlanLoadingStates((prev) => ({ ...prev, [planId]: true }));
|
setPlanLoadingStates((prev) => ({ ...prev, [planId]: true }));
|
||||||
};
|
};
|
||||||
|
@ -498,15 +655,22 @@ export function PricingSection({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{deploymentType === 'cloud' && (
|
||||||
|
<BillingPeriodToggle
|
||||||
|
billingPeriod={billingPeriod}
|
||||||
|
setBillingPeriod={setBillingPeriod}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{deploymentType === 'cloud' && (
|
{deploymentType === 'cloud' && (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"grid gap-4 w-full mx-auto",
|
"grid gap-4 w-full mx-auto",
|
||||||
{
|
{
|
||||||
"px-6 max-w-7xl": !insideDialog,
|
"px-6 max-w-7xl": !insideDialog,
|
||||||
"max-w-5xl": insideDialog
|
"max-w-7xl": insideDialog
|
||||||
},
|
},
|
||||||
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",
|
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
|
||||||
!insideDialog && "grid-rows-1 items-stretch"
|
!insideDialog && "grid-rows-1 items-stretch"
|
||||||
)}>
|
)}>
|
||||||
|
@ -524,6 +688,7 @@ export function PricingSection({
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
returnUrl={returnUrl}
|
returnUrl={returnUrl}
|
||||||
insideDialog={insideDialog}
|
insideDialog={insideDialog}
|
||||||
|
billingPeriod={billingPeriod}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,14 @@ export interface SubscriptionTiers {
|
||||||
TIER_50_400: SubscriptionTierData;
|
TIER_50_400: SubscriptionTierData;
|
||||||
TIER_125_800: SubscriptionTierData;
|
TIER_125_800: SubscriptionTierData;
|
||||||
TIER_200_1000: 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
|
// Configuration object
|
||||||
|
@ -64,6 +72,35 @@ const PROD_TIERS: SubscriptionTiers = {
|
||||||
priceId: 'price_1RILb3G6l1KZGqIrmauYPOiN',
|
priceId: 'price_1RILb3G6l1KZGqIrmauYPOiN',
|
||||||
name: '200h/$1000',
|
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;
|
} as const;
|
||||||
|
|
||||||
// Staging tier IDs
|
// Staging tier IDs
|
||||||
|
@ -100,6 +137,35 @@ const STAGING_TIERS: SubscriptionTiers = {
|
||||||
priceId: 'price_1RIKQ2G6l1KZGqIrum9n8SI7',
|
priceId: 'price_1RIKQ2G6l1KZGqIrum9n8SI7',
|
||||||
name: '200h/$1000',
|
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;
|
} as const;
|
||||||
|
|
||||||
// Determine the environment mode from environment variables
|
// Determine the environment mode from environment variables
|
||||||
|
|
|
@ -39,6 +39,7 @@ interface UpgradePlan {
|
||||||
export interface PricingTier {
|
export interface PricingTier {
|
||||||
name: string;
|
name: string;
|
||||||
price: string;
|
price: string;
|
||||||
|
yearlyPrice?: string; // Add yearly price support
|
||||||
description: string;
|
description: string;
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
buttonColor: string;
|
buttonColor: string;
|
||||||
|
@ -47,8 +48,12 @@ export interface PricingTier {
|
||||||
hours: string;
|
hours: string;
|
||||||
features: string[];
|
features: string[];
|
||||||
stripePriceId: string;
|
stripePriceId: string;
|
||||||
|
yearlyStripePriceId?: string; // Add yearly price ID support
|
||||||
upgradePlans: UpgradePlan[];
|
upgradePlans: UpgradePlan[];
|
||||||
hidden?: boolean; // Optional property to hide plans from display while keeping them in code
|
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 = {
|
export const siteConfig = {
|
||||||
|
@ -117,7 +122,7 @@ export const siteConfig = {
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
hours: '60 min',
|
hours: '60 min',
|
||||||
features: [
|
features: [
|
||||||
'$5 of usage',
|
'$5/month usage',
|
||||||
'Public Projects',
|
'Public Projects',
|
||||||
'Basic Model (Limited capabilities)',
|
'Basic Model (Limited capabilities)',
|
||||||
],
|
],
|
||||||
|
@ -127,6 +132,9 @@ export const siteConfig = {
|
||||||
{
|
{
|
||||||
name: 'Plus',
|
name: 'Plus',
|
||||||
price: '$20',
|
price: '$20',
|
||||||
|
yearlyPrice: '$204',
|
||||||
|
originalYearlyPrice: '$240',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Free, plus:',
|
description: 'Everything in Free, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-primary text-white dark:text-black',
|
buttonColor: 'bg-primary text-white dark:text-black',
|
||||||
|
@ -134,16 +142,20 @@ export const siteConfig = {
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
hours: '2 hours',
|
hours: '2 hours',
|
||||||
features: [
|
features: [
|
||||||
'$20 of usage',
|
'$20/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Pro',
|
name: 'Pro',
|
||||||
price: '$50',
|
price: '$50',
|
||||||
|
yearlyPrice: '$510',
|
||||||
|
originalYearlyPrice: '$600',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Free, plus:',
|
description: 'Everything in Free, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-secondary text-white',
|
buttonColor: 'bg-secondary text-white',
|
||||||
|
@ -151,76 +163,92 @@ export const siteConfig = {
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
hours: '6 hours',
|
hours: '6 hours',
|
||||||
features: [
|
features: [
|
||||||
'$50 of usage',
|
'$50/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Business',
|
name: 'Business',
|
||||||
price: '$100',
|
price: '$100',
|
||||||
|
yearlyPrice: '$1020',
|
||||||
|
originalYearlyPrice: '$1200',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Pro, plus:',
|
description: 'Everything in Pro, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-secondary text-white',
|
buttonColor: 'bg-secondary text-white',
|
||||||
isPopular: false,
|
isPopular: false,
|
||||||
hours: '12 hours',
|
hours: '12 hours',
|
||||||
features: [
|
features: [
|
||||||
'12 hours',
|
'$100/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
'Priority support',
|
'Priority support',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Ultra',
|
name: 'Ultra',
|
||||||
price: '$200',
|
price: '$200',
|
||||||
|
yearlyPrice: '$2040',
|
||||||
|
originalYearlyPrice: '$2400',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Free, plus:',
|
description: 'Everything in Free, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-primary text-white dark:text-black',
|
buttonColor: 'bg-primary text-white dark:text-black',
|
||||||
isPopular: false,
|
isPopular: false,
|
||||||
hours: '25 hours',
|
hours: '25 hours',
|
||||||
features: [
|
features: [
|
||||||
'$200 of usage',
|
'$200/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Enterprise',
|
name: 'Enterprise',
|
||||||
price: '$400',
|
price: '$400',
|
||||||
|
yearlyPrice: '$4080',
|
||||||
|
originalYearlyPrice: '$4800',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Ultra, plus:',
|
description: 'Everything in Ultra, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-secondary text-white',
|
buttonColor: 'bg-secondary text-white',
|
||||||
isPopular: false,
|
isPopular: false,
|
||||||
hours: '50 hours',
|
hours: '50 hours',
|
||||||
features: [
|
features: [
|
||||||
'50 hours',
|
'$400/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
'Priority support',
|
'Priority support',
|
||||||
'Custom integrations',
|
'Custom integrations',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Scale',
|
name: 'Scale',
|
||||||
price: '$800',
|
price: '$800',
|
||||||
|
yearlyPrice: '$8160',
|
||||||
|
originalYearlyPrice: '$9600',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Enterprise, plus:',
|
description: 'Everything in Enterprise, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-secondary text-white',
|
buttonColor: 'bg-secondary text-white',
|
||||||
isPopular: false,
|
isPopular: false,
|
||||||
hours: '125 hours',
|
hours: '125 hours',
|
||||||
features: [
|
features: [
|
||||||
'125 hours',
|
'$800/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
'Priority support',
|
'Priority support',
|
||||||
|
@ -228,19 +256,23 @@ export const siteConfig = {
|
||||||
'Dedicated account manager',
|
'Dedicated account manager',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Premium',
|
name: 'Premium',
|
||||||
price: '$1000',
|
price: '$1000',
|
||||||
|
yearlyPrice: '$10200',
|
||||||
|
originalYearlyPrice: '$12000',
|
||||||
|
discountPercentage: 15,
|
||||||
description: 'Everything in Scale, plus:',
|
description: 'Everything in Scale, plus:',
|
||||||
buttonText: 'Try Free',
|
buttonText: 'Try Free',
|
||||||
buttonColor: 'bg-secondary text-white',
|
buttonColor: 'bg-secondary text-white',
|
||||||
isPopular: false,
|
isPopular: false,
|
||||||
hours: '200 hours',
|
hours: '200 hours',
|
||||||
features: [
|
features: [
|
||||||
'200 hours',
|
'$1000/month usage',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Access to intelligent Model (Full Suna)',
|
'Access to intelligent Model (Full Suna)',
|
||||||
'Priority support',
|
'Priority support',
|
||||||
|
@ -249,6 +281,7 @@ export const siteConfig = {
|
||||||
'Custom SLA',
|
'Custom SLA',
|
||||||
],
|
],
|
||||||
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
|
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
|
||||||
|
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000_YEARLY.priceId,
|
||||||
upgradePlans: [],
|
upgradePlans: [],
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue