Skip to content

Commit

Permalink
Merge branch 'main' into mntor-4022
Browse files Browse the repository at this point in the history
  • Loading branch information
flozia authored Feb 5, 2025
2 parents 65e77b0 + 32e0789 commit e121c00
Show file tree
Hide file tree
Showing 12 changed files with 632 additions and 421 deletions.
12 changes: 12 additions & 0 deletions locales-pending/emails-all.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
email-footer-reason-subscriber = You’re receiving this automated email as a subscriber of { -brand-mozilla-monitor }. If you received it in error, no action is required. For more information, please visit <support-link>{ -brand-mozilla } Support</support-link>.
email-footer-reason-subscriber-one-time = You’ve received this one-time automated email because you are subscribed to { -brand-monitor-plus }. You won’t receive any further emails like this. For more information, please visit <support-link>{ -brand-mozilla } Support</support-link>.
# Variables:
# $support_link (string) - The URL the user can visit for support, e.g. "https://support.mozilla.org"
email-footer-support-content-plain = Visit our Support Center for help:
{ $support_link }
# Variables:
# $hibp_link (string) - URL to Have I Been Pwned, e.g. "https://haveibeenpwned.com".
email-footer-source-hibp-plain = Breach data provided by { -brand-HIBP }: { $hibp_link }
## Monthly overview email

email-monthly-free-subject = Your monthly { -brand-monitor } report
Expand Down Expand Up @@ -96,3 +104,7 @@ email-monthly-report-hero-free-no-breaches-heading = Great news!
email-monthly-report-hero-free-no-breaches-body = { -brand-monitor } didn’t find any data exposures to be resolved.
email-monthly-report-hero-free-no-breaches-cta = View your dashboard
email-unsubscribe-link = <link_to_unsub>Unsubscribe from this email</link_to_unsub> anytime.
# Variables:
# $unsub_link (string) - URL to the unsubscribe page, e.g. "https://monitor.mozilla.org/unsubscribe-email/...".
email-unsubscribe-link-plain = Unsubscribe from this email anytime:
{ $unsub_link }
7 changes: 7 additions & 0 deletions locales-pending/emails-plus.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ email-plus-expiration-body-part1 = Your { -brand-monitor-plus } subscription end
# Variables:
# $end_date (string) - The localised date the subscription will expire, e.g. "April 2, 1337".
email-plus-expiration-body-part2-styled = To keep your access, sign in and <renewal-link>renew your subscription</renewal-link> before <b>{ $end_date }</b>. If you need help, <support-link>contact our Support team</support-link>.
# Variables:
# $end_date (string) - The localised date the subscription will expire, e.g. "April 2, 1337".
# $renewal_link (string) - The URL the user can visit to renew their subscription, e.g. "https://monitor.mozilla.com/user/plus-expiration/"
email-plus-expiration-body-part2-plain = To keep your access, sign in and renew your subscription before { $end_date }: { $renewal_link }.
# Variables:
# $support_link (string) - The URL the user can visit to contact support, e.g. "https://support.mozilla.org/questions/new/monitor/form"
email-plus-expiration-body-part3-plain = If you need help, contact our Support team: { $support_link }.
# Variables:
# $end_date (string) - The localised date the subscription will expire, e.g. "April 2, 1337".
Expand Down
881 changes: 478 additions & 403 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
"npm": "10.1.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.738.0",
"@aws-sdk/lib-storage": "^3.738.0",
"@aws-sdk/client-s3": "^3.741.0",
"@aws-sdk/lib-storage": "^3.741.0",
"@fluent/bundle": "^0.18.0",
"@fluent/langneg": "^0.7.0",
"@fluent/react": "^0.15.2",
Expand All @@ -83,12 +83,12 @@
"@leeoniya/ufuzzy": "^1.0.18",
"@mozilla/glean": "^5.0.3",
"@next/third-parties": "15.1.6",
"@sentry/nextjs": "^8.52.0",
"@sentry/nextjs": "^8.54.0",
"@sentry/node": "^8.0.0",
"@sentry/utils": "^8.52.0",
"@sentry/utils": "^8.54.0",
"@stripe/stripe-js": "^5.6.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.12.0",
"@types/node": "^22.13.0",
"@types/react": "19.0.3",
"@types/react-dom": "19.0.2",
"canvas-confetti": "^1.9.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ import { isEligibleForPremium } from "../../../../../functions/universal/premium
import { MonthlyActivityFreeEmail } from "../../../../../../emails/templates/monthlyActivityFree/MonthlyActivityFreeEmail";
import { getMonthlyActivityFreeUnsubscribeLink } from "../../../../../../app/functions/cronjobs/unsubscribeLinks";
import { getScanResultsWithBroker } from "../../../../../../db/tables/onerep_scans";
import { UpcomingExpirationEmail } from "../../../../../../emails/templates/upcomingExpiration/UpcomingExpirationEmail";
import {
getUnstyledUpcomingExpirationEmail,
UpcomingExpirationEmail,
} from "../../../../../../emails/templates/upcomingExpiration/UpcomingExpirationEmail";
import { CONST_DAY_MILLISECONDS } from "../../../../../../constants";

