Skip to content

Commit 35b7d29

Browse files
committed
Merge branch '@invertase/provider-strategy' of https://github.com/firebase/firebaseui-web into @invertase/v7-development
2 parents 58d91a4 + 1c78f8e commit 35b7d29

File tree

4 files changed

+268
-21
lines changed

4 files changed

+268
-21
lines changed

packages/core/src/behaviors/anonymous-upgrade.test.ts

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { describe, it, expect, vi, beforeEach } from "vitest";
2-
import { Auth, AuthCredential, AuthProvider, linkWithCredential, linkWithRedirect, User } from "firebase/auth";
3-
import { autoUpgradeAnonymousCredentialHandler, autoUpgradeAnonymousProviderHandler } from "./anonymous-upgrade";
2+
import { Auth, AuthCredential, AuthProvider, linkWithCredential, linkWithRedirect, User, UserCredential } from "firebase/auth";
3+
import { autoUpgradeAnonymousCredentialHandler, autoUpgradeAnonymousProviderHandler, autoUpgradeAnonymousUserRedirectHandler, OnUpgradeCallback } from "./anonymous-upgrade";
44
import { createMockUI } from "~/tests/utils";
5+
import { getBehavior } from "~/behaviors";
56

67
vi.mock("firebase/auth", () => ({
78
linkWithCredential: vi.fn(),
89
linkWithRedirect: vi.fn(),
910
}));
1011

12+
vi.mock("~/behaviors", () => ({
13+
getBehavior: vi.fn(),
14+
}));
15+
1116
beforeEach(() => {
1217
vi.clearAllMocks();
1318
});
@@ -30,6 +35,38 @@ describe("autoUpgradeAnonymousCredentialHandler", () => {
3035
expect(result).toBe(mockResult);
3136
});
3237

