Browse Source

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
Thomas 3 years ago
parent
commit
43c92642c6

+ 1 - 0
services/web/app/src/Features/Project/ProjectListController.js

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

+ 11 - 0
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -130,6 +130,16 @@ async function plansPage(req, res) {
     plansPageViewSegmentation[inrGeoBannerSplitTestName] = inrGeoBannerVariant
     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(
   AnalyticsManager.recordEventForSession(
     req.session,
     req.session,
     'plans-page-view',
     'plans-page-view',
@@ -152,6 +162,7 @@ async function plansPage(req, res) {
     initialLocalizedGroupPrice:
     initialLocalizedGroupPrice:
       SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
       SubscriptionHelper.generateInitialLocalizedGroupPrice(currency),
     showInrGeoBanner,
     showInrGeoBanner,
+    showBackToSchoolBanner,
     annualTrialsAssignment: annualTrialsAssignment?.variant,
     annualTrialsAssignment: annualTrialsAssignment?.variant,
   })
   })
 }
 }

+ 1 - 0
services/web/app/views/project/list-react.pug

@@ -33,6 +33,7 @@ block append meta
 	meta(name="ol-inrGeoBannerSplitTestName" data-type="string" content=inrGeoBannerSplitTestName)
 	meta(name="ol-inrGeoBannerSplitTestName" data-type="string" content=inrGeoBannerSplitTestName)
 	meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
 	meta(name="ol-showLATAMBanner" data-type="boolean" content=showLATAMBanner)
 	meta(name="ol-recommendedCurrency" data-type="string" content=recommendedCurrency)
 	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-welcomePageRedesignVariant" data-type="string" content=welcomePageRedesignVariant)
 	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
 	meta(name="ol-groupSubscriptionsPendingEnrollment" data-type="json" content=groupSubscriptionsPendingEnrollment)
 
 

+ 25 - 0
services/web/app/views/subscriptions/interstitial-payment.pug

@@ -14,6 +14,31 @@ block append meta
 
 
 block content
 block content
 	main.content.content-alt#main-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
 		.content-page
 			.plans
 			.plans
 				.container
 				.container

+ 25 - 0
services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug

@@ -14,6 +14,31 @@ block append meta
 
 
 block content
 block content
 	main.content.content-alt#main-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
 		.content-page
 			.plans
 			.plans
 				.container
 				.container

+ 25 - 0
services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug

@@ -14,6 +14,31 @@ block append meta
 
 
 block content
 block content
 	main.content.content-alt#main-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
 		.content-page
 			.plans
 			.plans
 				.container
 				.container

+ 28 - 0
services/web/app/views/subscriptions/plans.pug

@@ -12,12 +12,40 @@ block append meta
 
 
 block content
 block content
 	main.content.content-alt#main-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
 		.content-page
 			.plans
 			.plans
 				.container(ng-cloak)
 				.container(ng-cloak)
 					if showInrGeoBanner
 					if showInrGeoBanner
 						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 						div.alert.alert-success.text-centered !{translate("inr_discount_offer_plans_page_banner", {flag: '🇮🇳'})}
 
 
+
 					.row
 					.row
 						.col-md-12
 						.col-md-12
 							.page-header.centered.plans-header.text-centered.top-page-header
 							.page-header.centered.plans-header.text-centered.top-page-header

+ 4 - 0
services/web/frontend/extracted-translations.json

@@ -86,6 +86,9 @@
   "autocomplete": "",
   "autocomplete": "",
   "autocomplete_references": "",
   "autocomplete_references": "",
   "back": "",
   "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_subscription": "",
   "back_to_your_projects": "",
   "back_to_your_projects": "",
   "beta_program_already_participating": "",
   "beta_program_already_participating": "",
@@ -139,6 +142,7 @@
   "checking_project_github_status": "",
   "checking_project_github_status": "",
   "choose_a_custom_color": "",
   "choose_a_custom_color": "",
   "choose_from_group_members": "",
   "choose_from_group_members": "",
