Merge pull request #13311 from overleaf/ab-tear-down-subscription-pages-react

[web] Tear down subscription-pages-react test and remove Angular code

GitOrigin-RevId: 3cf906e476ffa52a058ccb4e4acbb89a657bd021
This commit is contained in:
Alexandre Bourdin 2023-06-05 13:03:06 +03:00 committed by Copybot
parent 37f01dfe1e
commit 150cf21710
27 changed files with 50 additions and 2767 deletions

View file

@ -129,34 +129,12 @@ async function plansPage(req, res) {
})
}
async function paymentPage(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
// get to show the recurly.js page
if (assignment.variant === 'active') {
await _paymentReactPage(req, res)
} else {
await _paymentAngularPage(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _paymentAngularPage(req, res)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _paymentReactPage(req, res) {
async function paymentPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) {
@ -211,82 +189,6 @@ async function _paymentReactPage(req, res) {
}
}
async function _paymentAngularPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) {
return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
}
const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
if (hasSubscription) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
// LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
// Recurly as well at this point (we don't do this most places for speed).
const valid =
await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
user._id
)
if (!valid) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
let currency = null
if (req.query.currency) {
const queryCurrency = req.query.currency.toUpperCase()
if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
currency = queryCurrency
}
}
const { currencyCode: recommendedCurrency, countryCode } =
await GeoIpLookup.promises.getCurrencyCode(req.query?.ip || req.ip)
if (recommendedCurrency && currency == null) {
currency = recommendedCurrency
}
await SplitTestHandler.promises.getAssignment(
req,
res,
'student-check-modal'
)
res.render('subscriptions/new-refreshed', {
title: 'subscribe',
currency,
countryCode,
plan,
recurlyConfig: JSON.stringify({
currency,
subdomain: Settings.apis.recurly.subdomain,
}),
showCouponField: !!req.query.scf,
showVatField: !!req.query.svf,
})
}
}
}
async function userSubscriptionPage(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _userSubscriptionReactPage(req, res)
} else {
await _userSubscriptionAngularPage(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _userSubscriptionAngularPage(req, res)
}
}
function formatGroupPlansDataForDash() {
return {
plans: [...groupPlanModalOptions.plan_codes],
@ -301,7 +203,7 @@ function formatGroupPlansDataForDash() {
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _userSubscriptionReactPage(req, res) {
async function userSubscriptionPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const results =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
@ -356,58 +258,6 @@ async function _userSubscriptionReactPage(req, res) {
res.render('subscriptions/dashboard-react', data)
}
async function _userSubscriptionAngularPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const results =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user
)
const {
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
currentInstitutionsWithLicence,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
} = results
const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList(
personalSubscription ? personalSubscription.plan : undefined
)
AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-cancel-button'
)
const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
const data = {
title: 'your_subscription',
plans,
groupPlans: GroupPlansData,
user,
hasSubscription,
fromPlansPage,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
currentInstitutionsWithLicence,
groupPlanModalOptions,
cancelButtonNewCopy,
}
res.render('subscriptions/dashboard', data)
}
async function interstitialPaymentPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { recommendedCurrency, countryCode, geoPricingTestVariant } =
@ -515,33 +365,12 @@ async function createSubscription(req, res) {
}
}
async function successfulSubscription(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _successfulSubscriptionReact(req, res)
} else {
await _successfulSubscriptionAngular(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _successfulSubscriptionAngular(req, res)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _successfulSubscriptionReact(req, res) {
async function successfulSubscription(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
@ -561,26 +390,6 @@ async function _successfulSubscriptionReact(req, res) {
}
}
async function _successfulSubscriptionAngular(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user
)
const postCheckoutRedirect = req.session?.postCheckoutRedirect
if (!personalSubscription) {
res.redirect('/user/subscription/plans')
} else {
res.render('subscriptions/successful-subscription', {
title: 'thank_you',
personalSubscription,
postCheckoutRedirect,
})
}
}
function cancelSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
logger.debug({ userId: user._id }, 'canceling subscription')
@ -597,45 +406,18 @@ function cancelSubscription(req, res, next) {
})
}
async function canceledSubscription(req, res, next) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _canceledSubscriptionReact(req, res, next)
} else {
await _canceledSubscriptionAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _canceledSubscriptionAngular(req, res, next)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param {import("express").NextFunction} next
* @returns {Promise<void>}
*/
function _canceledSubscriptionReact(req, res, next) {
function canceledSubscription(req, res, next) {
return res.render('subscriptions/canceled-subscription-react', {
title: 'subscription_canceled',
})
}
function _canceledSubscriptionAngular(req, res, next) {
return res.render('subscriptions/canceled-subscription', {
title: 'subscription_canceled',
})
}
function cancelV1Subscription(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
logger.debug({ userId }, 'canceling v1 subscription')

View file

@ -16,110 +16,9 @@ const Errors = require('../Errors/Errors')
const EmailHelper = require('../Helpers/EmailHelper')
const { csvAttachment } = require('../../infrastructure/Response')
const { UserIsManagerError } = require('./UserMembershipErrors')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const CSVParser = require('json2csv').Parser
const logger = require('@overleaf/logger')
async function manageGroupMembers(req, res, next) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _manageGroupMembersReact(req, res, next)
} else {
await _indexAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _indexAngular(req, res, next)
}
}
async function manageGroupManagers(req, res, next) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _renderManagersPage(
req,
res,
next,
'user_membership/group-managers-react'
)
} else {
await _indexAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _indexAngular(req, res, next)
}
}
async function manageInstitutionManagers(req, res, next) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _renderManagersPage(
req,
res,
next,
'user_membership/institution-managers-react'
)
} else {
await _indexAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _indexAngular(req, res, next)
}
}
async function managePublisherManagers(req, res, next) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _renderManagersPage(
req,
res,
next,
'user_membership/publisher-managers-react'
)
} else {
await _indexAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _indexAngular(req, res, next)
}
}
async function _manageGroupMembersReact(req, res, next) {
const { entity, entityConfig } = req
return entity.fetchV1Data(function (error, entity) {
if (error != null) {
@ -149,6 +48,33 @@ async function _manageGroupMembersReact(req, res, next) {
})
}
async function manageGroupManagers(req, res, next) {
await _renderManagersPage(
req,
res,
next,
'user_membership/group-managers-react'
)
}
async function manageInstitutionManagers(req, res, next) {
await _renderManagersPage(
req,
res,
next,
'user_membership/institution-managers-react'
)
}
async function managePublisherManagers(req, res, next) {
await _renderManagersPage(
req,
res,
next,
'user_membership/publisher-managers-react'
)
}
async function _renderManagersPage(req, res, next, template) {
const { entity, entityConfig } = req
return entity.fetchV1Data(function (error, entity) {
@ -178,39 +104,6 @@ async function _renderManagersPage(req, res, next, template) {
})
}
function _indexAngular(req, res, next) {
const { entity, entityConfig } = req
return entity.fetchV1Data(function (error, entity) {
if (error != null) {
return next(error)
}
return UserMembershipHandler.getUsers(
entity,
entityConfig,
function (error, users) {
let entityName
if (error != null) {
return next(error)
}
const entityPrimaryKey =
entity[entityConfig.fields.primaryKey].toString()
if (entityConfig.fields.name) {
entityName = entity[entityConfig.fields.name]
}
return res.render('user_membership/index', {
name: entityName,
users,
groupSize: entityConfig.hasMembersLimit
? entity.membersLimit
: undefined,
translations: entityConfig.translations,
paths: entityConfig.pathsFor(entityPrimaryKey),
})
}
)
})
}
module.exports = {
manageGroupMembers,
manageGroupManagers,

View file

@ -1,15 +0,0 @@
extends ../layout
block content
main.content.content-alt#main-content
.container
.row
.col-md-8.col-md-offset-2
.card(ng-cloak)
.page-header
h2 #{translate("subscription_canceled")}
.alert.alert-info
p #{translate("to_modify_your_subscription_go_to")}
a(href="/user/subscription") #{translate("manage_subscription")}.
p
a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}

View file

@ -1,68 +0,0 @@
extends ../layout
include ./dashboard/_team_name_mixin
block head-scripts
script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
block append meta
meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd)
if (personalSubscription && personalSubscription.recurly)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
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#main-content(ng-cloak)
.container
.row
.col-md-8.col-md-offset-2
if (fromPlansPage)
.alert.alert-warning
p You already have a subscription
.card
.page-header
h1 #{translate("your_subscription")}
-var hasDisplayedSubscription = false
if (personalSubscription)
-hasDisplayedSubscription = true
include ./dashboard/_personal_subscription
if (managedGroupSubscriptions && managedGroupSubscriptions.length > 0)
include ./dashboard/_managed_groups
if (managedInstitutions && managedInstitutions.length > 0)
include ./dashboard/_managed_institutions
if (managedPublishers && managedPublishers.length > 0)
include ./dashboard/_managed_publishers
if (memberGroupSubscriptions && memberGroupSubscriptions.length > 0)
-hasDisplayedSubscription = true
include ./dashboard/_group_memberships
include ./dashboard/_institution_memberships
if (v1SubscriptionStatus)
include ./dashboard/_v1_subscription_status
if (!hasDisplayedSubscription)
if (hasSubscription)
-hasDisplayedSubscription = true
p(ng-non-bindable) You're on an #{settings.appName} Paid plan. Contact
a(href="mailto:support@overleaf.com") support@overleaf.com
| to find out more.
else
p(ng-non-bindable)
| You are on the #{settings.appName} Free plan. Upgrade to access these
a(href="/learn/how-to/Overleaf_premium_features") Premium Features:
ul
li #{translate('invite_more_collabs')}
for feature in ['realtime_track_changes', 'full_doc_history', 'reference_search', 'reference_sync', 'dropbox_integration_lowercase', 'github_integration_lowercase', 'priority_support']
li #{translate(feature)}
a(ng-controller="UpgradeSubscriptionController" href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary Upgrade now
!= moduleIncludes("contactModalGeneral", locals)

View file

@ -1,28 +0,0 @@
mixin printPlan(plan)
if (!plan.hideFromUsers)
tr(ng-controller="ChangePlanFormController", ng-init="plan="+JSON.stringify(plan))
td
strong(ng-non-bindable) #{plan.name}
td
if (plan.annual)
| {{displayPrice}} / #{translate("year")}
else
| {{displayPrice}} / #{translate("month")}
td
if (typeof(personalSubscription.planCode) != "undefined" && plan.planCode == personalSubscription.planCode.split("_")[0])
if (personalSubscription.pendingPlan)
form
input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
input(type="submit", ng-click="cancelPendingPlanChange()", value=translate("keep_current_plan")).btn.btn-primary
else
button.btn.disabled #{translate("your_plan")}
else if (personalSubscription.pendingPlan && typeof(personalSubscription.pendingPlan.planCode) != "undefined" && plan.planCode == personalSubscription.pendingPlan.planCode.split("_")[0])
button.btn.disabled #{translate("your_new_plan")}
else
form
input(type="hidden", ng-model="plan_code", name="plan_code", value=plan.planCode)
input(type="submit", ng-click="changePlan()", value=translate("change_to_this_plan")).btn.btn-primary
mixin printPlans(plans)
each plan in plans
+printPlan(plan)

View file

@ -1,32 +0,0 @@
div(ng-controller="GroupMembershipController")
each groupSubscription, index in memberGroupSubscriptions
unless (groupSubscription.userIsGroupManager)
if (user._id+'' != groupSubscription.admin_id._id+'')
div
p !{translate("you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z", {planName: groupSubscription.planLevelName, groupName: groupSubscription.teamName || '', adminEmail: groupSubscription.admin_id.email}, [{name: 'a', attrs: {href: '/user/subscription/plans'}}, 'strong'])}
if (groupSubscription.teamNotice && groupSubscription.teamNotice != '')
p
//- Team notice is sanitized in SubscriptionViewModelBuilder
em(ng-non-bindable) !{groupSubscription.teamNotice}
if index === memberGroupSubscriptions.length - 1
include ../_premium_features_link
span
button.btn.btn-danger.text-capitalise(ng-click="removeSelfFromGroup('"+groupSubscription._id+"')") #{translate("leave_group")}
hr
script(type='text/ng-template', id='LeaveGroupModalTemplate')
.modal-header
h3 #{translate("leave_group")}
.modal-body
p #{translate("sure_you_want_to_leave_group")}
.modal-footer
button.btn.btn-default(
ng-disabled="inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-danger(
ng-disabled="state.inflight"
ng-click="confirmLeaveGroup()"
)
span(ng-hide="inflight") #{translate("leave_now")}
span(ng-show="inflight") #{translate("processing")}…

View file

@ -1,10 +0,0 @@
if currentInstitutionsWithLicence === false
.alert.alert-warning
p Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.
else
each institution, index in currentInstitutionsWithLicence
-hasDisplayedSubscription = true
p !{translate("you_are_on_x_plan_as_a_confirmed_member_of_institution_y", {planName: 'Professional', institutionName: institution.name || ''}, [{name: 'a', attrs: {href: '/user/subscription/plans'}}, 'strong'])}
if (index === currentInstitutionsWithLicence.length - 1)
include ../_premium_features_link
hr

View file

@ -1,24 +0,0 @@
each managedGroupSubscription in managedGroupSubscriptions
if (managedGroupSubscription.userIsGroupMember)
p !{translate("you_are_a_manager_and_member_of_x_plan_as_member_of_group_subscription_y_administered_by_z", {planName: managedGroupSubscription.planLevelName, groupName: managedGroupSubscription.teamName || '', adminEmail: managedGroupSubscription.admin_id.email}, [{name: 'a', attrs: {href: '/user/subscription/plans'}}, 'strong'])}
else
p !{translate("you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z", {planName: managedGroupSubscription.planLevelName, groupName: managedGroupSubscription.teamName || '', adminEmail: managedGroupSubscription.admin_id.email}, [{name: 'a', attrs: {href: '/user/subscription/plans'}}, 'strong'])}
p
a.btn.btn-primary(href="/manage/groups/" + managedGroupSubscription._id + "/members")
i.fa.fa-fw.fa-users
| &nbsp;
| Manage members
| &nbsp;
p
a(href="/manage/groups/" + managedGroupSubscription._id + "/managers")
i.fa.fa-fw.fa-users
| &nbsp;
| Manage group managers
| &nbsp;
p
a(href="/metrics/groups/" + managedGroupSubscription._id)
i.fa.fa-fw.fa-line-chart
| &nbsp;
| View metrics
hr

View file

@ -1,24 +0,0 @@
each institution in managedInstitutions
p !{translate("you_are_a_manager_of_commons_at_institution_x", {institutionName: institution.name || ''}, ['strong'])}
p
a.btn.btn-primary(href="/metrics/institutions/" + institution.v1Id)
i.fa.fa-fw.fa-line-chart
| &nbsp;
| View metrics
p
a(href="/institutions/" + institution.v1Id + "/hub")
i.fa.fa-fw.fa-user-circle
| &nbsp;
| View hub
p
a(href="/manage/institutions/" + institution.v1Id + "/managers")
i.fa.fa-fw.fa-users
| &nbsp;
| Manage institution managers
div(ng-controller="MetricsEmailController", ng-cloak)
p
span Monthly metrics emails:&nbsp;
a(href ng-bind-html="institutionEmailSubscription('"+institution.v1Id+"')" ng-show="!subscriptionChanging" ng-click="changeInstitutionalEmailSubscription('"+institution.v1Id+"')")
span(ng-show="subscriptionChanging")
i.fa.fa-spin.fa-refresh(aria-hidden="true")
hr

View file

@ -1,16 +0,0 @@
each publisher in managedPublishers
p
| You are a manager of
|
strong(ng-non-bindable)= publisher.name
p
a(href="/publishers/" + publisher.slug + "/hub")
i.fa.fa-fw.fa-user-circle
| &nbsp;
| View hub
p
a(href="/manage/publishers/" + publisher.slug + "/managers")
i.fa.fa-fw.fa-users
| &nbsp;
| Manage publisher managers
hr

View file

@ -1,7 +0,0 @@
if (personalSubscription.recurly)
include ./_personal_subscription_recurly
include ./_personal_subscription_recurly_sync_email
else
include ./_personal_subscription_custom
hr

View file

@ -1,6 +0,0 @@
p
| Please
|
a(href="/contact") contact support
|
| to make changes to your plan

View file

@ -1,159 +0,0 @@
div(ng-controller="RecurlySubscriptionController")
div(ng-show="!showCancellation")
if (personalSubscription.recurly.account.has_past_due_invoice && personalSubscription.recurly.account.has_past_due_invoice._ == 'true')
.alert.alert-danger #{translate("account_has_past_due_invoice_change_plan_warning")}
| &nbsp;
a(href=personalSubscription.recurly.accountManagementLink, target="_blank") #{translate("view_your_invoices")}.
case personalSubscription.recurly.state
when "active"
p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])}
if (personalSubscription.pendingPlan)
if (personalSubscription.pendingPlan.name != personalSubscription.plan.name)
|
| !{translate("your_plan_is_changing_at_term_end", {pendingPlanName: personalSubscription.pendingPlan.name}, ['strong'])}
if (personalSubscription.recurly.pendingAdditionalLicenses > 0 || personalSubscription.recurly.additionalLicenses > 0)
|
| !{translate("pending_additional_licenses", {pendingAdditionalLicenses: personalSubscription.recurly.pendingAdditionalLicenses, pendingTotalLicenses: personalSubscription.recurly.pendingTotalLicenses}, ['strong', 'strong'])}
else if (personalSubscription.recurly.additionalLicenses > 0)
|
| !{translate("additional_licenses", {additionalLicenses: personalSubscription.recurly.additionalLicenses, totalLicenses: personalSubscription.recurly.totalLicenses}, ['strong', 'strong'])}
| &nbsp;
a(href, ng-click="switchToChangePlanView()", ng-if="showChangePlanButton") !{translate("change_plan")}.
if (personalSubscription.pendingPlan && personalSubscription.pendingPlan.name != personalSubscription.plan.name)
p #{translate("want_change_to_apply_before_plan_end")}
else if (personalSubscription.plan.groupPlan)
p !{translate("contact_support_to_change_group_subscription", {}, [{ name: "a", attrs: { href: "/contact"}}])}
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.displayPrice, collectionDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong', 'strong'])}
include ../_premium_features_link
include ./../_price_exceptions
p.pull-right
p
a(href=personalSubscription.recurly.billingDetailsLink, target="_blank").btn.btn-secondary-info.btn-secondary #{translate("update_your_billing_details")}
| &nbsp;
a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-secondary-info.btn-secondary #{translate("view_your_invoices")}
| &nbsp;
unless (cancelButtonNewCopy)
a(href, ng-click="switchToCancellationView()", ng-hide="recurlyLoadError", event-tracking='subscription-page-cancel-button-click', event-tracking-mb="true", event-tracking-trigger="click").btn.btn-danger !{translate("stop_your_subscription")}
if (cancelButtonNewCopy)
p
a(href, ng-click="switchToCancellationView()", ng-hide="recurlyLoadError", event-tracking='subscription-page-cancel-button-click', event-tracking-mb="true", event-tracking-trigger="click").btn.btn-danger !{translate("cancel_your_subscription")}
unless (personalSubscription.recurly.trialEndsAtFormatted && personalSubscription.recurly.trial_ends_at > Date.now())
p
i !{translate("subscription_will_remain_active_until_end_of_billing_period_x", {terminationDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong'])}
when "canceled"
p !{translate("currently_subscribed_to_plan", {planName: personalSubscription.plan.name}, ['strong'])}
p !{translate("subscription_canceled_and_terminate_on_x", {terminateDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong'])}
include ../_premium_features_link
p
a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-secondary-info.btn-secondary #{translate("view_your_invoices")}
p: form(action="/user/subscription/reactivate",method="post")
input(type="hidden", name="_csrf", value=csrfToken)
input(type="submit",value="Reactivate your subscription").btn.btn-primary
when "expired"
p !{translate("your_subscription_has_expired")}
p
a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-secondary-info.btn-secondary #{translate("view_your_invoices")}
| &nbsp;
a(href="/user/subscription/plans").btn.btn-primary !{translate("create_new_subscription")}
default
p !{translate("problem_with_subscription_contact_us")}
.alert.alert-warning(ng-show="recurlyLoadError")
strong #{translate('payment_provider_unreachable_error')}
include ./_change_plans_mixins
div(ng-show="showChangePlan", ng-cloak)
h2 !{translate("change_plan")}
p: table.table
tr
th !{translate("name")}
th !{translate("price")}
th
+printPlans(plans.studentAccounts)
+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-primary(
href="#groups"
ng-click="openGroupPlanModal()"
) #{translate('change_to_group_plan')}
div(ng-hide="isValidCurrencyForUpgrade")
span !{translate('contact_support_to_upgrade_to_group_subscription', {}, [{ name: "a", attrs: { href: "/contact"}}])}
.div(ng-controller="RecurlyCancellationController", ng-show="showCancellation").text-center
p
strong #{translate("wed_love_you_to_stay")}
div(ng-show="showExtendFreeTrial")
p !{translate("have_more_days_to_try", {days:14})}
p
button(type="submit", ng-click="extendTrial()", ng-disabled='inflight').btn.btn-primary #{translate("ill_take_it")}
p
a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
div(ng-show="showDowngrade")
div(ng-controller="ChangePlanFormController")
p !{translate("interested_in_cheaper_personal_plan", {price:'{{personalDisplayPrice}}'}, ['strong'] )}
p
button(type="submit", ng-click="downgradeToPaidPersonal()", ng-disabled='inflight').btn.btn-primary #{translate("yes_move_me_to_personal_plan")}
p
a(href, ng-click="cancelSubscription()", ng-disabled='inflight') #{translate("no_thanks_cancel_now")}
div(ng-show="showBasicCancel")
p
a(href, ng-click="switchToDefaultView()").btn.btn-secondary-info.btn-secondary #{translate("i_want_to_stay")}
p
a(href, ng-click="cancelSubscription()", ng-disabled='inflight').btn.btn-primary #{translate("cancel_my_account")}
script(type='text/ng-template', id='confirmChangePlanModalTemplate')
.modal-header
h3 #{translate("change_plan")}
.modal-body
.alert.alert-warning(ng-show="genericError")
strong #{translate("generic_something_went_wrong")}. #{translate("try_again")}. #{translate("generic_if_problem_continues_contact_us")}.
p !{translate("sure_you_want_to_change_plan", {planName: '{{plan.name}}'}, ['strong'])}
div(ng-show="planChangesAtTermEnd")
p #{translate("existing_plan_active_until_term_end")}
p #{translate("want_change_to_apply_before_plan_end")}
.modal-footer
button.btn.btn-default(
ng-disabled="inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="inflight"
ng-click="confirmChangePlan()"
)
span(ng-hide="inflight") #{translate("change_plan")}
span(ng-show="inflight") #{translate("processing")}…
script(type='text/ng-template', id='cancelPendingPlanChangeModalTemplate')
.modal-header
h3 #{translate("change_plan")}
.modal-body
.alert.alert-warning(ng-show="genericError")
strong #{translate("generic_something_went_wrong")}. #{translate("try_again")}. #{translate("generic_if_problem_continues_contact_us")}.
p !{translate("sure_you_want_to_cancel_plan_change", {planName: '{{plan.name}}'}, ['strong'])}
.modal-footer
button.btn.btn-default(
ng-disabled="inflight"
ng-click="cancel()"
) #{translate("cancel")}
button.btn.btn-primary(
ng-disabled="inflight"
ng-click="confirmCancelPendingPlanChange()"
)
span(ng-hide="inflight") #{translate("revert_pending_plan_change")}
span(ng-show="inflight") #{translate("processing")}…
include ../_plans_page_mixins
include ../_modal_group_upgrade

View file

@ -1,18 +0,0 @@
-if (user.email !== personalSubscription.recurly.account.email)
div
hr
form(async-form="updateAccountEmailAddress", name="updateAccountEmailAddress", action='/user/subscription/account/email', method="POST")
input(name='_csrf', type='hidden', value=csrfToken)
.form-group
form-messages(for="updateAccountEmailAddress")
.alert.alert-success(ng-show="updateAccountEmailAddress.response.success")
| #{translate('recurly_email_updated')}
div(ng-hide="updateAccountEmailAddress.response.success")
p(ng-non-bindable) !{translate("recurly_email_update_needed", { recurlyEmail: personalSubscription.recurly.account.email, userEmail: user.email }, ['em', 'em'])}
.actions
button.btn-primary.btn(
type='submit',
ng-disabled="updateAccountEmailAddress.inflight"
)
span(ng-show="!updateAccountEmailAddress.inflight") #{translate("update")}
span(ng-show="updateAccountEmailAddress.inflight") #{translate("updating")}…

View file

@ -1,9 +0,0 @@
mixin teamName(subscription)
if (subscription.teamName && subscription.teamName != '')
strong(ng-non-bindable)= subscription.teamName
else if (subscription.admin_id._id == user._id)
| a group account
else
| the group account owned by
|
strong= subscription.admin_id.email

View file

@ -1,62 +0,0 @@
if (v1SubscriptionStatus['team'] && v1SubscriptionStatus['team']['default_plan_name'] != 'free')
- hasDisplayedSubscription = true
p
| You have a legacy group licence from Overleaf v1.
if (v1SubscriptionStatus['team']['will_end_at'])
p
| Your current group licence ends on
|
strong= moment(v1SubscriptionStatus['team']['will_end_at']).format('Do MMM YY')
|
| and will
|
if (v1SubscriptionStatus['team']['will_renew'])
| be automatically renewed.
else
| not be automatically renewed.
if (v1SubscriptionStatus['can_cancel_team'])
p
form(method="POST", action="/user/subscription/v1/cancel")
input(type="hidden", name="_csrf", value=csrfToken)
button().btn.btn-danger Stop automatic renewal
else
p
| Please
|
a(href="/contact") contact support
|
| to make changes to your plan
hr
if (v1SubscriptionStatus['product'])
- hasDisplayedSubscription = true
p
| You have a legacy Overleaf v1
|
strong= v1SubscriptionStatus['product']['display_name']
|
| plan.
p
| Your plan ends on
|
strong= moment(v1SubscriptionStatus['product']['will_end_at']).format('Do MMM YY')
|
| and will
|
if (v1SubscriptionStatus['product']['will_renew'])
| be automatically renewed.
else
| not be automatically renewed.
if (v1SubscriptionStatus['can_cancel'])
p
form(method="POST", action="/user/subscription/v1/cancel")
input(type="hidden", name="_csrf", value=csrfToken)
button().btn.btn-danger Stop automatic renewal
else
p
| Please
|
a(href="/contact") contact support
|
| to make changes to your plan
hr

View file

@ -1,364 +0,0 @@
extends ../layout
include ./_new_mixins
block vars
- var suppressNavbarRight = true
- var suppressFooter = true
block append meta
meta(name="ol-countryCode" content=countryCode)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
meta(name="ol-recommendedCurrency" content=String(currency).slice(0,3))
block head-scripts
script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
block content
main.content.content-alt#main-content
.container(ng-controller="NewSubscriptionController" ng-cloak)
.row.card-group
.col-md-3.col-md-push-1
.card.card-highlighted
.price-feature-description
h4(ng-if="planName") {{planName}}
h4(ng-if="!planName") #{plan.name}
if plan.features
if plan.features.collaborators === 1
.text-small.number-of-collaborators #{translate("collabs_per_proj_single", {collabcount: 1})}
if plan.features.collaborators === -1
.text-small.number-of-collaborators #{translate("unlimited_collabs")}
if plan.features.collaborators > 1
.text-small.number-of-collaborators #{translate("collabs_per_proj", {collabcount: plan.features.collaborators})}
.text-small #{translate("all_premium_features_including")}
ul.small
if plan.features.compileTimeout > 1
li #{translate("increased_compile_timeout")}
if plan.features.dropbox && plan.features.github
li #{translate("sync_dropbox_github")}
if plan.features.versioning
li #{translate("full_doc_history")}
if plan.features.trackChanges
li #{translate("track_changes")}
if plan.features.references
li #{translate("reference_search")}
if plan.features.mendeley || plan.features.zotero
li #{translate("reference_sync")}
if plan.features.symbolPalette
li #{translate("symbol_palette")}
div.price-summary(ng-if="recurlyPrice")
- var priceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}"};
hr
h4 #{translate("payment_summary")}
div.small
.price-summary-line
span
| {{planName}}
span(ng-if="coupon")
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ coupon.normalPriceWithoutTax | number:2 }}
span(ng-if="!coupon")
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.subtotal }}
.price-summary-line(ng-if="coupon")
span
| {{ coupon.name }}
span
| &ndash;{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.discount}}
.price-summary-line(ng-if="taxes && taxes[0] && taxes[0].rate > 0")
span
| #{translate("vat")} {{taxes[0].rate * 100}}%
span
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.tax }}
.price-summary-line.price-summary-total-line
span
b {{ monthlyBilling ? '#{translate("total_per_month")}' : '#{translate("total_per_year")}'}}
span
b {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}
.change-currency
div.dropdown(ng-cloak dropdown)
button.btn.btn-link.dropdown-toggle.change-currency-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
) Change currency
ul.dropdown-menu(role="menu")
li(ng-repeat="(currency, value) in limitedCurrencies")
a(
ng-click="changeCurrency(currency)",
)
span.change-currency-dropdown-selected-icon(ng-show="currency == currencyCode")
i.fa.fa-check
| {{currency}} ({{value['symbol']}})
hr.thin(ng-if="trialLength || coupon")
div.trial-coupon-summary
div(ng-if="trialLength")
- var trialPriceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}", trialLen:'{{trialLength}}' };
| !{translate("first_x_days_free_after_that_y_per_month", trialPriceVars, ['strong'] )}
div(ng-if="recurlyPrice")
- var priceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}", discountMonths: "{{ coupon.discountMonths }}" };
span(ng-if="!coupon.singleUse && coupon.discountMonths > 0 && monthlyBilling")
| !{translate("x_price_for_y_months", priceVars, ['strong'] )}
span(ng-if="coupon.singleUse && monthlyBilling")
| !{translate("x_price_for_first_month", priceVars, ['strong'] )}
span(ng-if="coupon.singleUse && !monthlyBilling")
| !{translate("x_price_for_first_year", priceVars, ['strong'] )}
div(ng-if="coupon && coupon.normalPrice")
- var noDiscountPriceAngularExp = "{{ availableCurrencies[currencyCode]['symbol']}}{{coupon.normalPrice | number:2 }}";
span(ng-if="!coupon.singleUse && coupon.discountMonths > 0 && monthlyBilling")
| !{translate("then_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="!coupon.singleUse && !coupon.discountMonths && monthlyBilling")
| !{translate("normally_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="!coupon.singleUse && !monthlyBilling")
| !{translate("normally_x_price_per_year", { price: noDiscountPriceAngularExp } )}
span(ng-if="coupon.singleUse && monthlyBilling")
| !{translate("then_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="coupon.singleUse && !monthlyBilling")
| !{translate("then_x_price_per_year", { price: noDiscountPriceAngularExp } )}
hr.thin
p.price-cancel-anytime.text-center(ng-non-bindable) #{translate("cancel_anytime")}
.col-md-5.col-md-push-1
.card.card-highlighted.card-border(ng-hide="threeDSecureFlow")
.alert.alert-danger(ng-show="recurlyLoadError")
strong #{translate('payment_provider_unreachable_error')}
.price-switch-header(ng-hide="recurlyLoadError")
.row
.col-xs-9
h2 #{translate('select_a_payment_method')}
.row(ng-if="planCode == 'student-annual' || planCode == 'student-monthly' || planCode == 'student_free_trial_7_days'")
.col-xs-12
p.student-disclaimer #{translate('student_disclaimer')}
.row(ng-hide="recurlyLoadError")
div()
.col-md-12()
form(
name="simpleCCForm"
novalidate
)
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
.alert.alert-warning.small(ng-show="couponError")
strong {{couponError}}
div
.form-group.payment-method-toggle
hr.thin
.radio
.col-xs-8
label
input(
type="radio"
ng-model="paymentMethod.value"
name="payment_method"
checked=true
value="credit_card"
)
strong
| #{translate("card_payment")}
span.hidden-xs
| &nbsp;
i.fa.fa-cc-visa(aria-hidden="true")
| &nbsp;
i.fa.fa-cc-mastercard(aria-hidden="true")
| &nbsp;
i.fa.fa-cc-amex(aria-hidden="true")
.col-xs-4
label
input(
type="radio"
ng-model="paymentMethod.value"
name="payment_method"
checked=false
value="paypal"
)
strong PayPal
span.hidden-xs
| &nbsp;
i.fa.fa-cc-paypal(aria-hidden="true")
div(ng-show="paymentMethod.value === 'credit_card'")
.form-group(ng-class="showCardElementInvalid ? 'has-error' : ''")
label(for="recurly-card-input") #{translate("card_details")}
div#recurly-card-input
span.input-feedback-message(ng-if="showCardElementInvalid") Card details are not valid
.row
.col-xs-6
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
label(for="first-name") #{translate('first_name')}
input#first-name.form-control(
type="text"
maxlength='255'
data-recurly="first_name"
name="firstName"
ng-model="data.first_name"
required
)
span.input-feedback-message(ng-if="simpleCCForm.firstName.$error.required") #{translate('this_field_is_required')}
.col-xs-6
.form-group(ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
label(for="last-name") #{translate('last_name')}
input#last-name.form-control(
type="text"
maxlength='255'
data-recurly="last_name"
name="lastName"
ng-model="data.last_name"
required
)
span.input-feedback-message(ng-if="simpleCCForm.lastName.$error.required") #{translate('this_field_is_required')}
div
.row
.col-xs-12
.form-group(ng-class="validation.errorFields.address1 || inputHasError(simpleCCForm.address1) ? 'has-error' : ''")
label(for="address-line-1") #{translate('address_line_1')} &nbsp;
i.fa.fa-question-circle(
aria-label=translate('this_address_will_be_shown_on_the_invoice'),
tooltip=translate('this_address_will_be_shown_on_the_invoice'),
tooltip-placement="right",
tooltip-append-to-body="true",
)
input#address-line-1.form-control(
type="text"
maxlength="255"
data-recurly="address1"
name="address1"
ng-model="data.address1"
required
)
span.input-feedback-message(ng-if="simpleCCForm.address1.$error.required") #{translate('this_field_is_required')}
.row.toggle-address-second-line(ng-hide="ui.showAddressSecondLine")
.col-xs-12
a.text-small(
href="#"
ng-click="showAddressSecondLine($event)"
) + Add another address line
.row(ng-show="ui.showAddressSecondLine")
.col-xs-12
.form-group.has-feedback(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
label(for="address-line-2") #{translate('address_second_line_optional')}
input#address-line-2.form-control(
type="text"
maxlength="255"
data-recurly="address2"
name="address2"
ng-model="data.address2"
)
.row
.col-xs-4
.form-group(ng-class="validation.errorFields.postal_code || inputHasError(simpleCCForm.postalCode) ? 'has-error' : ''")
label(for="postal-code") #{translate('postal_code')}
input#postal-code.form-control(
type="text"
maxlength="255"
data-recurly="postal_code"
name="postalCode"
ng-model="data.postal_code"
required
)
span.input-feedback-message(ng-if="simpleCCForm.postalCode.$error.required") #{translate('this_field_is_required')}
.col-xs-8
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
name="country"
ng-change="updateCountry()"
ng-selected="{{country.code == data.country}}"
ng-model-options="{ debounce: 200 }"
required
)
option(value='', disabled) #{translate("country")}
option(value='-', disabled) --------------
option(ng-repeat="country in countries" ng-bind-html="country.name" value="{{country.code}}")
span.input-feedback-message(ng-if="simpleCCForm.country.$error.required") #{translate('this_field_is_required')}
.form-group
.checkbox
label
input(
type="checkbox"
ng-model="ui.addCompanyDetails"
)
|
| #{translate("add_company_details")}
.form-group(ng-show="ui.addCompanyDetails")
label(for="company-name") #{translate("company_name")}
input#company-name.form-control(
type="text"
name="companyName"
ng-model="data.company"
)
.form-group(ng-show="ui.addCompanyDetails && taxes.length")
label(for="vat-number") #{translate("vat_number")}
input#vat-number.form-control(
type="text"
name="vatNumber"
ng-model="data.vat_number"
ng-blur="applyVatNumber()"
)
if (showCouponField)
.form-group
label(for="coupon-code") #{translate('coupon_code')}
input#coupon-code.form-control(
type="text"
ng-blur="applyCoupon()"
ng-model="data.coupon"
)
p(ng-if="paymentMethod.value === 'paypal'") #{translate("proceeding_to_paypal_takes_you_to_the_paypal_site_to_pay")}
hr.thin
div.payment-submit
button.btn.btn-primary.btn-block(
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
)
span(ng-show="processing")
i.fa.fa-spinner.fa-spin(aria-hidden="true")
span.sr-only #{translate('processing')}
| &nbsp;
span(ng-if="paymentMethod.value === 'credit_card'")
| {{ trialLength ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_now")}'}}
span(ng-if="paymentMethod.value !== 'credit_card'") #{translate("proceed_to_paypal")}
p.tos-agreement-notice !{translate("by_subscribing_you_agree_to_our_terms_of_service", {}, [{name: 'a', attrs: {href: '/legal#Terms', target:'_blank', rel:'noopener noreferrer'}}])}
div.three-d-secure-container.card.card-highlighted.card-border(ng-show="threeDSecureFlow")
.alert.alert-info.small(aria-live="assertive")
strong #{translate('card_must_be_authenticated_by_3dsecure')}
div.three-d-secure-recurly-container
script(type="text/javascript", nonce=scriptNonce).
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
script(
type="text/ng-template"
id="cvv-tooltip-tpl.html"
)
p !{translate("for_visa_mastercard_and_discover", {}, ['strong', 'strong', 'strong'])}
p !{translate("for_american_express", {}, ['strong', 'strong', 'strong'])}
+studentCheckModal

View file

@ -1,40 +0,0 @@
extends ../layout
block content
- var featuresPageVariant= splitTestVariants && splitTestVariants['features-page'] ? splitTestVariants['features-page'] : 'default'
- var featuresLink = featuresPageVariant === 'new' ? "/about/features-overview" : "/learn/how-to/Overleaf_premium_features"
- var featuresTranslationKey = featuresPageVariant === 'new' ? 'get_most_subscription_by_checking_features' : 'get_most_subscription_by_checking_premium_features'
- var featuresLinkSegmentation = {splitTest: 'features-page', splitTestVariant: featuresPageVariant}
main.content.content-alt#main-content
.container
.row
.col-md-8.col-md-offset-2
.card(ng-cloak)
.page-header
h2 #{translate("thanks_for_subscribing")}
.alert.alert-success
if (personalSubscription.recurly.trial_ends_at)
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount: personalSubscription.recurly.displayPrice, collectionDate: personalSubscription.recurly.nextPaymentDueAt}, ['strong', 'strong'])}
include ./_price_exceptions
p #{translate("to_modify_your_subscription_go_to")}
a(href="/user/subscription") #{translate("manage_subscription")}.
p
if (personalSubscription.groupPlan == true)
a.btn.btn-primary.btn-large(href=`/manage/groups/${personalSubscription._id}/members`) #{translate("add_your_first_group_member_now")}
p.letter-from-founders
p #{translate("thanks_for_subscribing_you_help_sl", {planName:personalSubscription.plan.name})}
p !{translate(featuresTranslationKey, {}, [{name: 'a', attrs: {href: featuresLink, 'event-tracking':'features-page-link', 'event-tracking-trigger': 'click', 'event-tracking-mb': 'true', 'event-segmentation': featuresLinkSegmentation}}])}
p #{translate("need_anything_contact_us_at")}
a(href=`mailto:${settings.adminEmail}`, ng-non-bindable) #{settings.adminEmail}
| .
p !{translate("help_improve_overleaf_fill_out_this_survey", {}, [{name: 'a', attrs: {href: 'https://forms.gle/CdLNX9m6NLxkv1yr5', target:'_blank', rel:'noopener noreferrer'}}])}
p #{translate("regards")},
br(ng-non-bindable)
| The #{settings.appName} Team
p
if (postCheckoutRedirect)
a.btn.btn-primary(href=postCheckoutRedirect) &lt; #{translate("back_to_your_projects")}
else
a.btn.btn-primary(href="/project") &lt; #{translate("back_to_your_projects")}

View file

@ -1,119 +0,0 @@
extends ../layout
block append meta
meta(name="ol-users", data-type="json", content=users)
meta(name="ol-paths", data-type="json", content=paths)
meta(name="ol-groupSize", data-type="json", content=groupSize)
block content
main.content.content-alt#main-content
.container
.row
.col-md-10.col-md-offset-1
h1(ng-non-bindable) #{name || translate(translations.title)}
.card(ng-controller="UserMembershipController")
.page-header
.pull-right(ng-cloak)
small(ng-show="groupSize && selectedUsers.length == 0") !{translate("you_have_added_x_of_group_size_y", {addedUsersSize:'{{ users.length }}', groupSize: '{{ groupSize }}'}, ['strong', 'strong'])}
a.btn.btn-danger(
href,
ng-show="selectedUsers.length > 0"
ng-click="removeMembers()"
) #{translate(translations.remove)}
h3 #{translate(translations.subtitle)}
.row-spaced-small
div(ng-if="inputs.removeMembers.error", ng-cloak)
div.alert.alert-danger(ng-if="inputs.removeMembers.errorMessage")
| #{translate('error')}:
| {{ inputs.removeMembers.errorMessage }}
div.alert.alert-danger(ng-if="!inputs.removeMembers.errorMessage")
| #{translate('generic_something_went_wrong')}
ul.list-unstyled.structured-list(
select-all-list,
ng-cloak
)
li.container-fluid
.row
.col-md-4
input.select-all(
select-all,
type="checkbox"
)
span.header #{translate("email")}
.col-md-4
span.header #{translate("name")}
.col-md-2
span.header(
tooltip=translate('last_active_description')
tooltip-placement="left"
tooltip-append-to-body="true"
)
| #{translate("last_active")}
sup (?)
.col-md-2
span.header #{translate("accepted_invite")}
li.container-fluid(
ng-repeat="user in users | orderBy:'email':true",
ng-controller="UserMembershipListItemController"
)
.row
.col-md-4
input.select-item(
select-individual,
type="checkbox",
ng-model="user.selected"
)
span.email {{ user.email }}
.col-md-4
span.name {{ user.first_name }} {{ user.last_name }}
.col-md-2
span.lastLogin {{ user.last_active_at | formatDate:'Do MMM YYYY' }}
.col-md-2
span.registered
i.fa.fa-check.text-success(ng-show="!user.invite" aria-hidden="true")
span.sr-only(ng-show="!user.invite") #{translate('accepted_invite')}
i.fa.fa-times(ng-show="user.invite" aria-hidden="true")
span.sr-only(ng-show="user.invite") #{translate('invite_not_accepted')}
li(
ng-if="users.length == 0",
ng-cloak
)
.row
.col-md-12.text-centered
small #{translate("no_members")}
hr
div(ng-if="!groupSize || users.length < groupSize", ng-cloak)
p.small #{translate("add_more_members")}
div(ng-if="inputs.addMembers.error", ng-cloak)
div.alert.alert-danger(ng-if="inputs.addMembers.errorMessage")
| #{translate('error')}:
| {{ inputs.addMembers.errorMessage }}
div.alert.alert-danger(ng-if="!inputs.addMembers.errorMessage")
| #{translate('generic_something_went_wrong')}
form.form
.row
.col-xs-6
input.form-control(
name="email",
type="text",
placeholder="jane@example.com, joe@example.com",
ng-model="inputs.addMembers.content",
on-enter="addMembers()"
aria-describedby="add-members-description"
)
.col-xs-4
button.btn.btn-primary(ng-click="addMembers()", ng-disabled="inputs.addMembers.inflightCount > 0")
span(ng-show="inputs.addMembers.inflightCount === 0") #{translate("add")}
span(ng-show="inputs.addMembers.inflightCount > 0") #{translate("adding")}…
.col-xs-2(ng-if="paths.exportMembers", ng-cloak)
a(href=paths.exportMembers) #{translate('export_csv')}
.row
.col-xs-8
span.help-block #{translate('add_comma_separated_emails_help')}
div(ng-if="groupSize && users.length >= groupSize && users.length > 0", ng-cloak)
.row
.col-xs-2.col-xs-offset-10(ng-if="paths.exportMembers", ng-cloak)
a(href=paths.exportMembers) #{translate('export_csv')}