38+
it("should call onUpgrade callback when provided", async () => {
39+
const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User;
40+
const mockAuth = { currentUser: mockUser } as Auth;
41+
const mockUI = createMockUI({ auth: mockAuth });
42+
const mockCredential = { providerId: "password" } as AuthCredential;
43+
const mockResult = { user: { uid: "upgraded-123" } } as UserCredential;
44+
45+
vi.mocked(linkWithCredential).mockResolvedValue(mockResult);
46+
47+
const onUpgrade = vi.fn().mockResolvedValue(undefined);
48+
49+
const result = await autoUpgradeAnonymousCredentialHandler(mockUI, mockCredential, onUpgrade);
50+
51+
expect(onUpgrade).toHaveBeenCalledWith(mockUI, "anonymous-123", mockResult);
52+
expect(result).toBe(mockResult);
53+
});
54+
55+
it("should handle onUpgrade callback errors", async () => {
56+
const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User;
57+
const mockAuth = { currentUser: mockUser } as Auth;
58+
const mockUI = createMockUI({ auth: mockAuth });
59+
const mockCredential = { providerId: "password" } as AuthCredential;
60+
const mockResult = { user: { uid: "upgraded-123" } } as UserCredential;
61+
62+
vi.mocked(linkWithCredential).mockResolvedValue(mockResult);
63+
64+
const onUpgrade = vi.fn().mockRejectedValue(new Error("Callback error"));
65+
66+
await expect(autoUpgradeAnonymousCredentialHandler(mockUI, mockCredential, onUpgrade))
67+
.rejects.toThrow("Callback error");
68+
});
69+
3370
it("should not upgrade when user is not anonymous", async () => {
3471
const mockUser = { isAnonymous: false, uid: "regular-user-123" } as User;
3572
const mockAuth = { currentUser: mockUser } as Auth;
@@ -62,14 +99,55 @@ describe("autoUpgradeAnonymousProviderHandler", () => {
6299
const mockAuth = { currentUser: mockUser } as Auth;
63100
const mockUI = createMockUI({ auth: mockAuth });
64101
const mockProvider = { providerId: "google.com" } as AuthProvider;
102+
const mockResult = { user: { uid: "upgraded-123" } } as UserCredential;
65103

66-
vi.mocked(linkWithRedirect).mockResolvedValue({} as never);
104+
const mockProviderLinkStrategy = vi.fn().mockResolvedValue(mockResult);
105+
vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy);
67106

68-
await autoUpgradeAnonymousProviderHandler(mockUI, mockProvider);
107+
const localStorageSpy = vi.spyOn(Storage.prototype, 'setItem');
108+
const localStorageRemoveSpy = vi.spyOn(Storage.prototype, 'removeItem');
69109

70-
expect(linkWithRedirect).toHaveBeenCalledWith(mockUser, mockProvider);
71-
expect(mockUI.setState).toHaveBeenCalledWith("pending");
72-
expect(mockUI.setState).not.toHaveBeenCalledWith("idle");
110+
const result = await autoUpgradeAnonymousProviderHandler(mockUI, mockProvider);
111+
112+
expect(getBehavior).toHaveBeenCalledWith(mockUI, "providerLinkStrategy");
113+
expect(mockProviderLinkStrategy).toHaveBeenCalledWith(mockUI, mockUser, mockProvider);
114+
expect(localStorageSpy).toHaveBeenCalledWith("fbui:upgrade:oldUserId", "anonymous-123");
115+
expect(localStorageRemoveSpy).toHaveBeenCalledWith("fbui:upgrade:oldUserId");
116+
expect(result).toBe(mockResult);
117+
});
118+
119+
it("should call onUpgrade callback when provided", async () => {
120+
const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User;
121+
const mockAuth = { currentUser: mockUser } as Auth;
122+
const mockUI = createMockUI({ auth: mockAuth });
123+
const mockProvider = { providerId: "google.com" } as AuthProvider;
124+
const mockResult = { user: { uid: "upgraded-123" } } as UserCredential;
125+
126+
const mockProviderLinkStrategy = vi.fn().mockResolvedValue(mockResult);
127+
vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy);
128+
129+
const onUpgrade = vi.fn().mockResolvedValue(undefined);
130+
131+
const result = await autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, onUpgrade);
132+
133+
expect(onUpgrade).toHaveBeenCalledWith(mockUI, "anonymous-123", mockResult);
134+
expect(result).toBe(mockResult);
135+
});
136+
137+
it("should handle onUpgrade callback errors", async () => {
138+
const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User;
139+
const mockAuth = { currentUser: mockUser } as Auth;
140+
const mockUI = createMockUI({ auth: mockAuth });
141+
const mockProvider = { providerId: "google.com" } as AuthProvider;
142+
const mockResult = { user: { uid: "upgraded-123" } } as UserCredential;
143+
144+
const mockProviderLinkStrategy = vi.fn().mockResolvedValue(mockResult);
145+
vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy);
146+
147+
const onUpgrade = vi.fn().mockRejectedValue(new Error("Callback error"));
148+
149+
await expect(autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, onUpgrade))
150+
.rejects.toThrow("Callback error");
73151
});
74152

75153
it("should not upgrade when user is not anonymous", async () => {
@@ -95,3 +173,77 @@ describe("autoUpgradeAnonymousProviderHandler", () => {
95173
expect(mockUI.setState).not.toHaveBeenCalled();
96174
});
97175
});
176+
177+
describe("autoUpgradeAnonymousUserRedirectHandler", () => {
178+
beforeEach(() => {
179+
window.localStorage.clear();
180+
});
181+
182+
it("should call onUpgrade callback when oldUserId exists in localStorage", async () => {
183+
const mockUI = createMockUI();
184+
const mockCredential = { user: { uid: "upgraded-123" } } as UserCredential;
185+
const oldUserId = "anonymous-123";
186+
187+
window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId);
188+
189+
const onUpgrade = vi.fn().mockResolvedValue(undefined);
190+
191+
await autoUpgradeAnonymousUserRedirectHandler(mockUI, mockCredential, onUpgrade);
192+
193+
expect(onUpgrade).toHaveBeenCalledWith(mockUI, oldUserId, mockCredential);
194+
expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull();
195+
});
196+
197+
it("should not call onUpgrade callback when no oldUserId in localStorage", async () => {
198+
const mockUI = createMockUI();
199+
const mockCredential = { user: { uid: "upgraded-123" } } as UserCredential;
200+
201+
const onUpgrade = vi.fn().mockResolvedValue(undefined);
202+
203+
await autoUpgradeAnonymousUserRedirectHandler(mockUI, mockCredential, onUpgrade);
204+
205+
expect(onUpgrade).not.toHaveBeenCalled();
206+
});
207+
208+
it("should not call onUpgrade callback when no credential provided", async () => {
209+
const mockUI = createMockUI();
210+
const oldUserId = "anonymous-123";
211+
212+
window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId);
213+
214+
const onUpgrade = vi.fn().mockResolvedValue(undefined);
215+
216+
await autoUpgradeAnonymousUserRedirectHandler(mockUI, null, onUpgrade);
217+
218+
expect(onUpgrade).not.toHaveBeenCalled();
219+
});
220+
221+
it("should not call onUpgrade callback when no onUpgrade callback provided", async () => {
222+
const mockUI = createMockUI();
223+
const mockCredential = { user: { uid: "upgraded-123" } } as UserCredential;
224+
const oldUserId = "anonymous-123";
225+
226+
window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId);
227+
228+
await autoUpgradeAnonymousUserRedirectHandler(mockUI, mockCredential);
229+
230+
// Should not throw and should clean up localStorage even when no callback provided
231+
expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull();
232+
});
233+
234+
it("should handle onUpgrade callback errors", async () => {
235+
const mockUI = createMockUI();
236+
const mockCredential = { user: { uid: "upgraded-123" } } as UserCredential;
237+
const oldUserId = "anonymous-123";
238+
239+
window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId);
240+
241+
const onUpgrade = vi.fn().mockRejectedValue(new Error("Callback error"));
242+
243+
await expect(autoUpgradeAnonymousUserRedirectHandler(mockUI, mockCredential, onUpgrade))
244+
.rejects.toThrow("Callback error");
245+
246+
// Should clean up localStorage even when callback throws error
247+
expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull();
248+
});
249+
});
Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,66 @@
1-
import { AuthCredential, AuthProvider, linkWithCredential, linkWithRedirect } from "firebase/auth";
1+
import { AuthCredential, AuthProvider, linkWithCredential, linkWithRedirect, UserCredential } from "firebase/auth";
22
import { FirebaseUIConfiguration } from "~/config";
3-
import { RedirectHandler } from "./utils";
43
import { getBehavior } from "~/behaviors";
54

