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' }) + }) +})