Skip to content

Commit 82b6c22

Browse files
authored
Merge pull request #1212 from firebase/@invertase/require-display-name-behavior
2 parents 5db38b0 + 75137ae commit 82b6c22

File tree

12 files changed

+334
-14
lines changed

12 files changed

+334
-14
lines changed

packages/core/src/auth.test.ts

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,11 @@ describe("createUserWithEmailAndPassword", () => {
193193
const password = "password123";
194194

195195
const credential = EmailAuthProvider.credential(email, password);
196-
vi.mocked(hasBehavior).mockReturnValue(true);
196+
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
197+
if (behavior === "autoUpgradeAnonymousCredential") return true;
198+
if (behavior === "requireDisplayName") return false;
199+
return false;
200+
});
197201
const mockBehavior = vi.fn().mockResolvedValue({ providerId: "password" } as UserCredential);
198202
vi.mocked(getBehavior).mockReturnValue(mockBehavior);
199203

@@ -215,7 +219,11 @@ describe("createUserWithEmailAndPassword", () => {
215219
const password = "password123";
216220

217221
const credential = EmailAuthProvider.credential(email, password);
218-
vi.mocked(hasBehavior).mockReturnValue(true);
222+
vi.mocked(hasBehavior).mockImplementation((ui, behavior) => {
223+
if (behavior === "autoUpgradeAnonymousCredential") return true;
224+
if (behavior === "requireDisplayName") return false;
225+
return false;
226+
});
219227
const mockBehavior = vi.fn().mockResolvedValue(undefined);
220228
vi.mocked(getBehavior).mockReturnValue(mockBehavior);
221229

@@ -249,6 +257,125 @@ describe("createUserWithEmailAndPassword", () => {
249257
expect(handleFirebaseError).toHaveBeenCalledWith(mockUI, error);
250258
expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]);
251259
});
260+
261+
it("should call handleFirebaseError when requireDisplayName behavior is enabled but no displayName provided", async () => {
262+
const mockUI = createMockUI();
263+
const email = "[email protected]";
264+
const password = "password123";
265+
266+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
267+
if (behavior === "requireDisplayName") return true;
268+
if (behavior === "autoUpgradeAnonymousCredential") return false;
269+
return false;
270+
});
271+
272+
await createUserWithEmailAndPassword(mockUI, email, password);
273+
274+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
275+
expect(_createUserWithEmailAndPassword).not.toHaveBeenCalled();
276+
expect(handleFirebaseError).toHaveBeenCalled();
277+
});
278+
279+
it("should call requireDisplayName behavior when enabled and displayName provided", async () => {
280+
const mockUI = createMockUI();
281+
const email = "[email protected]";
282+
const password = "password123";
283+
const displayName = "John Doe";
284+
285+
const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined);
286+
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;
287+
288+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
289+
if (behavior === "requireDisplayName") return true;
290+
if (behavior === "autoUpgradeAnonymousCredential") return false;
291+
return false;
292+
});
293+
vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior);
294+
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);
295+
296+
const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);
297+
298+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
299+
expect(getBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
300+
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName);
301+
expect(result).toBe(mockResult);
302+
});
303+
304+
it("should call requireDisplayName behavior after autoUpgradeAnonymousCredential when both enabled", async () => {
305+
const mockUI = createMockUI();
306+
const email = "[email protected]";
307+
const password = "password123";
308+
const displayName = "John Doe";
309+
310+
const mockAutoUpgradeBehavior = vi
311+
.fn()
312+
.mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential);
313+
const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined);
314+
const credential = EmailAuthProvider.credential(email, password);
315+
316+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
317+
if (behavior === "requireDisplayName") return true;
318+
if (behavior === "autoUpgradeAnonymousCredential") return true;
319+
return false;
320+
});
321+
322+
vi.mocked(getBehavior).mockImplementation((_, behavior) => {
323+
if (behavior === "autoUpgradeAnonymousCredential") return mockAutoUpgradeBehavior;
324+
if (behavior === "requireDisplayName") return mockRequireDisplayNameBehavior;
325+
return vi.fn();
326+
});
327+
328+
vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential);
329+
330+
const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);
331+
332+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
333+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
334+
expect(mockAutoUpgradeBehavior).toHaveBeenCalledWith(mockUI, credential);
335+
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, { uid: "upgraded-user" }, displayName);
336+
expect(result).toEqual({ providerId: "upgraded", user: { uid: "upgraded-user" } });
337+
});
338+
339+
it("should not call requireDisplayName behavior when not enabled", async () => {
340+
const mockUI = createMockUI();
341+
const email = "[email protected]";
342+
const password = "password123";
343+
const displayName = "John Doe";
344+
345+
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;
346+
347+
vi.mocked(hasBehavior).mockReturnValue(false);
348+
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);
349+
350+
const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);
351+
352+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
353+
expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "requireDisplayName");
354+
expect(result).toBe(mockResult);
355+
});
356+
357+
it("should handle requireDisplayName behavior errors", async () => {
358+
const mockUI = createMockUI();
359+
const email = "[email protected]";
360+
const password = "password123";
361+
const displayName = "John Doe";
362+
363+
const mockRequireDisplayNameBehavior = vi.fn().mockRejectedValue(new Error("Display name update failed"));
364+
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;
365+
366+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
367+
if (behavior === "requireDisplayName") return true;
368+
if (behavior === "autoUpgradeAnonymousCredential") return false;
369+
return false;
370+
});
371+
vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior);
372+
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);
373+
374+
await createUserWithEmailAndPassword(mockUI, email, password, displayName);
375+
376+
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName);
377+
expect(handleFirebaseError).toHaveBeenCalled();
378+
});
252379
});
253380

