Implement Back to School 2023 banners and modal (#14363)

* Implement Back to School 2023 banners and modal

* Only show WritefullPromoBanner if not showing BackToSchoolModal

GitOrigin-RevId: 3bd2ea48fa6d66f112cc26662a61be05cf7daafd
This commit is contained in:
Thomas 2023-08-23 16:16:37 +02:00 committed by Copybot
parent 3f7b84cb40
commit 43c92642c6
15 changed files with 311 additions and 3 deletions

View file

@ -452,6 +452,7 @@ async function projectListPage(req, res, next) {
showInrGeoBanner,
inrGeoBannerVariant,
inrGeoBannerSplitTestName,
showBackToSchoolModal: Boolean(usersBestSubscription?.type === 'free'),
projectDashboardReact: true, // used in navbar
welcomePageRedesignVariant: welcomePageRedesignAssignment.variant,
groupSubscriptionsPendingEnrollment:

View file

@ -130,6 +130,16 @@ async function plansPage(req, res) {
plansPageViewSegmentation[inrGeoBannerSplitTestName] = inrGeoBannerVariant
}
let showBackToSchoolBanner
const userId = SessionManager.getLoggedInUserId(req.session)
if (userId) {
const usersBestSubscription =
await SubscriptionViewModelBuilder.promises.getBestSubscription({
_id: userId,
})
showBackToSchoolBanner = usersBestSubscription?.type === 'free'
}
AnalyticsManager.recordEventForSession(
req.session,
'plans-page-view',
@ -152,6 +162,7 @@ async function plansPage(req, res) {
initialLocalizedGroupPrice:
SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
showInrGeoBanner,
showBackToSchoolBanner,
annualTrialsAssignment: annualTrialsAssignment?.variant,
})
}

View file

@ -33,6 +33,7 @@ block append meta
meta(name="ol-inrGeoBannerSplitTestName" data-type="string" content=inrGeoBannerSplitTestName)
meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
meta(name="ol-showBackToSchoolModal" data-type="boolean" content=showBackToSchoolModal)
meta(name="ol-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant)
meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)

View file

@ -14,6 +14,31 @@ block append meta
block content
main.content.content-alt#main-content
.container
.user-notifications
ul.list-unstyled(ng-cloak)
li.notification-entry
div.alert.alert-back-to-school(
event-tracking-mb="true"
event-tracking="promo-prompt"
event-tracking-trigger="load"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner"}'
)
.notification-body
| 🎉  
p
strong #{translate("back_to_school_banner_bargain_with_x_percent_off_in_school_or_not", {x: '15'})}
br
| #{translate("back_to_school_banner_hurry_offer_ends_sep_30")}
.notification-action
a.btn.btn-sm.btn-default-outline(
href="/about/back-to-school-promo-2023"
event-tracking-mb="true"
event-tracking="promo-click"
event-tracking-trigger="click"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner", "type": "click"}'
) #{translate('claim_discounts')}
.content-page
.plans
.container

View file

@ -14,6 +14,31 @@ block append meta
block content
main.content.content-alt#main-content
.container
.user-notifications
ul.list-unstyled(ng-cloak)
li.notification-entry
div.alert.alert-back-to-school(
event-tracking-mb="true"
event-tracking="promo-prompt"
event-tracking-trigger="load"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner"}'
)
.notification-body
| 🎉  
p
strong #{translate("back_to_school_banner_bargain_with_x_percent_off_in_school_or_not", {x: '15'})}
br
| #{translate("back_to_school_banner_hurry_offer_ends_sep_30")}
.notification-action
a.btn.btn-sm.btn-default-outline(
href="/about/back-to-school-promo-2023"
event-tracking-mb="true"
event-tracking="promo-click"
event-tracking-trigger="click"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner", "type": "click"}'
) #{translate('claim_discounts')}
.content-page
.plans
.container

View file

@ -14,6 +14,31 @@ block append meta
block content
main.content.content-alt#main-content
.container
.user-notifications
ul.list-unstyled(ng-cloak)
li.notification-entry
div.alert.alert-back-to-school(
event-tracking-mb="true"
event-tracking="promo-prompt"
event-tracking-trigger="load"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner"}'
)
.notification-body
| 🎉  
p
strong #{translate("back_to_school_banner_bargain_with_x_percent_off_in_school_or_not", {x: '15'})}
br
| #{translate("back_to_school_banner_hurry_offer_ends_sep_30")}
.notification-action
a.btn.btn-sm.btn-default-outline(
href="/about/back-to-school-promo-2023"
event-tracking-mb="true"
event-tracking="promo-click"
event-tracking-trigger="click"
event-segmentation='{"location": "interstitial-page-banner", "name": "bts2023", "content": "banner", "type": "click"}'
) #{translate('claim_discounts')}
.content-page
.plans
.container

View file

@ -12,12 +12,40 @@ block append meta
block content
main.content.content-alt#main-content
if showBackToSchoolBanner
.container(
)
.user-notifications
ul.list-unstyled(ng-cloak)
li.notification-entry
div.alert.alert-back-to-school(
event-tracking-mb="true"
event-tracking="promo-prompt"
event-tracking-trigger="load"
event-segmentation='{"location": "plans-page-banner", "name": "bts2023", "content": "banner"}'
)
.notification-body
| 🎉  
p
strong #{translate("back_to_school_banner_bargain_with_x_percent_off_in_school_or_not", {x: '15'})}
br
| #{translate("back_to_school_banner_hurry_offer_ends_sep_30")}
.notification-action
a.btn.btn-sm.btn-default-outline(
href="/about/back-to-school-promo-2023"
event-tracking-mb="true"
event-tracking="promo-click"
event-tracking-trigger="click"
event-segmentation='{"location": "plans-page-banner", "name": "bts2023", "content": "banner", "type": "click"}'
) #{translate('claim_discounts')}
.content-page
.plans
.container(ng-cloak)
if showInrGeoBanner
div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
.row
.col-md-12
.page-header.centered.plans-header.text-centered.top-page-header

View file

@ -86,6 +86,9 @@
"autocomplete": "",
"autocomplete_references": "",
"back": "",
"back_to_school_modal_bargain_with_x_percent_off": "",
"back_to_school_modal_offer_ends_sep_30": "",
"back_to_school_modal_offers_from_writefull_and_papers": "",
"back_to_subscription": "",
"back_to_your_projects": "",
"beta_program_already_participating": "",
@ -139,6 +142,7 @@
"checking_project_github_status": "",
"choose_a_custom_color": "",
"choose_from_group_members": "",
"claim_discounts": "",
"clear_cached_files": "",
"clear_search": "",
"click_here_to_view_sl_in_lng": "",

View file

@ -0,0 +1,109 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import usePersistedState from '../../../../../shared/hooks/use-persisted-state'
import * as eventTracking from '../../../../../infrastructure/event-tracking'
import { Modal, Button } from 'react-bootstrap'
import AccessibleModal from '../../../../../shared/components/accessible-modal'
export default function BackToSchoolModal() {
const { t } = useTranslation()
const [dismissedUntil, setDismissedUntil] = usePersistedState<
Date | undefined
>(`has_dismissed_back_to_school_modal_until`)
const viewEventSent = useRef<boolean>(false)
const [showModal, setShowModal] = useState(true)
useEffect(() => {
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
return
}
if (!viewEventSent.current) {
eventTracking.sendMB('promo-prompt', {
name: 'bts2023',
location: 'dashboard-modal',
content: 'modal',
})
viewEventSent.current = true
}
}, [dismissedUntil])
const handleClick = useCallback(() => {
eventTracking.sendMB('promo-click', {
name: 'bts2023',
location: 'dashboard-modal',
content: 'modal',
type: 'click',
})
setShowModal(false)
window.open('/about/back-to-school-promo-2023')
}, [])
const bannerDismissed = useCallback(() => {
eventTracking.sendMB('promo-dismiss', {
name: 'bts2023',
location: 'dashboard-modal',
content: 'modal',
})
const until = new Date()
until.setDate(until.getDate() + 14) // 14 days
setDismissedUntil(until)
}, [setDismissedUntil])
const handleHide = useCallback(() => {
setShowModal(false)
bannerDismissed()
}, [bannerDismissed])
const handleMaybeLater = useCallback(() => {
eventTracking.sendMB('promo-click', {
name: 'bts2023',
location: 'dashboard-modal',
content: 'modal',
type: 'pause',
})
setShowModal(false)
const until = new Date()
until.setDate(until.getDate() + 1) // 1 day
setDismissedUntil(until)
}, [setDismissedUntil])
if (dismissedUntil && new Date(dismissedUntil) > new Date()) {
return null
}
return (
<AccessibleModal show={showModal} onHide={handleHide}>
<Modal.Header closeButton>
<Modal.Title>
{t('back_to_school_modal_bargain_with_x_percent_off', { x: '15' })}
</Modal.Title>
</Modal.Header>
<Modal.Body className="modal-body-share">
<p>
<img
alt=""
src="/img/subscriptions/back-to-school-modal.png"
style={{
width: '100%',
}}
/>
</p>
<p>{t('back_to_school_modal_offers_from_writefull_and_papers')}</p>
<p>
<strong>{t('back_to_school_modal_offer_ends_sep_30')}</strong>
</p>
</Modal.Body>
<Modal.Footer>
<Button bsStyle="default" onClick={handleMaybeLater}>
{t('maybe_later')}
</Button>
<Button type="button" bsStyle="primary" onClick={handleClick}>
{t('claim_discounts')}
</Button>
</Modal.Footer>
</AccessibleModal>
)
}

