diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 6c37446658..69f0a633a5 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -311,9 +311,10 @@ async function _userSubscriptionReactPage(req, res) { const hasSubscription = await LimitationsManager.promises.userHasV1OrV2Subscription(user) const fromPlansPage = req.query.hasSubscription - const plans = SubscriptionViewModelBuilder.buildPlansList( - personalSubscription ? personalSubscription.plan : undefined - ) + const plansData = + SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash( + personalSubscription?.plan + ) AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view') @@ -327,7 +328,8 @@ async function _userSubscriptionReactPage(req, res) { const data = { title: 'your_subscription', - plans, + plans: plansData?.plans, + planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd, groupPlans: GroupPlansData, user, hasSubscription, diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js index a455bd630f..00aad3516c 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -531,9 +531,36 @@ function buildGroupSubscriptionForView(groupSubscription) { } } +function buildPlansListForSubscriptionDash(currentPlan) { + const allPlansData = buildPlansList(currentPlan) + const plans = [] + // only list individual and visible plans for "change plans" UI + if (allPlansData.studentAccounts) { + plans.push( + ...allPlansData.studentAccounts.filter(plan => !plan.hideFromUsers) + ) + } + if (allPlansData.individualMonthlyPlans) { + plans.push( + ...allPlansData.individualMonthlyPlans.filter(plan => !plan.hideFromUsers) + ) + } + if (allPlansData.individualAnnualPlans) { + plans.push( + ...allPlansData.individualAnnualPlans.filter(plan => !plan.hideFromUsers) + ) + } + + return { + plans, + planCodesChangingAtTermEnd: allPlansData.planCodesChangingAtTermEnd, + } +} + module.exports = { buildUsersSubscriptionViewModel, buildPlansList, + buildPlansListForSubscriptionDash, getBestSubscription: callbackify(getBestSubscription), promises: { buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel), diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 1249864b8c..8403bd773e 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/services/web/app/views/subscriptions/dashboard-react.pug @@ -9,8 +9,9 @@ block head-scripts block append meta meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions) meta(name="ol-managedGroupSubscriptions", data-type="json" content=managedGroupSubscriptions) - meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd) + meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=planCodesChangingAtTermEnd) meta(name="ol-currentInstitutionsWithLicence", data-type="json" content=currentInstitutionsWithLicence) + meta(name="ol-plans", data-type="json" content=plans) if (personalSubscription && personalSubscription.recurly) meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) meta(name="ol-subscription" data-type="json" content=personalSubscription) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 336e0f8797..545b756df1 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -92,6 +92,7 @@ "change_plan": "", "change_primary_email_address_instructions": "", "change_project_owner": "", + "change_to_this_plan": "", "chat": "", "chat_error": "", "checking_dropbox_status": "", @@ -128,6 +129,7 @@ "connected_users": "", "contact_message_label": "", "contact_sales": "", + "contact_support_to_change_group_subscription": "", "contact_us": "", "continue_github_merge": "", "copy": "", @@ -374,6 +376,7 @@ "is_email_affiliated": "", "join_project": "", "joining": "", + "keep_current_plan": "", "keybindings": "", "labs_program_already_participating": "", "labs_program_benefits": "<0>", @@ -450,6 +453,7 @@ "mendeley_reference_loading_error_forbidden": "", "mendeley_sync_description": "", "menu": "", + "month": "", "more": "", "n_items": "", "n_items_plural": "", @@ -541,6 +545,7 @@ "premium_feature": "", "premium_plan_label": "", "press_shortcut_to_open_advanced_reference_search": "", + "price": "", "priority_support": "", "privacy_policy": "", "private": "", @@ -825,6 +830,7 @@ "word_count": "", "work_offline": "", "work_with_non_overleaf_users": "", + "year": "", "you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "", @@ -834,6 +840,8 @@ "your_affiliation_is_confirmed": "", "your_browser_does_not_support_this_feature": "", "your_message_to_collaborators": "", + "your_new_plan": "", + "your_plan": "", "your_plan_is_changing_at_term_end": "", "your_projects": "", "your_subscription": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx index 601a31b5ec..3aaa397697 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx @@ -1,15 +1,14 @@ import { Trans } from 'react-i18next' import { Institution } from '../../../../../../types/institution' +import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' import PremiumFeaturesLink from './premium-features-link' -type InstitutionMembershipsProps = { - memberships?: Array -} +function InstitutionMemberships() { + const { institutionMemberships } = useSubscriptionDashboardContext() -function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) { // memberships is undefined when data failed to load. If user has no memberships, then an empty array is returned - if (!memberships) { + if (!institutionMemberships) { return (

@@ -24,7 +23,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) { return ( <>

- {memberships.map((institution: Institution) => ( + {institutionMemberships.map((institution: Institution) => (
))} - {memberships.length > 0 && } + {institutionMemberships.length > 0 && }
) diff --git a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx index cecccb53de..28aa7ea0f9 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/managed-group-subscriptions.tsx @@ -1,29 +1,17 @@ import { Trans, useTranslation } from 'react-i18next' -import { GroupSubscription } from '../../../../../../types/subscription/dashboard/subscription' -import { User } from '../../../../../../types/user' +import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' -export type ManagedGroupSubscription = Omit & { - userIsGroupMember: boolean - planLevelName: string - admin_id: User -} - -type ManagedGroupSubscriptionsProps = { - subscriptions?: ManagedGroupSubscription[] -} - -export default function ManagedGroupSubscriptions({ - subscriptions, -}: ManagedGroupSubscriptionsProps) { +export default function ManagedGroupSubscriptions() { const { t } = useTranslation() + const { managedGroupSubscriptions } = useSubscriptionDashboardContext() - if (!subscriptions) { + if (!managedGroupSubscriptions) { return null } return ( <> - {subscriptions.map(subscription => ( + {managedGroupSubscriptions.map(subscription => (

{subscription.userIsGroupMember ? ( 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 9add8831e9..2bd80e849c 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 @@ -47,13 +47,9 @@ function PersonalSubscriptionStates({ } } -function PersonalSubscription({ - subscription, -}: { - subscription?: Subscription -}) { +function PersonalSubscription() { const { t } = useTranslation() - const { recurlyLoadError, setRecurlyLoadError } = + const { personalSubscription, recurlyLoadError, setRecurlyLoadError } = useSubscriptionDashboardContext() useEffect(() => { @@ -62,14 +58,15 @@ function PersonalSubscription({ } }) - if (!subscription) return null + if (!personalSubscription) return null return ( <> - {subscription.recurly.account.has_past_due_invoice._ === 'true' && ( - + {personalSubscription.recurly.account.has_past_due_invoice._ === + 'true' && ( + )} - + {recurlyLoadError && (

{t('payment_provider_unreachable_error')} 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 e8c2792805..04e0903b8c 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 @@ -8,6 +8,8 @@ import { CancelSubscription } from './cancel-subscription' import { PendingPlanChange } from './pending-plan-change' import { TrialEnding } from './trial-ending' import { ChangePlan } from './change-plan' +import { PendingAdditionalLicenses } from './pending-additional-licenses' +import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan' export function ActiveSubscription({ subscription, @@ -38,40 +40,32 @@ export function ActiveSubscription({ )} {!subscription.pendingPlan && subscription.recurly.additionalLicenses > 0 && ( - <> - {' '} - , - // eslint-disable-next-line react/jsx-key - , - ]} - /> - - )}{' '} + + )} {!recurlyLoadError && !subscription.groupPlan && subscription.recurly.account.has_past_due_invoice._ !== 'true' && ( - + <> + {' '} + + )}

- {/* && personalSubscription.pendingPlan.name != personalSubscription.plan.name */} {subscription.pendingPlan && subscription.pendingPlan.name !== subscription.plan.name && (

{t('want_change_to_apply_before_plan_end')}

)} - {/* TODO: groupPlan */} + {(!subscription.pendingPlan || + subscription.pendingPlan.name === subscription.plan.name) && + subscription.plan.groupPlan && } {subscription.recurly.trial_ends_at && subscription.recurly.trialEndsAtFormatted && ( + {/* todo: ng-model="plan_code" */} + + {/* todo: handle submit changePlan */} + + + ) +} + +function KeepCurrentPlanButton({ plan }: { plan: Plan }) { + const { t } = useTranslation() + // for when the user selected to change a plan, but the plan change is still pending + return ( +
+ {/* todo: ng-model="plan_code" */} + + {/* todo: handle submit cancelPendingPlanChange */} + +
+ ) +} + +function ChangePlanButton({ plan }: { plan: Plan }) { + const { t } = useTranslation() + const { personalSubscription } = useSubscriptionDashboardContext() + const isCurrentPlanForUser = + personalSubscription?.planCode && + plan.planCode === personalSubscription.planCode.split('_')[0] + + if (isCurrentPlanForUser && personalSubscription.pendingPlan) { + return + } else if (isCurrentPlanForUser && !personalSubscription.pendingPlan) { + return ( + + ) + } else if ( + personalSubscription?.pendingPlan?.planCode?.split('_')[0] === plan.planCode + ) { + return ( + + ) + } else { + return + } +} + +function PlansRow({ plan }: { plan: Plan }) { + const { t } = useTranslation() + + return ( + + + {plan.name} + + + {/* todo: {{ displayPrice }} */}/ {plan.annual ? t('year') : t('month')} + + + + + + ) +} + +function PlansRows({ plans }: { plans: Array }) { + return ( + <> + {plans && + plans.map(plan => ( + + ))} + + ) +} + export function ChangePlan() { const { t } = useTranslation() - const { showChangePersonalPlan } = useSubscriptionDashboardContext() + const { plans, showChangePersonalPlan } = useSubscriptionDashboardContext() - if (!showChangePersonalPlan) return null + if (!showChangePersonalPlan || !plans) return null return ( <>

{t('change_plan')}

-

- TODO: change subscription placeholder -

+ + + + + + + + + + +
{t('name')}{t('price')} +
) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/contact-support-to-change-group-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/contact-support-to-change-group-plan.tsx new file mode 100644 index 0000000000..ebb3cc1757 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/contact-support-to-change-group-plan.tsx @@ -0,0 +1,15 @@ +import { Trans } from 'react-i18next' + +export function ContactSupportToChangeGroupPlan() { + return ( +

+ , + ]} + /> +

+ ) +} 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 new file mode 100644 index 0000000000..83f5f652f2 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-additional-licenses.tsx @@ -0,0 +1,28 @@ +import { Trans } from 'react-i18next' + +export function PendingAdditionalLicenses({ + additionalLicenses, + totalLicenses, +}: { + additionalLicenses: number + totalLicenses: number +}) { + return ( + <> + {' '} + , + // eslint-disable-next-line react/jsx-key + , + ]} + /> + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx index 9a1d487407..039612bb87 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx @@ -1,20 +1,13 @@ import { useTranslation } from 'react-i18next' -import getMeta from '../../../../utils/meta' import InstitutionMemberships from './institution-memberships' import FreePlan from './free-plan' import PersonalSubscription from './personal-subscription' import ManagedGroupSubscriptions from './managed-group-subscriptions' +import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context' function SubscriptionDashboard() { const { t } = useTranslation() - const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence') - const subscription = getMeta('ol-subscription') - const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions') - - const hasDisplayedSubscription = - institutionMemberships?.length > 0 || - subscription || - managedGroupSubscriptions?.length > 0 + const { hasDisplayedSubscription } = useSubscriptionDashboardContext() return (
@@ -25,11 +18,9 @@ function SubscriptionDashboard() {

{t('your_subscription')}

- - - + + + {!hasDisplayedSubscription && }
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 04fdcc7629..8ba1a52513 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,6 +1,18 @@ import { createContext, ReactNode, useContext, useMemo, useState } from 'react' +import { + ManagedGroupSubscription, + Subscription, +} from '../../../../../types/subscription/dashboard/subscription' +import { Plan } from '../../../../../types/subscription/plan' +import { Institution } from '../../../../../types/institution' +import getMeta from '../../../utils/meta' type SubscriptionDashboardContextValue = { + hasDisplayedSubscription: boolean + institutionMemberships?: Array + managedGroupSubscriptions: Array + personalSubscription?: Subscription + plans: Array recurlyLoadError: boolean setRecurlyLoadError: React.Dispatch> showCancellation: boolean @@ -22,8 +34,23 @@ export function SubscriptionDashboardProvider({ const [showCancellation, setShowCancellation] = useState(false) const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false) + const plans = getMeta('ol-plans') + const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence') + const personalSubscription = getMeta('ol-subscription') + const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions') + + const hasDisplayedSubscription = + institutionMemberships?.length > 0 || + personalSubscription || + managedGroupSubscriptions + const value = useMemo( () => ({ + hasDisplayedSubscription, + institutionMemberships, + managedGroupSubscriptions, + personalSubscription, + plans, recurlyLoadError, setRecurlyLoadError, showCancellation, @@ -32,6 +59,11 @@ export function SubscriptionDashboardProvider({ setShowChangePersonalPlan, }), [ + hasDisplayedSubscription, + institutionMemberships, + managedGroupSubscriptions, + personalSubscription, + plans, recurlyLoadError, setRecurlyLoadError, showCancellation, diff --git a/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx index 8b76fac67b..8ab71902bb 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx @@ -1,6 +1,7 @@ import { expect } from 'chai' import { render, screen } from '@testing-library/react' import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships' +import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' const memberships = [ { @@ -35,7 +36,16 @@ describe('', function () { }) it('renders all insitutions with license', function () { - render() + window.metaAttributesCache.set( + 'ol-currentInstitutionsWithLicence', + memberships + ) + + render( + + + + ) const elements = screen.getAllByText('You are on our', { exact: false, @@ -50,14 +60,27 @@ describe('', function () { }) it('renders error message when failed to check commons licenses', function () { - render() + render( + + + + ) screen.getByText( 'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.' ) }) it('renders the "Get the most out of your" subscription text when a user has a subscription', function () { - render() + window.metaAttributesCache.set( + 'ol-currentInstitutionsWithLicence', + memberships + ) + + render( + + + + ) screen.getByText('Get the most out of your', { exact: false, }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx index 6a0d72c4be..0c4686fb29 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx @@ -1,12 +1,12 @@ import { expect } from 'chai' import { render, screen } from '@testing-library/react' -import ManagedGroupSubscriptions, { - ManagedGroupSubscription, -} from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions' import { groupActiveSubscription, groupActiveSubscriptionWithPendingLicenseChange, } from '../../fixtures/subscriptions' +import ManagedGroupSubscriptions from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions' +import { ManagedGroupSubscription } from '../../../../../../types/subscription/dashboard/subscription' +import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context' const managedGroupSubscriptions: ManagedGroupSubscription[] = [ { @@ -39,8 +39,14 @@ describe('', function () { }) it('renders all managed group subscriptions', function () { + window.metaAttributesCache.set( + 'ol-managedGroupSubscriptions', + managedGroupSubscriptions + ) render( - + + + ) const elements = screen.getAllByText('You are a', { @@ -85,7 +91,11 @@ describe('', function () { }) it('renders nothing when there are no group memberships', function () { - render() + render( + + + + ) const elements = screen.queryAllByText('You are a', { exact: false, }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx index 4eef227425..1487e4043a 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx @@ -25,7 +25,7 @@ describe('', function () { it('returns empty container', function () { const { container } = render( - + ) expect(container.firstChild).to.be.null @@ -34,9 +34,13 @@ describe('', function () { describe('subscription states ', function () { it('renders the active dash', function () { + window.metaAttributesCache.set( + 'ol-subscription', + annualActiveSubscription + ) render( - + ) @@ -44,9 +48,10 @@ describe('', function () { }) it('renders the canceled dash', function () { + window.metaAttributesCache.set('ol-subscription', canceledSubscription) render( - + ) screen.getByText( @@ -69,9 +74,13 @@ describe('', function () { }) it('renders the expired dash', function () { + window.metaAttributesCache.set( + 'ol-subscription', + pastDueExpiredSubscription + ) render( - + ) screen.getByText('Your subscription has expired.') @@ -83,9 +92,10 @@ describe('', function () { JSON.parse(JSON.stringify(annualActiveSubscription)) ) withStateDeleted.recurly.state = undefined + window.metaAttributesCache.set('ol-subscription', withStateDeleted) render( - + ) screen.getByText( @@ -96,9 +106,13 @@ describe('', function () { describe('past due subscription', function () { it('renders error alert', function () { + window.metaAttributesCache.set( + 'ol-subscription', + pastDueExpiredSubscription + ) render( - + ) screen.getByRole('alert') @@ -120,9 +134,13 @@ describe('', function () { it('shows an alert and hides "Change plan" option when Recurly did not load', function () { // @ts-ignore delete window.recurly + window.metaAttributesCache.set( + 'ol-subscription', + annualActiveSubscription + ) render( - + ) @@ -133,9 +151,13 @@ describe('', function () { }) it('should not show an alert and should show "Change plan" option when Recurly did load', function () { + window.metaAttributesCache.set( + 'ol-subscription', + annualActiveSubscription + ) render( - + ) 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.test.tsx index b0a10fea74..ad37cf164f 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.test.tsx @@ -12,12 +12,14 @@ import { trialSubscription, } from '../../../fixtures/subscriptions' import sinon from 'sinon' +import { plans } from '../../../fixtures/plans' describe('', function () { let sendMBSpy: sinon.SinonSpy beforeEach(function () { window.metaAttributesCache = new Map() + window.metaAttributesCache.set('ol-plans', plans) sendMBSpy = sinon.spy(eventTracking, 'sendMB') }) @@ -77,11 +79,13 @@ describe('', function () { const button = screen.getByRole('button', { name: 'Change plan' }) fireEvent.click(button) - // confirm main dash UI UI still shown - expectedInActiveSubscription(annualActiveSubscription) + // confirm main dash UI still shown + screen.getByText('You are currently subscribed to the', { exact: false }) - // TODO: add change plan UI - screen.getByText('change subscription placeholder', { exact: false }) + screen.getByRole('heading', { name: 'Change plan' }) + expect( + screen.getAllByRole('button', { name: 'Change to this plan' }).length > 0 + ).to.be.true }) it('notes when user is changing plan at end of current plan term', function () { @@ -103,17 +107,10 @@ describe('', function () { screen.getByText( 'If you wish this change to apply before the end of your current billing period, please contact us.' ) - }) - it('does not show "Change plan" option for group plans', function () { - render( - - - - ) - - const changePlan = screen.queryByRole('button', { name: 'Change plan' }) - expect(changePlan).to.be.null + 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 () { @@ -234,4 +231,29 @@ describe('', function () { screen.getByText('We’d love you to stay') }) + + describe('group plans', function () { + it('does not show "Change plan" option for group plans', function () { + render( + + + + ) + + const changePlan = screen.queryByRole('button', { name: 'Change plan' }) + expect(changePlan).to.be.null + }) + + it('shows contact support message for group plan change requests', function () { + render( + + + + ) + screen.getByRole('link', { name: 'contact support' }) + screen.getByText('if you wish to change your group subscription.', { + exact: false, + }) + }) + }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx new file mode 100644 index 0000000000..72eb1ee376 --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan.test.tsx @@ -0,0 +1,71 @@ +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 { + annualActiveSubscription, + pendingSubscriptionChange, +} 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) + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('does not render the UI when showChangePersonalPlan is false', function () { + window.metaAttributesCache.delete('ol-plans') + const { container } = render( + + + + ) + + expect(container.firstChild).to.be.null + }) + + it('renders the table of plans', function () { + window.metaAttributesCache.set('ol-subscription', annualActiveSubscription) + render( + + + + ) + + const button = screen.getByRole('button', { name: 'Change plan' }) + fireEvent.click(button) + + const changeToPlanButtons = screen.queryAllByRole('button', { + name: 'Change to this plan', + }) + expect(changeToPlanButtons.length).to.equal(plans.length - 1) + screen.getByRole('button', { name: 'Your plan' }) + + const annualPlans = plans.filter(plan => plan.annual) + expect(screen.getAllByText('/ year').length).to.equal(annualPlans.length) + expect(screen.getAllByText('/ month').length).to.equal( + plans.length - annualPlans.length + ) + }) + + it('renders "Your new plan" and "Keep current plan" when there is a pending plan change', function () { + window.metaAttributesCache.set('ol-subscription', pendingSubscriptionChange) + render( + + + + ) + + const button = screen.getByRole('button', { name: 'Change plan' }) + fireEvent.click(button) + + screen.getByText('Your new plan') + screen.getByRole('button', { name: 'Keep my current plan' }) + }) +}) diff --git a/services/web/test/frontend/features/subscription/fixtures/plans.tsx b/services/web/test/frontend/features/subscription/fixtures/plans.tsx new file mode 100644 index 0000000000..6a455a5a20 --- /dev/null +++ b/services/web/test/frontend/features/subscription/fixtures/plans.tsx @@ -0,0 +1,215 @@ +import { Plan } from '../../../../../types/subscription/plan' + +const features = { + student: { + collaborators: 6, + dropbox: true, + versioning: true, + github: true, + templates: true, + references: true, + referencesSearch: true, + gitBridge: true, + zotero: true, + mendeley: true, + compileTimeout: 240, + compileGroup: 'priority', + trackChanges: true, + symbolPalette: true, + }, + personal: { + collaborators: 1, + dropbox: true, + versioning: true, + github: true, + gitBridge: true, + templates: true, + references: true, + referencesSearch: true, + zotero: true, + mendeley: true, + compileTimeout: 240, + compileGroup: 'priority', + trackChanges: false, + symbolPalette: true, + }, + collaborator: { + collaborators: 10, + dropbox: true, + versioning: true, + github: true, + templates: true, + references: true, + referencesSearch: true, + zotero: true, + gitBridge: true, + mendeley: true, + compileTimeout: 240, + compileGroup: 'priority', + trackChanges: true, + symbolPalette: true, + }, + professional: { + collaborators: -1, + dropbox: true, + versioning: true, + github: true, + templates: true, + references: true, + referencesSearch: true, + zotero: true, + gitBridge: true, + mendeley: true, + compileTimeout: 240, + compileGroup: 'priority', + trackChanges: true, + symbolPalette: true, + }, +} + +const studentAccounts: Array = [ + { + planCode: 'student', + name: 'Student', + price_in_cents: 1000, + features: features.student, + featureDescription: [], + }, + { + planCode: 'student-annual', + name: 'Student Annual', + price_in_cents: 9900, + annual: true, + features: features.student, + featureDescription: [], + }, + { + planCode: 'student_free_trial', + name: 'Student', + price_in_cents: 800, + features: features.student, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'student_free_trial_7_days', + name: 'Student', + price_in_cents: 1000, + features: features.student, + hideFromUsers: true, + featureDescription: [], + }, +] + +const individualMonthlyPlans: Array = [ + { + planCode: 'paid-personal', + name: 'Personal', + price_in_cents: 1500, + features: features.personal, + featureDescription: [], + }, + { + planCode: 'paid-personal_free_trial_7_days', + name: 'Personal (Hidden)', + price_in_cents: 1500, + features: features.personal, + featureDescription: [], + hideFromUsers: true, + }, + { + planCode: 'collaborator', + name: 'Standard (Collaborator)', + price_in_cents: 2300, + features: features.collaborator, + featureDescription: [], + }, + { + planCode: 'professional', + name: 'Professional', + price_in_cents: 4500, + features: features.professional, + featureDescription: [], + }, + { + planCode: 'collaborator_free_trial', + name: 'Standard (Collaborator) (Hidden)', + price_in_cents: 1900, + features: features.collaborator, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'collaborator_free_trial_14_days', + name: 'Standard (Collaborator) (Hidden)', + price_in_cents: 1900, + features: features.collaborator, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'collaborator_free_trial_7_days', + name: 'Standard (Collaborator) (Hidden)', + price_in_cents: 2300, + features: features.collaborator, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'collaborator-annual_free_trial', + name: 'Standard (Collaborator) Annual (Hidden)', + price_in_cents: 18000, + features: features.collaborator, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'professional_free_trial', + name: 'Professional (Hidden)', + price_in_cents: 3000, + features: features.professional, + hideFromUsers: true, + featureDescription: [], + }, + { + planCode: 'professional_free_trial_7_days', + name: 'Professional (Hidden)', + price_in_cents: 4500, + features: features.professional, + hideFromUsers: true, + featureDescription: [], + }, +] + +const individualAnnualPlans: Array = [ + { + planCode: 'paid-personal-annual', + name: 'Personal Annual', + price_in_cents: 13900, + annual: true, + features: features.personal, + featureDescription: [], + }, + { + planCode: 'collaborator-annual', + name: 'Standard (Collaborator) Annual', + price_in_cents: 21900, + annual: true, + features: features.collaborator, + featureDescription: [], + }, + { + planCode: 'professional-annual', + name: 'Professional Annual', + price_in_cents: 42900, + annual: true, + features: features.professional, + featureDescription: [], + }, +] + +export const plans = [ + ...studentAccounts, + ...individualMonthlyPlans, + ...individualAnnualPlans, +] diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index faf932e45b..ea6e63c09e 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -1,5 +1,6 @@ import { Nullable } from '../../utils' import { Plan } from '../plan' +import { User } from '../../../types/user' type SubscriptionState = 'active' | 'canceled' | 'expired' @@ -54,3 +55,9 @@ export type Subscription = { export type GroupSubscription = Subscription & { teamName: string } + +export type ManagedGroupSubscription = Omit & { + userIsGroupMember: boolean + planLevelName: string + admin_id: User +}