+  "claim_discounts": "",
   "clear_cached_files": "",
   "clear_cached_files": "",
   "clear_search": "",
   "clear_search": "",
   "click_here_to_view_sl_in_lng": "",
   "click_here_to_view_sl_in_lng": "",

+ 109 - 0
services/web/frontend/js/features/project-list/components/notifications/ads/back-to-school-modal.tsx

@@ -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>
+  )
+}

+ 11 - 2
services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx

@@ -9,6 +9,7 @@ import INRBanner from './ads/inr-banner'
 import LATAMBanner from './ads/latam-banner'
 import LATAMBanner from './ads/latam-banner'
 import getMeta from '../../../../utils/meta'
 import getMeta from '../../../../utils/meta'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+import BackToSchoolModal from './ads/back-to-school-modal'
 
 
 type Subscription = {
 type Subscription = {
   groupId: string
   groupId: string
@@ -28,8 +29,12 @@ function UserNotifications() {
     'ol-groupSubscriptionsPendingEnrollment',
     'ol-groupSubscriptionsPendingEnrollment',
     []
     []
   )
   )
+  const showBackToSchoolModal = getMeta('ol-showBackToSchoolModal', false)
+
   const showInrGeoBanner = getMeta('ol-showInrGeoBanner', 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(
   const inrGeoBannerSplitTestName = getMeta(
     'ol-inrGeoBannerSplitTestName',
     'ol-inrGeoBannerSplitTestName',
     'unassigned'
     'unassigned'
@@ -61,7 +66,11 @@ function UserNotifications() {
         ) : (
         ) : (
           <GroupsAndEnterpriseBanner />
           <GroupsAndEnterpriseBanner />
         )}
         )}
-        <WritefullPromoBanner />
+        {showBackToSchoolModal ? (
+          <BackToSchoolModal />
+        ) : (
+          <WritefullPromoBanner />
+        )}
       </ul>
       </ul>
     </div>
     </div>
   )
   )

+ 36 - 1
services/web/frontend/stylesheets/app/homepage.less

@@ -53,7 +53,7 @@
   position: relative;
   position: relative;
   text-align: center;
   text-align: center;
   overflow: hidden;
   overflow: hidden;
-  padding-top: @header-height;
+  padding-top: 0px; // temporarily change from @header-height to 0px for back to school banner changes
   h1,
   h1,
   p,
   p,
   label {
   label {
@@ -331,6 +331,41 @@
   box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3);
   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) {
 @media only screen and (max-width: @screen-sm-max) {
   .doc-history-example {
   .doc-history-example {
     margin-bottom: @margin-md;
     margin-bottom: @margin-md;

+ 26 - 0
services/web/frontend/stylesheets/app/plans.less

@@ -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
 	Plans Test
 */
 */

+ 8 - 0
services/web/locales/en.json

@@ -138,6 +138,12 @@
   "back_to_account_settings": "Back to account settings",
   "back_to_account_settings": "Back to account settings",
   "back_to_editor": "Back to the editor",
   "back_to_editor": "Back to the editor",
   "back_to_log_in": "Back to log in",
   "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 you’re 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, you’ll have the tools you need to write smarter this year.",
   "back_to_subscription": "Back to Subscription",
   "back_to_subscription": "Back to Subscription",
   "back_to_your_projects": "Back to your projects",
   "back_to_your_projects": "Back to your projects",
   "become_an_advisor": "Become an __appName__ advisor",
   "become_an_advisor": "Become an __appName__ advisor",
@@ -229,6 +235,8 @@
   "choose_from_group_members": "Choose from Group Members",
   "choose_from_group_members": "Choose from Group Members",
   "choose_your_plan": "Choose your plan",
   "choose_your_plan": "Choose your plan",
   "city": "City",
   "city": "City",
+  "claim_discounts": "Claim discounts",
+  "claim_my_discount": "Claim my discount",
   "clear_cached_files": "Clear cached files",
   "clear_cached_files": "Clear cached files",
   "clear_search": "clear search",
   "clear_search": "clear search",
   "clear_sessions": "Clear Sessions",
   "clear_sessions": "Clear Sessions",

BIN
services/web/public/img/subscriptions/back-to-school-modal.png


+ 1 - 0
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

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