diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.js b/services/web/app/src/Features/Notifications/NotificationsBuilder.js
index 09a3bfb23d..3d636a8a65 100644
--- a/services/web/app/src/Features/Notifications/NotificationsBuilder.js
+++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.js
@@ -255,6 +255,32 @@ function groupInvitation(userId, subscriptionId, managedUsersEnabled) {
}
}
+function personalAndGroupSubscriptions(userId) {
+ return {
+ key: 'personal-and-group-subscriptions',
+ create(callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ NotificationsHandler.createNotification(
+ userId,
+ this.key,
+ 'notification_personal_and_group_subscriptions',
+ {},
+ null,
+ false,
+ callback
+ )
+ },
+ read(callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ NotificationsHandler.markAsReadByKeyOnly(this.key, callback)
+ },
+ }
+}
+
const NotificationsBuilder = {
// Note: notification keys should be url-safe
dropboxUnlinkedDueToLapsedReconfirmation,
@@ -265,6 +291,7 @@ const NotificationsBuilder = {
ipMatcherAffiliation,
tpdsFileLimit,
groupInvitation,
+ personalAndGroupSubscriptions,
}
NotificationsBuilder.promises = {
@@ -286,6 +313,9 @@ NotificationsBuilder.promises = {
projectInvite(invite, project, sendingUser, user) {
return promisifyAll(projectInvite(invite, project, sendingUser, user))
},
+ personalAndGroupSubscriptions(userId) {
+ return promisifyAll(personalAndGroupSubscriptions(userId))
+ },
}
module.exports = NotificationsBuilder
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 62da017cce..c7c9e72960 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -738,6 +738,7 @@
"normally_x_price_per_year": "",
"not_managed": "",
"not_now": "",
+ "notification_personal_and_group_subscriptions": "",
"notification_project_invite_accepted_message": "",
"notification_project_invite_message": "",
"number_of_users": "",
diff --git a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
index 46ac43a03c..aa6f3b48ea 100644
--- a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
+++ b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx
@@ -257,6 +257,19 @@ function CommonNotification({ notification }: CommonNotificationProps) {
) : templateKey === 'notification_group_invitation' ? (
+ ) : templateKey === 'notification_personal_and_group_subscriptions' ? (
+ id && handleDismiss(id)}
+ >
+
+ , ]}
+ />
+
+
) : (
id && handleDismiss(id)}>
{html}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index a85d1910bf..79f8da9cf5 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1161,6 +1161,7 @@
"note_features_under_development": "<0>Please note0> that features in this program are still being tested and actively developed. This means that they might <0>change0>, be <0>removed0> or <0>become part of a premium plan0>",
"nothing_to_install_ready_to_go": "There’s nothing complicated or difficult for you to install, and you can <0>__start_now__0>, even if you’ve never seen it before. __appName__ comes with a complete, ready to go LaTeX environment which runs on our servers.",
"notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleaf’s Professional features.",
+ "notification_personal_and_group_subscriptions": "We’ve spotted that you’ve got <0>more than one active __appName__ subscription0>. To avoid paying more than you need to, <1>review your subscriptions1>.",
"notification_personal_subscription_not_required_due_to_affiliation": " Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to Overleaf’s Professional features through your affiliation. You can cancel your individual subscription without losing access to any features.",
"notification_project_invite": "__userName__ would like you to join __projectName__ Join Project",
"notification_project_invite_accepted_message": "You’ve joined __projectName__",
diff --git a/services/web/scripts/back_fill_warning_user_personal_and_group_subscription.js b/services/web/scripts/back_fill_warning_user_personal_and_group_subscription.js
new file mode 100644
index 0000000000..30097c2db0
--- /dev/null
+++ b/services/web/scripts/back_fill_warning_user_personal_and_group_subscription.js
@@ -0,0 +1,73 @@
+const NotificationsBuilder = require('../app/src/Features/Notifications/NotificationsBuilder')
+const { db, waitForDb } = require('../app/src/infrastructure/mongodb')
+const { batchedUpdate } = require('./helpers/batchedUpdate')
+
+const DRY_RUN = !process.argv.includes('--dry-run=false')
+
+if (DRY_RUN) {
+ console.log('Doing dry run')
+}
+
+async function processBatch(groupSubscriptionsBatch) {
+ console.log('\n')
+ console.log('----- Batch computation started -----')
+ const flattenedMemberIds = groupSubscriptionsBatch
+ .map(sub => sub.member_ids)
+ .flatMap(memberId => memberId)
+ const uniqueFlattenedMemberIds = [...new Set(flattenedMemberIds)]
+
+ const userWithIndividualAndGroupSubscriptions = await db.subscriptions
+ .find({
+ groupPlan: false,
+ 'recurlyStatus.state': 'active',
+ admin_id: { $in: uniqueFlattenedMemberIds },
+ })
+ .toArray()
+
+ console.log(
+ `Found ${userWithIndividualAndGroupSubscriptions.length} affected users in this batch`
+ )
+
+ if (DRY_RUN) {
+ console.error('---')
+ console.error('Dry-run enabled, use --dry-run=false to commit changes')
+ console.error('---')
+ } else {
+ if (userWithIndividualAndGroupSubscriptions.length > 0) {
+ console.log(
+ `Notifying ${userWithIndividualAndGroupSubscriptions.length} users`
+ )
+
+ for (const notif of userWithIndividualAndGroupSubscriptions) {
+ await NotificationsBuilder.promises
+ .personalAndGroupSubscriptions(notif.admin_id.toString())
+ .create()
+ }
+
+ console.log(
+ `${userWithIndividualAndGroupSubscriptions.length} users successfully notified in this batch`
+ )
+ } else {
+ console.log(
+ 'No users currently subscribe to both individual and group subscription in this batch'
+ )
+ }
+ }
+}
+
+async function main() {
+ await waitForDb()
+
+ await batchedUpdate('subscriptions', { groupPlan: true }, processBatch, {
+ member_ids: 1,
+ })
+}
+
+main()
+ .then(() => {
+ process.exit(0)
+ })
+ .catch(err => {
+ console.error(err)
+ process.exit(1)
+ })
diff --git a/services/web/types/project/dashboard/notification.ts b/services/web/types/project/dashboard/notification.ts
index 22d616845b..20936cb9cc 100644
--- a/services/web/types/project/dashboard/notification.ts
+++ b/services/web/types/project/dashboard/notification.ts
@@ -6,6 +6,7 @@ type TemplateKey =
| 'notification_dropbox_duplicate_project_names'
| 'notification_dropbox_unlinked_due_to_lapsed_reconfirmation'
| 'notification_group_invitation'
+ | 'notification_personal_and_group_subscriptions'
type NotificationBase = {
_id?: number