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 =
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,

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

View file

@ -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)

View file

@ -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></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": "",

View file

@ -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<Institution>
}
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 (
<div className="alert alert-warning">
<p>
@ -24,7 +23,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
return (
<>
<div>
{memberships.map((institution: Institution) => (
{institutionMemberships.map((institution: Institution) => (
<div key={`${institution.id}`}>
<Trans
i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y"
@ -44,7 +43,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
<hr />
</div>
))}
{memberships.length > 0 && <PremiumFeaturesLink />}
{institutionMemberships.length > 0 && <PremiumFeaturesLink />}
</div>
</>
)

View file

@ -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<GroupSubscription, 'admin_id'> & {
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 => (
<div key={`managed-group-${subscription._id}`}>
<p>
{subscription.userIsGroupMember ? (

View file

@ -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' && (
<PastDueSubscriptionAlert subscription={subscription} />
{personalSubscription.recurly.account.has_past_due_invoice._ ===
'true' && (
<PastDueSubscriptionAlert subscription={personalSubscription} />
)}
<PersonalSubscriptionStates subscription={subscription} />
<PersonalSubscriptionStates subscription={personalSubscription} />
{recurlyLoadError && (
<div className="alert alert-warning" role="alert">
<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 { 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 && (
<>
{' '}
<Trans
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 />,
]}
/>
</>
)}{' '}
<PendingAdditionalLicenses
additionalLicenses={subscription.recurly.additionalLicenses}
totalLicenses={subscription.recurly.totalLicenses}
/>
)}
{!recurlyLoadError &&
!subscription.groupPlan &&
subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
<button
className="btn-inline-link"
onClick={() => setShowChangePersonalPlan(true)}
>
{t('change_plan')}
</button>
<>
{' '}
<button
className="btn-inline-link"
onClick={() => setShowChangePersonalPlan(true)}
>
{t('change_plan')}
</button>
</>
)}
</p>
{/* && personalSubscription.pendingPlan.name != personalSubscription.plan.name */}
{subscription.pendingPlan &&
subscription.pendingPlan.name !== subscription.plan.name && (
<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.trialEndsAtFormatted && (
<TrialEnding

View file

@ -1,18 +1,117 @@
import { useTranslation } from 'react-i18next'
import { Plan } from '../../../../../../../../types/subscription/plan'
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() {
const { t } = useTranslation()
const { showChangePersonalPlan } = useSubscriptionDashboardContext()
const { plans, showChangePersonalPlan } = useSubscriptionDashboardContext()
if (!showChangePersonalPlan) return null
if (!showChangePersonalPlan || !plans) return null
return (
<>
<h2>{t('change_plan')}</h2>
<p>
<strong>TODO: change subscription placeholder</strong>
</p>
<table className="table">
<thead>
<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 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 (
<div className="container">
@ -25,11 +18,9 @@ function SubscriptionDashboard() {
<h1>{t('your_subscription')}</h1>
</div>
<PersonalSubscription subscription={subscription} />
<ManagedGroupSubscriptions
subscriptions={managedGroupSubscriptions}
/>
<InstitutionMemberships memberships={institutionMemberships} />
<PersonalSubscription />
<ManagedGroupSubscriptions />
<InstitutionMemberships />
{!hasDisplayedSubscription && <FreePlan />}
</div>
</div>

View file

@ -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<Institution>
managedGroupSubscriptions: Array<ManagedGroupSubscription>
personalSubscription?: Subscription
plans: Array<Plan>
recurlyLoadError: boolean
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
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<SubscriptionDashboardContextValue>(
() => ({
hasDisplayedSubscription,
institutionMemberships,
managedGroupSubscriptions,
personalSubscription,
plans,
recurlyLoadError,
setRecurlyLoadError,
showCancellation,
@ -32,6 +59,11 @@ export function SubscriptionDashboardProvider({
setShowChangePersonalPlan,
}),
[
hasDisplayedSubscription,
institutionMemberships,
managedGroupSubscriptions,
personalSubscription,
plans,
recurlyLoadError,
setRecurlyLoadError,
showCancellation,

View file

@ -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('<InstitutionMemberships />', 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', {
exact: false,
@ -50,14 +60,27 @@ describe('<InstitutionMemberships />', function () {
})
it('renders error message when failed to check commons licenses', function () {
render(<InstitutionMemberships memberships={undefined} />)
render(
<SubscriptionDashboardProvider>
<InstitutionMemberships />
</SubscriptionDashboardProvider>
)
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(<InstitutionMemberships memberships={memberships} />)
window.metaAttributesCache.set(
'ol-currentInstitutionsWithLicence',
memberships
)
render(
<SubscriptionDashboardProvider>
<InstitutionMemberships />
</SubscriptionDashboardProvider>
)
screen.getByText('Get the most out of your', {
exact: false,
})

View file

@ -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('<ManagedGroupSubscriptions />', function () {
})
it('renders all managed group subscriptions', function () {
window.metaAttributesCache.set(
'ol-managedGroupSubscriptions',
managedGroupSubscriptions
)
render(
<ManagedGroupSubscriptions subscriptions={managedGroupSubscriptions} />
<SubscriptionDashboardProvider>
<ManagedGroupSubscriptions />
</SubscriptionDashboardProvider>
)
const elements = screen.getAllByText('You are a', {
@ -85,7 +91,11 @@ describe('<ManagedGroupSubscriptions />', 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', {
exact: false,
})

View file

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

View file

@ -12,12 +12,14 @@ import {
trialSubscription,
} from '../../../fixtures/subscriptions'
import sinon from 'sinon'
import { plans } from '../../../fixtures/plans'
describe('<ActiveSubscription />', 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('<ActiveSubscription />', 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('<ActiveSubscription />', 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(
<SubscriptionDashboardProvider>
<ActiveSubscription subscription={groupActiveSubscription} />
</SubscriptionDashboardProvider>
)
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('<ActiveSubscription />', function () {
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 { 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<GroupSubscription, 'admin_id'> & {
userIsGroupMember: boolean
planLevelName: string
admin_id: User
}