From d4fe9cf34b3253b07cd512582055cbd2e0bcf17d Mon Sep 17 00:00:00 2001
From: Simon Gardner
Date: Tue, 9 Sep 2025 10:35:59 +0100
Subject: [PATCH] Update unit tests for ActiveSubscription
GitOrigin-RevId: 181f5a097fff2fa31ed11d39b76f40c9a4b4ca31
---
.../dashboard/personal-subscription.tsx | 4 +-
.../dashboard/states/active/active-new.tsx | 391 ----------------
.../dashboard/states/active/active.tsx | 443 ++++++++++++------
.../dashboard/states/active/trial-ending.tsx | 2 +-
.../components/shared/price-exceptions.tsx | 2 +-
.../active-subscription.stories.tsx | 150 ++++++
.../dashboard/states/active/active.test.tsx | 243 ++++++----
.../subscription/fixtures/subscriptions.ts | 259 +++++++++-
.../helpers/render-active-subscription.tsx | 10 +-
.../render-with-subscription-dash-context.tsx | 59 +--
.../setup-subscription-dash-context.ts | 66 +++
11 files changed, 930 insertions(+), 699 deletions(-)
delete mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx
create mode 100644 services/web/frontend/stories/subscription/active-subscription.stories.tsx
create mode 100644 services/web/test/frontend/features/subscription/helpers/setup-subscription-dash-context.ts
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
index fcec0cecbb..0b9ce9a4f5 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
@@ -1,7 +1,7 @@
import { Trans, useTranslation } from 'react-i18next'
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
import { PausedSubscription } from './states/active/paused'
-import { ActiveSubscriptionNew } from '@/features/subscription/components/dashboard/states/active/active-new'
+import { ActiveSubscription } from '@/features/subscription/components/dashboard/states/active/active'
import { CanceledSubscription } from './states/canceled'
import { ExpiredSubscription } from './states/expired'
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
@@ -43,7 +43,7 @@ function PersonalSubscriptionStates({
if (state === 'active') {
// This version handles subscriptions with and without addons
- return
+ return
} else if (state === 'canceled') {
return
} else if (state === 'expired') {
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx
deleted file mode 100644
index 6f0a4aab28..0000000000
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx
+++ /dev/null
@@ -1,391 +0,0 @@
-import { useTranslation, Trans } from 'react-i18next'
-import { PriceExceptions } from '../../../shared/price-exceptions'
-import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
-import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
-import { CancelSubscriptionButton } from './cancel-subscription-button'
-import { CancelSubscription } from './cancel-plan/cancel-subscription'
-import { TrialEnding } from './trial-ending'
-import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
-import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
-import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
-import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
-import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal'
-import OLButton from '@/shared/components/ol/ol-button'
-import isInFreeTrial from '../../../../util/is-in-free-trial'
-import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons'
-import {
- AI_ADD_ON_CODE,
- AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE,
- isStandaloneAiPlanCode,
-} from '@/features/subscription/data/add-on-codes'
-import getMeta from '@/utils/meta'
-import SubscriptionRemainder from '@/features/subscription/components/dashboard/states/active/subscription-remainder'
-import { sendMB } from '../../../../../../infrastructure/event-tracking'
-import PauseSubscriptionModal from '@/features/subscription/components/dashboard/pause-modal'
-import LoadingSpinner from '@/shared/components/loading-spinner'
-import { postJSON } from '@/infrastructure/fetch-json'
-import { debugConsole } from '@/utils/debugging'
-import useAsync from '@/shared/hooks/use-async'
-import { useLocation } from '@/shared/hooks/use-location'
-import { FlashMessage } from '@/features/subscription/components/dashboard/states/active/flash-message'
-import Notification from '@/shared/components/notification'
-
-export function ActiveSubscriptionNew({
- subscription,
-}: {
- subscription: PaidSubscription
-}) {
- const { t } = useTranslation()
- const {
- recurlyLoadError,
- setModalIdShown,
- showCancellation,
- institutionMemberships,
- memberGroupSubscriptions,
- getFormattedRenewalDate,
- } = useSubscriptionDashboardContext()
- const cancelPauseReq = useAsync()
- const { isError: isErrorPause } = cancelPauseReq
-
- if (showCancellation) return
-
- const onStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
-
- let planName
- if (onStandalonePlan) {
- planName = 'Overleaf Free'
- if (institutionMemberships && institutionMemberships.length > 0) {
- planName = 'Overleaf Professional'
- }
- if (memberGroupSubscriptions.length > 0) {
- if (
- memberGroupSubscriptions.some(s => s.planLevelName === 'Professional')
- ) {
- planName = 'Overleaf Professional'
- } else {
- planName = 'Overleaf Standard'
- }
- }
- } else {
- planName = subscription.plan.name
- }
-
- const handlePlanChange = () => setModalIdShown('change-plan')
- const handleCancelClick = (addOnCode: string) => {
- if (
- [AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE, AI_ADD_ON_CODE].includes(
- addOnCode
- )
- ) {
- setModalIdShown('cancel-ai-add-on')
- }
- }
- const hasPendingPause = Boolean(
- subscription.payment.state === 'active' &&
- subscription.payment.remainingPauseCycles &&
- subscription.payment.remainingPauseCycles > 0
- )
-
- const isLegacyPlan =
- subscription.payment.totalLicenses !==
- subscription.payment.additionalLicenses
-
- return (
- <>
-
-
-
- {isErrorPause && (
-
- )}
-
- {t('billing')}
-
- {subscription.plan.annual ? (
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- ]}
- />
- ) : (
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- ]}
- />
- )}
-
-
- ]} // eslint-disable-line react/jsx-key
- />
-
-
-
-
- {!recurlyLoadError && (
-
-
-
-
-
- )}
-
-
- {t('plan')}
- {planName}
- {subscription.pendingPlan &&
- subscription.pendingPlan.name !== subscription.plan.name && (
- {t('want_change_to_apply_before_plan_end')}
- )}
- {isInFreeTrial(subscription.payment.trialEndsAt) &&
- subscription.payment.trialEndsAtFormatted && (
-
- )}
- {subscription.payment.totalLicenses > 0 && (
-
- {isLegacyPlan && subscription.payment.additionalLicenses > 0 ? (
- , ]} // eslint-disable-line react/jsx-key
- />
- ) : (
- ]} // eslint-disable-line react/jsx-key
- />
- )}
-
- )}
- {hasPendingPause && (
- <>
-
- ,
- ]}
- />
-
- {t('you_can_still_use_your_premium_features')}
- >
- )}
- {!onStandalonePlan && (
-
- {subscription.plan.annual
- ? t('x_price_per_year', {
- price: subscription.payment.planOnlyDisplayPrice,
- })
- : t('x_price_per_month', {
- price: subscription.payment.planOnlyDisplayPrice,
- })}
-
- )}
- {!recurlyLoadError && (
-
- )}
-
-
-
-
-
-
-
-
-
- >
- )
-}
-
-type PlanActionsProps = {
- subscription: PaidSubscription
- onStandalonePlan: boolean
- handlePlanChange: () => void
- hasPendingPause: boolean
- cancelPauseReq: ReturnType
-}
-
-function PlanActions({
- subscription,
- onStandalonePlan,
- handlePlanChange,
- hasPendingPause,
- cancelPauseReq,
-}: PlanActionsProps) {
- const { t } = useTranslation()
- const isSubscriptionEligibleForFlexibleGroupLicensing = getMeta(
- 'ol-canUseFlexibleLicensing'
- )
- const location = useLocation()
- const { runAsync: runAsyncCancelPause, isLoading: isLoadingCancelPause } =
- cancelPauseReq
-
- const handleCancelPendingPauseClick = async () => {
- try {
- await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
- const newUrl = new URL(location.toString())
- newUrl.searchParams.set('flash', 'unpaused')
- window.history.replaceState(null, '', newUrl)
- location.reload()
- } catch (e) {
- debugConsole.error(e)
- }
- }
-
- return (
-
- {isSubscriptionEligibleForFlexibleGroupLicensing ? (
-
- ) : (
- <>
- {!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
-
- {t('change_plan')}
-
- )}
- >
- )}
- {hasPendingPause && (
-
- {isLoadingCancelPause ? (
-
- ) : (
- t('unpause_subscription')
- )}
-
- )}
- {!onStandalonePlan && (
- <>
- {' '}
-
- >
- )}
-
- )
-}
-
-function FlexibleGroupLicensingActions({
- subscription,
-}: {
- subscription: PaidSubscription
-}) {
- const { t } = useTranslation()
-
- if (subscription.pendingPlan || subscription.payment.hasPastDueInvoice) {
- return null
- }
-
- const isProfessionalPlan = subscription.planCode
- .toLowerCase()
- .includes('professional')
-
- return (
- <>
- {!isProfessionalPlan && (
- <>
-
- sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
- }
- >
- {t('upgrade_plan')}
- {' '}
- >
- )}
- {subscription.plan.membersLimitAddOn === 'additional-license' && (
- sendMB('flex-add-users')}
- >
- {t('buy_more_licenses')}
-
- )}
- >
- )
-}
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 b0940e801b..6bcc5d9518 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
@@ -4,25 +4,32 @@ import { useSubscriptionDashboardContext } from '../../../../context/subscriptio
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import { CancelSubscriptionButton } from './cancel-subscription-button'
import { CancelSubscription } from './cancel-plan/cancel-subscription'
-import { PendingPlanChange } from './pending-plan-change'
import { TrialEnding } from './trial-ending'
-import { PendingAdditionalLicenses } from './pending-additional-licenses'
-import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan'
-import SubscriptionRemainder from './subscription-remainder'
-import isInFreeTrial from '../../../../util/is-in-free-trial'
import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
+import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal'
import OLButton from '@/shared/components/ol/ol-button'
-import useAsync from '@/shared/hooks/use-async'
-import { postJSON } from '@/infrastructure/fetch-json'
-import PauseSubscriptionModal from '../../pause-modal'
-import Notification from '@/shared/components/notification'
-import { debugConsole } from '@/utils/debugging'
-import { FlashMessage } from './flash-message'
-import { useLocation } from '@/shared/hooks/use-location'
+import isInFreeTrial from '../../../../util/is-in-free-trial'
+import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons'
+import {
+ AI_ADD_ON_CODE,
+ AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE,
+ isStandaloneAiPlanCode,
+} from '@/features/subscription/data/add-on-codes'
+import getMeta from '@/utils/meta'
+import SubscriptionRemainder from '@/features/subscription/components/dashboard/states/active/subscription-remainder'
+import { sendMB } from '../../../../../../infrastructure/event-tracking'
+import PauseSubscriptionModal from '@/features/subscription/components/dashboard/pause-modal'
import LoadingSpinner from '@/shared/components/loading-spinner'
+import { postJSON } from '@/infrastructure/fetch-json'
+import { debugConsole } from '@/utils/debugging'
+import useAsync from '@/shared/hooks/use-async'
+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'
export function ActiveSubscription({
subscription,
@@ -34,33 +41,55 @@ export function ActiveSubscription({
recurlyLoadError,
setModalIdShown,
showCancellation,
+ institutionMemberships,
+ memberGroupSubscriptions,
getFormattedRenewalDate,
} = useSubscriptionDashboardContext()
- const {
- isError: isErrorPause,
- runAsync: runAsyncCancelPause,
- isLoading: isLoadingCancelPause,
- } = useAsync()
- const location = useLocation()
+ const cancelPauseReq = useAsync()
+ const { isError: isErrorPause } = cancelPauseReq
if (showCancellation) return
- const hasPendingPause =
- subscription.payment.state === 'active' &&
- subscription.payment.remainingPauseCycles &&
- subscription.payment.remainingPauseCycles > 0
+ const onStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
- const handleCancelPendingPauseClick = async () => {
- try {
- await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
- const newUrl = new URL(location.toString())
- newUrl.searchParams.set('flash', 'unpaused')
- window.history.replaceState(null, '', newUrl)
- location.reload()
- } catch (e) {
- debugConsole.error(e)
+ let planName
+ if (onStandalonePlan) {
+ planName = 'Overleaf Free'
+ if (institutionMemberships && institutionMemberships.length > 0) {
+ planName = 'Overleaf Professional'
+ }
+ if (memberGroupSubscriptions.length > 0) {
+ if (
+ memberGroupSubscriptions.some(s => s.planLevelName === 'Professional')
+ ) {
+ planName = 'Overleaf Professional'
+ } else {
+ planName = 'Overleaf Standard'
+ }
+ }
+ } else {
+ planName = subscription.plan.name
+ }
+
+ const handlePlanChange = () => setModalIdShown('change-plan')
+ const handleCancelClick = (addOnCode: string) => {
+ if (
+ [AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE, AI_ADD_ON_CODE].includes(
+ addOnCode
+ )
+ ) {
+ setModalIdShown('cancel-ai-add-on')
}
}
+ const hasPendingPause = Boolean(
+ subscription.payment.state === 'active' &&
+ subscription.payment.remainingPauseCycles &&
+ subscription.payment.remainingPauseCycles > 0
+ )
+
+ const isLegacyPlan =
+ subscription.payment.totalLicenses !==
+ subscription.payment.additionalLicenses
return (
<>
@@ -74,66 +103,130 @@ export function ActiveSubscription({
/>
)}
-
- {!hasPendingPause && (
+
{t('billing')}
+
+ {subscription.plan.annual ? (
,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+ ) : (
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
]}
/>
)}
- {subscription.pendingPlan && (
- <>
- {' '}
-
- >
- )}
- {!subscription.pendingPlan &&
- subscription.payment.additionalLicenses > 0 && (
- <>
- {' '}
-
- >
- )}
- {!recurlyLoadError &&
- !subscription.groupPlan &&
- !hasPendingPause &&
- !subscription.payment.hasPastDueInvoice && (
- <>
- {' '}
- setModalIdShown('change-plan')}
- >
- {t('change_plan')}
-
- >
- )}
+
+ ]} // eslint-disable-line react/jsx-key
+ />
+
+
+
+
+ {!recurlyLoadError && (
+
+
+
+
+
+ )}
+
+
+ {t('plan')}
+ {planName}
{subscription.pendingPlan &&
subscription.pendingPlan.name !== subscription.plan.name && (
- {t('want_change_to_apply_before_plan_end')}
+ {t('want_change_to_apply_before_plan_end')}
)}
- {(!subscription.pendingPlan ||
- subscription.pendingPlan.name === subscription.plan.name) &&
- subscription.plan.groupPlan && }
{isInFreeTrial(subscription.payment.trialEndsAt) &&
subscription.payment.trialEndsAtFormatted && (
)}
+ {subscription.payment.totalLicenses > 0 && (
+
+ {isLegacyPlan &&
+ subscription.payment.additionalLicenses > 0 &&
+ !subscription.payment.pendingAdditionalLicenses ? (
+ , ]} // eslint-disable-line react/jsx-key
+ />
+ ) : (
+ ]} // eslint-disable-line react/jsx-key
+ />
+ )}
+
+ )}
+ {subscription.pendingPlan && (
+
+ {' '}
+
+
+ )}
{hasPendingPause && (
<>
@@ -154,83 +247,155 @@ export function ActiveSubscription({
/>
+ {subscription.plan.annual
+ ? t('x_price_per_year', {
+ price: subscription.payment.planOnlyDisplayPrice,
+ })
+ : t('x_price_per_month', {
+ price: subscription.payment.planOnlyDisplayPrice,
+ })}
+
+ )}
{!recurlyLoadError && (
- <>
-
+}
+
+function PlanActions({
+ subscription,
+ onStandalonePlan,
+ handlePlanChange,
+ hasPendingPause,
+ cancelPauseReq,
+}: PlanActionsProps) {
+ const { t } = useTranslation()
+ const isSubscriptionEligibleForFlexibleGroupLicensing = getMeta(
+ 'ol-canUseFlexibleLicensing'
+ )
+ const location = useLocation()
+ const { runAsync: runAsyncCancelPause, isLoading: isLoadingCancelPause } =
+ cancelPauseReq
+
+ const handleCancelPendingPauseClick = async () => {
+ try {
+ await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
+ const newUrl = new URL(location.toString())
+ newUrl.searchParams.set('flash', 'unpaused')
+ window.history.replaceState(null, '', newUrl)
+ location.reload()
+ } catch (e) {
+ debugConsole.error(e)
+ }
+ }
+
+ return (
+
+ {isSubscriptionEligibleForFlexibleGroupLicensing ? (
+
+ ) : (
+ <>
+ {!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
+
+ {t('change_plan')}
+
+ )}
+ >
+ )}
+ {hasPendingPause && (
+
+ {isLoadingCancelPause ? (
+
+ ) : (
+ t('unpause_subscription')
+ )}
+
+ )}
+ {!onStandalonePlan && (
+ <>
+ {' '}
+
+ >
+ )}
+
+ )
+}
+
+function FlexibleGroupLicensingActions({
+ subscription,
+}: {
+ subscription: PaidSubscription
+}) {
+ const { t } = useTranslation()
+
+ if (subscription.pendingPlan || subscription.payment.hasPastDueInvoice) {
+ return null
+ }
+
+ const isProfessionalPlan = subscription.planCode
+ .toLowerCase()
+ .includes('professional')
+
+ return (
+ <>
+ {!isProfessionalPlan && (
+ <>
+
+ sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
+ }
+ >
+ {t('upgrade_plan')}
+ {' '}
+ >
+ )}
+ {subscription.plan.membersLimitAddOn === 'additional-license' && (
+ sendMB('flex-add-users')}
+ >
+ {t('buy_more_licenses')}
+
+ )}
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx
index 11be8ce3b9..f7a107e31b 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx
@@ -10,7 +10,7 @@ export function TrialEnding({
className,
}: TrialEndingProps) {
return (
-
+
0 && (
<>
* {t('coupons_not_included')}:
-
+
{activeCoupons.map(coupon => (
-
{coupon.description || coupon.name}
diff --git a/services/web/frontend/stories/subscription/active-subscription.stories.tsx b/services/web/frontend/stories/subscription/active-subscription.stories.tsx
new file mode 100644
index 0000000000..bba1a4f7b0
--- /dev/null
+++ b/services/web/frontend/stories/subscription/active-subscription.stories.tsx
@@ -0,0 +1,150 @@
+import React from 'react'
+import { ActiveSubscription } from '../../js/features/subscription/components/dashboard/states/active/active'
+import {
+ annualActiveSubscription,
+ groupActiveSubscription,
+ groupActiveSubscriptionWithPendingLicenseChange,
+ monthlyActiveCollaborator,
+ pendingSubscriptionChange,
+ trialCollaboratorSubscription,
+ trialSubscription,
+ pastDueExpiredSubscription,
+ annualActiveSubscriptionEuro,
+ annualActiveSubscriptionWithAddons,
+ annualActiveSubscriptionWithCoupons,
+ pendingPausedSubscription,
+ groupProfessionalActiveSubscription,
+} from '../../../test/frontend/features/subscription/fixtures/subscriptions'
+import { SubscriptionDashboardProvider } from '../../js/features/subscription/context/subscription-dashboard-context'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
+import { PaidSubscription } from '@ol-types/subscription/dashboard/subscription'
+import type { StoryFn } from '@storybook/react'
+import { setupSubscriptionDashContext } from '../../../test/frontend/features/subscription/helpers/setup-subscription-dash-context'
+
+export default {
+ title: 'Subscription/ActiveSubscription',
+ component: ActiveSubscription,
+ argTypes: {
+ subscription: { control: 'object' },
+ canUseFlexibleLicensing: { control: 'boolean' },
+ },
+}
+
+const Template: StoryFn<{
+ subscription: PaidSubscription
+ canUseFlexibleLicensing: boolean
+}> = args => {
+ window.metaAttributesCache = window.metaAttributesCache || new Map()
+ // @ts-ignore
+ delete global.recurly
+ setupSubscriptionDashContext({
+ metaTags: [
+ { name: 'ol-subscription', value: args.subscription },
+ {
+ name: 'ol-canUseFlexibleLicensing',
+ value: args.canUseFlexibleLicensing,
+ },
+ ],
+ currencyCode: args.subscription.payment.currency,
+ recurlyNotLoaded: false,
+ queryingRecurly: false,
+ })
+ return (
+
+
+
+
+
+ )
+}
+
+export const CollaboratorAnnual = Template.bind({})
+CollaboratorAnnual.args = {
+ subscription: annualActiveSubscription,
+ canUseFlexibleLicensing:
+ annualActiveSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const CollaboratorMonthly = Template.bind({})
+CollaboratorMonthly.args = {
+ subscription: monthlyActiveCollaborator,
+ canUseFlexibleLicensing:
+ monthlyActiveCollaborator.plan?.canUseFlexibleLicensing,
+}
+
+export const CollaboratorAnnualEuro = Template.bind({})
+CollaboratorAnnualEuro.args = {
+ subscription: annualActiveSubscriptionEuro,
+ canUseFlexibleLicensing:
+ annualActiveSubscriptionEuro.plan?.canUseFlexibleLicensing,
+}
+
+export const CollaboratorTrial = Template.bind({})
+CollaboratorTrial.args = {
+ subscription: trialCollaboratorSubscription,
+ canUseFlexibleLicensing:
+ trialCollaboratorSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const PersonalTrial = Template.bind({})
+PersonalTrial.args = {
+ subscription: trialSubscription,
+ canUseFlexibleLicensing: trialSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const GroupStandard = Template.bind({})
+GroupStandard.args = {
+ subscription: groupActiveSubscription,
+ canUseFlexibleLicensing:
+ groupActiveSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const GroupProfessional = Template.bind({})
+GroupProfessional.args = {
+ subscription: groupProfessionalActiveSubscription,
+ canUseFlexibleLicensing:
+ groupProfessionalActiveSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const GroupPendingLicenseChange = Template.bind({})
+GroupPendingLicenseChange.args = {
+ subscription: groupActiveSubscriptionWithPendingLicenseChange,
+ canUseFlexibleLicensing:
+ groupActiveSubscriptionWithPendingLicenseChange.plan
+ ?.canUseFlexibleLicensing,
+}
+
+export const PastDueExpired = Template.bind({})
+PastDueExpired.args = {
+ subscription: pastDueExpiredSubscription,
+ canUseFlexibleLicensing:
+ pastDueExpiredSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const Addons = Template.bind({})
+Addons.args = {
+ subscription: annualActiveSubscriptionWithAddons,
+ canUseFlexibleLicensing:
+ annualActiveSubscriptionWithAddons.plan?.canUseFlexibleLicensing,
+}
+
+export const PendingPaused = Template.bind({})
+PendingPaused.args = {
+ subscription: pendingPausedSubscription,
+ canUseFlexibleLicensing:
+ pendingPausedSubscription.plan?.canUseFlexibleLicensing,
+}
+
+export const PendingPlanChange = Template.bind({})
+PendingPlanChange.args = {
+ subscription: pendingSubscriptionChange,
+ canUseFlexibleLicensing:
+ pendingSubscriptionChange.plan?.canUseFlexibleLicensing,
+}
+
+export const Coupons = Template.bind({})
+Coupons.args = {
+ subscription: annualActiveSubscriptionWithCoupons,
+ canUseFlexibleLicensing:
+ annualActiveSubscriptionWithCoupons.plan?.canUseFlexibleLicensing,
+}
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 baada41976..43bf25b6e4 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
@@ -1,11 +1,15 @@
import { expect } from 'chai'
-import { fireEvent, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor, within } from '@testing-library/react'
import * as eventTracking from '@/infrastructure/event-tracking'
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
import {
annualActiveSubscription,
+ annualActiveSubscriptionEuro,
+ annualActiveSubscriptionWithAddons,
+ annualActiveSubscriptionWithCoupons,
groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange,
+ groupProfessionalActiveSubscription,
monthlyActiveCollaborator,
pendingSubscriptionChange,
trialCollaboratorSubscription,
@@ -37,45 +41,78 @@ describe('', function () {
})
function expectedInActiveSubscription(subscription: PaidSubscription) {
- // sentence broken up by bolding
- screen.getByText('You are currently subscribed to the', { exact: false })
- screen.getByText(subscription.plan.name, { exact: false })
+ if (subscription.plan.annual) {
+ within(screen.getByTestId('billing-period')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Billed annually at ${subscription.payment.displayPrice}`
+ )
+ )
+ )
+ within(screen.getByTestId('plan-only-price')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `${subscription.payment.planOnlyDisplayPrice} per year`
+ )
+ )
+ )
+ } else {
+ within(screen.getByTestId('billing-period')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Billed monthly at ${subscription.payment.displayPrice}`
+ )
+ )
+ )
+ within(screen.getByTestId('plan-only-price')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `${subscription.payment.planOnlyDisplayPrice} per month`
+ )
+ )
+ )
+ }
+ within(screen.getByTestId('renews-on')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Renews on ${subscription.payment.nextPaymentDueDate}`
+ )
+ )
+ )
- screen.getByRole('button', { name: 'Change plan' })
-
- // sentence broken up by bolding
- screen.getByText('The next payment of', { exact: false })
- screen.getByText(subscription.payment.displayPrice, {
- exact: false,
- })
- screen.getByText('will be collected on', { exact: false })
- const dates = screen.getAllByText(subscription.payment.nextPaymentDueAt, {
- exact: false,
- })
- expect(dates.length).to.equal(2)
+ screen.getByRole('heading', { name: subscription.plan.name, level: 3 })
screen.getByText(
'* Prices may be subject to additional VAT, depending on your country.'
)
- screen.getByRole('link', { name: 'Update your billing details' })
- screen.getByRole('link', { name: 'View your invoices' })
+ screen.getByRole('link', { name: 'View invoices' })
+
+ if (subscription.payment.billingDetailsLink) {
+ screen.getByRole('link', { name: 'View billing details' })
+ }
}
it('renders the dash annual active subscription', function () {
renderActiveSubscription(annualActiveSubscription)
expectedInActiveSubscription(annualActiveSubscription)
+
+ const button = screen.getByRole('button', { name: 'Change plan' })
+ expect(button).to.exist
+ })
+
+ it('renders the dash annual active subscription in EUR', function () {
+ renderActiveSubscription(annualActiveSubscriptionEuro)
+ expectedInActiveSubscription(annualActiveSubscriptionEuro)
})
it('shows change plan UI when button clicked', async function () {
renderActiveSubscription(annualActiveSubscription)
+ expectedInActiveSubscription(annualActiveSubscription)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
- // confirm main dash UI still shown
- screen.getByText('You are currently subscribed to the', { exact: false })
-
await screen.findByRole('heading', { name: 'Change plan' })
await waitFor(
() =>
@@ -86,34 +123,9 @@ describe('', function () {
)
})
- it('notes when user is changing plan at end of current plan term', function () {
- renderActiveSubscription(pendingSubscriptionChange)
-
- expectedInActiveSubscription(pendingSubscriptionChange)
-
- screen.getByText('Your plan is changing to', { exact: false })
-
- screen.getByText(pendingSubscriptionChange.pendingPlan!.name)
- screen.getByText(' at the end of the current billing period', {
- exact: false,
- })
-
- screen.getByText(
- 'If you wish this change to apply before the end of your current billing period, please contact us.'
- )
-
- expect(screen.queryByRole('link', { name: 'contact Support' })).to.be.null
- expect(screen.queryByText('if you wish to change your group subscription.'))
- .to.be.null
- })
-
it('does not show "Change plan" option when past due', function () {
// account is likely in expired state, but be sure to not show option if state is still active
- const activePastDueSubscription = Object.assign(
- {},
- JSON.parse(JSON.stringify(annualActiveSubscription))
- )
-
+ const activePastDueSubscription = cloneDeep(annualActiveSubscription)
activePastDueSubscription.payment.hasPastDueInvoice = true
renderActiveSubscription(activePastDueSubscription)
@@ -122,23 +134,41 @@ describe('', function () {
expect(changePlan).to.be.null
})
+ it('notes when user is changing plan at end of current plan term', function () {
+ renderActiveSubscription(pendingSubscriptionChange)
+
+ expectedInActiveSubscription(pendingSubscriptionChange)
+
+ within(screen.getByTestId('pending-plan-change')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Your plan is changing to ${pendingSubscriptionChange.pendingPlan!.name} at the end of the current billing period`
+ )
+ )
+ )
+
+ screen.getByText(
+ 'If you wish this change to apply before the end of your current billing period, please contact us.'
+ )
+ })
+
it('shows the pending license change message when plan change is pending', function () {
renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
- screen.getByText('Your subscription is changing to include', {
- exact: false,
- })
-
- screen.getByText(
- groupActiveSubscriptionWithPendingLicenseChange.payment
- .pendingAdditionalLicenses!
+ within(screen.getByTestId('pending-plan-change')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Your subscription is changing to include ${groupActiveSubscriptionWithPendingLicenseChange.payment.pendingAdditionalLicenses} additional license(s) for a total of ${groupActiveSubscriptionWithPendingLicenseChange.payment.pendingTotalLicenses}`
+ )
+ )
)
- screen.getByText('additional license(s) for a total of', { exact: false })
-
- screen.getByText(
- groupActiveSubscriptionWithPendingLicenseChange.payment
- .pendingTotalLicenses!
+ within(screen.getByTestId('plan-licenses')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Supports up to ${groupActiveSubscriptionWithPendingLicenseChange.payment.totalLicenses}`
+ )
+ )
)
expect(
@@ -148,8 +178,8 @@ describe('', function () {
).to.be.null
})
- it('shows the pending license change message when plan change is not pending', function () {
- const subscription = Object.assign({}, groupActiveSubscription)
+ it('for legacy plans shows the pending license change message when plan change is not pending', function () {
+ const subscription = cloneDeep(groupActiveSubscription)
subscription.payment.additionalLicenses = 4
subscription.payment.totalLicenses =
subscription.payment.totalLicenses +
@@ -157,42 +187,71 @@ describe('', function () {
renderActiveSubscription(subscription)
- screen.getByText('Your subscription includes', {
- exact: false,
- })
-
- screen.getByText(subscription.payment.additionalLicenses)
-
- screen.getByText('additional license(s) for a total of', { exact: false })
-
- screen.getByText(subscription.payment.totalLicenses)
+ within(screen.getByTestId('plan-licenses')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `Plus ${subscription.payment.additionalLicenses} additional license(s) for a total of ${subscription.payment.totalLicenses}`
+ )
+ )
+ )
})
it('shows when trial ends and first payment collected and when subscription would become inactive if cancelled', function () {
renderActiveSubscription(trialSubscription)
- screen.getByText('You’re on a free trial which ends on', { exact: false })
- const endDate = screen.getAllByText(
- trialSubscription.payment.trialEndsAtFormatted!
+ within(screen.getByTestId('trial-ending')).getByText((_, el) =>
+ Boolean(
+ el?.textContent?.includes(
+ `You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}`
+ )
+ )
)
- expect(endDate.length).to.equal(3)
})
- it('shows current discounts', function () {
- const subscriptionWithActiveCoupons = cloneDeep(annualActiveSubscription)
- subscriptionWithActiveCoupons.payment.activeCoupons = [
- {
- name: 'fake coupon name',
- code: 'fake-coupon',
- description: '',
- },
- ]
- renderActiveSubscription(subscriptionWithActiveCoupons)
- screen.getByText(
- /this does not include your current discounts, which will be applied automatically before your next payment/i
+ it('shows correct actions for group plan: professional ', function () {
+ renderActiveSubscription(groupProfessionalActiveSubscription)
+ screen.getByRole('link', { name: /buy more licenses/i })
+ })
+
+ it('shows correct actions for group plan: standard (collaborator)', function () {
+ renderActiveSubscription(groupActiveSubscription)
+ screen.getByRole('link', { name: /upgrade plan/i })
+ screen.getByRole('link', { name: /buy more licenses/i })
+ })
+
+ it('shows add-ons if present', function () {
+ renderActiveSubscription(annualActiveSubscriptionWithAddons)
+ screen.getByText('AI Assist')
+ })
+
+ it('shows empty add-ons message if none present', function () {
+ renderActiveSubscription(annualActiveSubscription)
+ screen.getByText(/You don’t have any add-ons on your account/i)
+ })
+
+ it('shows multiple active coupons', function () {
+ renderActiveSubscription(annualActiveSubscriptionWithCoupons)
+ within(screen.getByTestId('active-coupons')).getByText(
+ 'Coupon1 for 10% off',
+ { exact: false }
)
- screen.getByText(
- subscriptionWithActiveCoupons.payment.activeCoupons[0].name
+ within(screen.getByTestId('active-coupons')).getByText(
+ 'Coupon2 for 15% off',
+ { exact: false }
+ )
+ })
+
+ it('renders correct hrefs for invoice and billing details links', function () {
+ renderActiveSubscription(annualActiveSubscription)
+ const invoiceLink = screen.getByRole('link', { name: 'View invoices' })
+ expect(invoiceLink.getAttribute('href')).to.equal(
+ annualActiveSubscription.payment.accountManagementLink
+ )
+ const billingLink = screen.getByRole('link', {
+ name: 'View billing details',
+ })
+ expect(billingLink.getAttribute('href')).to.equal(
+ annualActiveSubscription.payment.billingDetailsLink
)
})
@@ -523,13 +582,5 @@ describe('', function () {
const changePlan = screen.queryByRole('button', { name: 'Change plan' })
expect(changePlan).to.be.null
})
-
- it('shows contact Support message for group plan change requests', function () {
- renderActiveSubscription(groupActiveSubscription)
- screen.getByRole('link', { name: 'contact Support' })
- screen.getByText('if you wish to change your group subscription.', {
- exact: false,
- })
- })
})
})
diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
index 8011c5206d..56c37172dc 100644
--- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
+++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts
@@ -31,6 +31,7 @@ export const annualActiveSubscription: PaidSubscription = {
price_in_cents: 21900,
annual: true,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -48,7 +49,7 @@ export const annualActiveSubscription: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$199.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -57,6 +58,186 @@ export const annualActiveSubscription: PaidSubscription = {
},
}
+export const annualActiveSubscriptionWithCoupons: PaidSubscription = {
+ manager_ids: ['abc123'],
+ member_ids: [],
+ invited_emails: [],
+ groupPlan: false,
+ membersLimit: 0,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'collaborator-annual',
+ plan: {
+ planCode: 'collaborator-annual',
+ name: 'Standard (Collaborator) Annual',
+ price_in_cents: 21900,
+ annual: true,
+ featureDescription: [],
+ canUseFlexibleLicensing: false,
+ },
+ payment: {
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/payment/billing-details',
+ accountManagementLink: '/user/subscription/payment/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 0,
+ nextPaymentDueAt,
+ nextPaymentDueDate,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: null,
+ trialEndsAt: null,
+ activeCoupons: [
+ { name: 'Coupon1', code: 'c1', description: 'Coupon1 for 10% off' },
+ { name: 'Coupon2', code: 'c2', description: 'Coupon2 for 15% off' },
+ ],
+ accountEmail: 'fake@example.com',
+ hasPastDueInvoice: false,
+ displayPrice: '$199.00',
+ planOnlyDisplayPrice: '$199.00',
+ addOns: [],
+ addOnDisplayPricesWithoutAdditionalLicense: {},
+ isEligibleForGroupPlan: true,
+ isEligibleForPause: false,
+ isEligibleForDowngradeUpsell: false,
+ },
+}
+
+export const pendingPausedSubscription: PaidSubscription = {
+ manager_ids: ['abc123'],
+ member_ids: [],
+ invited_emails: [],
+ groupPlan: false,
+ membersLimit: 0,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'collaborator-annual',
+ plan: {
+ planCode: 'collaborator-annual',
+ name: 'Standard (Collaborator) Annual',
+ price_in_cents: 21900,
+ annual: true,
+ featureDescription: [],
+ canUseFlexibleLicensing: false,
+ },
+ payment: {
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/payment/billing-details',
+ accountManagementLink: '/user/subscription/payment/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 0,
+ nextPaymentDueAt,
+ nextPaymentDueDate,
+ currency: 'USD',
+ state: 'active',
+ remainingPauseCycles: 1,
+ trialEndsAtFormatted: null,
+ trialEndsAt: null,
+ activeCoupons: [],
+ accountEmail: 'fake@example.com',
+ hasPastDueInvoice: false,
+ displayPrice: '$199.00',
+ planOnlyDisplayPrice: '$199.00',
+ addOns: [],
+ addOnDisplayPricesWithoutAdditionalLicense: {},
+ isEligibleForGroupPlan: true,
+ isEligibleForPause: false,
+ isEligibleForDowngradeUpsell: false,
+ },
+}
+
+export const pausedSubscription: PaidSubscription = {
+ manager_ids: ['abc123'],
+ member_ids: [],
+ invited_emails: [],
+ groupPlan: false,
+ membersLimit: 0,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'collaborator-annual',
+ plan: {
+ planCode: 'collaborator-annual',
+ name: 'Standard (Collaborator) Annual',
+ price_in_cents: 21900,
+ annual: true,
+ featureDescription: [],
+ canUseFlexibleLicensing: false,
+ },
+ payment: {
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/payment/billing-details',
+ accountManagementLink: '/user/subscription/payment/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 0,
+ nextPaymentDueAt,
+ nextPaymentDueDate,
+ currency: 'USD',
+ state: 'paused',
+ remainingPauseCycles: 1,
+ trialEndsAtFormatted: null,
+ trialEndsAt: null,
+ activeCoupons: [],
+ accountEmail: 'fake@example.com',
+ hasPastDueInvoice: false,
+ displayPrice: '$199.00',
+ planOnlyDisplayPrice: '$199.00',
+ addOns: [],
+ addOnDisplayPricesWithoutAdditionalLicense: {},
+ isEligibleForGroupPlan: true,
+ isEligibleForPause: false,
+ isEligibleForDowngradeUpsell: false,
+ },
+}
+
+export const annualActiveSubscriptionWithAddons: PaidSubscription = {
+ manager_ids: ['abc123'],
+ member_ids: [],
+ invited_emails: [],
+ groupPlan: false,
+ membersLimit: 0,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'collaborator-annual',
+ plan: {
+ planCode: 'collaborator-annual',
+ name: 'Standard (Collaborator) Annual',
+ price_in_cents: 21900,
+ annual: true,
+ featureDescription: [],
+ canUseFlexibleLicensing: false,
+ },
+ payment: {
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/payment/billing-details',
+ accountManagementLink: '/user/subscription/payment/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 0,
+ nextPaymentDueAt,
+ nextPaymentDueDate,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: null,
+ trialEndsAt: null,
+ activeCoupons: [],
+ accountEmail: 'fake@example.com',
+ hasPastDueInvoice: false,
+ displayPrice: '$199.00',
+ planOnlyDisplayPrice: '$199.00',
+ addOns: [],
+ addOnDisplayPricesWithoutAdditionalLicense: {
+ assistant: '$100.00',
+ },
+ isEligibleForGroupPlan: true,
+ isEligibleForPause: false,
+ isEligibleForDowngradeUpsell: false,
+ },
+ addOns: [{ addOnCode: 'assistant', quantity: 1, unitAmountInCents: 10000 }],
+}
+
export const annualActiveSubscriptionEuro: PaidSubscription = {
manager_ids: ['abc123'],
member_ids: [],
@@ -73,6 +254,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
price_in_cents: 21900,
annual: true,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0.24,
@@ -90,7 +272,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '€221.96',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '€221.96',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -114,6 +296,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
name: 'Professional',
price_in_cents: 4500,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -131,7 +314,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$42.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$42.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -156,6 +339,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
price_in_cents: 21900,
annual: true,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -173,7 +357,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: true,
displayPrice: '$199.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$199.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -198,6 +382,7 @@ export const canceledSubscription: PaidSubscription = {
price_in_cents: 21900,
annual: true,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -215,7 +400,7 @@ export const canceledSubscription: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$199.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -240,6 +425,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
price_in_cents: 21900,
annual: true,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -257,7 +443,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$199.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$199.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -293,6 +479,7 @@ export const groupActiveSubscription: GroupSubscription = {
groupPlan: true,
membersLimit: 10,
membersLimitAddOn: 'additional-license',
+ canUseFlexibleLicensing: true,
},
payment: {
taxRate: 0,
@@ -310,7 +497,54 @@ export const groupActiveSubscription: GroupSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$1290.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$1290.00',
+ addOns: [],
+ addOnDisplayPricesWithoutAdditionalLicense: {},
+ isEligibleForGroupPlan: true,
+ isEligibleForPause: false,
+ isEligibleForDowngradeUpsell: false,
+ },
+}
+
+export const groupProfessionalActiveSubscription: GroupSubscription = {
+ manager_ids: ['abc123'],
+ member_ids: ['abc123'],
+ invited_emails: [],
+ groupPlan: true,
+ teamName: 'GAS',
+ membersLimit: 2,
+ _id: 'bcd567',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'group_professional_2_enterprise',
+ plan: {
+ planCode: 'group_professional_2_enterprise',
+ name: 'Group Professional Plan (2 licenses)',
+ hideFromUsers: true,
+ price_in_cents: 129000,
+ annual: true,
+ groupPlan: true,
+ membersLimit: 2,
+ membersLimitAddOn: 'additional-license',
+ canUseFlexibleLicensing: true,
+ },
+ payment: {
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/payment/billing-details',
+ accountManagementLink: '/user/subscription/payment/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 10,
+ nextPaymentDueAt,
+ nextPaymentDueDate,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: null,
+ trialEndsAt: null,
+ activeCoupons: [],
+ accountEmail: 'fake@example.com',
+ hasPastDueInvoice: false,
+ displayPrice: '$1290.00',
+ planOnlyDisplayPrice: '$1290.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -340,6 +574,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
groupPlan: true,
membersLimit: 10,
membersLimitAddOn: 'additional-license',
+ canUseFlexibleLicensing: true,
},
payment: {
taxRate: 0,
@@ -359,7 +594,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
displayPrice: '$2967.00',
pendingAdditionalLicenses: 13,
pendingTotalLicenses: 23,
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$2967.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -411,7 +646,7 @@ export const trialSubscription: PaidSubscription = {
accountEmail: 'fake@example.com',
hasPastDueInvoice: false,
displayPrice: '$14.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$14.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -456,6 +691,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
price_in_cents: 2300,
featureDescription: [],
hideFromUsers: true,
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -473,7 +709,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
accountEmail: 'foo@example.com',
hasPastDueInvoice: false,
displayPrice: '$21.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$21.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
@@ -497,6 +733,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
name: 'Standard (Collaborator)',
price_in_cents: 212300900,
featureDescription: [],
+ canUseFlexibleLicensing: false,
},
payment: {
taxRate: 0,
@@ -514,7 +751,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
accountEmail: 'foo@example.com',
hasPastDueInvoice: false,
displayPrice: '$21.00',
- planOnlyDisplayPrice: '',
+ planOnlyDisplayPrice: '$21.00',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
diff --git a/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx b/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
index 110c6c4f74..4f2effd4c7 100644
--- a/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
+++ b/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
@@ -8,7 +8,8 @@ import { CurrencyCode } from '../../../../../types/subscription/currency'
export function renderActiveSubscription(
subscription: PaidSubscription,
tags: MetaTag[] = [],
- currencyCode?: CurrencyCode
+ currencyCode?: CurrencyCode,
+ canUseFlexibleLicensing?: boolean
) {
renderWithSubscriptionDashContext(
,
@@ -26,6 +27,13 @@ export function renderActiveSubscription(
name: 'ol-recommendedCurrency',
value: currencyCode || 'USD',
},
+ {
+ name: 'ol-canUseFlexibleLicensing',
+ value:
+ canUseFlexibleLicensing ||
+ subscription.plan?.canUseFlexibleLicensing ||
+ false,
+ },
],
}
)
diff --git a/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx b/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx
index 1246b7271b..9faa955fb2 100644
--- a/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx
+++ b/services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx
@@ -1,10 +1,10 @@
import { render } from '@testing-library/react'
import _ from 'lodash'
import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
-import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
import fetchMock from 'fetch-mock'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { MetaTag } from '@/utils/meta'
+import { setupSubscriptionDashContext } from './setup-subscription-dash-context'
export function renderWithSubscriptionDashContext(
component: React.ReactElement,
@@ -25,62 +25,7 @@ export function renderWithSubscriptionDashContext(
)
- options?.metaTags?.forEach(tag =>
- window.metaAttributesCache.set(tag!.name, tag!.value)
- )
- window.metaAttributesCache.set('ol-user', {})
-
- if (!options?.recurlyNotLoaded) {
- // @ts-ignore
- global.recurly = {
- configure: () => {},
- Pricing: {
- Subscription: () => {
- return {
- plan: (planCode: string) => {
- let plan
- const isGroupPlan = planCode.includes('group')
- if (isGroupPlan) {
- const [, planType, size, usage] = planCode.split('_')
- const currencyCode = options?.currencyCode || 'USD'
- plan = _.get(groupPriceByUsageTypeAndSize, [
- usage,
- planType,
- currencyCode,
- size,
- ])
- } else {
- plan = plans.find(p => p.planCode === planCode)
- }
-
- const response = {
- next: {
- total: plan?.price_in_cents
- ? plan.price_in_cents / 100
- : undefined,
- },
- }
- return {
- currency: () => {
- return {
- catch: () => {
- return {
- done: (callback: (response: object) => void) => {
- if (!options?.queryingRecurly) {
- return callback(response)
- }
- },
- }
- },
- }
- },
- }
- },
- }
- },
- },
- }
- }
+ setupSubscriptionDashContext(options)
return render(component, {
wrapper: SubscriptionDashboardProviderWrapper,
diff --git a/services/web/test/frontend/features/subscription/helpers/setup-subscription-dash-context.ts b/services/web/test/frontend/features/subscription/helpers/setup-subscription-dash-context.ts
new file mode 100644
index 0000000000..292cfa8d08
--- /dev/null
+++ b/services/web/test/frontend/features/subscription/helpers/setup-subscription-dash-context.ts
@@ -0,0 +1,66 @@
+import _ from 'lodash'
+import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
+import { MetaTag } from '@/utils/meta'
+
+export function setupSubscriptionDashContext(options?: {
+ metaTags?: MetaTag[]
+ recurlyNotLoaded?: boolean
+ queryingRecurly?: boolean
+ currencyCode?: string
+}) {
+ options?.metaTags?.forEach(tag =>
+ window.metaAttributesCache.set(tag!.name, tag!.value)
+ )
+ window.metaAttributesCache.set('ol-user', {})
+ if (!options?.recurlyNotLoaded) {
+ // @ts-ignore
+ global.recurly = {
+ configure: () => {},
+ Pricing: {
+ Subscription: () => {
+ return {
+ plan: (planCode: string) => {
+ let plan
+ const isGroupPlan = planCode.includes('group')
+ if (isGroupPlan) {
+ const [, planType, size, usage] = planCode.split('_')
+ const currencyCode = options?.currencyCode || 'USD'
+ plan = _.get(groupPriceByUsageTypeAndSize, [
+ usage,
+ planType,
+ currencyCode,
+ size,
+ ])
+ } else {
+ plan = plans.find(p => p.planCode === planCode)
+ }
+
+ const response = {
+ next: {
+ total: plan?.price_in_cents
+ ? plan.price_in_cents / 100
+ : undefined,
+ },
+ }
+ return {
+ currency: () => {
+ return {
+ catch: () => {
+ return {
+ done: (callback: (response: object) => void) => {
+ if (!options?.queryingRecurly) {
+ return callback(response)
+ }
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ },
+ }
+ }
+}