Kaynağa Gözat

Update unit tests for ActiveSubscription

GitOrigin-RevId: 181f5a097fff2fa31ed11d39b76f40c9a4b4ca31
Simon Gardner 11 ay önce
ebeveyn
işleme
d4fe9cf34b

+ 2 - 2
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -1,7 +1,7 @@
 import { Trans, useTranslation } from 'react-i18next'
 import { Trans, useTranslation } from 'react-i18next'
 import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { PausedSubscription } from './states/active/paused'
 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 { CanceledSubscription } from './states/canceled'
 import { ExpiredSubscription } from './states/expired'
 import { ExpiredSubscription } from './states/expired'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
@@ -43,7 +43,7 @@ function PersonalSubscriptionStates({
 
 
   if (state === 'active') {
   if (state === 'active') {
     // This version handles subscriptions with and without addons
     // This version handles subscriptions with and without addons
-    return <ActiveSubscriptionNew subscription={subscription} />
+    return <ActiveSubscription subscription={subscription} />
   } else if (state === 'canceled') {
   } else if (state === 'canceled') {
     return <CanceledSubscription subscription={subscription} />
     return <CanceledSubscription subscription={subscription} />
   } else if (state === 'expired') {
   } else if (state === 'expired') {

+ 0 - 391
services/web/frontend/js/features/subscription/components/dashboard/states/active/active-new.tsx

@@ -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 <CancelSubscription />
-
-  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 (
-    <>
-      <div className="notification-list">
-        <FlashMessage />
-
-        {isErrorPause && (
-          <Notification
-            type="error"
-            content={t('generic_something_went_wrong')}
-          />
-        )}
-      </div>
-      <h2 className="h3 fw-bold">{t('billing')}</h2>
-      <p className="mb-1">
-        {subscription.plan.annual ? (
-          <Trans
-            i18nKey="billed_annually_at"
-            values={{ price: subscription.payment.displayPrice }}
-            shouldUnescape
-            tOptions={{ interpolation: { escapeValue: true } }}
-            components={[
-              // eslint-disable-next-line react/jsx-key
-              <strong />,
-              // eslint-disable-next-line react/jsx-key
-              <i />,
-            ]}
-          />
-        ) : (
-          <Trans
-            i18nKey="billed_monthly_at"
-            values={{ price: subscription.payment.displayPrice }}
-            shouldUnescape
-            tOptions={{ interpolation: { escapeValue: true } }}
-            components={[
-              // eslint-disable-next-line react/jsx-key
-              <strong />,
-              // eslint-disable-next-line react/jsx-key
-              <i />,
-            ]}
-          />
-        )}
-      </p>
-      <p className="mb-1">
-        <Trans
-          i18nKey="renews_on"
-          values={{ date: subscription.payment.nextPaymentDueDate }}
-          shouldUnescape
-          tOptions={{ interpolation: { escapeValue: true } }}
-          components={[<strong />]} // eslint-disable-line react/jsx-key
-        />
-      </p>
-      <div>
-        {subscription.payment.billingDetailsLink ? (
-          <>
-            <a
-              href={subscription.payment.accountManagementLink}
-              target="_blank"
-              rel="noreferrer noopener"
-              className="me-2"
-            >
-              {t('view_invoices')}
-            </a>
-            <a
-              href={subscription.payment.billingDetailsLink}
-              target="_blank"
-              rel="noreferrer noopener"
-            >
-              {t('view_billing_details')}
-            </a>
-          </>
-        ) : (
-          <a
-            href={subscription.payment.accountManagementLink}
-            rel="noreferrer noopener"
-            className="me-2"
-          >
-            {t('view_payment_portal')}
-          </a>
-        )}
-      </div>
-      <div className="mt-3">
-        <PriceExceptions subscription={subscription} />
-        {!recurlyLoadError && (
-          <p>
-            <i>
-              <SubscriptionRemainder subscription={subscription} hideTime />
-            </i>
-          </p>
-        )}
-      </div>
-      <hr />
-      <h2 className="h3 fw-bold">{t('plan')}</h2>
-      <h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
-      {subscription.pendingPlan &&
-        subscription.pendingPlan.name !== subscription.plan.name && (
-          <p className="mb-1">{t('want_change_to_apply_before_plan_end')}</p>
-        )}
-      {isInFreeTrial(subscription.payment.trialEndsAt) &&
-        subscription.payment.trialEndsAtFormatted && (
-          <TrialEnding
-            trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
-            className="mb-1"
-          />
-        )}
-      {subscription.payment.totalLicenses > 0 && (
-        <p className="mb-1">
-          {isLegacyPlan && subscription.payment.additionalLicenses > 0 ? (
-            <Trans
-              i18nKey="plus_x_additional_licenses_for_a_total_of_y_licenses"
-              values={{
-                count: subscription.payment.totalLicenses,
-                additionalLicenses: subscription.payment.additionalLicenses,
-              }}
-              shouldUnescape
-              tOptions={{ interpolation: { escapeValue: true } }}
-              components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
-            />
-          ) : (
-            <Trans
-              i18nKey="supports_up_to_x_licenses"
-              values={{ count: subscription.payment.totalLicenses }}
-              shouldUnescape
-              tOptions={{ interpolation: { escapeValue: true } }}
-              components={[<strong />]} // eslint-disable-line react/jsx-key
-            />
-          )}
-        </p>
-      )}
-      {hasPendingPause && (
-        <>
-          <p>
-            <Trans
-              i18nKey="your_subscription_will_pause_on"
-              values={{
-                planName: subscription.plan.name,
-                pauseDate: subscription.payment.nextPaymentDueAt,
-                reactivationDate: getFormattedRenewalDate(),
-              }}
-              shouldUnescape
-              tOptions={{ interpolation: { escapeValue: true } }}
-              components={[
-                // eslint-disable-next-line react/jsx-key
-                <strong />,
-              ]}
-            />
-          </p>
-          <p>{t('you_can_still_use_your_premium_features')}</p>
-        </>
-      )}
-      {!onStandalonePlan && (
-        <p className="mb-1">
-          {subscription.plan.annual
-            ? t('x_price_per_year', {
-                price: subscription.payment.planOnlyDisplayPrice,
-              })
-            : t('x_price_per_month', {
-                price: subscription.payment.planOnlyDisplayPrice,
-              })}
-        </p>
-      )}
-      {!recurlyLoadError && (
-        <PlanActions
-          subscription={subscription}
-          onStandalonePlan={onStandalonePlan}
-          handlePlanChange={handlePlanChange}
-          hasPendingPause={hasPendingPause}
-          cancelPauseReq={cancelPauseReq}
-        />
-      )}
-      <hr />
-      <AddOns
-        subscription={subscription}
-        onStandalonePlan={onStandalonePlan}
-        handleCancelClick={handleCancelClick}
-      />
-
-      <ChangePlanModal />
-      <ConfirmChangePlanModal />
-      <KeepCurrentPlanModal />
-      <ChangeToGroupModal />
-      <CancelAiAddOnModal />
-      <PauseSubscriptionModal />
-    </>
-  )
-}
-
-type PlanActionsProps = {
-  subscription: PaidSubscription
-  onStandalonePlan: boolean
-  handlePlanChange: () => void
-  hasPendingPause: boolean
-  cancelPauseReq: ReturnType<typeof useAsync>
-}
-
-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 (
-    <div className="mt-3">
-      {isSubscriptionEligibleForFlexibleGroupLicensing ? (
-        <FlexibleGroupLicensingActions subscription={subscription} />
-      ) : (
-        <>
-          {!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
-            <OLButton variant="secondary" onClick={handlePlanChange}>
-              {t('change_plan')}
-            </OLButton>
-          )}
-        </>
-      )}
-      {hasPendingPause && (
-        <OLButton
-          variant="primary"
-          onClick={handleCancelPendingPauseClick}
-          disabled={isLoadingCancelPause}
-        >
-          {isLoadingCancelPause ? (
-            <LoadingSpinner />
-          ) : (
-            t('unpause_subscription')
-          )}
-        </OLButton>
-      )}
-      {!onStandalonePlan && (
-        <>
-          {' '}
-          <CancelSubscriptionButton />
-        </>
-      )}
-    </div>
-  )
-}
-
-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 && (
-        <>
-          <OLButton
-            variant="secondary"
-            href="/user/subscription/group/upgrade-subscription"
-            onClick={() =>
-              sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
-            }
-          >
-            {t('upgrade_plan')}
-          </OLButton>{' '}
-        </>
-      )}
-      {subscription.plan.membersLimitAddOn === 'additional-license' && (
-        <OLButton
-          variant="secondary"
-          href="/user/subscription/group/add-users"
-          onClick={() => sendMB('flex-add-users')}
-        >
-          {t('buy_more_licenses')}
-        </OLButton>
-      )}
-    </>
-  )
-}

