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 - /> -

-
- {subscription.payment.billingDetailsLink ? ( - <> - - {t('view_invoices')} - - - {t('view_billing_details')} - - - ) : ( - - {t('view_payment_portal')} - - )} -
-
- - {!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 + /> +

+
+ {subscription.payment.billingDetailsLink ? ( + <> + + {t('view_invoices')} + + + {t('view_billing_details')} + + + ) : ( + + {t('view_payment_portal')} + + )} +
+
+ + {!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({ />

{t('you_can_still_use_your_premium_features')}

-

- - {isLoadingCancelPause ? ( - - ) : ( - t('unpause_subscription') - )} - -

)} - -

- , - // eslint-disable-next-line react/jsx-key - , - ]} - /> -

- -
- -

- - {t('update_your_billing_details')} - {' '} - - {t('view_your_invoices')} - - {!recurlyLoadError && ( - <> - {' '} - - - )} -

- + {!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/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) + } + }, + } + }, + } + }, + } + }, + } + }, + }, + } + } +}