diff --git a/apps/client/src/ee/billing/components/billing-details.tsx b/apps/client/src/ee/billing/components/billing-details.tsx index a4ea9547..0fb06147 100644 --- a/apps/client/src/ee/billing/components/billing-details.tsx +++ b/apps/client/src/ee/billing/components/billing-details.tsx @@ -117,7 +117,8 @@ export default function BillingDetails() { {billing.billingScheme === "tiered" && ( <> - ${billing.amount / 100} {billing.currency.toUpperCase()} + ${billing.amount / 100} {billing.currency.toUpperCase()} /{" "} + {billing.interval} per {billing.interval} @@ -129,7 +130,7 @@ export default function BillingDetails() { <> {(billing.amount / 100) * billing.quantity}{" "} - {billing.currency.toUpperCase()} + {billing.currency.toUpperCase()} / {billing.interval} ${billing.amount / 100} /user/{billing.interval} diff --git a/apps/client/src/ee/billing/components/billing-plans.tsx b/apps/client/src/ee/billing/components/billing-plans.tsx index 8d5f28d3..5bff1485 100644 --- a/apps/client/src/ee/billing/components/billing-plans.tsx +++ b/apps/client/src/ee/billing/components/billing-plans.tsx @@ -12,14 +12,18 @@ import { Badge, Flex, Switch, + Alert, } from "@mantine/core"; import { useState } from "react"; -import { IconCheck } from "@tabler/icons-react"; +import { IconCheck, IconInfoCircle } from "@tabler/icons-react"; import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts"; import { useBillingPlans } from "@/ee/billing/queries/billing-query.ts"; +import { useAtomValue } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom"; export default function BillingPlans() { const { data: plans } = useBillingPlans(); + const workspace = useAtomValue(workspaceAtom); const [isAnnual, setIsAnnual] = useState(true); const [selectedTierValue, setSelectedTierValue] = useState( null, @@ -36,49 +40,76 @@ export default function BillingPlans() { } }; + // TODO: remove by July 30. + // Check if workspace was created between June 28 and July 14, 2025 + const showTieredPricingNotice = (() => { + if (!workspace?.createdAt) return false; + const createdDate = new Date(workspace.createdAt); + const startDate = new Date('2025-06-20'); + const endDate = new Date('2025-07-14'); + return createdDate >= startDate && createdDate <= endDate; + })(); + if (!plans || plans.length === 0) { return null; } - const firstPlan = plans[0]; + // Check if any plan is tiered + const hasTieredPlans = plans.some(plan => plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0); + const firstTieredPlan = plans.find(plan => plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0); - // Set initial tier value if not set - if (!selectedTierValue && firstPlan.pricingTiers.length > 0) { - setSelectedTierValue(firstPlan.pricingTiers[0].upTo.toString()); + // Set initial tier value if not set and we have tiered plans + if (hasTieredPlans && !selectedTierValue && firstTieredPlan) { + setSelectedTierValue(firstTieredPlan.pricingTiers[0].upTo.toString()); return null; } - if (!selectedTierValue) { + // For tiered plans, ensure we have a selected tier + if (hasTieredPlans && !selectedTierValue) { return null; } - const selectData = firstPlan.pricingTiers - .filter((tier) => !tier.custom) + const selectData = firstTieredPlan?.pricingTiers + ?.filter((tier) => !tier.custom) .map((tier, index) => { const prevMaxUsers = - index > 0 ? firstPlan.pricingTiers[index - 1].upTo : 0; + index > 0 ? firstTieredPlan.pricingTiers[index - 1].upTo : 0; return { value: tier.upTo.toString(), label: `${prevMaxUsers + 1}-${tier.upTo} users`, }; - }); + }) || []; return ( + {/* Tiered pricing notice for eligible workspaces */} + {showTieredPricingNotice && !hasTieredPlans && ( + } + title="Want the old tiered pricing?" + color="blue" + mb="lg" + > + Contact support to switch back to our tiered pricing model. + + )} + {/* Controls Section */} {/* Team Size and Billing Controls */} - + )} @@ -102,17 +133,29 @@ export default function BillingPlans() { {/* Plans Grid */} {plans.map((plan, index) => { - const tieredPlan = plan; - const planSelectedTier = - tieredPlan.pricingTiers.find( - (tier) => tier.upTo.toString() === selectedTierValue, - ) || tieredPlan.pricingTiers[0]; - - const price = isAnnual - ? planSelectedTier.yearly - : planSelectedTier.monthly; + let price; + let displayPrice; const priceId = isAnnual ? plan.yearlyId : plan.monthlyId; + if (plan.billingScheme === 'tiered' && plan.pricingTiers?.length > 0) { + // Tiered billing logic + const planSelectedTier = + plan.pricingTiers.find( + (tier) => tier.upTo.toString() === selectedTierValue, + ) || plan.pricingTiers[0]; + + price = isAnnual + ? planSelectedTier.yearly + : planSelectedTier.monthly; + displayPrice = isAnnual ? (price / 12).toFixed(0) : price; + } else { + // Per-unit billing logic + const monthlyPrice = parseFloat(plan.price?.monthly || '0'); + const yearlyPrice = parseFloat(plan.price?.yearly || '0'); + price = isAnnual ? yearlyPrice : monthlyPrice; + displayPrice = isAnnual ? (yearlyPrice / 12).toFixed(0) : monthlyPrice; + } + return ( - ${isAnnual ? (price / 12).toFixed(0) : price} + ${displayPrice} - per {isAnnual ? "month" : "month"} + {plan.billingScheme === 'per_unit' + ? `per user/month` + : `per month`} {isAnnual && ( @@ -154,14 +199,16 @@ export default function BillingPlans() { Billed annually )} - - For {planSelectedTier.upTo} users - + {plan.billingScheme === 'tiered' && plan.pricingTiers && ( + + For {plan.pricingTiers.find(tier => tier.upTo.toString() === selectedTierValue)?.upTo || plan.pricingTiers[0].upTo} users + + )} {/* CTA Button */} {/* Features */} diff --git a/apps/client/src/ee/billing/types/billing.types.ts b/apps/client/src/ee/billing/types/billing.types.ts index dfa1a60b..58225519 100644 --- a/apps/client/src/ee/billing/types/billing.types.ts +++ b/apps/client/src/ee/billing/types/billing.types.ts @@ -53,7 +53,7 @@ export interface IBillingPlan { }; features: string[]; billingScheme: string | null; - pricingTiers: PricingTier[]; + pricingTiers?: PricingTier[]; } interface PricingTier { diff --git a/apps/server/src/ee b/apps/server/src/ee index 4c252d1e..49a16ab3 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 4c252d1ec35a3fb13c8eaf19509de83cf5fe2779 +Subproject commit 49a16ab3e03971a375bcbfac60c3c1150d19059b