diff --git a/package-lock.json b/package-lock.json
index f88a888ef6..c21c6ca33b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10483,9 +10483,9 @@
}
},
"node_modules/@customerio/cdp-analytics-core": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-core/-/cdp-analytics-core-0.3.0.tgz",
- "integrity": "sha512-5BJ8VgUkLT2YuDZ7Dr+oWnpFNObF5tgfj1hU/E01+kxUAvzUoOiLNL6NjKy9AeZYIFuzqgzYCfmR/1mGYdngsw==",
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-core/-/cdp-analytics-core-0.3.9.tgz",
+ "integrity": "sha512-AjMB48tTu8JN4NAgmbOoGAS1veE7AvLUyZD+qKXZLD/muWy6Vou4MEfXLVzhQP/cIbf3VBPGUxMnugjwZFc6Sw==",
"license": "MIT",
"dependencies": {
"@lukeed/uuid": "^2.0.0",
@@ -10494,11 +10494,11 @@
}
},
"node_modules/@customerio/cdp-analytics-node": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-node/-/cdp-analytics-node-0.3.0.tgz",
- "integrity": "sha512-p8WCtj+O3JoOooaFeENQzGPD8SDJ8x7z3P6s6xHtIaxsYjPFVfFVRV1avJQb3YTA+Z4Ak5h1oK/zayTIBJA6pQ==",
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@customerio/cdp-analytics-node/-/cdp-analytics-node-0.3.9.tgz",
+ "integrity": "sha512-+4DPQDCt0y+KhzqHoCrokicJhchiCPsnaC7W7h4ebrahc0zoLuovLKEn4pXeygi6yS5D2oKJ0bzi6xisb21mtQ==",
"dependencies": {
- "@customerio/cdp-analytics-core": "0.3.0",
+ "@customerio/cdp-analytics-core": "0.3.9",
"@lukeed/uuid": "^2.0.0",
"buffer": "^6.0.3",
"node-fetch": "^2.6.7",
@@ -58218,6 +58218,7 @@
"@aws-sdk/client-ses": "^3.864.0",
"@contentful/rich-text-html-renderer": "^16.0.2",
"@contentful/rich-text-types": "^16.0.2",
+ "@customerio/cdp-analytics-node": "^0.3.9",
"@google-cloud/bigquery": "^6.0.1",
"@google-cloud/storage": "^6.10.1",
"@node-oauth/oauth2-server": "^5.1.0",
diff --git a/services/web/app/src/Features/Newsletter/MailChimpClient.mjs b/services/web/app/src/Features/Newsletter/MailChimpClient.mjs
deleted file mode 100644
index 492a2e6eb3..0000000000
--- a/services/web/app/src/Features/Newsletter/MailChimpClient.mjs
+++ /dev/null
@@ -1,68 +0,0 @@
-import { fetchJson, fetchNothing } from '@overleaf/fetch-utils'
-
-class MailChimpClient {
- constructor(apiKey) {
- this.apiKey = apiKey
- this.dc = apiKey.split('-')[1]
- this.baseUrl = `https://${this.dc}.api.mailchimp.com/3.0/`
- this.fetchOptions = {
- method: 'GET',
- basicAuth: {
- user: 'any',
- password: this.apiKey,
- },
- }
- }
-
- async request(path, options) {
- try {
- const requestUrl = `${this.baseUrl}${path}`
- if (options.method === 'GET') {
- return await fetchJson(requestUrl, options)
- }
- await fetchNothing(requestUrl, options)
- } catch (err) {
- // if there's a json body in the response, expose it in the error (for compatibility with node-mailchimp)
- const errorBody = err.body ? JSON.parse(err.body) : {}
- const errWithBody = Object.assign(err, errorBody)
- throw errWithBody
- }
- }
-
- async get(path) {
- return await this.request(path, this.fetchOptions)
- }
-
- async put(path, body) {
- const options = Object.assign({}, this.fetchOptions)
- options.method = 'PUT'
- options.json = body
-
- return await this.request(path, options)
- }
-
- async post(path, body) {
- const options = Object.assign({}, this.fetchOptions)
- options.method = 'POST'
- options.json = body
-
- return await this.request(path, options)
- }
-
- async delete(path) {
- const options = Object.assign({}, this.fetchOptions)
- options.method = 'DELETE'
-
- return await this.request(path, options)
- }
-
- async patch(path, body) {
- const options = Object.assign({}, this.fetchOptions)
- options.method = 'PATCH'
- options.json = body
-
- return await this.request(path, options)
- }
-}
-
-export default MailChimpClient
diff --git a/services/web/app/src/Features/Newsletter/MailChimpProvider.mjs b/services/web/app/src/Features/Newsletter/MailChimpProvider.mjs
deleted file mode 100644
index 5f7f5163da..0000000000
--- a/services/web/app/src/Features/Newsletter/MailChimpProvider.mjs
+++ /dev/null
@@ -1,315 +0,0 @@
-import logger from '@overleaf/logger'
-import Settings from '@overleaf/settings'
-import crypto from 'node:crypto'
-import OError from '@overleaf/o-error'
-import { callbackify } from 'node:util'
-import MailChimpClient from './MailChimpClient.mjs'
-
-function mailchimpIsConfigured() {
- return Settings.mailchimp != null && Settings.mailchimp.api_key != null
-}
-
-function make(listName, listId) {
- let provider
- if (!mailchimpIsConfigured() || !listId) {
- logger.debug({ listName }, 'Using newsletter provider: none')
- provider = makeNullProvider(listName)
- } else {
- logger.debug({ listName }, 'Using newsletter provider: mailchimp')
- provider = makeMailchimpProvider(listName, listId)
- }
- return {
- subscribed: callbackify(provider.subscribed),
- subscribe: callbackify(provider.subscribe),
- unsubscribe: callbackify(provider.unsubscribe),
- changeEmail: callbackify(provider.changeEmail),
- tag: callbackify(provider.tag),
- removeTag: callbackify(provider.removeTag),
- promises: provider,
- }
-}
-
-export default {
- make,
-}
-
-class NonFatalEmailUpdateError extends OError {
- constructor(message, oldEmail, newEmail) {
- super(message, { oldEmail, newEmail })
- }
-}
-
-function makeMailchimpProvider(listName, listId) {
- const mailchimp = new MailChimpClient(Settings.mailchimp.api_key)
- const MAILCHIMP_LIST_ID = listId
-
- return {
- subscribed,
- subscribe,
- unsubscribe,
- changeEmail,
- tag,
- removeTag,
- }
-
- async function subscribed(user) {
- try {
- const path = getSubscriberPath(user.email)
- const result = await mailchimp.get(path)
- return result?.status === 'subscribed'
- } catch (err) {
- if (err?.response?.status === 404) {
- return false
- }
- throw OError.tag(err, 'error getting newsletter subscriptions status', {
- userId: user._id,
- listName,
- })
- }
- }
-
- async function subscribe(user) {
- try {
- const path = getSubscriberPath(user.email)
- await mailchimp.put(path, {
- email_address: user.email,
- status: 'subscribed',
- status_if_new: 'subscribed',
- merge_fields: getMergeFields(user),
- })
- logger.debug(
- { user, listName },
- 'finished subscribing user to newsletter'
- )
- } catch (err) {
- throw OError.tag(err, 'error subscribing user to newsletter', {
- userId: user._id,
- listName,
- })
- }
- }
-
- async function tag(user, tag) {
- try {
- const path = getMemberTagsPath(user.email)
- await mailchimp.post(path, {
- tags: [{ name: tag, status: 'active' }],
- })
- logger.debug({ user, listName }, `finished adding ${tag} to user`)
- } catch (err) {
- throw OError.tag(err, `error adding ${tag} to user`, {
- userId: user._id,
- listName,
- tag,
- })
- }
- }
-
- async function removeTag(user, tag) {
- try {
- const path = getMemberTagsPath(user.email)
- await mailchimp.post(path, {
- tags: [{ name: tag, status: 'inactive' }],
- })
- logger.debug({ user, listName }, `finished removing ${tag} from user`)
- } catch (err) {
- throw OError.tag(err, `error removing ${tag} from user`, {
- userId: user._id,
- listName,
- tag,
- })
- }
- }
-
- async function unsubscribe(user, options = {}) {
- try {
- const path = getSubscriberPath(user.email)
- if (options.delete) {
- await mailchimp.delete(path)
- } else {
- await mailchimp.patch(path, {
- status: 'unsubscribed',
- merge_fields: getMergeFields(user),
- })
- }
- logger.debug(
- { user, options, listName },
- 'finished unsubscribing user from newsletter'
- )
- } catch (err) {
- if ([404, 405].includes(err?.response?.status)) {
- // silently ignore users who were never subscribed (404) or previously deleted (405)
- return
- }
-
- if (err.message.includes('looks fake or invalid')) {
- logger.debug(
- { err, user, options, listName },
- 'Mailchimp declined to unsubscribe user because it finds the email looks fake'
- )
- return
- }
-
- throw OError.tag(err, 'error unsubscribing user from newsletter', {
- userId: user._id,
- listName,
- })
- }
- }
-
- async function changeEmail(user, newEmail) {
- const oldEmail = user.email
-
- try {
- await updateEmailInMailchimp(user, newEmail)
- } catch (updateError) {
- // if we failed to update the user, delete their old email address so that
- // we don't leave it stuck in mailchimp
- logger.debug(
- { oldEmail, newEmail, updateError, listName },
- 'unable to change email in newsletter, removing old mail'
- )
-
- try {
- await unsubscribe(user, { delete: true })
- } catch (unsubscribeError) {
- // something went wrong removing the user's address
- throw OError.tag(
- unsubscribeError,
- 'error unsubscribing old email in response to email change failure',
- { oldEmail, newEmail, updateError, listName }
- )
- }
-
- if (!(updateError instanceof NonFatalEmailUpdateError)) {
- throw updateError
- }
- }
- }
-
- async function updateEmailInMailchimp(user, newEmail) {
- const oldEmail = user.email
-
- // mailchimp doesn't give us error codes, so we have to parse the message :'(
- const errors = {
- 'merge fields were invalid': 'user has never subscribed',
- 'could not be validated':
- 'user has previously unsubscribed or new email already exist on list',
- 'is already a list member': 'new email is already on mailing list',
- 'looks fake or invalid': 'mail looks fake to mailchimp',
- }
-
- try {
- const path = getSubscriberPath(oldEmail)
- await mailchimp.patch(path, {
- email_address: newEmail,
- merge_fields: getMergeFields(user),
- })
- logger.debug(
- { newEmail, listName },
- 'finished changing email in the newsletter'
- )
- } catch (err) {
- // silently ignore users who were never subscribed
- if (err.status === 404) {
- return
- }
-
- // look through expected mailchimp errors and log if we find one
- Object.keys(errors).forEach(key => {
- if (err.message.includes(key)) {
- const message = `unable to change email in newsletter, ${errors[key]}`
-
- logger.debug({ oldEmail, newEmail, listName }, message)
-
- throw new NonFatalEmailUpdateError(
- message,
- oldEmail,
- newEmail
- ).withCause(err)
- }
- })
-
- // if we didn't find an expected error, generate something to throw
- throw OError.tag(err, 'error changing email in newsletter', {
- oldEmail,
- newEmail,
- listName,
- })
- }
- }
-
- function getSubscriberPath(email) {
- const emailHash = hashEmail(email)
- return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}`
- }
-
- function getMemberTagsPath(email) {
- const emailHash = hashEmail(email)
- return `/lists/${MAILCHIMP_LIST_ID}/members/${emailHash}/tags`
- }
-
- function hashEmail(email) {
- return crypto.createHash('md5').update(email.toLowerCase()).digest('hex')
- }
-
- function getMergeFields(user) {
- return {
- FNAME: user.first_name,
- LNAME: user.last_name,
- MONGO_ID: user._id.toString(),
- }
- }
-}
-
-function makeNullProvider(listName) {
- return {
- subscribed,
- subscribe,
- unsubscribe,
- changeEmail,
- tag,
- removeTag,
- }
-
- async function subscribed(user) {
- logger.debug(
- { user, listName },
- 'Not checking user because no newsletter provider is configured'
- )
- return false
- }
-
- async function subscribe(user) {
- logger.debug(
- { user, listName },
- 'Not subscribing user to newsletter because no newsletter provider is configured'
- )
- }
-
- async function unsubscribe(user) {
- logger.debug(
- { user, listName },
- 'Not unsubscribing user from newsletter because no newsletter provider is configured'
- )
- }
-
- async function changeEmail(user, newEmail) {
- logger.debug(
- { userId: user._id, newEmail, listName },
- 'Not changing email in newsletter for user because no newsletter provider is configured'
- )
- }
- async function tag(user, tag) {
- logger.debug(
- { userId: user._id, tag, listName },
- 'Not tagging user because no newsletter provider is configured'
- )
- }
- async function removeTag(user, tag) {
- logger.debug(
- { userId: user._id, tag, listName },
- 'Not removing tag for user because no newsletter provider is configured'
- )
- }
-}
diff --git a/services/web/app/src/Features/Newsletter/NewsletterManager.mjs b/services/web/app/src/Features/Newsletter/NewsletterManager.mjs
deleted file mode 100644
index 1b7f638c4a..0000000000
--- a/services/web/app/src/Features/Newsletter/NewsletterManager.mjs
+++ /dev/null
@@ -1,9 +0,0 @@
-import Settings from '@overleaf/settings'
-import MailchimpProvider from './MailChimpProvider.mjs'
-
-const provider = MailchimpProvider.make(
- 'newsletter',
- Settings.mailchimp ? Settings.mailchimp.list_id : null
-)
-
-export default provider
diff --git a/services/web/app/src/Features/User/UserController.mjs b/services/web/app/src/Features/User/UserController.mjs
index ac1ca8870b..37cc016f9c 100644
--- a/services/web/app/src/Features/User/UserController.mjs
+++ b/services/web/app/src/Features/User/UserController.mjs
@@ -2,7 +2,6 @@ import UserHandler from './UserHandler.mjs'
import UserDeleter from './UserDeleter.mjs'
import UserGetter from './UserGetter.mjs'
import { User } from '../../models/User.mjs'
-import NewsletterManager from '../Newsletter/NewsletterManager.mjs'
import logger from '@overleaf/logger'
import metrics from '@overleaf/metrics'
import AuthenticationManager from '../Authentication/AuthenticationManager.mjs'
@@ -300,15 +299,16 @@ async function subscribe(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
req.logger.addFields({ userId })
- const user = await UserGetter.promises.getUser(userId, {
- _id: 1,
- email: 1,
- first_name: 1,
- last_name: 1,
- })
- await NewsletterManager.promises.subscribe(user)
+ await Modules.promises.hooks.fire(
+ 'updateTopicSubscription',
+ userId,
+ 'newsletter',
+ true
+ )
+
res.json({
message: req.i18n.translate('thanks_settings_updated'),
+ subscribed: true,
})
}
@@ -316,16 +316,16 @@ async function unsubscribe(req, res, next) {
const userId = SessionManager.getLoggedInUserId(req.session)
req.logger.addFields({ userId })
- const user = await UserGetter.promises.getUser(userId, {
- _id: 1,
- email: 1,
- first_name: 1,
- last_name: 1,
- })
- await NewsletterManager.promises.unsubscribe(user)
- await Modules.promises.hooks.fire('newsletterUnsubscribed', user)
+ await Modules.promises.hooks.fire(
+ 'updateTopicSubscription',
+ userId,
+ 'newsletter',
+ false
+ )
+
res.json({
message: req.i18n.translate('thanks_settings_updated'),
+ subscribed: false,
})
}
diff --git a/services/web/app/src/Features/User/UserDeleter.mjs b/services/web/app/src/Features/User/UserDeleter.mjs
index d2a47e745c..3ba04c7ed6 100644
--- a/services/web/app/src/Features/User/UserDeleter.mjs
+++ b/services/web/app/src/Features/User/UserDeleter.mjs
@@ -4,7 +4,6 @@ import Settings from '@overleaf/settings'
import { User } from '../../models/User.mjs'
import { DeletedUser } from '../../models/DeletedUser.mjs'
import { UserAuditLogEntry } from '../../models/UserAuditLogEntry.mjs'
-import NewsletterManager from '../Newsletter/NewsletterManager.mjs'
import ProjectDeleter from '../Project/ProjectDeleter.mjs'
import SubscriptionHandler from '../Subscription/SubscriptionHandler.mjs'
import SubscriptionUpdater from '../Subscription/SubscriptionUpdater.mjs'
@@ -212,8 +211,6 @@ async function _cleanupUser(user) {
logger.info({ userId }, '[cleanupUser] removing user sessions from Redis')
await UserSessionsManager.promises.removeSessionsFromRedis(user)
if (Features.hasFeature('saas')) {
- logger.info({ userId }, '[cleanupUser] unsubscribing from newsletters')
- await NewsletterManager.promises.unsubscribe(user, { delete: true })
logger.info({ userId }, '[cleanupUser] cancelling subscription')
await SubscriptionHandler.promises.cancelSubscription(user)
logger.info({ userId }, '[cleanupUser] deleting affiliations')
diff --git a/services/web/app/src/Features/User/UserPagesController.mjs b/services/web/app/src/Features/User/UserPagesController.mjs
index e25c1ca8f8..16774e307d 100644
--- a/services/web/app/src/Features/User/UserPagesController.mjs
+++ b/services/web/app/src/Features/User/UserPagesController.mjs
@@ -5,8 +5,8 @@ import logger from '@overleaf/logger'
import Settings from '@overleaf/settings'
import AuthenticationController from '../Authentication/AuthenticationController.mjs'
import SessionManager from '../Authentication/SessionManager.mjs'
-import NewsletterManager from '../Newsletter/NewsletterManager.mjs'
import SubscriptionLocator from '../Subscription/SubscriptionLocator.mjs'
+import UserAnalyticsIdCache from '../Analytics/UserAnalyticsIdCache.mjs'
import _ from 'lodash'
import { expressify } from '@overleaf/promise-utils'
import Features from '../../infrastructure/Features.mjs'
@@ -199,6 +199,39 @@ async function reconfirmAccountPage(req, res) {
res.render('user/reconfirm', pageData)
}
+async function emailPreferencesPage(req, res) {
+ const userId = SessionManager.getLoggedInUserId(req.session)
+ const user = await UserGetter.promises.getUser(userId, {
+ _id: 1,
+ email: 1,
+ first_name: 1,
+ last_name: 1,
+ })
+
+ if (!user) {
+ throw new Error('User not found')
+ }
+
+ let subscribed = false
+
+ const analyticsId = await UserAnalyticsIdCache.get(userId)
+ if (analyticsId) {
+ const [preferences] = await Modules.promises.hooks.fire(
+ 'getSubscriptionPreferences',
+ analyticsId
+ )
+
+ subscribed = Boolean(preferences?.newsletter)
+ }
+
+ res.render('user/email-preferences', {
+ title: 'newsletter_info_title',
+ customerIoEnabled: true,
+ subscribed,
+ user,
+ })
+}
+
const UserPagesController = {
accountSuspended: expressify(accountSuspended),
@@ -279,28 +312,7 @@ const UserPagesController = {
)
},
- emailPreferencesPage(req, res, next) {
- const userId = SessionManager.getLoggedInUserId(req.session)
- UserGetter.getUser(
- userId,
- { _id: 1, email: 1, first_name: 1, last_name: 1 },
- (err, user) => {
- if (err != null) {
- return next(err)
- }
- NewsletterManager.subscribed(user, (err, subscribed) => {
- if (err != null) {
- OError.tag(err, 'error getting newsletter subscription status')
- return next(err)
- }
- res.render('user/email-preferences', {
- title: 'newsletter_info_title',
- subscribed,
- })
- })
- }
- )
- },
+ emailPreferencesPage: expressify(emailPreferencesPage),
async compromisedPasswordPage(req, res) {
res.render('user/compromised_password')
diff --git a/services/web/app/src/Features/User/UserRegistrationHandler.mjs b/services/web/app/src/Features/User/UserRegistrationHandler.mjs
index 5e77955da2..ab78de2bf7 100644
--- a/services/web/app/src/Features/User/UserRegistrationHandler.mjs
+++ b/services/web/app/src/Features/User/UserRegistrationHandler.mjs
@@ -2,7 +2,7 @@ import { User } from '../../models/User.mjs'
import UserCreator from './UserCreator.mjs'
import UserGetter from './UserGetter.mjs'
import AuthenticationManager from '../Authentication/AuthenticationManager.mjs'
-import NewsletterManager from '../Newsletter/NewsletterManager.mjs'
+import Modules from '../../infrastructure/Modules.mjs'
import logger from '@overleaf/logger'
import crypto from 'node:crypto'
import EmailHandler from '../Email/EmailHandler.mjs'
@@ -72,7 +72,12 @@ const UserRegistrationHandler = {
if (userDetails.subscribeToNewsletter === 'true') {
try {
- NewsletterManager.subscribe(user)
+ await Modules.promises.hooks.fire(
+ 'updateTopicSubscription',
+ user._id,
+ 'newsletter',
+ true
+ )
} catch (error) {
logger.warn(
{ err: error, user },
diff --git a/services/web/app/src/Features/User/UserUpdater.mjs b/services/web/app/src/Features/User/UserUpdater.mjs
index d27aff6ffe..c23ae8793c 100644
--- a/services/web/app/src/Features/User/UserUpdater.mjs
+++ b/services/web/app/src/Features/User/UserUpdater.mjs
@@ -10,7 +10,6 @@ import FeaturesUpdater from '../Subscription/FeaturesUpdater.mjs'
import EmailHandler from '../Email/EmailHandler.mjs'
import EmailHelper from '../Helpers/EmailHelper.mjs'
import Errors from '../Errors/Errors.js'
-import NewsletterManager from '../Newsletter/NewsletterManager.mjs'
import UserAuditLogHandler from './UserAuditLogHandler.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import EmailChangeHelper from '../Analytics/EmailChangeHelper.mjs'
@@ -295,14 +294,6 @@ async function setDefaultEmailAddress(
})
}
- try {
- await NewsletterManager.promises.changeEmail(user, email)
- } catch (error) {
- logger.warn(
- { err: error, oldEmail, newEmail: email },
- 'Failed to change email in newsletter subscription'
- )
- }
try {
await Modules.promises.hooks.fire('userEmailChanged', user, email)
} catch (err) {
diff --git a/services/web/app/views/_customer_io.pug b/services/web/app/views/_customer_io.pug
index 2b17d93fb3..e9506bbac1 100644
--- a/services/web/app/views/_customer_io.pug
+++ b/services/web/app/views/_customer_io.pug
@@ -3,7 +3,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
function boolAttr(value) {
return value !== undefined ? String(value) : null;
}
- script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-ai-blocked=boolAttr(aiBlocked), data-has-ai-assist=boolAttr(hasAiAssist), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId(), data-last-active=lastActive, data-sign-up-date=signUpDate, data-subject-area=subjectArea, data-role=role, data-used-latex=usedLatex, data-primary-occupation=primaryOccupation, data-country=countryCode, data-commons-institution=commonsInstitution, data-group-role=groupRole, data-features=user.features, data-is-managed-user=boolAttr(isManagedUser)).
+ script(type="text/javascript", id="cio-loader", nonce=scriptNonce, data-best-subscription=(usersBestSubscription && usersBestSubscription.type), data-ai-blocked=boolAttr(aiBlocked), data-has-ai-assist=boolAttr(hasAiAssist), data-cio-write-key=ExposedSettings.cioWriteKey, data-cio-site-id=ExposedSettings.cioSiteId, data-session-analytics-id=getSessionAnalyticsId(), data-user-id=getLoggedInUserId(), data-last-active=lastActive, data-sign-up-date=signUpDate, data-subject-area=subjectArea, data-role=role, data-used-latex=usedLatex, data-primary-occupation=primaryOccupation, data-country=countryCode, data-commons-institution=commonsInstitution, data-group-role=groupRole, data-features=user.features, data-email=user.email, data-is-managed-user=boolAttr(isManagedUser)).
function parseBool(value) {
return value === 'true' ? true : value === 'false' ? false : undefined;
@@ -24,6 +24,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
var primaryOccupation = cioSettings.primaryOccupation;
var usedLatex = cioSettings.usedLatex;
var features = cioSettings.features;
+ var email = cioSettings.email;
var countryCode = cioSettings.country;
var commonsInstitution = cioSettings.commonsInstitution;
@@ -65,6 +66,7 @@ if(customerIoEnabled && ExposedSettings.cioWriteKey && ExposedSettings.cioSiteId
addIfDefined(identifyData, 'groupRole', groupRole);
addIfDefined(identifyData, 'isManagedUser', isManagedUser);
addIfDefined(identifyData, 'features', features);
+ addIfDefined(identifyData, 'email', email);
analytics.identify(analyticsId, identifyData);
}}();
diff --git a/services/web/app/views/user/email-preferences.pug b/services/web/app/views/user/email-preferences.pug
index f0213bb8da..b799067434 100644
--- a/services/web/app/views/user/email-preferences.pug
+++ b/services/web/app/views/user/email-preferences.pug
@@ -1,48 +1,16 @@
extends ../layout-website-redesign
include ../_mixins/back_to_btns
+block entrypointVar
+ - entrypoint = 'pages/user/email-preferences'
+
+block append meta
+ meta(name='ol-newsletter-subscribed' data-type='boolean' content=subscribed)
+ meta(name='ol-user' data-type='json' content=user)
+
block vars
- isWebsiteRedesign = true
block content
- main#main-content.content
- .container
- .row
- .col-lg-10.offset-lg-1.col-xl-8.offset-xl-2
- .page-header
- h1 #{translate("newsletter_info_title")}
-
- p #{translate("newsletter_info_summary")}
-
- - var submitAction
- if subscribed
- - submitAction = '/user/newsletter/unsubscribe'
- p !{translate("newsletter_info_subscribed", {}, ['strong'])}
- else
- - submitAction = '/user/newsletter/subscribe'
- p !{translate("newsletter_info_unsubscribed", {}, ['strong'])}
-
- form(
- name='newsletterForm'
- data-ol-async-form
- data-ol-reload-on-success
- action=submitAction
- method='POST'
- )
- input(name='_csrf' type='hidden' value=csrfToken)
- +formMessages
- p.actions.text-center
- if subscribed
- button.btn-danger.btn(type='submit' data-ol-disabled-inflight)
- span(data-ol-inflight='idle') #{translate("unsubscribe")}
- span(hidden data-ol-inflight='pending') #{translate("saving")}…
- else
- button.btn-primary.btn(type='submit' data-ol-disabled-inflight)
- span(data-ol-inflight='idle') #{translate("subscribe")}
- span(hidden data-ol-inflight='pending') #{translate("saving")}…
-
- if subscribed
- p #{translate("newsletter_info_note")}
-
- .page-separator
- +back-to-btns
+ main#main-content.content.content-alt
+ #email-preferences-root
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index c9b9001337..cc25083b9f 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -1158,6 +1158,10 @@
"new_tag_name": "",
"newsletter": "",
"newsletter_info_note": "",
+ "newsletter_info_subscribed": "",
+ "newsletter_info_summary": "",
+ "newsletter_info_title": "",
+ "newsletter_info_unsubscribed": "",
"newsletter_onboarding_accept": "",
"next": "",
"next_page": "",
diff --git a/services/web/frontend/js/features/settings/components/email-preferences/email-preferences-form.tsx b/services/web/frontend/js/features/settings/components/email-preferences/email-preferences-form.tsx
new file mode 100644
index 0000000000..039a354913
--- /dev/null
+++ b/services/web/frontend/js/features/settings/components/email-preferences/email-preferences-form.tsx
@@ -0,0 +1,85 @@
+import { useState } from 'react'
+import { useTranslation, Trans } from 'react-i18next'
+import { postJSON, getUserFacingMessage } from '@/infrastructure/fetch-json'
+import useAsync from '@/shared/hooks/use-async'
+import OLButton from '@/shared/components/ol/ol-button'
+import OLNotification from '@/shared/components/ol/ol-notification'
+import getMeta from '@/utils/meta'
+
+function EmailPreferencesForm() {
+ const { t } = useTranslation()
+ const initialSubscribed = getMeta('ol-newsletter-subscribed')
+ const [subscribed, setSubscribed] = useState(initialSubscribed)
+ const { isLoading, isSuccess, isError, error, runAsync } = useAsync()
+
+ const handleToggleSubscription = () => {
+ const endpoint = subscribed
+ ? '/user/newsletter/unsubscribe'
+ : '/user/newsletter/subscribe'
+
+ runAsync(postJSON<{ subscribed: boolean }>(endpoint))
+ .then(response => setSubscribed(response.subscribed))
+ .catch(() => {})
+ }
+
+ return (
+ <>
+ {isError && (
+
+ {subscribed ? (
+
+ {subscribed ? (
+
{t('newsletter_info_note')}
} + > + ) +} + +export default EmailPreferencesForm diff --git a/services/web/frontend/js/features/settings/components/email-preferences/root.tsx b/services/web/frontend/js/features/settings/components/email-preferences/root.tsx new file mode 100644 index 0000000000..fa222c2a38 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/email-preferences/root.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from 'react-i18next' +import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' +import OLRow from '@/shared/components/ol/ol-row' +import OLCol from '@/shared/components/ol/ol-col' +import OLPageContentCard from '@/shared/components/ol/ol-page-content-card' +import EmailPreferencesForm from './email-preferences-form' + +function EmailPreferencesRoot() { + const { isReady } = useWaitForI18n() + + return ( +{t('newsletter_info_summary')}
+