Merge pull request #29034 from overleaf/as-groups-enterprise-banner

[web] Persist whether user has dismissed Groups and Enterprise banner on server-side

GitOrigin-RevId: a95060cc0fa772652299802ec467be61b09f5a1f
This commit is contained in:
MoxAmber
2025-10-16 12:16:54 +01:00
committed by Copybot
parent bafb7ec56d
commit 8f54468566
5 changed files with 71 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
// @ts-check
import _ from 'lodash'
import moment from 'moment'
import Metrics from '@overleaf/metrics'
import Settings from '@overleaf/settings'
@@ -414,11 +415,15 @@ async function projectListPage(req, res, next) {
const { showUSGovBanner, usGovBannerVariant } = usGovBanner
const isUser30DaysOld = moment.utc().diff(user.signUpDate, 'days') > 30
const showGroupsAndEnterpriseBanner =
Features.hasFeature('saas') &&
!showUSGovBanner &&
!userIsMemberOfGroupSubscription &&
!hasPaidAffiliation
!hasPaidAffiliation &&
!inactiveTutorials.includes('groups-enterprise-banner-repeat') &&
isUser30DaysOld
const groupsAndEnterpriseBannerVariant =
showGroupsAndEnterpriseBanner &&
@@ -564,6 +569,7 @@ async function projectListPage(req, res, next) {
primaryOccupation,
role,
usedLatex,
inactiveTutorials,
})
}

View File

@@ -22,6 +22,8 @@ const VALID_KEYS = [
'ide-redesign-beta-intro',
'ide-redesign-labs-user-beta-promo',
'rolling-compile-image-changed',
'groups-enterprise-banner',
'groups-enterprise-banner-repeat',
]
async function completeTutorial(req, res, next) {

View File

@@ -89,6 +89,7 @@ block append meta
data-type='string'
content=usGovBannerVariant
)
meta(name='ol-inactiveTutorials' data-type='json' content=inactiveTutorials)
block content
#project-list-root

View File

@@ -10,6 +10,9 @@ import {
GroupsAndEnterpriseBannerVariants,
} from '../../../../../../types/project/dashboard/notification'
import OLButton from '@/shared/components/ol/ol-button'
import { postJSON } from '@/infrastructure/fetch-json'
import moment from 'moment'
import { debugConsole } from '@/utils/debugging'
type urlForVariantsType = {
[key in GroupsAndEnterpriseBannerVariant]: string // eslint-disable-line no-unused-vars
@@ -20,6 +23,9 @@ const urlForVariants: urlForVariantsType = {
FOMO: '/for/contact-sales-4',
}
const INITIAL_TUTORIAL_KEY = 'groups-enterprise-banner'
const REPEAT_TUTORIAL_KEY = 'groups-enterprise-banner-repeat'
let viewEventSent = false
export default function GroupsAndEnterpriseBanner() {
@@ -32,23 +38,35 @@ export default function GroupsAndEnterpriseBanner() {
const groupsAndEnterpriseBannerVariant = getMeta(
'ol-groupsAndEnterpriseBannerVariant'
)
const inactiveTutorials = getMeta('ol-inactiveTutorials')
const hasDismissedGroupsAndEnterpriseBanner = hasRecentlyDismissedBanner()
const locallyDismissedBanner = hasLocallyDismissedBanner()
const contactSalesUrl = urlForVariants[groupsAndEnterpriseBannerVariant]
const shouldRenderBanner =
showGroupsAndEnterpriseBanner &&
totalProjectsCount !== 0 &&
!hasDismissedGroupsAndEnterpriseBanner &&
!inactiveTutorials.includes(REPEAT_TUTORIAL_KEY) &&
!locallyDismissedBanner &&
isVariantValid(groupsAndEnterpriseBannerVariant)
const handleClose = useCallback(() => {
customLocalStorage.setItem(
'has_dismissed_groups_and_enterprise_banner',
new Date()
)
}, [])
const handleClose = useCallback(async () => {
if (!inactiveTutorials.includes(INITIAL_TUTORIAL_KEY)) {
await postJSON(`/tutorial/${REPEAT_TUTORIAL_KEY}/postpone`, {
body: {
postponedUntil: moment().add(60, 'days').toISOString(),
},
}).catch(debugConsole.error)
await postJSON(`/tutorial/${INITIAL_TUTORIAL_KEY}/complete`).catch(
debugConsole.error
)
} else {
await postJSON(`/tutorial/${REPEAT_TUTORIAL_KEY}/complete`).catch(
debugConsole.error
)
}
}, [inactiveTutorials])
const handleClickContact = useCallback(() => {
eventTracking.sendMB('groups-and-enterprise-banner-click', {
@@ -67,6 +85,16 @@ export default function GroupsAndEnterpriseBanner() {
}
}, [shouldRenderBanner, groupsAndEnterpriseBannerVariant])
useEffect(() => {
// Persist local dismissal status from previous banner versions to the backend
if (
locallyDismissedBanner &&
!inactiveTutorials.includes(REPEAT_TUTORIAL_KEY)
) {
handleClose()
}
}, [handleClose, locallyDismissedBanner, inactiveTutorials])
if (!shouldRenderBanner) {
return null
}
@@ -120,7 +148,7 @@ function BannerContent({
}
}
function hasRecentlyDismissedBanner() {
function hasLocallyDismissedBanner() {
const dismissed = customLocalStorage.getItem(
'has_dismissed_groups_and_enterprise_banner'
)

View File

@@ -949,6 +949,8 @@ describe('<UserNotifications />', function () {
'ol-groupsAndEnterpriseBannerVariant',
'on-premise'
)
window.metaAttributesCache.set('ol-inactiveTutorials', '[]')
})
afterEach(function () {
@@ -974,9 +976,9 @@ describe('<UserNotifications />', function () {
await screen.findByRole('link', { name: 'Contact sales' })
})
it('shows the banner for users that have dismissed the banner more than 30 days ago', async function () {
it('does not show the banner for users that have dismissed the banner within the last 30 days and before server-side state', async function () {
const dismissed = new Date()
dismissed.setDate(dismissed.getDate() - 31) // 31 days
dismissed.setDate(dismissed.getDate() - 29) // 29 days
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
localStorage.setItem(
'has_dismissed_groups_and_enterprise_banner',
@@ -986,17 +988,28 @@ describe('<UserNotifications />', function () {
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
await screen.findByRole('link', { name: 'Contact sales' })
expect(screen.queryByRole('link', { name: 'Contact sales' })).to.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
it('shows the banner for users who have not dismissed the repeat appearance', async function () {
window.metaAttributesCache.set(
'ol-inactiveTutorials',
'["groups-enterprise-banner"]'
)
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)
expect(screen.queryByRole('link', { name: 'Contact sales' })).to.be.null
})
it('does not show the banner for users with both inactive tutorials', async function () {
window.metaAttributesCache.set(
'ol-inactiveTutorials',
'["groups-enterprise-banner", "groups-enterprise-banner-repeat"]'
)
window.metaAttributesCache.set('ol-showGroupsAndEnterpriseBanner', true)
renderWithinProjectListProvider(GroupsAndEnterpriseBanner)
await fetchMock.callHistory.flush(true)