Skip to content

Commit f353d00

Browse files
committed
feat(sms): Add localized body text to SMS
Because: * We want to include a localized message in addition to the code This commit: * Add ftl strings * Pass strings to sms manager for inclusion in sms * Add missing import in integration test * Add tags to accounts libs projects for inclusion in CI integration tests Closes #FXA-11007
1 parent 3e3fe88 commit f353d00

File tree

18 files changed

+325
-119
lines changed

18 files changed

+325
-119
lines changed

libs/accounts/recovery-phone/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ Run `nx build recovery-phone` to build the library.
88

99
## Running unit tests
1010

11-
Run `nx test recovery-phone` to execute the unit tests via [Jest](https://jestjs.io).
11+
Run `nx test-unit recovery-phone` to execute the unit tests via [Jest](https://jestjs.io).
12+
13+
## Running integration tests
14+
15+
Run `nx test-integration recovery-phone` to execute the integration tests via [Jest](https://jestjs.io).

libs/accounts/recovery-phone/src/lib/recovery-phone.manager.in.spec.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
RecoveryPhoneFactory,
1010
} from '@fxa/shared/db/mysql/account';
1111
import { Test } from '@nestjs/testing';
12-
import { RecoveryPhoneFactory } from '@fxa/shared/db/mysql/account';
1312

1413
describe('RecoveryPhoneManager', () => {
1514
let recoveryPhoneManager: RecoveryPhoneManager;
@@ -205,20 +204,26 @@ describe('RecoveryPhoneManager', () => {
205204
mockLookUpData
206205
);
207206

208-
const expectedData = JSON.stringify({
209-
createdAt: 1739227529776,
210-
phoneNumber,
211-
isSetup,
212-
lookupData: JSON.stringify(mockLookUpData),
213-
});
214207
const redisKey = `sms-attempt:${uid.toString('hex')}:${code}`;
215208

216209
expect(mockRedis.set).toHaveBeenCalledWith(
217210
redisKey,
218-
expectedData,
211+
expect.any(String),
219212
'EX',
220213
600
221214
);
215+
216+
const expectedData = expect.objectContaining({
217+
createdAt: expect.any(Number),
218+
phoneNumber,
219+
isSetup,
220+
lookupData: JSON.stringify(mockLookUpData),
221+
});
222+
223+
const storedData = mockRedis.set.mock.calls[0][1];
224+
expect(() => JSON.parse(storedData)).not.toThrow();
225+
const parsedData = JSON.parse(mockRedis.set.mock.calls[0][1]);
226+
expect(parsedData).toEqual(expectedData);
222227
});
223228

224229
it('should return null if no unconfirmed phone number data is found in Redis', async () => {

libs/accounts/recovery-phone/src/lib/recovery-phone.service.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,31 @@ describe('RecoveryPhoneService', () => {
130130
expect(mockRecoveryPhoneManager.getAllUnconfirmed).toBeCalledWith(uid);
131131
});
132132

133+
it('handles message template when provided to setup phone number', async () => {
134+
mockOtpManager.generateCode.mockReturnValue(code);
135+
const getFormattedMessage = jest.fn().mockResolvedValue('message');
136+
137+
const result = await service.setupPhoneNumber(
138+
uid,
139+
phoneNumber,
140+
getFormattedMessage
141+
);
142+
143+
expect(result).toBeTruthy();
144+
expect(mockOtpManager.generateCode).toBeCalled();
145+
expect(mockSmsManager.sendSMS).toBeCalledWith({
146+
to: phoneNumber,
147+
body: 'message',
148+
});
149+
expect(mockRecoveryPhoneManager.storeUnconfirmed).toBeCalledWith(
150+
uid,
151+
code,
152+
phoneNumber,
153+
true
154+
);
155+
expect(mockRecoveryPhoneManager.getAllUnconfirmed).toBeCalledWith(uid);
156+
});
157+
133158
it('Will reject a phone number that is not part of launch', async () => {
134159
const to = '+16005551234';
135160
await expect(service.setupPhoneNumber(uid, to)).rejects.toEqual(

libs/accounts/recovery-phone/src/lib/recovery-phone.service.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ export class RecoveryPhoneService {
5959
* by sending the phone number provided an OTP code to verify.
6060
* @param uid The account id
6161
* @param phoneNumber The phone number to register
62+
* @param localizedMessageBody Optional localized message body
6263
* @returns True if code was sent and stored
6364
*/
64-
public async setupPhoneNumber(uid: string, phoneNumber: string) {
65+
public async setupPhoneNumber(
66+
uid: string,
67+
phoneNumber: string,
68+
getFormattedMessage?: (code: string) => Promise<string>
69+
) {
6570
if (!this.config.enabled) {
6671
throw new RecoveryPhoneNotEnabled();
6772
}
@@ -88,21 +93,26 @@ export class RecoveryPhoneService {
8893
}
8994

9095
const code = await this.otpCode.generateCode();
91-
const msg = await this.smsManager.sendSMS({
92-
to: phoneNumber,
93-
body: code,
94-
});
9596

96-
if (!this.isSuccessfulSmsSend(msg)) {
97-
return false;
98-
}
9997
await this.recoveryPhoneManager.storeUnconfirmed(
10098
uid,
10199
code,
102100
phoneNumber,
103101
true
104102
);
105-
return true;
103+
104+
const formattedSMSbody = getFormattedMessage
105+
? await getFormattedMessage(code)
106+
: undefined;
107+
108+
const smsBody = formattedSMSbody || `${code}`;
109+
110+
const msg = await this.smsManager.sendSMS({
111+
to: phoneNumber,
112+
body: smsBody,
113+
});
114+
115+
return this.isSuccessfulSmsSend(msg);
106116
}
107117

108118
/**
@@ -294,9 +304,13 @@ export class RecoveryPhoneService {
294304
/**
295305
* Sends an totp code to a user
296306
* @param uid Account id
307+
* @param getFormattedMessage Optional template function to format the message
297308
* @returns True if message didn't fail to send.
298309
*/
299-
public async sendCode(uid: string) {
310+
public async sendCode(
311+
uid: string,
312+
getFormattedMessage?: (code: string) => Promise<string>
313+
) {
300314
if (!this.config.enabled) {
301315
throw new RecoveryPhoneNotEnabled();
302316
}
@@ -310,9 +324,16 @@ export class RecoveryPhoneService {
310324
phoneNumber,
311325
false
312326
);
327+
328+
const formattedSMSbody = getFormattedMessage
329+
? await getFormattedMessage(code)
330+
: undefined;
331+
332+
const smsBody = formattedSMSbody || `${code}`;
333+
313334
const msg = await this.smsManager.sendSMS({
314335
to: phoneNumber,
315-
body: `${code}`, // TODO: Other text or translation around code?
336+
body: smsBody,
316337
});
317338

318339
return this.isSuccessfulSmsSend(msg);

packages/functional-tests/lib/email.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export enum EmailType {
5959
verificationReminderFinal,
6060
cadReminderFirst,
6161
cadReminderSecond,
62+
postAddRecoveryPhone,
63+
postChangeRecoveryPhone,
64+
postRemoveRecoveryPhone,
65+
postSigninRecoveryPhone,
6266
}
6367

6468
export enum EmailHeader {

packages/functional-tests/tests/settings/recoveryPhone.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ test.describe('severity-1 #smoke', () => {
103103
await settings.disconnectTotp();
104104
});
105105

106-
test('can sign-in settings with recovery phone', async ({
106+
test('can sign-in to settings with recovery phone', async ({
107107
target,
108108
pages: {
109109
page,

packages/fxa-auth-server/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2196,7 +2196,7 @@ const convictConf = convict({
21962196
format: Array,
21972197
},
21982198
maxMessageLength: {
2199-
default: 60,
2199+
default: 160,
22002200
doc: 'Max allows sms message length',
22012201
env: 'RECOVERY_PHONE__SMS__MAX_MESSAGE_LENGTH',
22022202
format: Number,

packages/fxa-auth-server/lib/l10n/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import { FluentBundle, FluentResource } from '@fluent/bundle';
77
import { determineLocale, parseAcceptLanguage } from '@fxa/shared/l10n';
88
import { ILocalizerBindings } from './interfaces/ILocalizerBindings';
99

10+
/**
11+
* Represents a Fluent (FTL) message
12+
* @param id - unique identifier for the message
13+
* @param message - a fallback message in case the localized string cannot be found
14+
* @param vars - optional arguments to be interpolated into the localized string
15+
*/
1016
export interface FtlIdMsg {
1117
id: string;
1218
message: string;
19+
vars?: Record<string, string>;
1320
}
1421

1522
interface LocalizedStrings {
@@ -118,11 +125,11 @@ class Localizer {
118125

119126
const localizedFtlIdMsgs = await Promise.all(
120127
ftlIdMsgs.map(async (ftlIdMsg) => {
121-
const { id, message } = ftlIdMsg;
128+
const { id, message, vars } = ftlIdMsg;
122129
let localizedMessage;
123130
try {
124-
localizedMessage = (await l10n.formatValue(id, message)) || message;
125-
} catch {
131+
localizedMessage = (await l10n.formatValue(id, vars)) || message;
132+
} catch (e) {
126133
localizedMessage = message;
127134
}
128135
return Promise.resolve({

packages/fxa-auth-server/lib/l10n/server.ftl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,15 @@
22

33
session-verify-send-push-title-2 = Logging in to your { -product-mozilla-account }?
44
session-verify-send-push-body-2 = Click here to confirm it’s you
5+
6+
# Message sent by SMS with limited character length, please test translation with the messaging segment calculator
7+
# https://twiliodeved.github.io/message-segment-calculator/
8+
# Messages should be limited to one segment
9+
# $code - 6 digit code used to verify phone ownership when registering a recovery phone
10+
recovery-phone-setup-sms-body = { $code } is your { -brand-mozilla } verification code. Expires in 5 minutes.
11+
12+
# Message sent by SMS with limited character length, please test translation with the messaging segment calculator
13+
# https://twiliodeved.github.io/message-segment-calculator/
14+
# Messages should be limited to one segment
15+
# $code - 6 digit code used to sign in with a recovery phone as backup for two-step authentication
16+
recovery-phone-signin-sms-body = { $code } is your { -brand-mozilla } recovery code. Expires in 5 minutes.

0 commit comments

Comments
 (0)