Merge pull request #11509 from overleaf/jel-react-personal-subscription-dash-pt-4

[web] Continue migration of personal subscription dash to React

GitOrigin-RevId: f2d913099d727725f04697003516c90616faf014
This commit is contained in:
Jessica Lawshe 2023-02-07 09:38:04 -06:00 committed by Copybot
parent ffc9b63ab1
commit c36b872ae3
20 changed files with 664 additions and 113 deletions

View file

@ -311,9 +311,10 @@ async function _userSubscriptionReactPage(req, res) {
const hasSubscription = const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user) await LimitationsManager.promises.userHasV1OrV2Subscription(user)
const fromPlansPage = req.query.hasSubscription const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList( const plansData =
personalSubscription ? personalSubscription.plan : undefined SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
) personalSubscription?.plan
)
AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view') AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
@ -327,7 +328,8 @@ async function _userSubscriptionReactPage(req, res) {
const data = { const data = {
title: 'your_subscription', title: 'your_subscription',
plans, plans: plansData?.plans,
planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
groupPlans: GroupPlansData, groupPlans: GroupPlansData,
user, user,
hasSubscription, hasSubscription,

View file

@ -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 = { module.exports = {
buildUsersSubscriptionViewModel, buildUsersSubscriptionViewModel,
buildPlansList, buildPlansList,
buildPlansListForSubscriptionDash,
getBestSubscription: callbackify(getBestSubscription), getBestSubscription: callbackify(getBestSubscription),
promises: { promises: {
buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel), buildUsersSubscriptionViewModel: promisify(buildUsersSubscriptionViewModel),

View file

@ -9,8 +9,9 @@ block head-scripts
block append meta block append meta
meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions) meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
meta(name="ol-managedGroupSubscriptions", data-type="json" content=managedGroupSubscriptions) 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-currentInstitutionsWithLicence", data-type="json" content=currentInstitutionsWithLicence)
meta(name="ol-plans", data-type="json" content=plans)
if (personalSubscription && personalSubscription.recurly) if (personalSubscription && personalSubscription.recurly)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
meta(name="ol-subscription" data-type="json" content=personalSubscription) meta(name="ol-subscription" data-type="json" content=personalSubscription)

View file

@ -92,6 +92,7 @@
"change_plan": "", "change_plan": "",
"change_primary_email_address_instructions": "", "change_primary_email_address_instructions": "",
"change_project_owner": "", "change_project_owner": "",
"change_to_this_plan": "",
"chat": "", "chat": "",
"chat_error": "", "chat_error": "",
"checking_dropbox_status": "", "checking_dropbox_status": "",
@ -128,6 +129,7 @@
"connected_users": "", "connected_users": "",
"contact_message_label": "", "contact_message_label": "",
"contact_sales": "", "contact_sales": "",
"contact_support_to_change_group_subscription": "",
"contact_us": "", "contact_us": "",
"continue_github_merge": "", "continue_github_merge": "",
"copy": "", "copy": "",
@ -374,6 +376,7 @@
"is_email_affiliated": "", "is_email_affiliated": "",
"join_project": "", "join_project": "",
"joining": "", "joining": "",
"keep_current_plan": "",
"keybindings": "", "keybindings": "",
"labs_program_already_participating": "", "labs_program_already_participating": "",
"labs_program_benefits": "<0></0>", "labs_program_benefits": "<0></0>",
@ -450,6 +453,7 @@
"mendeley_reference_loading_error_forbidden": "", "mendeley_reference_loading_error_forbidden": "",
"mendeley_sync_description": "", "mendeley_sync_description": "",
"menu": "", "menu": "",
"month": "",
"more": "", "more": "",
"n_items": "", "n_items": "",
"n_items_plural": "", "n_items_plural": "",
@ -541,6 +545,7 @@
"premium_feature": "", "premium_feature": "",
"premium_plan_label": "", "premium_plan_label": "",
"press_shortcut_to_open_advanced_reference_search": "", "press_shortcut_to_open_advanced_reference_search": "",
"price": "",
"priority_support": "", "priority_support": "",
"privacy_policy": "", "privacy_policy": "",
"private": "", "private": "",
@ -825,6 +830,7 @@
"word_count": "", "word_count": "",
"work_offline": "", "work_offline": "",
"work_with_non_overleaf_users": "", "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_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_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": "", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "",
@ -834,6 +840,8 @@
"your_affiliation_is_confirmed": "", "your_affiliation_is_confirmed": "",
"your_browser_does_not_support_this_feature": "", "your_browser_does_not_support_this_feature": "",
"your_message_to_collaborators": "", "your_message_to_collaborators": "",
"your_new_plan": "",
"your_plan": "",
"your_plan_is_changing_at_term_end": "", "your_plan_is_changing_at_term_end": "",
"your_projects": "", "your_projects": "",
"your_subscription": "", "your_subscription": "",

View file

@ -1,15 +1,14 @@
import { Trans } from 'react-i18next' import { Trans } from 'react-i18next'
import { Institution } from '../../../../../../types/institution' import { Institution } from '../../../../../../types/institution'
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
import PremiumFeaturesLink from './premium-features-link' import PremiumFeaturesLink from './premium-features-link'
type InstitutionMembershipsProps = { function InstitutionMemberships() {
memberships?: Array<Institution> 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 // memberships is undefined when data failed to load. If user has no memberships, then an empty array is returned
if (!memberships) { if (!institutionMemberships) {
return ( return (
<div className="alert alert-warning"> <div className="alert alert-warning">
<p> <p>
@ -24,7 +23,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
return ( return (
<> <>
<div> <div>
{memberships.map((institution: Institution) => ( {institutionMemberships.map((institution: Institution) => (
<div key={`${institution.id}`}> <div key={`${institution.id}`}>
<Trans <Trans
i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y" i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y"
@ -44,7 +43,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
<hr /> <hr />
</div> </div>
))} ))}
{memberships.length > 0 && <PremiumFeaturesLink />} {institutionMemberships.length > 0 && <PremiumFeaturesLink />}
</div> </div>
</> </>
) )

View file

@ -1,29 +1,17 @@
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { GroupSubscription } from '../../../../../../types/subscription/dashboard/subscription' import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
import { User } from '../../../../../../types/user'
export type ManagedGroupSubscription = Omit<GroupSubscription, 'admin_id'> & { export default function ManagedGroupSubscriptions() {
userIsGroupMember: boolean
planLevelName: string
admin_id: User
}
type ManagedGroupSubscriptionsProps = {
subscriptions?: ManagedGroupSubscription[]
}
export default function ManagedGroupSubscriptions({
subscriptions,
}: ManagedGroupSubscriptionsProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { managedGroupSubscriptions } = useSubscriptionDashboardContext()
if (!subscriptions) { if (!managedGroupSubscriptions) {
return null return null
} }
return ( return (
<> <>
{subscriptions.map(subscription => ( {managedGroupSubscriptions.map(subscription => (
<div key={`managed-group-${subscription._id}`}> <div key={`managed-group-${subscription._id}`}>
<p> <p>
{subscription.userIsGroupMember ? ( {subscription.userIsGroupMember ? (

View file

@ -47,13 +47,9 @@ function PersonalSubscriptionStates({
} }
} }
function PersonalSubscription({ function PersonalSubscription() {
subscription,
}: {
subscription?: Subscription
}) {
const { t } = useTranslation() const { t } = useTranslation()
const { recurlyLoadError, setRecurlyLoadError } = const { personalSubscription, recurlyLoadError, setRecurlyLoadError } =
useSubscriptionDashboardContext() useSubscriptionDashboardContext()
useEffect(() => { useEffect(() => {
@ -62,14 +58,15 @@ function PersonalSubscription({
} }
}) })
if (!subscription) return null if (!personalSubscription) return null
return ( return (
<> <>
{subscription.recurly.account.has_past_due_invoice._ === 'true' && ( {personalSubscription.recurly.account.has_past_due_invoice._ ===
<PastDueSubscriptionAlert subscription={subscription} /> 'true' && (
<PastDueSubscriptionAlert subscription={personalSubscription} />
)} )}
<PersonalSubscriptionStates subscription={subscription} /> <PersonalSubscriptionStates subscription={personalSubscription} />
{recurlyLoadError && ( {recurlyLoadError && (
<div className="alert alert-warning" role="alert"> <div className="alert alert-warning" role="alert">
<strong>{t('payment_provider_unreachable_error')}</strong> <strong>{t('payment_provider_unreachable_error')}</strong>

View file

@ -8,6 +8,8 @@ import { CancelSubscription } from './cancel-subscription'
import { PendingPlanChange } from './pending-plan-change' import { PendingPlanChange } from './pending-plan-change'
import { TrialEnding } from './trial-ending' import { TrialEnding } from './trial-ending'
import { ChangePlan } from './change-plan' import { ChangePlan } from './change-plan'
import { PendingAdditionalLicenses } from './pending-additional-licenses'
import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan'
export function ActiveSubscription({ export function ActiveSubscription({
subscription, subscription,
@ -38,40 +40,32 @@ export function ActiveSubscription({
)} )}
{!subscription.pendingPlan && {!subscription.pendingPlan &&
subscription.recurly.additionalLicenses > 0 && ( subscription.recurly.additionalLicenses > 0 && (
<> <PendingAdditionalLicenses
{' '} additionalLicenses={subscription.recurly.additionalLicenses}
<Trans totalLicenses={subscription.recurly.totalLicenses}
i18nKey="additional_licenses" />
values={{ )}
additionalLicenses: subscription.recurly.additionalLicenses,
totalLicenses: subscription.recurly.totalLicenses,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</>
)}{' '}
{!recurlyLoadError && {!recurlyLoadError &&
!subscription.groupPlan && !subscription.groupPlan &&
subscription.recurly.account.has_past_due_invoice._ !== 'true' && ( subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
<button <>
className="btn-inline-link" {' '}
onClick={() => setShowChangePersonalPlan(true)} <button
> className="btn-inline-link"
{t('change_plan')} onClick={() => setShowChangePersonalPlan(true)}
</button> >
{t('change_plan')}
</button>
</>
)} )}
</p> </p>
{/* && personalSubscription.pendingPlan.name != personalSubscription.plan.name */}
{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>{t('want_change_to_apply_before_plan_end')}</p>
)} )}
{/* TODO: groupPlan */} {(!subscription.pendingPlan ||
subscription.pendingPlan.name === subscription.plan.name) &&
subscription.plan.groupPlan && <ContactSupportToChangeGroupPlan />}
{subscription.recurly.trial_ends_at && {subscription.recurly.trial_ends_at &&
subscription.recurly.trialEndsAtFormatted && ( subscription.recurly.trialEndsAtFormatted && (
<TrialEnding <TrialEnding

View file

@ -1,18 +1,117 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Plan } from '../../../../../../../../types/subscription/plan'
import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context' import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
function ChangeToPlanButton({ plan }: { plan: Plan }) {
const { t } = useTranslation()
// for when the user selected to change a plan, but the plan change is still pending
return (
<form>
{/* todo: ng-model="plan_code" */}
<input type="hidden" name="plan_code" value={plan.planCode} />
{/* todo: handle submit changePlan */}
<input
type="submit"
value={t('change_to_this_plan')}
className="btn btn-primary"
/>
</form>
)
}
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 (
<form>
{/* todo: ng-model="plan_code" */}
<input type="hidden" name="plan_code" value={plan.planCode} />
{/* todo: handle submit cancelPendingPlanChange */}
<input
type="submit"
value={t('keep_current_plan')}
className="btn btn-primary"
/>
</form>
)
}
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 <KeepCurrentPlanButton plan={plan} />
} else if (isCurrentPlanForUser && !personalSubscription.pendingPlan) {
return (
<button className="btn btn-secondary disabled">{t('your_plan')}</button>
)
} else if (
personalSubscription?.pendingPlan?.planCode?.split('_')[0] === plan.planCode
) {
return (
<button className="btn btn-secondary disabled">
{t('your_new_plan')}
</button>
)
} else {
return <ChangeToPlanButton plan={plan} />
}
}
function PlansRow({ plan }: { plan: Plan }) {
const { t } = useTranslation()
return (
<tr>
<td>
<strong>{plan.name}</strong>
</td>
<td>
{/* todo: {{ displayPrice }} */}/ {plan.annual ? t('year') : t('month')}
</td>
<td>
<ChangePlanButton plan={plan} />
</td>
</tr>
)
}
function PlansRows({ plans }: { plans: Array<Plan> }) {
return (
<>
{plans &&
plans.map(plan => (
<PlansRow key={`plans-row-${plan.planCode}`} plan={plan} />
))}
</>
)
}
export function ChangePlan() { export function ChangePlan() {
const { t } = useTranslation() const { t } = useTranslation()
const { showChangePersonalPlan } = useSubscriptionDashboardContext() const { plans, showChangePersonalPlan } = useSubscriptionDashboardContext()
if (!showChangePersonalPlan) return null if (!showChangePersonalPlan || !plans) return null
return ( return (
<> <>
<h2>{t('change_plan')}</h2> <h2>{t('change_plan')}</h2>
<p> <table className="table">
<strong>TODO: change subscription placeholder</strong> <thead>
</p> <tr>
<th>{t('name')}</th>
<th>{t('price')}</th>
<th />
</tr>
</thead>
<tbody>
<PlansRows plans={plans} />
</tbody>
</table>
</> </>
) )
} }

View file

@ -0,0 +1,15 @@
import { Trans } from 'react-i18next'
export function ContactSupportToChangeGroupPlan() {
return (
<p>
<Trans
i18nKey="contact_support_to_change_group_subscription"
components={[
// eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
<a href="/contact" />,
]}
/>
</p>
)
}

View file

@ -0,0 +1,28 @@
import { Trans } from 'react-i18next'
export function PendingAdditionalLicenses({
additionalLicenses,
totalLicenses,
}: {
additionalLicenses: number
totalLicenses: number
}) {
return (
<>
{' '}
<Trans
i18nKey="additional_licenses"
values={{
additionalLicenses,
totalLicenses,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</>
)
}

View file

@ -1,20 +1,13 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import getMeta from '../../../../utils/meta'
import InstitutionMemberships from './institution-memberships' import InstitutionMemberships from './institution-memberships'
import FreePlan from './free-plan' import FreePlan from './free-plan'
import PersonalSubscription from './personal-subscription' import PersonalSubscription from './personal-subscription'
import ManagedGroupSubscriptions from './managed-group-subscriptions' import ManagedGroupSubscriptions from './managed-group-subscriptions'
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
function SubscriptionDashboard() { function SubscriptionDashboard() {
const { t } = useTranslation() const { t } = useTranslation()
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence') const { hasDisplayedSubscription } = useSubscriptionDashboardContext()
const subscription = getMeta('ol-subscription')
const managedGroupSubscriptions = getMeta('ol-managedGroupSubscriptions')
const hasDisplayedSubscription =
institutionMemberships?.length > 0 ||
subscription ||
managedGroupSubscriptions?.length > 0
return ( return (
<div className="container"> <div className="container">
@ -25,11 +18,9 @@ function SubscriptionDashboard() {
<h1>{t('your_subscription')}</h1> <h1>{t('your_subscription')}</h1>
</div> </div>
<PersonalSubscription subscription={subscription} /> <PersonalSubscription />
<ManagedGroupSubscriptions <ManagedGroupSubscriptions />
subscriptions={managedGroupSubscriptions} <InstitutionMemberships />
/>
<InstitutionMemberships memberships={institutionMemberships} />
{!hasDisplayedSubscription && <FreePlan />} {!hasDisplayedSubscription && <FreePlan />}
</div> </div>
</div> </div>

View file

@ -1,6 +1,18 @@
import { createContext, ReactNode, useContext, useMemo, useState } from 'react' 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 = { type SubscriptionDashboardContextValue = {
hasDisplayedSubscription: boolean
institutionMemberships?: Array<Institution>
managedGroupSubscriptions: Array<ManagedGroupSubscription>
personalSubscription?: Subscription
plans: Array<Plan>
recurlyLoadError: boolean recurlyLoadError: boolean
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>> setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
showCancellation: boolean showCancellation: boolean
@ -22,8 +34,23 @@ export function SubscriptionDashboardProvider({
const [showCancellation, setShowCancellation] = useState(false) const [showCancellation, setShowCancellation] = useState(false)
const [showChangePersonalPlan, setShowChangePersonalPlan] = 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<SubscriptionDashboardContextValue>( const value = useMemo<SubscriptionDashboardContextValue>(
() => ({ () => ({
hasDisplayedSubscription,
institutionMemberships,
managedGroupSubscriptions,
personalSubscription,
plans,
recurlyLoadError, recurlyLoadError,
setRecurlyLoadError, setRecurlyLoadError,
showCancellation, showCancellation,
@ -32,6 +59,11 @@ export function SubscriptionDashboardProvider({
setShowChangePersonalPlan, setShowChangePersonalPlan,
}), }),
[ [
hasDisplayedSubscription,
institutionMemberships,
managedGroupSubscriptions,
personalSubscription,
plans,
recurlyLoadError, recurlyLoadError,
setRecurlyLoadError, setRecurlyLoadError,
showCancellation, showCancellation,

View file

@ -1,6 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships' import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships'
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
const memberships = [ const memberships = [
{ {
@ -35,7 +36,16 @@ describe('<InstitutionMemberships />', function () {
}) })
it('renders all insitutions with license', function () { it('renders all insitutions with license', function () {
render(<InstitutionMemberships memberships={memberships} />) window.metaAttributesCache.set(
'ol-currentInstitutionsWithLicence',
memberships
)
render(
<SubscriptionDashboardProvider>
<InstitutionMemberships />
</SubscriptionDashboardProvider>
)
const elements = screen.getAllByText('You are on our', { const elements = screen.getAllByText('You are on our', {
exact: false, exact: false,
@ -50,14 +60,27 @@ describe('<InstitutionMemberships />', function () {
}) })
it('renders error message when failed to check commons licenses', function () { it('renders error message when failed to check commons licenses', function () {
render(<InstitutionMemberships memberships={undefined} />) render(
<SubscriptionDashboardProvider>
<InstitutionMemberships />
</SubscriptionDashboardProvider>
)
screen.getByText( screen.getByText(
'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.' '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 () { it('renders the "Get the most out of your" subscription text when a user has a subscription', function () {
render(<InstitutionMemberships memberships={memberships} />) window.metaAttributesCache.set(
'ol-currentInstitutionsWithLicence',
memberships
)
render(
<SubscriptionDashboardProvider>
<InstitutionMemberships />
</SubscriptionDashboardProvider>
)
screen.getByText('Get the most out of your', { screen.getByText('Get the most out of your', {
exact: false, exact: false,
}) })

View file

@ -1,12 +1,12 @@
import { expect } from 'chai' import { expect } from 'chai'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import ManagedGroupSubscriptions, {
ManagedGroupSubscription,
} from '../../../../../../frontend/js/features/subscription/components/dashboard/managed-group-subscriptions'
import { import {
groupActiveSubscription, groupActiveSubscription,
groupActiveSubscriptionWithPendingLicenseChange, groupActiveSubscriptionWithPendingLicenseChange,
} from '../../fixtures/subscriptions' } 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[] = [ const managedGroupSubscriptions: ManagedGroupSubscription[] = [
{ {
@ -39,8 +39,14 @@ describe('<ManagedGroupSubscriptions />', function () {
}) })
it('renders all managed group subscriptions', function () { it('renders all managed group subscriptions', function () {
window.metaAttributesCache.set(
'ol-managedGroupSubscriptions',
managedGroupSubscriptions
)
render( render(
<ManagedGroupSubscriptions subscriptions={managedGroupSubscriptions} /> <SubscriptionDashboardProvider>
<ManagedGroupSubscriptions />
</SubscriptionDashboardProvider>
) )
const elements = screen.getAllByText('You are a', { const elements = screen.getAllByText('You are a', {
@ -85,7 +91,11 @@ describe('<ManagedGroupSubscriptions />', function () {
}) })
it('renders nothing when there are no group memberships', function () { it('renders nothing when there are no group memberships', function () {
render(<ManagedGroupSubscriptions subscriptions={undefined} />) render(
<SubscriptionDashboardProvider>
<ManagedGroupSubscriptions />
</SubscriptionDashboardProvider>
)
const elements = screen.queryAllByText('You are a', { const elements = screen.queryAllByText('You are a', {
exact: false, exact: false,
}) })

View file

@ -25,7 +25,7 @@ describe('<PersonalSubscription />', function () {
it('returns empty container', function () { it('returns empty container', function () {
const { container } = render( const { container } = render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={undefined} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
expect(container.firstChild).to.be.null expect(container.firstChild).to.be.null
@ -34,9 +34,13 @@ describe('<PersonalSubscription />', function () {
describe('subscription states ', function () { describe('subscription states ', function () {
it('renders the active dash', function () { it('renders the active dash', function () {
window.metaAttributesCache.set(
'ol-subscription',
annualActiveSubscription
)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={annualActiveSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
@ -44,9 +48,10 @@ describe('<PersonalSubscription />', function () {
}) })
it('renders the canceled dash', function () { it('renders the canceled dash', function () {
window.metaAttributesCache.set('ol-subscription', canceledSubscription)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={canceledSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
screen.getByText( screen.getByText(
@ -69,9 +74,13 @@ describe('<PersonalSubscription />', function () {
}) })
it('renders the expired dash', function () { it('renders the expired dash', function () {
window.metaAttributesCache.set(
'ol-subscription',
pastDueExpiredSubscription
)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={pastDueExpiredSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
screen.getByText('Your subscription has expired.') screen.getByText('Your subscription has expired.')
@ -83,9 +92,10 @@ describe('<PersonalSubscription />', function () {
JSON.parse(JSON.stringify(annualActiveSubscription)) JSON.parse(JSON.stringify(annualActiveSubscription))
) )
withStateDeleted.recurly.state = undefined withStateDeleted.recurly.state = undefined
window.metaAttributesCache.set('ol-subscription', withStateDeleted)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={withStateDeleted} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
screen.getByText( screen.getByText(
@ -96,9 +106,13 @@ describe('<PersonalSubscription />', function () {
describe('past due subscription', function () { describe('past due subscription', function () {
it('renders error alert', function () { it('renders error alert', function () {
window.metaAttributesCache.set(
'ol-subscription',
pastDueExpiredSubscription
)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={pastDueExpiredSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
screen.getByRole('alert') screen.getByRole('alert')
@ -120,9 +134,13 @@ describe('<PersonalSubscription />', function () {
it('shows an alert and hides "Change plan" option when Recurly did not load', function () { it('shows an alert and hides "Change plan" option when Recurly did not load', function () {
// @ts-ignore // @ts-ignore
delete window.recurly delete window.recurly
window.metaAttributesCache.set(
'ol-subscription',
annualActiveSubscription
)
render( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={annualActiveSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )
@ -133,9 +151,13 @@ describe('<PersonalSubscription />', function () {
}) })
it('should not show an alert and should show "Change plan" option when Recurly did load', 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( render(
<SubscriptionDashboardProvider> <SubscriptionDashboardProvider>
<PersonalSubscription subscription={annualActiveSubscription} /> <PersonalSubscription />
</SubscriptionDashboardProvider> </SubscriptionDashboardProvider>
) )

View file

@ -12,12 +12,14 @@ import {
trialSubscription, trialSubscription,
} from '../../../fixtures/subscriptions' } from '../../../fixtures/subscriptions'
import sinon from 'sinon' import sinon from 'sinon'
import { plans } from '../../../fixtures/plans'
describe('<ActiveSubscription />', function () { describe('<ActiveSubscription />', function () {
let sendMBSpy: sinon.SinonSpy let sendMBSpy: sinon.SinonSpy
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache = new Map() window.metaAttributesCache = new Map()
window.metaAttributesCache.set('ol-plans', plans)
sendMBSpy = sinon.spy(eventTracking, 'sendMB') sendMBSpy = sinon.spy(eventTracking, 'sendMB')
}) })
@ -77,11 +79,13 @@ describe('<ActiveSubscription />', function () {
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 UI still shown // confirm main dash UI still shown
expectedInActiveSubscription(annualActiveSubscription) screen.getByText('You are currently subscribed to the', { exact: false })
// TODO: add change plan UI screen.getByRole('heading', { name: 'Change plan' })
screen.getByText('change subscription placeholder', { exact: false }) 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 () { it('notes when user is changing plan at end of current plan term', function () {
@ -103,17 +107,10 @@ describe('<ActiveSubscription />', function () {
screen.getByText( screen.getByText(
'If you wish this change to apply before the end of your current billing period, please contact us.' '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 () { expect(screen.queryByRole('link', { name: 'contact support' })).to.be.null
render( expect(screen.queryByText('if you wish to change your group subscription.'))
<SubscriptionDashboardProvider> .to.be.null
<ActiveSubscription subscription={groupActiveSubscription} />
</SubscriptionDashboardProvider>
)
const changePlan = screen.queryByRole('button', { name: 'Change plan' })
expect(changePlan).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 () {
@ -234,4 +231,29 @@ describe('<ActiveSubscription />', function () {
screen.getByText('Wed love you to stay') screen.getByText('Wed love you to stay')
}) })
describe('group plans', function () {
it('does not show "Change plan" option for group plans', function () {
render(
<SubscriptionDashboardProvider>
<ActiveSubscription subscription={groupActiveSubscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubscription subscription={groupActiveSubscription} />
</SubscriptionDashboardProvider>
)
screen.getByRole('link', { name: 'contact support' })
screen.getByText('if you wish to change your group subscription.', {
exact: false,
})
})
})
}) })

View file

@ -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('<ChangePlan />', 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(
<SubscriptionDashboardProvider>
<ChangePlan />
</SubscriptionDashboardProvider>
)
expect(container.firstChild).to.be.null
})
it('renders the table of plans', function () {
window.metaAttributesCache.set('ol-subscription', annualActiveSubscription)
render(
<SubscriptionDashboardProvider>
<ActiveSubscription subscription={annualActiveSubscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubscription subscription={pendingSubscriptionChange} />
</SubscriptionDashboardProvider>
)
const button = screen.getByRole('button', { name: 'Change plan' })
fireEvent.click(button)
screen.getByText('Your new plan')
screen.getByRole('button', { name: 'Keep my current plan' })
})
})

View file

@ -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<Plan> = [
{
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<Plan> = [
{
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<Plan> = [
{
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,
]

View file

@ -1,5 +1,6 @@
import { Nullable } from '../../utils' import { Nullable } from '../../utils'
import { Plan } from '../plan' import { Plan } from '../plan'
import { User } from '../../../types/user'
type SubscriptionState = 'active' | 'canceled' | 'expired' type SubscriptionState = 'active' | 'canceled' | 'expired'
@ -54,3 +55,9 @@ export type Subscription = {
export type GroupSubscription = Subscription & { export type GroupSubscription = Subscription & {
teamName: string teamName: string
} }
export type ManagedGroupSubscription = Omit<GroupSubscription, 'admin_id'> & {
userIsGroupMember: boolean
planLevelName: string
admin_id: User
}