View file

@ -9,6 +9,7 @@ import INRBanner from './ads/inr-banner'
import LATAMBanner from './ads/latam-banner'
import getMeta from '../../../../utils/meta'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import BackToSchoolModal from './ads/back-to-school-modal'
type Subscription = {
groupId: string
@ -28,8 +29,12 @@ function UserNotifications() {
'ol-groupSubscriptionsPendingEnrollment',
[]
)
const showBackToSchoolModal = getMeta('ol-showBackToSchoolModal', false)
const showInrGeoBanner = getMeta('ol-showInrGeoBanner', false)
const inrGeoBannerVariant = getMeta('ol-inrGeoBannerVariant', 'default')
const inrGeoBannerVariant = showBackToSchoolModal
? 'default' // This test should be disabled to prevent double modals, but sanity check to be safe
: getMeta('ol-inrGeoBannerVariant', 'default')
const inrGeoBannerSplitTestName = getMeta(
'ol-inrGeoBannerSplitTestName',
'unassigned'
@ -61,7 +66,11 @@ function UserNotifications() {
) : (
<GroupsAndEnterpriseBanner />
)}
{showBackToSchoolModal ? (
<BackToSchoolModal />
) : (
<WritefullPromoBanner />
)}
</ul>
</div>
)

View file

@ -53,7 +53,7 @@
position: relative;
text-align: center;
overflow: hidden;
padding-top: @header-height;
padding-top: 0px; // temporarily change from @header-height to 0px for back to school banner changes
h1,
p,
label {
@ -331,6 +331,41 @@
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3);
}
.back-to-school {
.back-to-school-banner {
background: @blue-10;
padding: 0px 16px 0px 12px;
border-radius: 4px;
border: 1px solid @blue-20;
color: @neutral-90;
.banner-row {
height: 56px;
display: flex;
justify-content: center;
align-content: center;
flex-wrap: wrap;
}
.claim-my-discount {
font-size: 14px;
font-weight: 700;
}
p {
margin: 0;
}
span {
font-size: 14px;
font-weight: 700;
margin-left: 10px;
}
a {
text-decoration: underline;
}
}
.navbar-default {
position: relative;
}
}
@media only screen and (max-width: @screen-sm-max) {
.doc-history-example {
margin-bottom: @margin-md;

View file

@ -84,6 +84,32 @@
}
}
/**
Back to school promo
*/
.alert-back-to-school {
.alert-variant(@green-10; #BBDBB8; @neutral-90);
border-radius: 4px;
border: 1px solid #bbdbb8;
box-shadow: none !important;
.notification-body {
display: flex;
}
.btn.btn-default-outline {
border: 1px solid @neutral-60;
background: white;
font-size: 16px;
color: @neutral-90;
}
.btn.btn-default-outline:active {
background: @neutral-40;
}
}
/**
Plans Test
*/

View file

@ -138,6 +138,12 @@
"back_to_account_settings": "Back to account settings",
"back_to_editor": "Back to the editor",
"back_to_log_in": "Back to log in",
"back_to_school_banner_bargain_with_x_percent_off_in_school_or_not": "Grab a back-to-school bargain with __x__% off all individual Overleaf subscriptions (whether youre in school or not!)",
"back_to_school_banner_hurry_offer_ends_sep_30": "Hurry! Offer ends September 30.",
"back_to_school_homepage_bargain_with_x_percent_off": "Grab a back-to-school bargain with 15% off all individual Overleaf subscriptions",
"back_to_school_modal_bargain_with_x_percent_off": "Everyone can grab a back-to-school bargain this fall with __x__% off an Overleaf subscription",
"back_to_school_modal_offer_ends_sep_30": "Offer ends September 30.",
"back_to_school_modal_offers_from_writefull_and_papers": "And with offers from Writefull and Papers too, youll have the tools you need to write smarter this year.",
"back_to_subscription": "Back to Subscription",
"back_to_your_projects": "Back to your projects",
"become_an_advisor": "Become an __appName__ advisor",
@ -229,6 +235,8 @@
"choose_from_group_members": "Choose from Group Members",
"choose_your_plan": "Choose your plan",
"city": "City",
"claim_discounts": "Claim discounts",
"claim_my_discount": "Claim my discount",
"clear_cached_files": "Clear cached files",
"clear_search": "clear search",
"clear_sessions": "Clear Sessions",

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -78,6 +78,7 @@ describe('SubscriptionController', function () {
buildPlansList: sinon.stub(),
promises: {
buildUsersSubscriptionViewModel: sinon.stub().resolves({}),
getBestSubscription: sinon.stub().resolves({}),
},
buildPlansListForSubscriptionDash: sinon
.stub()