Skip to content

Commit b774d89

Browse files
committed
feat(core): Add requireDisplayName behavior
1 parent 35b7d29 commit b774d89

File tree

12 files changed

+342
-21
lines changed

12 files changed

+342
-21
lines changed

packages/core/src/auth.test.ts

Lines changed: 127 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,123 @@ 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.fn().mockResolvedValue({ providerId: "upgraded", user: { uid: "upgraded-user" } } as UserCredential);
311+
const mockRequireDisplayNameBehavior = vi.fn().mockResolvedValue(undefined);
312+
const credential = EmailAuthProvider.credential(email, password);
313+
314+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
315+
if (behavior === "requireDisplayName") return true;
316+
if (behavior === "autoUpgradeAnonymousCredential") return true;
317+
return false;
318+
});
319+
320+
vi.mocked(getBehavior).mockImplementation((_, behavior) => {
321+
if (behavior === "autoUpgradeAnonymousCredential") return mockAutoUpgradeBehavior;
322+
if (behavior === "requireDisplayName") return mockRequireDisplayNameBehavior;
323+
return vi.fn();
324+
});
325+
326+
vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential);
327+
328+
const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);
329+
330+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
331+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential");
332+
expect(mockAutoUpgradeBehavior).toHaveBeenCalledWith(mockUI, credential);
333+
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, { uid: "upgraded-user" }, displayName);
334+
expect(result).toEqual({ providerId: "upgraded", user: { uid: "upgraded-user" } });
335+
});
336+
337+
it("should not call requireDisplayName behavior when not enabled", async () => {
338+
const mockUI = createMockUI();
339+
const email = "[email protected]";
340+
const password = "password123";
341+
const displayName = "John Doe";
342+
343+
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;
344+
345+
vi.mocked(hasBehavior).mockReturnValue(false);
346+
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);
347+
348+
const result = await createUserWithEmailAndPassword(mockUI, email, password, displayName);
349+
350+
expect(hasBehavior).toHaveBeenCalledWith(mockUI, "requireDisplayName");
351+
expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "requireDisplayName");
352+
expect(result).toBe(mockResult);
353+
});
354+
355+
it("should handle requireDisplayName behavior errors", async () => {
356+
const mockUI = createMockUI();
357+
const email = "[email protected]";
358+
const password = "password123";
359+
const displayName = "John Doe";
360+
361+
const mockRequireDisplayNameBehavior = vi.fn().mockRejectedValue(new Error("Display name update failed"));
362+
const mockResult = { providerId: "password", user: { uid: "user123" } } as UserCredential;
363+
364+
vi.mocked(hasBehavior).mockImplementation((_, behavior) => {
365+
if (behavior === "requireDisplayName") return true;
366+
if (behavior === "autoUpgradeAnonymousCredential") return false;
367+
return false;
368+
});
369+
vi.mocked(getBehavior).mockReturnValue(mockRequireDisplayNameBehavior);
370+
vi.mocked(_createUserWithEmailAndPassword).mockResolvedValue(mockResult);
371+
372+
await createUserWithEmailAndPassword(mockUI, email, password, displayName);
373+
374+
expect(mockRequireDisplayNameBehavior).toHaveBeenCalledWith(mockUI, mockResult.user, displayName);
375+
expect(handleFirebaseError).toHaveBeenCalled();
376+
});
252377
});
253378

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

packages/core/src/auth.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ import {
3333
AuthCredential,
3434
} from "firebase/auth";
3535
import { FirebaseUIConfiguration } from "./config";
36-
import { handleFirebaseError } from "./errors";
36+
import { FirebaseUIError, 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");
@@ -63,7 +65,7 @@ export async function signInWithEmailAndPassword(
6365

6466
if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
6567
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);
66-
68+
6769
if (result) {
6870
return handlePendingCredential(ui, result);
6971
}
@@ -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);
@@ -223,7 +239,10 @@ export async function signInAnonymously(ui: FirebaseUIConfiguration): Promise<Us
223239
}
224240
}
225241

226-
export async function signInWithProvider(ui: FirebaseUIConfiguration, provider: AuthProvider): Promise<UserCredential | never> {
242+
export async function signInWithProvider(
243+
ui: FirebaseUIConfiguration,
244+
provider: AuthProvider
245+
): Promise<UserCredential | never> {
227246
try {
228247
if (hasBehavior(ui, "autoUpgradeAnonymousProvider")) {
229248
const credential = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider);

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: 10 additions & 3 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>;
@@ -60,9 +62,8 @@ export function autoUpgradeAnonymousUsers(
6062
autoUpgradeAnonymousProvider: callableBehavior((ui, provider) =>
6163
anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler(ui, provider, options?.onUpgrade)
6264
),
63-
autoUpgradeAnonymousUserRedirectHandler: redirectBehavior(
64-
(ui, credential) =>
65-
anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade)
65+
autoUpgradeAnonymousUserRedirectHandler: redirectBehavior((ui, credential) =>
66+
anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade)
6667
),
6768
};
6869
}
@@ -99,6 +100,12 @@ export function oneTapSignIn(options: OneTapSignInOptions): Behavior<"oneTapSign
99100
};
100101
}
101102

103+
export function requireDisplayName(): Behavior<"requireDisplayName"> {
104+
return {
105+
requireDisplayName: callableBehavior(requireDisplayNameHandlers.requireDisplayNameHandler),
106+
};
107+
}
108+
102109
export function hasBehavior<T extends keyof Registry>(ui: FirebaseUIConfiguration, key: T): boolean {
103110
return !!ui.behaviors[key];
104111
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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))
38+
.rejects.toThrow("Profile update failed");
39+
});
40+
});

0 commit comments

Comments
 (0)