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 (
+