From 78dd0a86819e4ba72d657bac9875865b1057d380 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Wed, 27 May 2026 12:32:05 +0200 Subject: [PATCH] [web] update email base templates (#33791) * update email template design * gate email template on split test GitOrigin-RevId: 2e0a1d9abf11a0c3f16e103511191d43d542b8a4 --- .../Email/Bodies/UpdatedNoCTAEmailBody.mjs | 28 +++ .../Email/Bodies/updated-cta-email.mjs | 65 +++++++ .../app/src/Features/Email/EmailBuilder.mjs | 32 +++- .../app/src/Features/Email/EmailHandler.mjs | 25 ++- .../Email/Layouts/UpdatedBaseEmailLayout.mjs | 169 ++++++++++++++++++ .../app/src/Features/Email/emailStyles.mjs | 12 ++ .../img/ol-brand/email-footer-tagline@2x.png | Bin 0 -> 10639 bytes .../web/public/img/ol-brand/email-logo@2x.png | Bin 0 -> 5813 bytes .../test/unit/src/Email/EmailBuilder.test.mjs | 102 ++++------- .../test/unit/src/Email/EmailHandler.test.mjs | 21 +++ .../src/Email/UpdatedBaseEmailLayout.test.mjs | 75 ++++++++ 11 files changed, 457 insertions(+), 72 deletions(-) create mode 100644 services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs create mode 100644 services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs create mode 100644 services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs create mode 100644 services/web/app/src/Features/Email/emailStyles.mjs create mode 100644 services/web/public/img/ol-brand/email-footer-tagline@2x.png create mode 100644 services/web/public/img/ol-brand/email-logo@2x.png create mode 100644 services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs diff --git a/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs b/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs new file mode 100644 index 0000000000..6a314c2926 --- /dev/null +++ b/services/web/app/src/Features/Email/Bodies/UpdatedNoCTAEmailBody.mjs @@ -0,0 +1,28 @@ +import _ from 'lodash' +import { fontFamily, colors } from '../emailStyles.mjs' + +export default _.template(`\ +<% if (title) { %> +

+ <%= title %> +

+<% } %> + +<% if (greeting) { %> +

+ <%= greeting %> +

+<% } %> + +<% (message).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+<% }) %> + +<% if (highlightedText) { %> +
+ <%= highlightedText %> +
+<% } %>\ +`) diff --git a/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs b/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs new file mode 100644 index 0000000000..8987b62251 --- /dev/null +++ b/services/web/app/src/Features/Email/Bodies/updated-cta-email.mjs @@ -0,0 +1,65 @@ +import _ from 'lodash' +import { fontFamily, colors } from '../emailStyles.mjs' + +export default _.template(`\ +<% if (title) { %> +

+ <%= title %> +

+<% } %> + +<% if (greeting) { %> +

+ <%= greeting %> +

+<% } %> + +<% (message).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+<% }) %> + + + + + +
+ + <%= ctaText %> + +
+ +<% if (secondaryMessage && secondaryMessage.length > 0) { %> + <% (secondaryMessage).forEach(function(paragraph) { %> +

+ <%= paragraph %> +

+ <% }) %> +<% } %> + +

+ If the button above does not appear, please copy and paste this link into your browser's address bar: +

+

+ <%= ctaURL %> +

