From b0320a81002126fde6adb64dd0fc07c126d9b684 Mon Sep 17 00:00:00 2001
From: Mathias Jakobsen
Date: Wed, 16 Apr 2025 11:47:37 +0100
Subject: [PATCH] Merge pull request #24923 from
overleaf/dp-mj-papers-notification
Add notification banner for Papers integration marketing
GitOrigin-RevId: 625c3afcc6ca617fd01af58a05a6c85f7126398b
---
.../Project/ProjectListController.mjs | 13 ++-
.../web/frontend/extracted-translations.json | 3 +
.../hooks/use-papers-notification.ts | 16 ++++
.../papers-notification-banner.tsx | 86 +++++++++++++++++++
.../notifications/user-notifications.tsx | 5 ++
services/web/locales/en.json | 3 +
6 files changed, 125 insertions(+), 1 deletion(-)
create mode 100644 services/web/frontend/js/features/project-list/components/notifications/hooks/use-papers-notification.ts
create mode 100644 services/web/frontend/js/features/project-list/components/notifications/papers-notification-banner.tsx
diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs
index 6ae7080961..88541ee826 100644
--- a/services/web/app/src/Features/Project/ProjectListController.mjs
+++ b/services/web/app/src/Features/Project/ProjectListController.mjs
@@ -115,7 +115,7 @@ async function projectListPage(req, res, next) {
})
const user = await User.findById(
userId,
- `email emails features alphaProgram betaProgram lastPrimaryEmailCheck signUpDate${
+ `email emails features alphaProgram betaProgram lastPrimaryEmailCheck signUpDate refProviders${
isSaas ? ' enrollment writefull completedTutorials' : ''
}`
)
@@ -126,6 +126,8 @@ async function projectListPage(req, res, next) {
return
}
+ user.refProviders = _.mapValues(user.refProviders, Boolean)
+
if (isSaas) {
await SplitTestSessionHandler.promises.sessionMaintenance(req, user)
@@ -418,6 +420,15 @@ async function projectListPage(req, res, next) {
'sidebar-navigation-ui-update'
)
+ // Get the user's assignment for the papers notification banner split test,
+ // which populates splitTestVariants with a value for the split test name and
+ // allows Pug to send it to the browser
+ await SplitTestHandler.promises.getAssignment(
+ req,
+ res,
+ 'papers-notification-banner'
+ )
+
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index b65269eeee..c5595af725 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -122,6 +122,7 @@
"all_projects_will_be_transferred_immediately": "",
"all_these_experiments_are_available_exclusively": "",
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "",
+ "already_have_a_papers_account": "",
"already_subscribed_try_refreshing_the_page": "",
"an_email_has_already_been_sent_to": "",
"an_error_occured_while_restoring_project": "",
@@ -1862,6 +1863,7 @@
"try_for_free": "",
"try_it_for_free": "",
"try_now": "",
+ "try_papers_for_free": "",
"try_premium_for_free": "",
"try_recompile_project_or_troubleshoot": "",
"try_relinking_provider": "",
@@ -2072,6 +2074,7 @@
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "",
"you_can_now_enable_sso": "",
"you_can_now_log_in_sso": "",
+ "you_can_now_sync_your_papers_library_directly_with_your_overleaf_projects": "",
"you_can_request_a_maximum_of_limit_fixes_per_day": "",
"you_can_select_or_invite_collaborator": "",
"you_can_select_or_invite_collaborator_plural": "",
diff --git a/services/web/frontend/js/features/project-list/components/notifications/hooks/use-papers-notification.ts b/services/web/frontend/js/features/project-list/components/notifications/hooks/use-papers-notification.ts
new file mode 100644
index 0000000000..2d5426bc39
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/notifications/hooks/use-papers-notification.ts
@@ -0,0 +1,16 @@
+import { useFeatureFlag } from '@/shared/context/split-test-context'
+import getMeta from '@/utils/meta'
+
+export const usePapersNotification = () => {
+ const user = getMeta('ol-user')
+ const inRollout = useFeatureFlag('papers-notification-banner')
+ const shouldShow =
+ inRollout &&
+ user &&
+ user.features?.references &&
+ !user.refProviders?.mendeley &&
+ !user.refProviders?.zotero &&
+ !user.refProviders?.papers
+
+ return { shouldShow }
+}
diff --git a/services/web/frontend/js/features/project-list/components/notifications/papers-notification-banner.tsx b/services/web/frontend/js/features/project-list/components/notifications/papers-notification-banner.tsx
new file mode 100644
index 0000000000..5cb0c0e024
--- /dev/null
+++ b/services/web/frontend/js/features/project-list/components/notifications/papers-notification-banner.tsx
@@ -0,0 +1,86 @@
+import { memo, useCallback, useEffect } from 'react'
+import Notification from './notification'
+import { Trans, useTranslation } from 'react-i18next'
+import OLButton from '@/features/ui/components/ol/ol-button'
+import usePersistedState from '@/shared/hooks/use-persisted-state'
+import { sendMB } from '@/infrastructure/event-tracking'
+
+function PapersNotificationBanner() {
+ const { t } = useTranslation()
+ const [dismissed, setDismissed] = usePersistedState(
+ 'papers-notification-banner-dismissed',
+ false
+ )
+
+ useEffect(() => {
+ if (!dismissed) {
+ sendMB('promo-prompt', {
+ location: 'dashboard-banner',
+ name: 'papers-integration',
+ })
+ }
+ }, [dismissed])
+
+ const handleClose = useCallback(() => {
+ sendMB('promo-dismiss', {
+ location: 'dashboard-banner',
+ name: 'papers-integration',
+ })
+ setDismissed(true)
+ }, [setDismissed])
+
+ const handlePapersButtonClick = useCallback(() => {
+ sendMB('promo-click', {
+ location: 'dashboard-banner',
+ name: 'papers-integration',
+ type: 'click-try-for-free',
+ })
+ }, [])
+
+ const handleSettingsLinkClick = useCallback(() => {
+ sendMB('promo-click', {
+ location: 'dashboard-banner',
+ name: 'papers-integration',
+ type: 'click-link-account',
+ })
+ }, [])
+
+ if (dismissed) return null
+
+ return (
+
+ ,
+ ]}
+ />
+
+ }
+ action={
+
+ {t('try_papers_for_free')}
+
+ }
+ />
+ )
+}
+
+export default memo(PapersNotificationBanner)
diff --git a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx b/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
index ef1fd85088..8edb6a53cc 100644
--- a/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
+++ b/services/web/frontend/js/features/project-list/components/notifications/user-notifications.tsx
@@ -13,6 +13,8 @@ import {
DeprecatedBrowser,
isDeprecatedBrowser,
} from '@/shared/components/deprecated-browser'
+import PapersNotificationBanner from './papers-notification-banner'
+import { usePapersNotification } from './hooks/use-papers-notification'
const [enrollmentNotificationModule] = importOverleafModules(
'managedGroupSubscriptionEnrollmentNotification'
@@ -32,6 +34,8 @@ function UserNotifications() {
const groupSubscriptionsPendingEnrollment =
getMeta('ol-groupSubscriptionsPendingEnrollment') || []
+ const { shouldShow: showPapersNotificationBanner } = usePapersNotification()
+
return (
@@ -52,6 +56,7 @@ function UserNotifications() {
{USGovBanner && }
+ {showPapersNotificationBanner && }
{isDeprecatedBrowser() && }
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 4201c15d75..f0339343ee 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -148,6 +148,7 @@
"all_the_pros_of_our_standard_plan_plus_unlimited_collab": "All the pros of our standard plan, plus unlimited collaborators per project.",
"all_these_experiments_are_available_exclusively": "All these experiments are available exclusively to members of the Labs program. If you sign up, you can choose which experiments you want to try.",
"allows_to_search_by_author_title_etc_possible_to_pull_results_directly_from_your_reference_manager_if_connected": "Allows to search by author, title, etc. Possible to pull results directly from your reference manager (if connected).",
+ "already_have_a_papers_account": "Managing your citations and bibliographies in Overleaf just got way easier! Already have a Papers account? <0>Link your account here0>.",
"already_have_an_account": "Already have an account?",
"already_have_sl_account": "Already have an __appName__ account?",
"already_subscribed_try_refreshing_the_page": "Already subscribed? Try refreshing the page.",
@@ -2392,6 +2393,7 @@
"try_for_free": "Try for free",
"try_it_for_free": "Try it for free",
"try_now": "Try Now",
+ "try_papers_for_free": "Try Papers for free",
"try_premium_for_free": "Try Premium for free",
"try_recompile_project_or_troubleshoot": "Please try recompiling the project from scratch, and if that doesn’t help, follow our <0>troubleshooting guide0>.",
"try_relinking_provider": "It looks like you need to re-link your __provider__ account.",
@@ -2628,6 +2630,7 @@
"you_can_manage_your_reference_manager_integrations_from_your_account_settings_page": "You can manage your reference manager integrations from your <0>account settings page0>.",
"you_can_now_enable_sso": "You can now enable SSO on your Group settings page.",
"you_can_now_log_in_sso": "You can now log in through your institution and if eligible you will receive <0>__appName__ Professional features0>.",
+ "you_can_now_sync_your_papers_library_directly_with_your_overleaf_projects": "You can now sync your Papers library directly with your Overleaf projects",
"you_can_opt_in_and_out_of_the_program_at_any_time_on_this_page": "You can <0>opt in and out0> of the program at any time on this page",
"you_can_request_a_maximum_of_limit_fixes_per_day": "You can request a maximum of __limit__ fixes per day. Please try again tomorrow.",
"you_can_select_or_invite_collaborator": "You can select or invite __count__ collaborator on your current plan. Upgrade to add more editors or reviewers.",