async function getAdminSubscriber(): Promise<SubscriberRow | null> {
const session = await getServerSession();
Expand All @@ -66,6 +70,7 @@ async function send(
emailAddress: string,
subject: string,
template: ReactNode,
plaintextVersion?: string,
) {
const subscriber = await getAdminSubscriber();
if (!subscriber) {
Expand All @@ -85,6 +90,7 @@ async function send(
emailAddress,
"Test email: " + subject,
await renderEmail(template),
plaintextVersion,
);
}

Expand Down Expand Up @@ -301,8 +307,13 @@ export async function triggerPlusExpirationEmail(emailAddress: string) {
<UpcomingExpirationEmail
subscriber={sanitizeSubscriberRow(subscriber)}
// Always pretend that the user's account expires in 7 days for the test email:
expirationDate={new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)}
expirationDate={new Date(Date.now() + 7 * CONST_DAY_MILLISECONDS)}
l10n={l10n}
/>,
getUnstyledUpcomingExpirationEmail({
subscriber: sanitizeSubscriberRow(subscriber),
expirationDate: new Date(Date.now() + 7 * CONST_DAY_MILLISECONDS),
l10n: l10n,
}),
);
}
43 changes: 43 additions & 0 deletions src/emails/components/EmailFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,46 @@ export const RedesignedEmailFooter = (props: Props) => {
</mj-wrapper>
);
};

export const getUnstyledRedesignedEmailFooter = (props: Props): string => {
const l10n = props.l10n;
const supportLinkUrlObject = new URL(CONST_URL_SUMO_MONITOR_SUPPORT_CENTER);
supportLinkUrlObject.searchParams.set("utm_medium", "product-email");
supportLinkUrlObject.searchParams.set("utm_source", "monitor-product");
supportLinkUrlObject.searchParams.set("utm_campaign", props.utm_campaign);
supportLinkUrlObject.searchParams.set("utm_content", "support-center");

const separator = "-".repeat(30);

return `
${separator}
${l10n.getString("email-footer-support-heading")}
${l10n.getString("email-footer-support-content-plain", { support_link: supportLinkUrlObject.href })}
${separator}
Mozilla Corporation
149 New Montgomery St, 4th Floor, San Francisco, CA 94105
${l10n.getString("email-footer-trigger-transactional")}
${
// We don't have emails yet that send both a plaintext version and an unsubscribe link:
/* c8 ignore next 7 */
typeof props.unsubscribeLink !== "undefined"
? "\n" +
l10n.getString("email-unsubscribe-link-plain", {
unsub_link: props.unsubscribeLink,
}) +
"\n"
: ""
}
${l10n.getString("email-footer-source-hibp-plain", { hibp_link: "https://haveibeenpwned.com" })}
${l10n.getString("terms-of-service")}:
${CONST_URL_TERMS}
${l10n.getString("email-footer-meta-privacy-notice")}:
${CONST_URL_PRIVACY_POLICY}
`;
};
4 changes: 2 additions & 2 deletions src/emails/components/EmailHeader.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FC } from "react";
import { StorybookEmailRenderer } from "../StorybookEmailRenderer";
import { EmailHeader, Props } from "./EmailHeader";
import { EmailHeader, getUnstyledEmailHeader, Props } from "./EmailHeader";
import { getL10n } from "../../app/functions/l10n/storybookAndJest";

