Update B2B Groups and Enterprise banners (#11989)

* Tear down B2B banner ad split test and implement updated ads

GitOrigin-RevId: 7d09d54bef7cb4e2b2b597d3834e0f58551b179e
This commit is contained in:
Thomas 2023-02-27 16:49:19 +01:00 committed by Copybot
parent 3ead64344f
commit 16b2d27fde
8 changed files with 164 additions and 151 deletions

View file

@ -532,26 +532,6 @@ const ProjectController = {
}
)
},
groupsAndEnterpriseBannerAssignment(cb) {
SplitTestHandler.getAssignment(
req,
res,
'groups-and-enterprise-banner',
(err, assignment) => {
if (err) {
logger.warn(
{ err },
'failed to get "groups-and-enterprise-banner" split test assignment'
)
const defaultAssignment = { variant: 'default' }
cb(null, defaultAssignment)
} else {
cb(null, assignment)
}
}
)
},
survey(cb) {
SurveyHandler.getSurvey(userId, (err, survey) => {
if (err) {
@ -573,7 +553,6 @@ const ProjectController = {
notifications,
user,
userEmailsData,
groupsAndEnterpriseBannerAssignment,
userIsMemberOfGroupSubscription,
} = results
@ -707,13 +686,15 @@ const ProjectController = {
affiliation => affiliation.licence && affiliation.licence !== 'free'
)
// groupsAndEnterpriseBannerAssignment.variant = 'default' | 'empower' | 'save' | 'did-you-know'
const showGroupsAndEnterpriseBanner =
groupsAndEnterpriseBannerAssignment.variant !== 'default' &&
Features.hasFeature('saas') &&
!userIsMemberOfGroupSubscription &&
!hasPaidAffiliation
const groupsAndEnterpriseBannerVariant =
showGroupsAndEnterpriseBanner &&
_.sample(['did-you-know', 'on-premise', 'people', 'FOMO'])
ProjectController._injectProjectUsers(projects, (error, projects) => {
if (error != null) {
return next(error)
@ -739,8 +720,7 @@ const ProjectController = {
usersBestSubscription: results.usersBestSubscription,
survey: results.survey,
showGroupsAndEnterpriseBanner,
groupsAndEnterpriseBannerVariant:
groupsAndEnterpriseBannerAssignment.variant,
groupsAndEnterpriseBannerVariant,
}
const paidUser =

View file

@ -17,7 +17,6 @@ const NotificationsHandler = require('../Notifications/NotificationsHandler')
const Modules = require('../../infrastructure/Modules')
const { OError, V1ConnectionError } = require('../Errors/Errors')
const { User } = require('../../models/User')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const UserPrimaryEmailCheckHandler = require('../User/UserPrimaryEmailCheckHandler')
const UserController = require('../User/UserController')
const LimitationsManager = require('../Subscription/LimitationsManager')
@ -270,25 +269,7 @@ async function projectListReactPage(req, res, next) {
status: prefetchedProjectsBlob ? 'success' : 'too-slow',
})
let showGroupsAndEnterpriseBanner = false
let groupsAndEnterpriseBannerAssignment
try {
groupsAndEnterpriseBannerAssignment =
await SplitTestHandler.promises.getAssignment(
req,
res,
'groups-and-enterprise-banner'
)
} catch (error) {
logger.error(
{ err: error },
'failed to get "groups-and-enterprise-banner" split test assignment'
)
}
let userIsMemberOfGroupSubscription = false
try {
const userIsMemberOfGroupSubscriptionPromise =
await LimitationsManager.promises.userIsMemberOfGroupSubscription(user)
@ -306,12 +287,15 @@ async function projectListReactPage(req, res, next) {
affiliation => affiliation.licence && affiliation.licence !== 'free'
)
showGroupsAndEnterpriseBanner =
(groupsAndEnterpriseBannerAssignment?.variant ?? 'default') !== 'default' &&
const showGroupsAndEnterpriseBanner =
Features.hasFeature('saas') &&
!userIsMemberOfGroupSubscription &&
!hasPaidAffiliation
const groupsAndEnterpriseBannerVariant =
showGroupsAndEnterpriseBanner &&
_.sample(['did-you-know', 'on-premise', 'people', 'FOMO'])
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
@ -327,8 +311,7 @@ async function projectListReactPage(req, res, next) {
portalTemplates,
prefetchedProjectsBlob,
showGroupsAndEnterpriseBanner,
groupsAndEnterpriseBannerVariant:
groupsAndEnterpriseBannerAssignment?.variant ?? 'default',
groupsAndEnterpriseBannerVariant,
projectDashboardReact: true, // used in navbar
})
}

View file

@ -267,12 +267,13 @@ include ../../_mixins/reconfirm_affiliation
)
.alert.alert-info
.notification-body(ng-switch="groupsAndEnterpriseBannerVariant")
span(ng-switch-when="empower") #{translate("empower_your_organization_to_work_in_overleaf")}
span(ng-switch-when="save") !{translate("save_money_groups_companies_research_organizations_can_save_money", {}, ['strong'])}
span(ng-switch-when="did-you-know") #{translate("did_you_know_that_overleaf_offers")}
span(ng-switch-when="on-premise") Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.
span(ng-switch-when="people") Other people at your company may already be using Overleaf. Save money with Overleaf group and company-wide subscriptions. Request more information.
span(ng-switch-when="FOMO") Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.
.notification-action
a.pull-right.btn.btn-sm.btn-info(
href="/for/contact-sales"
href="/for/contact-sales{{urlVariantSuffix}}"
target="_blank"
event-tracking="groups-and-enterprise-banner-click"
event-tracking-mb="true"

View file

@ -227,7 +227,6 @@
"email_or_password_wrong_try_again": "",
"emails_and_affiliations_explanation": "",
"emails_and_affiliations_title": "",
"empower_your_organization_to_work_in_overleaf": "",
"end_of_document": "",
"error": "",
"error_performing_request": "",
@ -670,7 +669,6 @@
"revoke_invite": "",
"rich_text_is_only_available_for_tex_files": "",
"role": "",
"save_money_groups_companies_research_organizations_can_save_money": "",
"save_or_cancel-cancel": "",
"save_or_cancel-or": "",
"save_or_cancel-save": "",

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react'
import { useCallback, useEffect } from 'react'
import Notification from './notification'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import getMeta from '../../../../utils/meta'
@ -6,74 +6,58 @@ import customLocalStorage from '../../../../infrastructure/local-storage'
import { useProjectListContext } from '../../context/project-list-context'
import { Trans, useTranslation } from 'react-i18next'
type GroupsAndEnterpriseBannerVariant =
| 'default'
| 'empower'
| 'save'
| 'did-you-know'
const variants = ['did-you-know', 'on-premise', 'people', 'FOMO'] as const
type GroupsAndEnterpriseBannerVariant = typeof variants[number]
let viewEventSent = false
export default function GroupsAndEnterpriseBanner() {
const { t } = useTranslation()
const { totalProjectsCount } = useProjectListContext()
const showGroupsAndEnterpriseBanner = getMeta(
const showGroupsAndEnterpriseBanner: boolean = getMeta(
'ol-showGroupsAndEnterpriseBanner'
) as boolean
const groupsAndEnterpriseBannerVariant = getMeta(
'ol-groupsAndEnterpriseBannerVariant'
) as GroupsAndEnterpriseBannerVariant
const eventTrackingSegmentation = useMemo(
() => ({
location: 'dashboard-banner-react',
variant: groupsAndEnterpriseBannerVariant,
page: '/project',
}),
[groupsAndEnterpriseBannerVariant]
)
const groupsAndEnterpriseBannerVariant: GroupsAndEnterpriseBannerVariant =
getMeta('ol-groupsAndEnterpriseBannerVariant')
const hasDismissedGroupsAndEnterpriseBanner = customLocalStorage.getItem(
'has_dismissed_groups_and_enterprise_banner'
)
const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner()
const contactSalesUrl = `/for/contact-sales-${
variants.indexOf(groupsAndEnterpriseBannerVariant) + 1
}`
const shouldRenderBanner =
showGroupsAndEnterpriseBanner &&
totalProjectsCount !== 0 &&
!hasDismissedGroupsAndEnterpriseBanner &&
isVariantValid(groupsAndEnterpriseBannerVariant)
const handleClose = useCallback(() => {
customLocalStorage.setItem(
'has_dismissed_groups_and_enterprise_banner',
true
new Date()
)
}, [])
const handleClickContact = useCallback(() => {
eventTracking.sendMB(
'groups-and-enterprise-banner-click',
eventTrackingSegmentation
)
}, [eventTrackingSegmentation])
eventTracking.sendMB('groups-and-enterprise-banner-click', {
location: 'dashboard-banner-react',
variant: groupsAndEnterpriseBannerVariant,
})
}, [groupsAndEnterpriseBannerVariant])
useEffect(() => {
eventTracking.sendMB(
'groups-and-enterprise-banner-prompt',
eventTrackingSegmentation
)
}, [eventTrackingSegmentation])
if (!viewEventSent && shouldRenderBanner) {
eventTracking.sendMB('groups-and-enterprise-banner-prompt', {
location: 'dashboard-banner-react',
variant: groupsAndEnterpriseBannerVariant,
})
viewEventSent = true
}
}, [shouldRenderBanner, groupsAndEnterpriseBannerVariant])
if (
totalProjectsCount === 0 ||
hasDismissedGroupsAndEnterpriseBanner ||
!showGroupsAndEnterpriseBanner
) {
return null
}
// `getText` function has no default switch case since the whole notification
// should not be rendered if the `groupsAndEnterpriseBannerVariant` is not valid
if (!isVariantValid(groupsAndEnterpriseBannerVariant)) {
return null
}
// this shouldn't ever happens since the value of `showGroupsAndEnterpriseBanner` should be false
// if `groupsAndEnterpriseBannerVariant` is 'default'
// but just adding this check as an extra measure
if (groupsAndEnterpriseBannerVariant === 'default') {
if (!shouldRenderBanner) {
return null
}
@ -85,8 +69,9 @@ export default function GroupsAndEnterpriseBanner() {
<Notification.Action>
<a
className="pull-right btn btn-info btn-sm"
href="/for/contact-sales"
href={contactSalesUrl}
target="_blank"
rel="noreferrer"
onClick={handleClickContact}
>
{t('contact_sales')}
@ -97,26 +82,35 @@ export default function GroupsAndEnterpriseBanner() {
}
function isVariantValid(variant: GroupsAndEnterpriseBannerVariant) {
return (
variant === 'empower' || variant === 'save' || variant === 'did-you-know'
)
return variants.includes(variant)
}
function getText(variant: GroupsAndEnterpriseBannerVariant) {
switch (variant) {
case 'empower':
return <Trans i18nKey="empower_your_organization_to_work_in_overleaf" />
case 'save':
return (
<Trans
i18nKey="save_money_groups_companies_research_organizations_can_save_money"
components={
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
[<strong />]
}
/>
)
case 'did-you-know':
return <Trans i18nKey="did_you_know_that_overleaf_offers" />
case 'on-premise':
return 'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
case 'people':
return 'Other people at your company may already be using Overleaf. Save money with Overleaf group and company-wide subscriptions. Request more information.'
case 'FOMO':
return 'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
}
}
function hasRecentlyDismissedBanner() {
const dismissed = customLocalStorage.getItem(
'has_dismissed_groups_and_enterprise_banner'
)
// previous banner set this to 'true', which shouldn't hide the new banner
if (!dismissed || dismissed === 'true') {
return false
}
const dismissedDate = new Date(dismissed)
const recentlyDismissedCutoff = new Date()
recentlyDismissedCutoff.setDate(recentlyDismissedCutoff.getDate() - 30) // 30 days
// once the dismissedDate passes the cut off mark, banner will be shown again
return dismissedDate > recentlyDismissedCutoff
}

View file

@ -40,10 +40,15 @@ App.controller(
'ol-groupsAndEnterpriseBannerVariant'
)
$scope.isVariantValid =
$scope.groupsAndEnterpriseBannerVariant === 'save' ||
$scope.groupsAndEnterpriseBannerVariant === 'empower' ||
$scope.groupsAndEnterpriseBannerVariant === 'did-you-know'
const valid = ['did-you-know', 'on-premise', 'people', 'FOMO']
$scope.isVariantValid = valid.includes(
$scope.groupsAndEnterpriseBannerVariant
)
$scope.urlVariantSuffix = $scope.isVariantValid
? `-${valid.indexOf($scope.groupsAndEnterpriseBannerVariant) + 1}`
: ''
}
)

View file

@ -420,7 +420,6 @@
"emails": "Emails",
"emails_and_affiliations_explanation": "Add additional email addresses to your account to access any upgrades your university or institution has, to make it easier for collaborators to find you, and to make sure you can recover your account.",
"emails_and_affiliations_title": "Emails and Affiliations",
"empower_your_organization_to_work_in_overleaf": "Empower your organization to work in __appName__! Get a group or organizational plan.",
"empty_zip_file": "Zip doesnt contain any file",
"en": "English",
"end_of_document": "End of document",
@ -1273,7 +1272,6 @@
"saml": "SAML",
"saml_create_admin_instructions": "Choose an email address for the first __appName__ admin account. This should correspond to an account in the SAML system. You will then be asked to log in with this account.",
"save_20_percent_by_paying_annually": "Save 20% by paying annually",
"save_money_groups_companies_research_organizations_can_save_money": "<0>Save Money</0>! Groups, Companies and Research Organizations can save money with our Group and Enterprise plans — request information or a quote.",
"save_or_cancel-cancel": "Cancel",
"save_or_cancel-or": "or",
"save_or_cancel-save": "Save",

View file

@ -658,6 +658,11 @@ describe('<UserNotifications />', function () {
totalSize: projects.length,
},
})
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'did-you-know'
)
})
afterEach(function () {
@ -665,7 +670,7 @@ describe('<UserNotifications />', function () {
window.metaAttributesCache = window.metaAttributesCache || new Map()
})
it('does not show the banner for users that are in group or are affiliated or assigned in the `default` variant', async function () {
it('does not show the banner for users that are in group or are affiliated', async function () {
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', false)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
@ -674,13 +679,45 @@ describe('<UserNotifications />', function () {
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
})
it('does not show the banner for users that have already dismissed it', async function () {
it('shows the banner for users that have dismissed the previous banners', async function () {
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
localStorage.setItem('has_dismissed_groups_and_enterprise_banner', true)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
.null
})
it('shows the banner for users that have dismissed the banner more than 30 days ago', async function () {
const dismissed = new Date()
dismissed.setDate(dismissed.getDate() - 31) // 31 days
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
localStorage.setItem(
'has_dismissed_groups_and_enterprise_banner',
dismissed
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.not.be
.null
})
it('does not show the banner for users that have dismissed the banner within the last 30 days', async function () {
const dismissed = new Date()
dismissed.setDate(dismissed.getDate() - 29) // 29 days
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
localStorage.setItem(
'has_dismissed_groups_and_enterprise_banner',
dismissed
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
expect(screen.queryByRole('link', { name: 'Contact Sales' })).to.be.null
})
@ -711,24 +748,7 @@ describe('<UserNotifications />', function () {
localStorage.clear()
})
it('will show the correct text for the `save` split test variant', async function () {
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'save'
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
screen.getByText(
/Groups, Companies and Research Organizations can save money with our Group and Enterprise plans — request information or a quote./
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
})
it('will show the correct text for the `did-you-know` split test variant', async function () {
it('will show the correct text for the `did-you-know` variant', async function () {
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'did-you-know'
@ -742,24 +762,58 @@ describe('<UserNotifications />', function () {
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-1`)
})
it('will show the correct text for the `empower` split test variant', async function () {
it('will show the correct text for the `on-premise` variant', async function () {
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'empower'
'on-premise'
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
screen.getByText(
'Empower your organization to work in Overleaf! Get a group or organizational plan.'
'Overleaf On-Premises: Does your company want to keep its data within its firewall? Overleaf offers Server Pro, an on-premises solution for companies. Get in touch to learn more.'
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales`)
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-2`)
})
it('will show the correct text for the `people` variant', async function () {
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'people'
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
screen.getByText(
'Other people at your company may already be using Overleaf. Save money with Overleaf group and company-wide subscriptions. Request more information.'
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-3`)
})
it('will show the correct text for the `FOMO` variant', async function () {
window.metaAttributesCache.set(
'ol-groupsAndEnterpriseBannerVariant',
'FOMO'
)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.flush(true)
screen.getByText(
'Why do Fortune 500 companies and top research institutions trust Overleaf to streamline their collaboration? Get in touch to learn more.'
)
const link = screen.getByRole('link', { name: 'Contact Sales' })
expect(link.getAttribute('href')).to.equal(`/for/contact-sales-4`)
})
})
})