overleaf/services/web/frontend/js/main/subscription-dashboard.js
Thomas 72af966c9c Schedule subscription downgrades to occur at the current term end (#3801)
* Schedule subscription downgrades to occur at the current term end.

If the plan is a downgrade, schedule the subscription change for term
end. Use Recurly v3 API subscription change event instead of v2 update
subscription.

* Add ability for user to revert a pending subscription change

In the case where a user has downgraded, but has since decided they'd
rather stay on their current plan, we need a way to let them revert. It
isn't enough to re-use a subscription change, because Recurly sees it as
an attempt to make a change from the current plan to itself.

Instead, we use a new dialog and call a new endpoint that has the
specific intent of reverting the pending plan change, by calling the
removeSubscriptionChange recurly client method.

* Add message prompting users to contact support for immediate changes

We're showing this in the confirmation modal for a plan change that
would occur in the future, and and on the subscription page if a pending
change is due.

Most users shouldn't need this, but it should help them out if they find
an edge case like moving from eg. Student (Annual) to Professional
(Monthly) and were expecting to be "upgraded" immediately.

GitOrigin-RevId: c5be0efbeb8568ed9caa941aadcef6f6db65c420
2021-04-28 02:10:31 +00:00

306 lines
9.1 KiB
JavaScript

import _ from 'lodash'
/* global recurly */
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS204: Change includes calls to have a more natural evaluation order
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../base'
import getMeta from '../utils/meta'
const SUBSCRIPTION_URL = '/user/subscription/update'
const ensureRecurlyIsSetup = _.once(() => {
if (typeof recurly === 'undefined' || !recurly) {
return false
}
recurly.configure(getMeta('ol-recurlyApiKey'))
return true
})
App.controller('MetricsEmailController', function ($scope, $http) {
$scope.institutionEmailSubscription = function (institutionId) {
var inst = _.find(window.managedInstitutions, function (institution) {
return institution.v1Id === parseInt(institutionId)
})
if (inst.metricsEmail.optedOutUserIds.includes(window.user_id)) {
return 'Subscribe'
} else {
return 'Unsubscribe'
}
}
$scope.changeInstitutionalEmailSubscription = function (institutionId) {
$scope.subscriptionChanging = true
return $http({
method: 'POST',
url: `/institutions/${institutionId}/emailSubscription`,
headers: {
'X-CSRF-Token': window.csrfToken,
},
}).then(function successCallback(response) {
window.managedInstitutions = _.map(
window.managedInstitutions,
function (institution) {
if (institution.v1Id === parseInt(institutionId)) {
institution.metricsEmail.optedOutUserIds = response.data
}
return institution
}
)
$scope.subscriptionChanging = false
})
}
})
App.factory('RecurlyPricing', function ($q, MultiCurrencyPricing) {
return {
loadDisplayPriceWithTax: function (planCode, currency, taxRate) {
if (!ensureRecurlyIsSetup()) return
const currencySymbol = MultiCurrencyPricing.plans[currency].symbol
const pricing = recurly.Pricing()
return $q(function (resolve, reject) {
pricing
.plan(planCode, { quantity: 1 })
.currency(currency)
.done(function (price) {
const totalPriceExTax = parseFloat(price.next.total)
let taxAmmount = totalPriceExTax * taxRate
if (isNaN(taxAmmount)) {
taxAmmount = 0
}
let total = totalPriceExTax + taxAmmount
if (total % 1 !== 0) {
total = total.toFixed(2)
}
resolve(`${currencySymbol}${total}`)
})
})
},
}
})
App.controller(
'ChangePlanFormController',
function ($scope, $modal, RecurlyPricing) {
if (!ensureRecurlyIsSetup()) return
$scope.changePlan = () =>
$modal.open({
templateUrl: 'confirmChangePlanModalTemplate',
controller: 'ConfirmChangePlanController',
scope: $scope,
})
$scope.cancelPendingPlanChange = () =>
$modal.open({
templateUrl: 'cancelPendingPlanChangeModalTemplate',
controller: 'CancelPendingPlanChangeController',
scope: $scope,
})
$scope.$watch('plan', function (plan) {
if (!plan) return
const planCodesChangingAtTermEnd = getMeta(
'ol-planCodesChangingAtTermEnd'
)
$scope.planChangesAtTermEnd = false
if (
planCodesChangingAtTermEnd &&
planCodesChangingAtTermEnd.indexOf(plan.planCode) > -1
) {
$scope.planChangesAtTermEnd = true
}
const planCode = plan.planCode
const subscription = getMeta('ol-subscription')
const { currency, taxRate } = subscription.recurly
$scope.price = '...' // Placeholder while we talk to recurly
RecurlyPricing.loadDisplayPriceWithTax(planCode, currency, taxRate).then(
price => {
$scope.price = price
}
)
})
}
)
App.controller(
'ConfirmChangePlanController',
function ($scope, $modalInstance, $http) {
$scope.confirmChangePlan = function () {
const body = {
plan_code: $scope.plan.planCode,
_csrf: window.csrfToken,
}
$scope.inflight = true
return $http
.post(`${SUBSCRIPTION_URL}?origin=confirmChangePlan`, body)
.then(() => location.reload())
.catch(() => console.log('something went wrong changing plan'))
}
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
}
)
App.controller(
'CancelPendingPlanChangeController',
function ($scope, $modalInstance, $http) {
$scope.confirmCancelPendingPlanChange = function () {
const body = {
_csrf: window.csrfToken,
}
$scope.inflight = true
return $http
.post('/user/subscription/cancel-pending', body)
.then(() => location.reload())
.catch(() =>
console.log('something went wrong reverting pending plan change')
)
}
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
}
)
App.controller(
'LeaveGroupModalController',
function ($scope, $modalInstance, $http) {
$scope.confirmLeaveGroup = function () {
$scope.inflight = true
return $http({
url: '/subscription/group/user',
method: 'DELETE',
params: {
subscriptionId: $scope.subscriptionId,
_csrf: window.csrfToken,
},
})
.then(() => location.reload())
.catch(() => console.log('something went wrong changing plan'))
}
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
}
)
App.controller('GroupMembershipController', function ($scope, $modal) {
$scope.removeSelfFromGroup = function (subscriptionId) {
$scope.subscriptionId = subscriptionId
return $modal.open({
templateUrl: 'LeaveGroupModalTemplate',
controller: 'LeaveGroupModalController',
scope: $scope,
})
}
})
App.controller('RecurlySubscriptionController', function ($scope) {
const recurlyIsSetup = ensureRecurlyIsSetup()
const subscription = getMeta('ol-subscription')
$scope.showChangePlanButton = recurlyIsSetup && !subscription.groupPlan
if (
window.subscription.recurly.account.has_past_due_invoice &&
window.subscription.recurly.account.has_past_due_invoice._ === 'true'
) {
$scope.showChangePlanButton = false
}
$scope.recurlyLoadError = !recurlyIsSetup
$scope.switchToDefaultView = () => {
$scope.showCancellation = false
$scope.showChangePlan = false
}
$scope.switchToDefaultView()
$scope.switchToCancellationView = () => {
$scope.showCancellation = true
$scope.showChangePlan = false
}
$scope.switchToChangePlanView = () => {
$scope.showCancellation = false
$scope.showChangePlan = true
}
})
App.controller(
'RecurlyCancellationController',
function ($scope, RecurlyPricing, $http) {
if (!ensureRecurlyIsSetup()) return
const subscription = getMeta('ol-subscription')
const sevenDaysTime = new Date()
sevenDaysTime.setDate(sevenDaysTime.getDate() + 7)
const freeTrialEndDate = new Date(subscription.recurly.trial_ends_at)
const freeTrialInFuture = freeTrialEndDate > new Date()
const freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
const isMonthlyCollab =
subscription.plan.planCode.indexOf('collaborator') !== -1 &&
subscription.plan.planCode.indexOf('ann') === -1 &&
!subscription.groupPlan
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
if (isMonthlyCollab && stillInFreeTrial) {
$scope.showExtendFreeTrial = true
} else if (isMonthlyCollab && !stillInFreeTrial) {
$scope.showDowngradeToStudent = true
} else {
$scope.showBasicCancel = true
}
const { currency, taxRate } = subscription.recurly
$scope.studentPrice = '...' // Placeholder while we talk to recurly
RecurlyPricing.loadDisplayPriceWithTax('student', currency, taxRate).then(
price => {
$scope.studentPrice = price
}
)
$scope.downgradeToStudent = function () {
const body = {
plan_code: 'student',
_csrf: window.csrfToken,
}
$scope.inflight = true
return $http
.post(`${SUBSCRIPTION_URL}?origin=downgradeToStudent`, body)
.then(() => location.reload())
.catch(() => console.log('something went wrong changing plan'))
}
$scope.cancelSubscription = function () {
const body = { _csrf: window.csrfToken }
$scope.inflight = true
return $http
.post('/user/subscription/cancel', body)
.then(() => (location.href = '/user/subscription/canceled'))
.catch(() => console.log('something went wrong changing plan'))
}
$scope.extendTrial = function () {
const body = { _csrf: window.csrfToken }
$scope.inflight = true
return $http
.put('/user/subscription/extend', body)
.then(() => location.reload())
.catch(() => console.log('something went wrong changing plan'))
}
}
)