Merge pull request #11147 from overleaf/ii-subscriptions-pages-react-split-test

[web] React subscription split test

GitOrigin-RevId: 6656b3895030bc677483a3e30d5e998f5f7d1458
This commit is contained in:
ilkin-overleaf 2023-01-12 16:53:35 +02:00 committed by Copybot
parent 7d661ee573
commit 7e68b4f0d5
22 changed files with 529 additions and 54 deletions

View file

@ -104,8 +104,106 @@ async function plansPage(req, res) {
}) })
} }
// get to show the recurly.js page
async function paymentPage(req, res) { async function paymentPage(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
// get to show the recurly.js page
if (assignment.variant === 'active') {
await _paymentReactPage(req, res)
} else {
await _paymentAngularPage(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _paymentAngularPage(req, res)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _paymentReactPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) {
return HttpErrorHandler.unprocessableEntity(req, res, 'Plan not found')
}
const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
if (hasSubscription) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
// LimitationsManager.userHasV2Subscription only checks Mongo. Double check with
// Recurly as well at this point (we don't do this most places for speed).
const valid =
await SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
user._id
)
if (!valid) {
res.redirect('/user/subscription?hasSubscription=true')
} else {
let currency = null
if (req.query.currency) {
const queryCurrency = req.query.currency.toUpperCase()
if (GeoIpLookup.isValidCurrencyParam(queryCurrency)) {
currency = queryCurrency
}
}
const { currencyCode: recommendedCurrency, countryCode } =
await GeoIpLookup.promises.getCurrencyCode(
(req.query ? req.query.ip : undefined) || req.ip
)
if (recommendedCurrency && currency == null) {
currency = recommendedCurrency
}
const refreshedPaymentPageAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'payment-page-refresh'
)
const useRefreshedPaymentPage =
refreshedPaymentPageAssignment &&
refreshedPaymentPageAssignment.variant === 'refreshed-payment-page'
await SplitTestHandler.promises.getAssignment(
req,
res,
'student-check-modal'
)
// TODO
const template = useRefreshedPaymentPage
? 'subscriptions/new-react'
: 'subscriptions/new-react'
res.render(template, {
title: 'subscribe',
currency,
countryCode,
plan,
recurlyConfig: JSON.stringify({
currency,
subdomain: Settings.apis.recurly.subdomain,
}),
showCouponField: !!req.query.scf,
showVatField: !!req.query.svf,
})
}
}
}
async function _paymentAngularPage(req, res) {
const user = SessionManager.getSessionUser(req.session) const user = SessionManager.getSessionUser(req.session)
const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode) const plan = PlansLocator.findLocalPlanInSettings(req.query.planCode)
if (!plan) { if (!plan) {
@ -177,6 +275,84 @@ async function paymentPage(req, res) {
} }
async function userSubscriptionPage(req, res) { async function userSubscriptionPage(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _userSubscriptionReactPage(req, res)
} else {
await _userSubscriptionAngularPage(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _userSubscriptionAngularPage(req, res)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _userSubscriptionReactPage(req, res) {
const user = SessionManager.getSessionUser(req.session)
const results =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user
)
const {
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
currentInstitutionsWithLicence,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
} = results
const hasSubscription =
await LimitationsManager.promises.userHasV1OrV2Subscription(user)
const fromPlansPage = req.query.hasSubscription
const plans = SubscriptionViewModelBuilder.buildPlansList(
personalSubscription ? personalSubscription.plan : undefined
)
AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')
const cancelButtonAssignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-cancel-button'
)
const cancelButtonNewCopy = cancelButtonAssignment?.variant === 'new-copy'
const data = {
title: 'your_subscription',
plans,
groupPlans: GroupPlansData,
user,
hasSubscription,
fromPlansPage,
personalSubscription,
memberGroupSubscriptions,
managedGroupSubscriptions,
managedInstitutions,
managedPublishers,
v1SubscriptionStatus,
currentInstitutionsWithLicence,
groupPlanModalOptions,
cancelButtonNewCopy,
}
res.render('subscriptions/dashboard-react', data)
}
async function _userSubscriptionAngularPage(req, res) {
const user = SessionManager.getSessionUser(req.session) const user = SessionManager.getSessionUser(req.session)
const results = const results =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
@ -302,6 +478,32 @@ async function createSubscription(req, res) {
} }
async function successfulSubscription(req, res) { async function successfulSubscription(req, res) {
try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _successfulSubscriptionReact(req, res)
} else {
await _successfulSubscriptionAngular(req, res)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _successfulSubscriptionAngular(req, res)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @returns {Promise<void>}
*/
async function _successfulSubscriptionReact(req, res) {
const user = SessionManager.getSessionUser(req.session) const user = SessionManager.getSessionUser(req.session)
const { personalSubscription } = const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
@ -313,7 +515,27 @@ async function successfulSubscription(req, res) {
if (!personalSubscription) { if (!personalSubscription) {
res.redirect('/user/subscription/plans') res.redirect('/user/subscription/plans')
} else { } else {
res.render('subscriptions/successful_subscription', { res.render('subscriptions/successful-subscription-react', {
title: 'thank_you',
personalSubscription,
postCheckoutRedirect,
})
}
}
async function _successfulSubscriptionAngular(req, res) {
const user = SessionManager.getSessionUser(req.session)
const { personalSubscription } =
await SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
user
)
const postCheckoutRedirect = req.session?.postCheckoutRedirect
if (!personalSubscription) {
res.redirect('/user/subscription/plans')
} else {
res.render('subscriptions/successful-subscription', {
title: 'thank_you', title: 'thank_you',
personalSubscription, personalSubscription,
postCheckoutRedirect, postCheckoutRedirect,
@ -337,8 +559,41 @@ function cancelSubscription(req, res, next) {
}) })
} }
function canceledSubscription(req, res, next) { async function canceledSubscription(req, res, next) {
return res.render('subscriptions/canceled_subscription', { try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _canceledSubscriptionReact(req, res, next)
} else {
await _canceledSubscriptionAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _canceledSubscriptionAngular(req, res, next)
}
}
/**
* @param {import("express").Request} req
* @param {import("express").Response} res
* @param {import("express").NextFunction} next
* @returns {Promise<void>}
*/
function _canceledSubscriptionReact(req, res, next) {
return res.render('subscriptions/canceled-subscription-react', {
title: 'subscription_canceled',
})
}
function _canceledSubscriptionAngular(req, res, next) {
return res.render('subscriptions/canceled-subscription', {
title: 'subscription_canceled', title: 'subscription_canceled',
}) })
} }

View file

@ -16,10 +16,65 @@ const Errors = require('../Errors/Errors')
const EmailHelper = require('../Helpers/EmailHelper') const EmailHelper = require('../Helpers/EmailHelper')
const { csvAttachment } = require('../../infrastructure/Response') const { csvAttachment } = require('../../infrastructure/Response')
const { UserIsManagerError } = require('./UserMembershipErrors') const { UserIsManagerError } = require('./UserMembershipErrors')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const CSVParser = require('json2csv').Parser const CSVParser = require('json2csv').Parser
const logger = require('@overleaf/logger')
module.exports = { async function index(req, res, next) {
index(req, res, next) { try {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
res,
'subscription-pages-react'
)
if (assignment.variant === 'active') {
await _indexReact(req, res, next)
} else {
await _indexAngular(req, res, next)
}
} catch (error) {
logger.warn(
{ err: error },
'failed to get "subscription-pages-react" split test assignment'
)
await _indexAngular(req, res, next)
}
}
function _indexReact(req, res, next) {
const { entity, entityConfig } = req
return entity.fetchV1Data(function (error, entity) {
if (error != null) {
return next(error)
}
return UserMembershipHandler.getUsers(
entity,
entityConfig,
function (error, users) {
let entityName
if (error != null) {
return next(error)
}
const entityPrimaryKey =
entity[entityConfig.fields.primaryKey].toString()
if (entityConfig.fields.name) {
entityName = entity[entityConfig.fields.name]
}
return res.render('user_membership/index-react', {
name: entityName,
users,
groupSize: entityConfig.hasMembersLimit
? entity.membersLimit
: undefined,
translations: entityConfig.translations,
paths: entityConfig.pathsFor(entityPrimaryKey),
})
}
)
})
}
function _indexAngular(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
return entity.fetchV1Data(function (error, entity) { return entity.fetchV1Data(function (error, entity) {
if (error != null) { if (error != null) {
@ -50,8 +105,10 @@ module.exports = {
} }
) )
}) })
}, }
module.exports = {
index,
add(req, res, next) { add(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
const email = EmailHelper.parseEmail(req.body.email) const email = EmailHelper.parseEmail(req.body.email)
@ -96,7 +153,6 @@ module.exports = {
} }
) )
}, },
remove(req, res, next) { remove(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
const { userId } = req.params const { userId } = req.params
@ -135,7 +191,6 @@ module.exports = {
} }
) )
}, },
exportCsv(req, res, next) { exportCsv(req, res, next) {
const { entity, entityConfig } = req const { entity, entityConfig } = req
const fields = ['email', 'last_logged_in_at', 'last_active_at'] const fields = ['email', 'last_logged_in_at', 'last_active_at']
@ -152,14 +207,12 @@ module.exports = {
} }
) )
}, },
new(req, res, next) { new(req, res, next) {
return res.render('user_membership/new', { return res.render('user_membership/new', {
entityName: req.params.name, entityName: req.params.name,
entityId: req.params.id, entityId: req.params.id,
}) })
}, },
create(req, res, next) { create(req, res, next) {
const entityId = req.params.id const entityId = req.params.id
const entityConfig = req.entityConfig const entityConfig = req.entityConfig

View file

@ -0,0 +1,7 @@
extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/canceled-subscription'
block content
main.content.content-alt#subscription-canceled-root

View file

@ -0,0 +1,17 @@
extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/dashboard'
block append meta
meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd)
if (personalSubscription && personalSubscription.recurly)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
meta(name="ol-subscription" data-type="json" content=personalSubscription)
meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)
meta(name="ol-groupPlans" data-type="json" content=groupPlans)
meta(name="ol-groupPlanModalOptions" data-type="json" content=groupPlanModalOptions)
block content
main.content.content-alt#subscription-dashboard-root

