Explorar el Código

Merge pull request #5117 from overleaf/jpa-plans-de-ng

[web] de-ng plans page

GitOrigin-RevId: b902b10b45ab3fd46e71545c94f65e8f079c4564
Jakob Ackermann hace 5 años
padre
commit
59d83c4b88

+ 44 - 1
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -18,6 +18,17 @@ const AnalyticsManager = require('../Analytics/AnalyticsManager')
 const RecurlyEventHandler = require('./RecurlyEventHandler')
 const RecurlyEventHandler = require('./RecurlyEventHandler')
 const { expressify } = require('../../util/promises')
 const { expressify } = require('../../util/promises')
 const OError = require('@overleaf/o-error')
 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) {
 async function plansPage(req, res) {
   const plans = SubscriptionViewModelBuilder.buildPlansList()
   const plans = SubscriptionViewModelBuilder.buildPlansList()
@@ -28,14 +39,46 @@ async function plansPage(req, res) {
     (req.query ? req.query.ip : undefined) || req.ip
     (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',
     title: 'plans_and_pricing',
     plans,
     plans,
     gaExperiments: Settings.gaExperiments.plansPage,
     gaExperiments: Settings.gaExperiments.plansPage,
     gaOptimize: true,
     gaOptimize: true,
+    itm_content: req.query && req.query.itm_content,
     recomendedCurrency: recommendedCurrency,
     recomendedCurrency: recommendedCurrency,
+    recommendedCurrency,
     planFeatures,
     planFeatures,
     groupPlans: GroupPlansData,
     groupPlans: GroupPlansData,
+    groupPlanModalOptions,
+    groupPlanModalDefaults,
   })
   })
 }
 }
 
 

+ 1 - 1
services/web/app/views/subscriptions/_plans_faq.pug

@@ -6,7 +6,7 @@
 	.row
 	.row
 		.col-md-6
 		.col-md-6
 			h3 #{translate("faq_how_free_trial_works_question")}
 			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
 		.col-md-6
 			h3 #{translate('faq_change_plans_question')}
 			h3 #{translate('faq_change_plans_question')}
 			p #{translate('faq_change_plans_answer')}
 			p #{translate('faq_change_plans_answer')}

+ 2 - 3
services/web/app/views/subscriptions/_plans_page_mixins.pug

@@ -22,7 +22,7 @@ mixin btn_buy_free(location)
 		span.text-capitalize #{translate('get_started_now')}
 		span.text-capitalize #{translate('get_started_now')}
 mixin btn_buy_professional(location)
 mixin btn_buy_professional(location)
 	a.btn.btn-primary(
 	a.btn.btn-primary(
-		ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}&currency={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
+		ng-href="/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || '_free_trial_7_days'}}&currency={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
 		ng-click="signUpNowClicked('professional','" + location + "')"
 		ng-click="signUpNowClicked('professional','" + location + "')"
 	)
 	)
 		span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
 		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 + "')"
 			ng-click="signUpNowClicked('student-annual','" + location + "')"
 		) #{translate("buy_now")}
 		) #{translate("buy_now")}
 	else
 	else
-		//- planQueryString will contain _free_trial_7_days
 		a.btn.btn-primary(
 		a.btn.btn-primary(
-			ng-href="/user/subscription/new?planCode=student{{planQueryString}}&currency={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
+			ng-href="/user/subscription/new?planCode=student_free_trial_7_days&currency={{currencyCode}}&itm_campaign=plans&itm_content=" + location,
 			ng-click="signUpNowClicked('student-monthly','" + location + "')"
 			ng-click="signUpNowClicked('student-monthly','" + location + "')"
 		) #{translate("start_free_trial")}
 		) #{translate("start_free_trial")}
 
 

+ 104 - 0
services/web/app/views/subscriptions/plans-marketing.pug

@@ -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)

+ 34 - 0
services/web/app/views/subscriptions/plans-marketing/_faq.pug

@@ -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') })}&nbsp;
+				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()" }}])}

+ 86 - 0
services/web/app/views/subscriptions/plans-marketing/_group_plan_modal.pug

@@ -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 &nbsp;
+								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 - 0
services/web/app/views/subscriptions/plans-marketing/_mixins.pug

@@ -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") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li(class="hidden-xs hidden-sm") &nbsp;
+		li
+			br
+			+btn_buy_free(location)
+mixin features_personal(location)
+	ul.list-unstyled
+		li #{translate("one_collaborator")}
+		li &nbsp;
+		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") &nbsp;
+		li
+			br
+			+btn_buy_personal(location)
+mixin features_premium
+	li &nbsp;
+	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)

+ 25 - 0
services/web/app/views/subscriptions/plans-marketing/_quotes.pug

@@ -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 - 0
services/web/app/views/subscriptions/plans-marketing/_tables.pug

@@ -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 &nbsp;
+			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 &nbsp;
+			td
+				+btn_buy_student('table', 'monthly')

+ 2 - 2
services/web/app/views/subscriptions/plans.pug

