From d29eefaa64ed5f311c92ef2ffe8466f1ee256ad8 Mon Sep 17 00:00:00 2001
From: Olzhas Askar
Date: Fri, 17 Jul 2026 18:08:27 +0500
Subject: [PATCH] Merge pull request #35478 from overleaf/oa-paywall-ctas
[web] Paywall CTAs
GitOrigin-RevId: 26703fe008926db67473a43ef64addca0e23accd
---
.../Features/Project/ProjectController.mjs | 37 ++++++++--------
.../web/frontend/extracted-translations.json | 1 +
.../change-list/owner-paywall-prompt.tsx | 10 ++++-
...pile-time-warning-upgrade-prompt-inner.tsx | 4 +-
.../upgrade-track-changes-modal.tsx | 4 +-
services/web/locales/cs.json | 1 +
services/web/locales/da.json | 1 +
services/web/locales/de.json | 1 +
services/web/locales/en.json | 1 +
services/web/locales/es.json | 3 +-
services/web/locales/fi.json | 1 +
services/web/locales/fr.json | 3 +-
services/web/locales/it.json | 1 +
services/web/locales/ja.json | 1 +
services/web/locales/ko.json | 1 +
services/web/locales/nl.json | 1 +
services/web/locales/no.json | 1 +
services/web/locales/pl.json | 1 +
services/web/locales/pt.json | 1 +
services/web/locales/ru.json | 3 +-
services/web/locales/sv.json | 1 +
services/web/locales/tr.json | 1 +
services/web/locales/zh-CN.json | 1 +
.../change-list/owner-paywall-prompt.test.tsx | 42 +++++++++++++++++++
...time-warning-upgrade-prompt-inner.test.tsx | 34 +++++++++++++++
25 files changed, 132 insertions(+), 24 deletions(-)
create mode 100644 services/web/test/frontend/features/history/components/change-list/owner-paywall-prompt.test.tsx
create mode 100644 services/web/test/frontend/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.test.tsx
diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs
index bcfce8c0e8..62201b4dde 100644
--- a/services/web/app/src/Features/Project/ProjectController.mjs
+++ b/services/web/app/src/Features/Project/ProjectController.mjs
@@ -518,7 +518,7 @@ const _ProjectController = {
user: (async () => {
const user = await User.findById(
userId,
- 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram labsExperiments completedTutorials writefull aiFeatures'
+ 'email first_name last_name referal_id signUpDate featureSwitches features featuresEpoch refProviders alphaProgram betaProgram isAdmin ace labsProgram labsExperiments completedTutorials writefull aiFeatures lastTrial'
).exec()
// Handle case of deleted user
if (!user) {
@@ -627,19 +627,26 @@ const _ProjectController = {
inEnterpriseCommons || affiliation.institution?.enterpriseCommons
}
- const getSplitTestAssignment = async splitTest => {
- return await SplitTestHandler.promises.getAssignment(
- req,
- res,
- splitTest
- )
+ const allowedFreeTrial =
+ subscription == null ||
+ isStandaloneAiAddOnPlanCode(subscription.planCode)
+
+ // The test's audience: free users who've spent their trial and can't start
+ // another, yet still see a misleading "free trial" CTA.
+ const trialIneligible =
+ !anonymous && allowedFreeTrial && Boolean(user?.lastTrial)
+
+ if (trialIneligible) {
+ splitTests.push('paywall-cta-trial-ineligible')
}
- const splitTestAssignments = {}
- await Promise.all(
- splitTests.map(async splitTest => {
- splitTestAssignments[splitTest] =
- await getSplitTestAssignment(splitTest)
- })
+
+ const splitTestAssignments = Object.fromEntries(
+ await Promise.all(
+ splitTests.map(async splitTest => [
+ splitTest,
+ await SplitTestHandler.promises.getAssignment(req, res, splitTest),
+ ])
+ )
)
// PDF caching, these tests are archived but we are keeping the frontend code unchanged for now
@@ -705,10 +712,6 @@ const _ProjectController = {
return res.sendStatus(401)
}
- const allowedFreeTrial =
- subscription == null ||
- isStandaloneAiAddOnPlanCode(subscription.planCode)
-
let wsUrl = Settings.wsUrl
let metricName = 'load-editor-ws'
if (user.betaProgram && Settings.wsUrlBeta !== undefined) {
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 8cfbe5b80b..8ba98a65db 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -1631,6 +1631,7 @@
"refresh": "",
"refresh_page_after_linking_dropbox": "",
"refresh_page_after_starting_free_trial": "",
+ "refresh_page_after_subscribing": "",
"refreshing": "",
"refund_with_description": "",
"regards": "",
diff --git a/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
index 0a8d0e6a75..c26018aafd 100644
--- a/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
+++ b/services/web/frontend/js/features/history/components/change-list/owner-paywall-prompt.tsx
@@ -3,10 +3,12 @@ import { useCallback, useEffect, useState } from 'react'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import StartFreeTrialButton from '../../../../shared/components/start-free-trial-button'
import UpgradeBenefits from '@/shared/components/upgrade-benefits'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
export function OwnerPaywallPrompt() {
const { t } = useTranslation()
const [clickedFreeTrialButton, setClickedFreeTrialButton] = useState(false)
+ const showSubscribeNow = useFeatureFlag('paywall-cta-trial-ineligible')
useEffect(() => {
eventTracking.send('subscription-funnel', 'editor-click-feature', 'history')
@@ -34,11 +36,15 @@ export function OwnerPaywallPrompt() {
buttonProps={{ variant: 'premium' }}
handleClick={handleFreeTrialClick}
>
- {t('start_free_trial')}
+ {showSubscribeNow ? t('subscribe_now') : t('start_free_trial')}
{clickedFreeTrialButton ? (
- {t('refresh_page_after_starting_free_trial')}
+
+ {showSubscribeNow
+ ? t('refresh_page_after_subscribing')
+ : t('refresh_page_after_starting_free_trial')}
+
) : null}
)
diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.tsx
index ff49e7cd4e..7561e74cee 100644
--- a/services/web/frontend/js/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.tsx
@@ -3,12 +3,14 @@ import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useTranslation } from 'react-i18next'
import { FC } from 'react'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
export const CompileTimeWarningUpgradePromptInner: FC<{
handleDismissWarning: () => void
segmentation: eventTracking.Segmentation
}> = ({ handleDismissWarning, segmentation }) => {
const { t } = useTranslation()
+ const showSubscribeNow = useFeatureFlag('paywall-cta-trial-ineligible')
return (
- {t('start_free_trial')}
+ {showSubscribeNow ? t('subscribe_now') : t('start_free_trial')}
}
ariaLive="polite"
diff --git a/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx b/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx
index 3d64635ce9..f9a3a1c9f8 100644
--- a/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx
+++ b/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx
@@ -18,11 +18,13 @@ import OLRow from '@/shared/components/ol/ol-row'
import OLCol from '@/shared/components/ol/ol-col'
import MaterialIcon from '@/shared/components/material-icon'
import { useEditorContext } from '@/shared/context/editor-context'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
function UpgradeTrackChangesModal() {
const { t } = useTranslation()
const { project } = useProjectContext()
const user = useUserContext()
+ const showSubscribeNow = useFeatureFlag('paywall-cta-trial-ineligible')
const {
upgradeTrackChangesModal: { show, location = 'unknown' },
setUpgradeTrackChangesModal,
@@ -94,7 +96,7 @@ function UpgradeTrackChangesModal() {
})
}
>
- {t('try_for_free')}
+ {showSubscribeNow ? t('subscribe_now') : t('try_for_free')}
) : (
+
+
+ )
+}
+
+describe('', function () {
+ let openStub: sinon.SinonStub
+
+ beforeEach(function () {
+ // StartFreeTrialButton opens the plans page in a new tab on click
+ openStub = sinon.stub(window, 'open')
+ })
+
+ afterEach(function () {
+ openStub.restore()
+ window.metaAttributesCache.delete('ol-splitTestVariants')
+ })
+
+ it('shows the "Start free trial" CTA and free-trial refresh message', function () {
+ window.metaAttributesCache.set('ol-splitTestVariants', {})
+ renderPrompt()
+ fireEvent.click(screen.getByRole('button', { name: 'Start free trial' }))
+ screen.getByText('Please refresh this page after starting your free trial.')
+ })
+
+ it('shows the "Subscribe now" CTA and subscribe refresh message when paywall-cta-trial-ineligible is enabled', function () {
+ window.metaAttributesCache.set('ol-splitTestVariants', {
+ 'paywall-cta-trial-ineligible': 'enabled',
+ })
+ renderPrompt()
+ fireEvent.click(screen.getByRole('button', { name: 'Subscribe now' }))
+ screen.getByText('Please refresh this page after subscribing.')
+ })
+})
diff --git a/services/web/test/frontend/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.test.tsx b/services/web/test/frontend/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.test.tsx
new file mode 100644
index 0000000000..232a240e92
--- /dev/null
+++ b/services/web/test/frontend/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from '@testing-library/react'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
+import { CompileTimeWarningUpgradePromptInner } from '@/features/pdf-preview/components/compile-time-warning-upgrade-prompt-inner'
+
+function renderPrompt() {
+ return render(
+
+ {}}
+ segmentation={{}}
+ />
+
+ )
+}
+
+describe('', function () {
+ afterEach(function () {
+ window.metaAttributesCache.delete('ol-splitTestVariants')
+ })
+
+ it('shows the "Start free trial" CTA by default', function () {
+ window.metaAttributesCache.set('ol-splitTestVariants', {})
+ renderPrompt()
+ screen.getByRole('button', { name: 'Start free trial' })
+ })
+
+ it('shows the "Subscribe now" CTA when paywall-cta-trial-ineligible is enabled', function () {
+ window.metaAttributesCache.set('ol-splitTestVariants', {
+ 'paywall-cta-trial-ineligible': 'enabled',
+ })
+ renderPrompt()
+ screen.getByRole('button', { name: 'Subscribe now' })
+ })
+})