+ 300 - 135
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 { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { CancelSubscriptionButton } from './cancel-subscription-button'
 import { CancelSubscriptionButton } from './cancel-subscription-button'
 import { CancelSubscription } from './cancel-plan/cancel-subscription'
 import { CancelSubscription } from './cancel-plan/cancel-subscription'
-import { PendingPlanChange } from './pending-plan-change'
 import { TrialEnding } from './trial-ending'
 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 { ChangePlanModal } from './change-plan/modals/change-plan-modal'
 import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
 import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
 import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
 import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
 import { ChangeToGroupModal } from './change-plan/modals/change-to-group-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 OLButton from '@/shared/components/ol/ol-button'
-import useAsync from '@/shared/hooks/use-async'
+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 { postJSON } from '@/infrastructure/fetch-json'
-import PauseSubscriptionModal from '../../pause-modal'
-import Notification from '@/shared/components/notification'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
-import { FlashMessage } from './flash-message'
+import useAsync from '@/shared/hooks/use-async'
 import { useLocation } from '@/shared/hooks/use-location'
 import { useLocation } from '@/shared/hooks/use-location'
-import LoadingSpinner from '@/shared/components/loading-spinner'
+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({
 export function ActiveSubscription({
   subscription,
   subscription,
@@ -34,33 +41,55 @@ export function ActiveSubscription({
     recurlyLoadError,
     recurlyLoadError,
     setModalIdShown,
     setModalIdShown,
     showCancellation,
     showCancellation,
+    institutionMemberships,
+    memberGroupSubscriptions,
     getFormattedRenewalDate,
     getFormattedRenewalDate,
   } = useSubscriptionDashboardContext()
   } = useSubscriptionDashboardContext()
-  const {
-    isError: isErrorPause,
-    runAsync: runAsyncCancelPause,
-    isLoading: isLoadingCancelPause,
-  } = useAsync()
-  const location = useLocation()
+  const cancelPauseReq = useAsync()
+  const { isError: isErrorPause } = cancelPauseReq
 
 
   if (showCancellation) return <CancelSubscription />
   if (showCancellation) return <CancelSubscription />
 
 
-  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 (
   return (
     <>
     <>
@@ -74,66 +103,130 @@ export function ActiveSubscription({
           />
           />
         )}
         )}
       </div>
       </div>
-      <p>
-        {!hasPendingPause && (
+      <h2 className="h3 fw-bold">{t('billing')}</h2>
+      <p className="mb-1" data-testid="billing-period">
+        {subscription.plan.annual ? (
+          <Trans
+            i18nKey="billed_annually_at"
+            values={{ price: subscription.payment.displayPrice }}
+            shouldUnescape
+            tOptions={{ interpolation: { escapeValue: true } }}
+            components={[
+              // eslint-disable-next-line react/jsx-key
+              <strong />,
+              // eslint-disable-next-line react/jsx-key
+              <i />,
+            ]}
+          />
+        ) : (
           <Trans
           <Trans
-            i18nKey="currently_subscribed_to_plan"
-            values={{
-              planName: subscription.plan.name,
-            }}
+            i18nKey="billed_monthly_at"
+            values={{ price: subscription.payment.displayPrice }}
             shouldUnescape
             shouldUnescape
             tOptions={{ interpolation: { escapeValue: true } }}
             tOptions={{ interpolation: { escapeValue: true } }}
             components={[
             components={[
               // eslint-disable-next-line react/jsx-key
               // eslint-disable-next-line react/jsx-key
               <strong />,
               <strong />,
+              // eslint-disable-next-line react/jsx-key
+              <i />,
             ]}
             ]}
           />
           />
         )}
         )}
-        {subscription.pendingPlan && (
+      </p>
+      <p className="mb-1" data-testid="renews-on">
+        <Trans
+          i18nKey="renews_on"
+          values={{ date: subscription.payment.nextPaymentDueDate }}
+          shouldUnescape
+          tOptions={{ interpolation: { escapeValue: true } }}
+          components={[<strong />]} // eslint-disable-line react/jsx-key
+        />
+      </p>
+      <div>
+        {subscription.payment.billingDetailsLink ? (
           <>
           <>
-            {' '}
-            <PendingPlanChange subscription={subscription} />
+            <a
+              href={subscription.payment.accountManagementLink}
+              target="_blank"
+              rel="noreferrer noopener"
+              className="me-2"
+            >
+              {t('view_invoices')}
+            </a>
+            <a
+              href={subscription.payment.billingDetailsLink}
+              target="_blank"
+              rel="noreferrer noopener"
+            >
+              {t('view_billing_details')}
+            </a>
           </>
           </>
+        ) : (
+          <a
+            href={subscription.payment.accountManagementLink}
+            rel="noreferrer noopener"
+            className="me-2"
+          >
+            {t('view_payment_portal')}
+          </a>
         )}
         )}
-        {!subscription.pendingPlan &&
-          subscription.payment.additionalLicenses > 0 && (
-            <>
-              {' '}
-              <PendingAdditionalLicenses
-                additionalLicenses={subscription.payment.additionalLicenses}
-                totalLicenses={subscription.payment.totalLicenses}
-              />
-            </>
-          )}
-        {!recurlyLoadError &&
-          !subscription.groupPlan &&
-          !hasPendingPause &&
-          !subscription.payment.hasPastDueInvoice && (
-            <>
-              {' '}
-              <OLButton
-                variant="link"
-                className="btn-inline-link"
-                onClick={() => setModalIdShown('change-plan')}
-              >
-                {t('change_plan')}
-              </OLButton>
-            </>
-          )}
-      </p>
+      </div>
+      <div className="mt-3">
+        <PriceExceptions subscription={subscription} />
+        {!recurlyLoadError && (
+          <p>
+            <i>
+              <SubscriptionRemainder subscription={subscription} hideTime />
+            </i>
+          </p>
+        )}
+      </div>
+      <hr />
+      <h2 className="h3 fw-bold">{t('plan')}</h2>
+      <h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
       {subscription.pendingPlan &&
       {subscription.pendingPlan &&
         subscription.pendingPlan.name !== subscription.plan.name && (
         subscription.pendingPlan.name !== subscription.plan.name && (
-          <p>{t('want_change_to_apply_before_plan_end')}</p>
+          <p className="mb-1">{t('want_change_to_apply_before_plan_end')}</p>
         )}
         )}
-      {(!subscription.pendingPlan ||
-        subscription.pendingPlan.name === subscription.plan.name) &&
-        subscription.plan.groupPlan && <ContactSupportToChangeGroupPlan />}
       {isInFreeTrial(subscription.payment.trialEndsAt) &&
       {isInFreeTrial(subscription.payment.trialEndsAt) &&
         subscription.payment.trialEndsAtFormatted && (
         subscription.payment.trialEndsAtFormatted && (
           <TrialEnding
           <TrialEnding
             trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
             trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
+            className="mb-1"
           />
           />
         )}
         )}
+      {subscription.payment.totalLicenses > 0 && (
+        <p className="mb-1" data-testid="plan-licenses">
+          {isLegacyPlan &&
+          subscription.payment.additionalLicenses > 0 &&
+          !subscription.payment.pendingAdditionalLicenses ? (
+            <Trans
+              i18nKey="plus_x_additional_licenses_for_a_total_of_y_licenses"
+              values={{
+                count: subscription.payment.totalLicenses,
+                additionalLicenses: subscription.payment.additionalLicenses,
+              }}
+              shouldUnescape
+              tOptions={{ interpolation: { escapeValue: true } }}
+              components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
+            />
+          ) : (
+            <Trans
+              i18nKey="supports_up_to_x_licenses"
+              values={{ count: subscription.payment.totalLicenses }}
+              shouldUnescape
+              tOptions={{ interpolation: { escapeValue: true } }}
+              components={[<strong />]} // eslint-disable-line react/jsx-key
+            />
+          )}
+        </p>
+      )}
+      {subscription.pendingPlan && (
+        <p className="mb-1" data-testid="pending-plan-change">
+          {' '}
+          <PendingPlanChange subscription={subscription} />
+        </p>
+      )}
 
 
       {hasPendingPause && (
       {hasPendingPause && (
         <>
         <>
@@ -154,83 +247,155 @@ export function ActiveSubscription({
             />
             />
           </p>
           </p>
           <p>{t('you_can_still_use_your_premium_features')}</p>
           <p>{t('you_can_still_use_your_premium_features')}</p>
-          <p>
-            <OLButton
-              variant="primary"
-              onClick={handleCancelPendingPauseClick}
-              disabled={isLoadingCancelPause}
-            >
-              {isLoadingCancelPause ? (
-                <LoadingSpinner />
-              ) : (
-                t('unpause_subscription')
-              )}
-            </OLButton>
-          </p>
         </>
         </>
       )}
       )}
-
-      <p>
-        <Trans
-          i18nKey="next_payment_of_x_collectected_on_y"
-          values={{
-            paymentAmmount: subscription.payment.displayPrice,
-            collectionDate: getFormattedRenewalDate(),
-          }}
-          shouldUnescape
-          tOptions={{ interpolation: { escapeValue: true } }}
-          components={[
-            // eslint-disable-next-line react/jsx-key
-            <strong />,
-            // eslint-disable-next-line react/jsx-key
-            <strong />,
-          ]}
-        />
-      </p>
-
-      <hr />
-      <PriceExceptions subscription={subscription} />
-      <p className="d-inline-flex flex-wrap gap-1">
-        <a
-          href={subscription.payment.billingDetailsLink}
-          target="_blank"
-          rel="noreferrer noopener"
-          className="btn btn-secondary-info btn-secondary"
-        >
-          {t('update_your_billing_details')}
-        </a>{' '}
-        <a
-          href={subscription.payment.accountManagementLink}
-          target="_blank"
-          rel="noreferrer noopener"
-          className="btn btn-secondary-info btn-secondary"
-        >
-          {t('view_your_invoices')}
-        </a>
-        {!recurlyLoadError && (
-          <>
-            {' '}
-            <CancelSubscriptionButton />
-          </>
-        )}
-      </p>
-
+      {!onStandalonePlan && (
+        <p className="mb-1" data-testid="plan-only-price">
+          {subscription.plan.annual
+            ? t('x_price_per_year', {
+                price: subscription.payment.planOnlyDisplayPrice,
+              })
+            : t('x_price_per_month', {
+                price: subscription.payment.planOnlyDisplayPrice,
+              })}
+        </p>
+      )}
       {!recurlyLoadError && (
       {!recurlyLoadError && (
-        <>
-          <br />
-          <p>
-            <i>
-              <SubscriptionRemainder subscription={subscription} />
-            </i>
-          </p>
-        </>
+        <PlanActions
+          subscription={subscription}
+          onStandalonePlan={onStandalonePlan}
+          handlePlanChange={handlePlanChange}
+          hasPendingPause={hasPendingPause}
+          cancelPauseReq={cancelPauseReq}
+        />
       )}
       )}
+      <hr />
+      <AddOns
+        subscription={subscription}
+        onStandalonePlan={onStandalonePlan}
+        handleCancelClick={handleCancelClick}
+      />
 
 
       <ChangePlanModal />
       <ChangePlanModal />
       <ConfirmChangePlanModal />
       <ConfirmChangePlanModal />
       <KeepCurrentPlanModal />
       <KeepCurrentPlanModal />
       <ChangeToGroupModal />
       <ChangeToGroupModal />
+      <CancelAiAddOnModal />
       <PauseSubscriptionModal />
       <PauseSubscriptionModal />
     </>
     </>
   )
   )
 }
 }
+
+type PlanActionsProps = {
+  subscription: PaidSubscription
+  onStandalonePlan: boolean
+  handlePlanChange: () => void
+  hasPendingPause: boolean
+  cancelPauseReq: ReturnType<typeof useAsync>
+}
+
+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 (
+    <div className="mt-3">
+      {isSubscriptionEligibleForFlexibleGroupLicensing ? (
+        <FlexibleGroupLicensingActions subscription={subscription} />
+      ) : (
+        <>
+          {!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
+            <OLButton variant="secondary" onClick={handlePlanChange}>
+              {t('change_plan')}
+            </OLButton>
+          )}
+        </>
+      )}
+      {hasPendingPause && (
+        <OLButton
+          variant="primary"
+          onClick={handleCancelPendingPauseClick}
+          disabled={isLoadingCancelPause}
+        >
+          {isLoadingCancelPause ? (
+            <LoadingSpinner />
+          ) : (
+            t('unpause_subscription')
+          )}
+        </OLButton>
+      )}
+      {!onStandalonePlan && (
+        <>
+          {' '}
+          <CancelSubscriptionButton />
+        </>
+      )}
+    </div>
+  )
+}
+
+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 && (
+        <>
+          <OLButton
+            variant="secondary"
+            href="/user/subscription/group/upgrade-subscription"
+            onClick={() =>
+              sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
+            }
+          >
+            {t('upgrade_plan')}
+          </OLButton>{' '}
+        </>
+      )}
+      {subscription.plan.membersLimitAddOn === 'additional-license' && (
+        <OLButton
+          variant="secondary"
+          href="/user/subscription/group/add-users"
+          onClick={() => sendMB('flex-add-users')}
+        >
+          {t('buy_more_licenses')}
+        </OLButton>
+      )}
+    </>
+  )
+}

