Interstitial Payment Page UI + Route (#8305)

GitOrigin-RevId: a31f7094db819e0dad4ff3f09c17544d40260dd4
This commit is contained in:
M Fahru 2022-06-13 05:33:26 -04:00 committed by Copybot
parent 94c09201bb
commit 73cf4116b6
10 changed files with 374 additions and 18 deletions

View file

@ -21,6 +21,7 @@ const { expressify } = require('../../util/promises')
const OError = require('@overleaf/o-error')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const SubscriptionHelper = require('./SubscriptionHelper')
const interstitialPaymentConfig = require('./interstitialPaymentConfig')
const groupPlanModalOptions = Settings.groupPlanModalOptions
const validGroupPlanModalOptions = {
@ -225,6 +226,28 @@ async function userSubscriptionPage(req, res) {
res.render('subscriptions/dashboard', data)
}
async function interstitialPaymentPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { currencyCode: recommendedCurrency } =
await GeoIpLookup.promises.getCurrencyCode(
(req.query ? req.query.ip : undefined) || req.ip
)
const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
if (hasSubscription) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
res.render('subscriptions/interstitial-payment', {
title: 'subscribe',
itm_content: req.query && req.query.itm_content,
recommendedCurrency,
interstitialPaymentConfig,
})
}
}
function createSubscription(req, res, next) {
const user = SessionManager.getSessionUser(req.session)
const recurlyTokenIds = {
@ -560,6 +583,7 @@ module.exports = {
plansPage: expressify(plansPage),
paymentPage: expressify(paymentPage),
userSubscriptionPage: expressify(userSubscriptionPage),
interstitialPaymentPage: expressify(interstitialPaymentPage),
createSubscription,
successfulSubscription,
cancelSubscription,

View file

@ -25,6 +25,12 @@ module.exports = {
SubscriptionController.paymentPage
)
webRouter.get(
'/user/subscription/interstitial-payment',
AuthenticationController.requireLogin(),
SubscriptionController.interstitialPaymentPage
)
webRouter.get(
'/user/subscription/thank-you',
AuthenticationController.requireLogin(),

View file

@ -0,0 +1,217 @@
const config = {
tableHead: {
individual_personal: {},
individual_collaborator: {},
individual_professional: {},
student_student: {
showExtraContent: true,
},
},
highlightedColumn: {
index: 1,
text: {
monthly: 'MOST POPULAR',
annual: 'MOST POPULAR',
},
},
showStudentsOnlyLabel: true,
features: [
{
divider: false,
items: [
{
feature: 'number_of_users',
info: 'number_of_users_info',
value: 'str',
plans: {
personal: '1 user',
collaborator: '1 user',
professional: '1 user',
student: '1 user',
},
},
{
feature: 'max_collab_per_project',
info: 'max_collab_per_project_info',
value: 'str',
plans: {
personal: 'You + 1',
collaborator: 'You + 10',
professional: 'Unlimited',
student: 'You + 6',
},
},
],
},
{
divider: true,
dividerLabel: 'you_and_collaborators_get_access_to',
dividerInfo: 'you_and_collaborators_get_access_to_info',
items: [
{
feature: 'compile_timeout_short',
info: 'compile_timeout_short_info',
value: 'str',
plans: {
personal: '4 minutes',
collaborator: '4 minutes',
professional: '4 minutes',
student: '4 minutes',
},
},
{
feature: 'realtime_track_changes',
info: 'realtime_track_changes_info_v2',
value: 'bool',
plans: {
personal: false,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'full_doc_history',
info: 'full_doc_history_info_v2',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'reference_search',
info: 'reference_search_info_v2',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'git_integration_lowercase',
info: 'git_integration_lowercase_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
],
},
{
divider: true,
dividerLabel: 'you_get_access_to',
dividerInfo: 'you_get_access_to_info',
items: [
{
feature: 'powerful_latex_editor_and_realtime_collaboration',
info: 'powerful_latex_editor_and_realtime_collaboration_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'unlimited_projects',
info: 'unlimited_projects_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'thousands_templates',
info: 'hundreds_templates_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'symbol_palette',
info: 'symbol_palette_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'github_only_integration_lowercase',
info: 'github_only_integration_lowercase_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'dropbox_integration_lowercase',
info: 'dropbox_integration_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'mendeley_integration_lowercase',
info: 'mendeley_integration_lowercase_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'zotero_integration_lowercase',
info: 'zotero_integration_lowercase_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
{
feature: 'priority_support',
info: 'priority_support_info',
value: 'bool',
plans: {
personal: true,
collaborator: true,
professional: true,
student: true,
},
},
],
},
],
}
module.exports = config

View file

@ -2,12 +2,12 @@ const plansV2Features = require('./plansV2Features')
const config = {
individual: {
tableHead: [
'individual_free',
'individual_personal',
'individual_collaborator',
'individual_professional',
],
tableHead: {
individual_free: {},
individual_personal: {},
individual_collaborator: {},
individual_professional: {},
},
features: plansV2Features.individual,
highlightedColumn: {
index: 2,
@ -18,11 +18,11 @@ const config = {
},
},
group: {
tableHead: [
'group_collaborator',
'group_professional',
'group_organization',
],
tableHead: {
group_collaborator: {},
group_professional: {},
group_organization: {},
},
features: plansV2Features.group,
highlightedColumn: {
index: 1,
@ -32,7 +32,13 @@ const config = {
},
},
student: {
tableHead: ['student_free', 'student_student', 'student_university'],
tableHead: {
student_free: {},
student_student: {
showExtraContent: false,
},
student_university: {},
},
features: plansV2Features.student,
highlightedColumn: {
index: 1,

View file

@ -0,0 +1,52 @@
extends ../layout-marketing
include ./plans-marketing/_mixins
include ./plans-marketing/_tables
include ./plans-marketing/v2/_mixins
block vars
- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
block append meta
meta(name="ol-recommendedCurrency" content=recommendedCurrency)
meta(name="ol-itm_content" content=itm_content)
block content
main.content.content-alt#main-content
.content-page
.plans
.container
.row
.col-md-12
.page-header.centered.plans-header.text-centered.top-page-header
h1.text-capitalize #{translate('choose_your_plan')}
//- TODO: add analytics by adding 2 arguments in the mixin below
+monthly_annual_switch()
.row.plans-v2-table-sticky-header.plans-v2-table-sticky-header-individual.sticky(data-ol-plans-v2-table-sticky-header='individual')
.plans-v2-table-sticky-header-item
span #{translate("personal")}
.plans-v2-table-sticky-header-item.plans-v2-table-sticky-header-item-highlighted
span #{translate("standard")}
.plans-v2-table-sticky-header-item
span #{translate("professional")}
.plans-v2-table-sticky-header-item
span #{translate("student")}
.row.plans-v2-table-container(data-ol-plans-v2-table-container='monthly')
.col-sm-12
.row
table.card.plans-v2-table.plans-v2-table-individual
+plans_v2_table('monthly', interstitialPaymentConfig)
.row.plans-v2-table-container(hidden data-ol-plans-v2-table-container='annual')
.col-sm-12
.row
table.card.plans-v2-table.plans-v2-table-individual
+plans_v2_table('annual', interstitialPaymentConfig)
//- sticky header on mobile will be "hidden" (by removing its sticky position) if it reaches this div
.invisible(aria-hidden="true" data-ol-plans-v2-table-sticky-header-stop)
!= moduleIncludes("contactModalGeneral-marketing", locals)

View file

@ -2,12 +2,14 @@ mixin plans_v2_table(period, config)
tr
th
- for (var i = 0; i < 4; i++)
- var tableHeadKey = Object.keys(config.tableHead)[i]
- var tableHeadOptions = Object.values(config.tableHead)[i]
th(
class=(i === config.highlightedColumn.index ? 'plans-v2-table-green-highlighted' : (i === config.highlightedColumn.index - 1 ? 'plans-v2-table-cell-before-highlighted-column' : ''))
)
if (i === config.highlightedColumn.index)
p.plans-v2-table-green-highlighted-text !{config.highlightedColumn.text[period]}
case config.tableHead[i]
case tableHeadKey
when 'individual_free'
+table_head_individual_free(period)
when 'individual_personal'
@ -25,14 +27,14 @@ mixin plans_v2_table(period, config)
when 'student_free'
+table_head_student_free(period)
when 'student_student'
+table_head_student_student(period)
+table_head_student_student(period, tableHeadOptions.showExtraContent)
when 'student_university'
+table_head_student_university(period)
for featuresPerSection in config.features
if featuresPerSection.divider
tr.plans-v2-table-divider
td(colspan=config.tableHead.length + 1)
td(colspan=Object.keys(config.tableHead).length + 1)
div
b.plans-v2-table-divider-label #{translate(featuresPerSection.dividerLabel)}
//- will only appear on screen width >= 768px (using CSS)
@ -220,7 +222,7 @@ mixin table_head_student_free(period)
.plans-v2-table-btn-buy-container-desktop
+btn_buy_student_free()
mixin table_head_student_student(period)
mixin table_head_student_student(period, showExtraContent)
div.plans-v2-table-th-content
p.plans-v2-table-th-content-title #{translate("student")}
+table_head_price('student', period)
@ -229,6 +231,9 @@ mixin table_head_student_student(period)
ul.plans-v2-table-th-content-benefit
li !{translate("x_collaborators_per_project", {collaboratorsCount: '6'})}
li #{translate("all_premium_features")}
if showExtraContent
li #{translate("for_students_only")}
.plans-v2-table-btn-buy-container-desktop
+btn_buy_student_student(period)

View file

@ -131,7 +131,13 @@ document
})
})
updateGroupModalPlanPricing()
const isGroupPlanModalAvailable = document.querySelector(
'[data-ol-group-plan-modal]'
)
if (isGroupPlanModalAvailable) {
updateGroupModalPlanPricing()
}
if (window.location.hash === '#groups') {
showGroupPlanModal()

View file

@ -63,4 +63,10 @@ document
.querySelectorAll('[data-ol-group-plan-form] input')
.forEach(el => el.addEventListener('change', changePlansV2MainPageGroupData))
hideCurrencyPicker()
const isGroupPlanModalAvailable = document.querySelector(
'[data-ol-group-plan-modal]'
)
if (isGroupPlanModalAvailable) {
hideCurrencyPicker()
}

View file

@ -898,6 +898,7 @@
"for_enterprise": "For enterprise",
"for_universities": "For universities",
"for_students": "For students",
"for_students_only": "For students only",
"get_involved": "Get involved",
"become_an_advisor": "Become an __appName__ advisor",
"participate_in_user_research": "Participate in user research",

View file

@ -220,6 +220,39 @@ describe('SubscriptionController', function () {
})
})
describe('interstitialPaymentPage', function () {
beforeEach(function () {
this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
this.GeoIpLookup.promises.getCurrencyCode.resolves({
currencyCode: this.stubbedCurrencyCode,
})
})
describe('with a user without subscription', function () {
it('should render the interstitial payment page', function (done) {
this.res.render = (page, opts) => {
page.should.equal('subscriptions/interstitial-payment')
done()
}
this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
})
})
describe('with a user with subscription', function () {
it('should redirect to the subscription dashboard', function (done) {
this.PlansLocator.findLocalPlanInSettings.returns({})
this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
true
)
this.res.redirect = url => {
url.should.equal('/user/subscription?hasSubscription=true')
done()
}
this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
})
})
})
describe('paymentPage', function () {
beforeEach(function () {
this.req.headers = {}