6-
export const autoUpgradeAnonymousCredentialHandler = async (ui: FirebaseUIConfiguration, credential: AuthCredential) => {
5+
export type OnUpgradeCallback = (ui: FirebaseUIConfiguration, oldUserId: string, credential: UserCredential) => Promise<void> | void;
6+
7+
export const autoUpgradeAnonymousCredentialHandler = async (ui: FirebaseUIConfiguration, credential: AuthCredential, onUpgrade?: OnUpgradeCallback) => {
78
const currentUser = ui.auth.currentUser;
89

910
if (!currentUser?.isAnonymous) {
1011
return;
1112
}
1213

14+
const oldUserId = currentUser.uid;
15+
1316
ui.setState("pending");
1417
const result = await linkWithCredential(currentUser, credential);
1518

19+
if (onUpgrade) {
20+
await onUpgrade(ui, oldUserId, result);
21+
}
22+
1623
ui.setState("idle");
1724
return result;
1825
};
1926

20-
export const autoUpgradeAnonymousProviderHandler = async (ui: FirebaseUIConfiguration, provider: AuthProvider) => {
27+
export const autoUpgradeAnonymousProviderHandler = async (ui: FirebaseUIConfiguration, provider: AuthProvider, onUpgrade?: OnUpgradeCallback) => {
2128
const currentUser = ui.auth.currentUser;
2229

2330
if (!currentUser?.isAnonymous) {
2431
return;
2532
}
2633

27-
return getBehavior(ui, "providerLinkStrategy")(ui, currentUser, provider);
34+
const oldUserId = currentUser.uid;
35+
36+
window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId);
37+
38+
const result = await getBehavior(ui, "providerLinkStrategy")(ui, currentUser, provider);
39+
40+
// If we got here, the user has been linked via a popup, so we need to call the onUpgrade callback
41+
// and delete the oldUserId from localStorage.
42+
// If we didn't get here, they'll be redirected and we'll handle the result inside of the autoUpgradeAnonymousUserRedirectHandler.
43+
44+
window.localStorage.removeItem("fbui:upgrade:oldUserId");
45+
46+
if (onUpgrade) {
47+
await onUpgrade(ui, oldUserId, result);
48+
}
49+
50+
return result;
2851
};
2952

30-
export const autoUpgradeAnonymousUserRedirectHandler: RedirectHandler = async () => {
31-
// TODO
53+
export const autoUpgradeAnonymousUserRedirectHandler = async (ui: FirebaseUIConfiguration, credential: UserCredential | null, onUpgrade?: OnUpgradeCallback) => {
54+
const oldUserId = window.localStorage.getItem("fbui:upgrade:oldUserId");
55+
56+
// Always clean up localStorage once we've retrieved the oldUserId
57+
if (oldUserId) {
58+
window.localStorage.removeItem("fbui:upgrade:oldUserId");
59+
}
60+
61+
if (!onUpgrade || !oldUserId || !credential) {
62+
return;
63+
}
64+
65+
await onUpgrade(ui, oldUserId, credential);
3266
};

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import {
99
defaultBehaviors,
1010
} from "./index";
1111

12+
// Mock the anonymous-upgrade handlers
13+
vi.mock("./anonymous-upgrade", () => ({
14+
autoUpgradeAnonymousCredentialHandler: vi.fn(),
15+
autoUpgradeAnonymousProviderHandler: vi.fn(),
16+
autoUpgradeAnonymousUserRedirectHandler: vi.fn(),
17+
}));
18+
1219
vi.mock("firebase/auth", () => ({
1320
RecaptchaVerifier: vi.fn(),
1421
}));
@@ -125,6 +132,43 @@ describe("autoUpgradeAnonymousUsers", () => {
125132
expect(typeof behavior.autoUpgradeAnonymousUserRedirectHandler.handler).toBe("function");
126133
});
127134

135+
it("should work with onUpgrade callback option", () => {
136+
const mockOnUpgrade = vi.fn();
137+
const behavior = autoUpgradeAnonymousUsers({ onUpgrade: mockOnUpgrade });
138+
139+
expect(behavior).toHaveProperty("autoUpgradeAnonymousCredential");
140+
expect(behavior).toHaveProperty("autoUpgradeAnonymousProvider");
141+
expect(behavior).toHaveProperty("autoUpgradeAnonymousUserRedirectHandler");
142+
143+
expect(typeof behavior.autoUpgradeAnonymousCredential.handler).toBe("function");
144+
expect(typeof behavior.autoUpgradeAnonymousProvider.handler).toBe("function");
145+
expect(typeof behavior.autoUpgradeAnonymousUserRedirectHandler.handler).toBe("function");
146+
});
147+
148+
it("should pass onUpgrade callback to handlers when called", async () => {
149+
const mockOnUpgrade = vi.fn();
150+
const behavior = autoUpgradeAnonymousUsers({ onUpgrade: mockOnUpgrade });
151+
152+
const mockUI = createMockUI();
153+
const mockCredential = { providerId: "password" } as any;
154+
const mockProvider = { providerId: "google.com" } as any;
155+
const mockUserCredential = { user: { uid: "upgraded-123" } } as any;
156+
157+
const {
158+
autoUpgradeAnonymousCredentialHandler,
159+
autoUpgradeAnonymousProviderHandler,
160+
autoUpgradeAnonymousUserRedirectHandler
161+
} = await import("./anonymous-upgrade");
162+
163+
await behavior.autoUpgradeAnonymousCredential.handler(mockUI, mockCredential);
164+
await behavior.autoUpgradeAnonymousProvider.handler(mockUI, mockProvider);
165+
await behavior.autoUpgradeAnonymousUserRedirectHandler.handler(mockUI, mockUserCredential);
166+
167+
expect(autoUpgradeAnonymousCredentialHandler).toHaveBeenCalledWith(mockUI, mockCredential, mockOnUpgrade);
168+
expect(autoUpgradeAnonymousProviderHandler).toHaveBeenCalledWith(mockUI, mockProvider, mockOnUpgrade);
169+
expect(autoUpgradeAnonymousUserRedirectHandler).toHaveBeenCalledWith(mockUI, mockUserCredential, mockOnUpgrade);
170+
});
171+
128172
it("should not include other behaviors", () => {
129173
const behavior = autoUpgradeAnonymousUsers();
130174

packages/core/src/behaviors/index.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FirebaseUIConfiguration } from "~/config";
2-
import type { RecaptchaVerifier } from "firebase/auth";
2+
import type { RecaptchaVerifier, UserCredential } from "firebase/auth";
33
import * as anonymousUpgradeHandlers from "./anonymous-upgrade";
44
import * as autoAnonymousLoginHandlers from "./auto-anonymous-login";
55
import * as recaptchaHandlers from "./recaptcha";
@@ -21,12 +21,18 @@ type Registry = {
2121
>;
2222
autoUpgradeAnonymousProvider: CallableBehavior<typeof anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler>;
2323
autoUpgradeAnonymousUserRedirectHandler: RedirectBehavior<
24-
typeof anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler
24+
(
25+
ui: FirebaseUIConfiguration,
26+
credential: UserCredential | null,
27+
onUpgrade?: anonymousUpgradeHandlers.OnUpgradeCallback
28+
) => ReturnType<typeof anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler>
2529
>;
2630
recaptchaVerification: CallableBehavior<(ui: FirebaseUIConfiguration, element: HTMLElement) => RecaptchaVerifier>;
2731
providerSignInStrategy: CallableBehavior<providerStrategyHandlers.ProviderSignInStrategyHandler>;
2832
providerLinkStrategy: CallableBehavior<providerStrategyHandlers.ProviderLinkStrategyHandler>;
29-
oneTapSignIn: InitBehavior<(ui: FirebaseUIConfiguration) => ReturnType<typeof oneTapSignInHandlers.oneTapSignInHandler>>;
33+
oneTapSignIn: InitBehavior<
34+
(ui: FirebaseUIConfiguration) => ReturnType<typeof oneTapSignInHandlers.oneTapSignInHandler>
35+
>;
3036
};
3137

3238
export type Behavior<T extends keyof Registry = keyof Registry> = Pick<Registry, T>;
@@ -38,14 +44,25 @@ export function autoAnonymousLogin(): Behavior<"autoAnonymousLogin"> {
3844
};
3945
}
4046

41-
export function autoUpgradeAnonymousUsers(): Behavior<
47+
export type AutoUpgradeAnonymousUsersOptions = {
48+
onUpgrade?: anonymousUpgradeHandlers.OnUpgradeCallback;
49+
};
50+
51+
export function autoUpgradeAnonymousUsers(
52+
options?: AutoUpgradeAnonymousUsersOptions
53+
): Behavior<
4254
"autoUpgradeAnonymousCredential" | "autoUpgradeAnonymousProvider" | "autoUpgradeAnonymousUserRedirectHandler"
4355
> {
4456
return {
45-
autoUpgradeAnonymousCredential: callableBehavior(anonymousUpgradeHandlers.autoUpgradeAnonymousCredentialHandler),
46-
autoUpgradeAnonymousProvider: callableBehavior(anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler),
57+
autoUpgradeAnonymousCredential: callableBehavior((ui, credential) =>
58+
anonymousUpgradeHandlers.autoUpgradeAnonymousCredentialHandler(ui, credential, options?.onUpgrade)
59+
),
60+
autoUpgradeAnonymousProvider: callableBehavior((ui, provider) =>
61+
anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler(ui, provider, options?.onUpgrade)
62+
),
4763
autoUpgradeAnonymousUserRedirectHandler: redirectBehavior(
48-
anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler
64+
(ui, credential) =>
65+
anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade)
4966
),
5067
};
5168
}

0 commit comments

Comments
 (0)