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:
Jessica Lawshe 2023-02-21 09:24:28 -06:00 committed by Copybot
parent b62cb86bf8
commit b2a10260be
17 changed files with 529 additions and 24 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,3 +48,7 @@
}
}
}
.list-item-with-margin-bottom {
margin-bottom: @line-height-computed;
}

View file

@ -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": "Youre 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": "Youll 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": "Id 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": "Well help you get everything set up and then were here to answer questions from your users about the platform, templates or LaTeX!",
"pl": "Polish",
"plan": "Plan",
"plan_tooltip": "Youre 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 &amp; 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, thats correct",

View file

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

View file

@ -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'],
}

View file

@ -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',

View file

@ -0,0 +1,7 @@
export type GroupPlans = {
plans: {
display: string
code: string
}[]
sizes: string[]
}

View file

@ -0,0 +1,4 @@
export type SubscriptionDashModalIds =
| 'change-to-plan'
| 'change-to-group'
| 'keep-current-plan'