From 895f52d41a8f72a157fb8bafad846361bb5bea82 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 14 Sep 2021 14:18:37 +0200 Subject: [PATCH] Let users upgrade to group plans via subscription dashboard (#5100) * Let users upgrade to group plans via subscription dashboard Users on an individual plan don't have a way to upgrade to a group subscription without contacting support. As a temporary measure, we're adding a way to do this by re-using the existing group plan modal from the plans pages, to allow users to configure and upgrade to a group plan directly. This is currently only available for USD, EUR, and GBP - since although we now support other currencies in Recurly, the group plans modal does not yet support them. The user however can not change currency here, their group subscription will be in the same currency as their current individual subscription. The group plan modal has been duplicated rather than extended, to keep this code seperate as it is potentially only a stopgap measure - and we don't want to be untangling the additional logic from the existing modal/template later down the line. GitOrigin-RevId: 10664bd19af2c3870dfe7e19fd0f9c5b7c877cc6 --- .../Subscription/SubscriptionController.js | 1 + .../Subscription/SubscriptionHelper.js | 12 +- .../subscriptions/_modal_group_upgrade.pug | 59 ++++++++++ .../web/app/views/subscriptions/dashboard.pug | 1 + .../_personal_subscription_recurly.pug | 20 +++- .../js/main/subscription-dashboard.js | 106 ++++++++++++++++++ services/web/locales/en.json | 3 + .../Subscription/SubscriptionHelperTests.js | 30 ++++- 8 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 services/web/app/views/subscriptions/_modal_group_upgrade.pug diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index e1dabb55bc..f0c7786a85 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -121,6 +121,7 @@ async function userSubscriptionPage(req, res) { const data = { title: 'your_subscription', plans, + groupPlans: GroupPlansData, user, hasSubscription, fromPlansPage, diff --git a/services/web/app/src/Features/Subscription/SubscriptionHelper.js b/services/web/app/src/Features/Subscription/SubscriptionHelper.js index f3135f06b8..ba6a2e23de 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHelper.js +++ b/services/web/app/src/Features/Subscription/SubscriptionHelper.js @@ -3,7 +3,17 @@ * This is to avoid unintended/artifical credits on users Recurly accounts. */ function shouldPlanChangeAtTermEnd(oldPlan, newPlan) { - return oldPlan.price > newPlan.price + return getPlanPrice(oldPlan) > getPlanPrice(newPlan) +} + +/** + * Group plans have their price in dollars, but individual plans store the price in cents + */ +function getPlanPrice(plan) { + if (plan.groupPlan) { + return plan.price * 100 + } + return plan.price } module.exports = { diff --git a/services/web/app/views/subscriptions/_modal_group_upgrade.pug b/services/web/app/views/subscriptions/_modal_group_upgrade.pug new file mode 100644 index 0000000000..a6320af348 --- /dev/null +++ b/services/web/app/views/subscriptions/_modal_group_upgrade.pug @@ -0,0 +1,59 @@ +script(type="text/ng-template", id="groupPlanModalUpgradeTemplate") + .modal-header + h3 Save 30% or more with a group license + .modal-body.plans + .container-fluid + .row + .col-md-6.text-center + .circle.circle-lg + | {{ displayPrice }} + span.small / year + br + span.circle-subtext For {{ selected.size }} users + ul.list-unstyled + li Each user will have access to: + li   + li(ng-if="selected.plan_code == 'collaborator'") + strong #{translate("collabs_per_proj", {collabcount:10})} + li(ng-if="selected.plan_code == 'professional'") + strong #{translate("unlimited_collabs")} + +features_premium + .col-md-6 + form.form + .form-group + label(for='plan_code') + | Plan + select.form-control(id="plan_code", ng-model="selected.plan_code") + option(ng-repeat="plan_code in options.plan_codes", value="{{plan_code.code}}") {{ plan_code.display }} + .form-group + label(for='size') + | Number of users + select.form-control(id="size", ng-model="selected.size") + option(ng-repeat="size in options.sizes", value="{{size}}") {{ size }} + .form-group + label(for='currency') + | Currency + select.form-control(disabled id="currency", ng-model="selected.currency") + option(ng-repeat="currency in options.currencies", value="{{currency.code}}") {{ currency.display }} + .form-group + label(for='usage') + | Usage + select.form-control(id="usage", ng-model="selected.usage") + option(ng-repeat="usage in options.usages", value="{{usage.code}}") {{ usage.display }} + p.small.text-center.row-spaced-small(ng-show="selected.usage == 'educational'") + | The 40% educational discount can be used by students or faculty using Overleaf for teaching + p.small.text-center.row-spaced-small(ng-show="selected.usage == 'enterprise'") + | Save an additional 40% on groups of 10 or more with our educational discount + .modal-footer + .text-center + p + strong Your new subscription will be billed immediately to your current payment method. + hr.thin + button.btn.btn-primary.btn-lg(ng-disabled='inflight' ng-click="upgrade()") Upgrade Now + hr.thin + a( + href + ng-controller="ContactGeneralModal" + ng-click="openModal()" + ) Need more than 50 licenses? Please get in touch + diff --git a/services/web/app/views/subscriptions/dashboard.pug b/services/web/app/views/subscriptions/dashboard.pug index b4dbb05645..5dd7cbffb4 100644 --- a/services/web/app/views/subscriptions/dashboard.pug +++ b/services/web/app/views/subscriptions/dashboard.pug @@ -12,6 +12,7 @@ block append meta meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey) meta(name="ol-subscription" data-type="json" content=personalSubscription) meta(name="ol-recomendedCurrency" content=personalSubscription.recurly.currency) + meta(name="ol-groupPlans" data-type="json" content=groupPlans) block content main.content.content-alt#main-content(ng-cloak) diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug index 06aae4fc5f..d1423ec7de 100644 --- a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug +++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly.pug @@ -21,6 +21,8 @@ div(ng-controller="RecurlySubscriptionController") a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}. if (personalSubscription.pendingPlan) p #{translate("want_change_to_apply_before_plan_end")} + else if (personalSubscription.plan.groupPlan) + p #{translate("contact_support_to_change_group_subscription")} if (personalSubscription.recurly.trialEndsAtFormatted && personalSubscription.recurly.trial_ends_at > Date.now()) p You're on a free trial which ends on #{personalSubscription.recurly.trialEndsAtFormatted} p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount: personalSubscription.recurly.price, collectionDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong', 'strong'])} @@ -64,6 +66,19 @@ div(ng-controller="RecurlySubscriptionController") +printPlans(plans.individualMonthlyPlans) +printPlans(plans.individualAnnualPlans) + div(ng-controller="ChangePlanToGroupFormController") + h2 #{translate('looking_multiple_licenses')} + div(ng-show="isValidCurrencyForUpgrade") + span #{translate('reduce_costs_group_licenses')} + br + br + a.btn.btn-success( + href="#groups" + ng-click="openGroupPlanModal()" + ) #{translate('change_to_group_plan')} + div(ng-hide="isValidCurrencyForUpgrade") + span #{translate('contact_support_to_upgrade_to_group_subscription')} + .div(ng-controller="RecurlyCancellationController", ng-show="showCancellation").text-center p @@ -129,4 +144,7 @@ script(type='text/ng-template', id='cancelPendingPlanChangeModalTemplate') ng-click="confirmCancelPendingPlanChange()" ) span(ng-hide="inflight") #{translate("revert_pending_plan_change")} - span(ng-show="inflight") #{translate("processing")}… \ No newline at end of file + span(ng-show="inflight") #{translate("processing")}… + +include ../_plans_page_mixins +include ../_modal_group_upgrade \ No newline at end of file diff --git a/services/web/frontend/js/main/subscription-dashboard.js b/services/web/frontend/js/main/subscription-dashboard.js index 2984f060b9..1735d4fa45 100644 --- a/services/web/frontend/js/main/subscription-dashboard.js +++ b/services/web/frontend/js/main/subscription-dashboard.js @@ -91,6 +91,112 @@ App.factory('RecurlyPricing', function ($q, MultiCurrencyPricing) { } }) +App.controller('ChangePlanToGroupFormController', function ($scope, $modal) { + if (!ensureRecurlyIsSetup()) return + + const subscription = getMeta('ol-subscription') + const currency = subscription.recurly.currency + + if (['USD', 'GBP', 'EUR'].includes(currency)) { + $scope.isValidCurrencyForUpgrade = true + } + + $scope.openGroupPlanModal = function () { + const planCode = subscription.plan.planCode + $scope.defaultGroupPlan = planCode.includes('professional') + ? 'professional' + : 'collaborator' + $scope.currentPlanCurrency = currency + $modal.open({ + templateUrl: 'groupPlanModalUpgradeTemplate', + controller: 'GroupPlansModalUpgradeController', + scope: $scope, + }) + } +}) + +App.controller( + 'GroupPlansModalUpgradeController', + function ($scope, $modal, $location, $http) { + $scope.options = { + plan_codes: [ + { + display: 'Collaborator', + code: 'collaborator', + }, + { + display: 'Professional', + code: 'professional', + }, + ], + currencies: [ + { + display: 'USD ($)', + code: 'USD', + }, + { + display: 'GBP (£)', + code: 'GBP', + }, + { + display: 'EUR (€)', + code: 'EUR', + }, + ], + currencySymbols: { + USD: '$', + EUR: '€', + GBP: '£', + }, + sizes: [2, 3, 4, 5, 10, 20, 50], + usages: [ + { + display: 'Enterprise', + code: 'enterprise', + }, + { + display: 'Educational', + code: 'educational', + }, + ], + } + + $scope.prices = getMeta('ol-groupPlans') + + const currency = $scope.currentPlanCurrency + + // default selected + $scope.selected = { + plan_code: $scope.defaultGroupPlan || 'collaborator', + currency, + size: '10', + usage: 'enterprise', + } + + $scope.recalculatePrice = function () { + const { usage, plan_code, currency, size } = $scope.selected + const price = $scope.prices[usage][plan_code][currency][size] + const currencySymbol = $scope.options.currencySymbols[currency] + $scope.displayPrice = `${currencySymbol}${price}` + } + + $scope.$watch('selected', $scope.recalculatePrice, true) + $scope.recalculatePrice() + + $scope.upgrade = function () { + const { plan_code, size, usage } = $scope.selected + const body = { + _csrf: window.csrfToken, + plan_code: `group_${plan_code}_${size}_${usage}`, + } + $scope.inflight = true + $http + .post(`/user/subscription/update`, body) + .then(() => location.reload()) + } + } +) + App.controller( 'ChangePlanFormController', function ($scope, $modal, RecurlyPricing) { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index ec9455ead5..b8a0b197d4 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1045,7 +1045,10 @@ "currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__ plan.", "your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__ at the end of the current billing period.", "want_change_to_apply_before_plan_end": "If you wish this change to apply before the end of your current billing period, please contact us.", + "contact_support_to_change_group_subscription": "Please contact support if you wish to change your group subscription.", + "contact_support_to_upgrade_to_group_subscription": "Please contact support if you wish to be upgraded to a group subscription.", "change_plan": "Change plan", + "change_to_group_plan": "Change to a group plan", "next_payment_of_x_collectected_on_y": "The next payment of <0>__paymentAmmount__ will be collected on <1>__collectionDate__.", "additional_licenses": "Your subscription includes <0>__additionalLicenses__ additional license(s) for a total of <1>__totalLicenses__ licenses.", "pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__ additional license(s) for a total of <1>__pendingTotalLicenses__ licenses.", diff --git a/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js b/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js index 2465682d31..1631b8c60c 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js @@ -6,15 +6,25 @@ const modulePath = const plans = { expensive: { planCode: 'expensive', - price: 15, + price: 1500, }, cheaper: { planCode: 'cheaper', - price: 5, + price: 500, }, alsoCheap: { plancode: 'also-cheap', - price: 5, + price: 500, + }, + expensiveGroup: { + plancode: 'group_expensive', + price: 495, + groupPlan: true, + }, + cheapGroup: { + plancode: 'group_cheap', + price: 10, + groupPlan: true, }, bad: {}, } @@ -46,5 +56,19 @@ describe('SubscriptionHelper', function () { ) expect(changeAtTermEnd).to.be.false }) + it('should return false if the change is from an individual plan to a more expensive group plan', function () { + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( + plans.expensive, + plans.expensiveGroup + ) + expect(changeAtTermEnd).to.be.false + }) + it('should return true if the change is from an individual plan to a cheaper group plan', function () { + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( + plans.expensive, + plans.cheapGroup + ) + expect(changeAtTermEnd).to.be.true + }) }) })