+ 1 - 1
services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx

@@ -10,7 +10,7 @@ export function TrialEnding({
   className,
   className,
 }: TrialEndingProps) {
 }: TrialEndingProps) {
   return (
   return (
-    <p className={className}>
+    <p className={className} data-testid="trial-ending">
       <Trans
       <Trans
         i18nKey="youre_on_free_trial_which_ends_on"
         i18nKey="youre_on_free_trial_which_ends_on"
         values={{ date: trialEndsAtFormatted }}
         values={{ date: trialEndsAtFormatted }}

+ 1 - 1
services/web/frontend/js/features/subscription/components/shared/price-exceptions.tsx

@@ -17,7 +17,7 @@ export function PriceExceptions({ subscription }: PriceExceptionsProps) {
       {activeCoupons.length > 0 && (
       {activeCoupons.length > 0 && (
         <>
         <>
           <i>* {t('coupons_not_included')}:</i>
           <i>* {t('coupons_not_included')}:</i>
-          <ul>
+          <ul data-testid="active-coupons">
             {activeCoupons.map(coupon => (
             {activeCoupons.map(coupon => (
               <li key={coupon.code}>
               <li key={coupon.code}>
                 <i>{coupon.description || coupon.name}</i>
                 <i>{coupon.description || coupon.name}</i>

+ 150 - 0
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 (
+    <SplitTestProvider>
+      <SubscriptionDashboardProvider>
+        <ActiveSubscription {...args} />
+      </SubscriptionDashboardProvider>
+    </SplitTestProvider>
+  )
+}
+
+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,
+}

+ 145 - 94
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -1,11 +1,15 @@
 import { expect } from 'chai'
 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 * as eventTracking from '@/infrastructure/event-tracking'
 import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import {
 import {
   annualActiveSubscription,
   annualActiveSubscription,
+  annualActiveSubscriptionEuro,
+  annualActiveSubscriptionWithAddons,
+  annualActiveSubscriptionWithCoupons,
   groupActiveSubscription,
   groupActiveSubscription,
   groupActiveSubscriptionWithPendingLicenseChange,
   groupActiveSubscriptionWithPendingLicenseChange,
+  groupProfessionalActiveSubscription,
   monthlyActiveCollaborator,
   monthlyActiveCollaborator,
   pendingSubscriptionChange,
   pendingSubscriptionChange,
   trialCollaboratorSubscription,
   trialCollaboratorSubscription,
@@ -37,45 +41,78 @@ describe('<ActiveSubscription />', function () {
   })
   })
 
 
   function expectedInActiveSubscription(subscription: PaidSubscription) {
   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 })
-
-    screen.getByRole('button', { name: 'Change plan' })
+    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}`
+        )
+      )
+    )
 
 
-    // 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(
     screen.getByText(
       '* Prices may be subject to additional VAT, depending on your country.'
       '* 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 () {
   it('renders the dash annual active subscription', function () {
     renderActiveSubscription(annualActiveSubscription)
     renderActiveSubscription(annualActiveSubscription)
     expectedInActiveSubscription(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 () {
   it('shows change plan UI when button clicked', async function () {
     renderActiveSubscription(annualActiveSubscription)
     renderActiveSubscription(annualActiveSubscription)
+    expectedInActiveSubscription(annualActiveSubscription)
 
 
     const button = screen.getByRole('button', { name: 'Change plan' })
     const button = screen.getByRole('button', { name: 'Change plan' })
     fireEvent.click(button)
     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 screen.findByRole('heading', { name: 'Change plan' })
     await waitFor(
     await waitFor(
       () =>
       () =>
@@ -86,34 +123,9 @@ describe('<ActiveSubscription />', 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 () {
   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
     // 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
     activePastDueSubscription.payment.hasPastDueInvoice = true
 
 
     renderActiveSubscription(activePastDueSubscription)
     renderActiveSubscription(activePastDueSubscription)
@@ -122,23 +134,41 @@ describe('<ActiveSubscription />', function () {
     expect(changePlan).to.be.null
     expect(changePlan).to.be.null
   })
   })
 
 
-  it('shows the pending license change message when plan change is pending', function () {
-    renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
+  it('notes when user is changing plan at end of current plan term', function () {
+    renderActiveSubscription(pendingSubscriptionChange)
 
 
-    screen.getByText('Your subscription is changing to include', {
-      exact: false,
-    })
+    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(
     screen.getByText(
-      groupActiveSubscriptionWithPendingLicenseChange.payment
-        .pendingAdditionalLicenses!
+      'If you wish this change to apply before the end of your current billing period, please contact us.'
     )
     )
+  })
 
 
-    screen.getByText('additional license(s) for a total of', { exact: false })
+  it('shows the pending license change message when plan change is pending', function () {
+    renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
 
 
-    screen.getByText(
-      groupActiveSubscriptionWithPendingLicenseChange.payment
-        .pendingTotalLicenses!
+    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}`
+        )
+      )
+    )
+
+    within(screen.getByTestId('plan-licenses')).getByText((_, el) =>
+      Boolean(
+        el?.textContent?.includes(
+          `Supports up to ${groupActiveSubscriptionWithPendingLicenseChange.payment.totalLicenses}`
+        )
+      )
     )
     )
 
 
     expect(
     expect(
@@ -148,8 +178,8 @@ describe('<ActiveSubscription />', function () {
     ).to.be.null
     ).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.additionalLicenses = 4
     subscription.payment.totalLicenses =
     subscription.payment.totalLicenses =
       subscription.payment.totalLicenses +
       subscription.payment.totalLicenses +
@@ -157,42 +187,71 @@ describe('<ActiveSubscription />', function () {
 
 
     renderActiveSubscription(subscription)
     renderActiveSubscription(subscription)
 
 
-    screen.getByText('Your subscription includes', {
-      exact: false,
-    })
+    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(subscription.payment.additionalLicenses)
+    within(screen.getByTestId('trial-ending')).getByText((_, el) =>
+      Boolean(
+        el?.textContent?.includes(
+          `You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}`
+        )
+      )
+    )
+  })
 
 
-    screen.getByText('additional license(s) for a total of', { exact: false })
+  it('shows correct actions for group plan: professional ', function () {
+    renderActiveSubscription(groupProfessionalActiveSubscription)
+    screen.getByRole('link', { name: /buy more licenses/i })
+  })
 
 
-    screen.getByText(subscription.payment.totalLicenses)
+  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 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 })
+  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)
+  })
 
 
-    const endDate = screen.getAllByText(
-      trialSubscription.payment.trialEndsAtFormatted!
+  it('shows multiple active coupons', function () {
+    renderActiveSubscription(annualActiveSubscriptionWithCoupons)
+    within(screen.getByTestId('active-coupons')).getByText(
+      'Coupon1 for 10% off',
+      { exact: false }
+    )
+    within(screen.getByTestId('active-coupons')).getByText(
+      'Coupon2 for 15% off',
+      { exact: false }
     )
     )
-    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('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
     )
     )
-    screen.getByText(
-      subscriptionWithActiveCoupons.payment.activeCoupons[0].name
+    const billingLink = screen.getByRole('link', {
+      name: 'View billing details',
+    })
+    expect(billingLink.getAttribute('href')).to.equal(
+      annualActiveSubscription.payment.billingDetailsLink
     )
     )
   })
   })
 
 
