Skip to content

Commit fba78c5

Browse files
committed
refactor(core,react): Break out phone auth schemas
1 parent 954d34f commit fba78c5

File tree

5 files changed

+166
-35
lines changed

5 files changed

+166
-35
lines changed

packages/core/src/schemas.test.ts

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { createMockUI } from "~/tests/utils";
33
import {
44
createEmailLinkAuthFormSchema,
55
createForgotPasswordAuthFormSchema,
6-
createPhoneAuthFormSchema,
6+
createPhoneAuthNumberFormSchema,
7+
createPhoneAuthVerifyFormSchema,
78
createSignInAuthFormSchema,
89
createSignUpAuthFormSchema,
910
} from "./schemas";
@@ -204,19 +205,19 @@ describe("createEmailLinkAuthFormSchema", () => {
204205
});
205206
});
206207

207-
describe("createPhoneAuthFormSchema", () => {
208-
it("should create a phone auth form schema and show missing phone number error", () => {
208+
describe("createPhoneAuthNumberFormSchema", () => {
209+
it("should create a phone auth number form schema and show missing phone number error", () => {
209210
const testLocale = registerLocale("test", {
210211
errors: {
211-
missingPhoneNumber: "createPhoneAuthFormSchema + missingPhoneNumber",
212+
missingPhoneNumber: "createPhoneAuthNumberFormSchema + missingPhoneNumber",
212213
},
213214
});
214215

215216
const mockUI = createMockUI({
216217
locale: testLocale,
217218
});
218219

219-
const schema = createPhoneAuthFormSchema(mockUI);
220+
const schema = createPhoneAuthNumberFormSchema(mockUI);
220221

221222
// Cause the schema to fail...
222223
// TODO(ehesp): If no value is provided, the schema error is just "Required" - should this also be translated?
@@ -227,59 +228,84 @@ describe("createPhoneAuthFormSchema", () => {
227228
expect(result.success).toBe(false);
228229
expect(result.error).toBeDefined();
229230

230-
expect(result.error?.issues[0]?.message).toBe("createPhoneAuthFormSchema + missingPhoneNumber");
231+
expect(result.error?.issues[0]?.message).toBe("createPhoneAuthNumberFormSchema + missingPhoneNumber");
231232
});
232233

233-
it("should create a phone auth form schema and show an error if the phone number is too long", () => {
234+
it("should create a phone auth number form schema and show an error if the phone number is too long", () => {
234235
const testLocale = registerLocale("test", {
235236
errors: {
236-
invalidPhoneNumber: "createPhoneAuthFormSchema + invalidPhoneNumber",
237+
invalidPhoneNumber: "createPhoneAuthNumberFormSchema + invalidPhoneNumber",
237238
},
238239
});
239240

240241
const mockUI = createMockUI({
241242
locale: testLocale,
242243
});
243244

244-
const schema = createPhoneAuthFormSchema(mockUI);
245+
const schema = createPhoneAuthNumberFormSchema(mockUI);
245246

246247
// Cause the schema to fail...
247248
// TODO(ehesp): If no value is provided, the schema error is just "Required" - should this also be translated?
248249
const result = schema.safeParse({
249250
phoneNumber: "12345678901",
250-
verificationCode: "123",
251-
recaptchaVerifier: null,
252251
});
253252

254253
expect(result.success).toBe(false);
255254
expect(result.error).toBeDefined();
256255

257-
expect(result.error?.issues[0]?.message).toBe("createPhoneAuthFormSchema + invalidPhoneNumber");
256+
expect(result.error?.issues[0]?.message).toBe("createPhoneAuthNumberFormSchema + invalidPhoneNumber");
257+
});
258+
});
259+
260+
describe("createPhoneAuthVerifyFormSchema", () => {
261+
it("should create a phone auth verify form schema and show missing verification ID error", () => {
262+
const testLocale = registerLocale("test", {
263+
errors: {
264+
missingVerificationId: "createPhoneAuthVerifyFormSchema + missingVerificationId",
265+
},
266+
});
267+
268+
const mockUI = createMockUI({
269+
locale: testLocale,
270+
});
271+
272+
const schema = createPhoneAuthVerifyFormSchema(mockUI);
273+
274+
const result = schema.safeParse({
275+
verificationId: "",
276+
verificationCode: "123456",
277+
});
278+
279+
expect(result.success).toBe(false);
280+
expect(result.error).toBeDefined();
281+
282+
expect(result.error?.issues[0]?.message).toBe("createPhoneAuthVerifyFormSchema + missingVerificationId");
258283
});
259284

260-
it("should create a phone auth form schema and show an error if the verification code is too short", () => {
285+
it("should create a phone auth verify form schema and show an error if the verification code is too short", () => {
261286
const testLocale = registerLocale("test", {
262287
errors: {
263-
invalidVerificationCode: "createPhoneAuthFormSchema + invalidVerificationCode",
288+
invalidVerificationCode: "createPhoneAuthVerifyFormSchema + invalidVerificationCode",
264289
},
265290
});
266291

267292
const mockUI = createMockUI({
268293
locale: testLocale,
269294
});
270295

271-
const schema = createPhoneAuthFormSchema(mockUI);
296+
const schema = createPhoneAuthVerifyFormSchema(mockUI);
272297

273298
const result = schema.safeParse({
274-
phoneNumber: "1234567890",
299+
verificationId: "test-verification-id",
275300
verificationCode: "123",
276-
recaptchaVerifier: {} as RecaptchaVerifier, // Workaround for RecaptchaVerifier failing with Node env.
277301
});
278302

279303
expect(result.success).toBe(false);
280304
expect(result.error).toBeDefined();
281305
expect(
282-
result.error?.issues.some((issue) => issue.message === "createPhoneAuthFormSchema + invalidVerificationCode")
306+
result.error?.issues.some(
307+
(issue) => issue.message === "createPhoneAuthVerifyFormSchema + invalidVerificationCode"
308+
)
283309
).toBe(true);
284310
});
285311
});

packages/core/src/schemas.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616

1717
import * as z from "zod";
18-
import { RecaptchaVerifier } from "firebase/auth";
1918
import { getTranslation } from "./translations";
2019
import { FirebaseUIConfiguration } from "./config";
2120
import { hasBehavior } from "./behaviors";
@@ -56,21 +55,27 @@ export function createEmailLinkAuthFormSchema(ui: FirebaseUIConfiguration) {
5655
});
5756
}
5857

59-
export function createPhoneAuthFormSchema(ui: FirebaseUIConfiguration) {
58+
export function createPhoneAuthNumberFormSchema(ui: FirebaseUIConfiguration) {
6059
return z.object({
6160
phoneNumber: z
6261
.string()
6362
.min(1, getTranslation(ui, "errors", "missingPhoneNumber"))
6463
.max(10, getTranslation(ui, "errors", "invalidPhoneNumber")),
64+
});
65+
}
66+
67+
export function createPhoneAuthVerifyFormSchema(ui: FirebaseUIConfiguration) {
68+
return z.object({
69+
verificationId: z.string().min(1, getTranslation(ui, "errors", "missingVerificationId")),
6570
verificationCode: z.string().refine((val) => !val || val.length >= 6, {
6671
error: getTranslation(ui, "errors", "invalidVerificationCode"),
6772
}),
68-
recaptchaVerifier: z.instanceof(RecaptchaVerifier),
6973
});
7074
}
7175

7276
export type SignInAuthFormSchema = z.infer<ReturnType<typeof createSignInAuthFormSchema>>;
7377
export type SignUpAuthFormSchema = z.infer<ReturnType<typeof createSignUpAuthFormSchema>>;
7478
export type ForgotPasswordAuthFormSchema = z.infer<ReturnType<typeof createForgotPasswordAuthFormSchema>>;
7579
export type EmailLinkAuthFormSchema = z.infer<ReturnType<typeof createEmailLinkAuthFormSchema>>;
76-
export type PhoneAuthFormSchema = z.infer<ReturnType<typeof createPhoneAuthFormSchema>>;
80+
export type PhoneAuthNumberFormSchema = z.infer<ReturnType<typeof createPhoneAuthNumberFormSchema>>;
81+
export type PhoneAuthVerifyFormSchema = z.infer<ReturnType<typeof createPhoneAuthVerifyFormSchema>>;

packages/react/src/auth/forms/phone-auth-form.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from "@firebase-ui/core";
2828
import { RecaptchaVerifier, UserCredential } from "firebase/auth";
2929
import { useCallback, useRef, useState } from "react";
30-
import { usePhoneAuthFormSchema, useRecaptchaVerifier, useUI } from "~/hooks";
30+
import { usePhoneAuthNumberFormSchema, usePhoneAuthVerifyFormSchema, useRecaptchaVerifier, useUI } from "~/hooks";
3131
import { form } from "~/components/form";
3232
import { Policies } from "~/components/policies";
3333
import { CountrySelector } from "~/components/country-selector";
@@ -51,7 +51,7 @@ type UsePhoneNumberForm = {
5151

5252
export function usePhoneNumberForm({ recaptchaVerifier, onSuccess, formatPhoneNumber }: UsePhoneNumberForm) {
5353
const action = usePhoneNumberFormAction();
54-
const schema = usePhoneAuthFormSchema().pick({ phoneNumber: true });
54+
const schema = usePhoneAuthNumberFormSchema();
5555

5656
return form.useAppForm({
5757
defaultValues: {
@@ -146,19 +146,20 @@ type UseVerifyPhoneNumberForm = {
146146
};
147147

148148
export function useVerifyPhoneNumberForm({ verificationId, onSuccess }: UseVerifyPhoneNumberForm) {
149-
const schema = usePhoneAuthFormSchema().pick({ verificationCode: true });
149+
const schema = usePhoneAuthVerifyFormSchema();
150150
const action = useVerifyPhoneNumberFormAction();
151151

152152
return form.useAppForm({
153153
defaultValues: {
154+
verificationId,
154155
verificationCode: "",
155156
},
156157
validators: {
157158
onSubmit: schema,
158159
onBlur: schema,
159160
onSubmitAsync: async ({ value }) => {
160161
try {
161-
const credential = await action({ verificationId, verificationCode: value.verificationCode });
162+
const credential = await action(value);
162163
return onSuccess(credential);
163164
} catch (error) {
164165
return error instanceof FirebaseUIError ? error.message : String(error);

packages/react/src/hooks.test.tsx

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
useSignUpAuthFormSchema,
2323
useForgotPasswordAuthFormSchema,
2424
useEmailLinkAuthFormSchema,
25-
usePhoneAuthFormSchema,
25+
usePhoneAuthNumberFormSchema,
26+
usePhoneAuthVerifyFormSchema,
2627
} from "./hooks";
2728
import { createFirebaseUIProvider, createMockUI } from "~/tests/utils";
2829
import { registerLocale, enUs } from "@firebase-ui/translations";
@@ -508,7 +509,7 @@ describe("useEmailLinkAuthFormSchema", () => {
508509
});
509510
});
510511

511-
describe("usePhoneAuthFormSchema", () => {
512+
describe("usePhoneAuthNumberFormSchema", () => {
512513
beforeEach(() => {
513514
vi.clearAllMocks();
514515
cleanup();
@@ -517,7 +518,7 @@ describe("usePhoneAuthFormSchema", () => {
517518
it("returns schema with default English error messages", () => {
518519
const mockUI = createMockUI();
519520

520-
const { result } = renderHook(() => usePhoneAuthFormSchema(), {
521+
const { result } = renderHook(() => usePhoneAuthNumberFormSchema(), {
521522
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
522523
});
523524

@@ -540,7 +541,7 @@ describe("usePhoneAuthFormSchema", () => {
540541
const customLocale = registerLocale("es-ES", customTranslations);
541542
const mockUI = createMockUI({ locale: customLocale });
542543

543-
const { result } = renderHook(() => usePhoneAuthFormSchema(), {
544+
const { result } = renderHook(() => usePhoneAuthNumberFormSchema(), {
544545
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
545546
});
546547

@@ -556,7 +557,7 @@ describe("usePhoneAuthFormSchema", () => {
556557
it("returns stable reference when UI hasn't changed", () => {
557558
const mockUI = createMockUI();
558559

559-
const { result, rerender } = renderHook(() => usePhoneAuthFormSchema(), {
560+
const { result, rerender } = renderHook(() => usePhoneAuthNumberFormSchema(), {
560561
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
561562
});
562563

@@ -570,7 +571,7 @@ describe("usePhoneAuthFormSchema", () => {
570571
it("returns new schema when locale changes", () => {
571572
const mockUI = createMockUI();
572573

573-
const { result, rerender } = renderHook(() => usePhoneAuthFormSchema(), {
574+
const { result, rerender } = renderHook(() => usePhoneAuthNumberFormSchema(), {
574575
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
575576
});
576577

@@ -599,3 +600,95 @@ describe("usePhoneAuthFormSchema", () => {
599600
}
600601
});
601602
});
603+
604+
describe("usePhoneAuthVerifyFormSchema", () => {
605+
beforeEach(() => {
606+
vi.clearAllMocks();
607+
cleanup();
608+
});
609+
610+
it("returns schema with default English error messages", () => {
611+
const mockUI = createMockUI();
612+
613+
const { result } = renderHook(() => usePhoneAuthVerifyFormSchema(), {
614+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
615+
});
616+
617+
const schema = result.current;
618+
619+
const verifyResult = schema.safeParse({ verificationId: "test-id", verificationCode: "123" });
620+
expect(verifyResult.success).toBe(false);
621+
if (!verifyResult.success) {
622+
expect(verifyResult.error.issues[0]!.message).toBe(enUs.translations.errors!.invalidVerificationCode);
623+
}
624+
});
625+
626+
it("returns schema with custom error messages when locale changes", () => {
627+
const customTranslations = {
628+
errors: {
629+
invalidVerificationCode: "Por favor ingresa un código de verificación válido",
630+
},
631+
};
632+
633+
const customLocale = registerLocale("es-ES", customTranslations);
634+
const mockUI = createMockUI({ locale: customLocale });
635+
636+
const { result } = renderHook(() => usePhoneAuthVerifyFormSchema(), {
637+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
638+
});
639+
640+
const schema = result.current;
641+
642+
const verifyResult = schema.safeParse({ verificationId: "test-id", verificationCode: "123" });
643+
expect(verifyResult.success).toBe(false);
644+
if (!verifyResult.success) {
645+
expect(verifyResult.error.issues[0]!.message).toBe("Por favor ingresa un código de verificación válido");
646+
}
647+
});
648+
649+
it("returns stable reference when UI hasn't changed", () => {
650+
const mockUI = createMockUI();
651+
652+
const { result, rerender } = renderHook(() => usePhoneAuthVerifyFormSchema(), {
653+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
654+
});
655+
656+
const initialSchema = result.current;
657+
658+
rerender();
659+
660+
expect(result.current).toBe(initialSchema);
661+
});
662+
663+
it("returns new schema when locale changes", () => {
664+
const mockUI = createMockUI();
665+
666+
const { result, rerender } = renderHook(() => usePhoneAuthVerifyFormSchema(), {
667+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
668+
});
669+
670+
const initialSchema = result.current;
671+
672+
const customTranslations = {
673+
errors: {
674+
invalidVerificationCode: "Custom verification error",
675+
},
676+
};
677+
const customLocale = registerLocale("fr-FR", customTranslations);
678+
679+
act(() => {
680+
mockUI.setKey("locale", customLocale);
681+
});
682+
683+
rerender();
684+
685+
expect(result.current).not.toBe(initialSchema);
686+
687+
const verifyResult = result.current.safeParse({ verificationId: "test-id", verificationCode: "123" });
688+
expect(verifyResult.success).toBe(false);
689+
690+
if (!verifyResult.success) {
691+
expect(verifyResult.error.issues[0]!.message).toBe("Custom verification error");
692+
}
693+
});
694+
});

packages/react/src/hooks.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { useContext, useMemo, useEffect } from "react";
1818
import {
1919
createEmailLinkAuthFormSchema,
2020
createForgotPasswordAuthFormSchema,
21-
createPhoneAuthFormSchema,
21+
createPhoneAuthNumberFormSchema,
22+
createPhoneAuthVerifyFormSchema,
2223
createSignInAuthFormSchema,
2324
createSignUpAuthFormSchema,
2425
getBehavior,
@@ -61,9 +62,14 @@ export function useEmailLinkAuthFormSchema() {
6162
return useMemo(() => createEmailLinkAuthFormSchema(ui), [ui]);
6263
}
6364

64-
export function usePhoneAuthFormSchema() {
65+
export function usePhoneAuthNumberFormSchema() {
6566
const ui = useUI();
66-
return useMemo(() => createPhoneAuthFormSchema(ui), [ui]);
67+
return useMemo(() => createPhoneAuthNumberFormSchema(ui), [ui]);
68+
}
69+
70+
export function usePhoneAuthVerifyFormSchema() {
71+
const ui = useUI();
72+
return useMemo(() => createPhoneAuthVerifyFormSchema(ui), [ui]);
6773
}
6874

6975
export function useRecaptchaVerifier(ref: React.RefObject<HTMLDivElement | null>) {

0 commit comments

Comments
 (0)