From a6e3209e7b3f79c025e00f83a57dd301c18edc0e Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Tue, 21 Feb 2023 09:24:28 -0600 Subject: [PATCH] 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 --- .../Subscription/SubscriptionController.js | 14 +- .../views/subscriptions/dashboard-react.pug | 1 - .../web/frontend/extracted-translations.json | 18 ++ .../states/active/change-plan/change-plan.tsx | 6 +- .../change-plan/change-to-group-plan.tsx | 13 +- .../modals/change-to-group-modal.tsx | 270 ++++++++++++++++++ .../confirm-change-plan-modal.tsx | 13 +- .../{ => modals}/keep-current-plan-modal.tsx | 11 +- .../subscription-dashboard-context.tsx | 44 ++- .../stylesheets/components/forms.less | 10 + .../stylesheets/components/lists.less | 4 + services/web/locales/en.json | 15 + .../active/change-plan/change-plan.test.tsx | 102 ++++++- .../features/subscription/fixtures/plans.tsx | 15 + .../helpers/render-active-subscription.tsx | 6 +- .../subscription/dashboard/group-plans.ts | 7 + .../types/subscription/dashboard/modal-ids.ts | 4 + 17 files changed, 529 insertions(+), 24 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/modals/change-to-group-modal.tsx rename services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/{ => modals}/confirm-change-plan-modal.tsx (83%) rename services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan/{ => modals}/keep-current-plan-modal.tsx (84%) create mode 100644 services/web/types/subscription/dashboard/group-plans.ts create mode 100644 services/web/types/subscription/dashboard/modal-ids.ts 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')} +
  • +
+
+ +
+
+
+ {t('plan')} + {groupPlans.plans.map(option => ( + + ))} +
+ +
+ + +
+ +
+ + + +
+ +
+ +
+
+
+
+
+
+
+ {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 unlinked 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)", "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__ additional license(s) for a total of <1>__pendingTotalLicenses__ 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 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__ for your first month", "x_price_for_first_year": "<0>__price__ for your first year", "x_price_for_y_months": "<0>__price__ 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'