From 3188fb5295e2188d164498987063c9864e81e611 Mon Sep 17 00:00:00 2001 From: Praise Date: Thu, 29 Jan 2026 10:39:14 +0100 Subject: [PATCH] fix: standardize billing price display to integers when necessary (#17445) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Fixes #17420** ### Problem The billing/pricing display showed inconsistencies: - "Get Started" / trial signup flow: simple `$9` (no decimals) - Pricing page: `$9.00` (with decimals) For yearly plans (with discount), the effective monthly price might not always be an integer, leading to messy floats in UI (e.g., 7.5 showing as 7.499999 or inconsistent formatting). ### Solution Added a helper function `formatYearlyPriceIfNeccessary` that: - Only applies to yearly subscriptions (`SubscriptionInterval.Year`) - Calculates effective monthly price (`price / 12`) - Returns an integer if the result is whole (`Number.isInteger`) - Otherwise formats to exactly 2 decimal places (`toFixed(2)` → Number) This ensures clean, consistent display: - Whole numbers: no decimals (e.g., $9 → 9) - Fractional: precise 2 decimals (e.g., $81 yearly → 6.75) ### Changes - Added `formatYearlyPriceIfNeccessary` function (likely in a utils/pricing file — specify the file path if you know it, e.g. `src/utils/pricing.ts`) - Applied it where yearly effective prices are rendered (e.g., in BillingPlan component, Pricing page, Signup flow — list the files/components you changed) ### Before / After - Before (yearly example with discount): Might show 7.5 or 7.499999 - After: Always 7.5 (or 7 if integer) ### Testing - Ran locally with `yarn dev` - Checked signup flow and pricing page: prices now consistent and clean - Verified non-yearly plans unchanged Let me know if there's a preferred formatting style (e.g., always show .00, or use Intl.NumberFormat) or if this should be applied in more places! Thanks for the great project, my first contribution btw Closes #17420 --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> --- .../modules/billing/components/SubscriptionPrice.tsx | 12 +++++++++++- .../onboarding/internal/ChooseYourPlanContent.tsx | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx b/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx index bfe183ded5..f2eaa95eb7 100644 --- a/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx +++ b/packages/twenty-front/src/modules/billing/components/SubscriptionPrice.tsx @@ -20,10 +20,20 @@ const StyledPriceUnitSpan = styled.span` font-weight: ${({ theme }) => theme.font.weight.medium}; `; +const formatYearlyPriceToMonthly = (price: number): number => { + const monthlyPrice = price / 12; + + return Number.isInteger(monthlyPrice) + ? monthlyPrice + : Number(monthlyPrice.toFixed(2)); +}; + export const SubscriptionPrice = ({ type, price }: SubscriptionPriceProps) => { const { t } = useLingui(); const pricePerSeat = - type === SubscriptionInterval.Year ? (price / 12).toFixed(2) : price; + type === SubscriptionInterval.Year + ? formatYearlyPriceToMonthly(price) + : price; let priceUnit = ''; switch (type) { diff --git a/packages/twenty-front/src/pages/onboarding/internal/ChooseYourPlanContent.tsx b/packages/twenty-front/src/pages/onboarding/internal/ChooseYourPlanContent.tsx index d37ef5b2cc..a227986e43 100644 --- a/packages/twenty-front/src/pages/onboarding/internal/ChooseYourPlanContent.tsx +++ b/packages/twenty-front/src/pages/onboarding/internal/ChooseYourPlanContent.tsx @@ -6,11 +6,14 @@ import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessio import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit'; import { SubscriptionPrice } from '@/billing/components/SubscriptionPrice'; import { TrialCard } from '@/billing/components/TrialCard'; +import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval'; +import { useBaseProductByPlanKey } from '@/billing/hooks/useBaseProductByPlanKey'; import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession'; import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState'; import styled from '@emotion/styled'; import { Trans, useLingui } from '@lingui/react/macro'; import { useRecoilState, useRecoilValue } from 'recoil'; +import { AppPath } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Loader } from 'twenty-ui/feedback'; import { CardPicker, MainButton } from 'twenty-ui/input'; @@ -21,9 +24,6 @@ import { } from 'twenty-ui/navigation'; import { BillingPlanKey } from '~/generated-metadata/graphql'; import { type Billing } from '~/generated/graphql'; -import { AppPath } from 'twenty-shared/types'; -import { useBaseProductByPlanKey } from '@/billing/hooks/useBaseProductByPlanKey'; -import { useBaseLicensedPriceByPlanKeyAndInterval } from '@/billing/hooks/useBaseLicensedPriceByPlanKeyAndInterval'; const StyledSubscriptionContainer = styled.div<{ withLongerMarginBottom: boolean;