From 3351ac3dc8b08061fae8bc9c19f6ebcd9eb45be8 Mon Sep 17 00:00:00 2001 From: Liangjun Song <146005915+adai26@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:32:29 +0100 Subject: [PATCH] Merge pull request #22184 from overleaf/ls-group-plan-upgrade-page Group plan upgrade page GitOrigin-RevId: 6c99173c013d84943276dbd43f468026c4d44558 --- .../Features/Subscription/RecurlyEntities.js | 24 ++++ .../SubscriptionGroupController.mjs | 35 ++++- .../Subscription/SubscriptionGroupHandler.js | 53 ++++++++ .../Subscription/SubscriptionRouter.mjs | 15 +++ .../upgrade-group-subscription-react.pug | 15 +++ .../web/frontend/extracted-translations.json | 13 ++ .../components/request-status.tsx | 6 +- .../upgrade-subscription-plan-details.tsx | 67 ++++++++++ .../upgrade-subscription-upgrade-summary.tsx | 88 +++++++++++++ .../upgrade-subscription.tsx | 122 ++++++++++++++++++ .../upgrade-group-subscription.tsx | 8 ++ .../bootstrap-5/pages/subscription.scss | 28 ++++ services/web/locales/en.json | 10 ++ .../SubscriptionGroupControllerTests.mjs | 11 ++ .../subscription-change-preview.ts | 8 ++ 15 files changed, 500 insertions(+), 3 deletions(-) create mode 100644 services/web/app/views/subscriptions/upgrade-group-subscription-react.pug create mode 100644 services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-plan-details.tsx create mode 100644 services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription-upgrade-summary.tsx create mode 100644 services/web/frontend/js/features/group-management/components/upgrade-subscription/upgrade-subscription.tsx create mode 100644 services/web/frontend/js/pages/user/subscription/group-management/upgrade-group-subscription.tsx diff --git a/services/web/app/src/Features/Subscription/RecurlyEntities.js b/services/web/app/src/Features/Subscription/RecurlyEntities.js index dbbcdc3356..c21eb62634 100644 --- a/services/web/app/src/Features/Subscription/RecurlyEntities.js +++ b/services/web/app/src/Features/Subscription/RecurlyEntities.js @@ -216,6 +216,30 @@ class RecurlySubscription { addOnUpdates, }) } + + /** + * Upgrade group plan with the plan code provided + * + * @param {string} newPlanCode + * @return {RecurlySubscriptionChangeRequest} + */ + getRequestForFlexibleLicensingGroupPlanUpgrade(newPlanCode) { + // Ensure all the existing add-ons are added to the new plan + const existingAddOns = this.addOns.map( + addOn => + new RecurlySubscriptionAddOnUpdate({ + code: addOn.code, + quantity: addOn.quantity, + }) + ) + + return new RecurlySubscriptionChangeRequest({ + subscription: this, + timeframe: 'now', + addOnUpdates: existingAddOns, + planCode: newPlanCode, + }) + } } /** diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index 33792bb184..ff7fad8d3e 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -12,9 +12,10 @@ import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import ErrorController from '../Errors/ErrorController.js' import SalesContactFormController from '../../../../modules/cms/app/src/controllers/SalesContactFormController.mjs' import UserGetter from '../User/UserGetter.js' +import { Subscription } from '../../models/Subscription.js' /** - * @import { Subscription } from "../../../../types/subscription/dashboard/subscription" + * @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js" */ /** @@ -220,6 +221,36 @@ async function flexibleLicensingSplitTest(req, res, next) { next() } +async function subscriptionUpgradePage(req, res) { + try { + const userId = SessionManager.getLoggedInUserId(req.session) + const changePreview = + await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId) + const olSubscription = await Subscription.findOne({ + admin_id: userId, + }).exec() + res.render('subscriptions/upgrade-group-subscription-react', { + changePreview, + totalLicenses: olSubscription.membersLimit, + groupName: olSubscription.teamName, + }) + } catch (error) { + logger.err({ error }, 'error loading upgrade subscription page') + return res.render('/user/subscription') + } +} + +async function upgradeSubscription(req, res) { + try { + const userId = SessionManager.getLoggedInUserId(req.session) + await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId) + return res.sendStatus(200) + } catch (error) { + logger.err({ error }, 'error trying to upgrade subscription') + return res.sendStatus(500) + } +} + export default { removeUserFromGroup: expressify(removeUserFromGroup), removeSelfFromGroup: expressify(removeSelfFromGroup), @@ -232,4 +263,6 @@ export default { createAddSeatsSubscriptionChange: expressify( createAddSeatsSubscriptionChange ), + subscriptionUpgradePage: expressify(subscriptionUpgradePage), + upgradeSubscription: expressify(upgradeSubscription), } diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js index 42c79279ca..087e581c33 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js @@ -7,6 +7,11 @@ const SessionManager = require('../Authentication/SessionManager') const RecurlyClient = require('./RecurlyClient') const PlansLocator = require('./PlansLocator') +const PLAN_UPGRADE_MAP = { + group_collaborator: 'group_professional', + group_collaborator_educational: 'group_professional_educational', +} + async function removeUserFromGroup(subscriptionId, userIdToRemove) { await SubscriptionUpdater.promises.removeUserFromGroup( subscriptionId, @@ -140,12 +145,58 @@ async function createAddSeatsSubscriptionChange(req) { return { adding: req.body.adding } } +async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) { + if (!Object.keys(PLAN_UPGRADE_MAP).includes(subscription.planCode)) { + throw new Error('Not eligible for group plan upgrade') + } + + return PLAN_UPGRADE_MAP[subscription.planCode] +} + +async function _getGroupPlanUpgradeChangeRequest(ownerId) { + const olSubscription = + await SubscriptionLocator.promises.getUsersSubscription(ownerId) + + const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription) + + const recurlySubscription = await RecurlyClient.promises.getSubscription( + olSubscription.recurlySubscription_id + ) + return recurlySubscription.getRequestForFlexibleLicensingGroupPlanUpgrade( + newPlanCode + ) +} + +async function getGroupPlanUpgradePreview(ownerId) { + const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId) + const subscriptionChange = + await RecurlyClient.promises.previewSubscriptionChange(changeRequest) + const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId) + return SubscriptionController.makeChangePreview( + { + type: 'group-plan-upgrade', + prevPlan: { + name: subscriptionChange.subscription.planName, + }, + }, + subscriptionChange, + paymentMethod + ) +} + +async function upgradeGroupPlan(ownerId) { + const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId) + await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest) +} + module.exports = { removeUserFromGroup: callbackify(removeUserFromGroup), replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups), ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled), getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup), isUserPartOfGroup: callbackify(isUserPartOfGroup), + getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview), + upgradeGroupPlan: callbackify(upgradeGroupPlan), promises: { removeUserFromGroup, replaceUserReferencesInGroups, @@ -155,5 +206,7 @@ module.exports = { getUsersGroupSubscriptionDetails, previewAddSeatsSubscriptionChange, createAddSeatsSubscriptionChange, + getGroupPlanUpgradePreview, + upgradeGroupPlan, }, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs b/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs index e186aa710f..f5ebb9de92 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.mjs @@ -108,6 +108,21 @@ export default { SubscriptionGroupController.submitForm ) + webRouter.get( + '/user/subscription/group/upgrade-subscription', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit(subscriptionRateLimiter), + SubscriptionGroupController.flexibleLicensingSplitTest, + SubscriptionGroupController.subscriptionUpgradePage + ) + + webRouter.post( + '/user/subscription/group/upgrade-subscription', + AuthenticationController.requireLogin(), + RateLimiterMiddleware.rateLimit(subscriptionRateLimiter), + SubscriptionGroupController.upgradeSubscription + ) + // Team invites webRouter.get( '/subscription/invites/:token/', diff --git a/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug new file mode 100644 index 0000000000..4ac2999ee5 --- /dev/null +++ b/services/web/app/views/subscriptions/upgrade-group-subscription-react.pug @@ -0,0 +1,15 @@ +extends ../layout-marketing + +block vars + - bootstrap5PageStatus = 'enabled' // Enforce BS5 version + +block entrypointVar + - entrypoint = 'pages/user/subscription/group-management/upgrade-group-subscription' + +block append meta + meta(name="ol-subscriptionChangePreview" data-type="json" content=changePreview) + meta(name="ol-totalLicenses", data-type="number", content=totalLicenses) + meta(name="ol-groupName", data-type="string", content=groupName) + +block content + main.content.content-alt#upgrade-group-subscription-root diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6d8da57d3b..9c48a99cdc 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -75,6 +75,7 @@ "add_more_managers": "", "add_more_members": "", "add_more_users": "", + "add_more_users_to_my_plan": "", "add_new_email": "", "add_ons_are": "", "add_or_remove_project_from_tag": "", @@ -108,6 +109,7 @@ "ai_feedback_there_was_no_code_fix_suggested": "", "alignment": "", "all_borders": "", + "all_features_in_group_standard_plus": "", "all_premium_features": "", "all_premium_features_including": "", "all_projects": "", @@ -159,6 +161,7 @@ "beta_program_benefits": "", "beta_program_not_participating": "", "better_bibliographies": "", + "billed_yearly": "", "binary_history_error": "", "blank_project": "", "blocked_filename": "", @@ -619,6 +622,7 @@ "group_libraries": "", "group_managed_by_group_administrator": "", "group_plan_tooltip": "", + "group_plan_upgrade_description": "", "group_plan_with_name_tooltip": "", "group_sso_configuration_idp_metadata": "", "group_sso_configure_service_provider_in_idp": "", @@ -896,6 +900,7 @@ "manage_sessions": "", "manage_subscription": "", "managed": "", + "managed_user_accounts": "", "managed_user_invite_has_been_sent_to_email": "", "managed_users": "", "managed_users_explanation": "", @@ -1076,6 +1081,7 @@ "pending_additional_licenses": "", "pending_addon_cancellation": "", "pending_invite": "", + "per_user": "", "percent_discount_for_groups": "", "percent_is_the_percentage_of_the_line_width": "", "permanently_disables_the_preview": "", @@ -1723,6 +1729,7 @@ "university": "", "university_school": "", "unknown": "", + "unlimited_collaborators_per_project": "", "unlimited_collabs": "", "unlimited_projects": "", "unlink": "", @@ -1760,10 +1767,12 @@ "upgrade_for_12x_more_compile_time": "", "upgrade_my_plan": "", "upgrade_now": "", + "upgrade_summary": "", "upgrade_to_add_more_editors": "", "upgrade_to_add_more_editors_and_access_collaboration_features": "", "upgrade_to_get_feature": "", "upgrade_to_track_changes": "", + "upgrade_your_subscription": "", "upload": "", "upload_from_computer": "", "upload_project": "", @@ -1784,6 +1793,7 @@ "user_first_name_attribute": "", "user_last_name_attribute": "", "user_sessions": "", + "users": "", "using_latex": "", "using_premium_features": "", "using_the_overleaf_editor": "", @@ -1830,6 +1840,7 @@ "we_logged_you_in": "", "we_sent_new_code": "", "we_will_charge_you_now_for_the_cost_of_your_additional_users_based_on_remaining_months": "", + "we_will_charge_you_now_for_your_new_plan_based_on_the_remaining_months_of_your_current_subscription": "", "webinars": "", "website_status": "", "wed_love_you_to_stay": "", @@ -1901,6 +1912,7 @@ "you_have_been_invited_to_transfer_management_of_your_account_to": "", "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "", "you_have_x_users_and_your_plan_supports_up_to_y": "", + "you_have_x_users_on_your_subscription": "", "you_need_to_configure_your_sso_settings": "", "you_will_be_able_to_reassign_subscription": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "", @@ -1949,6 +1961,7 @@ "youve_added_x_more_users_to_your_subscription_invite_people": "", "youve_lost_edit_access": "", "youve_unlinked_all_users": "", + "youve_upgraded_your_plan": "", "zoom_in": "", "zoom_out": "", "zoom_to": "", diff --git a/services/web/frontend/js/features/group-management/components/request-status.tsx b/services/web/frontend/js/features/group-management/components/request-status.tsx index ca581949eb..e7bf3d9cff 100644 --- a/services/web/frontend/js/features/group-management/components/request-status.tsx +++ b/services/web/frontend/js/features/group-management/components/request-status.tsx @@ -9,7 +9,7 @@ import classnames from 'classnames' type RequestStatusProps = { icon: string title: string - content: React.ReactNode + content?: React.ReactNode variant?: 'primary' | 'danger' } @@ -44,7 +44,9 @@ function RequestStatus({ icon, title, content, variant }: RequestStatusProps) {

