From c076086a1fddfe0ea6353cab5e0e110ce3dc0fee Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Wed, 2 Sep 2026 14:55:36 +0100 Subject: [PATCH] refactor(checkout): sell Team capacity in users, not servers Marketing's mockup sells the plan by user count, not by the thing Stripe happens to bill for. The buyer picks 100 / 200 / 300 / 400 or Other; the line item prices it "Team - $99/mo per 100 users". Nothing customer-facing says "server" any more. That reverses the D291 rule the earlier prototype set, where selling surfaces spoke servers and measuring surfaces spoke users. Both now speak users, which is also what the Usage & Billing design does. Blocks survive only where they have to: server_quantity on the checkout request is what the subscription line item counts, so it stays in blocks and the stage translates. "Other" rounds up to the next whole block, because a part block cannot be bought. Also renames the tier from Server to Team in licenseService, which supplies the modal title. Main already says "Team plan" everywhere in copy and ships an unused [plan.team] key; the tier name was the last place still saying Server. --- .../public/locales/en-US/translation.toml | 22 +-- .../shared/stripeCheckout/StripeCheckout.tsx | 10 +- .../stripeCheckout/hooks/useCheckoutState.ts | 2 +- .../stages/CapacityStage.stories.tsx | 20 +-- .../stripeCheckout/stages/CapacityStage.tsx | 165 +++++++++++------- .../stripeCheckout/utils/capacity.test.ts | 52 +++--- .../shared/stripeCheckout/utils/capacity.ts | 39 +++-- .../stripeCheckout/utils/checkoutUtils.ts | 8 +- .../proprietary/services/licenseService.ts | 8 +- 9 files changed, 184 insertions(+), 142 deletions(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index d24860a8ef..4d8018ee1a 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -6279,19 +6279,21 @@ upgradeTitle = "Upgrade to {{planName}}" yearly = "Yearly" [payment.capacityStage] -addedUsers = "+{{users}} users" continue = "Continue to payment" -coversUpTo = "Covers up to" +customHint = "Rounded up to the next block of {{users}}." +customLabel = "Number of users" +dueToday = "Due today" enterpriseQuote = "Get an enterprise quote" -heading = "How many users do you need?" -lineItem = "Servers x {{servers}}" -minimumForCurrentUsers = "You have {{users}} users, so you need at least {{servers}} servers." -modalTitle = "Choose Capacity - {{planName}}" -serversLabel = "Servers" -subheading = "Each server covers {{users}} users. Add servers to cover more." -totalMonthly = "Total, billed monthly" -totalYearly = "Total, billed yearly" +lineItem = "Team · {{price}}{{period}} per {{block}} users" +minimumForCurrentUsers = "You have {{users}} users, so the plan must cover at least {{minimum}}." +modalTitle = "Upgrade to {{planName}}" +other = "Other" +perMonth = "/mo" +perYear = "/yr" +renewalNote = "Renews at {{total}}{{period}}. Cancel any time in Usage & Billing." +subheading = "Covers everyone you invite, in blocks of {{users}} users." userTotal = "{{users}} users" +usersLabel = "Users" [payment.emailStage] continue = "Continue" diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx b/frontend/editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx index 6cf6818b7e..47589389e8 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/StripeCheckout.tsx @@ -19,7 +19,7 @@ import { useCheckoutSession } from "@app/components/shared/stripeCheckout/hooks/ import { EmailStage } from "@app/components/shared/stripeCheckout/stages/EmailStage"; import { PlanSelectionStage } from "@app/components/shared/stripeCheckout/stages/PlanSelectionStage"; import { CapacityStage } from "@app/components/shared/stripeCheckout/stages/CapacityStage"; -import { serversForUsers } from "@app/components/shared/stripeCheckout/utils/capacity"; +import { blocksForUsers } from "@app/components/shared/stripeCheckout/utils/capacity"; import { PaymentStage } from "@app/components/shared/stripeCheckout/stages/PaymentStage"; import { SuccessStage } from "@app/components/shared/stripeCheckout/stages/SuccessStage"; import { ErrorStage } from "@app/components/shared/stripeCheckout/stages/ErrorStage"; @@ -109,16 +109,16 @@ const StripeCheckout: React.FC = ({ } }; - // Only the Server tier is sold by the server. Enterprise is priced per seat and free has nothing - // to size, so both go straight to payment. + // Only the Team tier is sold by capacity. Enterprise is priced per seat and free has nothing to + // size, so both go straight to payment. const sellsCapacity = planGroup.tier === "server"; // Plan selection handler const handlePlanSelect = (period: "monthly" | "yearly") => { checkoutState.setSelectedPeriod(period); if (sellsCapacity) { - // Arrive on the capacity an installation already needs rather than on a blocked "1". - checkoutState.setServerQuantity(serversForUsers(minimumSeats)); + // Arrive on the capacity an installation already needs rather than on a blocked minimum. + checkoutState.setServerQuantity(blocksForUsers(minimumSeats)); navigation.goToStage("capacity"); return; } diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/hooks/useCheckoutState.ts b/frontend/editor/src/proprietary/components/shared/stripeCheckout/hooks/useCheckoutState.ts index 0a5dcff584..3529566edb 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/hooks/useCheckoutState.ts +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/hooks/useCheckoutState.ts @@ -20,7 +20,7 @@ export const useCheckoutState = (planGroup: PlanTierGroup) => { const [selectedPeriod, setSelectedPeriod] = useState<"monthly" | "yearly">( planGroup.yearly ? "yearly" : "monthly", ); - // Servers to buy. Only the Server tier asks; every other tier stays at one. + // Blocks of users to buy. Only the Team tier asks; every other tier stays at one. const [serverQuantity, setServerQuantity] = useState(1); const [installationId, setInstallationId] = useState(null); const [currentLicenseKey, setCurrentLicenseKey] = useState( diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.stories.tsx b/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.stories.tsx index 69b3cb3dd2..5e4177a418 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.stories.tsx +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.stories.tsx @@ -4,12 +4,12 @@ import { CapacityStage } from "@app/components/shared/stripeCheckout/stages/Capa import { PlanTier } from "@app/services/licenseService"; /** - * The capacity step of the Stripe checkout, between billing period and payment. Only the Server - * tier reaches it: the stepper counts servers, and every figure beside it is stated in users. + * The capacity step of the Stripe checkout, between billing period and payment. Only the Team tier + * reaches it: the buyer picks users, and the plan is priced per block of 100. */ const yearlyPlan: PlanTier = { id: "server-yearly", - name: "Server", + name: "Team", price: 990, currency: "$", period: "/year", @@ -50,16 +50,16 @@ const meta = { export default meta; type Story = StoryObj; -/** A fresh purchase: one server, 100 users. */ -export const SingleServer: Story = {}; +/** A fresh purchase: the smallest block, 100 users. */ +export const SingleBlock: Story = {}; -/** Three servers on the monthly plan, so the total is three times the unit price. */ -export const MultipleServersMonthly: Story = { +/** 300 users on the monthly plan, so the total is three block prices. */ +export const ThreeBlocksMonthly: Story = { args: { selectedPlan: monthlyPlan, serverQuantity: 3 }, }; /** - * An installation already running 240 users cannot buy fewer than three servers. Reducing capacity + * An installation already running 240 users cannot buy cover for fewer than 300. Reducing capacity * is a renewal conversation, not something checkout does by stranding accounts. */ export const ConstrainedByCurrentUsers: Story = { @@ -72,8 +72,8 @@ export const BelowCurrentUsage: Story = { }; /** - * At five servers the enterprise quote is offered beside the purchase. It is an option, never a - * wall: self-serve checkout still completes. + * At the self-serve maximum the enterprise quote is offered beside the purchase. It is an option, + * never a wall: self-serve checkout still completes. */ export const OffersEnterpriseQuote: Story = { args: { serverQuantity: 5, onContactSales: () => {} }, diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.tsx b/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.tsx index bfed8470d4..4cd131b3f9 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.tsx +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/stages/CapacityStage.tsx @@ -1,20 +1,25 @@ -import React from "react"; +import React, { useState } from "react"; import { Stack, Text, Group, Divider, Alert, NumberInput } from "@mantine/core"; import { Button } from "@app/ui/Button"; import { useTranslation } from "react-i18next"; import { PlanTier } from "@app/services/licenseService"; import { formatPrice } from "@app/components/shared/stripeCheckout/utils/pricingUtils"; import { - USERS_PER_SERVER, - SELF_SERVE_MAX_SERVERS, - usersForServers, - serversForUsers, + USERS_PER_BLOCK, + SELF_SERVE_MAX_BLOCKS, + USER_PRESETS, + usersForBlocks, + blocksForUsers, shouldOfferEnterprise, } from "@app/components/shared/stripeCheckout/utils/capacity"; interface CapacityStageProps { - /** The plan the buyer picked a billing period for; supplies unit price and currency. */ + /** The plan the buyer picked a billing period for; supplies block price and currency. */ selectedPlan: PlanTier | null; + /** + * Blocks of users being bought. Held in blocks because that is what the Stripe line item counts, + * but nothing shown to the buyer says so. + */ serverQuantity: number; setServerQuantity: (quantity: number) => void; /** Users already on this installation, so capacity cannot be set below what is in use. */ @@ -24,10 +29,11 @@ interface CapacityStageProps { } /** - * Pick how much capacity to buy. + * Choose how many users the Team plan should cover. * - * The stepper counts servers because that is the unit we sell; every figure beside it is stated in - * users because that is the unit an admin measures. The line item does the translation. + * The buyer picks users; the plan is priced per block of {@link USERS_PER_BLOCK}. Presets cover the + * common sizes and "Other" opens a free entry that rounds up to the next whole block, because a + * part-block cannot be bought. */ export const CapacityStage: React.FC = ({ selectedPlan, @@ -40,69 +46,92 @@ export const CapacityStage: React.FC = ({ const { t } = useTranslation(); const currency = selectedPlan?.currency || "$"; - const unitPrice = selectedPlan?.price || 0; + const blockPrice = selectedPlan?.price || 0; const isYearly = selectedPlan?.period?.includes("year") ?? false; - const covered = usersForServers(serverQuantity); - const total = unitPrice * serverQuantity; + const covered = usersForBlocks(serverQuantity); + const total = blockPrice * serverQuantity; // Never sell less capacity than is already in use; reducing capacity happens at renewal rather - // than by stranding accounts that already exist. The stage is entered pre-seeded to this minimum, - // so the guard below only fires if a caller passes something lower. - const minServers = serversForUsers(currentUsers); - const belowCurrentUsage = serverQuantity < minServers; + // than by stranding accounts that already exist. The stage is entered pre-seeded to this minimum. + const minBlocks = blocksForUsers(currentUsers); + const minUsers = usersForBlocks(minBlocks); + const maxUsers = usersForBlocks(SELF_SERVE_MAX_BLOCKS); + const belowCurrentUsage = serverQuantity < minBlocks; const offerEnterprise = shouldOfferEnterprise(serverQuantity); - const setClamped = (value: number) => - setServerQuantity( - Math.max(minServers, Math.min(SELF_SERVE_MAX_SERVERS, value)), - ); + const presets = USER_PRESETS.filter((users) => users <= maxUsers); + const [showCustom, setShowCustom] = useState( + () => !presets.includes(usersForBlocks(serverQuantity)), + ); + + const period = isYearly + ? t("payment.capacityStage.perYear", "/yr") + : t("payment.capacityStage.perMonth", "/mo"); + + const selectUsers = (users: number) => + setServerQuantity(blocksForUsers(users)); return ( -
- - {t("payment.capacityStage.heading", "How many users do you need?")} - - - {t( - "payment.capacityStage.subheading", - "Each server covers {{users}} users. Add servers to cover more.", - { users: USERS_PER_SERVER }, - )} - -
+ + {t( + "payment.capacityStage.subheading", + "Covers everyone you invite, in blocks of {{users}} users.", + { users: USERS_PER_BLOCK }, + )} + - + + + {t("payment.capacityStage.usersLabel", "Users")} + + {presets.map((users) => ( + + ))} + + + + {showCustom && ( setClamped(Number(value) || minServers)} - min={minServers} - max={SELF_SERVE_MAX_SERVERS} + label={t("payment.capacityStage.customLabel", "Number of users")} + description={t( + "payment.capacityStage.customHint", + "Rounded up to the next block of {{users}}.", + { users: USERS_PER_BLOCK }, + )} + value={covered} + onChange={(value) => selectUsers(Number(value) || minUsers)} + min={minUsers} + max={maxUsers} + step={USERS_PER_BLOCK} clampBehavior="strict" allowDecimal={false} allowNegative={false} - size="lg" - style={{ width: 140 }} + style={{ width: 220 }} /> - - - {t("payment.capacityStage.coversUpTo", "Covers up to")} - - - {t("payment.capacityStage.userTotal", "{{users}} users", { - users: covered, - })} - - - + )} {belowCurrentUsage && ( {t( "payment.capacityStage.minimumForCurrentUsers", - "You have {{users}} users, so you need at least {{servers}} servers.", - { users: currentUsers, servers: minServers }, + "You have {{users}} users, so the plan must cover at least {{minimum}}.", + { users: currentUsers, minimum: minUsers }, )} )} @@ -112,29 +141,37 @@ export const CapacityStage: React.FC = ({ - {t("payment.capacityStage.lineItem", "Servers x {{servers}}", { - servers: serverQuantity, - })} + {t( + "payment.capacityStage.lineItem", + "Team · {{price}}{{period}} per {{block}} users", + { + price: formatPrice(blockPrice, currency, 0), + period, + block: USERS_PER_BLOCK, + }, + )} - - {t("payment.capacityStage.addedUsers", "+{{users}} users", { + + {t("payment.capacityStage.userTotal", "{{users}} users", { users: covered, })} - {isYearly - ? t("payment.capacityStage.totalYearly", "Total, billed yearly") - : t( - "payment.capacityStage.totalMonthly", - "Total, billed monthly", - )} + {t("payment.capacityStage.dueToday", "Due today")} {formatPrice(total, currency)} + + {t( + "payment.capacityStage.renewalNote", + "Renews at {{total}}{{period}}. Cancel any time in Usage & Billing.", + { total: formatPrice(total, currency, 0), period }, + )} + diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.test.ts b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.test.ts index b7b94b9fe0..87c9e66a63 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.test.ts +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.test.ts @@ -1,42 +1,42 @@ import { describe, expect, it } from "vitest"; import { - USERS_PER_SERVER, - SELF_SERVE_MAX_SERVERS, + USERS_PER_BLOCK, + SELF_SERVE_MAX_BLOCKS, ENTERPRISE_ADVISORY_USERS, - usersForServers, - serversForUsers, + usersForBlocks, + blocksForUsers, shouldOfferEnterprise, } from "@app/components/shared/stripeCheckout/utils/capacity"; -describe("usersForServers", () => { - it("multiplies servers by the block size", () => { - expect(usersForServers(1)).toBe(USERS_PER_SERVER); - expect(usersForServers(3)).toBe(USERS_PER_SERVER * 3); +describe("usersForBlocks", () => { + it("multiplies blocks by the block size", () => { + expect(usersForBlocks(1)).toBe(USERS_PER_BLOCK); + expect(usersForBlocks(3)).toBe(USERS_PER_BLOCK * 3); }); - it("treats a missing quantity as one server", () => { + it("treats a missing quantity as one block", () => { // Zero would read as "no capacity" and price the plan at nothing. - expect(usersForServers(0)).toBe(USERS_PER_SERVER); - expect(usersForServers(-2)).toBe(USERS_PER_SERVER); + expect(usersForBlocks(0)).toBe(USERS_PER_BLOCK); + expect(usersForBlocks(-2)).toBe(USERS_PER_BLOCK); }); }); -describe("serversForUsers", () => { - it("rounds part-full servers up", () => { - expect(serversForUsers(1)).toBe(1); - expect(serversForUsers(USERS_PER_SERVER)).toBe(1); - expect(serversForUsers(USERS_PER_SERVER + 1)).toBe(2); - expect(serversForUsers(240)).toBe(3); +describe("blocksForUsers", () => { + it("rounds a part-full block up", () => { + expect(blocksForUsers(1)).toBe(1); + expect(blocksForUsers(USERS_PER_BLOCK)).toBe(1); + expect(blocksForUsers(USERS_PER_BLOCK + 1)).toBe(2); + expect(blocksForUsers(240)).toBe(3); }); - it("never returns zero servers", () => { - // Checkout seeds the stepper from this, and a zero would render a blocked stage. - expect(serversForUsers(0)).toBe(1); - expect(serversForUsers(-5)).toBe(1); + it("never returns zero blocks", () => { + // Checkout seeds the picker from this, and a zero would render a blocked stage. + expect(blocksForUsers(0)).toBe(1); + expect(blocksForUsers(-5)).toBe(1); }); - it("round-trips with usersForServers", () => { - expect(serversForUsers(usersForServers(4))).toBe(4); + it("round-trips with usersForBlocks", () => { + expect(blocksForUsers(usersForBlocks(4))).toBe(4); }); }); @@ -47,12 +47,12 @@ describe("shouldOfferEnterprise", () => { }); it("offers the quote once the purchase reaches the self-serve maximum", () => { - expect(shouldOfferEnterprise(SELF_SERVE_MAX_SERVERS)).toBe(true); + expect(shouldOfferEnterprise(SELF_SERVE_MAX_BLOCKS)).toBe(true); }); it("offers the quote once the resulting capacity is enterprise-sized", () => { - const servers = Math.ceil(ENTERPRISE_ADVISORY_USERS / USERS_PER_SERVER); - expect(usersForServers(servers)).toBeGreaterThanOrEqual( + const servers = Math.ceil(ENTERPRISE_ADVISORY_USERS / USERS_PER_BLOCK); + expect(usersForBlocks(servers)).toBeGreaterThanOrEqual( ENTERPRISE_ADVISORY_USERS, ); expect(shouldOfferEnterprise(servers)).toBe(true); diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.ts b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.ts index b96b4dba22..9af68bd64b 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.ts +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/capacity.ts @@ -1,5 +1,10 @@ /** - * Server-plan capacity: one server grants a block of users. + * Team-plan capacity: the plan is sold in blocks of users. + * + * A block is what Stripe charges for (one unit of `selfhosted:server:*`), but nothing + * customer-facing says so: the buyer picks a number of users and the line item prices it per block. + * Only `server_quantity` on the checkout request speaks in blocks, because that is what the + * subscription line item counts. * * The authoritative block size lives on the pricing policy and is resolved server-side when the * licence is issued, then baked into licence metadata. Before a purchase there is no licence to read @@ -7,34 +12,34 @@ * `pricing_policy.server_plan_user_block`; after purchase the licence is what counts, and the admin * surfaces read `userBlockSize` off `/license-info` rather than this constant. */ -export const USERS_PER_SERVER = 100; +export const USERS_PER_BLOCK = 100; -/** - * Servers a buyer can put through self-serve checkout in one go. Past this the enterprise - * conversation is offered alongside the purchase, never instead of it. - */ -export const SELF_SERVE_MAX_SERVERS = 5; +/** Blocks a buyer can put through self-serve checkout in one go. */ +export const SELF_SERVE_MAX_BLOCKS = 5; -/** Resulting capacity at which an enterprise quote is also worth offering. */ +/** Capacity at which an enterprise quote is also worth offering. */ export const ENTERPRISE_ADVISORY_USERS = 1000; -/** Servers needed to cover a given number of users. Always at least one. */ -export function serversForUsers(users: number): number { - return Math.max(1, Math.ceil(Math.max(0, users) / USERS_PER_SERVER)); +/** The user counts offered as one-click presets, before "Other". */ +export const USER_PRESETS = [100, 200, 300, 400]; + +/** Users covered by a given number of blocks. */ +export function usersForBlocks(blocks: number): number { + return Math.max(1, blocks) * USERS_PER_BLOCK; } -/** Users covered by a given number of servers. */ -export function usersForServers(servers: number): number { - return Math.max(1, servers) * USERS_PER_SERVER; +/** Blocks needed to cover a given number of users. Always at least one. */ +export function blocksForUsers(users: number): number { + return Math.max(1, Math.ceil(Math.max(0, users) / USERS_PER_BLOCK)); } /** * Whether to surface the enterprise door beside the purchase. Deliberately an option rather than a * gate: a buyer past these numbers can still complete self-serve checkout. */ -export function shouldOfferEnterprise(servers: number): boolean { +export function shouldOfferEnterprise(blocks: number): boolean { return ( - servers >= SELF_SERVE_MAX_SERVERS || - usersForServers(servers) >= ENTERPRISE_ADVISORY_USERS + blocks >= SELF_SERVE_MAX_BLOCKS || + usersForBlocks(blocks) >= ENTERPRISE_ADVISORY_USERS ); } diff --git a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/checkoutUtils.ts b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/checkoutUtils.ts index 99ebc01bc6..6b021ff962 100644 --- a/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/checkoutUtils.ts +++ b/frontend/editor/src/proprietary/components/shared/stripeCheckout/utils/checkoutUtils.ts @@ -37,11 +37,9 @@ export const getModalTitle = ( { planName }, ); case "capacity": - return t( - "payment.capacityStage.modalTitle", - "Choose Capacity - {{planName}}", - { planName }, - ); + return t("payment.capacityStage.modalTitle", "Upgrade to {{planName}}", { + planName, + }); case "payment": return t( "payment.paymentStage.modalTitle", diff --git a/frontend/editor/src/proprietary/services/licenseService.ts b/frontend/editor/src/proprietary/services/licenseService.ts index dbd6e5c915..63320ba6ba 100644 --- a/frontend/editor/src/proprietary/services/licenseService.ts +++ b/frontend/editor/src/proprietary/services/licenseService.ts @@ -42,7 +42,7 @@ export interface CheckoutSessionRequest { current_license_key?: string; // Current license key for upgrades requires_seats?: boolean; // Whether to add adjustable seat pricing seat_count?: number; // Initial number of seats for enterprise plans (user can adjust in Stripe UI) - server_quantity?: number; // Servers to buy; each grants a block of users + server_quantity?: number; // Blocks of users to buy; the Stripe line item counts these email?: string; // Customer email for checkout pre-fill successUrl?: string; cancelUrl?: string; @@ -187,7 +187,7 @@ const licenseService = { { id: "selfhosted:server:monthly", lookupKey: "selfhosted:server:monthly", - name: "Server - Monthly", + name: "Team - Monthly", price: getPriceInfo("selfhosted:server:monthly"), currency: currencySymbol, period: "/month", @@ -198,7 +198,7 @@ const licenseService = { { id: "selfhosted:server:yearly", lookupKey: "selfhosted:server:yearly", - name: "Server - Yearly", + name: "Team - Yearly", price: getPriceInfo("selfhosted:server:yearly"), currency: currencySymbol, period: "/year", @@ -298,7 +298,7 @@ const licenseService = { if (serverMonthly || serverYearly) { groups.push({ tier: "server", - name: "Server", + name: "Team", monthly: serverMonthly || null, yearly: serverYearly || null, features: (serverMonthly || serverYearly)!.features,