mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-25 06:12:52 +00:00
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
This commit is contained in:
parent
8197411cdf
commit
895f52d41a
8 changed files with 227 additions and 5 deletions
|
@ -121,6 +121,7 @@ async function userSubscriptionPage(req, res) {
|
|||
const data = {
|
||||
title: 'your_subscription',
|
||||
plans,
|
||||
groupPlans: GroupPlansData,
|
||||
user,
|
||||
hasSubscription,
|
||||
fromPlansPage,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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 <strong ng-non-bindable>#{personalSubscription.recurly.trialEndsAtFormatted}</strong>
|
||||
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")}…
|
||||
span(ng-show="inflight") #{translate("processing")}…
|
||||
|
||||
include ../_plans_page_mixins
|
||||
include ../_modal_group_upgrade
|
|
@ -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) {
|
||||
|
|
|
@ -1045,7 +1045,10 @@
|
|||
"currently_subscribed_to_plan": "You are currently subscribed to the <0>__planName__</0> plan.",
|
||||
"your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__</0> 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__</0> will be collected on <1>__collectionDate__</1>.",
|
||||
"additional_licenses": "Your subscription includes <0>__additionalLicenses__</0> additional license(s) for a total of <1>__totalLicenses__</1> licenses.",
|
||||
"pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__</0> additional license(s) for a total of <1>__pendingTotalLicenses__</1> licenses.",
|
||||
|
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue