Skip to content

Commit c797e14

Browse files
committed
fix(react,shadcn): Add descriptions to MFA flows
1 parent 06c2334 commit c797e14

14 files changed

+271
-71
lines changed

packages/core/src/translations.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ describe("getTranslation", () => {
2424
const translation = getTranslation(mockUI, "errors", "userNotFound");
2525

2626
expect(translation).toBe("test + userNotFound");
27-
expect(_getTranslation).toHaveBeenCalledWith(testLocale, "errors", "userNotFound");
27+
expect(_getTranslation).toHaveBeenCalledWith(testLocale, "errors", "userNotFound", undefined);
28+
});
29+
30+
it("should pass replacements to the underlying getTranslation function", () => {
31+
const testLocale = registerLocale("test", {
32+
messages: {
33+
termsAndPrivacy: "By continuing, you agree to our {tos} and {privacy}.",
34+
},
35+
});
36+
37+
vi.mocked(_getTranslation).mockReturnValue("By continuing, you agree to our Terms of Service and Privacy Policy.");
38+
39+
const mockUI = createMockUI({ locale: testLocale });
40+
const replacements = {
41+
tos: "Terms of Service",
42+
privacy: "Privacy Policy",
43+
};
44+
const translation = getTranslation(mockUI, "messages", "termsAndPrivacy", replacements);
45+
46+
expect(translation).toBe("By continuing, you agree to our Terms of Service and Privacy Policy.");
47+
expect(_getTranslation).toHaveBeenCalledWith(testLocale, "messages", "termsAndPrivacy", replacements);
2848
});
2949
});

packages/core/src/translations.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import {
2121
} from "@invertase/firebaseui-translations";
2222
import { type FirebaseUI } from "./config";
2323

24-
export function getTranslation<T extends TranslationCategory>(ui: FirebaseUI, category: T, key: TranslationKey<T>) {
25-
return _getTranslation(ui.locale, category, key);
24+
export function getTranslation<T extends TranslationCategory>(
25+
ui: FirebaseUI,
26+
category: T,
27+
key: TranslationKey<T>,
28+
replacements?: Record<string, string>
29+
) {
30+
return _getTranslation(ui.locale, category, key, replacements);
2631
}

packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.test.tsx

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
173173
locale: registerLocale("test", {
174174
labels: {
175175
sendCode: "sendCode",
176-
phoneNumber: "phoneNumber",
176+
},
177+
messages: {
178+
mfaSmsAssertionPrompt:
179+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
177180
},
178181
}),
179182
});
@@ -195,8 +198,9 @@ describe("<SmsMultiFactorAssertionForm />", () => {
195198
const form = container.querySelectorAll("form.fui-form");
196199
expect(form.length).toBe(1);
197200

198-
expect(screen.getByRole("textbox", { name: /phoneNumber/i })).toBeInTheDocument();
199-
expect(screen.getByRole("textbox", { name: /phoneNumber/i })).toHaveValue("+1234567890");
201+
expect(
202+
screen.getByText("A verification code will be sent to +1234567890 to complete the authentication process.")
203+
).toBeInTheDocument();
200204

201205
const sendCodeButton = screen.getByRole("button", { name: "sendCode" });
202206
expect(sendCodeButton).toBeInTheDocument();
@@ -208,8 +212,9 @@ describe("<SmsMultiFactorAssertionForm />", () => {
208212
it("should display phone number from hint", () => {
209213
const mockUI = createMockUI({
210214
locale: registerLocale("test", {
211-
labels: {
212-
phoneNumber: "phoneNumber",
215+
messages: {
216+
mfaSmsAssertionPrompt:
217+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
213218
},
214219
}),
215220
});
@@ -228,15 +233,17 @@ describe("<SmsMultiFactorAssertionForm />", () => {
228233
})
229234
);
230235

231-
const phoneInput = screen.getByRole("textbox", { name: /phoneNumber/i });
232-
expect(phoneInput).toHaveValue("+1234567890");
236+
expect(
237+
screen.getByText("A verification code will be sent to +1234567890 to complete the authentication process.")
238+
).toBeInTheDocument();
233239
});
234240

235241
it("should handle missing phone number in hint", () => {
236242
const mockUI = createMockUI({
237243
locale: registerLocale("test", {
238-
labels: {
239-
phoneNumber: "phoneNumber",
244+
messages: {
245+
mfaSmsAssertionPrompt:
246+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
240247
},
241248
}),
242249
});
@@ -254,15 +261,18 @@ describe("<SmsMultiFactorAssertionForm />", () => {
254261
})
255262
);
256263

257-
const phoneInput = screen.getByRole("textbox", { name: /phoneNumber/i });
258-
expect(phoneInput).toHaveValue("");
264+
// When phone number is missing, the placeholder remains because empty string is falsy in the replacement logic
265+
expect(
266+
screen.getByText("A verification code will be sent to {phoneNumber} to complete the authentication process.")
267+
).toBeInTheDocument();
259268
});
260269

261270
it("should accept onSuccess callback prop", () => {
262271
const mockUI = createMockUI({
263272
locale: registerLocale("test", {
264-
labels: {
265-
phoneNumber: "phoneNumber",
273+
messages: {
274+
mfaSmsAssertionPrompt:
275+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
266276
},
267277
}),
268278
});
@@ -290,10 +300,13 @@ describe("<SmsMultiFactorAssertionForm />", () => {
290300
locale: registerLocale("test", {
291301
labels: {
292302
sendCode: "sendCode",
293-
phoneNumber: "phoneNumber",
294303
verificationCode: "verificationCode",
295304
verifyCode: "verifyCode",
296305
},
306+
messages: {
307+
mfaSmsAssertionPrompt:
308+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
309+
},
297310
}),
298311
});
299312

@@ -304,9 +317,7 @@ describe("<SmsMultiFactorAssertionForm />", () => {
304317
enrollmentTime: "2023-01-01T00:00:00Z",
305318
};
306319

307-
// First step returns a verificationId
308320
vi.mocked(verifyPhoneNumber).mockResolvedValue("vid-123");
309-
// Second step returns a credential from MFA assertion
310321
const mockCredential = { user: { uid: "sms-cred-user" } } as any;
311322
vi.mocked(signInWithMultiFactorAssertion).mockResolvedValue(mockCredential);
312323

packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ import {
1414
verifyPhoneNumber,
1515
} from "@invertase/firebaseui-core";
1616
import { form } from "~/components/form";
17-
import {
18-
useMultiFactorPhoneAuthAssertionFormSchema,
19-
useMultiFactorPhoneAuthVerifyFormSchema,
20-
useRecaptchaVerifier,
21-
useUI,
22-
} from "~/hooks";
17+
import { useMultiFactorPhoneAuthVerifyFormSchema, useRecaptchaVerifier, useUI } from "~/hooks";
2318

2419
type PhoneMultiFactorInfo = MultiFactorInfo & {
2520
phoneNumber?: string;
@@ -48,14 +43,9 @@ export function useSmsMultiFactorAssertionPhoneForm({
4843
onSuccess,
4944
}: UseSmsMultiFactorAssertionPhoneForm) {
5045
const action = useSmsMultiFactorAssertionPhoneFormAction();
51-
const schema = useMultiFactorPhoneAuthAssertionFormSchema();
5246

5347
return form.useAppForm({
54-
defaultValues: {
55-
phoneNumber: (hint as PhoneMultiFactorInfo).phoneNumber || "",
56-
},
5748
validators: {
58-
onBlur: schema,
5949
onSubmitAsync: async () => {
6050
try {
6151
const verificationId = await action({ hint, recaptchaVerifier });
@@ -94,16 +84,13 @@ function SmsMultiFactorAssertionPhoneForm(props: SmsMultiFactorAssertionPhoneFor
9484
>
9585
<form.AppForm>
9686
<fieldset>
97-
<form.AppField name="phoneNumber">
98-
{(field) => (
99-
<field.Input
100-
label={getTranslation(ui, "labels", "phoneNumber")}
101-
type="tel"
102-
disabled
103-
value={(props.hint as PhoneMultiFactorInfo).phoneNumber || ""}
104-
/>
105-
)}
106-
</form.AppField>
87+
<label>
88+
<div data-input-description>
89+
{getTranslation(ui, "messages", "mfaSmsAssertionPrompt", {
90+
phoneNumber: (props.hint as PhoneMultiFactorInfo).phoneNumber || "",
91+
})}
92+
</div>
93+
</label>
10794
</fieldset>
10895
<fieldset>
10996
<div className="fui-recaptcha-container" ref={recaptchaContainerRef} />
@@ -185,7 +172,13 @@ function SmsMultiFactorAssertionVerifyForm(props: SmsMultiFactorAssertionVerifyF
185172
<form.AppForm>
186173
<fieldset>
187174
<form.AppField name="verificationCode">
188-
{(field) => <field.Input label={getTranslation(ui, "labels", "verificationCode")} type="text" />}
175+
{(field) => (
176+
<field.Input
177+
label={getTranslation(ui, "labels", "verificationCode")}
178+
type="text"
179+
description={getTranslation(ui, "prompts", "smsVerificationPrompt")}
180+
/>
181+
)}
189182
</form.AppField>
190183
</fieldset>
191184
<fieldset>

packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ describe("<MultiFactorEnrollmentVerifyPhoneNumberForm />", () => {
218218
verifyCode: "verifyCode",
219219
},
220220
prompts: {
221-
mfaSmsEnrollmentVerificationPrompt: "mfaSmsEnrollmentVerificationPrompt",
221+
smsVerificationPrompt: "smsVerificationPrompt",
222222
},
223223
}),
224224
});
@@ -243,7 +243,7 @@ describe("<MultiFactorEnrollmentVerifyPhoneNumberForm />", () => {
243243

244244
const description = container.querySelector("[data-input-description]");
245245
expect(description).toBeInTheDocument();
246-
expect(description).toHaveTextContent("mfaSmsEnrollmentVerificationPrompt");
246+
expect(description).toHaveTextContent("smsVerificationPrompt");
247247

248248
const verifyCodeButton = screen.getByRole("button", { name: "verifyCode" });
249249
expect(verifyCodeButton).toBeInTheDocument();

packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export function MultiFactorEnrollmentVerifyPhoneNumberForm(props: MultiFactorEnr
195195
<form.AppField name="verificationCode">
196196
{(field) => (
197197
<field.Input
198-
description={getTranslation(ui, "prompts", "mfaSmsEnrollmentVerificationPrompt")}
198+
description={getTranslation(ui, "prompts", "smsVerificationPrompt")}
199199
label={getTranslation(ui, "labels", "verificationCode")}
200200
type="text"
201201
/>

packages/shadcn/src/components/sms-multi-factor-assertion-form.test.tsx

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ vi.mock("@/components/ui/input-otp", () => ({
2525
}),
2626
}));
2727

28+
vi.mock("@/components/ui/form", async (importOriginal) => {
29+
const mod = await importOriginal<typeof import("@/components/ui/form")>();
30+
return {
31+
...mod,
32+
FormItem: ({ children, ...props }: any) => React.createElement("div", { ...props }, children),
33+
FormLabel: ({ children, ...props }: any) => React.createElement("label", { ...props }, children),
34+
FormDescription: ({ children, ...props }: any) => React.createElement("p", { ...props }, children),
35+
};
36+
});
37+
2838
vi.mock("@invertase/firebaseui-core", async (importOriginal) => {
2939
const mod = await importOriginal<typeof import("@invertase/firebaseui-core")>();
3040
return {
@@ -71,6 +81,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
7181
phoneNumber: "Phone Number",
7282
sendCode: "Send Code",
7383
},
84+
messages: {
85+
mfaSmsAssertionPrompt:
86+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
87+
},
7488
}),
7589
});
7690

@@ -82,7 +96,9 @@ describe("<SmsMultiFactorAssertionForm />", () => {
8296
);
8397

8498
expect(screen.getByText("Phone Number")).toBeInTheDocument();
85-
expect(screen.getByText("+1234567890")).toBeInTheDocument();
99+
expect(
100+
screen.getByText("A verification code will be sent to +1234567890 to complete the authentication process.")
101+
).toBeInTheDocument();
86102
expect(screen.getByRole("button", { name: "Send Code" })).toBeInTheDocument();
87103
});
88104

@@ -106,6 +122,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
106122
verificationCode: "Verification Code",
107123
verifyCode: "Verify Code",
108124
},
125+
messages: {
126+
mfaSmsAssertionPrompt:
127+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
128+
},
109129
}),
110130
});
111131

@@ -149,6 +169,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
149169
verificationCode: "Verification Code",
150170
verifyCode: "Verify Code",
151171
},
172+
messages: {
173+
mfaSmsAssertionPrompt:
174+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
175+
},
152176
}),
153177
});
154178

@@ -193,6 +217,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
193217
phoneNumber: "Phone Number",
194218
sendCode: "Send Code",
195219
},
220+
messages: {
221+
mfaSmsAssertionPrompt:
222+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
223+
},
196224
}),
197225
});
198226

@@ -233,6 +261,10 @@ describe("<SmsMultiFactorAssertionForm />", () => {
233261
verificationCode: "Verification Code",
234262
verifyCode: "Verify Code",
235263
},
264+
messages: {
265+
mfaSmsAssertionPrompt:
266+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
267+
},
236268
}),
237269
});
238270

@@ -258,4 +290,38 @@ describe("<SmsMultiFactorAssertionForm />", () => {
258290
expect(screen.getByText("Error: Verification failed")).toBeInTheDocument();
259291
});
260292
});
293+
294+
it("should handle missing phone number in hint", () => {
295+
const mockHint = {
296+
uid: "test-uid",
297+
factorId: "phone" as const,
298+
displayName: "Test Phone",
299+
enrollmentTime: "2023-01-01T00:00:00.000Z",
300+
};
301+
302+
const mockUI = createMockUI({
303+
locale: registerLocale("test", {
304+
labels: {
305+
phoneNumber: "Phone Number",
306+
sendCode: "Send Code",
307+
},
308+
messages: {
309+
mfaSmsAssertionPrompt:
310+
"A verification code will be sent to {phoneNumber} to complete the authentication process.",
311+
},
312+
}),
313+
});
314+
315+
render(
316+
createFirebaseUIProvider({
317+
children: <SmsMultiFactorAssertionForm hint={mockHint} />,
318+
ui: mockUI,
319+
})
320+
);
321+
322+
// When phone number is missing, the placeholder remains because empty string is falsy in the replacement logic
323+
expect(
324+
screen.getByText("A verification code will be sent to {phoneNumber} to complete the authentication process.")
325+
).toBeInTheDocument();
326+
});
261327
});

0 commit comments

Comments
 (0)