mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
ffc9b63ab1
commit
c36b872ae3
20 changed files with 664 additions and 113 deletions
|
@ -311,8 +311,9 @@ 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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</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
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 />,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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('We’d 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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' })
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
]
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue