mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
37f01dfe1e
commit
150cf21710
27 changed files with 50 additions and 2767 deletions
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") < #{translate("back_to_your_projects")}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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")}…
|
|
@ -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
|
|
@ -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
|
||||
|
|
||||
| Manage members
|
||||
|
|
||||
p
|
||||
a(href="/manage/groups/" + managedGroupSubscription._id + "/managers")
|
||||
i.fa.fa-fw.fa-users
|
||||
|
|
||||
| Manage group managers
|
||||
|
|
||||
p
|
||||
a(href="/metrics/groups/" + managedGroupSubscription._id)
|
||||
i.fa.fa-fw.fa-line-chart
|
||||
|
|
||||
| View metrics
|
||||
|
||||
hr
|
|
@ -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
|
||||
|
|
||||
| View metrics
|
||||
p
|
||||
a(href="/institutions/" + institution.v1Id + "/hub")
|
||||
i.fa.fa-fw.fa-user-circle
|
||||
|
|
||||
| View hub
|
||||
p
|
||||
a(href="/manage/institutions/" + institution.v1Id + "/managers")
|
||||
i.fa.fa-fw.fa-users
|
||||
|
|
||||
| Manage institution managers
|
||||
div(ng-controller="MetricsEmailController", ng-cloak)
|
||||
p
|
||||
span Monthly metrics emails:
|
||||
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
|
|
@ -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
|
||||
|
|
||||
| View hub
|
||||
p
|
||||
a(href="/manage/publishers/" + publisher.slug + "/managers")
|
||||
i.fa.fa-fw.fa-users
|
||||
|
|
||||
| Manage publisher managers
|
||||
hr
|
|
@ -1,7 +0,0 @@
|
|||
if (personalSubscription.recurly)
|
||||
include ./_personal_subscription_recurly
|
||||
include ./_personal_subscription_recurly_sync_email
|
||||
else
|
||||
include ./_personal_subscription_custom
|
||||
|
||||
hr
|
|
@ -1,6 +0,0 @@
|
|||
p
|
||||
| Please
|
||||
|
|
||||
a(href="/contact") contact support
|
||||
|
|
||||
| to make changes to your plan
|
|
@ -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")}
|
||||
|
|
||||
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'])}
|
||||
|
|
||||
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")}
|
||||
|
|
||||
a(href=personalSubscription.recurly.accountManagementLink, target="_blank").btn.btn-secondary-info.btn-secondary #{translate("view_your_invoices")}
|
||||
|
|
||||
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")}
|
||||
|
|
||||
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
|
|
@ -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")}…
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
| –{{ 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
|
||||
|
|
||||
i.fa.fa-cc-visa(aria-hidden="true")
|
||||
|
|
||||
i.fa.fa-cc-mastercard(aria-hidden="true")
|
||||
|
|
||||
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
|
||||
|
|
||||
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')}
|
||||
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')}
|
||||
|
|
||||
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
|
|
@ -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) < #{translate("back_to_your_projects")}
|
||||
else
|
||||
a.btn.btn-primary(href="/project") < #{translate("back_to_your_projects")}
|
|
@ -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')}
|
|
@ -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'
|
||||
|
|
|
@ -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}` +
|
||||
`¤cy=${$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 d’Ivoire' },
|
||||
{ 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')
|
||||
}
|
||||
)
|
|
@ -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'))
|
||||
}
|
||||
}
|
||||
)
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
)
|
|
@ -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.",
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue