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 note that features in this program are still being tested and actively developed. This means that they might <0>change, be <0>removed or <0>become part of a premium plan", "nothing_to_install_ready_to_go": "There’s nothing complicated or difficult for you to install, and you can <0>__start_now__, 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__ subscription. To avoid paying more than you need to, <1>review your subscriptions.", "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