mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 09:22:05 +10:00
Compare commits
3 Commits
fix/editor
...
fix/billin
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f3efe2a8 | |||
| 1d87c0085c | |||
| c2c165528b |
@ -117,7 +117,8 @@ export default function BillingDetails() {
|
||||
{billing.billingScheme === "tiered" && (
|
||||
<>
|
||||
<Text fw={700} fz="lg">
|
||||
${billing.amount / 100} {billing.currency.toUpperCase()}
|
||||
${billing.amount / 100} {billing.currency.toUpperCase()} /{" "}
|
||||
{billing.interval}
|
||||
</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
per {billing.interval}
|
||||
@ -129,7 +130,7 @@ export default function BillingDetails() {
|
||||
<>
|
||||
<Text fw={700} fz="lg">
|
||||
{(billing.amount / 100) * billing.quantity}{" "}
|
||||
{billing.currency.toUpperCase()}
|
||||
{billing.currency.toUpperCase()} / {billing.interval}
|
||||
</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
${billing.amount / 100} /user/{billing.interval}
|
||||
|
||||
@ -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<string | 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) {
|
||||
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 (
|
||||
<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 */}
|
||||
<Stack gap="xl" mb="md">
|
||||
{/* Team Size and Billing Controls */}
|
||||
<Group justify="center" align="center" gap="sm">
|
||||
<Select
|
||||
label="Team size"
|
||||
description="Select the number of users"
|
||||
value={selectedTierValue}
|
||||
onChange={setSelectedTierValue}
|
||||
data={selectData}
|
||||
w={250}
|
||||
size="md"
|
||||
allowDeselect={false}
|
||||
/>
|
||||
{hasTieredPlans && (
|
||||
<Select
|
||||
label="Team size"
|
||||
description="Select the number of users"
|
||||
value={selectedTierValue}
|
||||
onChange={setSelectedTierValue}
|
||||
data={selectData}
|
||||
w={250}
|
||||
size="md"
|
||||
allowDeselect={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Group justify="center" align="start">
|
||||
<Flex justify="center" gap="md" align="center">
|
||||
@ -102,17 +133,29 @@ export default function BillingPlans() {
|
||||
{/* Plans Grid */}
|
||||
<Group justify="center" gap="lg" align="stretch">
|
||||
{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 (
|
||||
<Card
|
||||
key={plan.name}
|
||||
@ -143,10 +186,12 @@ export default function BillingPlans() {
|
||||
<Stack gap="xs">
|
||||
<Group align="baseline" gap="xs">
|
||||
<Title order={1} size="h1">
|
||||
${isAnnual ? (price / 12).toFixed(0) : price}
|
||||
${displayPrice}
|
||||
</Title>
|
||||
<Text size="lg" c="dimmed">
|
||||
per {isAnnual ? "month" : "month"}
|
||||
{plan.billingScheme === 'per_unit'
|
||||
? `per user/month`
|
||||
: `per month`}
|
||||
</Text>
|
||||
</Group>
|
||||
{isAnnual && (
|
||||
@ -154,14 +199,16 @@ export default function BillingPlans() {
|
||||
Billed annually
|
||||
</Text>
|
||||
)}
|
||||
<Text size="md" fw={500}>
|
||||
For {planSelectedTier.upTo} users
|
||||
</Text>
|
||||
{plan.billingScheme === 'tiered' && plan.pricingTiers && (
|
||||
<Text size="md" fw={500}>
|
||||
For {plan.pricingTiers.find(tier => tier.upTo.toString() === selectedTierValue)?.upTo || plan.pricingTiers[0].upTo} users
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* CTA Button */}
|
||||
<Button onClick={() => handleCheckout(priceId)} fullWidth>
|
||||
Upgrade
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
{/* Features */}
|
||||
|
||||
@ -53,7 +53,7 @@ export interface IBillingPlan {
|
||||
};
|
||||
features: string[];
|
||||
billingScheme: string | null;
|
||||
pricingTiers: PricingTier[];
|
||||
pricingTiers?: PricingTier[];
|
||||
}
|
||||
|
||||
interface PricingTier {
|
||||
|
||||
@ -126,7 +126,15 @@ export default function PageEditor({
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
const isTokenExpired = now >= payload.exp;
|
||||
if (isTokenExpired) {
|
||||
refetchCollabToken();
|
||||
refetchCollabToken().then((result) => {
|
||||
if (result.data?.token) {
|
||||
remote.disconnect();
|
||||
setTimeout(() => {
|
||||
remote.configuration.token = result.data.token;
|
||||
remote.connect();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onStatus: (status) => {
|
||||
@ -152,6 +160,21 @@ export default function PageEditor({
|
||||
};
|
||||
}, [pageId]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
// Handle token updates by reconnecting with new token
|
||||
if (providersRef.current?.remote && collabQuery?.token) {
|
||||
const currentToken = providersRef.current.remote.configuration.token;
|
||||
if (currentToken !== collabQuery.token) {
|
||||
// Token has changed, need to reconnect with new token
|
||||
providersRef.current.remote.disconnect();
|
||||
providersRef.current.remote.configuration.token = collabQuery.token;
|
||||
providersRef.current.remote.connect();
|
||||
}
|
||||
}
|
||||
}, [collabQuery?.token]);
|
||||
*/
|
||||
|
||||
// Only connect/disconnect on tab/idle, not destroy
|
||||
useEffect(() => {
|
||||
if (!providersReady || !providersRef.current) return;
|
||||
|
||||
Submodule apps/server/src/ee updated: 4c252d1ec3...49a16ab3e0
Reference in New Issue
Block a user