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 =
|
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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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": "",
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
<button
|
||||||
className="btn-inline-link"
|
className="btn-inline-link"
|
||||||
onClick={() => setShowChangePersonalPlan(true)}
|
onClick={() => setShowChangePersonalPlan(true)}
|
||||||
>
|
>
|
||||||
{t('change_plan')}
|
{t('change_plan')}
|
||||||
</button>
|
</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
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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('We’d love you to stay')
|
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 { 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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue