From 07835f92b3bbd706f4ce3425590257fa959f36ea Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Wed, 12 Aug 2026 11:47:42 +0200 Subject: [PATCH] Merge pull request #36475 from overleaf/revert-36409-ac-remove-subscription-formatters Revert "[web] Remove SubscriptionFormatters, format subscription dates in the frontend" GitOrigin-RevId: d4c6bb600f7315c27016a45a8250a61b46de0119 --- .../Subscription/SubscriptionFormatters.mjs | 20 +++++ .../SubscriptionViewModelBuilder.mjs | 17 +++- .../components/dashboard/pause-modal.tsx | 6 +- .../dashboard/states/active/active.tsx | 23 ++--- .../dashboard/states/active/add-ons.tsx | 3 +- .../active/cancel-subscription-button.tsx | 6 +- .../dashboard/states/active/flash-message.tsx | 5 +- .../states/active/subscription-remainder.tsx | 14 ++- .../dashboard/states/active/trial-ending.tsx | 10 ++- .../components/dashboard/states/canceled.tsx | 5 +- .../dashboard/subscription-dashboard.tsx | 3 +- .../successful-subscription.tsx | 10 +-- .../subscription-dashboard-context.tsx | 3 +- .../subscription/util/is-in-free-trial.ts | 4 +- .../subscription/util/payment-dates.ts | 15 ---- services/web/package.json | 2 + .../dashboard/personal-subscription.test.tsx | 10 +-- .../dashboard/states/active/active.test.tsx | 15 +--- .../successful-subscription.test.tsx | 3 +- .../subscription/fixtures/subscriptions.ts | 86 ++++++++++++++----- .../util/is-in-free-trial.test.ts | 7 +- .../subscription/util/payment-dates.test.ts | 31 ------- .../SubscriptionFormatters.test.mjs | 30 +++++++ .../SubscriptionViewModelBuilder.test.mjs | 4 +- .../subscription/dashboard/subscription.ts | 4 +- yarn.lock | 16 ++++ 26 files changed, 203 insertions(+), 149 deletions(-) create mode 100644 services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs delete mode 100644 services/web/frontend/js/features/subscription/util/payment-dates.ts delete mode 100644 services/web/test/frontend/features/subscription/util/payment-dates.test.ts create mode 100644 services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs diff --git a/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs b/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs new file mode 100644 index 0000000000..3073f46fb3 --- /dev/null +++ b/services/web/app/src/Features/Subscription/SubscriptionFormatters.mjs @@ -0,0 +1,20 @@ +import dateformat from 'dateformat' + +function formatDateTime(date) { + if (!date) { + return null + } + return dateformat(date, 'mmmm dS, yyyy h:MM TT Z', true) +} + +function formatDate(date) { + if (!date) { + return null + } + return dateformat(date, 'mmmm dS, yyyy', true) +} + +export default { + formatDateTime, + formatDate, +} diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.mjs b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.mjs index 57aadd99de..d1d2bb486b 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.mjs @@ -5,6 +5,7 @@ import PlansLocator from './PlansLocator.mjs' import { getLocalizedPlanPricing } from './PriceVersions.mjs' import { isStandaloneAiAddOnPlanCode } from './AiHelper.mjs' import PaymentProviderEntities from './PaymentProviderEntities.mjs' +import SubscriptionFormatters from './SubscriptionFormatters.mjs' import SubscriptionLocator from './SubscriptionLocator.mjs' import InstitutionsGetter from '../Institutions/InstitutionsGetter.mjs' import InstitutionsManager from '../Institutions/InstitutionsManager.mjs' @@ -256,9 +257,9 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') { } }) const totalLicenses = (plan.membersLimit || 0) + additionalLicenses - const isInTrial = SubscriptionHelper.isInTrial( - paymentRecord.subscription.trialPeriodEnd - ) + const isInTrial = + paymentRecord.subscription.trialPeriodEnd && + paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now() let isEligibleForPause = false const commonPauseConditions = @@ -301,10 +302,18 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') { additionalLicenses, addOns, totalLicenses, - periodEnd: paymentRecord.subscription.periodEnd, + nextPaymentDueAt: SubscriptionFormatters.formatDateTime( + paymentRecord.subscription.periodEnd + ), + nextPaymentDueDate: SubscriptionFormatters.formatDate( + paymentRecord.subscription.periodEnd + ), currency: paymentRecord.subscription.currency, planPrice: paymentRecord.subscription.planPrice, state: paymentRecord.subscription.state, + trialEndsAtFormatted: SubscriptionFormatters.formatDateTime( + paymentRecord.subscription.trialPeriodEnd + ), trialEndsAt: paymentRecord.subscription.trialPeriodEnd, activeCoupons, accountEmail: paymentRecord.account.email, diff --git a/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx index 53ca2113cb..39cbdfd053 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/pause-modal.tsx @@ -17,7 +17,6 @@ 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] @@ -40,7 +39,10 @@ export default function PauseSubscriptionModal() { const subscription = personalSubscription as PaidSubscription eventTracking.sendMB('subscription-page-cancel-button-click', { plan_code: subscription?.planCode, - is_trial: isInFreeTrial(subscription?.payment.trialEndsAt), + is_trial: + subscription?.payment.trialEndsAtFormatted && + subscription?.payment.trialEndsAt && + new Date(subscription.payment.trialEndsAt).getTime() > Date.now(), }) setShowCancellation(true) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx index dd163e2ac1..2fc0185e1a 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx @@ -30,10 +30,6 @@ 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, @@ -142,7 +138,7 @@ export function ActiveSubscription({

]} // eslint-disable-line react/jsx-key @@ -202,12 +198,13 @@ export function ActiveSubscription({


