Przeglądaj źródła

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
MoxAmber 10 miesięcy temu
rodzic
commit
0f0608eff0

+ 7 - 1
services/web/app/src/Features/Project/ProjectListController.mjs

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

+ 2 - 0
services/web/app/src/Features/Tutorial/TutorialController.mjs

@@ -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) {

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

@@ -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

+ 38 - 10
services/web/frontend/js/features/project-list/components/notifications/groups-and-enterprise-banner.tsx

@@ -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'
   )

+ 22 - 9
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -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
+    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)
-      localStorage.setItem(
-        'has_dismissed_groups_and_enterprise_banner',
-        dismissed
+
+      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)