diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 545b756df1..c7d5babc3b 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -425,6 +425,7 @@ "log_viewer_error": "", "login_with_service": "", "logs_and_output_files": "", + "looking_multiple_licenses": "", "looks_like_youre_at": "", "main_document": "", "main_file_not_found": "", 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 2bd80e849c..a625b2efda 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,4 +1,3 @@ -import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Subscription } from '../../../../../../types/subscription/dashboard/subscription' import { ActiveSubscription } from './states/active/active' @@ -49,15 +48,9 @@ function PersonalSubscriptionStates({ function PersonalSubscription() { const { t } = useTranslation() - const { personalSubscription, recurlyLoadError, setRecurlyLoadError } = + const { personalSubscription, recurlyLoadError } = useSubscriptionDashboardContext() - useEffect(() => { - if (typeof window.recurly === 'undefined' || !window.recurly) { - setRecurlyLoadError(true) - } - }) - if (!personalSubscription) return null return ( 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 04e0903b8c..3cc3c3d0cc 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 @@ -7,7 +7,7 @@ import { CancelSubscriptionButton } from './cancel-subscription-button' import { CancelSubscription } from './cancel-subscription' import { PendingPlanChange } from './pending-plan-change' import { TrialEnding } from './trial-ending' -import { ChangePlan } from './change-plan' +import { ChangePlan } from './change-plan/change-plan' import { PendingAdditionalLicenses } from './pending-additional-licenses' import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan' @@ -36,14 +36,20 @@ export function ActiveSubscription({ ]} /> {subscription.pendingPlan && ( - + <> + {' '} + + )} {!subscription.pendingPlan && subscription.recurly.additionalLicenses > 0 && ( - + <> + {' '} + + )} {!recurlyLoadError && !subscription.groupPlan && diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx new file mode 100644 index 0000000000..2dbda78ae5 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next' +import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context' +import { ChangeToGroupPlan } from './change-to-group-plan' +import { IndividualPlansTable } from './individual-plans-table' + +export function ChangePlan() { + const { t } = useTranslation() + const { plans, recurlyLoadError, showChangePersonalPlan } = + useSubscriptionDashboardContext() + + if (!showChangePersonalPlan || !plans || recurlyLoadError) return null + + return ( + <> +

{t('change_plan')}

+ + + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx new file mode 100644 index 0000000000..098542d022 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx @@ -0,0 +1,11 @@ +import { useTranslation } from 'react-i18next' + +export function ChangeToGroupPlan() { + const { t } = useTranslation() + return ( + <> +

{t('looking_multiple_licenses')}

+ {/* todo: if/else isValidCurrencyForUpgrade and modal */} + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx similarity index 73% rename from services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx rename to services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx index d39f37f69b..68ff59beeb 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/individual-plans-table.tsx @@ -1,6 +1,7 @@ import { useTranslation } from 'react-i18next' -import { Plan } from '../../../../../../../../types/subscription/plan' -import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' +import { Plan } from '../../../../../../../../../types/subscription/plan' +import Icon from '../../../../../../../shared/components/icon' +import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context' function ChangeToPlanButton({ plan }: { plan: Plan }) { const { t } = useTranslation() @@ -47,15 +48,17 @@ function ChangePlanButton({ plan }: { plan: Plan }) { return } else if (isCurrentPlanForUser && !personalSubscription.pendingPlan) { return ( - + + {t('your_plan')} + ) } else if ( personalSubscription?.pendingPlan?.planCode?.split('_')[0] === plan.planCode ) { return ( - + + {t('your_new_plan')} + ) } else { return @@ -91,27 +94,21 @@ function PlansRows({ plans }: { plans: Array }) { ) } -export function ChangePlan() { +export function IndividualPlansTable({ plans }: { plans: Array }) { const { t } = useTranslation() - const { plans, showChangePersonalPlan } = useSubscriptionDashboardContext() - - if (!showChangePersonalPlan || !plans) return null return ( - <> -

{t('change_plan')}

- - - - - - - - - - -
{t('name')}{t('price')} -
- + + + + + + + + + + +
{t('name')}{t('price')} +
) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-additional-licenses.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-additional-licenses.tsx index 83f5f652f2..ee58117152 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-additional-licenses.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-additional-licenses.tsx @@ -8,21 +8,18 @@ export function PendingAdditionalLicenses({ totalLicenses: number }) { return ( - <> - {' '} - , - // eslint-disable-next-line react/jsx-key - , - ]} - /> - + , + // eslint-disable-next-line react/jsx-key + , + ]} + /> ) } diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx index 8ba1a52513..297950c665 100644 --- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx +++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx @@ -1,4 +1,11 @@ -import { createContext, ReactNode, useContext, useMemo, useState } from 'react' +import { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, + useState, +} from 'react' import { ManagedGroupSubscription, Subscription, @@ -44,6 +51,12 @@ export function SubscriptionDashboardProvider({ personalSubscription || managedGroupSubscriptions + useEffect(() => { + if (typeof window.recurly === 'undefined' || !window.recurly) { + setRecurlyLoadError(true) + } + }, [setRecurlyLoadError]) + const value = useMemo( () => ({ hasDisplayedSubscription, diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx similarity index 92% rename from services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx rename to services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index ad37cf164f..fcb67b99d4 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -1,18 +1,18 @@ import { expect } from 'chai' import { fireEvent, render, screen } from '@testing-library/react' -import * as eventTracking from '../../../../../../../frontend/js/infrastructure/event-tracking' -import { ActiveSubscription } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active' -import { SubscriptionDashboardProvider } from '../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' -import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription' +import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking' +import { ActiveSubscription } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active' +import { SubscriptionDashboardProvider } from '../../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' +import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription' import { annualActiveSubscription, groupActiveSubscription, groupActiveSubscriptionWithPendingLicenseChange, pendingSubscriptionChange, trialSubscription, -} from '../../../fixtures/subscriptions' +} from '../../../../fixtures/subscriptions' import sinon from 'sinon' -import { plans } from '../../../fixtures/plans' +import { plans } from '../../../../fixtures/plans' describe('', function () { let sendMBSpy: sinon.SinonSpy @@ -20,11 +20,15 @@ describe('', function () { beforeEach(function () { window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-plans', plans) + // @ts-ignore + window.recurly = {} sendMBSpy = sinon.spy(eventTracking, 'sendMB') }) afterEach(function () { window.metaAttributesCache = new Map() + // @ts-ignore + delete window.recurly sendMBSpy.restore() }) @@ -117,7 +121,7 @@ describe('', function () { // account is likely in expired state, but be sure to not show option if state is still active const activePastDueSubscription = Object.assign( {}, - annualActiveSubscription + JSON.parse(JSON.stringify(annualActiveSubscription)) ) activePastDueSubscription.recurly.account.has_past_due_invoice._ = 'true' diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx similarity index 59% rename from services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx rename to services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx index 72eb1ee376..f3acca284d 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx @@ -1,22 +1,26 @@ import { expect } from 'chai' import { fireEvent, render, screen } from '@testing-library/react' -import { SubscriptionDashboardProvider } from '../../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' -import { ChangePlan } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan' -import { plans } from '../../../../fixtures/plans' +import { SubscriptionDashboardProvider } from '../../../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' +import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan' +import { plans } from '../../../../../fixtures/plans' import { annualActiveSubscription, pendingSubscriptionChange, -} from '../../../../fixtures/subscriptions' -import { ActiveSubscription } from '../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active' +} from '../../../../../fixtures/subscriptions' +import { ActiveSubscription } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active' describe('', function () { beforeEach(function () { window.metaAttributesCache = new Map() window.metaAttributesCache.set('ol-plans', plans) + // @ts-ignore + window.recurly = {} }) afterEach(function () { window.metaAttributesCache = new Map() + // @ts-ignore + delete window.recurly }) it('does not render the UI when showChangePersonalPlan is false', function () { @@ -30,7 +34,7 @@ describe('', function () { expect(container.firstChild).to.be.null }) - it('renders the table of plans', function () { + it('renders the individual plans table', function () { window.metaAttributesCache.set('ol-subscription', annualActiveSubscription) render( @@ -45,7 +49,7 @@ describe('', function () { name: 'Change to this plan', }) expect(changeToPlanButtons.length).to.equal(plans.length - 1) - screen.getByRole('button', { name: 'Your plan' }) + screen.getByText('Your plan') const annualPlans = plans.filter(plan => plan.annual) expect(screen.getAllByText('/ year').length).to.equal(annualPlans.length) @@ -54,6 +58,20 @@ describe('', function () { ) }) + it('renders the change to group plan UI', function () { + window.metaAttributesCache.set('ol-subscription', annualActiveSubscription) + render( + + + + ) + + const button = screen.getByRole('button', { name: 'Change plan' }) + fireEvent.click(button) + + screen.getByText('Looking for multiple licenses?') + }) + it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', function () { window.metaAttributesCache.set('ol-subscription', pendingSubscriptionChange) render( @@ -68,4 +86,15 @@ describe('', function () { screen.getByText('Your new plan') screen.getByRole('button', { name: 'Keep my current plan' }) }) + + it('does not render when Recurly did not load', function () { + // @ts-ignore + delete window.recurly + const { container } = render( + + + + ) + expect(container).not.to.be.null + }) }) diff --git a/services/web/types/window.ts b/services/web/types/window.ts index 9f02651ea4..7706f37c0c 100644 --- a/services/web/types/window.ts +++ b/services/web/types/window.ts @@ -2,6 +2,7 @@ import { ExposedSettings } from './exposed-settings' import { OAuthProviders } from './oauth-providers' import { OverallThemeMeta } from './project-settings' import { User } from './user' +import 'recurly__recurly-js' declare global { // eslint-disable-next-line no-unused-vars