Skip to content

Commit d011a8d

Browse files
authored
Merge pull request #19900 from mozilla/fxa-mailer-polish
polish(accounts-email): A little polishing before building
2 parents 980aa3c + 2ec06f0 commit d011a8d

7 files changed

Lines changed: 182 additions & 203 deletions

File tree

libs/accounts/email-renderer/src/renderer/email-link-builder.spec.ts

Lines changed: 42 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('EmailLinkBuilder', () => {
1010
initiatePasswordResetUrl: 'http://localhost:3030/reset_password',
1111
privacyUrl: 'http://localhost:3030/privacy',
1212
supportUrl: 'http://localhost:3030/support',
13+
accountSettingsUrl: 'http://localhost:3030/settings',
1314
};
1415

1516
let linkBuilder: EmailLinkBuilder;
@@ -18,108 +19,11 @@ describe('EmailLinkBuilder', () => {
1819
linkBuilder = new EmailLinkBuilder(mockConfig);
1920
});
2021

21-
describe('urls getter', () => {
22-
it('should return configured URLs', () => {
23-
const urls = linkBuilder.urls;
24-
25-
expect(urls.initiatePasswordReset).toBe(
26-
'http://localhost:3030/reset_password'
27-
);
28-
expect(urls.privacy).toBe('http://localhost:3030/privacy');
29-
expect(urls.support).toBe('http://localhost:3030/support');
30-
});
31-
});
32-
33-
describe('getCampaign', () => {
34-
it('should return campaign with prefix for valid template', () => {
35-
const templateName = 'recovery';
36-
37-
const campaign = linkBuilder.getCampaign(templateName);
38-
39-
expect(campaign).toBe('fx-forgot-password');
40-
});
41-
42-
it('should return empty string for unknown template', () => {
43-
const templateName = 'unknownTemplate';
44-
45-
const campaign = linkBuilder.getCampaign(templateName);
46-
47-
expect(campaign).toBe('');
48-
});
49-
});
50-
51-
describe('getContent', () => {
52-
it('should return content for valid template', () => {
53-
const templateName = 'recovery';
54-
55-
const content = linkBuilder.getContent(templateName);
56-
57-
expect(content).toBe('reset-password');
58-
});
59-
60-
it('should return empty string for unknown template', () => {
61-
const templateName = 'unknownTemplate';
62-
63-
const content = linkBuilder.getContent(templateName);
64-
65-
expect(content).toBe('');
66-
});
67-
});
68-
69-
describe('addUTMParams', () => {
70-
it('should add UTM parameters when metrics enabled', () => {
71-
const link = new URL('http://localhost:3030/some-page');
72-
const templateName = 'recovery';
73-
74-
linkBuilder.addUTMParams(link, templateName);
75-
76-
expect(link.searchParams.get('utm_medium')).toBe('email');
77-
expect(link.searchParams.get('utm_campaign')).toBe('fx-forgot-password');
78-
expect(link.searchParams.get('utm_content')).toBe('fx-reset-password');
79-
});
80-
81-
it('should use custom content when provided', () => {
82-
const link = new URL('http://localhost:3030/some-page');
83-
const templateName = 'recovery';
84-
const customContent = 'custom-content';
85-
86-
linkBuilder.addUTMParams(link, templateName, customContent);
87-
88-
expect(link.searchParams.get('utm_content')).toBe('fx-custom-content');
89-
});
90-
91-
it('should not add UTM parameters when metrics disabled', () => {
92-
const disabledLinkBuilder = new EmailLinkBuilder({
93-
...mockConfig,
94-
metricsEnabled: false,
95-
});
96-
const link = new URL('http://localhost:3030/some-page');
97-
const templateName = 'recovery';
98-
99-
disabledLinkBuilder.addUTMParams(link, templateName);
100-
101-
expect(link.searchParams.get('utm_medium')).toBeNull();
102-
expect(link.searchParams.get('utm_campaign')).toBeNull();
103-
expect(link.searchParams.get('utm_content')).toBeNull();
104-
});
105-
106-
it('should not override existing utm_campaign', () => {
107-
const link = new URL(
108-
'http://localhost:3030/some-page?utm_campaign=existing'
109-
);
110-
const templateName = 'recovery';
111-
112-
linkBuilder.addUTMParams(link, templateName);
113-
114-
expect(link.searchParams.get('utm_campaign')).toBe('existing');
115-
});
116-
});
117-
11822
describe('buildCommonLinks', () => {
11923
it('should build privacy and support links with UTM params', () => {
12024
const templateName = 'recovery';
12125

122-
const links = linkBuilder.buildCommonLinks(templateName);
26+
const links = linkBuilder.buildCommonLinks(templateName, true);
12327

12428
expect(links.privacyUrl).toContain('http://localhost:3030/privacy');
12529
expect(links.privacyUrl).toContain('utm_medium=email');
@@ -135,40 +39,52 @@ describe('EmailLinkBuilder', () => {
13539

13640
describe('buildLinkWithQueryParamsAndUTM', () => {
13741
it('should add query params and UTM params to link', () => {
138-
const link = new URL('http://localhost:3030/some-page');
139-
const templateName = 'recovery';
140-
const queryParams = {
141-
uid: '12345',
142-
token: 'abc123',
143-
144-
};
145-
146-
linkBuilder.buildLinkWithQueryParamsAndUTM(
147-
link,
148-
templateName,
149-
queryParams
42+
const link = linkBuilder.buildLinkWithQueryParamsAndUTM(
43+
'http://localhost:3030/some-page',
44+
'recovery',
45+
{
46+
uid: '12345',
47+
token: 'abc123',
48+
49+
},
50+
true
15051
);
15152

152-
expect(link.searchParams.get('uid')).toBe('12345');
153-
expect(link.searchParams.get('token')).toBe('abc123');
154-
expect(link.searchParams.get('email')).toBe('[email protected]');
155-
expect(link.searchParams.get('utm_medium')).toBe('email');
156-
expect(link.searchParams.get('utm_campaign')).toBe('fx-forgot-password');
53+
const url = new URL(link);
54+
expect(url.searchParams.get('uid')).toBe('12345');
55+
expect(url.searchParams.get('token')).toBe('abc123');
56+
expect(url.searchParams.get('email')).toBe('[email protected]');
57+
expect(url.searchParams.get('utm_medium')).toBe('email');
58+
expect(url.searchParams.get('utm_campaign')).toBe('fx-forgot-password');
15759
});
15860

15961
it('should handle empty query params', () => {
160-
const link = new URL('http://localhost:3030/some-page');
16162
const templateName = 'recovery';
16263
const queryParams = {};
16364

164-
linkBuilder.buildLinkWithQueryParamsAndUTM(
165-
link,
65+
const link = linkBuilder.buildLinkWithQueryParamsAndUTM(
66+
'http://localhost:3030/some-page',
16667
templateName,
167-
queryParams
68+
queryParams,
69+
true
16870
);
16971

170-
expect(link.searchParams.get('utm_medium')).toBe('email');
171-
expect(link.searchParams.get('utm_campaign')).toBe('fx-forgot-password');
72+
const url = new URL(link);
73+
expect(url.searchParams.get('utm_medium')).toBe('email');
74+
expect(url.searchParams.get('utm_campaign')).toBe('fx-forgot-password');
75+
});
76+
77+
it('should respect metricsEnabled flag', () => {
78+
const link = linkBuilder.buildLinkWithQueryParamsAndUTM(
79+
'http://localhost:3030/some-page',
80+
'recovery',
81+
{},
82+
false
83+
);
84+
85+
const url = new URL(link);
86+
expect(url.searchParams.get('utm_medium')).toBeNull();
87+
expect(url.searchParams.get('utm_campaign')).toBeNull();
17288
});
17389
});
17490

@@ -179,7 +95,11 @@ describe('EmailLinkBuilder', () => {
17995
18096
};
18197

182-
const link = linkBuilder.buildPasswordChangeRequiredLink(opts);
98+
const link = linkBuilder.buildPasswordChangeRequiredLink(
99+
opts.url,
100+
opts.email,
101+
true
102+
);
183103

184104
expect(link).toContain('http://localhost:3030/reset_password');
185105
expect(link).toContain('email=test%40example.com');

libs/accounts/email-renderer/src/renderer/email-link-builder.ts

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class EmailLinkBuilder {
9292
* Common base URLs used in emails. Most often paired with UTM parameters
9393
* to attach tracking info.
9494
*/
95-
get urls() {
95+
private get urls() {
9696
return {
9797
initiatePasswordReset: this.config.initiatePasswordResetUrl,
9898
privacy: this.config.privacyUrl,
@@ -104,10 +104,18 @@ export class EmailLinkBuilder {
104104
* Adds UTM parameters to the provided link if metrics are enabled.
105105
* @param link - URL object to add parameters to
106106
* @param templateName - Email template name (used to lookup campaign)
107+
* @param metricsEnabled - Inidicates if metrics/tracking is enabled for the user
107108
* @param content - Optional content override (defaults to template's content map value)
108109
*/
109-
addUTMParams(link: URL, templateName: string, content?: string): void {
110-
if (!this.config.metricsEnabled) {
110+
private addUTMParams(
111+
link: URL,
112+
templateName: string,
113+
metricsEnabled: boolean,
114+
content?: string
115+
): void {
116+
// Don't include utm parameters if metrics are disabled. This flag
117+
// comes from the users's account state and must be supplied.
118+
if (!metricsEnabled) {
111119
return;
112120
}
113121

@@ -127,14 +135,15 @@ export class EmailLinkBuilder {
127135
/**
128136
* Build common links with UTM parameters (privacy, support)
129137
* @param templateName
138+
* @param metricsEnabled - Inidicates if metrics/tracking is enabled for the user
130139
* @returns Object containing privacyUrl and supportUrl as strings
131140
*/
132-
buildCommonLinks(templateName: string) {
141+
buildCommonLinks(templateName: string, metricsEnabled: boolean) {
133142
const privacyUrl = new URL(this.urls.privacy);
134143
const supportUrl = new URL(this.urls.support);
135144

136-
this.addUTMParams(privacyUrl, templateName, 'privacy');
137-
this.addUTMParams(supportUrl, templateName, 'support');
145+
this.addUTMParams(privacyUrl, templateName, metricsEnabled, 'privacy');
146+
this.addUTMParams(supportUrl, templateName, metricsEnabled, 'support');
138147

139148
return {
140149
privacyUrl: privacyUrl.toString(),
@@ -145,7 +154,7 @@ export class EmailLinkBuilder {
145154
/**
146155
* Get the UTM campaign name for a template
147156
*/
148-
getCampaign(templateName: string): string {
157+
private getCampaign(templateName: string): string {
149158
const campaign = TEMPLATE_NAME_TO_CAMPAIGN_MAP[templateName];
150159
// should probably have some logging/statsd if campaign is undefined
151160
return campaign ? UTM_PREFIX + campaign : '';
@@ -154,34 +163,69 @@ export class EmailLinkBuilder {
154163
/**
155164
* Get the UTM content name for a template
156165
*/
157-
getContent(templateName: string): string {
166+
private getContent(templateName: string): string {
158167
return TEMPLATE_NAME_TO_CONTENT_MAP[templateName] || '';
159168
}
160169

161170
/**
162171
* Adds query parameters to the provided link; includes UTM parameters if metrics are enabled.
163-
* @param link
164-
* @param templateName
165-
* @param opts
172+
* @param link - Base link string
173+
* @param templateName - Email template name (used to lookup campaign)
174+
* @param opts - Key/value pairs to add as query parameters
175+
* @param metricsEnabled - Inidicates if metrics/tracking is enabled for the user
176+
* @returns Link string with query parameters and UTM parameters (if enabled)
166177
*/
167178
buildLinkWithQueryParamsAndUTM(
168-
link: URL | string,
179+
link: string,
169180
templateName: string,
170-
opts: Record<string, string>
171-
) {
172-
if (typeof link === 'string') {
173-
link = new URL(link);
174-
}
181+
opts: Record<string, string | undefined>,
182+
metricsEnabled: boolean
183+
): string {
184+
const url = new URL(link);
185+
175186
for (const [key, value] of Object.entries(opts)) {
176-
link.searchParams.set(key, value);
187+
if (value !== undefined) {
188+
url.searchParams.set(key, value);
189+
}
177190
}
178-
this.addUTMParams(link, templateName);
191+
this.addUTMParams(url, templateName, metricsEnabled);
192+
return url.toString();
179193
}
180194

181-
buildPasswordChangeRequiredLink(opts: { url: string; email: string }) {
182-
const link = new URL(opts.url);
183-
this.addUTMParams(link, 'passwordResetRequired');
184-
link.searchParams.set('email', opts.email);
195+
/**
196+
* Builds a password change required link with email and UTM parameters.
197+
* @param opts
198+
* @returns Link to for changing user's password
199+
*/
200+
buildPasswordChangeRequiredLink(
201+
url: string,
202+
email: string,
203+
metricsEnabled: boolean
204+
): string {
205+
const link = new URL(url);
206+
this.addUTMParams(link, 'passwordResetRequired', metricsEnabled);
207+
link.searchParams.set('email', email);
185208
return link.toString();
186209
}
210+
211+
buildInitiatePasswordResetLink(
212+
opts: {
213+
uid: string;
214+
token: string;
215+
code: string;
216+
email: string;
217+
service?: string;
218+
redirectTo?: string;
219+
resume?: string;
220+
emailToHashWith?: string;
221+
},
222+
metricsEnabled: boolean
223+
): string {
224+
return this.buildLinkWithQueryParamsAndUTM(
225+
this.urls.initiatePasswordReset,
226+
'recovery',
227+
opts,
228+
metricsEnabled
229+
);
230+
}
187231
}

0 commit comments

Comments
 (0)