Add split test for persistent upgrade prompts project dash and editor header (#7590)
* Add Upgrade buttons on project dash and editor header GitOrigin-RevId: 16325ffb2e63beeaff914e0b95db3faddcdf241a
This commit is contained in:
@@ -40,6 +40,7 @@ const FeaturesUpdater = require('../Subscription/FeaturesUpdater')
|
||||
const SpellingHandler = require('../Spelling/SpellingHandler')
|
||||
const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
|
||||
const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
|
||||
const InstitutionsFeatures = require('../Institutions/InstitutionsFeatures')
|
||||
|
||||
const _ssoAvailable = (affiliation, session, linkedInstitutionIds) => {
|
||||
if (!affiliation.institution) return false
|
||||
@@ -458,14 +459,38 @@ const ProjectController = {
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
persistentUpgradePromptsAssignment(cb) {
|
||||
SplitTestHandler.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'persistent-upgrade-prompt',
|
||||
(err, assignment) => {
|
||||
if (err) {
|
||||
logger.warn(
|
||||
{ err },
|
||||
'failed to get "persistent-upgrade-prompt" split test assignment'
|
||||
)
|
||||
cb(null, { variant: 'default' })
|
||||
} else {
|
||||
cb(null, assignment)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
(err, results) => {
|
||||
if (err != null) {
|
||||
OError.tag(err, 'error getting data for project list page')
|
||||
return next(err)
|
||||
}
|
||||
const { notifications, user, userEmailsData, primaryEmailCheckActive } =
|
||||
results
|
||||
const {
|
||||
notifications,
|
||||
user,
|
||||
userEmailsData,
|
||||
primaryEmailCheckActive,
|
||||
persistentUpgradePromptsAssignment,
|
||||
} = results
|
||||
|
||||
if (
|
||||
user &&
|
||||
@@ -594,6 +619,12 @@ const ProjectController = {
|
||||
)
|
||||
}
|
||||
|
||||
// Persistent upgrade prompts
|
||||
const showToolbarUpgradePrompt =
|
||||
persistentUpgradePromptsAssignment.variant === 'persistent-upgrade' &&
|
||||
!results.hasSubscription &&
|
||||
!userEmails.some(e => e.emailHasInstitutionLicence)
|
||||
|
||||
ProjectController._injectProjectUsers(projects, (error, projects) => {
|
||||
if (error != null) {
|
||||
return next(error)
|
||||
@@ -616,6 +647,7 @@ const ProjectController = {
|
||||
isOverleaf: !!Settings.overleaf,
|
||||
metadata: { viewport: false },
|
||||
showThinFooter: true, // don't show the fat footer on the projects dashboard, as there's a fixed space available
|
||||
showToolbarUpgradePrompt,
|
||||
}
|
||||
|
||||
const paidUser =
|
||||
@@ -720,6 +752,18 @@ const ProjectController = {
|
||||
)
|
||||
}
|
||||
},
|
||||
userHasInstitutionLicence(cb) {
|
||||
if (!userId) {
|
||||
return cb(null, false)
|
||||
}
|
||||
InstitutionsFeatures.hasLicence(userId, (error, hasLicence) => {
|
||||
if (error) {
|
||||
// Don't fail if we can't get affiliation licences
|
||||
return cb(null, false)
|
||||
}
|
||||
cb(null, hasLicence)
|
||||
})
|
||||
},
|
||||
learnedWords(cb) {
|
||||
if (!userId) {
|
||||
return cb(null, [])
|
||||
@@ -838,12 +882,28 @@ const ProjectController = {
|
||||
}
|
||||
)
|
||||
},
|
||||
persistentUpgradePromptsAssignment(cb) {
|
||||
SplitTestHandler.getAssignment(
|
||||
req,
|
||||
res,
|
||||
'persistent-upgrade-prompt',
|
||||
(error, assignment) => {
|
||||
// do not fail editor load if assignment fails
|
||||
if (error) {
|
||||
cb(null, { variant: 'default' })
|
||||
} else {
|
||||
cb(null, assignment)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
},
|
||||
(
|
||||
err,
|
||||
{
|
||||
project,
|
||||
user,
|
||||
userHasInstitutionLicence,
|
||||
learnedWords,
|
||||
subscription,
|
||||
isTokenMember,
|
||||
@@ -851,6 +911,7 @@ const ProjectController = {
|
||||
newSourceEditorAssignment,
|
||||
pdfDetachAssignment,
|
||||
pdfjsAssignment,
|
||||
persistentUpgradePromptsAssignment,
|
||||
}
|
||||
) => {
|
||||
if (err != null) {
|
||||
@@ -956,6 +1017,14 @@ const ProjectController = {
|
||||
!Features.hasFeature('saas') ||
|
||||
(user.features && user.features.symbolPalette)
|
||||
|
||||
// Persistent upgrade prompts
|
||||
const showHeaderUpgradePrompt =
|
||||
persistentUpgradePromptsAssignment.variant ===
|
||||
'persistent-upgrade' &&
|
||||
userId &&
|
||||
!subscription &&
|
||||
!userHasInstitutionLicence
|
||||
|
||||
const template =
|
||||
detachRole === 'detached'
|
||||
? 'project/editor_detached'
|
||||
@@ -1027,6 +1096,7 @@ const ProjectController = {
|
||||
!shouldDisplayFeature('enable_pdf_caching', false),
|
||||
detachRole,
|
||||
metadata: { viewport: false },
|
||||
showHeaderUpgradePrompt,
|
||||
})
|
||||
timer.done()
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ meta(name="ol-enablePdfCaching" data-type="boolean" content=enablePdfCaching)
|
||||
meta(name="ol-trackPdfDownload" data-type="boolean" content=trackPdfDownload)
|
||||
meta(name="ol-resetServiceWorker" data-type="boolean" content=resetServiceWorker)
|
||||
meta(name="ol-detachRole" data-type="string" content=detachRole)
|
||||
meta(name="ol-showHeaderUpgradePrompt" data-type="boolean" content=showHeaderUpgradePrompt)
|
||||
|
||||
- var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {})
|
||||
meta(name="ol-fileActionI18n" data-type="json" content=fileActionI18n)
|
||||
|
||||
@@ -23,7 +23,21 @@
|
||||
ng-show="searchText.value.length > 0"
|
||||
) #{translate('clear_search')}
|
||||
|
||||
|
||||
.project-tools(ng-cloak)
|
||||
if (showToolbarUpgradePrompt)
|
||||
.project-list-upgrade-prompt(ng-cloak ng-hide="selectedProjects.length > 0")
|
||||
span You're on the free plan
|
||||
a.btn.btn-primary(
|
||||
href="/user/subscription/plans"
|
||||
event-tracking="upgrade-button-click"
|
||||
event-tracking-mb="true"
|
||||
event-tracking-ga="subscription-funnel"
|
||||
event-tracking-action="dashboard-top"
|
||||
event-tracking-label="upgrade"
|
||||
event-tracking-trigger="click"
|
||||
event-segmentation='{"source": "dashboard-top"}'
|
||||
) Upgrade
|
||||
.btn-toolbar
|
||||
.btn-group(ng-hide="selectedProjects.length < 1")
|
||||
a.btn.btn-default(
|
||||
|
||||
@@ -134,11 +134,12 @@ if (isOverleaf)
|
||||
href="/user/settings"
|
||||
) Add Affiliation
|
||||
|
||||
.row-spaced(ng-if="hasProjects && userHasNoSubscription && !userOnPayingUniversity", ng-cloak).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus" ng-click="share()") #{translate("sharing_sl")}.
|
||||
unless (showToolbarUpgradePrompt)
|
||||
.row-spaced(ng-if="hasProjects && userHasNoSubscription && !userOnPayingUniversity", ng-cloak).text-centered
|
||||
hr
|
||||
p.small #{translate("on_free_sl")}
|
||||
p
|
||||
a(href="/user/subscription/plans" ng-click="upgradeSubscription()").btn.btn-primary #{translate("upgrade")}
|
||||
p.small.text-centered
|
||||
| #{translate("or_unlock_features_bonus")}
|
||||
a(href="/user/bonus" ng-click="share()") #{translate("sharing_sl")}.
|
||||
|
||||
+4
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import MenuButton from './menu-button'
|
||||
import CobrandingLogo from './cobranding-logo'
|
||||
import BackToProjectsButton from './back-to-projects-button'
|
||||
import UpgradePrompt from './upgrade-prompt'
|
||||
import ChatToggleButton from './chat-toggle-button'
|
||||
import LayoutDropdownButton from './layout-dropdown-button'
|
||||
import OnlineUsersWidget from './online-users-widget'
|
||||
@@ -64,6 +65,9 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
|
||||
pdfViewIsOpen={pdfViewIsOpen}
|
||||
/>
|
||||
)}
|
||||
{window.showHeaderUpgradePrompt && !pdfButtonIsVisible && (
|
||||
<UpgradePrompt />
|
||||
)}
|
||||
<ProjectNameEditableLabel
|
||||
className="toolbar-center"
|
||||
projectName={projectName}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
function UpgradePrompt() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
function handleClick(e) {
|
||||
eventTracking.send('subscription-funnel', 'code-editor', 'upgrade')
|
||||
eventTracking.sendMB('upgrade-button-click', { source: 'code-editor' })
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="toolbar-header-upgrade-prompt btn btn-primary btn-xs"
|
||||
href="/user/subscription/plans"
|
||||
target="_blank"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t('upgrade')}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradePrompt
|
||||
@@ -222,6 +222,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-header-upgrade-prompt {
|
||||
@media (max-width: @screen-md-min) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-dark {
|
||||
.toolbar-alt {
|
||||
background-color: darken(@editor-dark-background-color, 0%);
|
||||
|
||||
@@ -129,6 +129,9 @@ describe('ProjectController', function () {
|
||||
},
|
||||
getAssignment: sinon.stub().yields(null, { variant: 'default' }),
|
||||
}
|
||||
this.InstitutionsFeatures = {
|
||||
hasLicence: sinon.stub().callsArgWith(1, null, false),
|
||||
}
|
||||
|
||||
this.ProjectController = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
@@ -171,6 +174,7 @@ describe('ProjectController', function () {
|
||||
'../Spelling/SpellingHandler': {
|
||||
getUserDictionary: sinon.stub().yields(null, []),
|
||||
},
|
||||
'../Institutions/InstitutionsFeatures': this.InstitutionsFeatures,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -546,6 +550,57 @@ describe('ProjectController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistent upgrade prompt', function () {
|
||||
describe('if the user has the default variant', function (done) {
|
||||
it('should not show', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showToolbarUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('if the user has the persistent-upgrade variant', function (done) {
|
||||
beforeEach(function () {
|
||||
this.SplitTestHandler.getAssignment
|
||||
.withArgs(this.req, this.res, 'persistent-upgrade-prompt')
|
||||
.yields(null, { variant: 'persistent-upgrade' })
|
||||
})
|
||||
it('should show for a user without a subscription or only non-paid affiliations', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showToolbarUpgradePrompt).to.equal(true)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should not show for a user with a subscription', function (done) {
|
||||
this.LimitationsManager.hasPaidSubscription = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showToolbarUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
it('should not show for a user with an affiliated paid university', function (done) {
|
||||
const emailWithProAffiliation = {
|
||||
email: 'pro@example.com',
|
||||
emailHasInstitutionLicence: true,
|
||||
}
|
||||
this.UserGetter.getUserFullEmails = sinon
|
||||
.stub()
|
||||
.yields(null, [emailWithProAffiliation])
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showToolbarUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.projectListPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('With Institution SSO feature', function () {
|
||||
beforeEach(function (done) {
|
||||
this.institutionEmail = 'test@overleaf.com'
|
||||
@@ -1441,6 +1496,59 @@ describe('ProjectController', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistent upgrade prompt', function () {
|
||||
beforeEach(function () {
|
||||
// default to without a subscription
|
||||
this.SubscriptionLocator.getUsersSubscription = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, null)
|
||||
})
|
||||
describe('if the user has the default variant', function (done) {
|
||||
it('should not show', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showHeaderUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('if the user has the persistent-upgrade variant', function (done) {
|
||||
beforeEach(function () {
|
||||
this.SplitTestHandler.getAssignment
|
||||
.withArgs(this.req, this.res, 'persistent-upgrade-prompt')
|
||||
.yields(null, { variant: 'persistent-upgrade' })
|
||||
})
|
||||
it('should show for a user without a subscription or only non-paid affiliations', function (done) {
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showHeaderUpgradePrompt).to.equal(true)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
it('should not show for a user with a subscription', function (done) {
|
||||
this.SubscriptionLocator.getUsersSubscription = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, {})
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showHeaderUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
it('should not show for a user with an affiliated paid university', function (done) {
|
||||
this.InstitutionsFeatures.hasLicence = sinon
|
||||
.stub()
|
||||
.callsArgWith(1, null, true)
|
||||
this.res.render = (pageName, opts) => {
|
||||
expect(opts.showHeaderUpgradePrompt).to.equal(false)
|
||||
done()
|
||||
}
|
||||
this.ProjectController.loadEditor(this.req, this.res)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('userProjectsJson', function () {
|
||||
|
||||
Reference in New Issue
Block a user