Skip to content

Commit 33ae264

Browse files
committed
feat(auth): Use libs/email sender in fxa-auth-server
Because: - The ability to render and send emails was moved out of auth-server This Commit: - Updates the first emails (password reset) to use the new email-sender - Updates a few templates that needed changed - Adds a new email-helper for general functions around localized date strings, and splitting a list of emails into 'to' and 'cc' - Updates tests
1 parent d73eeae commit 33ae264

22 files changed

Lines changed: 1068 additions & 80 deletions

File tree

libs/accounts/email-renderer/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ export * from './renderer';
66
export * from './renderer/bindings-node';
77
export * from './bindings';
88
export * from './templates';
9+
export * from './layouts/fxa';
10+
export * from './renderer/email-helpers';

libs/accounts/email-renderer/src/partials/userInfo/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ export type TemplateData = UserDeviceTemplateData &
1111
primaryEmail?: string;
1212
date?: string;
1313
time?: string;
14+
//acceptLanguage: string;
1415
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import {
6+
constructLocalDateString,
7+
constructLocalTimeAndDateStrings,
8+
} from './email-helpers';
9+
10+
describe('EmailHelpers', () => {
11+
afterEach(() => {
12+
jest.restoreAllMocks();
13+
});
14+
15+
describe('constructLocalTimeAndDateStrings', () => {
16+
beforeEach(() => {
17+
jest
18+
.spyOn(Date, 'now')
19+
.mockReturnValue(new Date('2024-01-15T20:30:45Z').getTime());
20+
});
21+
22+
it('should construct time and date strings with timezone', () => {
23+
const result = constructLocalTimeAndDateStrings(
24+
'America/Los_Angeles',
25+
'en-US'
26+
);
27+
28+
expect(result.time).toEqual('12:30:45 PM (PST)');
29+
expect(result.date).toEqual('Monday, Jan 15, 2024');
30+
});
31+
32+
it('should use default timezone when not provided', () => {
33+
const result = constructLocalTimeAndDateStrings(undefined, 'en-US');
34+
35+
expect(result.time).toEqual('8:30:45 PM (UTC)');
36+
expect(result.date).toEqual('Monday, Jan 15, 2024');
37+
expect(result.acceptLanguage).toEqual('en-US');
38+
expect(result.timeZone).toEqual('Etc/UTC');
39+
});
40+
41+
it('should use default locale when not provided', () => {
42+
const result = constructLocalTimeAndDateStrings('America/New_York');
43+
44+
expect(result.time).toEqual('3:30:45 PM (EST)');
45+
expect(result.date).toEqual('Monday, Jan 15, 2024');
46+
expect(result.acceptLanguage).toEqual('en');
47+
expect(result.timeZone).toEqual('America/New_York');
48+
});
49+
50+
it('should handle different locales', () => {
51+
const result = constructLocalTimeAndDateStrings('Europe/London', 'fr-FR');
52+
53+
expect(result.time).toEqual('20:30:45 (GMT)');
54+
expect(result.date).toEqual('lundi, 15 janv. 2024');
55+
});
56+
57+
it('should format with no parameters', () => {
58+
const result = constructLocalTimeAndDateStrings();
59+
60+
expect(result.time).toEqual('8:30:45 PM (UTC)');
61+
expect(result.date).toEqual('Monday, Jan 15, 2024');
62+
});
63+
});
64+
65+
describe('constructLocalDateString', () => {
66+
it('should construct localized date string with timezone', () => {
67+
const date = new Date('2024-01-15T12:00:00Z');
68+
69+
const result = constructLocalDateString(
70+
'America/Los_Angeles',
71+
'en-US',
72+
date
73+
);
74+
75+
expect(result).toEqual('01/15/2024');
76+
});
77+
78+
it('should accept custom format string', () => {
79+
const date = new Date('2024-01-15T12:00:00Z');
80+
81+
const result = constructLocalDateString(
82+
'America/Chicago',
83+
'en-US',
84+
date,
85+
'YYYY-MM-DD'
86+
);
87+
88+
expect(result).toEqual('2024-01-15');
89+
});
90+
91+
it('should use current date when not provided', () => {
92+
jest
93+
.spyOn(Date, 'now')
94+
.mockReturnValue(new Date('2024-01-15T12:00:00Z').getTime());
95+
96+
const result = constructLocalDateString('America/Denver', 'en-US');
97+
98+
expect(result).toEqual('01/15/2024');
99+
});
100+
101+
it('should handle different locales', () => {
102+
const date = new Date('2024-01-15T12:00:00Z');
103+
104+
const result = constructLocalDateString('Europe/Paris', 'de-DE', date);
105+
106+
expect(result).toEqual('15.01.2024');
107+
});
108+
109+
it('should accept timestamp as date parameter', () => {
110+
jest
111+
.spyOn(Date, 'now')
112+
.mockReturnValue(new Date('2025-12-31T23:59:59Z').getTime());
113+
114+
const timestamp = Date.now();
115+
116+
const result = constructLocalDateString(
117+
'Asia/Tokyo',
118+
'ja-JP',
119+
timestamp // epoch timestamp instead of Date object
120+
);
121+
122+
expect(result).toEqual('2026/01/01');
123+
});
124+
125+
it('should use default timezone when not provided', () => {
126+
const date = new Date('2024-01-15T02:00:00Z');
127+
const resultDefault = constructLocalDateString(undefined, 'en-US', date);
128+
129+
const resultEst = constructLocalDateString(
130+
'America/New_York',
131+
'en-US',
132+
date
133+
);
134+
135+
expect(resultDefault).not.toBe(resultEst);
136+
expect(resultDefault).toEqual('01/15/2024');
137+
expect(resultEst).toEqual('01/14/2024');
138+
});
139+
});
140+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import moment from 'moment-timezone';
6+
import { determineLocale } from '@fxa/shared/l10n';
7+
8+
const DEFAULT_LOCALE = 'en';
9+
const DEFAULT_TIMEZONE = 'Etc/UTC';
10+
11+
/**
12+
* Takes a list of emails, returning the primary email as "to" and
13+
* verified, non-primary emails as "cc".
14+
* @param emails
15+
* @returns
16+
*/
17+
export const splitEmails = (
18+
emails: { email: string; isPrimary?: boolean; isVerified?: boolean }[]
19+
): { to: string; cc: string[] } => {
20+
return emails.reduce(
21+
(result: { to: string; cc: string[] }, item) => {
22+
const { email } = item;
23+
24+
if (item.isPrimary) {
25+
result.to = email;
26+
} else if (item.isVerified) {
27+
result.cc.push(email);
28+
}
29+
30+
return result;
31+
},
32+
{ to: '', cc: [] }
33+
);
34+
};
35+
36+
/**
37+
* Construct a localized time string with timezone.
38+
* Returns an object containing the localized time and date strings,
39+
* as well as the acceptLanguage and timeZone used to generate them.
40+
*
41+
* Example output: ['9:41:00 AM (PDT)', 'Monday, Jan 1, 2024']
42+
*
43+
* @param timeZone - IANA timezone (e.g., 'America/Los_Angeles')
44+
* @param acceptLanguage - Accept-Language header value
45+
*/
46+
export const constructLocalTimeAndDateStrings = (
47+
timeZone?: string,
48+
acceptLanguage?: string
49+
): {
50+
acceptLanguage: string;
51+
date: string;
52+
time: string;
53+
timeZone: string;
54+
} => {
55+
moment.tz.setDefault(DEFAULT_TIMEZONE);
56+
57+
const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
58+
moment.locale(locale);
59+
60+
let timeMoment = moment();
61+
if (timeZone) {
62+
timeMoment = timeMoment.tz(timeZone);
63+
}
64+
65+
const time = timeMoment.format('LTS (z)');
66+
const date = timeMoment.format('dddd, ll');
67+
68+
return {
69+
acceptLanguage: locale,
70+
date,
71+
time,
72+
timeZone: timeZone || DEFAULT_TIMEZONE,
73+
};
74+
};
75+
76+
/**
77+
* Construct a localized date string.
78+
*
79+
* @param timeZone - IANA timezone (e.g., 'America/Los_Angeles')
80+
* @param acceptLanguage - Accept-Language header value
81+
* @param date - Date to format (defaults to now)
82+
* @param formatString - Moment.js format string (defaults to 'L' for localized date)
83+
*/
84+
export const constructLocalDateString = (
85+
timeZone?: string,
86+
acceptLanguage?: string,
87+
date?: Date | number,
88+
formatString = 'L'
89+
): string => {
90+
moment.tz.setDefault(DEFAULT_TIMEZONE);
91+
92+
const locale = determineLocale(acceptLanguage) || DEFAULT_LOCALE;
93+
moment.locale(locale);
94+
95+
let time = moment(date);
96+
if (timeZone) {
97+
time = time.tz(timeZone);
98+
}
99+
100+
return time.format(formatString);
101+
};

0 commit comments

Comments
 (0)