View file

@ -0,0 +1,14 @@
extends ../layout-marketing
include ./_new_mixins
block entrypointVar
- entrypoint = 'pages/user/subscription/new'
block append meta
meta(name="ol-countryCode" content=countryCode)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
meta(name="ol-recommendedCurrency" content=String(currency).slice(0,3))
block content
main.content.content-alt#subscription-new-root

View file

@ -0,0 +1,7 @@
extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/successful-subscription'
block content
main.content.content-alt#subscription-success-root

View file

@ -0,0 +1,12 @@
extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/membership/groups'
block append meta
meta(name="ol-users", data-type="json", content=users)
meta(name="ol-paths", data-type="json", content=paths)
meta(name="ol-groupSize", data-type="json", content=groupSize)
block content
main.content.content-alt#subscription-manage-groups-root

View file

@ -0,0 +1,5 @@
function Root() {
return <h2>React Manage Group Subscription</h2>
}
export default Root

View file

@ -0,0 +1,13 @@
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
function Root() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <h2>React Subscription Canceled</h2>
}
export default Root

View file

@ -0,0 +1,13 @@
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
function Root() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <h2>React Subscription Dashboard</h2>
}
export default Root

View file

@ -0,0 +1,13 @@
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
function Root() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <h2>React Subscription New</h2>
}
export default Root

