Przeglądaj źródła

Merge pull request #16167 from overleaf/em-writefull-promo

Writefull integration 10% promo

GitOrigin-RevId: 27ef5e51d7d9c56d85ccc44692444bff46fbeeec
Eric Mc Sween 2 lat temu
rodzic
commit
ee0dbcf331

+ 14 - 1
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

+ 61 - 18
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',
-        })
+    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
+        }
       }
-      return show
+    } 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}
-        <WritefullPromoBanner
-          show={showWritefull}
-          setShow={setShowWritefull}
-          onDismiss={() => {
-            setDismissedWritefull(true)
-          }}
-        />
+        {writefullIntegrationSplitTestEnabled ? (
+          <WritefullPremiumPromoBanner
+            show={showWritefull}
+            setShow={setShowWritefull}
+            onDismiss={() => {
+              setDismissedWritefull(true)
+            }}
+          />
+        ) : (
+          <WritefullPromoBanner
+            show={showWritefull}
+            setShow={setShowWritefull}
+            onDismiss={() => {
+              setDismissedWritefull(true)
+            }}
+          />
+        )}
       </ul>
     </div>
   )

+ 67 - 0
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 (
+    <Notification
+      type="info"
+      isDismissible
+      onDismiss={handleClose}
+      content={
+        <>
+          Enjoying Writefull? Get <strong>10% off Writefull Premium</strong>,
+          giving you access to TeXGPT—AI assistance to generate LaTeX code. Use{' '}
+          <strong>OVERLEAF10</strong> at the checkout.
+        </>
+      }
+      action={
+        <a
+          className="btn btn-secondary"
+          href="https://my.writefull.com/plans"
+          target="_blank"
+          rel="noreferrer"
+          onClick={() => {
+            sendMB('promo-click', eventSegmentation)
+          }}
+        >
+          <WritefullLogo width="16" height="16" />{' '}
+          <span>Get Writefull Premium</span>
+        </a>
+      }
+    />
+  )
+}
+
+export default memo(WritefullPremiumPromoBanner)

+ 1 - 0
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"
           />
           <span>Get Writefull for Overleaf</span>
         </a>

+ 4 - 4
services/web/frontend/js/shared/svgs/writefull-logo.jsx → services/web/frontend/js/shared/svgs/writefull-logo.tsx

@@ -1,14 +1,14 @@
-function WritefullLogo() {
+function WritefullLogo({ width = '40', height = '40' }) {
   return (
     <svg
-      width="40"
-      height="40"
+      width={width}
+      height={height}
       viewBox="0 0 40 40"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"
       xmlnsXlink="http://www.w3.org/1999/xlink"
+      style={{ verticalAlign: 'middle' }}
     >
-      <rect width="40" height="40" fill="white" />
       <rect
         x="1.6665"
         y="1.66666"

+ 134 - 0
services/web/test/frontend/features/project-list/components/notifications.test.tsx

@@ -26,6 +26,7 @@ import Common from '../../../../../frontend/js/features/project-list/components/
 import Institution from '../../../../../frontend/js/features/project-list/components/notifications/groups/institution'
 import ConfirmEmail from '../../../../../frontend/js/features/project-list/components/notifications/groups/confirm-email'
 import ReconfirmationInfo from '../../../../../frontend/js/features/project-list/components/notifications/groups/affiliation/reconfirmation-info'
+import UserNotifications from '../../../../../frontend/js/features/project-list/components/notifications/user-notifications'
 import { ProjectListProvider } from '../../../../../frontend/js/features/project-list/context/project-list-context'
 import {
   Notification,
@@ -959,6 +960,139 @@ describe('<UserNotifications />', function () {
     })
   })
 
+  describe('<WritefullPromoBanner>', 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()