const meta: Meta<FC<Props>> = {
title: "Emails/Components/Header",
component: (props: Props) => (
<StorybookEmailRenderer plainTextVersion={null}>
<StorybookEmailRenderer plainTextVersion={getUnstyledEmailHeader(props)}>
<mjml>
<mj-body>
<EmailHeader {...props} />
Expand Down
9 changes: 9 additions & 0 deletions src/emails/components/EmailHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,12 @@ export const EmailHeader = (props: Props) => {
</mj-section>
);
};

export const getUnstyledEmailHeader = (props: Props) => {
const l10n = props.l10n;

return `\
${l10n.getString("public-nav-name")}
${"-".repeat(30)}
`;
};
10 changes: 8 additions & 2 deletions src/emails/components/RedesignedEmailFooter.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
import type { Meta, StoryObj } from "@storybook/react";
import { FC } from "react";
import { StorybookEmailRenderer } from "../StorybookEmailRenderer";
import { RedesignedEmailFooter, Props } from "./EmailFooter";
import {
RedesignedEmailFooter,
Props,
getUnstyledRedesignedEmailFooter,
} from "./EmailFooter";
import { getL10n } from "../../app/functions/l10n/storybookAndJest";

const meta: Meta<FC<Props>> = {
title: "Emails/Components/Redesigned footer",
component: (props: Props) => (
<StorybookEmailRenderer plainTextVersion={null}>
<StorybookEmailRenderer
plainTextVersion={getUnstyledRedesignedEmailFooter(props)}
>
<mjml>
<mj-body>
<RedesignedEmailFooter {...props} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

import { SanitizedSubscriberRow } from "../../../app/functions/server/sanitize";
import { ExtendedReactLocalization } from "../../../app/functions/l10n";
import { RedesignedEmailFooter } from "../../components/EmailFooter";
import { EmailHeader } from "../../components/EmailHeader";
import {
getUnstyledRedesignedEmailFooter,
RedesignedEmailFooter,
} from "../../components/EmailFooter";
import {
EmailHeader,
getUnstyledEmailHeader,
} from "../../components/EmailHeader";
import { HeaderStyles, MetaTags } from "../../components/HeaderStyles";
import { getLocale } from "../../../app/functions/universal/getLocale";

Expand Down Expand Up @@ -119,6 +125,37 @@ export const UpcomingExpirationEmail = (props: Props) => {
);
};

export const getUnstyledUpcomingExpirationEmail = (_props: Props): string => {
return "TODO";
export const getUnstyledUpcomingExpirationEmail = (props: Props): string => {
const l10n = props.l10n;
return `
${getUnstyledEmailHeader({ l10n: l10n, utm_campaign: utmCampaign })}
# ${l10n.getString("email-plus-expiration-heading")}
${l10n.getString("email-plus-expiration-body-part1", {
end_date: props.expirationDate.toLocaleDateString(getLocale(l10n), {
day: "numeric",
month: "long",
year: "numeric",
}),
})}
${l10n.getString("email-plus-expiration-body-part2-plain", {
end_date: props.expirationDate.toLocaleDateString(getLocale(l10n), {
day: "numeric",
month: "long",
year: "numeric",
}),
renewal_link: `${process.env.SERVER_URL}/user/plus-expiration/?utm_campaign=${utmCampaign}&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_content=resubscribe-link-plain`,
})}
${l10n.getString("email-plus-expiration-body-part3-plain", {
support_link: `https://support.mozilla.org?utm_campaign=${utmCampaign}&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_content=expiration-support-plain`,
})}
${l10n.getString("email-plus-expiration-signoff")}
${l10n.getString("email-plus-expiration-sender")}
${getUnstyledRedesignedEmailFooter({ l10n: l10n, utm_campaign: utmCampaign })}
`;
};
10 changes: 9 additions & 1 deletion src/scripts/cronjobs/churnDiscount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { SubscriberChurnRow, SubscriberRow } from "knex/types/tables";
import { sanitizeSubscriberRow } from "../../app/functions/server/sanitize";
import { getCronjobL10n } from "../../app/functions/l10n/cronjobs";
import { renderEmail } from "../../emails/renderEmail";
import { UpcomingExpirationEmail } from "../../emails/templates/upcomingExpiration/UpcomingExpirationEmail";
import {
getUnstyledUpcomingExpirationEmail,
UpcomingExpirationEmail,
} from "../../emails/templates/upcomingExpiration/UpcomingExpirationEmail";
import { getFeatureFlagData } from "../../db/tables/featureFlags";

await run();
Expand Down Expand Up @@ -71,6 +74,11 @@ async function sendChurnDiscountEmail(
l10n={l10n}
/>,
),
getUnstyledUpcomingExpirationEmail({
subscriber: sanitizedSubscriber,
expirationDate: subscriber.current_period_end,
l10n: l10n,
}),
);

logger.info(`sent email to: ${subscriber.userid}`);
Expand Down
7 changes: 5 additions & 2 deletions src/utils/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { createTransport, Transporter } from "nodemailer";
import { createTransport, Transporter, SendMailOptions } from "nodemailer";
import crypto from "crypto";

import { SentMessageInfo } from "nodemailer/lib/smtp-transport/index.js";
Expand Down Expand Up @@ -47,22 +47,25 @@ export function closeEmailPool() {
* @param recipient
* @param subject
* @param html
* @param plaintext
*/
export async function sendEmail(
recipient: string,
subject: string,
html: string,
plaintext?: string,
): Promise<SentMessageInfo> {
if (!gTransporter) {
throw new Error("SMTP transport not initialized");
}

const emailFrom = envVars.EMAIL_FROM;
const mailOptions = {
const mailOptions: SendMailOptions = {
from: emailFrom,
to: recipient,
subject,
html,
text: plaintext,
headers: {
"x-ses-configuration-set": envVars.SES_CONFIG_SET,
},
Expand Down

0 comments on commit e121c00

Please sign in to comment.