View file

@ -0,0 +1,13 @@
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
function Root() {
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <h2>React Subscription Success</h2>
}
export default Root

View file

@ -0,0 +1,7 @@
import ReactDOM from 'react-dom'
import Root from '../../../features/membership/components/groups-root'
const element = document.getElementById('subscription-manage-groups-root')
if (element) {
ReactDOM.render(<Root />, element)
}

View file

@ -0,0 +1,11 @@
import 'jquery'
import 'bootstrap'
import './../../../utils/meta'
import './../../../utils/webpack-public-path'
import './../../../infrastructure/error-reporter'
import './../../../i18n'
import '../../../cdn-load-test'
import '../../../features/contact-form'
import '../../../features/event-tracking'
import '../../../features/cookie-banner'
import '../../../features/link-helpers/slow-link'

View file

@ -0,0 +1,8 @@
import './base'
import ReactDOM from 'react-dom'
import Root from '../../../features/subscription/components/canceled-subscription/root'
const element = document.getElementById('subscription-canceled-root')
if (element) {
ReactDOM.render(<Root />, element)
}

View file

@ -0,0 +1,8 @@
import './base'
import ReactDOM from 'react-dom'
import Root from '../../../features/subscription/components/dashboard/root'
const element = document.getElementById('subscription-dashboard-root')
if (element) {
ReactDOM.render(<Root />, element)
}

View file

@ -0,0 +1,8 @@
import './base'
import ReactDOM from 'react-dom'
import Root from '../../../features/subscription/components/new/root'
const element = document.getElementById('subscription-new-root')
if (element) {
ReactDOM.render(<Root />, element)
}

View file

@ -0,0 +1,8 @@
import './base'
import ReactDOM from 'react-dom'
import Root from '../../../features/subscription/components/successful-subscription/root'
const element = document.getElementById('subscription-success-root')
if (element) {
ReactDOM.render(<Root />, element)
}

View file

