mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Interstitial Payment Page UI + Route (#8305)
GitOrigin-RevId: a31f7094db819e0dad4ff3f09c17544d40260dd4
This commit is contained in:
parent
94c09201bb
commit
73cf4116b6
10 changed files with 374 additions and 18 deletions
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -131,7 +131,13 @@ document
|
|||
})
|
||||
})
|
||||
|
||||
updateGroupModalPlanPricing()
|
||||
const isGroupPlanModalAvailable = document.querySelector(
|
||||
'[data-ol-group-plan-modal]'
|
||||
)
|
||||
|
||||
if (isGroupPlanModalAvailable) {
|
||||
updateGroupModalPlanPricing()
|
||||
}
|
||||
|
||||
if (window.location.hash === '#groups') {
|
||||
showGroupPlanModal()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
Loading…
Reference in a new issue