View file

@ -14,17 +14,13 @@ import './main/account-settings'
import './main/clear-sessions'
import './main/account-upgrade-angular'
import './main/plans'
import './main/user-membership'
import './main/scribtex-popup'
import './main/event'
import './main/bonus'
import './main/system-messages'
import './main/translations'
import './main/subscription-dashboard'
import './main/new-subscription'
import './main/annual-upgrade'
import './main/subscription/team-invite-controller'
import './main/subscription/upgrade-subscription'
import './main/learn'
import './main/affiliations/components/inputSuggestions'
import './main/affiliations/factories/UserAffiliationsDataService'

View file

@ -1,793 +0,0 @@
import _ from 'lodash'
/* eslint-disable
camelcase,
max-len,
no-return-assign
*/
/* global recurly */
import App from '../base'
import getMeta from '../utils/meta'
import { assign } from '../shared/components/location'
export default App.controller(
'NewSubscriptionController',
function (
$scope,
$modal,
MultiCurrencyPricing,
$http,
$location,
eventTracking
) {
window.couponCode = $location.search().cc || ''
window.plan_code = $location.search().planCode || ''
window.ITMCampaign = $location.search().itm_campaign || ''
window.ITMContent = $location.search().itm_content || ''
window.ITMReferrer = $location.search().itm_referrer || ''
if (typeof recurly === 'undefined' || !recurly) {
$scope.recurlyLoadError = true
return
}
$scope.ui = {
showCurrencyDropdown: false,
showAddressSecondLine: false,
addCompanyDetails: false,
}
$scope.recurlyLoadError = false
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.initiallySelectedCurrencyCode = MultiCurrencyPricing.currencyCode
$scope.allCurrencies = MultiCurrencyPricing.plans
$scope.availableCurrencies = {}
$scope.planCode = window.plan_code
const isStudentCheckModalEnabled =
getMeta('ol-splitTestVariants')?.['student-check-modal'] === 'enabled'
if (isStudentCheckModalEnabled && $scope.planCode.includes('student')) {
$modal.open({
templateUrl: 'StudentCheckModalTemplate',
controller: 'StudentCheckModalController',
backdrop: 'static',
size: 'dialog-centered',
})
}
$scope.switchToStudent = function () {
const currentPlanCode = window.plan_code
const planCode = currentPlanCode.replace('collaborator', 'student')
eventTracking.sendMB('payment-page-switch-to-student', {
plan_code: window.plan_code,
})
eventTracking.send(
'subscription-funnel',
'subscription-form-switch-to-student',
window.plan_code
)
window.location =
'/user/subscription/new' +
`?planCode=${planCode}` +
`&currency=${$scope.currencyCode}` +
`&cc=${$scope.data.coupon}` +
`&itm_campaign=${window.ITMCampaign}` +
`&itm_content=${window.ITMContent}` +
`&itm_referrer=${window.ITMReferrer}`
}
eventTracking.sendMB('payment-page-view', {
plan: window.plan_code,
currency: $scope.currencyCode,
})
eventTracking.send(
'subscription-funnel',
'subscription-form-viewed',
window.plan_code
)
$scope.paymentMethod = { value: 'credit_card' }
$scope.data = {
first_name: '',
last_name: '',
postal_code: '',
address1: '',
address2: '',
state: '',
city: '',
company: '',
vat_number: '',
country: getMeta('ol-countryCode'),
coupon: window.couponCode,
}
$scope.validation = {}
$scope.processing = false
$scope.threeDSecureFlow = false
$scope.threeDSecureContainer = document.querySelector(
'.three-d-secure-container'
)
$scope.threeDSecureRecurlyContainer = document.querySelector(
'.three-d-secure-recurly-container'
)
recurly.configure({
publicKey: getMeta('ol-recurlyApiKey'),
style: {
all: {
fontFamily: '"Open Sans", sans-serif',
fontSize: '16px',
fontColor: '#7a7a7a',
},
month: {
placeholder: 'MM',
},
year: {
placeholder: 'YY',
},
cvv: {
placeholder: 'CVV',
},
},
})
const pricing = recurly.Pricing()
window.pricing = pricing
function setupPricing() {
pricing
.plan(window.plan_code, { quantity: 1 })
.address({
country: $scope.data.country,
})
.tax({ tax_code: 'digital', vat_number: '' })
.currency($scope.currencyCode)
.coupon($scope.data.coupon)
.catch(function (err) {
if (
$scope.currencyCode !== 'USD' &&
err.name === 'invalid-currency'
) {
$scope.currencyCode = 'USD'
setupPricing()
} else if (err.name === 'api-error' && err.code === 'not-found') {
// not-found here should refer to the coupon code, plan_code should be valid
$scope.$applyAsync(() => {
$scope.couponError = 'Coupon code is not valid for selected plan'
})
} else {
// Bail out on other errors, form state will not be correct
$scope.$applyAsync(() => {
$scope.recurlyLoadError = true
})
throw err
}
})
.done()
}
setupPricing()
pricing.on('change', () => {
$scope.planName = pricing.items.plan.name
if (pricing.items.plan.trial) {
$scope.trialLength = pricing.items.plan.trial.length
}
$scope.recurlyPrice = $scope.trialLength
? pricing.price.next
: pricing.price.now
$scope.taxes = pricing.price.taxes
$scope.monthlyBilling = pricing.items.plan.period.length === 1
$scope.availableCurrencies = {}
for (const currencyCode in pricing.items.plan.price) {
if (MultiCurrencyPricing.plans[currencyCode]) {
$scope.availableCurrencies[currencyCode] =
MultiCurrencyPricing.plans[currencyCode]
}
}
$scope.limitedCurrencies = {}
const limitedCurrencyCodes = ['USD', 'EUR', 'GBP']
if (
limitedCurrencyCodes.indexOf($scope.initiallySelectedCurrencyCode) ===
-1
) {
limitedCurrencyCodes.unshift($scope.initiallySelectedCurrencyCode)
}
limitedCurrencyCodes.forEach(currencyCode => {
$scope.limitedCurrencies[currencyCode] =
MultiCurrencyPricing.plans[currencyCode]
})
if (
pricing.items &&
pricing.items.coupon &&
pricing.items.coupon.discount &&
pricing.items.coupon.discount.type === 'percent'
) {
const basePrice = parseInt(pricing.price.base.plan.unit, 10)
$scope.coupon = {
singleUse: pricing.items.coupon.single_use,
normalPrice: basePrice,
name: pricing.items.coupon.name,
normalPriceWithoutTax: basePrice,
}
if (
pricing.items.coupon.applies_for_months > 0 &&
pricing.items.coupon.discount.rate &&
pricing.items.coupon.applies_for_months
) {
$scope.coupon.discountMonths = pricing.items.coupon.applies_for_months
$scope.coupon.discountRate = pricing.items.coupon.discount.rate * 100
}
if (pricing.price.taxes[0] && pricing.price.taxes[0].rate) {
$scope.coupon.normalPrice += basePrice * pricing.price.taxes[0].rate
}
} else {
$scope.coupon = null
}
$scope.$apply()
})
$scope.applyCoupon = () => {
$scope.couponError = ''
pricing
.coupon($scope.data.coupon)
.catch(err => {
if (err.name === 'api-error' && err.code === 'not-found') {
$scope.$applyAsync(() => {
$scope.couponError = 'Coupon code is not valid for selected plan'
})
} else {
$scope.$applyAsync(() => {
$scope.couponError =
'An error occured when verifying the coupon code'
})
throw err
}
})
.done()
}
$scope.showAddressSecondLine = function (e) {
e.preventDefault()
$scope.ui.showAddressSecondLine = true
}
$scope.showCurrencyDropdown = function (e) {
e.preventDefault()
$scope.ui.showCurrencyDropdown = true
}
const elements = recurly.Elements()
const card = elements.CardElement({
displayIcon: true,
style: {
inputType: 'mobileSelect',
fontColor: '#5d6879',
placeholder: {},
invalid: {
fontColor: '#a93529',
},
},
})
card.attach('#recurly-card-input')
card.on('change', state => {
$scope.$applyAsync(() => {
$scope.showCardElementInvalid =
!state.focus && !state.empty && !state.valid
$scope.cardIsValid = state.valid
})
})
$scope.applyVatNumber = () =>
pricing
.tax({ tax_code: 'digital', vat_number: $scope.data.vat_number })
.done()
$scope.changeCurrency = function (newCurrency) {
$scope.currencyCode = newCurrency
return pricing
.currency(newCurrency)
.catch(function (err) {
if (
$scope.currencyCode !== 'USD' &&
err.name === 'invalid-currency'
) {
$scope.changeCurrency('USD')
} else {
throw err
}
})
.done()
}
$scope.inputHasError = function (formItem) {
if (formItem == null) {
return false
}
return formItem.$touched && formItem.$invalid
}
$scope.isFormValid = function (form) {
if ($scope.paymentMethod.value === 'paypal') {
return $scope.data.country !== ''
} else {
return form.$valid && $scope.cardIsValid
}
}
$scope.updateCountry = () =>
pricing.address({ country: $scope.data.country }).done()
$scope.setPaymentMethod = function (method) {
$scope.paymentMethod.value = method
$scope.validation.errorFields = {}
$scope.genericError = ''
}
let cachedRecurlyBillingToken
const completeSubscription = function (
err,
recurlyBillingToken,
recurly3DSecureResultToken
) {
if (recurlyBillingToken) {
// temporary store the billing token as it might be needed when
// re-sending the request after SCA authentication
cachedRecurlyBillingToken = recurlyBillingToken
}
$scope.validation.errorFields = {}
if (err != null) {
eventTracking.sendMB('payment-page-form-error', err)
eventTracking.send('subscription-funnel', 'subscription-error')
// We may or may not be in a digest loop here depending on
// whether recurly could do validation locally, so do it async
$scope.$evalAsync(function () {
$scope.processing = false
$scope.genericError = err.message
_.each(
err.fields,
field => ($scope.validation.errorFields[field] = true)
)
})
} else {
const postData = {
_csrf: window.csrfToken,
recurly_token_id: cachedRecurlyBillingToken.id,
recurly_three_d_secure_action_result_token_id:
recurly3DSecureResultToken && recurly3DSecureResultToken.id,
subscriptionDetails: {
currencyCode: pricing.items.currency,
plan_code: pricing.items.plan.code,
coupon_code: pricing.items.coupon ? pricing.items.coupon.code : '',
first_name: $scope.data.first_name,
last_name: $scope.data.last_name,
isPaypal: $scope.paymentMethod.value === 'paypal',
address: {
address1: $scope.data.address1,
address2: $scope.data.address2,
country: $scope.data.country,
state: $scope.data.state,
zip: $scope.data.postal_code,
},
ITMCampaign: window.ITMCampaign,
ITMContent: window.ITMContent,
ITMReferrer: window.ITMReferrer,
},
}
if (
postData.subscriptionDetails.isPaypal &&
$scope.ui.addCompanyDetails
) {
postData.subscriptionDetails.billing_info = {}
if ($scope.data.company && $scope.data.company !== '') {
postData.subscriptionDetails.billing_info.company =
$scope.data.company
}
if ($scope.data.vat_number && $scope.data.vat_number !== '') {
postData.subscriptionDetails.billing_info.vat_number =
$scope.data.vat_number
}
}
eventTracking.sendMB('payment-page-form-submit', {
currencyCode: postData.subscriptionDetails.currencyCode,
plan_code: postData.subscriptionDetails.plan_code,
coupon_code: postData.subscriptionDetails.coupon_code,
isPaypal: postData.subscriptionDetails.isPaypal,
})
eventTracking.send(
'subscription-funnel',
'subscription-form-submitted',
postData.subscriptionDetails.plan_code
)
return $http
.post('/user/subscription/create', postData)
.then(function () {
eventTracking.sendMB('payment-page-form-success')
eventTracking.send(
'subscription-funnel',
'subscription-submission-success',
postData.subscriptionDetails.plan_code
)
window.location.href = '/user/subscription/thank-you'
})
.catch(response => {
$scope.processing = false
const { data } = response
$scope.genericError =
(data && data.message) ||
'Something went wrong processing the request'
if (data.threeDSecureActionTokenId) {
initThreeDSecure(data.threeDSecureActionTokenId)
}
})
}
}
$scope.submit = function () {
$scope.processing = true
if ($scope.paymentMethod.value === 'paypal') {
const opts = { description: $scope.planName }
recurly.paypal(opts, completeSubscription)
} else {
const tokenData = _.cloneDeep($scope.data)
if (!$scope.ui.addCompanyDetails) {
delete tokenData.company
delete tokenData.vat_number
}
recurly.token(elements, tokenData, completeSubscription)
}
}
const initThreeDSecure = function (threeDSecureActionTokenId) {
// instanciate and configure Recurly 3DSecure flow
const risk = recurly.Risk()
const threeDSecure = risk.ThreeDSecure({
actionTokenId: threeDSecureActionTokenId,
})
// on SCA verification error: show payment UI with the error message
threeDSecure.on('error', error => {
$scope.genericError = `Error: ${error.message}`
$scope.threeDSecureFlow = false
$scope.$apply()
})
// on SCA verification success: show payment UI in processing mode and
// resubmit the payment with the new token final success or error will be
// handled by `completeSubscription`
threeDSecure.on('token', recurly3DSecureResultToken => {
completeSubscription(null, null, recurly3DSecureResultToken)
$scope.genericError = null
$scope.threeDSecureFlow = false
$scope.processing = true
$scope.$apply()
})
// make sure the threeDSecureRecurlyContainer is empty (in case of
// retries) and show 3DSecure UI
$scope.threeDSecureRecurlyContainer.innerHTML = ''
$scope.threeDSecureFlow = true
threeDSecure.attach($scope.threeDSecureRecurlyContainer)
// scroll the UI into view (timeout needed to make sure the element is
// visible)
window.setTimeout(() => {
$scope.threeDSecureContainer.scrollIntoView()
}, 0)
}
// list taken from Recurly (see https://docs.recurly.com/docs/countries-provinces-and-states). Country code must exist on Recurly, so update with care
$scope.countries = [
{ code: 'AF', name: 'Afghanistan' },
{ code: 'AX', name: 'Åland Islands' },
{ code: 'AL', name: 'Albania' },
{ code: 'DZ', name: 'Algeria' },
{ code: 'AS', name: 'American Samoa' },
{ code: 'AD', name: 'Andorra' },
{ code: 'AO', name: 'Angola' },
{ code: 'AI', name: 'Anguilla' },
{ code: 'AQ', name: 'Antarctica' },
{ code: 'AG', name: 'Antigua and Barbuda' },
{ code: 'AR', name: 'Argentina' },
{ code: 'AM', name: 'Armenia' },
{ code: 'AW', name: 'Aruba' },
{ code: 'AC', name: 'Ascension Island' },
{ code: 'AU', name: 'Australia' },
{ code: 'AT', name: 'Austria' },
{ code: 'AZ', name: 'Azerbaijan' },
{ code: 'BS', name: 'Bahamas' },
{ code: 'BH', name: 'Bahrain' },
{ code: 'BD', name: 'Bangladesh' },
{ code: 'BB', name: 'Barbados' },
{ code: 'BY', name: 'Belarus' },
{ code: 'BE', name: 'Belgium' },
{ code: 'BZ', name: 'Belize' },
{ code: 'BJ', name: 'Benin' },
{ code: 'BM', name: 'Bermuda' },
{ code: 'BT', name: 'Bhutan' },
{ code: 'BO', name: 'Bolivia' },
{ code: 'BA', name: 'Bosnia and Herzegovina' },
{ code: 'BW', name: 'Botswana' },
{ code: 'BV', name: 'Bouvet Island' },
{ code: 'BR', name: 'Brazil' },
{ code: 'BQ', name: 'British Antarctic Territory' },
{ code: 'IO', name: 'British Indian Ocean Territory' },
{ code: 'VG', name: 'British Virgin Islands' },
{ code: 'BN', name: 'Brunei' },
{ code: 'BG', name: 'Bulgaria' },
{ code: 'BF', name: 'Burkina Faso' },
{ code: 'BI', name: 'Burundi' },
{ code: 'CV', name: 'Cabo Verde' },
{ code: 'KH', name: 'Cambodia' },
{ code: 'CM', name: 'Cameroon' },
{ code: 'CA', name: 'Canada' },
{ code: 'IC', name: 'Canary Islands' },
{ code: 'CT', name: 'Canton and Enderbury Islands' },
{ code: 'KY', name: 'Cayman Islands' },
{ code: 'CF', name: 'Central African Republic' },
{ code: 'EA', name: 'Ceuta and Melilla' },
{ code: 'TD', name: 'Chad' },
{ code: 'CL', name: 'Chile' },
{ code: 'CN', name: 'China' },
{ code: 'CX', name: 'Christmas Island' },
{ code: 'CP', name: 'Clipperton Island' },
{ code: 'CC', name: 'Cocos [Keeling] Islands' },
{ code: 'CO', name: 'Colombia' },
{ code: 'KM', name: 'Comoros' },
{ code: 'CG', name: 'Congo - Brazzaville' },
{ code: 'CD', name: 'Congo - Kinshasa' },
{ code: 'CD', name: 'Congo [DRC]' },
{ code: 'CG', name: 'Congo [Republic]' },
{ code: 'CK', name: 'Cook Islands' },
{ code: 'CR', name: 'Costa Rica' },
{ code: 'CI', name: 'Côte dIvoire' },
{ code: 'HR', name: 'Croatia' },
{ code: 'CU', name: 'Cuba' },
{ code: 'CY', name: 'Cyprus' },
{ code: 'CZ', name: 'Czech Republic' },
{ code: 'DK', name: 'Denmark' },
{ code: 'DG', name: 'Diego Garcia' },
{ code: 'DJ', name: 'Djibouti' },
{ code: 'DM', name: 'Dominica' },
{ code: 'DO', name: 'Dominican Republic' },
{ code: 'NQ', name: 'Dronning Maud Land' },
{ code: 'TL', name: 'East Timor' },
{ code: 'EC', name: 'Ecuador' },
{ code: 'EG', name: 'Egypt' },
{ code: 'SV', name: 'El Salvador' },
{ code: 'GQ', name: 'Equatorial Guinea' },
{ code: 'ER', name: 'Eritrea' },
{ code: 'EE', name: 'Estonia' },
{ code: 'ET', name: 'Ethiopia' },
{ code: 'FK', name: 'Falkland Islands [Islas Malvinas]' },
{ code: 'FK', name: 'Falkland Islands' },
{ code: 'FO', name: 'Faroe Islands' },
{ code: 'FJ', name: 'Fiji' },
{ code: 'FI', name: 'Finland' },
{ code: 'FR', name: 'France' },
{ code: 'GF', name: 'French Guiana' },
{ code: 'PF', name: 'French Polynesia' },
{ code: 'FQ', name: 'French Southern and Antarctic Territories' },
{ code: 'TF', name: 'French Southern Territories' },
{ code: 'GA', name: 'Gabon' },
{ code: 'GM', name: 'Gambia' },
{ code: 'GE', name: 'Georgia' },
{ code: 'DE', name: 'Germany' },
{ code: 'GH', name: 'Ghana' },
{ code: 'GI', name: 'Gibraltar' },
{ code: 'GR', name: 'Greece' },
{ code: 'GL', name: 'Greenland' },
{ code: 'GD', name: 'Grenada' },
{ code: 'GP', name: 'Guadeloupe' },
{ code: 'GU', name: 'Guam' },
{ code: 'GT', name: 'Guatemala' },
{ code: 'GG', name: 'Guernsey' },
{ code: 'GW', name: 'Guinea-Bissau' },
{ code: 'GN', name: 'Guinea' },
{ code: 'GY', name: 'Guyana' },
{ code: 'HT', name: 'Haiti' },
{ code: 'HM', name: 'Heard Island and McDonald Islands' },
{ code: 'HN', name: 'Honduras' },
{ code: 'HK', name: 'Hong Kong' },
{ code: 'HU', name: 'Hungary' },
{ code: 'IS', name: 'Iceland' },
{ code: 'IN', name: 'India' },
{ code: 'ID', name: 'Indonesia' },
{ code: 'IR', name: 'Iran' },
{ code: 'IQ', name: 'Iraq' },
{ code: 'IE', name: 'Ireland' },
{ code: 'IM', name: 'Isle of Man' },
{ code: 'IL', name: 'Israel' },
{ code: 'IT', name: 'Italy' },
{ code: 'CI', name: 'Ivory Coast' },
{ code: 'JM', name: 'Jamaica' },
{ code: 'JP', name: 'Japan' },
{ code: 'JE', name: 'Jersey' },
{ code: 'JT', name: 'Johnston Island' },
{ code: 'JO', name: 'Jordan' },
{ code: 'KZ', name: 'Kazakhstan' },
{ code: 'KE', name: 'Kenya' },
{ code: 'KI', name: 'Kiribati' },
{ code: 'KW', name: 'Kuwait' },
{ code: 'KG', name: 'Kyrgyzstan' },
{ code: 'LA', name: 'Laos' },
{ code: 'LV', name: 'Latvia' },
{ code: 'LB', name: 'Lebanon' },
{ code: 'LS', name: 'Lesotho' },
{ code: 'LR', name: 'Liberia' },
{ code: 'LY', name: 'Libya' },
{ code: 'LI', name: 'Liechtenstein' },
{ code: 'LT', name: 'Lithuania' },
{ code: 'LU', name: 'Luxembourg' },
{ code: 'MO', name: 'Macau SAR China' },
{ code: 'MO', name: 'Macau' },
{ code: 'MK', name: 'Macedonia [FYROM]' },
{ code: 'MK', name: 'Macedonia' },
{ code: 'MG', name: 'Madagascar' },
{ code: 'MW', name: 'Malawi' },
{ code: 'MY', name: 'Malaysia' },
{ code: 'MV', name: 'Maldives' },
{ code: 'ML', name: 'Mali' },
{ code: 'MT', name: 'Malta' },
{ code: 'MH', name: 'Marshall Islands' },
{ code: 'MQ', name: 'Martinique' },
{ code: 'MR', name: 'Mauritania' },
{ code: 'MU', name: 'Mauritius' },
{ code: 'YT', name: 'Mayotte' },
{ code: 'FX', name: 'Metropolitan France' },
{ code: 'MX', name: 'Mexico' },
{ code: 'FM', name: 'Micronesia' },
{ code: 'MI', name: 'Midway Islands' },
{ code: 'MD', name: 'Moldova' },
{ code: 'MC', name: 'Monaco' },
{ code: 'MN', name: 'Mongolia' },
{ code: 'ME', name: 'Montenegro' },
{ code: 'MS', name: 'Montserrat' },
{ code: 'MA', name: 'Morocco' },
{ code: 'MZ', name: 'Mozambique' },
{ code: 'MM', name: 'Myanmar [Burma]' },
{ code: 'NA', name: 'Namibia' },
{ code: 'NR', name: 'Nauru' },
{ code: 'NP', name: 'Nepal' },
{ code: 'AN', name: 'Netherlands Antilles' },
{ code: 'NL', name: 'Netherlands' },
{ code: 'NC', name: 'New Caledonia' },
{ code: 'NZ', name: 'New Zealand' },
{ code: 'NI', name: 'Nicaragua' },
{ code: 'NE', name: 'Niger' },
{ code: 'NG', name: 'Nigeria' },
{ code: 'NU', name: 'Niue' },
{ code: 'NF', name: 'Norfolk Island' },
{ code: 'KP', name: 'North Korea' },
{ code: 'VD', name: 'North Vietnam' },
{ code: 'MP', name: 'Northern Mariana Islands' },
{ code: 'NO', name: 'Norway' },
{ code: 'OM', name: 'Oman' },
{ code: 'QO', name: 'Outlying Oceania' },
{ code: 'PC', name: 'Pacific Islands Trust Territory' },
{ code: 'PK', name: 'Pakistan' },
{ code: 'PW', name: 'Palau' },
{ code: 'PS', name: 'Palestinian Territories' },
{ code: 'PZ', name: 'Panama Canal Zone' },
{ code: 'PA', name: 'Panama' },
{ code: 'PG', name: 'Papua New Guinea' },
{ code: 'PY', name: 'Paraguay' },
{ code: 'YD', name: "People's Democratic Republic of Yemen" },
{ code: 'PE', name: 'Peru' },
{ code: 'PH', name: 'Philippines' },
{ code: 'PN', name: 'Pitcairn Islands' },
{ code: 'PL', name: 'Poland' },
{ code: 'PT', name: 'Portugal' },
{ code: 'PR', name: 'Puerto Rico' },
{ code: 'QA', name: 'Qatar' },
{ code: 'RE', name: 'Réunion' },
{ code: 'RO', name: 'Romania' },
{ code: 'RU', name: 'Russia' },
{ code: 'RW', name: 'Rwanda' },
{ code: 'BL', name: 'Saint Barthélemy' },
{ code: 'SH', name: 'Saint Helena' },
{ code: 'KN', name: 'Saint Kitts and Nevis' },
{ code: 'LC', name: 'Saint Lucia' },
{ code: 'MF', name: 'Saint Martin' },
{ code: 'PM', name: 'Saint Pierre and Miquelon' },
{ code: 'VC', name: 'Saint Vincent and the Grenadines' },
{ code: 'WS', name: 'Samoa' },
{ code: 'SM', name: 'San Marino' },
{ code: 'ST', name: 'São Tomé and Príncipe' },
{ code: 'SA', name: 'Saudi Arabia' },
{ code: 'SN', name: 'Senegal' },
{ code: 'CS', name: 'Serbia and Montenegro' },
{ code: 'RS', name: 'Serbia' },
{ code: 'SC', name: 'Seychelles' },
{ code: 'SL', name: 'Sierra Leone' },
{ code: 'SG', name: 'Singapore' },
{ code: 'SK', name: 'Slovakia' },
{ code: 'SI', name: 'Slovenia' },
{ code: 'SB', name: 'Solomon Islands' },
{ code: 'SO', name: 'Somalia' },
{ code: 'ZA', name: 'South Africa' },
{ code: 'GS', name: 'South Georgia and the South Sandwich Islands' },
{ code: 'KR', name: 'South Korea' },
{ code: 'ES', name: 'Spain' },
{ code: 'LK', name: 'Sri Lanka' },
{ code: 'SD', name: 'Sudan' },
{ code: 'SR', name: 'Suriname' },
{ code: 'SJ', name: 'Svalbard and Jan Mayen' },
{ code: 'SZ', name: 'Swaziland' },
{ code: 'SE', name: 'Sweden' },
{ code: 'CH', name: 'Switzerland' },
{ code: 'SY', name: 'Syria' },
{ code: 'TW', name: 'Taiwan' },
{ code: 'TJ', name: 'Tajikistan' },
{ code: 'TZ', name: 'Tanzania' },
{ code: 'TH', name: 'Thailand' },
{ code: 'TL', name: 'Timor-Leste' },
{ code: 'TG', name: 'Togo' },
{ code: 'TK', name: 'Tokelau' },
{ code: 'TO', name: 'Tonga' },
{ code: 'TT', name: 'Trinidad and Tobago' },
{ code: 'TA', name: 'Tristan da Cunha' },
{ code: 'TN', name: 'Tunisia' },
{ code: 'TR', name: 'Turkey' },
{ code: 'TM', name: 'Turkmenistan' },
{ code: 'TC', name: 'Turks and Caicos Islands' },
{ code: 'TV', name: 'Tuvalu' },
{ code: 'UM', name: 'U.S. Minor Outlying Islands' },
{ code: 'PU', name: 'U.S. Miscellaneous Pacific Islands' },
{ code: 'VI', name: 'U.S. Virgin Islands' },
{ code: 'UG', name: 'Uganda' },
{ code: 'UA', name: 'Ukraine' },
{ code: 'AE', name: 'United Arab Emirates' },
{ code: 'GB', name: 'United Kingdom' },
{ code: 'US', name: 'United States' },
{ code: 'UY', name: 'Uruguay' },
{ code: 'UZ', name: 'Uzbekistan' },
{ code: 'VU', name: 'Vanuatu' },
{ code: 'VA', name: 'Vatican City' },
{ code: 'VE', name: 'Venezuela' },
{ code: 'VN', name: 'Vietnam' },
{ code: 'WK', name: 'Wake Island' },
{ code: 'WF', name: 'Wallis and Futuna' },
{ code: 'EH', name: 'Western Sahara' },
{ code: 'YE', name: 'Yemen' },
{ code: 'ZM', name: 'Zambia' },
{ code: 'ZW', name: 'Zimbabwe' },
]
}
)
App.controller(
'StudentCheckModalController',
function ($scope, $modalInstance, eventTracking) {
$modalInstance.rendered.then(() => {
eventTracking.sendMB('student-check-displayed')
})
$scope.browsePlans = () => {
if (document.referrer?.includes('/user/subscription/choose-your-plan')) {
// redirect to interstitial page with `itm_referrer` param
assign(
'/user/subscription/choose-your-plan?itm_referrer=student-status-declined'
)
} else {
// redirect to plans page with `itm_referrer` param
assign('/user/subscription/plans?itm_referrer=student-status-declined')
}
}
$scope.confirm = () => $modalInstance.dismiss('cancel')
}
)

View file

@ -1,451 +0,0 @@
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 GROUP_PLAN_MODAL_OPTIONS = getMeta('ol-groupPlanModalOptions')
const ensureRecurlyIsSetup = _.once(() => {
if (typeof recurly === 'undefined' || !recurly) {
return false
}
recurly.configure(getMeta('ol-recurlyApiKey'))
return true
})
function getPricePerUser(price, currencySymbol, size) {
let perUserPrice = price / size
if (perUserPrice % 1 !== 0) {
perUserPrice = perUserPrice.toFixed(2)
}
return `${currencySymbol}${perUserPrice}`
}
App.controller('MetricsEmailController', function ($scope, $http) {
$scope.institutionEmailSubscription = function (institutionId) {
const 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 taxAmount = totalPriceExTax * taxRate
if (isNaN(taxAmount)) {
taxAmount = 0
}
let total = totalPriceExTax + taxAmount
if (total % 1 !== 0) {
total = total.toFixed(2)
}
resolve({
total: `${currencySymbol}${total}`,
totalValue: total,
subtotal: `${currencySymbol}${totalPriceExTax.toFixed(2)}`,
tax: `${currencySymbol}${taxAmount.toFixed(2)}`,
includesTax: taxAmount !== 0,
})
})
})
},
}
})
App.controller('ChangePlanToGroupFormController', function ($scope, $modal) {
if (!ensureRecurlyIsSetup()) return
const subscription = getMeta('ol-subscription')
const currency = subscription.recurly.currency
const validCurrencies = GROUP_PLAN_MODAL_OPTIONS.currencies.map(
item => item.code
)
if (validCurrencies.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, RecurlyPricing) {
$scope.options = GROUP_PLAN_MODAL_OPTIONS
$scope.groupPlans = 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 subscription = getMeta('ol-subscription')
const { taxRate } = subscription.recurly
const { usage, plan_code, currency, size } = $scope.selected
$scope.discountEligible = size >= 10
const recurlyPricePlaceholder = { total: '...' }
let perUserDisplayPricePlaceholder = '...'
const currencySymbol = $scope.options.currencySymbols[currency]
if (taxRate === 0) {
const basePriceInCents =
$scope.groupPlans[usage][plan_code][currency][size].price_in_cents
const basePriceInUnit = (basePriceInCents / 100).toFixed()
recurlyPricePlaceholder.total = `${currencySymbol}${basePriceInUnit}`
perUserDisplayPricePlaceholder = getPricePerUser(
basePriceInUnit,
currencySymbol,
size
)
}
$scope.recurlyPrice = recurlyPricePlaceholder // Placeholder while we talk to recurly
$scope.perUserDisplayPrice = perUserDisplayPricePlaceholder // Placeholder while we talk to recurly
const recurlyPlanCode = `group_${plan_code}_${size}_${usage}`
RecurlyPricing.loadDisplayPriceWithTax(
recurlyPlanCode,
currency,
taxRate
).then(price => {
$scope.recurlyPrice = price
$scope.perUserDisplayPrice = getPricePerUser(
price.totalValue,
currencySymbol,
size
)
})
}
$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) {
if (!ensureRecurlyIsSetup()) return
function stripCentsIfZero(displayPrice) {
return displayPrice ? displayPrice.replace(/\.00$/, '') : '...'
}
$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
if (subscription.recurly.displayPrice) {
if (subscription.pendingPlan?.planCode === planCode) {
$scope.displayPrice = stripCentsIfZero(
subscription.recurly.displayPrice
)
return
}
if (subscription.planCode === planCode) {
if (subscription.pendingPlan) {
$scope.displayPrice = stripCentsIfZero(
subscription.recurly.currentPlanDisplayPrice
)
} else {
$scope.displayPrice = stripCentsIfZero(
subscription.recurly.displayPrice
)
}
return
}
}
$scope.displayPrice = '...' // Placeholder while we talk to recurly
RecurlyPricing.loadDisplayPriceWithTax(planCode, currency, taxRate).then(
recurlyPrice => {
$scope.displayPrice = recurlyPrice.total
}
)
})
}
)
App.controller(
'ConfirmChangePlanController',
function ($scope, $modalInstance, $http) {
$scope.confirmChangePlan = function () {
const body = {
plan_code: $scope.plan.planCode,
_csrf: window.csrfToken,
}
$scope.genericError = false
$scope.inflight = true
return $http
.post(`${SUBSCRIPTION_URL}?origin=confirmChangePlan`, body)
.then(() => location.reload())
.catch(() => {
$scope.genericError = true
$scope.inflight = false
})
}
return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
}
)
App.controller(
'CancelPendingPlanChangeController',
function ($scope, $modalInstance, $http) {
$scope.confirmCancelPendingPlanChange = function () {
const body = {
_csrf: window.csrfToken,
}
$scope.genericError = false
$scope.inflight = true
return $http
.post('/user/subscription/cancel-pending', body)
.then(() => location.reload())
.catch(() => {
$scope.genericError = true
$scope.inflight = false
})
}
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(() => window.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.showDowngrade = true
} else {
$scope.showBasicCancel = true
}
const planCode = 'paid-personal'
const { currency, taxRate } = subscription.recurly
$scope.personalDisplayPrice = '...' // Placeholder while we talk to recurly
RecurlyPricing.loadDisplayPriceWithTax(planCode, currency, taxRate).then(
price => {
$scope.personalDisplayPrice = price.total
}
)
$scope.downgradeToPaidPersonal = function () {
const body = {
plan_code: planCode,
_csrf: window.csrfToken,
}
$scope.inflight = true
return $http
.post(`${SUBSCRIPTION_URL}?origin=downgradeToPaidPersonal`, 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'))
}
}
)

View file

@ -1,13 +0,0 @@
import App from '../../base'
export default App.controller(
'UpgradeSubscriptionController',
function ($scope, eventTracking) {
$scope.upgradeSubscription = function () {
eventTracking.send('subscription-funnel', 'subscription-page', 'upgrade')
eventTracking.sendMB('upgrade-button-click', {
source: 'subscription-page',
})
}
}
)

View file

@ -1,126 +0,0 @@
import _ from 'lodash'
/* eslint-disable
max-len,
no-return-assign,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import App from '../base'
App.controller('UserMembershipController', function ($scope, queuedHttp) {
$scope.users = window.users
$scope.groupSize = window.groupSize
$scope.paths = window.paths
$scope.selectedUsers = []
$scope.inputs = {
addMembers: {
content: '',
error: false,
errorMessage: null,
inflightCount: 0,
},
removeMembers: {
error: false,
errorMessage: null,
},
}
const parseEmails = function (emailsString) {
const regexBySpaceOrComma = /[\s,]+/
let emails = emailsString.split(regexBySpaceOrComma)
emails = _.map(emails, email => (email = email.trim()))
emails = _.filter(emails, email => email.indexOf('@') !== -1)
return emails
}
$scope.addMembers = function () {
$scope.inputs.addMembers.error = false
$scope.inputs.addMembers.errorMessage = null
$scope.inputs.addMembers.inflightCount = 0
const emails = parseEmails($scope.inputs.addMembers.content)
return Array.from(emails).map(email => {
$scope.inputs.addMembers.inflightCount += 1
return queuedHttp
.post(window.paths.addMember, {
email,
_csrf: window.csrfToken,
})
.then(function (response) {
$scope.inputs.addMembers.inflightCount -= 1
const { data } = response
if (data.user != null) {
const alreadyListed = $scope.users.find(
scopeUser => scopeUser.email === data.user.email
)
if (!alreadyListed) {
$scope.users.push(data.user)
}
}
return ($scope.inputs.addMembers.content = '')
})
.catch(function (response) {
$scope.inputs.addMembers.inflightCount -= 1
const { data } = response
$scope.inputs.addMembers.error = true
return ($scope.inputs.addMembers.errorMessage =
data.error != null ? data.error.message : undefined)
})
})
}
$scope.removeMembers = function () {
$scope.inputs.removeMembers.error = false
$scope.inputs.removeMembers.errorMessage = null
for (const user of Array.from($scope.selectedUsers)) {
;(function (user) {
let url
if (window.paths.removeInvite && user.invite && user._id == null) {
url = `${window.paths.removeInvite}/${encodeURIComponent(user.email)}`
} else if (window.paths.removeMember && user._id != null) {
url = `${window.paths.removeMember}/${user._id}`
} else {
return
}
return queuedHttp({
method: 'DELETE',
url,
headers: {
'X-Csrf-Token': window.csrfToken,
},
})
.then(function () {
const index = $scope.users.indexOf(user)
if (index === -1) {
return
}
return $scope.users.splice(index, 1)
})
.catch(function (response) {
const { data } = response
$scope.inputs.removeMembers.error = true
return ($scope.inputs.removeMembers.errorMessage =
data.error != null ? data.error.message : undefined)
})
})(user)
}
return $scope.updateSelectedUsers
}
return ($scope.updateSelectedUsers = () =>
($scope.selectedUsers = $scope.users.filter(user => user.selected)))
})
export default App.controller('UserMembershipListItemController', $scope =>
$scope.$watch('user.selected', function (value) {
if (value != null) {
return $scope.updateSelectedUsers()
}
})
)

View file

@ -272,7 +272,6 @@
"contact_message_label": "Message",
"contact_sales": "Contact Sales",
"contact_support_to_change_group_subscription": "Please <0>contact support</0> if you wish to change your group subscription.",
"contact_support_to_upgrade_to_group_subscription": "Please <0>contact support</0> if you wish to be upgraded to a group subscription.",
"contact_us": "Contact Us",
"contact_us_lowercase": "Contact us",
"continue": "Continue",
@ -1476,7 +1475,6 @@
"stop_on_first_error_enabled_description": "<0>“Stop on first error” is enabled.</0> Disabling it may allow the compiler to produce a PDF (but your project will still have errors).",
"stop_on_first_error_enabled_title": "No PDF: Stop on first error enabled",
"stop_on_validation_error": "Check syntax before compile",
"stop_your_subscription": "Stop Your Subscription",
"store_your_work": "Store your work on your own infrastructure",
"student": "Student",
"student_and_faculty_support_make_difference": "Student and faculty support make a difference! We can share this information with our contacts at your university when discussing an Overleaf institutional account.",

View file

@ -78,6 +78,9 @@ describe('SubscriptionController', function () {
promises: {
buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
},
buildPlansListForSubscriptionDash: sinon
.stub()
.returns({ plans: [], planCodesChangingAtTermEnd: [] }),
}
this.settings = {
coupon_codes: {
@ -283,10 +286,10 @@ describe('SubscriptionController', function () {
describe('with a valid plan code', function () {
it('should render the new subscription page', function (done) {
this.res.render = (page, opts) => {
page.should.equal('subscriptions/new-refreshed')
page.should.equal('subscriptions/new-react')
done()
}
this.SubscriptionController.paymentPage(this.req, this.res)
this.SubscriptionController.paymentPage(this.req, this.res, done)
})
})
})
@ -407,7 +410,7 @@ describe('SubscriptionController', function () {
}
)
this.res.render = (url, variables) => {
url.should.equal('subscriptions/successful-subscription')
url.should.equal('subscriptions/successful-subscription-react')
assert.deepEqual(variables, {
title: 'thank_you',
personalSubscription: 'foo',
@ -448,13 +451,19 @@ describe('SubscriptionController', function () {
this.SubscriptionViewModelBuilder.buildPlansList.returns(
(this.plans = { plans: 'mock' })
)
this.SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash.returns(
{
plans: this.plans,
planCodesChangingAtTermEnd: [],
}
)
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(false)
this.res.render = (view, data) => {
this.data = data
expect(view).to.equal('subscriptions/dashboard')
expect(view).to.equal('subscriptions/dashboard-react')
done()
}
this.SubscriptionController.userSubscriptionPage(this.req, this.res)
this.SubscriptionController.userSubscriptionPage(this.req, this.res, done)
})
it('should load the personal, groups and v1 subscriptions', function () {

View file

@ -110,13 +110,9 @@ describe('UserMembershipController', function () {
it('render group view', async function () {
return await this.UserMembershipController.manageGroupMembers(this.req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index')
expect(viewPath).to.equal('user_membership/group-members-react')
expect(viewParams.users).to.deep.equal(this.users)
expect(viewParams.groupSize).to.equal(this.subscription.membersLimit)
expect(viewParams.translations.title).to.equal('group_subscription')
expect(viewParams.paths.addMember).to.equal(
`/manage/groups/${this.subscription._id}/invites`
)
},
})
})
@ -125,13 +121,8 @@ describe('UserMembershipController', function () {
this.req.entityConfig = EntityConfigs.groupManagers
return await this.UserMembershipController.manageGroupManagers(this.req, {
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index')
expect(viewPath).to.equal('user_membership/group-managers-react')
expect(viewParams.groupSize).to.equal(undefined)
expect(viewParams.translations.title).to.equal('group_subscription')
expect(viewParams.translations.subtitle).to.equal(
'managers_management'
)
expect(viewParams.paths.exportMembers).to.be.undefined
},
})
})
@ -143,13 +134,11 @@ describe('UserMembershipController', function () {
this.req,
{
render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index')
expect(viewPath).to.equal(
'user_membership/institution-managers-react'
)
expect(viewParams.name).to.equal('Test Institution Name')
expect(viewParams.groupSize).to.equal(undefined)
expect(viewParams.translations.title).to.equal(
'institution_account'
)
expect(viewParams.paths.exportMembers).to.be.undefined
},
}
)