From e856c8eb691e67333bbf6b22dfd5c5d16c8b0c30 Mon Sep 17 00:00:00 2001
From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
Date: Mon, 14 Jul 2025 02:34:18 -0700
Subject: [PATCH] (cloud) fix: updates to billing (#1367)
* billing updates (cloud)
* old billing grace period
---
.../ee/billing/components/billing-details.tsx | 5 +-
.../ee/billing/components/billing-plans.tsx | 117 ++++++++++++------
.../src/ee/billing/types/billing.types.ts | 2 +-
apps/server/src/ee | 2 +-
4 files changed, 87 insertions(+), 39 deletions(-)
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 */}
-
+ {hasTieredPlans && (
+
+ )}
@@ -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