From b774d89864c2616720b76527109e29b6e7215551 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Thu, 2 Oct 2025 16:10:08 +0100 Subject: [PATCH 1/2] feat(core): Add requireDisplayName behavior --- packages/core/src/auth.test.ts | 129 +++++++++++++++++- packages/core/src/auth.ts | 27 +++- packages/core/src/behaviors/index.test.ts | 35 ++++- packages/core/src/behaviors/index.ts | 13 +- .../behaviors/require-display-name.test.ts | 40 ++++++ .../src/behaviors/require-display-name.ts | 6 + packages/core/src/schemas.test.ts | 96 +++++++++++-- packages/core/src/schemas.ts | 7 + packages/translations/src/locales/en-us.ts | 1 + packages/translations/src/mapping.test.ts | 7 +- packages/translations/src/mapping.ts | 1 + packages/translations/src/types.ts | 1 + 12 files changed, 342 insertions(+), 21 deletions(-) create mode 100644 packages/core/src/behaviors/require-display-name.test.ts create mode 100644 packages/core/src/behaviors/require-display-name.ts diff --git a/packages/core/src/auth.test.ts b/packages/core/src/auth.test.ts index 408c8beb..6cbc145d 100644 --- a/packages/core/src/auth.test.ts +++ b/packages/core/src/auth.test.ts @@ -193,7 +193,11 @@ describe("createUserWithEmailAndPassword", () => { const password = "password123"; const credential = EmailAuthProvider.credential(email, password); - vi.mocked(hasBehavior).mockReturnValue(true); + vi.mocked(hasBehavior).mockImplementation((ui, behavior) => { + if (behavior === "autoUpgradeAnonymousCredential") return true; + if (behavior === "requireDisplayName") return false; + return false; + }); const mockBehavior = vi.fn().mockResolvedValue({ providerId: "password" } as UserCredential); vi.mocked(getBehavior).mockReturnValue(mockBehavior); @@ -215,7 +219,11 @@ describe("createUserWithEmailAndPassword", () => { const password = "password123"; const credential = EmailAuthProvider.credential(email, password); - vi.mocked(hasBehavior).mockReturnValue(true); + vi.mocked(hasBehavior).mockImplementation((ui, behavior) => { + if (behavior === "autoUpgradeAnonymousCredential") return true; + if (behavior === "requireDisplayName") return false; + return false; + }); const mockBehavior = vi.fn().mockResolvedValue(undefined); vi.mocked(getBehavior).mockReturnValue(mockBehavior); @@ -249,6 +257,123 @@ describe("createUserWithEmailAndPassword", () => { expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); + + it("should call handleFirebaseError when requireDisplayName behavior is enabled but no displayName provided", async () => { + const mockUI = createMockUI(); + const email = "test@example.com"; + const password = "password123"; + + vi.mocked(hasBehavior).mockImplementation((_, behavior) => { + if (behavior === "requireDisplayName") return true; + if (behavior === "autoUpgradeAnonymousCredential") return false; + return false; + }); + + await createUserWithEmailAndPassword(mockUI, email, password); + + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(_createUserWithEmailAndPassword).not.toHaveBeenCalled(); + expect(handleFirebaseError).toHaveBeenCalled(); + }); + + it("should call requireDisplayName behavior when enabled and displayName provided", async () => { + const mockUI = createMockUI(); + const email = "test@example.com"; + const password = "password123"; + const displayName = "John Doe"; + + const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined); + const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential; + + vi.mocked(hasBehavior).mockImplementation((_, behavior) => { + if (behavior === "requireDisplayName") return true; + if (behavior === "autoUpgradeAnonymousCredential") return false; + return false; + }); + vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior); + vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult); + + const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName); + + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(getBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName); + expect(result).toBe(mockResult); + }); + + it("should call requireDisplayName behavior after autoUpgradeAnonymousCredential when both enabled", async () => { + const mockUI = createMockUI(); + const email = "test@example.com"; + const password = "password123"; + const displayName = "John Doe"; + + const mockAutoUpgradeBehavior = vi.fn().mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential); + const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined); + const credential = EmailAuthProvider.credential(email, password); + + vi.mocked(hasBehavior).mockImplementation((_, behavior) => { + if (behavior === "requireDisplayName") return true; + if (behavior === "autoUpgradeAnonymousCredential") return true; + return false; + }); + + vi.mocked(getBehavior).mockImplementation((_, behavior) => { + if (behavior === "autoUpgradeAnonymousCredential") return mockAutoUpgradeBehavior; + if (behavior === "requireDisplayName") return mockRequireDisplayNameBehavior; + return vi.fn(); + }); + + vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential); + + const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName); + + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); + expect(mockAutoUpgradeBehavior).toHaveBeenCalledWith(mockUI, credential); + expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, { uid: "upgraded-user" }, displayName); + expect(result).toEqual({ providerId: "upgraded", user: { uid: "upgraded-user" } }); + }); + + it("should not call requireDisplayName behavior when not enabled", async () => { + const mockUI = createMockUI(); + const email = "test@example.com"; + const password = "password123"; + const displayName = "John Doe"; + + const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential; + + vi.mocked(hasBehavior).mockReturnValue(false); + vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult); + + const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName); + + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "requireDisplayName"); + expect(result).toBe(mockResult); + }); + + it("should handle requireDisplayName behavior errors", async () => { + const mockUI = createMockUI(); + const email = "test@example.com"; + const password = "password123"; + const displayName = "John Doe"; + + const mockRequireDisplayNameBehavior = vi.fn().mockRejectedValue(new Error("Display name update failed")); + const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential; + + vi.mocked(hasBehavior).mockImplementation((_, behavior) => { + if (behavior === "requireDisplayName") return true; + if (behavior === "autoUpgradeAnonymousCredential") return false; + return false; + }); + vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior); + vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult); + + await createUserWithEmailAndPassword(mockUI, email, password, displayName); + + expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName); + expect(handleFirebaseError).toHaveBeenCalled(); + }); }); describe("signInWithPhoneNumber", () => { diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 22996d4c..c053d35c 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -33,8 +33,10 @@ import { AuthCredential, } from "firebase/auth"; import { FirebaseUIConfiguration } from "./config"; -import { handleFirebaseError } from "./errors"; +import { FirebaseUIError, handleFirebaseError } from "./errors"; import { hasBehavior, getBehavior } from "./behaviors/index"; +import { FirebaseError } from "firebase/app"; +import { getTranslation } from "./translations"; async function handlePendingCredential(ui: FirebaseUIConfiguration, user: UserCredential): Promise { const pendingCredString = window.sessionStorage.getItem("pendingCred"); @@ -63,7 +65,7 @@ export async function signInWithEmailAndPassword( if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) { const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); - + if (result) { return handlePendingCredential(ui, result); } @@ -82,21 +84,35 @@ export async function signInWithEmailAndPassword( export async function createUserWithEmailAndPassword( ui: FirebaseUIConfiguration, email: string, - password: string + password: string, + displayName?: string ): Promise { try { const credential = EmailAuthProvider.credential(email, password); + if (hasBehavior(ui, "requireDisplayName") && !displayName) { + throw new FirebaseError("auth/display-name-required", getTranslation(ui, "errors", "displayNameRequired")); + } + if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) { const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); if (result) { + if (hasBehavior(ui, "requireDisplayName")) { + await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!); + } + return handlePendingCredential(ui, result); } } ui.setState("pending"); const result = await _createUserWithEmailAndPassword(ui.auth, email, password); + + if (hasBehavior(ui, "requireDisplayName")) { + await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!); + } + return handlePendingCredential(ui, result); } catch (error) { handleFirebaseError(ui, error); @@ -223,7 +239,10 @@ export async function signInAnonymously(ui: FirebaseUIConfiguration): Promise { +export async function signInWithProvider( + ui: FirebaseUIConfiguration, + provider: AuthProvider +): Promise { try { if (hasBehavior(ui, "autoUpgradeAnonymousProvider")) { const credential = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider); diff --git a/packages/core/src/behaviors/index.test.ts b/packages/core/src/behaviors/index.test.ts index 99a32a7c..dedf3342 100644 --- a/packages/core/src/behaviors/index.test.ts +++ b/packages/core/src/behaviors/index.test.ts @@ -6,16 +6,20 @@ import { getBehavior, hasBehavior, recaptchaVerification, + requireDisplayName, defaultBehaviors, } from "./index"; -// Mock the anonymous-upgrade handlers vi.mock("./anonymous-upgrade", () => ({ autoUpgradeAnonymousCredentialHandler: vi.fn(), autoUpgradeAnonymousProviderHandler: vi.fn(), autoUpgradeAnonymousUserRedirectHandler: vi.fn(), })); +vi.mock("./require-display-name", () => ({ + requireDisplayNameHandler: vi.fn(), +})); + vi.mock("firebase/auth", () => ({ RecaptchaVerifier: vi.fn(), })); @@ -49,6 +53,7 @@ describe("hasBehavior", () => { autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() }, autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() }, recaptchaVerification: { type: "callable" as const, handler: vi.fn() }, + requireDisplayName: { type: "callable" as const, handler: vi.fn() }, } as any, }); @@ -56,6 +61,7 @@ describe("hasBehavior", () => { expect(hasBehavior(mockUI, "autoUpgradeAnonymousCredential")).toBe(true); expect(hasBehavior(mockUI, "autoUpgradeAnonymousProvider")).toBe(true); expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(true); + expect(hasBehavior(mockUI, "requireDisplayName")).toBe(true); }); }); @@ -83,6 +89,7 @@ describe("getBehavior", () => { autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() }, autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() }, recaptchaVerification: { type: "callable" as const, handler: vi.fn() }, + requireDisplayName: { type: "callable" as const, handler: vi.fn() }, }; const ui = createMockUI({ behaviors: mockBehaviors as any }); @@ -93,6 +100,7 @@ describe("getBehavior", () => { ); expect(getBehavior(ui, "autoUpgradeAnonymousProvider")).toBe(mockBehaviors.autoUpgradeAnonymousProvider.handler); expect(getBehavior(ui, "recaptchaVerification")).toBe(mockBehaviors.recaptchaVerification.handler); + expect(getBehavior(ui, "requireDisplayName")).toBe(mockBehaviors.requireDisplayName.handler); }); }); @@ -211,6 +219,30 @@ describe("recaptchaVerification", () => { }); }); +describe("requireDisplayName", () => { + it("should return behavior with correct structure", () => { + const behavior = requireDisplayName(); + + expect(behavior).toHaveProperty("requireDisplayName"); + expect(behavior.requireDisplayName).toHaveProperty("type", "callable"); + expect(behavior.requireDisplayName).toHaveProperty("handler"); + expect(typeof behavior.requireDisplayName.handler).toBe("function"); + }); + + it("should call the requireDisplayNameHandler when executed", async () => { + const behavior = requireDisplayName(); + const mockUI = createMockUI(); + const mockUser = { uid: "test-user-123" } as any; + const displayName = "John Doe"; + + const { requireDisplayNameHandler } = await import("./require-display-name"); + + await behavior.requireDisplayName.handler(mockUI, mockUser, displayName); + + expect(requireDisplayNameHandler).toHaveBeenCalledWith(mockUI, mockUser, displayName); + }); +}); + describe("defaultBehaviors", () => { it("should include recaptchaVerification by default", () => { expect(defaultBehaviors).toHaveProperty("recaptchaVerification"); @@ -222,5 +254,6 @@ describe("defaultBehaviors", () => { expect(defaultBehaviors).not.toHaveProperty("autoAnonymousLogin"); expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousCredential"); expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousProvider"); + expect(defaultBehaviors).not.toHaveProperty("requireDisplayName"); }); }); diff --git a/packages/core/src/behaviors/index.ts b/packages/core/src/behaviors/index.ts index 6a3b9ca5..1b1ddd21 100644 --- a/packages/core/src/behaviors/index.ts +++ b/packages/core/src/behaviors/index.ts @@ -5,6 +5,7 @@ import * as autoAnonymousLoginHandlers from "./auto-anonymous-login"; import * as recaptchaHandlers from "./recaptcha"; import * as providerStrategyHandlers from "./provider-strategy"; import * as oneTapSignInHandlers from "./one-tap"; +import * as requireDisplayNameHandlers from "./require-display-name"; import { callableBehavior, initBehavior, @@ -33,6 +34,7 @@ type Registry = { oneTapSignIn: InitBehavior< (ui: FirebaseUIConfiguration) => ReturnType >; + requireDisplayName: CallableBehavior; }; export type Behavior = Pick; @@ -60,9 +62,8 @@ export function autoUpgradeAnonymousUsers( autoUpgradeAnonymousProvider: callableBehavior((ui, provider) => anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler(ui, provider, options?.onUpgrade) ), - autoUpgradeAnonymousUserRedirectHandler: redirectBehavior( - (ui, credential) => - anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade) + autoUpgradeAnonymousUserRedirectHandler: redirectBehavior((ui, credential) => + anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade) ), }; } @@ -99,6 +100,12 @@ export function oneTapSignIn(options: OneTapSignInOptions): Behavior<"oneTapSign }; } +export function requireDisplayName(): Behavior<"requireDisplayName"> { + return { + requireDisplayName: callableBehavior(requireDisplayNameHandlers.requireDisplayNameHandler), + }; +} + export function hasBehavior(ui: FirebaseUIConfiguration, key: T): boolean { return !!ui.behaviors[key]; } diff --git a/packages/core/src/behaviors/require-display-name.test.ts b/packages/core/src/behaviors/require-display-name.test.ts new file mode 100644 index 00000000..69476441 --- /dev/null +++ b/packages/core/src/behaviors/require-display-name.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { User } from "firebase/auth"; +import { requireDisplayNameHandler } from "./require-display-name"; +import { createMockUI } from "~/tests/utils"; + +vi.mock("firebase/auth", () => ({ + updateProfile: vi.fn(), +})); + +import { updateProfile } from "firebase/auth"; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("requireDisplayNameHandler", () => { + it("should update user profile with display name", async () => { + const mockUser = { uid: "test-user-123" } as User; + const mockUI = createMockUI(); + const displayName = "John Doe"; + + vi.mocked(updateProfile).mockResolvedValue(); + + await requireDisplayNameHandler(mockUI, mockUser, displayName); + + expect(updateProfile).toHaveBeenCalledWith(mockUser, { displayName }); + }); + + it("should handle updateProfile errors", async () => { + const mockUser = { uid: "test-user-123" } as User; + const mockUI = createMockUI(); + const displayName = "John Doe"; + const mockError = new Error("Profile update failed"); + + vi.mocked(updateProfile).mockRejectedValue(mockError); + + await expect(requireDisplayNameHandler(mockUI, mockUser, displayName)) + .rejects.toThrow("Profile update failed"); + }); +}); diff --git a/packages/core/src/behaviors/require-display-name.ts b/packages/core/src/behaviors/require-display-name.ts new file mode 100644 index 00000000..4f6fe86e --- /dev/null +++ b/packages/core/src/behaviors/require-display-name.ts @@ -0,0 +1,6 @@ +import { updateProfile, User } from "firebase/auth"; +import { FirebaseUIConfiguration } from "~/config"; + +export const requireDisplayNameHandler = async (_: FirebaseUIConfiguration, user: User, displayName: string) => { + await updateProfile(user, { displayName }); +}; diff --git a/packages/core/src/schemas.test.ts b/packages/core/src/schemas.test.ts index f3991408..c52c1f00 100644 --- a/packages/core/src/schemas.test.ts +++ b/packages/core/src/schemas.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { createMockUI } from "~/tests/utils"; import { createEmailLinkAuthFormSchema, createForgotPasswordAuthFormSchema, createPhoneAuthFormSchema, createSignInAuthFormSchema, createSignUpAuthFormSchema } from "./schemas"; import { registerLocale } from "@firebase-ui/translations"; @@ -36,33 +36,109 @@ describe("createSignInAuthFormSchema", () => { }); describe("createSignUpAuthFormSchema", () => { - it("should create a sign up auth form schema with valid error messages", () => { + it("should create a sign up auth form schema with valid error messages when requireDisplayName behavior is not enabled", () => { const testLocale = registerLocale('test', { errors: { invalidEmail: "createSignUpAuthFormSchema + invalidEmail", weakPassword: "createSignUpAuthFormSchema + weakPassword", + displayNameRequired: "createSignUpAuthFormSchema + displayNameRequired", }, }); const mockUI = createMockUI({ locale: testLocale, + behaviors: {}, // No requireDisplayName behavior }); const schema = createSignUpAuthFormSchema(mockUI); - // Cause the schema to fail... - // TODO(ehesp): If no value is provided, the schema error is just "Required" - should this also be translated? - const result = schema.safeParse({ + const validResult = schema.safeParse({ + email: 'test@example.com', + password: 'password123', + }); + + expect(validResult.success).toBe(true); + + const validWithDisplayNameResult = schema.safeParse({ + email: 'test@example.com', + password: 'password123', + displayName: 'John Doe', + }); + + expect(validWithDisplayNameResult.success).toBe(true); + + const invalidResult = schema.safeParse({ email: '', password: '', }); - expect(result.success).toBe(false); - expect(result.error).toBeDefined(); - expect(result.error?.issues.length).toBe(2); + expect(invalidResult.success).toBe(false); + expect(invalidResult.error).toBeDefined(); + expect(invalidResult.error?.issues.length).toBe(2); + + expect(invalidResult.error?.issues[0]?.message).toBe("createSignUpAuthFormSchema + invalidEmail"); + expect(invalidResult.error?.issues[1]?.message).toBe("createSignUpAuthFormSchema + weakPassword"); + }); + + it("should create a sign up auth form schema with required displayName when requireDisplayName behavior is enabled", () => { + const testLocale = registerLocale('test', { + errors: { + invalidEmail: "createSignUpAuthFormSchema + invalidEmail", + weakPassword: "createSignUpAuthFormSchema + weakPassword", + displayNameRequired: "createSignUpAuthFormSchema + displayNameRequired", + }, + }); + + const mockUI = createMockUI({ + locale: testLocale, + behaviors: { + requireDisplayName: { type: "callable" as const, handler: vi.fn() }, + } as any, + }); + + const schema = createSignUpAuthFormSchema(mockUI); + + const validResult = schema.safeParse({ + email: 'test@example.com', + password: 'password123', + displayName: 'John Doe', + }); + + expect(validResult.success).toBe(true); + + const missingDisplayNameResult = schema.safeParse({ + email: 'test@example.com', + password: 'password123', + displayName: '', + }); + + expect(missingDisplayNameResult.success).toBe(false); + expect(missingDisplayNameResult.error).toBeDefined(); + expect(missingDisplayNameResult.error?.issues.length).toBe(1); + expect(missingDisplayNameResult.error?.issues[0]?.message).toBe("createSignUpAuthFormSchema + displayNameRequired"); + + const emptyDisplayNameResult = schema.safeParse({ + email: 'test@example.com', + password: 'password123', + displayName: '', + }); + + expect(emptyDisplayNameResult.success).toBe(false); + expect(emptyDisplayNameResult.error).toBeDefined(); + expect(emptyDisplayNameResult.error?.issues.length).toBe(1); + expect(emptyDisplayNameResult.error?.issues[0]?.message).toBe("createSignUpAuthFormSchema + displayNameRequired"); + + const invalidEmailPasswordResult = schema.safeParse({ + email: '', + password: '', + displayName: 'John Doe', + }); - expect(result.error?.issues[0]?.message).toBe("createSignUpAuthFormSchema + invalidEmail"); - expect(result.error?.issues[1]?.message).toBe("createSignUpAuthFormSchema + weakPassword"); + expect(invalidEmailPasswordResult.success).toBe(false); + expect(invalidEmailPasswordResult.error).toBeDefined(); + expect(invalidEmailPasswordResult.error?.issues.length).toBe(2); + expect(invalidEmailPasswordResult.error?.issues[0]?.message).toBe("createSignUpAuthFormSchema + invalidEmail"); + expect(invalidEmailPasswordResult.error?.issues[1]?.message).toBe("createSignUpAuthFormSchema + weakPassword"); }); }); diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index 4d90c034..29172877 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -18,6 +18,7 @@ import * as z from "zod"; import { RecaptchaVerifier } from "firebase/auth"; import { getTranslation } from "./translations"; import { FirebaseUIConfiguration } from "./config"; +import { hasBehavior } from "./behaviors"; export const LoginTypes = ["email", "phone", "anonymous", "emailLink", "google"] as const; export type LoginType = (typeof LoginTypes)[number]; @@ -31,9 +32,15 @@ export function createSignInAuthFormSchema(ui: FirebaseUIConfiguration) { } export function createSignUpAuthFormSchema(ui: FirebaseUIConfiguration) { + const requireDisplayName = hasBehavior(ui, "requireDisplayName"); + const displayNameRequiredMessage = getTranslation(ui, "errors", "displayNameRequired"); + return z.object({ email: z.email(getTranslation(ui, "errors", "invalidEmail")), password: z.string().min(6, getTranslation(ui, "errors", "weakPassword")), + displayName: requireDisplayName + ? z.string().min(1, displayNameRequiredMessage) + : z.string().min(1, displayNameRequiredMessage).optional(), }); } diff --git a/packages/translations/src/locales/en-us.ts b/packages/translations/src/locales/en-us.ts index 015edabb..1d660e43 100644 --- a/packages/translations/src/locales/en-us.ts +++ b/packages/translations/src/locales/en-us.ts @@ -44,6 +44,7 @@ export const enUS = { popupClosed: "The sign-in popup was closed. Please try again.", accountExistsWithDifferentCredential: "An account already exists with this email. Please sign in with the original provider.", + displayNameRequired: "Please provide a display name", }, messages: { passwordResetEmailSent: "Password reset email sent successfully", diff --git a/packages/translations/src/mapping.test.ts b/packages/translations/src/mapping.test.ts index 63c2ca0b..31abfd01 100644 --- a/packages/translations/src/mapping.test.ts +++ b/packages/translations/src/mapping.test.ts @@ -60,6 +60,10 @@ describe("mapping.ts", () => { expect(ERROR_CODE_MAP["auth/account-exists-with-different-credential"]).toBe("accountExistsWithDifferentCredential"); }); + it("should map display name error codes", () => { + expect(ERROR_CODE_MAP["auth/display-name-required"]).toBe("displayNameRequired"); + }); + it("should have correct type structure", () => { const errorKeys = Object.values(ERROR_CODE_MAP); const validErrorKeys = [ @@ -69,7 +73,8 @@ describe("mapping.ts", () => { "missingPhoneNumber", "quotaExceeded", "codeExpired", "captchaCheckFailed", "missingVerificationId", "missingEmail", "invalidActionCode", "credentialAlreadyInUse", "requiresRecentLogin", - "providerAlreadyLinked", "invalidVerificationCode", "accountExistsWithDifferentCredential" + "providerAlreadyLinked", "invalidVerificationCode", "accountExistsWithDifferentCredential", + "displayNameRequired" ]; errorKeys.forEach(key => { diff --git a/packages/translations/src/mapping.ts b/packages/translations/src/mapping.ts index bd5cea8c..43112ffc 100644 --- a/packages/translations/src/mapping.ts +++ b/packages/translations/src/mapping.ts @@ -41,6 +41,7 @@ export const ERROR_CODE_MAP = { "auth/provider-already-linked": "providerAlreadyLinked", "auth/invalid-verification-code": "invalidVerificationCode", "auth/account-exists-with-different-credential": "accountExistsWithDifferentCredential", + "auth/display-name-required": "displayNameRequired", } satisfies Record; export type ErrorCode = keyof typeof ERROR_CODE_MAP; diff --git a/packages/translations/src/types.ts b/packages/translations/src/types.ts index 8ed7820d..66d7e786 100644 --- a/packages/translations/src/types.ts +++ b/packages/translations/src/types.ts @@ -41,6 +41,7 @@ export type Translations = { captchaCheckFailed?: string; missingVerificationId?: string; missingEmail?: string; + displayNameRequired?: string; invalidActionCode?: string; credentialAlreadyInUse?: string; requiresRecentLogin?: string; From 75137ae9932479c8297ce0fab8ffe84e112e7052 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Fri, 3 Oct 2025 17:09:33 +0100 Subject: [PATCH 2/2] chore: Align formatting with dev branch --- packages/core/src/auth.test.ts | 4 +++- packages/core/src/auth.ts | 2 +- packages/core/src/behaviors/require-display-name.test.ts | 3 +-- packages/core/src/schemas.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/src/auth.test.ts b/packages/core/src/auth.test.ts index 6cbc145d..9a0d32d1 100644 --- a/packages/core/src/auth.test.ts +++ b/packages/core/src/auth.test.ts @@ -307,7 +307,9 @@ describe("createUserWithEmailAndPassword", () => { const password = "password123"; const displayName = "John Doe"; - const mockAutoUpgradeBehavior = vi.fn().mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential); + const mockAutoUpgradeBehavior = vi + .fn() + .mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential); const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined); const credential = EmailAuthProvider.credential(email, password); diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 1c5dd89e..7e2b4240 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -33,7 +33,7 @@ import { AuthCredential, } from "firebase/auth"; import { FirebaseUIConfiguration } from "./config"; -import { FirebaseUIError, handleFirebaseError } from "./errors"; +import { handleFirebaseError } from "./errors"; import { hasBehavior, getBehavior } from "./behaviors/index"; import { FirebaseError } from "firebase/app"; import { getTranslation } from "./translations"; diff --git a/packages/core/src/behaviors/require-display-name.test.ts b/packages/core/src/behaviors/require-display-name.test.ts index 69476441..e9134de0 100644 --- a/packages/core/src/behaviors/require-display-name.test.ts +++ b/packages/core/src/behaviors/require-display-name.test.ts @@ -34,7 +34,6 @@ describe("requireDisplayNameHandler", () => { vi.mocked(updateProfile).mockRejectedValue(mockError); - await expect(requireDisplayNameHandler(mockUI, mockUser, displayName)) - .rejects.toThrow("Profile update failed"); + await expect(requireDisplayNameHandler(mockUI, mockUser, displayName)).rejects.toThrow("Profile update failed"); }); }); diff --git a/packages/core/src/schemas.ts b/packages/core/src/schemas.ts index 29172877..5530affe 100644 --- a/packages/core/src/schemas.ts +++ b/packages/core/src/schemas.ts @@ -38,7 +38,7 @@ export function createSignUpAuthFormSchema(ui: FirebaseUIConfiguration) { return z.object({ email: z.email(getTranslation(ui, "errors", "invalidEmail")), password: z.string().min(6, getTranslation(ui, "errors", "weakPassword")), - displayName: requireDisplayName + displayName: requireDisplayName ? z.string().min(1, displayNameRequiredMessage) : z.string().min(1, displayNameRequiredMessage).optional(), });