mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #5117 from overleaf/jpa-plans-de-ng
[web] de-ng plans page GitOrigin-RevId: b902b10b45ab3fd46e71545c94f65e8f079c4564
This commit is contained in:
@@ -18,6 +18,17 @@ const AnalyticsManager = require('../Analytics/AnalyticsManager')
|
||||
const RecurlyEventHandler = require('./RecurlyEventHandler')
|
||||
const { expressify } = require('../../util/promises')
|
||||
const OError = require('@overleaf/o-error')
|
||||
const {
|
||||
getAssignmentForSession,
|
||||
} = require('../SplitTests/SplitTestV2Handler').promises
|
||||
|
||||
const groupPlanModalOptions = Settings.groupPlanModalOptions
|
||||
const validGroupPlanModalOptions = {
|
||||
plan_code: groupPlanModalOptions.plan_codes.map(item => item.code),
|
||||
currency: groupPlanModalOptions.currencies.map(item => item.code),
|
||||
size: groupPlanModalOptions.sizes,
|
||||
usage: groupPlanModalOptions.usages.map(item => item.code),
|
||||
}
|
||||
|
||||
async function plansPage(req, res) {
|
||||
const plans = SubscriptionViewModelBuilder.buildPlansList()
|
||||
@@ -28,14 +39,46 @@ async function plansPage(req, res) {
|
||||
(req.query ? req.query.ip : undefined) || req.ip
|
||||
)
|
||||
|
||||
res.render('subscriptions/plans', {
|
||||
function getDefault(param, category, defaultValue) {
|
||||
const v = req.query && req.query[param]
|
||||
if (v && validGroupPlanModalOptions[category].includes(v)) {
|
||||
return v
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
let defaultGroupPlanModalCurrency = 'USD'
|
||||
if (validGroupPlanModalOptions.currency.includes(recommendedCurrency)) {
|
||||
defaultGroupPlanModalCurrency = recommendedCurrency
|
||||
}
|
||||
const groupPlanModalDefaults = {
|
||||
plan_code: getDefault('plan', 'plan_code', 'collaborator'),
|
||||
size: getDefault('number', 'size', '10'),
|
||||
currency: getDefault('currency', 'currency', defaultGroupPlanModalCurrency),
|
||||
usage: getDefault('usage', 'usage', 'enterprise'),
|
||||
}
|
||||
|
||||
const { variant: templateVariant } = await getAssignmentForSession(
|
||||
req.session,
|
||||
'plans-page-de-ng'
|
||||
)
|
||||
const template =
|
||||
templateVariant === 'de-ng'
|
||||
? 'subscriptions/plans-marketing'
|
||||
: 'subscriptions/plans'
|
||||
|
||||
res.render(template, {
|
||||
title: 'plans_and_pricing',
|
||||
plans,
|
||||
gaExperiments: Settings.gaExperiments.plansPage,
|
||||
gaOptimize: true,
|
||||
itm_content: req.query && req.query.itm_content,
|
||||
recomendedCurrency: recommendedCurrency,
|
||||
recommendedCurrency,
|
||||
planFeatures,
|
||||
groupPlans: GroupPlansData,
|
||||
groupPlanModalOptions,
|
||||
groupPlanModalDefaults,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate("faq_how_free_trial_works_question")}
|
||||
p #{translate('faq_how_does_free_trial_works_answer', { appName:'{{settings.appName}}', len:'{{trial_len}}' })}
|
||||
p #{translate('faq_how_does_free_trial_works_answer', { appName:'{{settings.appName}}', len:'7' })}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_change_plans_question')}
|
||||
p #{translate('faq_change_plans_answer')}
|
||||
|
||||
@@ -22,7 +22,7 @@ mixin btn_buy_free(location)
|
||||
span.text-capitalize #{translate('get_started_now')}
|
||||
mixin btn_buy_professional(location)
|
||||
a.btn.btn-primary(
|
||||
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
|
||||
ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || '_free_trial_7_days'}}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
|
||||
ng-click="signUpNowClicked('professional','" + location + "')"
|
||||
)
|
||||
span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
|
||||
@@ -34,9 +34,8 @@ mixin btn_buy_student(location, plan)
|
||||
ng-click="signUpNowClicked('student-annual','" + location + "')"
|
||||
) #{translate("buy_now")}
|
||||
else
|
||||
//- planQueryString will contain _free_trial_7_days
|
||||
a.btn.btn-primary(
|
||||
ng-href="/user/subscription/new?planCode=student{{planQueryString}}¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
|
||||
ng-href="/user/subscription/new?planCode=student_free_trial_7_days¤cy={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
|
||||
ng-click="signUpNowClicked('student-monthly','" + location + "')"
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
|
||||
104
services/web/app/views/subscriptions/plans-marketing.pug
Normal file
104
services/web/app/views/subscriptions/plans-marketing.pug
Normal file
@@ -0,0 +1,104 @@
|
||||
extends ../layout-marketing
|
||||
|
||||
include ./plans-marketing/_mixins
|
||||
include ./plans-marketing/_tables
|
||||
|
||||
block vars
|
||||
- metadata = { viewport: true }
|
||||
- entrypoint = 'pages/user/subscription/plans'
|
||||
|
||||
block append meta
|
||||
meta(name="ol-recommendedCurrency" content=recommendedCurrency)
|
||||
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
|
||||
meta(name="ol-currencySymbols" data-type="json" content=groupPlanModalOptions.currencySymbols)
|
||||
meta(name="ol-itm_content" content=itm_content)
|
||||
|
||||
block content
|
||||
main.content.content-alt#main-content
|
||||
.container
|
||||
.user-notifications
|
||||
ul.list-unstyled(ng-cloak)
|
||||
li.notification-entry
|
||||
.alert.alert-info
|
||||
.notification-body
|
||||
span To help you work from home throughout 2021, we're providing discounted plans and special initiatives.
|
||||
.notification-action
|
||||
a.btn.btn-sm.btn-info(href="https://www.overleaf.com/events/wfh2021" event-tracking="Event-Pages" event-tracking-trigger="click" event-tracking-ga="WFH-Offer-Click" event-tracking-label="Plans-Banner") Upgrade
|
||||
.content-page
|
||||
.plans
|
||||
.container(ng-cloak)
|
||||
.row
|
||||
.col-md-12
|
||||
.page-header.centered.plans-header.text-centered
|
||||
h1.text-capitalize(ng-non-bindable) #{translate('get_instant_access_to')} #{settings.appName}
|
||||
.row
|
||||
.col-md-8.col-md-offset-2
|
||||
p.text-centered #{translate("sl_benefits_plans")}
|
||||
|
||||
+allCardsAndControls()
|
||||
|
||||
.row.row-spaced-large.text-centered
|
||||
.col-xs-12
|
||||
p.text-centered !{translate('also_provides_free_plan', {}, [{ name: 'a', attrs: { href: '/register' }}])}
|
||||
i.fa.fa-cc-mastercard.fa-2x(aria-hidden="true")
|
||||
span.sr-only Mastercard accepted
|
||||
i.fa.fa-cc-visa.fa-2x(aria-hidden="true")
|
||||
span.sr-only Visa accepted
|
||||
i.fa.fa-cc-amex.fa-2x(aria-hidden="true")
|
||||
span.sr-only Amex accepted
|
||||
i.fa.fa-cc-paypal.fa-2x(aria-hidden="true")
|
||||
span.sr-only Paypal accepted
|
||||
div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'7'})}
|
||||
br
|
||||
div.text-centered #{translate('subject_to_additional_vat')}<br/> #{translate('select_country_vat')}
|
||||
|
||||
.row.row-spaced-large
|
||||
.col-md-8.col-md-offset-2
|
||||
.card.text-centered
|
||||
.card-header
|
||||
h2 #{translate('looking_multiple_licenses')}
|
||||
span #{translate('reduce_costs_group_licenses')}
|
||||
br
|
||||
br
|
||||
a.btn.btn-default(
|
||||
href="#groups"
|
||||
data-ol-open-group-plan-modal
|
||||
) #{translate('find_out_more')}
|
||||
|
||||
.row.row-spaced-large
|
||||
.col-sm-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate('compare_plan_features')}
|
||||
.row
|
||||
.col-md-6.col-md-offset-3
|
||||
+plan_switch('table')
|
||||
.col-md-3.text-right
|
||||
+currency_dropdown
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label="exp-")
|
||||
.col-sm-12(data-ol-view='monthly')
|
||||
+table_premium
|
||||
.col-sm-12(hidden data-ol-view='annual')
|
||||
+table_premium
|
||||
.col-sm-12(hidden data-ol-view='student')
|
||||
+table_student
|
||||
|
||||
include ./plans-marketing/_quotes
|
||||
|
||||
include ./plans-marketing/_faq
|
||||
|
||||
#bottom-cards.row.row-spaced(style="display: none;")
|
||||
.col-sm-12
|
||||
+allCardsAndControls(true, 'bottom')
|
||||
|
||||
.row.row-spaced-large
|
||||
.col-md-12
|
||||
.plans-header.plans-subheader.text-centered
|
||||
h2.header-with-btn #{translate('still_have_questions')}
|
||||
button.btn.btn-default.btn-header.text-capitalize(
|
||||
data-ol-open-contact-form-modal="general"
|
||||
) #{translate('get_in_touch')}
|
||||
|
||||
.row.row-spaced
|
||||
|
||||
include ./plans-marketing/_group_plan_modal
|
||||
!= moduleIncludes("contactModalGeneral-marketing", locals)
|
||||
@@ -0,0 +1,34 @@
|
||||
.faq
|
||||
.row.row-spaced-large
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 FAQ
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate("faq_how_free_trial_works_question")}
|
||||
p #{translate('faq_how_does_free_trial_works_answer', { len:'7' })}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_change_plans_question')}
|
||||
p #{translate('faq_change_plans_answer')}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_do_collab_need_premium_question')}
|
||||
p #{translate('faq_do_collab_need_premium_answer')}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_need_more_collab_question')}
|
||||
p !{translate('faq_need_more_collab_answer', { referFriendsLink: translate('referring_your_friends') })}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_purchase_more_licenses_question')}
|
||||
p !{translate('faq_purchase_more_licenses_answer', { groupLink: translate('discounted_group_accounts') })}
|
||||
a(href='#groups', ng-click="openGroupPlanModal()") #{translate("get_in_touch_for_details")}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_monthly_or_annual_question')}
|
||||
p #{translate('faq_monthly_or_annual_answer')}
|
||||
.row
|
||||
.col-md-6
|
||||
h3 #{translate('faq_how_to_pay_question')}
|
||||
p #{translate('faq_how_to_pay_answer')}
|
||||
.col-md-6
|
||||
h3 #{translate('faq_pay_by_invoice_question')}
|
||||
p !{translate('faq_pay_by_invoice_answer', {}, [{ name: 'a', attrs: { href: "#pay-by-invoice", 'ng-controller': "ContactGeneralModal", 'ng-click': "openModal()" }}])}
|
||||
@@ -0,0 +1,86 @@
|
||||
div.modal.fade(tabindex="-1" role="dialog" data-ol-group-plan-modal)
|
||||
.modal-dialog(role="document")
|
||||
.modal-content
|
||||
.modal-header
|
||||
h3 Save 30% or more with a group license
|
||||
.modal-body.plans
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6.text-center
|
||||
.circle.circle-lg
|
||||
span(data-ol-group-plan-display-price) ...
|
||||
span.small / year
|
||||
br
|
||||
span.circle-subtext(data-ol-group-plan-for-n-users) For ... users
|
||||
ul.list-unstyled
|
||||
li Each user will have access to:
|
||||
li
|
||||
li(
|
||||
hidden=(groupPlanModalDefaults.plan_code !== 'collaborator')
|
||||
data-ol-group-plan-plan-code='collaborator'
|
||||
)
|
||||
strong #{translate("collabs_per_proj", {collabcount:10})}
|
||||
li(
|
||||
hidden=(groupPlanModalDefaults.plan_code !== 'professional')
|
||||
data-ol-group-plan-plan-code='professional'
|
||||
)
|
||||
strong #{translate("unlimited_collabs")}
|
||||
+features_premium
|
||||
.col-md-6
|
||||
form.form(data-ol-group-plan-form)
|
||||
.form-group
|
||||
label(for='plan_code')
|
||||
| Plan
|
||||
select.form-control(id="plan_code")
|
||||
for plan_code in groupPlanModalOptions.plan_codes
|
||||
option(
|
||||
value=plan_code.code
|
||||
selected=(plan_code.code === groupPlanModalDefaults.plan_code)
|
||||
) #{plan_code.display}
|
||||
.form-group
|
||||
label(for='size')
|
||||
| Number of users
|
||||
select.form-control(id="size")
|
||||
for size in groupPlanModalOptions.sizes
|
||||
option(
|
||||
value=size
|
||||
selected=(size === groupPlanModalDefaults.size)
|
||||
) #{size}
|
||||
.form-group
|
||||
label(for='currency')
|
||||
| Currency
|
||||
select.form-control(id="currency")
|
||||
for currency in groupPlanModalOptions.currencies
|
||||
option(
|
||||
value=currency.code
|
||||
selected=(currency.code === groupPlanModalDefaults.currency)
|
||||
) #{currency.display}
|
||||
.form-group
|
||||
label(for='usage')
|
||||
| Usage
|
||||
select.form-control(id="usage")
|
||||
for usage in groupPlanModalOptions.usages
|
||||
option(
|
||||
value=usage.code
|
||||
selected=(usage.code === groupPlanModalDefaults.usage)
|
||||
) #{usage.display}
|
||||
p.small.text-center.row-spaced-small
|
||||
span(
|
||||
hidden=(groupPlanModalDefaults.usage !== 'educational')
|
||||
data-ol-group-plan-usage='educational'
|
||||
)
|
||||
| The 40% educational discount can be used by students or faculty using Overleaf for teaching
|
||||
span(
|
||||
hidden=(groupPlanModalDefaults.usage !== 'enterprise')
|
||||
data-ol-group-plan-usage='enterprise'
|
||||
)
|
||||
| Save an additional 40% on groups of 10 or more with our educational discount
|
||||
.modal-footer
|
||||
.text-center
|
||||
button.btn.btn-primary.btn-lg(data-ol-purchase-group-plan) Purchase Now
|
||||
hr.thin
|
||||
a(
|
||||
href
|
||||
data-ol-open-contact-form-for-more-than-50-licenses
|
||||
) Need more than 50 licenses? Please get in touch
|
||||
|
||||
250
services/web/app/views/subscriptions/plans-marketing/_mixins.pug
Normal file
250
services/web/app/views/subscriptions/plans-marketing/_mixins.pug
Normal file
@@ -0,0 +1,250 @@
|
||||
//- Buy Buttons
|
||||
mixin btn_buy_collaborator(location)
|
||||
a.btn.btn-primary(
|
||||
data-ol-start-new-subscription='collaborator'
|
||||
data-ol-location=location
|
||||
)
|
||||
span(data-ol-view='monthly') #{translate("start_free_trial")}
|
||||
span(hidden data-ol-view='annual') #{translate("buy_now")}
|
||||
mixin btn_buy_personal(location)
|
||||
a.btn.btn-primary(
|
||||
data-ol-start-new-subscription='paid-personal'
|
||||
data-ol-tracking-plan='personal'
|
||||
data-ol-location=location
|
||||
)
|
||||
span(data-ol-view='monthly') #{translate("start_free_trial")}
|
||||
span(hidden data-ol-view='annual') #{translate("buy_now")}
|
||||
mixin btn_buy_free(location)
|
||||
a.btn.btn-primary(
|
||||
data-ol-has-custom-href
|
||||
href="/register"
|
||||
style=(getLoggedInUserId() === null ? "" : "visibility: hidden")
|
||||
data-ol-start-new-subscription='free'
|
||||
data-ol-location=location
|
||||
)
|
||||
span.text-capitalize #{translate('get_started_now')}
|
||||
mixin btn_buy_professional(location)
|
||||
a.btn.btn-primary(
|
||||
data-ol-start-new-subscription='professional'
|
||||
data-ol-location=location
|
||||
)
|
||||
span(data-ol-view='monthly') #{translate("start_free_trial")}
|
||||
span(hidden data-ol-view='annual') #{translate("buy_now")}
|
||||
mixin btn_buy_student(location, plan)
|
||||
if plan == 'annual'
|
||||
a.btn.btn-primary(
|
||||
data-ol-start-new-subscription='student'
|
||||
data-ol-item-view='annual'
|
||||
data-ol-tracking-label='student-annual'
|
||||
data-ol-location=location
|
||||
) #{translate("buy_now")}
|
||||
else
|
||||
a.btn.btn-primary(
|
||||
data-ol-start-new-subscription='student'
|
||||
data-ol-item-view='monthly'
|
||||
data-ol-tracking-label='student-monthly'
|
||||
data-ol-location=location
|
||||
) #{translate("start_free_trial")}
|
||||
|
||||
//- Cards
|
||||
mixin card_student_annual(location)
|
||||
.best-value
|
||||
strong #{translate('best_value')}
|
||||
.card-header
|
||||
h2 #{translate("student")} (#{translate("annual")})
|
||||
h5.tagline #{translate('tagline_student_annual')}
|
||||
.circle
|
||||
span
|
||||
+price_student_annual
|
||||
+features_student(location, 'annual')
|
||||
mixin card_student_monthly(location)
|
||||
.card-header
|
||||
h2 #{translate("student")}
|
||||
h5.tagline #{translate('tagline_student_monthly')}
|
||||
.circle
|
||||
span
|
||||
+price_student_monthly
|
||||
+features_student(location, 'monthly')
|
||||
|
||||
//- Features Lists, used within cards
|
||||
mixin features_collaborator(location)
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:10})}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_collaborator(location)
|
||||
mixin features_free(location)
|
||||
ul.list-unstyled
|
||||
li #{translate("one_collaborator")}
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li
|
||||
br
|
||||
+btn_buy_free(location)
|
||||
mixin features_personal(location)
|
||||
ul.list-unstyled
|
||||
li #{translate("one_collaborator")}
|
||||
li
|
||||
li
|
||||
strong #{translate('premium_features')}
|
||||
li #{translate('sync_dropbox_github')}
|
||||
li #{translate('full_doc_history')}
|
||||
li + #{translate('more').toLowerCase()}
|
||||
li(class="hidden-xs hidden-sm")
|
||||
li
|
||||
br
|
||||
+btn_buy_personal(location)
|
||||
mixin features_premium
|
||||
li
|
||||
li
|
||||
strong #{translate('all_premium_features')}
|
||||
li #{translate('sync_dropbox_github')}
|
||||
li #{translate('full_doc_history')}
|
||||
li #{translate('track_changes')}
|
||||
li + #{translate('more').toLowerCase()}
|
||||
mixin features_professional(location)
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("unlimited_collabs")}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_professional(location)
|
||||
mixin features_student(location, plan)
|
||||
ul.list-unstyled
|
||||
li
|
||||
strong #{translate("collabs_per_proj", {collabcount:6})}
|
||||
+features_premium
|
||||
li
|
||||
br
|
||||
+btn_buy_student(location, plan)
|
||||
|
||||
mixin gen_localized_price_for_plan_view(plan, view)
|
||||
for currencyCode in Object.keys(settings.localizedPlanPricing)
|
||||
span(
|
||||
hidden=(currencyCode !== recommendedCurrency)
|
||||
data-ol-currencyCode=currencyCode
|
||||
) #{settings.localizedPlanPricing[currencyCode][plan][view]}
|
||||
|
||||
mixin gen_localized_price_for_plan(plan)
|
||||
div(data-ol-view='monthly')
|
||||
+gen_localized_price_for_plan_view(plan, 'monthly')
|
||||
span.small /mo
|
||||
div(hidden data-ol-view='annual')
|
||||
+gen_localized_price_for_plan_view(plan, 'annual')
|
||||
span.small /yr
|
||||
|
||||
//- Prices
|
||||
mixin price_personal
|
||||
+gen_localized_price_for_plan('personal')
|
||||
mixin price_collaborator
|
||||
+gen_localized_price_for_plan('collaborator')
|
||||
mixin price_professional
|
||||
+gen_localized_price_for_plan('professional')
|
||||
mixin price_student_annual
|
||||
+gen_localized_price_for_plan_view('student', 'annual')
|
||||
span.small /yr
|
||||
mixin price_student_monthly
|
||||
+gen_localized_price_for_plan_view('student', 'monthly')
|
||||
span.small /mo
|
||||
|
||||
//- UI Control
|
||||
mixin currency_dropdown
|
||||
.dropdown.currency-dropdown(dropdown)
|
||||
a.btn.btn-default.dropdown-toggle(
|
||||
href="#",
|
||||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
for currencyCode in Object.keys(settings.localizedPlanPricing)
|
||||
span(
|
||||
hidden=(currencyCode !== recommendedCurrency)
|
||||
data-ol-currencyCode=currencyCode
|
||||
) #{currencyCode} (#{settings.localizedPlanPricing[currencyCode]['symbol']})
|
||||
span.caret
|
||||
|
||||
ul.dropdown-menu.dropdown-menu-right.text-right(role="menu")
|
||||
for currencyCode in Object.keys(settings.localizedPlanPricing)
|
||||
li
|
||||
a(
|
||||
href='#'
|
||||
data-ol-currencyCode-switch=currencyCode
|
||||
) #{currencyCode} #{settings.localizedPlanPricing[currencyCode]['symbol']}
|
||||
|
||||
mixin plan_switch(location)
|
||||
ul.nav.nav-pills
|
||||
li.active(data-ol-view-tab='monthly')
|
||||
a.btn.btn-default-outline(
|
||||
href="#"
|
||||
) #{translate("monthly")}
|
||||
li(data-ol-view-tab='annual')
|
||||
a.btn.btn-default-outline(
|
||||
href="#"
|
||||
) #{translate("annual")}
|
||||
li(data-ol-view-tab='student')
|
||||
a.btn.btn-default-outline(
|
||||
href="#"
|
||||
) #{translate("special_price_student")}
|
||||
|
||||
mixin allCardsAndControls(controlsRowSpaced, listLocation)
|
||||
- var location = listLocation ? 'card_' + listLocation : 'card'
|
||||
.row.top-switch(class=(controlsRowSpaced ? "row-spaced" : ""))
|
||||
.col-md-6.col-md-offset-3
|
||||
+plan_switch('card')
|
||||
.col-md-2.text-right
|
||||
+currency_dropdown
|
||||
|
||||
.row
|
||||
.col-md-10.col-md-offset-1
|
||||
.row
|
||||
for view in ['monthly', 'annual']
|
||||
.card-group.text-centered(data-ol-view=view hidden=(view==='annual'))
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("personal")}
|
||||
h5.tagline #{translate("tagline_personal")}
|
||||
.circle
|
||||
+price_personal
|
||||
+features_personal(location)
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
.best-value
|
||||
strong #{translate('best_value')}
|
||||
.card-header
|
||||
h2 #{translate("collaborator")}
|
||||
h5.tagline #{translate("tagline_collaborator")}
|
||||
.circle
|
||||
+price_collaborator
|
||||
+features_collaborator(location)
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
.card-header
|
||||
h2 #{translate("professional")}
|
||||
h5.tagline #{translate("tagline_professional")}
|
||||
.circle
|
||||
+price_professional
|
||||
+features_professional(location)
|
||||
|
||||
.card-group.text-centered(hidden data-ol-view='student')
|
||||
.col-md-4
|
||||
.card.card-first
|
||||
.card-header
|
||||
h2 #{translate("free")}
|
||||
h5.tagline #{translate("tagline_free")}
|
||||
.circle #{translate("free")}
|
||||
+features_free(location)
|
||||
|
||||
.col-md-4
|
||||
.card.card-highlighted
|
||||
+card_student_annual(location)
|
||||
|
||||
.col-md-4
|
||||
.card.card-last
|
||||
+card_student_monthly(location)
|
||||
@@ -0,0 +1,25 @@
|
||||
.row.row-spaced-large
|
||||
.col-md-12
|
||||
.page-header.plans-header.plans-subheader.text-centered
|
||||
h2 #{translate('in_good_company')}
|
||||
.row
|
||||
.col-md-6
|
||||
div
|
||||
.row
|
||||
.col-md-3
|
||||
.circle-img
|
||||
img(src=buildImgPath('advocates/schultz.jpg') alt="Kevin Schultz")
|
||||
.col-md-9
|
||||
blockquote
|
||||
p It is the ability to collaborate very easily that drew me to Overleaf.
|
||||
footer Kevin Schultz, Assistant Professor of Physics, Hartwick College
|
||||
.col-md-6
|
||||
div
|
||||
.row
|
||||
.col-md-3
|
||||
.circle-img
|
||||
img(src=buildImgPath('advocates/dagoret-campagne.jpg') alt="Dr Sylvie Dagoret-Campagne")
|
||||
.col-md-9
|
||||
blockquote
|
||||
p Overleaf is a great educational tool for publishing scientific documents.
|
||||
footer Dr Sylvie Dagoret-Campagne, Director of Research at CNRS, University of Paris-Saclay
|
||||
116
services/web/app/views/subscriptions/plans-marketing/_tables.pug
Normal file
116
services/web/app/views/subscriptions/plans-marketing/_tables.pug
Normal file
@@ -0,0 +1,116 @@
|
||||
//- Features Tables
|
||||
mixin table_premium
|
||||
table.card.plans-table.plans-table-main
|
||||
tr
|
||||
th
|
||||
th #{translate("free")}
|
||||
th #{translate("personal")}
|
||||
th #{translate("collaborator")}
|
||||
.outer.outer-top
|
||||
.outer-content
|
||||
.best-value
|
||||
strong #{translate('best_value')}
|
||||
th #{translate("professional")}
|
||||
|
||||
tr
|
||||
td #{translate("price")}
|
||||
td #{translate("free")}
|
||||
td
|
||||
+price_personal
|
||||
td
|
||||
+price_collaborator
|
||||
td
|
||||
+price_professional
|
||||
|
||||
for feature in planFeatures
|
||||
tr
|
||||
td(event-tracking="features-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}`)
|
||||
if feature.info
|
||||
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
|
||||
else
|
||||
| #{translate(feature.feature)}
|
||||
for plan in feature.plans
|
||||
td(ng-non-bindable)
|
||||
if feature.value == 'str'
|
||||
| #{plan}
|
||||
else if plan
|
||||
i.fa.fa-check(aria-hidden="true")
|
||||
span.sr-only Feature included
|
||||
else
|
||||
i.fa.fa-times(aria-hidden="true")
|
||||
span.sr-only Feature not included
|
||||
|
||||
tr
|
||||
td
|
||||
td
|
||||
+btn_buy_free('table')
|
||||
td
|
||||
+btn_buy_personal('table')
|
||||
td
|
||||
+btn_buy_collaborator('table')
|
||||
.outer.outer-btm
|
||||
.outer-content
|
||||
td
|
||||
+btn_buy_professional('table')
|
||||
|
||||
mixin table_cell_student(feature)
|
||||
if feature.value == 'str'
|
||||
| #{feature.student}
|
||||
else if feature.student
|
||||
i.fa.fa-check(aria-hidden="true")
|
||||
span.sr-only Feature included
|
||||
else
|
||||
i.fa.fa-times(aria-hidden="true")
|
||||
span.sr-only Feature not included
|
||||
|
||||
mixin table_student
|
||||
table.card.plans-table.plans-table-student
|
||||
tr
|
||||
th
|
||||
th #{translate("free")}
|
||||
th #{translate("student")} (#{translate("annual")})
|
||||
.outer.outer-top
|
||||
.outer-content
|
||||
.best-value
|
||||
strong Best Value
|
||||
th #{translate("student")}
|
||||
|
||||
tr
|
||||
td #{translate("price")}
|
||||
td #{translate("free")}
|
||||
td
|
||||
+price_student_annual
|
||||
td
|
||||
+price_student_monthly
|
||||
|
||||
for feature in planFeatures
|
||||
tr
|
||||
td(event-tracking="plans-page-table" event-tracking-trigger="hover" event-tracking-ga="subscription-funnel" event-tracking-label=`${feature.feature}`)
|
||||
if feature.info
|
||||
span(tooltip=translate(feature.info)) #{translate(feature.feature)}
|
||||
else
|
||||
| #{translate(feature.feature)}
|
||||
td(ng-non-bindable)
|
||||
if feature.value == 'str'
|
||||
| #{feature.plans.free}
|
||||
else if feature.plans.free
|
||||
i.fa.fa-check(aria-hidden="true")
|
||||
span.sr-only Feature included
|
||||
else
|
||||
i.fa.fa-times(aria-hidden="true")
|
||||
span.sr-only Feature included
|
||||
td(ng-non-bindable)
|
||||
+table_cell_student(feature)
|
||||
td(ng-non-bindable)
|
||||
+table_cell_student(feature)
|
||||
|
||||
tr
|
||||
td
|
||||
td
|
||||
+btn_buy_free('table')
|
||||
td
|
||||
+btn_buy_student('table', 'annual')
|
||||
.outer.outer-btm
|
||||
.outer-content
|
||||
td
|
||||
+btn_buy_student('table', 'monthly')
|
||||
@@ -45,7 +45,7 @@ block content
|
||||
span.sr-only Amex accepted
|
||||
i.fa.fa-cc-paypal.fa-2x(aria-hidden="true")
|
||||
span.sr-only Paypal accepted
|
||||
div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'{{trial_len}}'})}
|
||||
div.text-centered #{translate('change_plans_any_time')}<br/> #{translate('billed_after_x_days', {len:'7'})}
|
||||
br
|
||||
div.text-centered #{translate('subject_to_additional_vat')}<br/> #{translate('select_country_vat')}
|
||||
|
||||
@@ -71,7 +71,7 @@ block content
|
||||
+plan_switch('table')
|
||||
.col-md-3.text-right
|
||||
+currency_dropdown
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label=`exp-{{plansVariant}}`)
|
||||
.row(event-tracking="features-table-viewed" event-tracking-ga="subscription-funnel" event-tracking-trigger="scroll" event-tracking-send-once="true" event-tracking-label="exp-")
|
||||
.col-sm-12(ng-if="ui.view != 'student'")
|
||||
+table_premium
|
||||
.col-sm-12(ng-if="ui.view == 'student'")
|
||||
|
||||
@@ -300,6 +300,12 @@ module.exports = {
|
||||
personal: defaultFeatures,
|
||||
},
|
||||
|
||||
groupPlanModalOptions: {
|
||||
plan_codes: [],
|
||||
currencies: [],
|
||||
sizes: [],
|
||||
usages: [],
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
planCode: 'personal',
|
||||
|
||||
@@ -7,6 +7,15 @@ document
|
||||
document.querySelectorAll('a[ng-click="contactUsModal()"]').forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
$('[data-ol-contact-form-modal]').modal()
|
||||
$('[data-ol-contact-form-modal="contact-us"]').modal()
|
||||
})
|
||||
})
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-open-contact-form-modal="general"]')
|
||||
.forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
$('[data-ol-contact-form-modal="general"]').modal()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import getMeta from '../../../utils/meta'
|
||||
import { swapModal } from '../../utils/swapModal'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
function getFormValues() {
|
||||
const modalEl = document.querySelector('[data-ol-group-plan-modal]')
|
||||
const planCode = modalEl.querySelector('#plan_code').value
|
||||
const size = modalEl.querySelector('#size').value
|
||||
const currency = modalEl.querySelector('#currency').value
|
||||
const usage = modalEl.querySelector('#usage').value
|
||||
return { planCode, size, currency, usage }
|
||||
}
|
||||
|
||||
function updateGroupPlanView() {
|
||||
const prices = getMeta('ol-groupPlans')
|
||||
const currencySymbols = getMeta('ol-currencySymbols')
|
||||
|
||||
const modalEl = document.querySelector('[data-ol-group-plan-modal]')
|
||||
const { planCode, size, currency, usage } = getFormValues()
|
||||
|
||||
const price = prices[usage][planCode][currency][size]
|
||||
const currencySymbol = currencySymbols[currency]
|
||||
const displayPrice = `${currencySymbol}${price}`
|
||||
|
||||
modalEl.querySelectorAll('[data-ol-group-plan-plan-code]').forEach(el => {
|
||||
el.hidden = el.getAttribute('data-ol-group-plan-plan-code') !== planCode
|
||||
})
|
||||
modalEl.querySelectorAll('[data-ol-group-plan-usage]').forEach(el => {
|
||||
el.hidden = el.getAttribute('data-ol-group-plan-usage') !== usage
|
||||
})
|
||||
modalEl.querySelector(
|
||||
'[data-ol-group-plan-display-price]'
|
||||
).innerText = displayPrice
|
||||
modalEl.querySelector(
|
||||
'[data-ol-group-plan-for-n-users]'
|
||||
).innerText = `For ${size} users`
|
||||
}
|
||||
|
||||
const modalEl = $('[data-ol-group-plan-modal]')
|
||||
modalEl
|
||||
.on('shown.bs.modal', function () {
|
||||
const path = `${window.location.pathname}${window.location.search}`
|
||||
history.replaceState(null, document.title, path + '#groups')
|
||||
})
|
||||
.on('hidden.bs.modal', function () {
|
||||
history.replaceState(null, document.title, window.location.pathname)
|
||||
})
|
||||
|
||||
function showGroupPlanModal() {
|
||||
modalEl.modal()
|
||||
eventTracking.send(
|
||||
'subscription-funnel',
|
||||
'plans-page',
|
||||
'group-inquiry-potential'
|
||||
)
|
||||
}
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-group-plan-form] select')
|
||||
.forEach(el => el.addEventListener('change', updateGroupPlanView))
|
||||
document.querySelectorAll('[data-ol-purchase-group-plan]').forEach(el =>
|
||||
el.addEventListener('click', e => {
|
||||
e.preventDefault()
|
||||
|
||||
const { planCode, size, currency, usage } = getFormValues()
|
||||
const queryParams = new URLSearchParams(
|
||||
Object.entries({
|
||||
planCode: `group_${planCode}_${size}_${usage}`,
|
||||
currency,
|
||||
itm_campaign: 'groups',
|
||||
})
|
||||
)
|
||||
const itmContent = getMeta('ol-itm_content')
|
||||
if (itmContent) {
|
||||
queryParams.set('itm_content', itmContent)
|
||||
}
|
||||
const url = new URL('/user/subscription/new', window.origin)
|
||||
url.search = queryParams.toString()
|
||||
window.location = url.toString()
|
||||
})
|
||||
)
|
||||
|
||||
document.querySelectorAll('[data-ol-open-group-plan-modal]').forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
showGroupPlanModal()
|
||||
})
|
||||
})
|
||||
|
||||
document
|
||||
.querySelectorAll('[data-ol-open-contact-form-for-more-than-50-licenses]')
|
||||
.forEach(el => {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
swapModal(
|
||||
'[data-ol-group-plan-modal]',
|
||||
'[data-ol-contact-form-modal="general"]'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
updateGroupPlanView()
|
||||
|
||||
if (window.location.hash === '#groups') {
|
||||
showGroupPlanModal()
|
||||
}
|
||||
12
services/web/frontend/js/features/utils/swapModal.js
Normal file
12
services/web/frontend/js/features/utils/swapModal.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export function swapModal(selectorBefore, selectorAfter) {
|
||||
const modalBefore = $(selectorBefore)
|
||||
const modalAfter = $(selectorAfter)
|
||||
|
||||
// Disable the fade-out + fade-in animation when swapping the forms.
|
||||
modalBefore.removeClass('fade')
|
||||
modalAfter.removeClass('fade')
|
||||
modalAfter.modal()
|
||||
modalBefore.modal('hide')
|
||||
modalBefore.addClass('fade')
|
||||
modalAfter.addClass('fade')
|
||||
}
|
||||
@@ -248,10 +248,6 @@ App.controller(
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
|
||||
$scope.trial_len = 7
|
||||
|
||||
$scope.planQueryString = '_free_trial_7_days'
|
||||
|
||||
$scope.ui = { view: 'monthly' }
|
||||
|
||||
$scope.changeCurreny = function (e, newCurrency) {
|
||||
@@ -265,7 +261,7 @@ App.controller(
|
||||
if (view === 'annual') {
|
||||
return 'collaborator-annual'
|
||||
} else {
|
||||
return `collaborator${$scope.planQueryString}`
|
||||
return `collaborator_free_trial_7_days`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +270,7 @@ App.controller(
|
||||
if (view === 'annual') {
|
||||
return 'paid-personal-annual'
|
||||
} else {
|
||||
return `paid-personal${$scope.planQueryString}`
|
||||
return `paid-personal_free_trial_7_days`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
services/web/frontend/js/pages/user/subscription/plans.js
Normal file
78
services/web/frontend/js/pages/user/subscription/plans.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import '../../../marketing'
|
||||
import '../../../features/plans/group-plan-modal'
|
||||
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import getMeta from '../../../utils/meta'
|
||||
|
||||
let currentView = 'monthly'
|
||||
let currentCurrencyCode = getMeta('ol-recommendedCurrency')
|
||||
|
||||
function setUpViewSwitching(liEl) {
|
||||
const view = liEl.getAttribute('data-ol-view-tab')
|
||||
liEl.querySelector('a').addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
eventTracking.send('subscription-funnel', 'plans-page', `${view}-prices`)
|
||||
document.querySelectorAll('[data-ol-view-tab]').forEach(el => {
|
||||
if (el.getAttribute('data-ol-view-tab') === view) {
|
||||
el.classList.add('active')
|
||||
} else {
|
||||
el.classList.remove('active')
|
||||
}
|
||||
})
|
||||
document.querySelectorAll('[data-ol-view]').forEach(el => {
|
||||
el.hidden = el.getAttribute('data-ol-view') !== view
|
||||
})
|
||||
currentView = view
|
||||
updateLinkTargets()
|
||||
})
|
||||
}
|
||||
|
||||
function setUpCurrencySwitching(linkEl) {
|
||||
const currencyCode = linkEl.getAttribute('data-ol-currencyCode-switch')
|
||||
linkEl.addEventListener('click', function (e) {
|
||||
e.preventDefault()
|
||||
document.querySelectorAll('[data-ol-currencyCode]').forEach(el => {
|
||||
el.hidden = el.getAttribute('data-ol-currencyCode') !== currencyCode
|
||||
})
|
||||
currentCurrencyCode = currencyCode
|
||||
updateLinkTargets()
|
||||
})
|
||||
}
|
||||
|
||||
function setUpSubscriptionTracking(linkEl) {
|
||||
const plan =
|
||||
linkEl.getAttribute('data-ol-tracking-plan') ||
|
||||
linkEl.getAttribute('data-ol-start-new-subscription')
|
||||
|
||||
linkEl.addEventListener('click', function () {
|
||||
const customLabel = linkEl.getAttribute('data-ol-tracking-label')
|
||||
const computedLabel = currentView === 'annual' ? `${plan}_annual` : plan
|
||||
const label = customLabel || computedLabel
|
||||
|
||||
eventTracking.sendMB('plans-page-start-trial')
|
||||
eventTracking.send('subscription-funnel', 'sign_up_now_button', label)
|
||||
})
|
||||
}
|
||||
|
||||
function updateLinkTargets() {
|
||||
document.querySelectorAll('[data-ol-start-new-subscription]').forEach(el => {
|
||||
if (el.hasAttribute('data-ol-has-custom-href')) return
|
||||
|
||||
const plan = el.getAttribute('data-ol-start-new-subscription')
|
||||
const view = el.getAttribute('data-ol-item-view') || currentView
|
||||
const suffix = view === 'annual' ? `_annual` : `_free_trial_7_days`
|
||||
const planCode = `${plan}${suffix}`
|
||||
|
||||
const location = el.getAttribute('data-ol-location')
|
||||
el.href = `/user/subscription/new?planCode=${planCode}¤cy=${currentCurrencyCode}&itm_campaign=plans&itm_content=${location}`
|
||||
})
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-ol-view-tab]').forEach(setUpViewSwitching)
|
||||
document
|
||||
.querySelectorAll('[data-ol-currencyCode-switch]')
|
||||
.forEach(setUpCurrencySwitching)
|
||||
document
|
||||
.querySelectorAll('[data-ol-start-new-subscription]')
|
||||
.forEach(setUpSubscriptionTracking)
|
||||
updateLinkTargets()
|
||||
@@ -99,6 +99,17 @@ describe('SubscriptionController', function () {
|
||||
collaborator: 'COLLABORATORCODEHERE',
|
||||
},
|
||||
},
|
||||
groupPlanModalOptions: {
|
||||
plan_codes: [],
|
||||
currencies: [
|
||||
{
|
||||
display: 'GBP (£)',
|
||||
code: 'GBP',
|
||||
},
|
||||
],
|
||||
sizes: ['42'],
|
||||
usages: [{ code: 'foo', display: 'Foo' }],
|
||||
},
|
||||
apis: {
|
||||
recurly: {
|
||||
subdomain: 'sl',
|
||||
@@ -120,8 +131,14 @@ describe('SubscriptionController', function () {
|
||||
getUser: sinon.stub().resolves(this.user),
|
||||
},
|
||||
}
|
||||
this.SplitTestV2Hander = {
|
||||
promises: {
|
||||
getAssignmentForSession: sinon.stub().resolves({ variant: 'default' }),
|
||||
},
|
||||
}
|
||||
this.SubscriptionController = SandboxedModule.require(modulePath, {
|
||||
requires: {
|
||||
'../SplitTests/SplitTestV2Handler': this.SplitTestV2Hander,
|
||||
'../Authentication/SessionManager': this.SessionManager,
|
||||
'./SubscriptionHandler': this.SubscriptionHandler,
|
||||
'./PlansLocator': this.PlansLocator,
|
||||
@@ -166,6 +183,80 @@ describe('SubscriptionController', function () {
|
||||
currencyCode: this.stubbedCurrencyCode,
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitTest', function () {
|
||||
const cases = [
|
||||
{
|
||||
variant: 'default',
|
||||
template: 'subscriptions/plans',
|
||||
},
|
||||
{
|
||||
variant: 'de-ng',
|
||||
template: 'subscriptions/plans-marketing',
|
||||
},
|
||||
]
|
||||
for (const { variant, template } of cases) {
|
||||
describe(variant, function () {
|
||||
beforeEach(function () {
|
||||
const assignment = { variant }
|
||||
this.SplitTestV2Hander.promises.getAssignmentForSession.resolves(
|
||||
assignment
|
||||
)
|
||||
})
|
||||
it(`should render template ${template}`, function (done) {
|
||||
this.res.render = page => {
|
||||
page.should.equal(template)
|
||||
done()
|
||||
}
|
||||
this.SubscriptionController.plansPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('groupPlanModal data', function () {
|
||||
it('should pass local currency if valid', function (done) {
|
||||
this.res.render = (page, opts) => {
|
||||
page.should.equal('subscriptions/plans')
|
||||
opts.groupPlanModalDefaults.currency.should.equal('GBP')
|
||||
done()
|
||||
}
|
||||
this.GeoIpLookup.promises.getCurrencyCode.resolves({
|
||||
currencyCode: 'GBP',
|
||||
})
|
||||
this.SubscriptionController.plansPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should fallback to USD when valid', function (done) {
|
||||
this.res.render = (page, opts) => {
|
||||
page.should.equal('subscriptions/plans')
|
||||
opts.groupPlanModalDefaults.currency.should.equal('USD')
|
||||
done()
|
||||
}
|
||||
this.GeoIpLookup.promises.getCurrencyCode.resolves({
|
||||
currencyCode: 'FOO',
|
||||
})
|
||||
this.SubscriptionController.plansPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('should pass valid options for group plan modal and discard invalid', function (done) {
|
||||
this.res.render = (page, opts) => {
|
||||
page.should.equal('subscriptions/plans')
|
||||
opts.groupPlanModalDefaults.size.should.equal('42')
|
||||
opts.groupPlanModalDefaults.plan_code.should.equal('collaborator')
|
||||
opts.groupPlanModalDefaults.currency.should.equal('GBP')
|
||||
opts.groupPlanModalDefaults.usage.should.equal('foo')
|
||||
done()
|
||||
}
|
||||
this.req.query = {
|
||||
number: '42',
|
||||
currency: 'ABC',
|
||||
plan: 'does-not-exist',
|
||||
usage: 'foo',
|
||||
}
|
||||
this.SubscriptionController.plansPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('paymentPage', function () {
|
||||
|
||||
Reference in New Issue
Block a user