mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 11:01:56 +02:00
Merge pull request #16167 from overleaf/em-writefull-promo
Writefull integration 10% promo GitOrigin-RevId: 27ef5e51d7d9c56d85ccc44692444bff46fbeeec
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -62,6 +62,7 @@ function WritefullPromoBanner({
|
||||
height={16}
|
||||
width={16}
|
||||
style={{ marginRight: 4 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Get Writefull for Overleaf</span>
|
||||
</a>
|
||||
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user