mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-22 10:36:57 +00:00
Merge pull request #11773 from overleaf/jel-subscription-dash-change-to-group
[web] Begin change to group plan via React subscription dash GitOrigin-RevId: 3f0f2820ab18ecc8337746282295302d7951c56f
This commit is contained in:
parent
b62cb86bf8
commit
b2a10260be
17 changed files with 529 additions and 24 deletions
|
@ -289,6 +289,15 @@ async function userSubscriptionPage(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatGroupPlansDataForDash() {
|
||||
return {
|
||||
plans: [...groupPlanModalOptions.plan_codes],
|
||||
sizes: [...groupPlanModalOptions.sizes],
|
||||
usages: [...groupPlanModalOptions.usages],
|
||||
priceByUsageTypeAndSize: JSON.parse(JSON.stringify(GroupPlansData)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("express").Request} req
|
||||
* @param {import("express").Response} res
|
||||
|
@ -327,11 +336,12 @@ async function _userSubscriptionReactPage(req, res) {
|
|||
|
||||
const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
|
||||
|
||||
const groupPlansDataForDash = formatGroupPlansDataForDash()
|
||||
|
||||
const data = {
|
||||
title: 'your_subscription',
|
||||
plans: plansData?.plans,
|
||||
planCodesChangingAtTermEnd: plansData?.planCodesChangingAtTermEnd,
|
||||
groupPlans: GroupPlansData,
|
||||
user,
|
||||
hasSubscription,
|
||||
fromPlansPage,
|
||||
|
@ -342,8 +352,8 @@ async function _userSubscriptionReactPage(req, res) {
|
|||
managedPublishers,
|
||||
v1SubscriptionStatus,
|
||||
currentInstitutionsWithLicence,
|
||||
groupPlanModalOptions,
|
||||
cancelButtonNewCopy,
|
||||
groupPlans: groupPlansDataForDash,
|
||||
}
|
||||
res.render('subscriptions/dashboard-react', data)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ block append meta
|
|||
meta(name="ol-subscription" data-type="json" content=personalSubscription)
|
||||
meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)
|
||||
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
|
||||
meta(name="ol-groupPlanModalOptions" data-type="json" content=groupPlanModalOptions)
|
||||
|
||||
block content
|
||||
main.content.content-alt#subscription-dashboard-root
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"additional_licenses": "",
|
||||
"address_line_1": "",
|
||||
"address_second_line_optional": "",
|
||||
"all_premium_features": "",
|
||||
"all_premium_features_including": "",
|
||||
"all_projects": "",
|
||||
"also": "",
|
||||
|
@ -96,6 +97,7 @@
|
|||
"change_plan": "",
|
||||
"change_primary_email_address_instructions": "",
|
||||
"change_project_owner": "",
|
||||
"change_to_group_plan": "",
|
||||
"change_to_this_plan": "",
|
||||
"chat": "",
|
||||
"chat_error": "",
|
||||
|
@ -153,6 +155,7 @@
|
|||
"creating": "",
|
||||
"current_password": "",
|
||||
"currently_subscribed_to_plan": "",
|
||||
"customize_your_group_subscription": "",
|
||||
"date_and_owner": "",
|
||||
"delete": "",
|
||||
"delete_account": "",
|
||||
|
@ -205,6 +208,7 @@
|
|||
"dropbox_unlinked_premium_feature": "",
|
||||
"duplicate_file": "",
|
||||
"duplicate_projects": "",
|
||||
"each_user_will_have_access_to": "",
|
||||
"easily_manage_your_project_files_everywhere": "",
|
||||
"edit": "",
|
||||
"edit_dictionary": "",
|
||||
|
@ -215,6 +219,8 @@
|
|||
"editor_and_pdf": "&",
|
||||
"editor_only_hide_pdf": "",
|
||||
"editor_theme": "",
|
||||
"educational_discount_for_groups_of_x_or_more": "",
|
||||
"educational_percent_discount_applied": "",
|
||||
"email": "",
|
||||
"email_or_password_wrong_try_again": "",
|
||||
"emails_and_affiliations_explanation": "",
|
||||
|
@ -405,6 +411,7 @@
|
|||
"leave": "",
|
||||
"leave_projects": "",
|
||||
"let_us_know": "",
|
||||
"license_for_educational_purposes": "",
|
||||
"limited_offer": "",
|
||||
"line_height": "",
|
||||
"link": "",
|
||||
|
@ -472,6 +479,7 @@
|
|||
"name": "",
|
||||
"navigate_log_source": "",
|
||||
"navigation": "",
|
||||
"need_more_than_x_licenses": "",
|
||||
"need_to_add_new_primary_before_remove": "",
|
||||
"need_to_leave": "",
|
||||
"need_to_upgrade_for_more_collabs": "",
|
||||
|
@ -481,6 +489,7 @@
|
|||
"new_name": "",
|
||||
"new_password": "",
|
||||
"new_project": "",
|
||||
"new_subscription_will_be_billed_immediately": "",
|
||||
"new_to_latex_look_at": "",
|
||||
"newsletter": "",
|
||||
"next_payment_of_x_collectected_on_y": "",
|
||||
|
@ -503,6 +512,7 @@
|
|||
"normally_x_price_per_year": "",
|
||||
"notification_project_invite_accepted_message": "",
|
||||
"notification_project_invite_message": "",
|
||||
"number_of_users": "",
|
||||
"oauth_orcid_description": "",
|
||||
"of": "",
|
||||
"off": "",
|
||||
|
@ -539,6 +549,8 @@
|
|||
"pdf_viewer": "",
|
||||
"pdf_viewer_error": "",
|
||||
"pending_additional_licenses": "",
|
||||
"percent_discount_for_groups": "",
|
||||
"plan": "",
|
||||
"plan_tooltip": "",
|
||||
"please_change_primary_to_remove": "",
|
||||
"please_check_your_inbox": "",
|
||||
|
@ -547,6 +559,7 @@
|
|||
"please_compile_pdf_before_word_count": "",
|
||||
"please_confirm_email": "",
|
||||
"please_confirm_your_email_before_making_it_default": "",
|
||||
"please_get_in_touch": "",
|
||||
"please_link_before_making_primary": "",
|
||||
"please_reconfirm_institutional_email": "",
|
||||
"please_reconfirm_your_affiliation_before_making_this_primary": "",
|
||||
|
@ -555,6 +568,7 @@
|
|||
"please_select_a_project": "",
|
||||
"please_select_an_output_file": "",
|
||||
"please_set_main_file": "",
|
||||
"plus_more": "",
|
||||
"plus_upgraded_accounts_receive": "",
|
||||
"postal_code": "",
|
||||
"premium_feature": "",
|
||||
|
@ -611,6 +625,7 @@
|
|||
"recompile_pdf": "",
|
||||
"reconnect": "",
|
||||
"redirect_to_editor": "",
|
||||
"reduce_costs_group_licenses": "",
|
||||
"reference_error_relink_hint": "",
|
||||
"reference_managers": "",
|
||||
"reference_search": "",
|
||||
|
@ -646,6 +661,7 @@
|
|||
"save_or_cancel-cancel": "",
|
||||
"save_or_cancel-or": "",
|
||||
"save_or_cancel-save": "",
|
||||
"save_x_percent_or_more": "",
|
||||
"saved_bibtex_appended_to_galileo_bib": "",
|
||||
"saved_bibtex_to_new_galileo_bib": "",
|
||||
"saving": "",
|
||||
|
@ -881,6 +897,8 @@
|
|||
"x_price_for_first_month": "",
|
||||
"x_price_for_first_year": "",
|
||||
"x_price_for_y_months": "",
|
||||
"x_price_per_user": "",
|
||||
"x_price_per_year": "",
|
||||
"year": "",
|
||||
"you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "",
|
||||
"you_are_a_manager_of_commons_at_institution_x": "",
|
||||
|
|
|
@ -2,9 +2,10 @@ import { useTranslation } from 'react-i18next'
|
|||
import LoadingSpinner from '../../../../../../../shared/components/loading-spinner'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||
import { ChangeToGroupPlan } from './change-to-group-plan'
|
||||
import { ConfirmChangePlanModal } from './confirm-change-plan-modal'
|
||||
import { ConfirmChangePlanModal } from './modals/confirm-change-plan-modal'
|
||||
import { IndividualPlansTable } from './individual-plans-table'
|
||||
import { KeepCurrentPlanModal } from './keep-current-plan-modal'
|
||||
import { KeepCurrentPlanModal } from './modals/keep-current-plan-modal'
|
||||
import { ChangeToGroupModal } from './modals/change-to-group-modal'
|
||||
|
||||
export function ChangePlan() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -32,6 +33,7 @@ export function ChangePlan() {
|
|||
<ChangeToGroupPlan />
|
||||
<ConfirmChangePlanModal />
|
||||
<KeepCurrentPlanModal />
|
||||
<ChangeToGroupModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||
|
||||
export function ChangeToGroupPlan() {
|
||||
const { t } = useTranslation()
|
||||
const { handleOpenModal } = useSubscriptionDashboardContext()
|
||||
|
||||
const handleClick = () => {
|
||||
handleOpenModal('change-to-group')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>{t('looking_multiple_licenses')}</h2>
|
||||
{/* todo: if/else isValidCurrencyForUpgrade and modal */}
|
||||
<p style={{ margin: 0 }}>{t('reduce_costs_group_licenses')}</p>
|
||||
<br />
|
||||
<button className="btn btn-primary" onClick={handleClick}>
|
||||
{t('change_to_group_plan')}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
import { useEffect } from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { GroupPlans } from '../../../../../../../../../../types/subscription/dashboard/group-plans'
|
||||
import { Subscription } from '../../../../../../../../../../types/subscription/dashboard/subscription'
|
||||
import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
|
||||
import getMeta from '../../../../../../../../utils/meta'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
||||
|
||||
const educationalPercentDiscount = 40
|
||||
const groupSizeForEducationalDiscount = 10
|
||||
|
||||
function GroupPlanCollaboratorCount({ planCode }: { planCode: string }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (planCode === 'collaborator') {
|
||||
return (
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="collabs_per_proj"
|
||||
values={{
|
||||
collabcount: 10,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
} else if (planCode === 'professional') {
|
||||
return <>{t('unlimited_collabs')}</>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
|
||||
const size = parseInt(groupSize)
|
||||
if (size >= groupSizeForEducationalDiscount) {
|
||||
return (
|
||||
<p className="applied">
|
||||
<Trans
|
||||
i18nKey="educational_percent_discount_applied"
|
||||
values={{ percent: educationalPercentDiscount }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="ineligible">
|
||||
<Trans
|
||||
i18nKey="educational_discount_for_groups_of_x_or_more"
|
||||
values={{ size: groupSizeForEducationalDiscount }}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupPrice() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden>
|
||||
X <span className="small">/ {t('year')}</span>
|
||||
</span>
|
||||
<span className="sr-only">
|
||||
{/* TODO: price */}
|
||||
<Trans i18nKey="x_price_per_year" values={{ price: '$X' }} />
|
||||
</span>
|
||||
<br />
|
||||
<span className="circle-subtext">
|
||||
{/* TODO: price */}
|
||||
<Trans i18nKey="x_price_per_user" values={{ price: '$X' }} />
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ChangeToGroupModal() {
|
||||
const modalId = 'change-to-group'
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
groupPlanToChangeToCode,
|
||||
groupPlanToChangeToSize,
|
||||
groupPlanToChangeToUsage,
|
||||
handleCloseModal,
|
||||
modalIdShown,
|
||||
setGroupPlanToChangeToCode,
|
||||
setGroupPlanToChangeToSize,
|
||||
setGroupPlanToChangeToUsage,
|
||||
} = useSubscriptionDashboardContext()
|
||||
const groupPlans: GroupPlans = getMeta('ol-groupPlans')
|
||||
const personalSubscription: Subscription = getMeta('ol-subscription')
|
||||
|
||||
useEffect(() => {
|
||||
const defaultPlanOption = personalSubscription.plan.planCode.includes(
|
||||
'professional'
|
||||
)
|
||||
? 'professional'
|
||||
: 'collaborator'
|
||||
setGroupPlanToChangeToCode(defaultPlanOption)
|
||||
}, [personalSubscription, setGroupPlanToChangeToCode])
|
||||
|
||||
if (
|
||||
modalIdShown !== modalId ||
|
||||
!groupPlans ||
|
||||
!groupPlans.plans ||
|
||||
!groupPlans.sizes ||
|
||||
!groupPlanToChangeToCode
|
||||
)
|
||||
return null
|
||||
|
||||
return (
|
||||
<AccessibleModal
|
||||
id={modalId}
|
||||
show
|
||||
animation
|
||||
onHide={handleCloseModal}
|
||||
backdrop="static"
|
||||
>
|
||||
<Modal.Header>
|
||||
<button className="close" onClick={handleCloseModal}>
|
||||
<span aria-hidden="true">×</span>
|
||||
<span className="sr-only">{t('close')}</span>
|
||||
</button>
|
||||
<div className="modal-title">
|
||||
<h2>{t('customize_your_group_subscription')}</h2>
|
||||
<h3>
|
||||
<Trans
|
||||
i18nKey="save_x_percent_or_more"
|
||||
values={{
|
||||
percent: '30',
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<div className="container-fluid plans group-subscription-modal">
|
||||
<div className="row">
|
||||
<div className="col-md-6 text-center">
|
||||
<div className="circle circle-lg">
|
||||
<GroupPrice />
|
||||
</div>
|
||||
<p>{t('each_user_will_have_access_to')}:</p>
|
||||
<ul className="list-unstyled">
|
||||
<li className="list-item-with-margin-bottom">
|
||||
<strong>
|
||||
<GroupPlanCollaboratorCount
|
||||
planCode={groupPlanToChangeToCode}
|
||||
/>
|
||||
</strong>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{t('all_premium_features')}</strong>
|
||||
</li>
|
||||
<li>{t('sync_dropbox_github')}</li>
|
||||
<li>{t('full_doc_history')}</li>
|
||||
<li>{t('track_changes')}</li>
|
||||
<li>
|
||||
<span aria-hidden>+ {t('more').toLowerCase()}</span>
|
||||
<span className="sr-only">{t('plus_more')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<form className="form">
|
||||
<fieldset className="form-group">
|
||||
<legend className="legend-as-label">{t('plan')}</legend>
|
||||
{groupPlans.plans.map(option => (
|
||||
<label
|
||||
htmlFor={`plan-option-${option.code}`}
|
||||
key={option.code}
|
||||
className="group-plan-option"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="plan-code"
|
||||
value={option.code}
|
||||
id={`plan-option-${option.code}`}
|
||||
onChange={e =>
|
||||
setGroupPlanToChangeToCode(e.target.value)
|
||||
}
|
||||
checked={option.code === groupPlanToChangeToCode}
|
||||
/>
|
||||
<span>{option.display}</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="size">{t('number_of_users')}</label>
|
||||
<select
|
||||
name="size"
|
||||
id="size"
|
||||
className="form-control"
|
||||
value={groupPlanToChangeToSize}
|
||||
onChange={e => setGroupPlanToChangeToSize(e.target.value)}
|
||||
>
|
||||
{groupPlans.sizes.map(size => (
|
||||
<option key={`size-option-${size}`}>{size}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<strong>
|
||||
<Trans
|
||||
i18nKey="percent_discount_for_groups"
|
||||
values={{
|
||||
percent: educationalPercentDiscount,
|
||||
size: groupSizeForEducationalDiscount,
|
||||
}}
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<div className="form-group group-plan-option">
|
||||
<label htmlFor="usage">
|
||||
<input
|
||||
id="usage"
|
||||
type="checkbox"
|
||||
checked={groupPlanToChangeToUsage === 'educational'}
|
||||
onChange={e => {
|
||||
if (e.target.checked) {
|
||||
setGroupPlanToChangeToUsage('educational')
|
||||
} else {
|
||||
setGroupPlanToChangeToUsage('enterprise')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{t('license_for_educational_purposes')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 text-center">
|
||||
<div className="educational-discount-badge">
|
||||
{groupPlanToChangeToUsage === 'educational' && (
|
||||
<EducationDiscountAppliedOrNot
|
||||
groupSize={groupPlanToChangeToSize}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<div className="text-center">
|
||||
<p>
|
||||
<strong>{t('new_subscription_will_be_billed_immediately')}</strong>
|
||||
</p>
|
||||
<hr className="thin" />
|
||||
<button className="btn btn-primary btn-lg">{t('upgrade_now')}</button>
|
||||
<hr className="thin" />
|
||||
<button
|
||||
className="btn-inline-link"
|
||||
data-ol-open-contact-form-for-more-than-50-licenses
|
||||
>
|
||||
<Trans i18nKey="need_more_than_x_licenses" values={{ x: 50 }} />{' '}
|
||||
{t('please_get_in_touch')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
)
|
||||
}
|
|
@ -1,14 +1,15 @@
|
|||
import { useState } from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { postJSON } from '../../../../../../../infrastructure/fetch-json'
|
||||
import AccessibleModal from '../../../../../../../shared/components/accessible-modal'
|
||||
import getMeta from '../../../../../../../utils/meta'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||
import { subscriptionUrl } from '../../../../../data/subscription-url'
|
||||
import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
|
||||
import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
|
||||
import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
|
||||
import getMeta from '../../../../../../../../utils/meta'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
||||
import { subscriptionUrl } from '../../../../../../data/subscription-url'
|
||||
|
||||
export function ConfirmChangePlanModal() {
|
||||
const modalId = 'change-to-plan'
|
||||
const modalId: SubscriptionDashModalIds = 'change-to-plan'
|
||||
const [error, setError] = useState(false)
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const { t } = useTranslation()
|
|
@ -1,13 +1,14 @@
|
|||
import { useState } from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { postJSON } from '../../../../../../../infrastructure/fetch-json'
|
||||
import AccessibleModal from '../../../../../../../shared/components/accessible-modal'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../context/subscription-dashboard-context'
|
||||
import { cancelPendingSubscriptionChangeUrl } from '../../../../../data/subscription-url'
|
||||
import { SubscriptionDashModalIds } from '../../../../../../../../../../types/subscription/dashboard/modal-ids'
|
||||
import { postJSON } from '../../../../../../../../infrastructure/fetch-json'
|
||||
import AccessibleModal from '../../../../../../../../shared/components/accessible-modal'
|
||||
import { useSubscriptionDashboardContext } from '../../../../../../context/subscription-dashboard-context'
|
||||
import { cancelPendingSubscriptionChangeUrl } from '../../../../../../data/subscription-url'
|
||||
|
||||
export function KeepCurrentPlanModal() {
|
||||
const modalId = 'keep-current-plan'
|
||||
const modalId: SubscriptionDashModalIds = 'keep-current-plan'
|
||||
const [error, setError] = useState(false)
|
||||
const [inflight, setInflight] = useState(false)
|
||||
const { t } = useTranslation()
|
|
@ -17,22 +17,36 @@ import { Institution } from '../../../../../types/institution'
|
|||
import getMeta from '../../../utils/meta'
|
||||
import { loadDisplayPriceWithTaxPromise } from '../util/recurly-pricing'
|
||||
import { isRecurlyLoaded } from '../util/is-recurly-loaded'
|
||||
import { SubscriptionDashModalIds } from '../../../../../types/subscription/dashboard/modal-ids'
|
||||
|
||||
type SubscriptionDashboardContextValue = {
|
||||
groupPlanToChangeToCode?: string
|
||||
groupPlanToChangeToSize: string
|
||||
groupPlanToChangeToUsage?: string
|
||||
handleCloseModal: () => void
|
||||
handleOpenModal: (modalIdToOpen: string, planCode?: string) => void
|
||||
handleOpenModal: (
|
||||
modalIdToOpen: SubscriptionDashModalIds,
|
||||
planCode?: string
|
||||
) => void
|
||||
hasDisplayedSubscription: boolean
|
||||
institutionMemberships?: Institution[]
|
||||
managedGroupSubscriptions: ManagedGroupSubscription[]
|
||||
managedInstitutions: ManagedInstitution[]
|
||||
updateManagedInstitution: (institution: ManagedInstitution) => void
|
||||
modalIdShown?: string
|
||||
modalIdShown?: SubscriptionDashModalIds
|
||||
personalSubscription?: Subscription
|
||||
plans: Plan[]
|
||||
planCodeToChangeTo?: string
|
||||
queryingIndividualPlansData: boolean
|
||||
recurlyLoadError: boolean
|
||||
setModalIdShown: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
setGroupPlanToChangeToCode: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>
|
||||
setGroupPlanToChangeToSize: React.Dispatch<React.SetStateAction<string>>
|
||||
setGroupPlanToChangeToUsage: React.Dispatch<React.SetStateAction<string>>
|
||||
setModalIdShown: React.Dispatch<
|
||||
React.SetStateAction<SubscriptionDashModalIds | undefined>
|
||||
>
|
||||
setPlanCodeToChangeTo: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>
|
||||
|
@ -52,7 +66,9 @@ export function SubscriptionDashboardProvider({
|
|||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [modalIdShown, setModalIdShown] = useState<string | undefined>()
|
||||
const [modalIdShown, setModalIdShown] = useState<
|
||||
SubscriptionDashModalIds | undefined
|
||||
>()
|
||||
const [recurlyLoadError, setRecurlyLoadError] = useState(false)
|
||||
const [showCancellation, setShowCancellation] = useState(false)
|
||||
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
|
||||
|
@ -62,6 +78,12 @@ export function SubscriptionDashboardProvider({
|
|||
const [planCodeToChangeTo, setPlanCodeToChangeTo] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
const [groupPlanToChangeToSize, setGroupPlanToChangeToSize] = useState('10')
|
||||
const [groupPlanToChangeToCode, setGroupPlanToChangeToCode] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
const [groupPlanToChangeToUsage, setGroupPlanToChangeToUsage] =
|
||||
useState('enterprise')
|
||||
|
||||
const plansWithoutDisplayPrice = getMeta('ol-plans')
|
||||
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
||||
|
@ -124,7 +146,7 @@ export function SubscriptionDashboardProvider({
|
|||
[]
|
||||
)
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setModalIdShown('')
|
||||
setModalIdShown(undefined)
|
||||
setPlanCodeToChangeTo(undefined)
|
||||
}, [setModalIdShown, setPlanCodeToChangeTo])
|
||||
|
||||
|
@ -138,6 +160,9 @@ export function SubscriptionDashboardProvider({
|
|||
|
||||
const value = useMemo<SubscriptionDashboardContextValue>(
|
||||
() => ({
|
||||
groupPlanToChangeToCode,
|
||||
groupPlanToChangeToSize,
|
||||
groupPlanToChangeToUsage,
|
||||
handleCloseModal,
|
||||
handleOpenModal,
|
||||
hasDisplayedSubscription,
|
||||
|
@ -151,6 +176,9 @@ export function SubscriptionDashboardProvider({
|
|||
planCodeToChangeTo,
|
||||
queryingIndividualPlansData,
|
||||
recurlyLoadError,
|
||||
setGroupPlanToChangeToCode,
|
||||
setGroupPlanToChangeToSize,
|
||||
setGroupPlanToChangeToUsage,
|
||||
setModalIdShown,
|
||||
setPlanCodeToChangeTo,
|
||||
setRecurlyLoadError,
|
||||
|
@ -160,6 +188,9 @@ export function SubscriptionDashboardProvider({
|
|||
setShowChangePersonalPlan,
|
||||
}),
|
||||
[
|
||||
groupPlanToChangeToCode,
|
||||
groupPlanToChangeToSize,
|
||||
groupPlanToChangeToUsage,
|
||||
handleCloseModal,
|
||||
handleOpenModal,
|
||||
hasDisplayedSubscription,
|
||||
|
@ -173,6 +204,9 @@ export function SubscriptionDashboardProvider({
|
|||
planCodeToChangeTo,
|
||||
queryingIndividualPlansData,
|
||||
recurlyLoadError,
|
||||
setGroupPlanToChangeToCode,
|
||||
setGroupPlanToChangeToSize,
|
||||
setGroupPlanToChangeToUsage,
|
||||
setModalIdShown,
|
||||
setPlanCodeToChangeTo,
|
||||
setRecurlyLoadError,
|
||||
|
|
|
@ -32,6 +32,16 @@ label {
|
|||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
|
||||
// also update .legend-as-label if changes are made to label
|
||||
}
|
||||
|
||||
.legend-as-label {
|
||||
// display a legend like a label
|
||||
&:extend(label);
|
||||
font-size: @font-size-base;
|
||||
color: @text-color;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// Normalize form controls
|
||||
|
|
|
@ -48,3 +48,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-with-margin-bottom {
|
||||
margin-bottom: @line-height-computed;
|
||||
}
|
||||
|
|
|
@ -295,6 +295,7 @@
|
|||
"credit_card": "Credit Card",
|
||||
"credit_card_number": "Credit Card Number",
|
||||
"cs": "Czech",
|
||||
"currency": "Currency",
|
||||
"current_experiments": "Current Experiments",
|
||||
"current_file": "Current file",
|
||||
"current_password": "Current Password",
|
||||
|
@ -304,6 +305,7 @@
|
|||
"custom_resource_portal": "Custom resource portal",
|
||||
"custom_resource_portal_info": "You can have your own custom portal page on Overleaf. This is a great place for your users to find out more about Overleaf, access templates, FAQs and Help resources, and sign up to Overleaf.",
|
||||
"customize": "Customize",
|
||||
"customize_your_group_subscription": "Customize your group subscription",
|
||||
"customize_your_plan": "Customize your plan",
|
||||
"da": "Danish",
|
||||
"date": "Date",
|
||||
|
@ -387,6 +389,7 @@
|
|||
"dropbox_unlinked_premium_feature": "<0>Your Dropbox account has been unlinked</0> because Dropbox Sync is a premium feature that you had through an institutional license.",
|
||||
"duplicate_file": "Duplicate File",
|
||||
"duplicate_projects": "This user has projects with duplicate names",
|
||||
"each_user_will_have_access_to": "Each user will have access to",
|
||||
"ease_of_use": " Ease of Use",
|
||||
"easily_manage_your_project_files_everywhere": "Easily manage your project files, everywhere",
|
||||
"edit": "Edit",
|
||||
|
@ -400,6 +403,8 @@
|
|||
"editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
|
||||
"editor_resources": "Editor Resources",
|
||||
"editor_theme": "Editor theme",
|
||||
"educational_discount_for_groups_of_x_or_more": "The educational discount is available for groups of __size__ or more",
|
||||
"educational_percent_discount_applied": "__percent__% educational discount applied!",
|
||||
"email": "Email",
|
||||
"email_already_associated_with": "The <b>__email1__</b> email is already associated with the <b>__email2__</b> <b>__appName__</b> account.",
|
||||
"email_already_registered": "This email is already registered",
|
||||
|
@ -807,6 +812,7 @@
|
|||
"leave_projects": "Leave Projects",
|
||||
"let_us_know": "Let us know",
|
||||
"license": "License",
|
||||
"license_for_educational_purposes": "This license is for educational purposes (applies to students or faculty using __appName__ for teaching)",
|
||||
"limited_offer": "Limited offer",
|
||||
"line_height": "Line Height",
|
||||
"link": "Link",
|
||||
|
@ -930,6 +936,7 @@
|
|||
"navigation": "Navigation",
|
||||
"nearly_activated": "You’re one step away from activating your __appName__ account!",
|
||||
"need_anything_contact_us_at": "If there is anything you ever need please feel free to contact us directly at",
|
||||
"need_more_than_x_licenses": "Need more than __x__ licenses?",
|
||||
"need_to_add_new_primary_before_remove": "You’ll need to add a new primary email address before you can remove this one.",
|
||||
"need_to_leave": "Need to leave?",
|
||||
"need_to_upgrade_for_more_collabs": "You need to upgrade your account to add more collaborators",
|
||||
|
@ -940,6 +947,7 @@
|
|||
"new_password": "New Password",
|
||||
"new_project": "New Project",
|
||||
"new_snippet_project": "Untitled",
|
||||
"new_subscription_will_be_billed_immediately": "Your new subscription will be billed immediately to your current payment method.",
|
||||
"new_to_latex_look_at": "New to LaTeX? Start by having a look at our",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletter-accept": "I’d like emails about product offers and company news and events.",
|
||||
|
@ -1062,10 +1070,12 @@
|
|||
"pdf_viewer_error": "There was a problem displaying the PDF for this project.",
|
||||
"pending": "Pending",
|
||||
"pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__</0> additional license(s) for a total of <1>__pendingTotalLicenses__</1> licenses.",
|
||||
"percent_discount_for_groups": "__appName__ offers a __percent__% educational discount for groups of __size__ or more.",
|
||||
"personal": "Personal",
|
||||
"personalized_onboarding": "Personalized onboarding",
|
||||
"personalized_onboarding_info": "We’ll help you get everything set up and then we’re here to answer questions from your users about the platform, templates or LaTeX!",
|
||||
"pl": "Polish",
|
||||
"plan": "Plan",
|
||||
"plan_tooltip": "You’re on the __plan__ plan. Click to find out how to make the most of your Overleaf premium features!",
|
||||
"planned_maintenance": "Planned Maintenance",
|
||||
"plans_amper_pricing": "Plans & Pricing",
|
||||
|
@ -1079,6 +1089,7 @@
|
|||
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
|
||||
"please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
|
||||
"please_enter_email": "Please enter your email address",
|
||||
"please_get_in_touch": "Please get in touch",
|
||||
"please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",
|
||||
"please_reconfirm_institutional_email": "Please take a moment to confirm your institutional email address or <0>remove it</0> from your account.",
|
||||
"please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
|
||||
|
@ -1089,6 +1100,7 @@
|
|||
"please_select_an_output_file": "Please Select an Output File",
|
||||
"please_set_a_password": "Please set a password",
|
||||
"please_set_main_file": "Please choose the main file for this project in the project menu. ",
|
||||
"plus_more": "plus more",
|
||||
"plus_upgraded_accounts_receive": "Plus with an upgraded account you get",
|
||||
"portal_add_affiliation_to_join": "It looks like you are already logged in to __appName__! If you have a __portalTitle__ email you can add it now.",
|
||||
"position": "Position",
|
||||
|
@ -1262,6 +1274,7 @@
|
|||
"save_or_cancel-cancel": "Cancel",
|
||||
"save_or_cancel-or": "or",
|
||||
"save_or_cancel-save": "Save",
|
||||
"save_x_percent_or_more": "Save __percent__% or more",
|
||||
"saved_bibtex_appended_to_galileo_bib": "The <strong>__citeKey__</strong> cite key has been added to the <strong>__galileoBib__</strong> file in your project.",
|
||||
"saved_bibtex_to_new_galileo_bib": "The <strong>__citeKey__</strong> cite key has been copied into a new <strong>__galileoBib__</strong> file in your project. Include this file in your project using the appropriate method for your citation package.",
|
||||
"saving": "Saving",
|
||||
|
@ -1658,6 +1671,8 @@
|
|||
"x_price_for_first_month": "<0>__price__</0> for your first month",
|
||||
"x_price_for_first_year": "<0>__price__</0> for your first year",
|
||||
"x_price_for_y_months": "<0>__price__</0> for your first __discountMonths__ months",
|
||||
"x_price_per_user": "__price__ per user",
|
||||
"x_price_per_year": "__price__ per year",
|
||||
"year": "year",
|
||||
"yes_move_me_to_personal_plan": "Yes, move me to the Personal plan",
|
||||
"yes_that_is_correct": "Yes, that’s correct",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { expect } from 'chai'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import { ChangePlan } from '../../../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan'
|
||||
import { plans } from '../../../../../fixtures/plans'
|
||||
import { groupPlans, plans } from '../../../../../fixtures/plans'
|
||||
import {
|
||||
annualActiveSubscription,
|
||||
pendingSubscriptionChange,
|
||||
|
@ -322,4 +322,104 @@ describe('<ChangePlan />', function () {
|
|||
).to.not.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('Change to group plan modal', function () {
|
||||
const standardPlanCollaboratorText = '10 collaborators per project'
|
||||
const professionalPlanCollaboratorText = 'Unlimited collaborators'
|
||||
it('open group plan modal "Change to a group plan" clicked', async function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
const buttonGroupModal = await screen.findByRole('button', {
|
||||
name: 'Change to a group plan',
|
||||
})
|
||||
fireEvent.click(buttonGroupModal)
|
||||
|
||||
const modal = await screen.findByRole('dialog')
|
||||
|
||||
within(modal).getByText('Customize your group subscription')
|
||||
within(modal).getByText('Save 30% or more')
|
||||
within(modal).getByText('Each user will have access to:')
|
||||
within(modal).getByText('All premium features')
|
||||
within(modal).getByText('Sync with Dropbox and GitHub')
|
||||
within(modal).getByText('Full document history')
|
||||
within(modal).getByText('plus more')
|
||||
|
||||
within(modal).getByText(standardPlanCollaboratorText)
|
||||
expect(within(modal).queryByText(professionalPlanCollaboratorText)).to.be
|
||||
.null
|
||||
|
||||
const plans = within(modal).getByRole('group')
|
||||
const planOptions = within(plans).getAllByRole('radio')
|
||||
expect(planOptions.length).to.equal(groupPlans.plans.length)
|
||||
|
||||
const sizeSelect = within(modal).getByRole('combobox')
|
||||
const sizeOption = within(sizeSelect).getAllByRole('option')
|
||||
expect(sizeOption.length).to.equal(groupPlans.sizes.length)
|
||||
within(modal).getByText(
|
||||
'Overleaf offers a 40% educational discount for groups of 10 or more.'
|
||||
)
|
||||
|
||||
within(modal).getByRole('checkbox')
|
||||
within(modal).getByText(
|
||||
'This license is for educational purposes (applies to students or faculty using Overleaf for teaching)'
|
||||
)
|
||||
|
||||
within(modal).getByText(
|
||||
'Your new subscription will be billed immediately to your current payment method.'
|
||||
)
|
||||
|
||||
within(modal).getByRole('button', { name: 'Upgrade Now' })
|
||||
|
||||
within(modal).getByRole('button', {
|
||||
name: 'Need more than 50 licenses? Please get in touch',
|
||||
})
|
||||
})
|
||||
|
||||
it('changes the collaborator count when the plan changes', async function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
const buttonGroupModal = await screen.findByRole('button', {
|
||||
name: 'Change to a group plan',
|
||||
})
|
||||
fireEvent.click(buttonGroupModal)
|
||||
|
||||
const modal = await screen.findByRole('dialog')
|
||||
const professionalPlanOption =
|
||||
within(modal).getByLabelText('Professional')
|
||||
fireEvent.click(professionalPlanOption)
|
||||
|
||||
within(modal).getByText(professionalPlanCollaboratorText)
|
||||
expect(within(modal).queryByText(standardPlanCollaboratorText)).to.be.null
|
||||
})
|
||||
|
||||
it('shows educational discount applied when input checked', async function () {
|
||||
const discountAppliedText = '40% educational discount applied!'
|
||||
const discountNotAppliedText =
|
||||
'The educational discount is available for groups of 10 or more'
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
const buttonGroupModal = await screen.findByRole('button', {
|
||||
name: 'Change to a group plan',
|
||||
})
|
||||
fireEvent.click(buttonGroupModal)
|
||||
|
||||
const modal = await screen.findByRole('dialog')
|
||||
|
||||
const educationInput = within(modal).getByLabelText(
|
||||
'This license is for educational purposes (applies to students or faculty using Overleaf for teaching)'
|
||||
)
|
||||
fireEvent.click(educationInput)
|
||||
within(modal).getByText(discountAppliedText)
|
||||
expect(within(modal).queryByText(discountNotAppliedText)).to.be.null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GroupPlans } from '../../../../../types/subscription/dashboard/group-plans'
|
||||
import { Plan } from '../../../../../types/subscription/plan'
|
||||
|
||||
const features = {
|
||||
|
@ -213,3 +214,17 @@ export const plans = [
|
|||
...individualMonthlyPlans,
|
||||
...individualAnnualPlans,
|
||||
]
|
||||
|
||||
export const groupPlans: GroupPlans = {
|
||||
plans: [
|
||||
{
|
||||
display: 'Standard',
|
||||
code: 'collaborator',
|
||||
},
|
||||
{
|
||||
display: 'Professional',
|
||||
code: 'professional',
|
||||
},
|
||||
],
|
||||
sizes: ['2', '3', '4', '5', '10', '20', '50'],
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ActiveSubscription } from '../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
|
||||
import { Subscription } from '../../../../../types/subscription/dashboard/subscription'
|
||||
import { plans } from '../fixtures/plans'
|
||||
import { groupPlans, plans } from '../fixtures/plans'
|
||||
import { renderWithSubscriptionDashContext } from './render-with-subscription-dash-context'
|
||||
|
||||
export function renderActiveSubscription(
|
||||
|
@ -11,6 +11,10 @@ export function renderActiveSubscription(
|
|||
metaTags: [
|
||||
...tags,
|
||||
{ name: 'ol-plans', value: plans },
|
||||
{
|
||||
name: 'ol-groupPlans',
|
||||
value: groupPlans,
|
||||
},
|
||||
{ name: 'ol-subscription', value: subscription },
|
||||
{
|
||||
name: 'ol-recommendedCurrency',
|
||||
|
|
7
services/web/types/subscription/dashboard/group-plans.ts
Normal file
7
services/web/types/subscription/dashboard/group-plans.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type GroupPlans = {
|
||||
plans: {
|
||||
display: string
|
||||
code: string
|
||||
}[]
|
||||
sizes: string[]
|
||||
}
|
4
services/web/types/subscription/dashboard/modal-ids.ts
Normal file
4
services/web/types/subscription/dashboard/modal-ids.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type SubscriptionDashModalIds =
|
||||
| 'change-to-plan'
|
||||
| 'change-to-group'
|
||||
| 'keep-current-plan'
|
Loading…
Add table
Reference in a new issue