@ -399,7 +399,7 @@ describe('SubscriptionController', function () {
} }
) )
this.res.render = (url, variables) => { this.res.render = (url, variables) => {
url.should.equal('subscriptions/successful_subscription') url.should.equal('subscriptions/successful-subscription')
assert.deepEqual(variables, { assert.deepEqual(variables, {
title: 'thank_you', title: 'thank_you',
personalSubscription: 'foo', personalSubscription: 'foo',

View file

@ -70,12 +70,19 @@ describe('UserMembershipController', function () {
addUser: sinon.stub().yields(null, this.newUser), addUser: sinon.stub().yields(null, this.newUser),
removeUser: sinon.stub().yields(null), removeUser: sinon.stub().yields(null),
} }
this.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'default' }),
},
getAssignment: sinon.stub().yields(null, { variant: 'default' }),
}
return (this.UserMembershipController = SandboxedModule.require( return (this.UserMembershipController = SandboxedModule.require(
modulePath, modulePath,
{ {
requires: { requires: {
'./UserMembershipErrors': { UserIsManagerError }, './UserMembershipErrors': { UserIsManagerError },
'../Authentication/SessionManager': this.SessionManager, '../Authentication/SessionManager': this.SessionManager,
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
'./UserMembershipHandler': this.UserMembershipHandler, './UserMembershipHandler': this.UserMembershipHandler,
}, },
} }
@ -88,21 +95,20 @@ describe('UserMembershipController', function () {
return (this.req.entityConfig = EntityConfigs.group) return (this.req.entityConfig = EntityConfigs.group)
}) })
it('get users', function (done) { it('get users', async function () {
return this.UserMembershipController.index(this.req, { return await this.UserMembershipController.index(this.req, {
render: () => { render: () => {
sinon.assert.calledWithMatch( sinon.assert.calledWithMatch(
this.UserMembershipHandler.getUsers, this.UserMembershipHandler.getUsers,
this.subscription, this.subscription,
{ modelName: 'Subscription' } { modelName: 'Subscription' }
) )
return done()
}, },
}) })
}) })
it('render group view', function (done) { it('render group view', async function () {
return this.UserMembershipController.index(this.req, { return await this.UserMembershipController.index(this.req, {
render: (viewPath, viewParams) => { render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index') expect(viewPath).to.equal('user_membership/index')
expect(viewParams.users).to.deep.equal(this.users) expect(viewParams.users).to.deep.equal(this.users)
@ -111,14 +117,13 @@ describe('UserMembershipController', function () {
expect(viewParams.paths.addMember).to.equal( expect(viewParams.paths.addMember).to.equal(
`/manage/groups/${this.subscription._id}/invites` `/manage/groups/${this.subscription._id}/invites`
) )
return done()
}, },
}) })
}) })
it('render group managers view', function (done) { it('render group managers view', async function () {
this.req.entityConfig = EntityConfigs.groupManagers this.req.entityConfig = EntityConfigs.groupManagers
return this.UserMembershipController.index(this.req, { return await this.UserMembershipController.index(this.req, {
render: (viewPath, viewParams) => { render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index') expect(viewPath).to.equal('user_membership/index')
expect(viewParams.groupSize).to.equal(undefined) expect(viewParams.groupSize).to.equal(undefined)
@ -127,22 +132,20 @@ describe('UserMembershipController', function () {
'managers_management' 'managers_management'
) )
expect(viewParams.paths.exportMembers).to.be.undefined expect(viewParams.paths.exportMembers).to.be.undefined
return done()
}, },
}) })
}) })
it('render institution view', function (done) { it('render institution view', async function () {
this.req.entity = this.institution this.req.entity = this.institution
this.req.entityConfig = EntityConfigs.institution this.req.entityConfig = EntityConfigs.institution
return this.UserMembershipController.index(this.req, { return await this.UserMembershipController.index(this.req, {
render: (viewPath, viewParams) => { render: (viewPath, viewParams) => {
expect(viewPath).to.equal('user_membership/index') expect(viewPath).to.equal('user_membership/index')
expect(viewParams.name).to.equal('Test Institution Name') expect(viewParams.name).to.equal('Test Institution Name')
expect(viewParams.groupSize).to.equal(undefined) expect(viewParams.groupSize).to.equal(undefined)
expect(viewParams.translations.title).to.equal('institution_account') expect(viewParams.translations.title).to.equal('institution_account')
expect(viewParams.paths.exportMembers).to.be.undefined expect(viewParams.paths.exportMembers).to.be.undefined
return done()
}, },
}) })
}) })