diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js index c7701ba17d..b1dff38fd1 100644 --- a/services/web/app/src/Features/Project/ProjectListController.js +++ b/services/web/app/src/Features/Project/ProjectListController.js @@ -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: diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index b23af6139f..d2b68c8bda 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -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, }) } diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index c82ed6c124..db50495447 100644 --- a/services/web/app/views/project/list-react.pug +++ b/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-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) diff --git a/services/web/app/views/subscriptions/interstitial-payment.pug b/services/web/app/views/subscriptions/interstitial-payment.pug index 172308a04a..bb266c37fb 100644 --- a/services/web/app/views/subscriptions/interstitial-payment.pug +++ b/services/web/app/views/subscriptions/interstitial-payment.pug @@ -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 diff --git a/services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug b/services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug index 3c2f712af2..65ed2f48a7 100644 --- a/services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug +++ b/services/web/app/views/subscriptions/interstitial-payment_no_nudge_monthly.pug @@ -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 diff --git a/services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug b/services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug index 47f01ff18e..67a94744df 100644 --- a/services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug +++ b/services/web/app/views/subscriptions/interstitial-payment_nudge_annual.pug @@ -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 diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index baeef21b28..fda1235030 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -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 diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index dacbdd8ef1..69571259d8 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/project-list/components/notifications/ads/back-to-school-modal.tsx b/services/web/frontend/js/features/project-list/components/notifications/ads/back-to-school-modal.tsx new file mode 100644 index 0000000000..1e561db012 --- /dev/null +++ b/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(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 ( + + + + {t('back_to_school_modal_bargain_with_x_percent_off', { x: '15' })} + + + +

+ +

+

{t('back_to_school_modal_offers_from_writefull_and_papers')}

+

+ {t('back_to_school_modal_offer_ends_sep_30')} +

+
+ + + + +
+ ) +} diff --git a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx b/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx index 41b21ed98d..8d826c6197 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx +++ b/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 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() { ) : ( )} - + {showBackToSchoolModal ? ( + + ) : ( + + )} ) diff --git a/services/web/frontend/stylesheets/app/homepage.less b/services/web/frontend/stylesheets/app/homepage.less index 6c9600d1b2..6852be0a0f 100644 --- a/services/web/frontend/stylesheets/app/homepage.less +++ b/services/web/frontend/stylesheets/app/homepage.less @@ -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; diff --git a/services/web/frontend/stylesheets/app/plans.less b/services/web/frontend/stylesheets/app/plans.less index ad74716c9f..7261cdca5f 100644 --- a/services/web/frontend/stylesheets/app/plans.less +++ b/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 */ diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d6424f874f..12f9a9e788 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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 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_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", diff --git a/services/web/public/img/subscriptions/back-to-school-modal.png b/services/web/public/img/subscriptions/back-to-school-modal.png new file mode 100644 index 0000000000..256e3e8cee Binary files /dev/null and b/services/web/public/img/subscriptions/back-to-school-modal.png differ diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js index b5fe555c05..ed8a27f953 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js @@ -78,6 +78,7 @@ describe('SubscriptionController', function () { buildPlansList: sinon.stub(), promises: { buildUsersSubscriptionViewModel: sinon.stub().resolves({}), + getBestSubscription: sinon.stub().resolves({}), }, buildPlansListForSubscriptionDash: sinon .stub()