254381
describe("signInWithPhoneNumber", () => {

packages/core/src/auth.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
import { FirebaseUIConfiguration } from "./config";
3636
import { handleFirebaseError } from "./errors";
3737
import { hasBehavior, getBehavior } from "./behaviors/index";
38+
import { FirebaseError } from "firebase/app";
39+
import { getTranslation } from "./translations";
3840

3941
async function handlePendingCredential(ui: FirebaseUIConfiguration, user: UserCredential): Promise<UserCredential> {
4042
const pendingCredString = window.sessionStorage.getItem("pendingCred");
@@ -82,21 +84,35 @@ export async function signInWithEmailAndPassword(
8284
export async function createUserWithEmailAndPassword(
8385
ui: FirebaseUIConfiguration,
8486
email: string,
85-
password: string
87+
password: string,
88+
displayName?: string
8689
): Promise<UserCredential> {
8790
try {
8891
const credential = EmailAuthProvider.credential(email, password);
8992

93+
if (hasBehavior(ui, "requireDisplayName") && !displayName) {
94+
throw new FirebaseError("auth/display-name-required", getTranslation(ui, "errors", "displayNameRequired"));
95+
}
96+
9097
if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
9198
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);
9299

93100
if (result) {
101+
if (hasBehavior(ui, "requireDisplayName")) {
102+
await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!);
103+
}
104+
94105
return handlePendingCredential(ui, result);
95106
}
96107
}
97108

98109
ui.setState("pending");
99110
const result = await _createUserWithEmailAndPassword(ui.auth, email, password);
111+
112+
if (hasBehavior(ui, "requireDisplayName")) {
113+
await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!);
114+
}
115+
100116
return handlePendingCredential(ui, result);
101117
} catch (error) {
102118
handleFirebaseError(ui, error);

packages/core/src/behaviors/index.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ import {
66
getBehavior,
77
hasBehavior,
88
recaptchaVerification,
9+
requireDisplayName,
910
defaultBehaviors,
1011
} from "./index";
1112

12-
// Mock the anonymous-upgrade handlers
1313
vi.mock("./anonymous-upgrade", () => ({
1414
autoUpgradeAnonymousCredentialHandler: vi.fn(),
1515
autoUpgradeAnonymousProviderHandler: vi.fn(),
1616
autoUpgradeAnonymousUserRedirectHandler: vi.fn(),
1717
}));
1818

19+
vi.mock("./require-display-name", () => ({
20+
requireDisplayNameHandler: vi.fn(),
21+
}));
22+
1923
vi.mock("firebase/auth", () => ({
2024
RecaptchaVerifier: vi.fn(),
2125
}));
@@ -49,13 +53,15 @@ describe("hasBehavior", () => {
4953
autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() },
5054
autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() },
5155
recaptchaVerification: { type: "callable" as const, handler: vi.fn() },
56+
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
5257
} as any,
5358
});
5459

5560
expect(hasBehavior(mockUI, "autoAnonymousLogin")).toBe(true);
5661
expect(hasBehavior(mockUI, "autoUpgradeAnonymousCredential")).toBe(true);
5762
expect(hasBehavior(mockUI, "autoUpgradeAnonymousProvider")).toBe(true);
5863
expect(hasBehavior(mockUI, "recaptchaVerification")).toBe(true);
64+
expect(hasBehavior(mockUI, "requireDisplayName")).toBe(true);
5965
});
6066
});
6167

@@ -83,6 +89,7 @@ describe("getBehavior", () => {
8389
autoUpgradeAnonymousCredential: { type: "callable" as const, handler: vi.fn() },
8490
autoUpgradeAnonymousProvider: { type: "callable" as const, handler: vi.fn() },
8591
recaptchaVerification: { type: "callable" as const, handler: vi.fn() },
92+
requireDisplayName: { type: "callable" as const, handler: vi.fn() },
8693
};
8794

8895
const ui = createMockUI({ behaviors: mockBehaviors as any });
@@ -93,6 +100,7 @@ describe("getBehavior", () => {
93100
);
94101
expect(getBehavior(ui, "autoUpgradeAnonymousProvider")).toBe(mockBehaviors.autoUpgradeAnonymousProvider.handler);
95102
expect(getBehavior(ui, "recaptchaVerification")).toBe(mockBehaviors.recaptchaVerification.handler);
103+
expect(getBehavior(ui, "requireDisplayName")).toBe(mockBehaviors.requireDisplayName.handler);
96104
});
97105
});
98106

