Merge pull request #35478 from overleaf/oa-paywall-ctas

[web] Paywall CTAs

GitOrigin-RevId: 26703fe008926db67473a43ef64addca0e23accd
This commit is contained in:
Olzhas Askar
2026-09-17 11:30:50 +00:00
committed by Copybot
parent 76e004b3e2
commit d29eefaa64
25 changed files with 132 additions and 24 deletions
@@ -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) {
@@ -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": "",
@@ -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')}
</StartFreeTrialButton>
</p>
{clickedFreeTrialButton ? (
<p className="small">{t('refresh_page_after_starting_free_trial')}</p>
<p className="small">
{showSubscribeNow
? t('refresh_page_after_subscribing')
: t('refresh_page_after_starting_free_trial')}
</p>
) : null}
</div>
)
@@ -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 (
<Notification
@@ -21,7 +23,7 @@ export const CompileTimeWarningUpgradePromptInner: FC<{
size: 'sm',
}}
>
{t('start_free_trial')}
{showSubscribeNow ? t('subscribe_now') : t('start_free_trial')}
</StartFreeTrialButton>
}
ariaLive="polite"
@@ -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')}
</OLButton>
) : (
<OLButton
+1
View File
@@ -229,6 +229,7 @@
"reconnecting": "Obnovuji připojení",
"reconnecting_in_x_secs": "Obnovuji připojení za __seconds__ sek",
"refresh_page_after_starting_free_trial": "Obnovte prosím stránku poté co začnete používat svou bezplatnou trial verzi.",
"refresh_page_after_subscribing": "Obnovte prosím stránku po předplacení.",
"regards": "S pozdravem",
"register": "Registrovat",
"register_to_edit_template": "Pro úpravu šablony __templateName__ se prosím přihlašte",
+1
View File
@@ -1411,6 +1411,7 @@
"refresh": "Genindlæs",
"refresh_page_after_linking_dropbox": "Genindlæs venligst denne side efter at have forbundet din konto til Dropbox",
"refresh_page_after_starting_free_trial": "Genindlæs venligst denne side efter du har startet din gratis prøveperiode.",
"refresh_page_after_subscribing": "Genindlæs venligst denne side efter du har tilmeldt dig.",
"refreshing": "Genindlæser",
"regards": "Venligst",
"register": "Registrer",
+1
View File
@@ -1193,6 +1193,7 @@
"refresh": "Aktualisieren",
"refresh_page_after_linking_dropbox": "Bitte aktualisiere diese Seite, nachdem du dein Konto mit Dropbox verknüpft hast.",
"refresh_page_after_starting_free_trial": "Bitte aktualisiere diese Seite, nachdem du deinen kostenlosen Test gestartet hast.",
"refresh_page_after_subscribing": "Bitte aktualisiere diese Seite, nachdem du abonniert hast.",
"refreshing": "Aktualisiere",
"regards": "Viele Grüße",
"register": "Registrieren",
+1
View File
@@ -2126,6 +2126,7 @@
"refresh": "Refresh",
"refresh_page_after_linking_dropbox": "Please refresh this page after linking your account to Dropbox.",
"refresh_page_after_starting_free_trial": "Please refresh this page after starting your free trial.",
"refresh_page_after_subscribing": "Please refresh this page after subscribing.",
"refreshing": "Refreshing",
"refund_with_description": "Refund: __description__",
"regards": "Regards",
+2 -1
View File
@@ -476,7 +476,8 @@
"reconnecting": "Volviendo a conectar",
"reconnecting_in_x_secs": "Volviendo a conectar en __seconds__ segundos",
"reference_error_relink_hint": "Si el error persiste, prueba a volver a vincular la cuenta aquí:",
"refresh_page_after_starting_free_trial": "Por favor actualiza esta página para empezar tu prueba gratuita.",
"refresh_page_after_starting_free_trial": "Por favor, actualiza esta página después de empezar tu prueba gratuita.",
"refresh_page_after_subscribing": "Por favor, actualiza esta página después de suscribirte.",
"regards": "Atentamente",
"register": "Registrarse",
"register_to_edit_template": "Por favor regístrate para editar la plantilla __templateName__",
+1
View File
@@ -239,6 +239,7 @@
"reconnecting": "Yhdistetään uudelleen",
"reconnecting_in_x_secs": "Yhteys muodostetaan uudestaan __seconds__ sekunnin kuluttua",
"refresh_page_after_starting_free_trial": "Ole hyvä ja päivitä tämä sivu aloitettuasi ilmaisen kokeilusi.",
"refresh_page_after_subscribing": "Ole hyvä ja päivitä tämä sivu tilattuasi.",
"regards": "Kiittäen",
"register": "Rekisteröidy",
"register_to_edit_template": "Ole hyvä ja rekisteröidy muokataksesi mallia __templateName__",
+2 -1
View File
@@ -876,7 +876,8 @@
"reference_sync": "Synchro. avec gestionnaire de références",
"refresh": "Rafraîchir",
"refresh_page_after_linking_dropbox": "Veuillez rafraîchir cette page après avoir lié votre compte à Dropbox.",
"refresh_page_after_starting_free_trial": "Veuillez actualiser cette page avant de commencer votre essai gratuit.",
"refresh_page_after_starting_free_trial": "Veuillez actualiser cette page après avoir commencé votre essai gratuit.",
"refresh_page_after_subscribing": "Veuillez actualiser cette page après votre abonnement.",
"refreshing": "Actualisation",
"regards": "Merci",
"register": "S’inscrire",
+1
View File
@@ -265,6 +265,7 @@
"reconnecting": "Riconnessione",
"reconnecting_in_x_secs": "Riconnessione fra __seconds__ secondi",
"refresh_page_after_starting_free_trial": "Per favore aggiorna questa pagina dopo l’inizio del tuo trial gratuito.",
"refresh_page_after_subscribing": "Per favore aggiorna questa pagina dopo l’abbonamento.",
"regards": "Saluti",
"register": "Registrati",
"register_to_edit_template": "Per favore registrati per modificare il modello __templateName__",
+1
View File
@@ -341,6 +341,7 @@
"reconnecting_in_x_secs": "__seconds__秒後に再接続",
"reference_error_relink_hint": "エラーが継続して発生する場合は、こちらでアカウントを再リンク付けしてください:",
"refresh_page_after_starting_free_trial": "無料トライアルを開始したらこのページの再読み込みを行ってください。",
"refresh_page_after_subscribing": "ご登録後、このページの再読み込みを行ってください。",
"regards": "よろしくお願いいたします",
"register": "登録する",
"register_to_edit_template": "__templateName__ テンプレートを編集するには登録してください",
+1
View File
@@ -385,6 +385,7 @@
"reference_search": "고급 레퍼런스 검색",
"reference_sync": "레퍼런스 매니저 동기화",
"refresh_page_after_starting_free_trial": "무료 시험 시작 후 이 페이지를 새로고침하세요.",
"refresh_page_after_subscribing": "구독 후 이 페이지를 새로고침하세요.",
"regards": "감사합니다",
"register": "등록",
"register_to_edit_template": "__templateName__ 템플릿을 편집하기위해 등록해주세요",
+1
View File
@@ -390,6 +390,7 @@
"reduce_costs_group_licenses": "Je kunt de administratie verkleinen en de kosten verlagen met onze scherp geprijsde groeplicenties.",
"reference_error_relink_hint": "Als deze foutmelding blijft verschijnen, probeer dan je account hier opnieuw te linken:",
"refresh_page_after_starting_free_trial": "Ververs deze pagina nadat je de gratis proefperiode hebt gestart.",
"refresh_page_after_subscribing": "Ververs deze pagina nadat je een abonnement hebt afgesloten.",
"regards": "Groeten",
"register": "Registreren",
"register_to_edit_template": "Registreet om het sjabloon __templateName__ te bewerken",
+1
View File
@@ -272,6 +272,7 @@
"reconnecting": "Kobler til",
"reconnecting_in_x_secs": "Kobler til om __seconds__ sekunder",
"refresh_page_after_starting_free_trial": "Vennligst last inn siden på nytt etter at du har startet din gratis prøveperiode.",
"refresh_page_after_subscribing": "Vennligst last inn siden på nytt etter at du har abonnert.",
"regards": "Takk",
"register": "Registrer",
"register_to_edit_template": "Vennligst registrer deg for å redigere __templateName__ malen",
+1
View File
@@ -158,6 +158,7 @@
"reconnecting": "Ponowne łączenie",
"reconnecting_in_x_secs": "Próba połączenia za __seconds__ s",
"refresh_page_after_starting_free_trial": "Odśwież tę stronę po rozpoczęciu darmowego trialu",
"refresh_page_after_subscribing": "Odśwież tę stronę po zasubskrybowaniu.",
"regards": "Dziękujemy",
"register": "Zarejestruj się",
"register_to_edit_template": "Zarejestruj się, żeby edytować szablon __templateName__",
+1
View File
@@ -467,6 +467,7 @@
"reference_search": "Busca avançada de referências",
"reference_sync": "Gerenciador de sincronia de referências",
"refresh_page_after_starting_free_trial": "Por favor atualize essa página depois de iniciar seu teste grátis.",
"refresh_page_after_subscribing": "Por favor, atualize esta página depois de assinar.",
"regards": "Saudações",
"register": "Registrar",
"register_to_edit_template": "Por favor, registre-se para editar o modelo __templateName__",
+2 -1
View File
@@ -330,7 +330,8 @@
"recompile_pdf": "Перекомпилировать PDF",
"reconnecting": "Пересоединение",
"reconnecting_in_x_secs": "Повторное соединение через __seconds__ секунд",
"refresh_page_after_starting_free_trial": "Пожалуйста, обновите страницу",
"refresh_page_after_starting_free_trial": "Пожалуйста, обновите страницу после начала бесплатного пробного периода.",
"refresh_page_after_subscribing": "Пожалуйста, обновите страницу после оформления подписки.",
"regards": "С уважением",
"register": "Регистрация",
"register_to_edit_template": "Пожалуйста, зарегистрируйтесь, чтобы редактировать шаблон __templateName__",
+1
View File
@@ -656,6 +656,7 @@
"reference_sync": "Referenshanterare synk",
"refresh": "Uppdatera",
"refresh_page_after_starting_free_trial": "Vänligen uppdatera denna sida efter att du startat din gratis provapå period.",
"refresh_page_after_subscribing": "Vänligen uppdatera denna sida efter att du prenumererat.",
"regards": "Vänliga hälsningar",
"register": "Registrera",
"register_error": "Registreringsfel",
+1
View File
@@ -266,6 +266,7 @@
"reconnecting": "Yeniden bağlanıyor",
"reconnecting_in_x_secs": "__seconds__ saniye içinde tekrar bağlanılmaya çalışılacak",
"refresh_page_after_starting_free_trial": "Ücretsiz denemenize başladıktan sonra lütfen sayfayı yenileyin",
"refresh_page_after_subscribing": "Abone olduktan sonra lütfen sayfayı yenileyin.",
"regards": "Saygılarımızla",
"register": "Kayıt ol",
"register_to_edit_template": "__templateName__ şablonunu düzenlemek için lütfen kayıt olunuz",
+1
View File
@@ -1610,6 +1610,7 @@
"refresh": "刷新",
"refresh_page_after_linking_dropbox": "请在将您的帐户链接到Dropbox后刷新此页。",
"refresh_page_after_starting_free_trial": "请在您开始免费试用之后刷新此页面",
"refresh_page_after_subscribing": "请在您订阅后刷新此页。",
"refreshing": "正在刷新",
"regards": "感谢",
"register": "注册",
@@ -0,0 +1,42 @@
import { render, screen, fireEvent } from '@testing-library/react'
import sinon from 'sinon'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { OwnerPaywallPrompt } from '@/features/history/components/change-list/owner-paywall-prompt'
function renderPrompt() {
return render(
<SplitTestProvider>
<OwnerPaywallPrompt />
</SplitTestProvider>
)
}
describe('<OwnerPaywallPrompt/>', 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.')
})
})
@@ -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(
<SplitTestProvider>
<CompileTimeWarningUpgradePromptInner
handleDismissWarning={() => {}}
segmentation={{}}
/>
</SplitTestProvider>
)
}
describe('<CompileTimeWarningUpgradePromptInner/>', 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' })
})
})