{title}

-
{content}
+ {content && ( +
{content}
+ )}
+ +
+ + + + + + ) +} + +export default UpgradeSubscription diff --git a/services/web/frontend/js/pages/user/subscription/group-management/upgrade-group-subscription.tsx b/services/web/frontend/js/pages/user/subscription/group-management/upgrade-group-subscription.tsx new file mode 100644 index 0000000000..abed1387d2 --- /dev/null +++ b/services/web/frontend/js/pages/user/subscription/group-management/upgrade-group-subscription.tsx @@ -0,0 +1,8 @@ +import '../base' +import ReactDOM from 'react-dom' +import UpgradeSubscription from '@/features/group-management/components/upgrade-subscription/upgrade-subscription' + +const element = document.getElementById('upgrade-group-subscription-root') +if (element) { + ReactDOM.render(, element) +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss index f8b065743a..2c6f49689d 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/subscription.scss @@ -382,3 +382,31 @@ height: 400px; } } + +.group-subscription-upgrade-features { + .material-symbols { + @include body-sm; + + vertical-align: top; + } + + .per-user-price { + @include heading-lg; + + color: var(--content-primary); + } + + .per-user-price-text { + @include body-xs; + } + + .feature-list-item { + @include body-sm; + } +} + +.group-subscription-upgrade-card { + .title { + @include body-lg; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index f060f2f4f0..c5dbd30031 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -87,6 +87,7 @@ "add_more_managers": "Add more managers", "add_more_members": "Add more members", "add_more_users": "Add more users", + "add_more_users_to_my_plan": "Add more users to my plan", "add_new_email": "Add new email", "add_ons_are": "Add-ons: __addOnName__", "add_or_remove_project_from_tag": "Add or remove project from tag __tagName__", @@ -128,6 +129,7 @@ "alignment": "Alignment", "all": "All", "all_borders": "All borders", + "all_features_in_group_standard_plus": "All features in Group Standard, plus:", "all_our_group_plans_offer_educational_discount": "All of our <0>group plans offer an <1>educational discount for students and faculty", "all_premium_features": "All premium features", "all_premium_features_including": "All premium features, including:", @@ -213,6 +215,7 @@ "beta_program_opt_out_action": "Opt-Out of Beta Program", "better_bibliographies": "Better bibliographies", "bibliographies": "Bibliographies", + "billed_yearly": "billed yearly", "billing_period_sentence_case": "Billing period", "binary_history_error": "Preview not available for this file type", "blank_project": "Blank Project", @@ -888,6 +891,7 @@ "group_members_get_access_to_info": "These features are available only to group members (subscribers).", "group_plan_admins_can_easily_add_and_remove_users_from_a_group": "Group plan admins can easily add and remove users from a group. For site-wide plans, users are automatically upgraded when they register or add their email address to Overleaf (domain-based enrollment or SSO).", "group_plan_tooltip": "You are on the __plan__ plan as a member of a group subscription. Click to find out how to make the most of your Overleaf premium features.", + "group_plan_upgrade_description": "You’re on the <0>__currentPlan__ plan and you’re upgrading to the <0>__nextPlan__ plan. If you’re interested in a site-wide Overleaf Commons plan please <1>get in touch.", "group_plan_with_name_tooltip": "You are on the __plan__ plan as a member of a group subscription, __groupName__. Click to find out how to make the most of your Overleaf premium features.", "group_plans": "Group Plans", "group_professional": "Group Professional", @@ -2392,10 +2396,12 @@ "upgrade_for_12x_more_compile_time": "Upgrade to get 12x more compile time", "upgrade_my_plan": "Upgrade my plan", "upgrade_now": "Upgrade now", + "upgrade_summary": "Upgrade summary", "upgrade_to_add_more_editors": "Upgrade to add more editors to your project", "upgrade_to_add_more_editors_and_access_collaboration_features": "Upgrade to add more editors and access collaboration features like track changes and full project history.", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_track_changes": "Upgrade to track changes", + "upgrade_your_subscription": "Upgrade your subscription", "upload": "Upload", "upload_failed": "Upload failed", "upload_from_computer": "Upload from computer", @@ -2427,6 +2433,7 @@ "user_not_found": "User not found", "user_sessions": "User Sessions", "user_wants_you_to_see_project": "__username__ would like you to join __projectname__", + "users": "users", "using_latex": "Using LaTeX", "using_premium_features": "Using premium features", "using_the_overleaf_editor": "Using the __appName__ Editor", @@ -2477,6 +2484,7 @@ "we_may_also_contact_you_from_time_to_time_by_email_with_a_survey": "<0>We may also contact you from time to time by email with a survey, or to see if you would like to participate in other user research initiatives", "we_sent_new_code": "We’ve sent a new code. If it doesn’t arrive, make sure to check your spam and any promotions folders.", "we_will_charge_you_now_for_the_cost_of_your_additional_users_based_on_remaining_months": "We’ll charge you now for the cost of your additional users based on the remaining months of your current subscription.", + "we_will_charge_you_now_for_your_new_plan_based_on_the_remaining_months_of_your_current_subscription": "We’ll charge you now for your new plan based on the remaining months of your current subscription.", "webinars": "Webinars", "website_status": "Website status", "wed_love_you_to_stay": "We’d love you to stay", @@ -2565,6 +2573,7 @@ "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.", "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.", "you_have_x_users_and_your_plan_supports_up_to_y": "You have __addedUsersSize__ users and your plan supports up to __groupSize__.", + "you_have_x_users_on_your_subscription": "You have __groupSize__ users on your subscription.", "you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO", "you_plus_1": "You + 1", "you_plus_10": "You + 10", @@ -2622,6 +2631,7 @@ "youve_added_x_more_users_to_your_subscription_invite_people": "You’ve added __users__ more users to your subscription. <0>Invite people.", "youve_lost_edit_access": "You’ve lost edit access", "youve_unlinked_all_users": "You’ve unlinked all users", + "youve_upgraded_your_plan": "You’ve upgraded your plan!", "zh-CN": "Chinese", "zip_contents_too_large": "Zip contents too large", "zoom_in": "Zoom in", diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs index 8fada7e568..0abf3596e4 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs @@ -77,6 +77,12 @@ describe('SubscriptionGroupController', function () { }, } + this.RecurlyClient = {} + + this.SubscriptionController = {} + + this.SubscriptionModel = { Subscription: {} } + this.Controller = await esmock.strict(modulePath, { '../../../../app/src/Features/Subscription/SubscriptionGroupHandler': this.SubscriptionGroupHandler, @@ -94,6 +100,11 @@ describe('SubscriptionGroupController', function () { (this.ErrorController = { notFound: sinon.stub(), }), + '../../../../app/src/Features/Subscription/SubscriptionController': + this.SubscriptionController, + '../../../../app/src/Features/Subscription/RecurlyClient': + this.RecurlyClient, + '../../../../app/src/models/Subscription': this.SubscriptionModel, }) }) diff --git a/services/web/types/subscription/subscription-change-preview.ts b/services/web/types/subscription/subscription-change-preview.ts index 9b583c32a7..79328545c0 100644 --- a/services/web/types/subscription/subscription-change-preview.ts +++ b/services/web/types/subscription/subscription-change-preview.ts @@ -38,6 +38,7 @@ export type SubscriptionChangeDescription = | AddOnPurchase | AddOnUpdate | PremiumSubscription + | GroupPlanUpgrade export type AddOnPurchase = { type: 'add-on-purchase' @@ -51,6 +52,13 @@ export type AddOnUpdate = { } } +export type GroupPlanUpgrade = { + type: 'group-plan-upgrade' + prevPlan: { + name: string + } +} + type PremiumSubscription = { type: 'premium-subscription' plan: {