@@ -523,13 +582,5 @@ describe('<ActiveSubscription />', function () {
       const changePlan = screen.queryByRole('button', { name: 'Change plan' })
       const changePlan = screen.queryByRole('button', { name: 'Change plan' })
       expect(changePlan).to.be.null
       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,
-      })
-    })
   })
   })
 })
 })

+ 248 - 11
services/web/test/frontend/features/subscription/fixtures/subscriptions.ts

@@ -31,6 +31,7 @@ export const annualActiveSubscription: PaidSubscription = {
     price_in_cents: 21900,
     price_in_cents: 21900,
     annual: true,
     annual: true,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -48,7 +49,7 @@ export const annualActiveSubscription: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$199.00',
     displayPrice: '$199.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$199.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     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 = {
 export const annualActiveSubscriptionEuro: PaidSubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
@@ -73,6 +254,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
     price_in_cents: 21900,
     price_in_cents: 21900,
     annual: true,
     annual: true,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0.24,
     taxRate: 0.24,
@@ -90,7 +272,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '€221.96',
     displayPrice: '€221.96',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '€221.96',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -114,6 +296,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
     name: 'Professional',
     name: 'Professional',
     price_in_cents: 4500,
     price_in_cents: 4500,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -131,7 +314,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$42.00',
     displayPrice: '$42.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$42.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -156,6 +339,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
     price_in_cents: 21900,
     price_in_cents: 21900,
     annual: true,
     annual: true,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -173,7 +357,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: true,
     hasPastDueInvoice: true,
     displayPrice: '$199.00',
     displayPrice: '$199.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$199.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -198,6 +382,7 @@ export const canceledSubscription: PaidSubscription = {
     price_in_cents: 21900,
     price_in_cents: 21900,
     annual: true,
     annual: true,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -215,7 +400,7 @@ export const canceledSubscription: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$199.00',
     displayPrice: '$199.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$199.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -240,6 +425,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
     price_in_cents: 21900,
     price_in_cents: 21900,
     annual: true,
     annual: true,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -257,7 +443,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$199.00',
     displayPrice: '$199.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$199.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -293,6 +479,54 @@ export const groupActiveSubscription: GroupSubscription = {
     groupPlan: true,
     groupPlan: true,
     membersLimit: 10,
     membersLimit: 10,
     membersLimitAddOn: 'additional-license',
     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,
+    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: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -310,7 +544,7 @@ export const groupActiveSubscription: GroupSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$1290.00',
     displayPrice: '$1290.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$1290.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -340,6 +574,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
       groupPlan: true,
       groupPlan: true,
       membersLimit: 10,
       membersLimit: 10,
       membersLimitAddOn: 'additional-license',
       membersLimitAddOn: 'additional-license',
+      canUseFlexibleLicensing: true,
     },
     },
     payment: {
     payment: {
       taxRate: 0,
       taxRate: 0,
@@ -359,7 +594,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
       displayPrice: '$2967.00',
       displayPrice: '$2967.00',
       pendingAdditionalLicenses: 13,
       pendingAdditionalLicenses: 13,
       pendingTotalLicenses: 23,
       pendingTotalLicenses: 23,
-      planOnlyDisplayPrice: '',
+      planOnlyDisplayPrice: '$2967.00',
       addOns: [],
       addOns: [],
       addOnDisplayPricesWithoutAdditionalLicense: {},
       addOnDisplayPricesWithoutAdditionalLicense: {},
       isEligibleForGroupPlan: true,
       isEligibleForGroupPlan: true,
@@ -411,7 +646,7 @@ export const trialSubscription: PaidSubscription = {
     accountEmail: 'fake@example.com',
     accountEmail: 'fake@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$14.00',
     displayPrice: '$14.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$14.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -456,6 +691,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
     price_in_cents: 2300,
     price_in_cents: 2300,
     featureDescription: [],
     featureDescription: [],
     hideFromUsers: true,
     hideFromUsers: true,
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -473,7 +709,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
     accountEmail: 'foo@example.com',
     accountEmail: 'foo@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$21.00',
     displayPrice: '$21.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$21.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,
@@ -497,6 +733,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
     name: 'Standard (Collaborator)',
     name: 'Standard (Collaborator)',
     price_in_cents: 212300900,
     price_in_cents: 212300900,
     featureDescription: [],
     featureDescription: [],
+    canUseFlexibleLicensing: false,
   },
   },
   payment: {
   payment: {
     taxRate: 0,
     taxRate: 0,
@@ -514,7 +751,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
     accountEmail: 'foo@example.com',
     accountEmail: 'foo@example.com',
     hasPastDueInvoice: false,
     hasPastDueInvoice: false,
     displayPrice: '$21.00',
     displayPrice: '$21.00',
-    planOnlyDisplayPrice: '',
+    planOnlyDisplayPrice: '$21.00',
     addOns: [],
     addOns: [],
     addOnDisplayPricesWithoutAdditionalLicense: {},
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForGroupPlan: true,

+ 9 - 1
services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx

@@ -8,7 +8,8 @@ import { CurrencyCode } from '../../../../../types/subscription/currency'
 export function renderActiveSubscription(
 export function renderActiveSubscription(
   subscription: PaidSubscription,
   subscription: PaidSubscription,
   tags: MetaTag[] = [],
   tags: MetaTag[] = [],
-  currencyCode?: CurrencyCode
+  currencyCode?: CurrencyCode,
+  canUseFlexibleLicensing?: boolean
 ) {
 ) {
   renderWithSubscriptionDashContext(
   renderWithSubscriptionDashContext(
     <ActiveSubscription subscription={subscription} />,
     <ActiveSubscription subscription={subscription} />,
@@ -26,6 +27,13 @@ export function renderActiveSubscription(
           name: 'ol-recommendedCurrency',
           name: 'ol-recommendedCurrency',
           value: currencyCode || 'USD',
           value: currencyCode || 'USD',
         },
         },
+        {
+          name: 'ol-canUseFlexibleLicensing',
+          value:
+            canUseFlexibleLicensing ||
+            subscription.plan?.canUseFlexibleLicensing ||
+            false,
+        },
       ],
       ],
     }
     }
   )
   )