@@ -45,7 +45,7 @@ block content
 							span.sr-only Amex accepted
 							span.sr-only Amex accepted
 							i.fa.fa-cc-paypal.fa-2x(aria-hidden="true") &nbsp;
 							i.fa.fa-cc-paypal.fa-2x(aria-hidden="true") &nbsp;
 							span.sr-only Paypal accepted
 							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
 							br
 							div.text-centered #{translate('subject_to_additional_vat')}<br/> #{translate('select_country_vat')}
 							div.text-centered #{translate('subject_to_additional_vat')}<br/> #{translate('select_country_vat')}
 
 
@@ -71,7 +71,7 @@ block content
 							+plan_switch('table')
 							+plan_switch('table')
 						.col-md-3.text-right
 						.col-md-3.text-right
 							+currency_dropdown
 							+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'")
 						.col-sm-12(ng-if="ui.view != 'student'")
 							+table_premium
 							+table_premium
 						.col-sm-12(ng-if="ui.view == 'student'")
 						.col-sm-12(ng-if="ui.view == 'student'")

+ 6 - 0
services/web/config/settings.defaults.js

@@ -300,6 +300,12 @@ module.exports = {
     personal: defaultFeatures,
     personal: defaultFeatures,
   },
   },
 
 
+  groupPlanModalOptions: {
+    plan_codes: [],
+    currencies: [],
+    sizes: [],
+    usages: [],
+  },
   plans: [
   plans: [
     {
     {
       planCode: 'personal',
       planCode: 'personal',

+ 10 - 1
services/web/frontend/js/features/contact-form/index.js

@@ -7,6 +7,15 @@ document
 document.querySelectorAll('a[ng-click="contactUsModal()"]').forEach(el => {
 document.querySelectorAll('a[ng-click="contactUsModal()"]').forEach(el => {
   el.addEventListener('click', function (e) {
   el.addEventListener('click', function (e) {
     e.preventDefault()
     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()
+    })
+  })

+ 106 - 0
services/web/frontend/js/features/plans/group-plan-modal/index.js

@@ -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 - 0
services/web/frontend/js/features/utils/swapModal.js

@@ -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')
+}

+ 2 - 6
services/web/frontend/js/main/plans.js

@@ -248,10 +248,6 @@ App.controller(
 
 
     $scope.currencyCode = MultiCurrencyPricing.currencyCode
     $scope.currencyCode = MultiCurrencyPricing.currencyCode
 
 
-    $scope.trial_len = 7
-
-    $scope.planQueryString = '_free_trial_7_days'
-
     $scope.ui = { view: 'monthly' }
     $scope.ui = { view: 'monthly' }
 
 
     $scope.changeCurreny = function (e, newCurrency) {
     $scope.changeCurreny = function (e, newCurrency) {
@@ -265,7 +261,7 @@ App.controller(
       if (view === 'annual') {
       if (view === 'annual') {
         return 'collaborator-annual'
         return 'collaborator-annual'
       } else {
       } else {
-        return `collaborator${$scope.planQueryString}`
+        return `collaborator_free_trial_7_days`
       }
       }
     }
     }
 
 
@@ -274,7 +270,7 @@ App.controller(
       if (view === 'annual') {
       if (view === 'annual') {
         return 'paid-personal-annual'
         return 'paid-personal-annual'
       } else {
       } else {
-        return `paid-personal${$scope.planQueryString}`
+        return `paid-personal_free_trial_7_days`
       }
       }
     }
     }
 
 

+ 78 - 0
services/web/frontend/js/pages/user/subscription/plans.js

@@ -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}&currency=${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()

+ 91 - 0
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -99,6 +99,17 @@ describe('SubscriptionController', function () {
           collaborator: 'COLLABORATORCODEHERE',
           collaborator: 'COLLABORATORCODEHERE',
         },
         },
       },
       },
+      groupPlanModalOptions: {
+        plan_codes: [],
+        currencies: [
+          {
+            display: 'GBP (£)',
+            code: 'GBP',
+          },
+        ],
+        sizes: ['42'],
+        usages: [{ code: 'foo', display: 'Foo' }],
+      },
       apis: {
       apis: {
         recurly: {
         recurly: {
           subdomain: 'sl',
           subdomain: 'sl',
@@ -120,8 +131,14 @@ describe('SubscriptionController', function () {
         getUser: sinon.stub().resolves(this.user),
         getUser: sinon.stub().resolves(this.user),
       },
       },
     }
     }
+    this.SplitTestV2Hander = {
+      promises: {
+        getAssignmentForSession: sinon.stub().resolves({ variant: 'default' }),
+      },
+    }
     this.SubscriptionController = SandboxedModule.require(modulePath, {
     this.SubscriptionController = SandboxedModule.require(modulePath, {
       requires: {
       requires: {
+        '../SplitTests/SplitTestV2Handler': this.SplitTestV2Hander,
         '../Authentication/SessionManager': this.SessionManager,
         '../Authentication/SessionManager': this.SessionManager,
         './SubscriptionHandler': this.SubscriptionHandler,
         './SubscriptionHandler': this.SubscriptionHandler,
         './PlansLocator': this.PlansLocator,
         './PlansLocator': this.PlansLocator,
@@ -166,6 +183,80 @@ describe('SubscriptionController', function () {
         currencyCode: this.stubbedCurrencyCode,
         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 () {
   describe('paymentPage', function () {