mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 01:42:06 +10:00
Compare commits
2 Commits
main
...
fix/billin
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f3efe2a8 | |||
| 1d87c0085c |
@ -117,7 +117,8 @@ export default function BillingDetails() {
|
|||||||
{billing.billingScheme === "tiered" && (
|
{billing.billingScheme === "tiered" && (
|
||||||
<>
|
<>
|
||||||
<Text fw={700} fz="lg">
|
<Text fw={700} fz="lg">
|
||||||
${billing.amount / 100} {billing.currency.toUpperCase()}
|
${billing.amount / 100} {billing.currency.toUpperCase()} /{" "}
|
||||||
|
{billing.interval}
|
||||||
</Text>
|
</Text>
|
||||||
<Text c="dimmed" fz="sm">
|
<Text c="dimmed" fz="sm">
|
||||||
per {billing.interval}
|
per {billing.interval}
|
||||||
@ -129,7 +130,7 @@ export default function BillingDetails() {
|
|||||||
<>
|
<>
|
||||||
<Text fw={700} fz="lg">
|
<Text fw={700} fz="lg">
|
||||||
{(billing.amount / 100) * billing.quantity}{" "}
|
{(billing.amount / 100) * billing.quantity}{" "}
|
||||||
{billing.currency.toUpperCase()}
|
{billing.currency.toUpperCase()} / {billing.interval}
|
||||||
</Text>
|
</Text>
|
||||||
<Text c="dimmed" fz="sm">
|
<Text c="dimmed" fz="sm">
|
||||||
${billing.amount / 100} /user/{billing.interval}
|
${billing.amount / 100} /user/{billing.interval}
|
||||||
|
|||||||
@ -12,14 +12,18 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Flex,
|
Flex,
|
||||||
Switch,
|
Switch,
|
||||||
|
Alert,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useState } from "react";
|
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 { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
|
||||||
import { useBillingPlans } from "@/ee/billing/queries/billing-query.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() {
|
export default function BillingPlans() {
|
||||||
const { data: plans } = useBillingPlans();
|
const { data: plans } = useBillingPlans();
|
||||||
|
const workspace = useAtomValue(workspaceAtom);
|
||||||
const [isAnnual, setIsAnnual] = useState(true);
|
const [isAnnual, setIsAnnual] = useState(true);
|
||||||
const [selectedTierValue, setSelectedTierValue] = useState<string | null>(
|
const [selectedTierValue, setSelectedTierValue] = useState<string | null>(
|
||||||
null,
|
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) {
|
if (!plans || plans.length === 0) {
|
||||||
return null;
|
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
|
// Set initial tier value if not set and we have tiered plans
|
||||||
if (!selectedTierValue && firstPlan.pricingTiers.length > 0) {
|
if (hasTieredPlans && !selectedTierValue && firstTieredPlan) {
|
||||||
setSelectedTierValue(firstPlan.pricingTiers[0].upTo.toString());
|
setSelectedTierValue(firstTieredPlan.pricingTiers[0].upTo.toString());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedTierValue) {
|
// For tiered plans, ensure we have a selected tier
|
||||||
|
if (hasTieredPlans && !selectedTierValue) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectData = firstPlan.pricingTiers
|
const selectData = firstTieredPlan?.pricingTiers
|
||||||
.filter((tier) => !tier.custom)
|
?.filter((tier) => !tier.custom)
|
||||||
.map((tier, index) => {
|
.map((tier, index) => {
|
||||||
const prevMaxUsers =
|
const prevMaxUsers =
|
||||||
index > 0 ? firstPlan.pricingTiers[index - 1].upTo : 0;
|
index > 0 ? firstTieredPlan.pricingTiers[index - 1].upTo : 0;
|
||||||
return {
|
return {
|
||||||
value: tier.upTo.toString(),
|
value: tier.upTo.toString(),
|
||||||
label: `${prevMaxUsers + 1}-${tier.upTo} users`,
|
label: `${prevMaxUsers + 1}-${tier.upTo} users`,
|
||||||
};
|
};
|
||||||
});
|
}) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl">
|
<Container size="xl" py="xl">
|
||||||
|
{/* Tiered pricing notice for eligible workspaces */}
|
||||||
|
{showTieredPricingNotice && !hasTieredPlans && (
|
||||||
|
<Alert
|
||||||
|
icon={<IconInfoCircle size={16} />}
|
||||||
|
title="Want the old tiered pricing?"
|
||||||
|
color="blue"
|
||||||
|
mb="lg"
|
||||||
|
>
|
||||||
|
Contact support to switch back to our tiered pricing model.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Controls Section */}
|
{/* Controls Section */}
|
||||||
<Stack gap="xl" mb="md">
|
<Stack gap="xl" mb="md">
|
||||||
{/* Team Size and Billing Controls */}
|
{/* Team Size and Billing Controls */}
|
||||||
<Group justify="center" align="center" gap="sm">
|
<Group justify="center" align="center" gap="sm">
|
||||||
<Select
|
{hasTieredPlans && (
|
||||||
label="Team size"
|
<Select
|
||||||
description="Select the number of users"
|
label="Team size"
|
||||||
value={selectedTierValue}
|
description="Select the number of users"
|
||||||
onChange={setSelectedTierValue}
|
value={selectedTierValue}
|
||||||
data={selectData}
|
onChange={setSelectedTierValue}
|
||||||
w={250}
|
data={selectData}
|
||||||
size="md"
|
w={250}
|
||||||
allowDeselect={false}
|
size="md"
|
||||||
/>
|
allowDeselect={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Group justify="center" align="start">
|
<Group justify="center" align="start">
|
||||||
<Flex justify="center" gap="md" align="center">
|
<Flex justify="center" gap="md" align="center">
|
||||||
@ -102,17 +133,29 @@ export default function BillingPlans() {
|
|||||||
{/* Plans Grid */}
|
{/* Plans Grid */}
|
||||||
<Group justify="center" gap="lg" align="stretch">
|
<Group justify="center" gap="lg" align="stretch">
|
||||||
{plans.map((plan, index) => {
|
{plans.map((plan, index) => {
|
||||||
const tieredPlan = plan;
|
let price;
|
||||||
const planSelectedTier =
|
let displayPrice;
|
||||||
tieredPlan.pricingTiers.find(
|
|
||||||
(tier) => tier.upTo.toString() === selectedTierValue,
|
|
||||||
) || tieredPlan.pricingTiers[0];
|
|
||||||
|
|
||||||
const price = isAnnual
|
|
||||||
? planSelectedTier.yearly
|
|
||||||
: planSelectedTier.monthly;
|
|
||||||
const priceId = isAnnual ? plan.yearlyId : plan.monthlyId;
|
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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={plan.name}
|
key={plan.name}
|
||||||
@ -143,10 +186,12 @@ export default function BillingPlans() {
|
|||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group align="baseline" gap="xs">
|
<Group align="baseline" gap="xs">
|
||||||
<Title order={1} size="h1">
|
<Title order={1} size="h1">
|
||||||
${isAnnual ? (price / 12).toFixed(0) : price}
|
${displayPrice}
|
||||||
</Title>
|
</Title>
|
||||||
<Text size="lg" c="dimmed">
|
<Text size="lg" c="dimmed">
|
||||||
per {isAnnual ? "month" : "month"}
|
{plan.billingScheme === 'per_unit'
|
||||||
|
? `per user/month`
|
||||||
|
: `per month`}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
{isAnnual && (
|
{isAnnual && (
|
||||||
@ -154,14 +199,16 @@ export default function BillingPlans() {
|
|||||||
Billed annually
|
Billed annually
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text size="md" fw={500}>
|
{plan.billingScheme === 'tiered' && plan.pricingTiers && (
|
||||||
For {planSelectedTier.upTo} users
|
<Text size="md" fw={500}>
|
||||||
</Text>
|
For {plan.pricingTiers.find(tier => tier.upTo.toString() === selectedTierValue)?.upTo || plan.pricingTiers[0].upTo} users
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<Button onClick={() => handleCheckout(priceId)} fullWidth>
|
<Button onClick={() => handleCheckout(priceId)} fullWidth>
|
||||||
Upgrade
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Features */}
|
{/* Features */}
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export interface IBillingPlan {
|
|||||||
};
|
};
|
||||||
features: string[];
|
features: string[];
|
||||||
billingScheme: string | null;
|
billingScheme: string | null;
|
||||||
pricingTiers: PricingTier[];
|
pricingTiers?: PricingTier[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PricingTier {
|
interface PricingTier {
|
||||||
|
|||||||
Submodule apps/server/src/ee updated: 4c252d1ec3...49a16ab3e0
Reference in New Issue
Block a user