{t('plan')}

{planName}

- {isInFreeTrial(subscription.payment.trialEndsAt) && ( - - )} + {isInFreeTrial(subscription.payment.trialEndsAt) && + subscription.payment.trialEndsAtFormatted && ( + + )} {subscription.payment.totalLicenses > 0 && (

{isLegacyPlan && @@ -248,9 +245,7 @@ export function ActiveSubscription({ i18nKey="your_subscription_will_pause_on" values={{ planName: subscription.plan.name, - pauseDate: formatPaymentDateTime( - subscription.payment.periodEnd - ), + pauseDate: subscription.payment.nextPaymentDueAt, reactivationDate: getFormattedRenewalDate(), }} shouldUnescape diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx index e78b79cd10..4e9be6bc04 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/add-ons.tsx @@ -22,7 +22,6 @@ 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 @@ -190,7 +189,7 @@ function AddOns({ ) } displayPrice={addOnsDisplayPrices[addOn.addOnCode]} - nextBillingDate={formatPaymentDate(subscription.payment.periodEnd)!} + nextBillingDate={subscription.payment.nextPaymentDueDate} /> ))} {hasAiAssistViaWritefull && } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx index 09fc01053e..6c8a34dd18 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx @@ -6,7 +6,6 @@ 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() @@ -19,7 +18,10 @@ export function CancelSubscriptionButton() { } = useSubscriptionDashboardContext() const subscription = personalSubscription as PaidSubscription - const isInTrial = isInFreeTrial(subscription?.payment.trialEndsAt) + const isInTrial = + subscription?.payment.trialEndsAtFormatted && + subscription?.payment.trialEndsAt && + new Date(subscription.payment.trialEndsAt).getTime() > Date.now() const hasPendingOrActivePause = subscription.payment.state === 'paused' || (subscription.payment.state === 'active' && diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/flash-message.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/flash-message.tsx index bf0a49c7b3..4c489f824e 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/flash-message.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/flash-message.tsx @@ -4,7 +4,6 @@ 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' @@ -37,9 +36,7 @@ export function FlashMessage() { Date.now() const terminationDate = hideTime - ? formatPaymentDate(subscription.payment.periodEnd) - : formatPaymentDateTime(subscription.payment.periodEnd) + ? subscription.payment.nextPaymentDueDate + : subscription.payment.nextPaymentDueAt return stillInATrial ? ( diff --git a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx index 731c42c2ba..70f37964b5 100644 --- a/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/successful-subscription/successful-subscription.tsx @@ -15,7 +15,6 @@ import { import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription' import { useBroadcastUser } from '@/shared/hooks/user-channel/use-broadcast-user' import { getUpgradePlanDisplayName } from '../../util/plan-display-names' -import { formatPaymentDateTime } from '../../util/payment-dates' function SuccessfulSubscription() { const { t } = useTranslation() @@ -57,9 +56,8 @@ function SuccessfulSubscription() { i18nKey="next_payment_of_x_collectected_on_y" values={{ paymentAmmount: subscription.payment.displayPrice, - collectionDate: formatPaymentDateTime( - subscription.payment.periodEnd - ), + collectionDate: + subscription.payment.nextPaymentDueAt, }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }} @@ -177,9 +175,7 @@ function UpgradeSuccess({ i18nKey="next_payment_of_x_collectected_on_y" values={{ paymentAmmount: subscription.payment.displayPrice, - collectionDate: formatPaymentDateTime( - subscription.payment.periodEnd - ), + collectionDate: subscription.payment.nextPaymentDueAt, }} shouldUnescape tOptions={{ interpolation: { escapeValue: true } }} diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx index a0b15ba6b5..df08a1fc16 100644 --- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx +++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx @@ -33,7 +33,6 @@ import { formatCurrency } from '@/shared/utils/currency' import { ManagedInstitution } from '../../../../../types/subscription/dashboard/managed-institution' import { Publisher } from '../../../../../types/subscription/dashboard/publisher' import { formatTime } from '@/features/utils/format-date' -import { formatPaymentDateTime } from '../util/payment-dates' type SubscriptionDashboardContextValue = { groupPlanToChangeToCode: string @@ -151,7 +150,7 @@ export function SubscriptionDashboardProvider({ !personalSubscription.payment.pausedAt || !personalSubscription.payment.remainingPauseCycles ) { - return formatPaymentDateTime(personalSubscription.payment.periodEnd)! + return personalSubscription.payment.nextPaymentDueAt } const pausedDate = new Date(personalSubscription.payment.pausedAt) pausedDate.setMonth( diff --git a/services/web/frontend/js/features/subscription/util/is-in-free-trial.ts b/services/web/frontend/js/features/subscription/util/is-in-free-trial.ts index abeaabb702..ea52a5faa0 100644 --- a/services/web/frontend/js/features/subscription/util/is-in-free-trial.ts +++ b/services/web/frontend/js/features/subscription/util/is-in-free-trial.ts @@ -1,6 +1,4 @@ -export default function isInFreeTrial( - trialEndsAt?: string | null -): trialEndsAt is string { +export default function isInFreeTrial(trialEndsAt?: string | null) { if (!trialEndsAt) return false const endDate = new Date(trialEndsAt) diff --git a/services/web/frontend/js/features/subscription/util/payment-dates.ts b/services/web/frontend/js/features/subscription/util/payment-dates.ts deleted file mode 100644 index 27aa74817e..0000000000 --- a/services/web/frontend/js/features/subscription/util/payment-dates.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { formatTime } from '@/features/utils/format-date' - -export function formatPaymentDate(date: string | null | undefined) { - if (!date) { - return null - } - return formatTime(date, 'MMMM Do, YYYY', true) -} - -export function formatPaymentDateTime(date: string | null | undefined) { - if (!date) { - return null - } - return formatTime(date, 'MMMM Do, YYYY h:mm A [UTC]', true) -} diff --git a/services/web/package.json b/services/web/package.json index 5ca05f2ec0..52e971dc62 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -130,6 +130,7 @@ "crc-32": "^1.2.2", "csurf": "patch:csurf@npm%3A1.11.0#~/.yarn/patches/csurf-npm-1.11.0-c1b9cbb35b.patch", "csv": "^6.2.5", + "dateformat": "1.0.4-1.2.3", "ejs": "^3.1.10", "email-addresses": "^5.0.0", "eventsource-parser": "^1.1.2", @@ -263,6 +264,7 @@ "@types/bootstrap": "^5.2.10", "@types/chai": "^4.3.0", "@types/chai-as-promised": "^8.0.2", + "@types/dateformat": "^5.0.3", "@types/diff": "^5.2.3", "@types/dom-speech-recognition": "^0.0.7", "@types/events": "^3.0.3", diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx index 8355ea5639..45e3bf977a 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx @@ -22,7 +22,6 @@ import { reactivateSubscriptionUrl } from '../../../../../../frontend/js/feature import fetchMock from 'fetch-mock' import sinon from 'sinon' import { location } from '@/shared/components/location' -import { formatPaymentDateTime } from '@/features/subscription/util/payment-dates' describe('', function () { afterEach(function () { @@ -78,12 +77,9 @@ describe('', function () { 'Your subscription has been canceled and will terminate on', { exact: false } ) - screen.getByText( - formatPaymentDateTime(canceledSubscription.payment!.periodEnd)!, - { - exact: false, - } - ) + screen.getByText(canceledSubscription.payment!.nextPaymentDueAt, { + exact: false, + }) screen.getByText('No further payments will be taken.', { exact: false }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index a63d1aa81e..1428ff33c5 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -27,10 +27,6 @@ import { } from '@/features/subscription/data/subscription-url' import { location } from '@/shared/components/location' import { MetaTag } from '@/utils/meta' -import { - formatPaymentDate, - formatPaymentDateTime, -} from '@/features/subscription/util/payment-dates' describe('', function () { let sendMBSpy: sinon.SinonSpy @@ -79,7 +75,7 @@ describe('', function () { within(screen.getByTestId('renews-on')).getByText((_, el) => Boolean( el?.textContent?.includes( - `Renews on ${formatPaymentDate(subscription.payment.periodEnd)}` + `Renews on ${subscription.payment.nextPaymentDueDate}` ) ) ) @@ -214,7 +210,7 @@ describe('', function () { within(screen.getByTestId('trial-ending')).getByText((_, el) => Boolean( el?.textContent?.includes( - `You’re on a free trial which ends on ${formatPaymentDateTime(trialSubscription.payment.trialEndsAt)}` + `You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}` ) ) ) @@ -292,7 +288,7 @@ describe('', function () { { exact: false } ) const dates = screen.getAllByText( - formatPaymentDate(annualActiveSubscription.payment.periodEnd)!, + annualActiveSubscription.payment.nextPaymentDueAt, { exact: false, } @@ -311,10 +307,7 @@ describe('', function () { { exact: false } ) const dates = screen.getAllByText( - formatPaymentDate(trialSubscription.payment.trialEndsAt)!, - { - exact: false, - } + trialSubscription.payment.trialEndsAtFormatted! ) expect(dates.length).to.equal(3) const button = screen.getByRole('button', { diff --git a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx index 000418f3a6..3d6a5784af 100644 --- a/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/successful-subscription/successful-subscription.test.tsx @@ -9,7 +9,6 @@ import { } from '../../fixtures/subscriptions' import { ExposedSettings } from '../../../../../../types/exposed-settings' import { UserProvider } from '@/shared/context/user-context' -import { formatPaymentDateTime } from '@/features/subscription/util/payment-dates' describe('successful subscription page', function () { it('renders the invoices link', function () { @@ -123,7 +122,7 @@ describe('successful subscription page', function () { .getByText(/the next payment of/i) .textContent?.replace(/\xA0/g, ' ') ).to.equal( - `The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${formatPaymentDateTime(annualActiveSubscriptionPro.payment.periodEnd)}.` + `The next payment of ${annualActiveSubscriptionPro.payment.displayPrice} will be collected on ${annualActiveSubscriptionPro.payment.nextPaymentDueAt}.` ) screen.getByText(/taxes may be added, depending on your billing address/i) diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts index 886f611802..e67817673f 100644 --- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts @@ -3,11 +3,17 @@ import { GroupSubscription, PaidSubscription, } from '../../../../../types/subscription/dashboard/subscription' +import dateformat from 'dateformat' + const today = new Date() const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1) -const periodEnd = new Date(oneYearFromToday).toISOString() +const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy') +const nextPaymentDueDate = dateformat(oneYearFromToday, 'dS mmmm yyyy') const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) -const sevenDaysFromTodayISO = new Date(sevenDaysFromToday).toISOString() +const sevenDaysFromTodayFormatted = dateformat( + sevenDaysFromToday, + 'dS mmmm yyyy' +) export const annualActiveSubscription: PaidSubscription = { manager_ids: ['abc123'], @@ -33,9 +39,11 @@ export const annualActiveSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -76,9 +84,11 @@ export const pastDueActiveSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'past_due', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -119,9 +129,11 @@ export const annualActiveSubscriptionWithCoupons: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [ { name: 'Coupon1', code: 'c1', description: 'Coupon1 for 10% off' }, @@ -165,10 +177,12 @@ export const pendingPausedSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', remainingPauseCycles: 1, + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -209,10 +223,12 @@ export const pausedSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'paused', remainingPauseCycles: 1, + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -253,9 +269,11 @@ export const annualActiveSubscriptionWithAddons: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -299,9 +317,11 @@ export const annualActiveSubscriptionEuro: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'EUR', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -341,9 +361,11 @@ export const annualActiveSubscriptionPro: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -384,9 +406,11 @@ export const pastDueExpiredSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'expired', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -427,9 +451,11 @@ export const canceledSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'canceled', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -470,9 +496,11 @@ export const pendingAddOnChange: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -527,9 +555,11 @@ export const pendingSubscriptionChange: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -581,9 +611,11 @@ export const groupActiveSubscription: GroupSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 10, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -628,9 +660,11 @@ export const groupProfessionalActiveSubscription: GroupSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 10, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -676,9 +710,11 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 11, totalLicenses: 21, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'fake@example.com', @@ -730,10 +766,12 @@ export const trialSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd: sevenDaysFromTodayISO, + nextPaymentDueAt: sevenDaysFromTodayFormatted, + nextPaymentDueDate: sevenDaysFromTodayFormatted, currency: 'USD', state: 'active', - trialEndsAt: sevenDaysFromTodayISO, + trialEndsAtFormatted: sevenDaysFromTodayFormatted, + trialEndsAt: new Date(sevenDaysFromToday).toString(), activeCoupons: [], accountEmail: 'fake@example.com', hasPastDueInvoice: false, @@ -793,10 +831,12 @@ export const trialCollaboratorSubscription: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd: sevenDaysFromTodayISO, + nextPaymentDueAt: sevenDaysFromTodayFormatted, + nextPaymentDueDate: sevenDaysFromTodayFormatted, currency: 'USD', state: 'active', - trialEndsAt: sevenDaysFromTodayISO, + trialEndsAtFormatted: sevenDaysFromTodayFormatted, + trialEndsAt: new Date(sevenDaysFromToday).toString(), activeCoupons: [], accountEmail: 'foo@example.com', hasPastDueInvoice: false, @@ -835,9 +875,11 @@ export const monthlyActiveCollaborator: PaidSubscription = { accountManagementLink: '/user/subscription/payment/account-management', additionalLicenses: 0, totalLicenses: 0, - periodEnd, + nextPaymentDueAt, + nextPaymentDueDate, currency: 'USD', state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'foo@example.com', diff --git a/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts b/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts index 3bc58f9dee..1ba3fdc3e6 100644 --- a/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts +++ b/services/web/test/frontend/features/subscription/util/is-in-free-trial.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai' import isInFreeTrial from '../../../../../frontend/js/features/subscription/util/is-in-free-trial' +import dateformat from 'dateformat' describe('isInFreeTrial', function () { it('returns false when no date sent', function () { @@ -14,6 +15,10 @@ describe('isInFreeTrial', function () { it('returns true when date is in the future', function () { const today = new Date() const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) - expect(isInFreeTrial(new Date(sevenDaysFromToday).toISOString())).to.be.true + const sevenDaysFromTodayFormatted = dateformat( + sevenDaysFromToday, + 'dS mmmm yyyy' + ) + expect(isInFreeTrial(sevenDaysFromTodayFormatted)).to.be.true }) }) diff --git a/services/web/test/frontend/features/subscription/util/payment-dates.test.ts b/services/web/test/frontend/features/subscription/util/payment-dates.test.ts deleted file mode 100644 index 6eed7dc21a..0000000000 --- a/services/web/test/frontend/features/subscription/util/payment-dates.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { expect } from 'chai' -import { - formatPaymentDate, - formatPaymentDateTime, -} from '../../../../../frontend/js/features/subscription/util/payment-dates' - -describe('payment-dates', function () { - describe('formatPaymentDate', function () { - it('formats a date in UTC', function () { - expect(formatPaymentDate('2025-02-20T12:00:00.000Z')).to.equal( - 'February 20th, 2025' - ) - }) - it('returns null for missing dates', function () { - expect(formatPaymentDate(null)).to.be.null - expect(formatPaymentDate(undefined)).to.be.null - }) - }) - - describe('formatPaymentDateTime', function () { - it('formats a date and time in UTC', function () { - expect(formatPaymentDateTime('2025-02-20T12:00:00.000Z')).to.equal( - 'February 20th, 2025 12:00 PM UTC' - ) - }) - it('returns null for missing dates', function () { - expect(formatPaymentDateTime(null)).to.be.null - expect(formatPaymentDateTime(undefined)).to.be.null - }) - }) -}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs new file mode 100644 index 0000000000..8fe50cdf28 --- /dev/null +++ b/services/web/test/unit/src/Subscription/SubscriptionFormatters.test.mjs @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import SubscriptionFormatters from '../../../../app/src/Features/Subscription/SubscriptionFormatters.mjs' + +describe('SubscriptionFormatters', function () { + describe('formatDateTime', function () { + it('should return null if no date', function () { + const result = SubscriptionFormatters.formatDateTime(null) + expect(result).to.equal(null) + }) + + it('should format date with time', function () { + const date = new Date(1639904485000) + const result = SubscriptionFormatters.formatDateTime(date) + expect(result).to.equal('December 19th, 2021 9:01 AM UTC') + }) + }) + + describe('formatDate', function () { + it('should return null if no date', function () { + const result = SubscriptionFormatters.formatDate(null) + expect(result).to.equal(null) + }) + + it('should format date', function () { + const date = new Date(1639904485000) + const result = SubscriptionFormatters.formatDate(date) + expect(result).to.equal('December 19th, 2021') + }) + }) +}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilder.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilder.test.mjs index 43d623482e..8a55b08e51 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilder.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilder.test.mjs @@ -745,10 +745,12 @@ describe('SubscriptionViewModelBuilder', function () { }, ], totalLicenses: 0, - periodEnd: new Date('2025-02-20T12:00:00.000Z'), + nextPaymentDueAt: 'February 20th, 2025 12:00 PM UTC', + nextPaymentDueDate: 'February 20th, 2025', currency: 'EUR', planPrice: 13, state: 'active', + trialEndsAtFormatted: null, trialEndsAt: null, activeCoupons: [], accountEmail: 'example@example.com', diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index b19cf08dba..1a5796fd60 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -37,10 +37,12 @@ type PaymentProviderRecord = { additionalLicenses: number addOns: PaymentProviderAddOn[] totalLicenses: number - periodEnd: string + nextPaymentDueAt: string + nextPaymentDueDate: string currency: CurrencyCode planPrice: number state?: SubscriptionState + trialEndsAtFormatted: Nullable trialEndsAt: Nullable activeCoupons: PaymentProviderCoupon[] accountEmail: string diff --git a/yarn.lock b/yarn.lock index bf464559fd..ceceaaa783 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8006,6 +8006,7 @@ __metadata: "@types/bootstrap": "npm:^5.2.10" "@types/chai": "npm:^4.3.0" "@types/chai-as-promised": "npm:^8.0.2" + "@types/dateformat": "npm:^5.0.3" "@types/diff": "npm:^5.2.3" "@types/dom-speech-recognition": "npm:^0.0.7" "@types/events": "npm:^3.0.3" @@ -8099,6 +8100,7 @@ __metadata: cypress-multi-reporters: "npm:^2.0.5" cypress-plugin-tab: "npm:^1.0.5" d3: "npm:^3.5.16" + dateformat: "npm:1.0.4-1.2.3" daterangepicker: "npm:2.1.27" diff: "npm:^5.1.0" dompurify: "npm:^3.3.2" @@ -10793,6 +10795,13 @@ __metadata: languageName: node linkType: hard +"@types/dateformat@npm:^5.0.3": + version: 5.0.3 + resolution: "@types/dateformat@npm:5.0.3" + checksum: 10c0/828452c096b0b6901a64ce1f3a47fb96755f2451a2605b59fb271407526c62020d171aab6946f9268ca5a4d63db7952c32595bf30b49ed54b218414ec6a02261 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12, @types/debug@npm:^4.1.8": version: 4.1.13 resolution: "@types/debug@npm:4.1.13" @@ -16422,6 +16431,13 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:1.0.4-1.2.3": + version: 1.0.4-1.2.3 + resolution: "dateformat@npm:1.0.4-1.2.3" + checksum: 10c0/290dec607d80e0a7dbc22d0c2ad19db04ef8c50d1969689b4542bca91c7383e99eadfbca3bc38dab7e8d77e987cc017d051744fe7b29a8b0926b2fe7b4a667b2 + languageName: node + linkType: hard + "daterangepicker@npm:2.1.27": version: 2.1.27 resolution: "daterangepicker@npm:2.1.27"