diff --git a/services/web/app/src/Features/Project/ProjectListController.js b/services/web/app/src/Features/Project/ProjectListController.js index 90f09b23fc..9de1af45a8 100644 --- a/services/web/app/src/Features/Project/ProjectListController.js +++ b/services/web/app/src/Features/Project/ProjectListController.js @@ -103,7 +103,7 @@ async function projectListPage(req, res, next) { const user = await User.findById( userId, `email emails features alphaProgram betaProgram lastPrimaryEmailCheck signUpDate${ - isSaas ? ' enrollment' : '' + isSaas ? ' enrollment writefull' : '' }` ) @@ -385,6 +385,19 @@ async function projectListPage(req, res, next) { } } + try { + await SplitTestHandler.promises.getAssignment( + req, + res, + 'writefull-integration' + ) + } catch (err) { + logger.warn( + { err }, + 'failed to get "writefull-integration" split test assignment' + ) + } + let showInrGeoBanner, inrGeoBannerSplitTestName let inrGeoBannerVariant = 'default' let showLATAMBanner = false 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 2662e99301..09522f38bd 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 @@ -5,6 +5,7 @@ import ConfirmEmail from './groups/confirm-email' import ReconfirmationInfo from './groups/affiliation/reconfirmation-info' import GroupsAndEnterpriseBanner from './groups-and-enterprise-banner' import WritefullPromoBanner from './writefull-promo-banner' +import WritefullPremiumPromoBanner from './writefull-premium-promo-banner' import GroupSsoSetupSuccess from './groups/group-sso-setup-success' import INRBanner from './ads/inr-banner' import LATAMBanner from './ads/latam-banner' @@ -12,7 +13,9 @@ import getMeta from '../../../../utils/meta' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' import customLocalStorage from '../../../../infrastructure/local-storage' import { sendMB } from '../../../../infrastructure/event-tracking' +import { isSplitTestEnabled } from '@/utils/splitTestUtils' +const WRITEFULL_PROMO_DELAY_MS = 24 * 60 * 60 * 1000 // 1 day const isChromium = () => (window.navigator as any).userAgentData?.brands?.some( (item: { brand: string }) => item.brand === 'Chromium' @@ -44,22 +47,52 @@ function UserNotifications() { 'unassigned' ) const showLATAMBanner = getMeta('ol-showLATAMBanner', false) + const writefullIntegrationSplitTestEnabled = isSplitTestEnabled( + 'writefull-integration' + ) // Temporary workaround to prevent also showing groups/enterprise banner const [showWritefull, setShowWritefull] = useState(() => { - if (isChromium()) { - const show = - getMeta('ol-showWritefullPromoBanner') && - !customLocalStorage.getItem('has_dismissed_writefull_promo_banner') - if (show) { - sendMB('promo-prompt', { - location: 'dashboard-banner', - page: '/project', - name: 'writefull', - }) - } - return show + const dismissed = customLocalStorage.getItem( + 'has_dismissed_writefull_promo_banner' + ) + if (dismissed) { + return false } + + let show = false + if (writefullIntegrationSplitTestEnabled) { + // Show the Writefull promo 1 day after it has been enabled + const user = getMeta('ol-user') + if (user.writefull?.enabled) { + const scheduledAt = customLocalStorage.getItem( + 'writefull_promo_scheduled_at' + ) + if (scheduledAt == null) { + customLocalStorage.setItem( + 'writefull_promo_scheduled_at', + new Date(Date.now() + WRITEFULL_PROMO_DELAY_MS).toISOString() + ) + } else if (new Date() >= new Date(scheduledAt)) { + show = true + } + } + } else { + // Only show the Writefull extension promo on Chrome browsers + show = isChromium() && getMeta('ol-showWritefullPromoBanner') + } + + if (show) { + sendMB('promo-prompt', { + location: 'dashboard-banner', + page: '/project', + name: writefullIntegrationSplitTestEnabled + ? 'writefull-premium' + : 'writefull', + }) + } + + return show }) const [dismissedWritefull, setDismissedWritefull] = useState(false) @@ -91,13 +124,23 @@ function UserNotifications() { splitTestName={inrGeoBannerSplitTestName} /> ) : null} - { - setDismissedWritefull(true) - }} - /> + {writefullIntegrationSplitTestEnabled ? ( + { + setDismissedWritefull(true) + }} + /> + ) : ( + { + setDismissedWritefull(true) + }} + /> + )} ) diff --git a/services/web/frontend/js/features/project-list/components/notifications/writefull-premium-promo-banner.tsx b/services/web/frontend/js/features/project-list/components/notifications/writefull-premium-promo-banner.tsx new file mode 100644 index 0000000000..d55499d9f7 --- /dev/null +++ b/services/web/frontend/js/features/project-list/components/notifications/writefull-premium-promo-banner.tsx @@ -0,0 +1,67 @@ +import { memo, useCallback } from 'react' +import Notification from '@/shared/components/notification' +import { sendMB } from '@/infrastructure/event-tracking' +import customLocalStorage from '@/infrastructure/local-storage' +import WritefullLogo from '@/shared/svgs/writefull-logo' + +const eventSegmentation = { + location: 'dashboard-banner', + page: '/project', + name: 'writefull-premium', +} + +function WritefullPremiumPromoBanner({ + show, + setShow, + onDismiss, +}: { + show: boolean + setShow: (value: boolean) => void + onDismiss: () => void +}) { + const handleClose = useCallback(() => { + customLocalStorage.setItem( + 'has_dismissed_writefull_promo_banner', + new Date() + ) + customLocalStorage.removeItem('writefull_promo_scheduled_at') + setShow(false) + sendMB('promo-dismiss', eventSegmentation) + onDismiss() + }, [setShow, onDismiss]) + + if (!show) { + return null + } + + return ( + + Enjoying Writefull? Get 10% off Writefull Premium, + giving you access to TeXGPT—AI assistance to generate LaTeX code. Use{' '} + OVERLEAF10 at the checkout. + + } + action={ + { + sendMB('promo-click', eventSegmentation) + }} + > + {' '} + Get Writefull Premium + + } + /> + ) +} + +export default memo(WritefullPremiumPromoBanner) diff --git a/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx b/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx index 6c41ce7646..fc2560ccd4 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx +++ b/services/web/frontend/js/features/project-list/components/notifications/writefull-promo-banner.tsx @@ -62,6 +62,7 @@ function WritefullPromoBanner({ height={16} width={16} style={{ marginRight: 4 }} + aria-hidden="true" /> Get Writefull for Overleaf diff --git a/services/web/frontend/js/shared/svgs/writefull-logo.jsx b/services/web/frontend/js/shared/svgs/writefull-logo.tsx similarity index 99% rename from services/web/frontend/js/shared/svgs/writefull-logo.jsx rename to services/web/frontend/js/shared/svgs/writefull-logo.tsx index ad0ec12d88..67bfd9f8b6 100644 --- a/services/web/frontend/js/shared/svgs/writefull-logo.jsx +++ b/services/web/frontend/js/shared/svgs/writefull-logo.tsx @@ -1,14 +1,14 @@ -function WritefullLogo() { +function WritefullLogo({ width = '40', height = '40' }) { return ( - ', function () { }) }) + describe('', function () { + beforeEach(function () { + window.metaAttributesCache = window.metaAttributesCache || new Map() + window.metaAttributesCache.set('ol-ExposedSettings', exposedSettings) + window.metaAttributesCache.set('ol-showWritefullPromoBanner', true) + + // The older banner is only shown to Chrome users + const navigator = window.navigator as any + navigator.userAgentData = { brands: [{ brand: 'Chromium' }] } + + localStorage.clear() + }) + + afterEach(function () { + window.metaAttributesCache = window.metaAttributesCache || new Map() + }) + + describe('when writefull-integration split test is not enabled', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-splitTestVariants', { + 'writefull-integration': 'default', + }) + }) + + it('shows the older banner', function () { + renderWithinProjectListProvider(UserNotifications) + const ctaLink = screen.getByRole('link', { + name: 'Get Writefull for Overleaf', + }) + expect(ctaLink.getAttribute('href')).to.equal( + 'https://my.writefull.com/overleaf-invite?code=OVERLEAF10' + ) + }) + + it('dismisses the banner when the close button is clicked', function () { + renderWithinProjectListProvider(UserNotifications) + screen.getByRole('link', { name: /Writefull/ }) + const closeButton = screen.getByRole('button', { name: 'Close' }) + fireEvent.click(closeButton) + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + expect(localStorage.getItem('has_dismissed_writefull_promo_banner')).to + .exist + }) + + it("doesn't show the banner if it has been dismissed", function () { + localStorage.setItem( + 'has_dismissed_writefull_promo_banner', + new Date(Date.now() - 1000) + ) + renderWithinProjectListProvider(UserNotifications) + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + }) + }) + + describe('when writefull-integration split test is enabled', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-splitTestVariants', { + 'writefull-integration': 'enabled', + }) + }) + + describe('when the writefull integration is enabled', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-user', { + writefull: { enabled: true }, + }) + }) + + it('schedules the notification for the next day', function () { + renderWithinProjectListProvider(UserNotifications) + expect(localStorage.getItem('writefull_promo_scheduled_at')).to.exist + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + }) + + it('shows the banner after it has been scheduled', function () { + localStorage.setItem( + 'writefull_promo_scheduled_at', + new Date(Date.now() - 1000) + ) + renderWithinProjectListProvider(UserNotifications) + const ctaLink = screen.getByRole('link', { + name: 'Get Writefull Premium', + }) + expect(ctaLink.getAttribute('href')).to.equal( + 'https://my.writefull.com/plans' + ) + }) + + it('dismisses the banner when the close button is clicked', function () { + localStorage.setItem( + 'writefull_promo_scheduled_at', + new Date(Date.now() - 1000) + ) + renderWithinProjectListProvider(UserNotifications) + screen.getByRole('link', { name: /Writefull/ }) + const closeButton = screen.getByRole('button', { name: 'Close' }) + fireEvent.click(closeButton) + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + expect(localStorage.getItem('has_dismissed_writefull_promo_banner')) + .to.exist + expect(localStorage.getItem('writefull_promo_scheduled_at')).not.to + .exist + }) + + it("doesn't show the banner if it has been dismissed", function () { + localStorage.setItem( + 'writefull_promo_scheduled_at', + new Date(Date.now() - 1000) + ) + localStorage.setItem( + 'has_dismissed_writefull_promo_banner', + new Date(Date.now() - 500) + ) + renderWithinProjectListProvider(UserNotifications) + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + }) + }) + + describe('when the writefull integration is not enabled', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-user', { + writefull: { enabled: false }, + }) + }) + + it("doesn't show the banner", function () { + renderWithinProjectListProvider(UserNotifications) + expect(screen.queryByRole('link', { name: /Writefull/ })).to.be.null + }) + }) + }) + }) + describe('GroupSsoSetupSuccess', function () { beforeEach(function () { window.metaAttributesCache = window.metaAttributesCache || new Map()