diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js
index 8b6daf60da..10b38e537d 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionController.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionController.js
@@ -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)
}
diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug
index 8403bd773e..61513faf09 100644
--- a/services/web/app/views/subscriptions/dashboard-react.pug
+++ b/services/web/app/views/subscriptions/dashboard-react.pug
@@ -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
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 1b5bd173ea..fc82cffcff 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -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": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx
index adeb755ed8..0bb7678a49 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-plan.tsx
@@ -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() {
+
>
)
}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx
index 098542d022..97b1ad275b 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/change-to-group-plan.tsx
@@ -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 (
<>
{t('looking_multiple_licenses')}
- {/* todo: if/else isValidCurrencyForUpgrade and modal */}
+ {t('reduce_costs_group_licenses')}
+
+
>
)
}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx
new file mode 100644
index 0000000000..0742902ea8
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx
@@ -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 (
+ <>
+
+ >
+ )
+ } else if (planCode === 'professional') {
+ return <>{t('unlimited_collabs')}>
+ }
+ return null
+}
+
+function EducationDiscountAppliedOrNot({ groupSize }: { groupSize: string }) {
+ const size = parseInt(groupSize)
+ if (size >= groupSizeForEducationalDiscount) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
+function GroupPrice() {
+ const { t } = useTranslation()
+ return (
+ <>
+
+ X / {t('year')}
+
+
+ {/* TODO: price */}
+
+
+
+
+ {/* TODO: price */}
+
+
+ >
+ )
+}
+
+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 (
+
+
+
+
+
{t('customize_your_group_subscription')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{t('each_user_will_have_access_to')}:
+
+ -
+
+
+
+
+ -
+ {t('all_premium_features')}
+
+ - {t('sync_dropbox_github')}
+ - {t('full_doc_history')}
+ - {t('track_changes')}
+ -
+ + {t('more').toLowerCase()}
+ {t('plus_more')}
+
+
+
+
+
+
+
+
+
+ {groupPlanToChangeToUsage === 'educational' && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {t('new_subscription_will_be_billed_immediately')}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/confirm-change-plan-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx
similarity index 83%
rename from services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/confirm-change-plan-modal.tsx
rename to services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx
index 6818c15e4b..060faa9abd 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/confirm-change-plan-modal.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/confirm-change-plan-modal.tsx
@@ -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()
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/keep-current-plan-modal.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx
similarity index 84%
rename from services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/keep-current-plan-modal.tsx
rename to services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx
index 46e30701b7..a32a8d4aaf 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/keep-current-plan-modal.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/keep-current-plan-modal.tsx
@@ -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()
diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
index 75c828cb54..967b6adacc 100644
--- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
+++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
@@ -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>
+ setGroupPlanToChangeToCode: React.Dispatch<
+ React.SetStateAction
+ >
+ setGroupPlanToChangeToSize: React.Dispatch>
+ setGroupPlanToChangeToUsage: React.Dispatch>
+ setModalIdShown: React.Dispatch<
+ React.SetStateAction
+ >
setPlanCodeToChangeTo: React.Dispatch<
React.SetStateAction
>
@@ -52,7 +66,9 @@ export function SubscriptionDashboardProvider({
}: {
children: ReactNode
}) {
- const [modalIdShown, setModalIdShown] = useState()
+ 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(
() => ({
+ 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,
diff --git a/services/web/frontend/stylesheets/components/forms.less b/services/web/frontend/stylesheets/components/forms.less
index 6f846d467d..4ff26219a5 100755
--- a/services/web/frontend/stylesheets/components/forms.less
+++ b/services/web/frontend/stylesheets/components/forms.less
@@ -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
diff --git a/services/web/frontend/stylesheets/components/lists.less b/services/web/frontend/stylesheets/components/lists.less
index 755941664c..9ed8c726bc 100644
--- a/services/web/frontend/stylesheets/components/lists.less
+++ b/services/web/frontend/stylesheets/components/lists.less
@@ -48,3 +48,7 @@
}
}
}
+
+.list-item-with-margin-bottom {
+ margin-bottom: @line-height-computed;
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 054e69a2e9..b50a7766c9 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -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 unlinked0> 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 __email1__ email is already associated with the __email2__ __appName__ 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 it0> 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 __citeKey__ cite key has been added to the __galileoBib__ file in your project.",
"saved_bibtex_to_new_galileo_bib": "The __citeKey__ cite key has been copied into a new __galileoBib__ 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",
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
index f0767df4e6..9797200b87 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx
@@ -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('', 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
+ })
+ })
})
diff --git a/services/web/test/frontend/features/subscription/fixtures/plans.tsx b/services/web/test/frontend/features/subscription/fixtures/plans.tsx
index 6a455a5a20..ebf326f75a 100644
--- a/services/web/test/frontend/features/subscription/fixtures/plans.tsx
+++ b/services/web/test/frontend/features/subscription/fixtures/plans.tsx
@@ -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'],
+}
diff --git a/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx b/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
index 4a1d6f9de4..9756105b2d 100644
--- a/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
+++ b/services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx
@@ -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',
diff --git a/services/web/types/subscription/dashboard/group-plans.ts b/services/web/types/subscription/dashboard/group-plans.ts
new file mode 100644
index 0000000000..de93fe6a6d
--- /dev/null
+++ b/services/web/types/subscription/dashboard/group-plans.ts
@@ -0,0 +1,7 @@
+export type GroupPlans = {
+ plans: {
+ display: string
+ code: string
+ }[]
+ sizes: string[]
+}
diff --git a/services/web/types/subscription/dashboard/modal-ids.ts b/services/web/types/subscription/dashboard/modal-ids.ts
new file mode 100644
index 0000000000..cfe06b7ebd
--- /dev/null
+++ b/services/web/types/subscription/dashboard/modal-ids.ts
@@ -0,0 +1,4 @@
+export type SubscriptionDashModalIds =
+ | 'change-to-plan'
+ | 'change-to-group'
+ | 'keep-current-plan'