[web] Remove SubscriptionFormatters, format subscription dates in the frontend (#36409)

* [web] Remove SubscriptionFormatters, format subscription dates in the frontend

Replace the server-formatted nextPaymentDueAt/nextPaymentDueDate/
trialEndsAtFormatted fields with the raw periodEnd date and format in
the React dashboard (same strings as before, English + UTC). Also
consolidate hand-rolled trial checks onto isInFreeTrial /
SubscriptionHelper.isInTrial.

* [web] Remove the now-unused dateformat dependency

* [web] Address review: payment-dates unit test, UTC dates in acceptance test

Add a dedicated unit test asserting the exact formatted strings from
formatPaymentDate/formatPaymentDateTime, and use explicit UTC timestamps
in the SubscriptionDashboard acceptance fixtures instead of
local-timezone Date constructors.

GitOrigin-RevId: 16ac4f1098b14ef9f1478d22cf39c1357ead7dea
This commit is contained in:
Antoine Clausse
2026-09-17 11:52:44 +00:00
committed by Copybot
parent d0399bd09f
commit ba4f024f34
26 changed files with 149 additions and 203 deletions
@@ -17,6 +17,7 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
import PauseDuck from '../../images/pause-duck.svg'
import GenericErrorAlert from './generic-error-alert'
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import isInFreeTrial from '../../util/is-in-free-trial'
const pauseMonthDurationOptions = [1, 2, 3]
@@ -39,10 +40,7 @@ export default function PauseSubscriptionModal() {
const subscription = personalSubscription as PaidSubscription
eventTracking.sendMB('subscription-page-cancel-button-click', {
plan_code: subscription?.planCode,
is_trial:
subscription?.payment.trialEndsAtFormatted &&
subscription?.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now(),
is_trial: isInFreeTrial(subscription?.payment.trialEndsAt),
})
setShowCancellation(true)
}
@@ -30,6 +30,10 @@ import { useLocation } from '@/shared/hooks/use-location'
import { FlashMessage } from '@/features/subscription/components/dashboard/states/active/flash-message'
import Notification from '@/shared/components/notification'
import { PendingPlanChange } from './pending-plan-change'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '../../../../util/payment-dates'
export function ActiveSubscription({
subscription,
@@ -138,7 +142,7 @@ export function ActiveSubscription({
<p className="mb-1" data-testid="renews-on">
<Trans
i18nKey="renews_on"
values={{ date: subscription.payment.nextPaymentDueDate }}
values={{ date: formatPaymentDate(subscription.payment.periodEnd) }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[<strong />]} // eslint-disable-line react/jsx-key
@@ -198,13 +202,12 @@ export function ActiveSubscription({
<hr />
<h2 className="h3 fw-bold">{t('plan')}</h2>
<h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
{isInFreeTrial(subscription.payment.trialEndsAt) &&
subscription.payment.trialEndsAtFormatted && (
<TrialEnding
trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
className="mb-1"
/>
)}
{isInFreeTrial(subscription.payment.trialEndsAt) && (
<TrialEnding
trialEndsAt={subscription.payment.trialEndsAt}
className="mb-1"
/>
)}
{subscription.payment.totalLicenses > 0 && (
<p className="mb-1" data-testid="plan-licenses">
{isLegacyPlan &&
@@ -245,7 +248,9 @@ export function ActiveSubscription({
i18nKey="your_subscription_will_pause_on"
values={{
planName: subscription.plan.name,
pauseDate: subscription.payment.nextPaymentDueAt,
pauseDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
reactivationDate: getFormattedRenewalDate(),
}}
shouldUnescape
@@ -22,6 +22,7 @@ import sparkle from '@/shared/svgs/sparkle.svg'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { LICENSE_ADD_ON } from '@/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details'
import WritefullManagedBundleAddOn from './change-plan/modals/writefull-bundle-management-modal'
import { formatPaymentDate } from '../../../../util/payment-dates'
type AddOnsProps = {
subscription: PaidSubscription
@@ -189,7 +190,7 @@ function AddOns({
)
}
displayPrice={addOnsDisplayPrices[addOn.addOnCode]}
nextBillingDate={subscription.payment.nextPaymentDueDate}
nextBillingDate={formatPaymentDate(subscription.payment.periodEnd)!}
/>
))}
{hasAiAssistViaWritefull && <WritefullManagedBundleAddOn />}
@@ -6,6 +6,7 @@ import { PaidSubscription } from '../../../../../../../../types/subscription/das
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useLocation } from '@/shared/hooks/use-location'
import { stripHasSubscription } from '../../../../data/subscription-url'
import isInFreeTrial from '../../../../util/is-in-free-trial'
export function CancelSubscriptionButton() {
const { t } = useTranslation()
@@ -18,10 +19,7 @@ export function CancelSubscriptionButton() {
} = useSubscriptionDashboardContext()
const subscription = personalSubscription as PaidSubscription
const isInTrial =
subscription?.payment.trialEndsAtFormatted &&
subscription?.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
const isInTrial = isInFreeTrial(subscription?.payment.trialEndsAt)
const hasPendingOrActivePause =
subscription.payment.state === 'paused' ||
(subscription.payment.state === 'active' &&
@@ -4,6 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { useEffect, useState } from 'react'
import { useLocation } from '@/shared/hooks/use-location'
import { formatPaymentDateTime } from '../../../../util/payment-dates'
export type FlashMessageName = 'paused' | 'unpaused' | 'error'
@@ -36,7 +37,9 @@ export function FlashMessage() {
<Trans
i18nKey="your_subscription_will_pause_on_short"
values={{
pauseDate: subscription.payment.nextPaymentDueAt,
pauseDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -1,5 +1,10 @@
import { Trans } from 'react-i18next'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import isInFreeTrial from '../../../../util/is-in-free-trial'
import {
formatPaymentDate,
formatPaymentDateTime,
} from '../../../../util/payment-dates'
type SubscriptionRemainderProps = {
subscription: PaidSubscription
@@ -10,14 +15,11 @@ function SubscriptionRemainder({
subscription,
hideTime,
}: SubscriptionRemainderProps) {
const stillInATrial =
subscription.payment.trialEndsAtFormatted &&
subscription.payment.trialEndsAt &&
new Date(subscription.payment.trialEndsAt).getTime() > Date.now()
const stillInATrial = isInFreeTrial(subscription.payment.trialEndsAt)
const terminationDate = hideTime
? subscription.payment.nextPaymentDueDate
: subscription.payment.nextPaymentDueAt
? formatPaymentDate(subscription.payment.periodEnd)
: formatPaymentDateTime(subscription.payment.periodEnd)
return stillInATrial ? (
<Trans
i18nKey="subscription_will_remain_active_until_end_of_trial_period_x"
@@ -1,19 +1,17 @@
import { Trans } from 'react-i18next'
import { formatPaymentDateTime } from '../../../../util/payment-dates'
type TrialEndingProps = {
trialEndsAtFormatted: string
trialEndsAt: string
className?: string
}
export function TrialEnding({
trialEndsAtFormatted,
className,
}: TrialEndingProps) {
export function TrialEnding({ trialEndsAt, className }: TrialEndingProps) {
return (
<p className={className} data-testid="trial-ending">
<Trans
i18nKey="youre_on_free_trial_which_ends_on"
values={{ date: trialEndsAtFormatted }}
values={{ date: formatPaymentDateTime(trialEndsAt) }}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
@@ -6,6 +6,7 @@ import {
} from '../../../data/add-on-codes'
import ReactivateSubscription from '../reactivate-subscription'
import OLButton from '@/shared/components/ol/ol-button'
import { formatPaymentDateTime } from '../../../util/payment-dates'
export function CanceledSubscription({
subscription,
@@ -48,7 +49,9 @@ export function CanceledSubscription({
<Trans
i18nKey="subscription_canceled_and_terminate_on_x"
values={{
terminateDate: subscription.payment.nextPaymentDueAt,
terminateDate: formatPaymentDateTime(
subscription.payment.periodEnd
),
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
@@ -17,6 +17,7 @@ import Notification from '@/shared/components/notification'
import WritefullManagedBundleAddOn from './states/active/change-plan/modals/writefull-bundle-management-modal'
import RedirectAlerts from './redirect-alerts'
import { PaidSubscription } from '@ol-types/subscription/dashboard/subscription'
import { formatPaymentDate } from '../../util/payment-dates'
function SubscriptionDashboard() {
const { t } = useTranslation()
@@ -42,7 +43,7 @@ function SubscriptionDashboard() {
subscription &&
subscription.pendingPlan &&
subscription.pendingPlan.name !== subscription.plan.name
const nextPaymentDueDate = subscription?.payment?.nextPaymentDueDate
const nextPaymentDueDate = formatPaymentDate(subscription?.payment?.periodEnd)
return (
<div className="container">