+ +<% if (gmailGoToAction) { %> + +<% } %>\ +`) diff --git a/services/web/app/src/Features/Email/EmailBuilder.mjs b/services/web/app/src/Features/Email/EmailBuilder.mjs index 2635ebf52b..21a1314b08 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.mjs +++ b/services/web/app/src/Features/Email/EmailBuilder.mjs @@ -4,9 +4,12 @@ import moment from 'moment' import EmailMessageHelper from './EmailMessageHelper.mjs' import StringHelper from '../Helpers/StringHelper.mjs' import BaseEmailLayout from './Layouts/BaseEmailLayout.mjs' +import UpdatedBaseEmailLayout from './Layouts/UpdatedBaseEmailLayout.mjs' import SpamSafe from './SpamSafe.mjs' import ctaEmailBody from './Bodies/cta-email.mjs' +import updatedCtaEmailBody from './Bodies/updated-cta-email.mjs' import NoCTAEmailBody from './Bodies/NoCTAEmailBody.mjs' +import UpdatedNoCTAEmailBody from './Bodies/UpdatedNoCTAEmailBody.mjs' function _emailBodyPlainText(content, opts, ctaEmail) { let emailBody = `${content.greeting(opts, true)}` @@ -75,7 +78,12 @@ function ctaTemplate(content) { subject(opts) { return content.subject(opts) }, - layout: BaseEmailLayout, + layout(opts) { + const layoutFn = opts.useNewEmailDesign + ? UpdatedBaseEmailLayout + : BaseEmailLayout + return layoutFn(opts) + }, footerMessage(opts) { return content.footerMessage(opts) }, @@ -83,7 +91,8 @@ function ctaTemplate(content) { return _emailBodyPlainText(content, opts, true) }, compiledTemplate(opts) { - return ctaEmailBody({ + const bodyFn = opts.useNewEmailDesign ? updatedCtaEmailBody : ctaEmailBody + return bodyFn({ title: content.title(opts), greeting: content.greeting(opts), message: content.message(opts), @@ -108,7 +117,12 @@ function NoCTAEmailTemplate(content) { subject(opts) { return content.subject(opts) }, - layout: BaseEmailLayout, + layout(opts) { + const layoutFn = opts.useNewEmailDesign + ? UpdatedBaseEmailLayout + : BaseEmailLayout + return layoutFn(opts) + }, plainTextTemplate(opts) { return `\ ${content.greeting(opts)} @@ -120,7 +134,10 @@ The ${settings.appName} Team - ${settings.siteUrl}\ ` }, compiledTemplate(opts) { - return NoCTAEmailBody({ + const bodyFn = opts.useNewEmailDesign + ? UpdatedNoCTAEmailBody + : NoCTAEmailBody + return bodyFn({ title: typeof content.title === 'function' ? content.title(opts) : undefined, greeting: content.greeting(opts), @@ -137,7 +154,6 @@ The ${settings.appName} Team - ${settings.siteUrl}\ function buildEmail(templateName, opts) { const template = templates[templateName] - opts.siteUrl = settings.siteUrl opts.body = template.compiledTemplate(opts) opts.footerMessage = template.footerMessage ? template.footerMessage(opts) @@ -189,7 +205,7 @@ templates.canceledSubscription = ctaTemplate({ return ['Thank you in advance!'] }, ctaText() { - return 'Leave Feedback' + return 'Leave feedback' }, ctaURL(opts) { return 'https://docs.google.com/forms/d/e/1FAIpQLSfa7z_s-cucRRXm70N4jEcSbFsZeb0yuKThHGQL8ySEaQzF0Q/viewform?usp=sf_link' @@ -394,7 +410,7 @@ templates.reconfirmEmail = ctaTemplate({ ] }, ctaText() { - return 'Reconfirm Email' + return 'Reconfirm email' }, ctaURL(opts) { return opts.confirmEmailUrl @@ -894,7 +910,7 @@ templates.SAMLDataCleared = ctaTemplate({ ] }, ctaText(opts) { - return 'Update my Emails and affiliations' + return 'Update my emails and affiliations' }, ctaURL(opts) { return `${settings.siteUrl}/user/settings` diff --git a/services/web/app/src/Features/Email/EmailHandler.mjs b/services/web/app/src/Features/Email/EmailHandler.mjs index 85a94d5439..0c0b769688 100644 --- a/services/web/app/src/Features/Email/EmailHandler.mjs +++ b/services/web/app/src/Features/Email/EmailHandler.mjs @@ -4,14 +4,37 @@ import logger from '@overleaf/logger' import EmailBuilder from './EmailBuilder.mjs' import EmailSender from './EmailSender.mjs' import Queues from '../../infrastructure/Queues.mjs' +import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs' +import UserGetter from '../User/UserGetter.mjs' const EMAIL_SETTINGS = Settings.email || {} /** * @param {string} emailType - * @param {opts} any + * @param {object} opts */ async function sendEmail(emailType, opts) { + if (opts.to) { + try { + const user = await UserGetter.promises.getUserByAnyEmail(opts.to, { + _id: 1, + }) + if (user) { + const { variant } = + await SplitTestHandler.promises.getAssignmentForUser( + user._id.toString(), + 'new-email-design' + ) + opts.useNewEmailDesign = variant === 'enabled' + } + } catch (err) { + logger.warn( + { err }, + 'failed to get new-email-design split test assignment' + ) + } + } + const email = EmailBuilder.buildEmail(emailType, opts) if (email.type === 'lifecycle' && !EMAIL_SETTINGS.lifecycle) { return diff --git a/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs b/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs new file mode 100644 index 0000000000..174bd6d8d1 --- /dev/null +++ b/services/web/app/src/Features/Email/Layouts/UpdatedBaseEmailLayout.mjs @@ -0,0 +1,169 @@ +import _ from 'lodash' +import settings from '@overleaf/settings' +import { fontFamily, colors } from '../emailStyles.mjs' + +const _isSaas = settings.env === 'saas' +const _imageBase = settings.cdn?.web?.host || settings.siteUrl || null + +let _logoHtml = `${settings.appName}` +let _taglineHtml = '' +if (_isSaas && _imageBase) { + _logoHtml = ` + + ${settings.appName} + + ` + + _taglineHtml = ` + + + The home of scientific and technical writing + + ` +} + +const _customFooter = settings.email?.template?.customFooter || '' + +export default _.template(`\ + + + + + + + + + + + + +
+                               +                               +
+ + + + + + + + +\ +`) diff --git a/services/web/app/src/Features/Email/emailStyles.mjs b/services/web/app/src/Features/Email/emailStyles.mjs new file mode 100644 index 0000000000..5ad855e6c2 --- /dev/null +++ b/services/web/app/src/Features/Email/emailStyles.mjs @@ -0,0 +1,12 @@ +export const fontFamily = 'Arial, Helvetica, sans-serif' + +export const colors = { + textDark: '#1b222c', + textMuted: '#495365', + background: '#f4f5f6', + ctaGreen: '#098842', + ctaText: '#ffffff', + linkGreen: '#1e6b41', + linkHover: '#155a30', + logoGreen: '#04652f', +} diff --git a/services/web/public/img/ol-brand/email-footer-tagline@2x.png b/services/web/public/img/ol-brand/email-footer-tagline@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..de4d7a2b1ebad026dbdf0e45598dc4d9c10c1f20 GIT binary patch literal 10639 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 literal 0 HcmV?d00001 diff --git a/services/web/public/img/ol-brand/email-logo@2x.png b/services/web/public/img/ol-brand/email-logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..900c7a113b57841709b99a4225e7150cb58c89ec GIT binary patch literal 5813 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 ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.UserGetter = { + promises: { + getUserByAnyEmail: sinon.stub().resolves(null), + }, + } + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + ctx.EmailHandler = (await import(MODULE_PATH)).default }) diff --git a/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs b/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs new file mode 100644 index 0000000000..cf2df55888 --- /dev/null +++ b/services/web/test/unit/src/Email/UpdatedBaseEmailLayout.test.mjs @@ -0,0 +1,75 @@ +import { vi, expect } from 'vitest' +import { load } from 'cheerio' +import path from 'node:path' + +const LAYOUT_PATH = path.join( + import.meta.dirname, + '../../../../app/src/Features/Email/Layouts/UpdatedBaseEmailLayout' +) + +async function buildLayoutWithEnv(env) { + vi.resetModules() + vi.doMock('@overleaf/settings', () => ({ + default: { + appName: 'TestApp', + siteUrl: 'https://www.example.com', + env, + }, + })) + const { default: layout } = await import(LAYOUT_PATH) + return layout({ body: 'Test body', footerMessage: '' }) +} + +describe('UpdatedBaseEmailLayout branding', function () { + afterEach(function () { + vi.restoreAllMocks() + vi.resetModules() + }) + + describe('on SaaS', function () { + it('should show the logo image', async function () { + const html = await buildLayoutWithEnv('saas') + const dom = load(html) + const logoImg = dom('img[alt="TestApp"]') + expect(logoImg.length).to.equal(1) + expect(logoImg.attr('src')).to.contain('email-logo@2x.png') + }) + + it('should show the tagline image', async function () { + const html = await buildLayoutWithEnv('saas') + expect(html).to.contain('email-footer-tagline@2x.png') + }) + }) + + describe('on server-ce', function () { + it('should show the app name as text instead of logo', async function () { + const html = await buildLayoutWithEnv('server-ce') + const dom = load(html) + const logoImg = dom('img[alt="TestApp"]') + expect(logoImg.length).to.equal(0) + expect(html).to.contain('TestApp') + expect(html).to.not.contain('email-logo@2x.png') + }) + + it('should not show the tagline image', async function () { + const html = await buildLayoutWithEnv('server-ce') + expect(html).to.not.contain('email-footer-tagline@2x.png') + }) + }) + + describe('on server-pro', function () { + it('should show the app name as text instead of logo', async function () { + const html = await buildLayoutWithEnv('server-pro') + const dom = load(html) + const logoImg = dom('img[alt="TestApp"]') + expect(logoImg.length).to.equal(0) + expect(html).to.contain('TestApp') + expect(html).to.not.contain('email-logo@2x.png') + }) + + it('should not show the tagline image', async function () { + const html = await buildLayoutWithEnv('server-pro') + expect(html).to.not.contain('email-footer-tagline@2x.png') + }) + }) +})