@@ -211,6 +219,30 @@ describe("recaptchaVerification", () => {
211219
});
212220
});
213221

222+
describe("requireDisplayName", () => {
223+
it("should return behavior with correct structure", () => {
224+
const behavior = requireDisplayName();
225+
226+
expect(behavior).toHaveProperty("requireDisplayName");
227+
expect(behavior.requireDisplayName).toHaveProperty("type", "callable");
228+
expect(behavior.requireDisplayName).toHaveProperty("handler");
229+
expect(typeof behavior.requireDisplayName.handler).toBe("function");
230+
});
231+
232+
it("should call the requireDisplayNameHandler when executed", async () => {
233+
const behavior = requireDisplayName();
234+
const mockUI = createMockUI();
235+
const mockUser = { uid: "test-user-123" } as any;
236+
const displayName = "John Doe";
237+
238+
const { requireDisplayNameHandler } = await import("./require-display-name");
239+
240+
await behavior.requireDisplayName.handler(mockUI, mockUser, displayName);
241+
242+
expect(requireDisplayNameHandler).toHaveBeenCalledWith(mockUI, mockUser, displayName);
243+
});
244+
});
245+
214246
describe("defaultBehaviors", () => {
215247
it("should include recaptchaVerification by default", () => {
216248
expect(defaultBehaviors).toHaveProperty("recaptchaVerification");
@@ -222,5 +254,6 @@ describe("defaultBehaviors", () => {
222254
expect(defaultBehaviors).not.toHaveProperty("autoAnonymousLogin");
223255
expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousCredential");
224256
expect(defaultBehaviors).not.toHaveProperty("autoUpgradeAnonymousProvider");
257+
expect(defaultBehaviors).not.toHaveProperty("requireDisplayName");
225258
});
226259
});

packages/core/src/behaviors/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as autoAnonymousLoginHandlers from "./auto-anonymous-login";
55
import * as recaptchaHandlers from "./recaptcha";
66
import * as providerStrategyHandlers from "./provider-strategy";
77
import * as oneTapSignInHandlers from "./one-tap";
8+
import * as requireDisplayNameHandlers from "./require-display-name";
89
import {
910
callableBehavior,
1011
initBehavior,
@@ -33,6 +34,7 @@ type Registry = {
3334
oneTapSignIn: InitBehavior<
3435
(ui: FirebaseUIConfiguration) => ReturnType<typeof oneTapSignInHandlers.oneTapSignInHandler>
3536
>;
37+
requireDisplayName: CallableBehavior<typeof requireDisplayNameHandlers.requireDisplayNameHandler>;
3638
};
3739

3840
export type Behavior<T extends keyof Registry = keyof Registry> = Pick<Registry, T>;
@@ -98,6 +100,12 @@ export function oneTapSignIn(options: OneTapSignInOptions): Behavior<"oneTapSign
98100
};
99101
}
100102

103+
export function requireDisplayName(): Behavior<"requireDisplayName"> {
104+
return {
105+
requireDisplayName: callableBehavior(requireDisplayNameHandlers.requireDisplayNameHandler),
106+
};
107+
}
108+
101109
export function hasBehavior<T extends keyof Registry>(ui: FirebaseUIConfiguration, key: T): boolean {
102110
return !!ui.behaviors[key];
103111
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { User } from "firebase/auth";
3+
import { requireDisplayNameHandler } from "./require-display-name";
4+
import { createMockUI } from "~/tests/utils";
5+
6+
vi.mock("firebase/auth", () => ({
7+
updateProfile: vi.fn(),
8+
}));
9+
10+
import { updateProfile } from "firebase/auth";
11+
12+
beforeEach(() => {
13+
vi.clearAllMocks();
14+
});
15+
16+
describe("requireDisplayNameHandler", () => {
17+
it("should update user profile with display name", async () => {
18+
const mockUser = { uid: "test-user-123" } as User;
19+
const mockUI = createMockUI();
20+
const displayName = "John Doe";
21+
22+
vi.mocked(updateProfile).mockResolvedValue();
23+
24+
await requireDisplayNameHandler(mockUI, mockUser, displayName);
25+
26+
expect(updateProfile).toHaveBeenCalledWith(mockUser, { displayName });
27+
});
28+
29+
it("should handle updateProfile errors", async () => {
30+
const mockUser = { uid: "test-user-123" } as User;
31+
const mockUI = createMockUI();
32+
const displayName = "John Doe";
33+
const mockError = new Error("Profile update failed");
34+
35+
vi.mocked(updateProfile).mockRejectedValue(mockError);
36+
37+
await expect(requireDisplayNameHandler(mockUI, mockUser, displayName)).rejects.toThrow("Profile update failed");
38+
});
39+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { updateProfile, User } from "firebase/auth";
2+
import { FirebaseUIConfiguration } from "~/config";
3+
4+
export const requireDisplayNameHandler = async (_: FirebaseUIConfiguration, user: User, displayName: string) => {
5+
await updateProfile(user, { displayName });
6+
};

0 commit comments

Comments
 (0)