+ 2 - 57
services/web/test/frontend/features/subscription/helpers/render-with-subscription-dash-context.tsx

@@ -1,10 +1,10 @@
 import { render } from '@testing-library/react'
 import { render } from '@testing-library/react'
 import _ from 'lodash'
 import _ from 'lodash'
 import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
 import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
-import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
 import fetchMock from 'fetch-mock'
 import fetchMock from 'fetch-mock'
 import { SplitTestProvider } from '@/shared/context/split-test-context'
 import { SplitTestProvider } from '@/shared/context/split-test-context'
 import { MetaTag } from '@/utils/meta'
 import { MetaTag } from '@/utils/meta'
+import { setupSubscriptionDashContext } from './setup-subscription-dash-context'
 
 
 export function renderWithSubscriptionDashContext(
 export function renderWithSubscriptionDashContext(
   component: React.ReactElement,
   component: React.ReactElement,
@@ -25,62 +25,7 @@ export function renderWithSubscriptionDashContext(
     </SplitTestProvider>
     </SplitTestProvider>
   )
   )
 
 
-  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, {
   return render(component, {
     wrapper: SubscriptionDashboardProviderWrapper,
     wrapper: SubscriptionDashboardProviderWrapper,

+ 66 - 0
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)
+                          }
+                        },
+                      }
+                    },
+                  }
+                },
+              }
+            },
+          }
+        },
+      },
+    }
+  }
+}