From b0530ada60889ea85b895e6476d0fe60156339f0 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 6 May 2026 13:13:52 +0100 Subject: [PATCH 1/6] feat(auth): onUpgradeFailure callback when account linking fails --- .../core/src/behaviors/anonymous-upgrade.ts | 61 ++++++++++++++++--- packages/core/src/behaviors/index.ts | 16 ++++- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/core/src/behaviors/anonymous-upgrade.ts b/packages/core/src/behaviors/anonymous-upgrade.ts index 1f93fc311..e80c0e0d5 100644 --- a/packages/core/src/behaviors/anonymous-upgrade.ts +++ b/packages/core/src/behaviors/anonymous-upgrade.ts @@ -19,11 +19,38 @@ import { type FirebaseUI } from "~/config"; import { getBehavior } from "~/behaviors"; export type OnUpgradeCallback = (ui: FirebaseUI, oldUserId: string, credential: UserCredential) => Promise | void; +export type OnUpgradeFailureResult = "handled" | void; +export type OnUpgradeFailureContext = { + ui: FirebaseUI; + oldUserId: string; + error: unknown; + credential?: AuthCredential; + provider?: AuthProvider; +}; +export type OnUpgradeFailureCallback = ( + context: OnUpgradeFailureContext +) => Promise | OnUpgradeFailureResult; + +async function handleUpgradeFailure( + context: OnUpgradeFailureContext, + onUpgradeFailure?: OnUpgradeFailureCallback +): Promise { + try { + return (await onUpgradeFailure?.(context)) === "handled"; + } catch (callbackError) { + if (callbackError instanceof Error && !("cause" in callbackError)) { + (callbackError as Error & { cause?: unknown }).cause = context.error; + } + + throw callbackError; + } +} export const autoUpgradeAnonymousCredentialHandler = async ( ui: FirebaseUI, credential: AuthCredential, - onUpgrade?: OnUpgradeCallback + onUpgrade?: OnUpgradeCallback, + onUpgradeFailure?: OnUpgradeFailureCallback ) => { const currentUser = ui.auth.currentUser; @@ -33,7 +60,17 @@ export const autoUpgradeAnonymousCredentialHandler = async ( const oldUserId = currentUser.uid; - const result = await linkWithCredential(currentUser, credential); + let result: UserCredential; + + try { + result = await linkWithCredential(currentUser, credential); + } catch (error) { + if (await handleUpgradeFailure({ ui, oldUserId, error, credential }, onUpgradeFailure)) { + return; + } + + throw error; + } if (onUpgrade) { await onUpgrade(ui, oldUserId, result); @@ -45,7 +82,8 @@ export const autoUpgradeAnonymousCredentialHandler = async ( export const autoUpgradeAnonymousProviderHandler = async ( ui: FirebaseUI, provider: AuthProvider, - onUpgrade?: OnUpgradeCallback + onUpgrade?: OnUpgradeCallback, + onUpgradeFailure?: OnUpgradeFailureCallback ) => { const currentUser = ui.auth.currentUser; @@ -57,13 +95,20 @@ export const autoUpgradeAnonymousProviderHandler = async ( window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId); - const result = await getBehavior(ui, "providerLinkStrategy")(ui, currentUser, provider); + let result: UserCredential; - // If we got here, the user has been linked via a popup, so we need to call the onUpgrade callback - // and delete the oldUserId from localStorage. - // If we didn't get here, they'll be redirected and we'll handle the result inside of the autoUpgradeAnonymousUserRedirectHandler. + try { + result = await getBehavior(ui, "providerLinkStrategy")(ui, currentUser, provider); + } catch (error) { + if (await handleUpgradeFailure({ ui, oldUserId, error, provider }, onUpgradeFailure)) { + return; + } - window.localStorage.removeItem("fbui:upgrade:oldUserId"); + throw error; + } finally { + // When the link attempt settles locally, the stored ID is no longer needed. + window.localStorage.removeItem("fbui:upgrade:oldUserId"); + } if (onUpgrade) { await onUpgrade(ui, oldUserId, result); diff --git a/packages/core/src/behaviors/index.ts b/packages/core/src/behaviors/index.ts index 5841d41e5..ada2e251f 100644 --- a/packages/core/src/behaviors/index.ts +++ b/packages/core/src/behaviors/index.ts @@ -73,6 +73,8 @@ export function autoAnonymousLogin(): Behavior<"autoAnonymousLogin"> { export type AutoUpgradeAnonymousUsersOptions = { /** Optional callback function that is called when an anonymous user is upgraded. */ onUpgrade?: anonymousUpgradeHandlers.OnUpgradeCallback; + /** Optional callback function that is called when credential or provider linking fails locally. */ + onUpgradeFailure?: anonymousUpgradeHandlers.OnUpgradeFailureCallback; }; /** @@ -91,10 +93,20 @@ export function autoUpgradeAnonymousUsers( > { return { autoUpgradeAnonymousCredential: callableBehavior((ui, credential) => - anonymousUpgradeHandlers.autoUpgradeAnonymousCredentialHandler(ui, credential, options?.onUpgrade) + anonymousUpgradeHandlers.autoUpgradeAnonymousCredentialHandler( + ui, + credential, + options?.onUpgrade, + options?.onUpgradeFailure + ) ), autoUpgradeAnonymousProvider: callableBehavior((ui, provider) => - anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler(ui, provider, options?.onUpgrade) + anonymousUpgradeHandlers.autoUpgradeAnonymousProviderHandler( + ui, + provider, + options?.onUpgrade, + options?.onUpgradeFailure + ) ), autoUpgradeAnonymousUserRedirectHandler: redirectBehavior((ui, credential) => anonymousUpgradeHandlers.autoUpgradeAnonymousUserRedirectHandler(ui, credential, options?.onUpgrade) From 679248a17da1db82b59dac29f1a0c6c822f5ba2c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 6 May 2026 13:14:02 +0100 Subject: [PATCH 2/6] test: onUpgradeFailure implementation --- .../src/behaviors/anonymous-upgrade.test.ts | 126 ++++++++++++++++++ packages/core/src/behaviors/index.test.ts | 30 ++++- 2 files changed, 153 insertions(+), 3 deletions(-) diff --git a/packages/core/src/behaviors/anonymous-upgrade.test.ts b/packages/core/src/behaviors/anonymous-upgrade.test.ts index e551a25ad..34d679eeb 100644 --- a/packages/core/src/behaviors/anonymous-upgrade.test.ts +++ b/packages/core/src/behaviors/anonymous-upgrade.test.ts @@ -44,6 +44,7 @@ vi.mock("~/behaviors", () => ({ beforeEach(() => { vi.clearAllMocks(); + window.localStorage.clear(); }); describe("autoUpgradeAnonymousCredentialHandler", () => { @@ -95,6 +96,67 @@ describe("autoUpgradeAnonymousCredentialHandler", () => { ); }); + it("should call onUpgradeFailure and rethrow when credential linking fails", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockCredential = { providerId: "google.com" } as AuthCredential; + const mockError = new Error("Link failed"); + const onUpgradeFailure = vi.fn().mockResolvedValue(undefined); + + vi.mocked(linkWithCredential).mockRejectedValue(mockError); + + await expect( + autoUpgradeAnonymousCredentialHandler(mockUI, mockCredential, undefined, onUpgradeFailure) + ).rejects.toThrow("Link failed"); + + expect(onUpgradeFailure).toHaveBeenCalledWith({ + ui: mockUI, + oldUserId: "anonymous-123", + error: mockError, + credential: mockCredential, + }); + }); + + it("should suppress credential linking errors when onUpgradeFailure returns handled", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockCredential = { providerId: "google.com" } as AuthCredential; + const mockError = new Error("Link failed"); + const onUpgradeFailure = vi.fn().mockResolvedValue("handled"); + + vi.mocked(linkWithCredential).mockRejectedValue(mockError); + + const result = await autoUpgradeAnonymousCredentialHandler(mockUI, mockCredential, undefined, onUpgradeFailure); + + expect(result).toBeUndefined(); + expect(onUpgradeFailure).toHaveBeenCalledWith({ + ui: mockUI, + oldUserId: "anonymous-123", + error: mockError, + credential: mockCredential, + }); + }); + + it("should surface onUpgradeFailure callback errors for credential linking", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockCredential = { providerId: "google.com" } as AuthCredential; + const mockError = new Error("Link failed"); + const callbackError = new Error("Callback failed"); + const onUpgradeFailure = vi.fn().mockRejectedValue(callbackError); + + vi.mocked(linkWithCredential).mockRejectedValue(mockError); + + await expect( + autoUpgradeAnonymousCredentialHandler(mockUI, mockCredential, undefined, onUpgradeFailure) + ).rejects.toThrow("Callback failed"); + + expect((callbackError as Error & { cause?: unknown }).cause).toBe(mockError); + }); + it("should not upgrade when user is not anonymous", async () => { const mockUser = { isAnonymous: false, uid: "regular-user-123" } as User; const mockAuth = { currentUser: mockUser } as Auth; @@ -179,6 +241,70 @@ describe("autoUpgradeAnonymousProviderHandler", () => { ); }); + it("should call onUpgradeFailure and rethrow when provider linking fails", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockProvider = { providerId: "google.com" } as AuthProvider; + const mockError = new Error("Provider link failed"); + const onUpgradeFailure = vi.fn().mockResolvedValue(undefined); + const mockProviderLinkStrategy = vi.fn().mockRejectedValue(mockError); + vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy); + + await expect( + autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, undefined, onUpgradeFailure) + ).rejects.toThrow("Provider link failed"); + + expect(onUpgradeFailure).toHaveBeenCalledWith({ + ui: mockUI, + oldUserId: "anonymous-123", + error: mockError, + provider: mockProvider, + }); + expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull(); + }); + + it("should suppress provider linking errors when onUpgradeFailure returns handled", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockProvider = { providerId: "google.com" } as AuthProvider; + const mockError = new Error("Provider link failed"); + const onUpgradeFailure = vi.fn().mockResolvedValue("handled"); + const mockProviderLinkStrategy = vi.fn().mockRejectedValue(mockError); + vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy); + + const result = await autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, undefined, onUpgradeFailure); + + expect(result).toBeUndefined(); + expect(onUpgradeFailure).toHaveBeenCalledWith({ + ui: mockUI, + oldUserId: "anonymous-123", + error: mockError, + provider: mockProvider, + }); + expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull(); + }); + + it("should surface onUpgradeFailure callback errors for provider linking", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockProvider = { providerId: "google.com" } as AuthProvider; + const mockError = new Error("Provider link failed"); + const callbackError = new Error("Callback failed"); + const onUpgradeFailure = vi.fn().mockRejectedValue(callbackError); + const mockProviderLinkStrategy = vi.fn().mockRejectedValue(mockError); + vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy); + + await expect( + autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, undefined, onUpgradeFailure) + ).rejects.toThrow("Callback failed"); + + expect((callbackError as Error & { cause?: unknown }).cause).toBe(mockError); + expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull(); + }); + it("should not upgrade when user is not anonymous", async () => { const mockUser = { isAnonymous: false, uid: "regular-user-123" } as User; const mockAuth = { currentUser: mockUser } as Auth; diff --git a/packages/core/src/behaviors/index.test.ts b/packages/core/src/behaviors/index.test.ts index 0994558eb..34fda0074 100644 --- a/packages/core/src/behaviors/index.test.ts +++ b/packages/core/src/behaviors/index.test.ts @@ -173,9 +173,23 @@ describe("autoUpgradeAnonymousUsers", () => { expect(typeof behavior.autoUpgradeAnonymousUserRedirectHandler.handler).toBe("function"); }); + it("should work with onUpgradeFailure callback option", () => { + const mockOnUpgradeFailure = vi.fn(); + const behavior = autoUpgradeAnonymousUsers({ onUpgradeFailure: mockOnUpgradeFailure }); + + expect(behavior).toHaveProperty("autoUpgradeAnonymousCredential"); + expect(behavior).toHaveProperty("autoUpgradeAnonymousProvider"); + expect(behavior).toHaveProperty("autoUpgradeAnonymousUserRedirectHandler"); + + expect(typeof behavior.autoUpgradeAnonymousCredential.handler).toBe("function"); + expect(typeof behavior.autoUpgradeAnonymousProvider.handler).toBe("function"); + expect(typeof behavior.autoUpgradeAnonymousUserRedirectHandler.handler).toBe("function"); + }); + it("should pass onUpgrade callback to handlers when called", async () => { const mockOnUpgrade = vi.fn(); - const behavior = autoUpgradeAnonymousUsers({ onUpgrade: mockOnUpgrade }); + const mockOnUpgradeFailure = vi.fn(); + const behavior = autoUpgradeAnonymousUsers({ onUpgrade: mockOnUpgrade, onUpgradeFailure: mockOnUpgradeFailure }); const mockUI = createMockUI(); const mockCredential = { providerId: "password" } as any; @@ -192,8 +206,18 @@ describe("autoUpgradeAnonymousUsers", () => { await behavior.autoUpgradeAnonymousProvider.handler(mockUI, mockProvider); await behavior.autoUpgradeAnonymousUserRedirectHandler.handler(mockUI, mockUserCredential); - expect(autoUpgradeAnonymousCredentialHandler).toHaveBeenCalledWith(mockUI, mockCredential, mockOnUpgrade); - expect(autoUpgradeAnonymousProviderHandler).toHaveBeenCalledWith(mockUI, mockProvider, mockOnUpgrade); + expect(autoUpgradeAnonymousCredentialHandler).toHaveBeenCalledWith( + mockUI, + mockCredential, + mockOnUpgrade, + mockOnUpgradeFailure + ); + expect(autoUpgradeAnonymousProviderHandler).toHaveBeenCalledWith( + mockUI, + mockProvider, + mockOnUpgrade, + mockOnUpgradeFailure + ); expect(autoUpgradeAnonymousUserRedirectHandler).toHaveBeenCalledWith(mockUI, mockUserCredential, mockOnUpgrade); }); From 3557923cf91ce9e1e31ea28cced7eac75291fa20 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 6 May 2026 13:18:23 +0100 Subject: [PATCH 3/6] docs: update documentation on how to handle failed auto-linking --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c5b45120..b620ec174 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,9 @@ const ui = initializeUI({ #### `autoUpgradeAnonymousUsers` -The `autoUpgradeAnonymousUsers` behavior will automatically upgrade a user who is anonymously authenticated with your application upon a successful sign in (including OAuth). You can optionally provide a callback to handle an upgrade (such as merging account data). During the async callback, the UI will stay in a pending state. +The `autoUpgradeAnonymousUsers` behavior will automatically upgrade a user who is anonymously authenticated with your application upon a successful sign in (including OAuth). You can optionally provide callbacks to handle successful upgrades and failed upgrade attempts. During async callbacks, the UI will stay in a pending state. + +When an upgrade succeeds, the anonymous user's UID is preserved and the new credential is linked to that user. When an upgrade fails (for example, because an OAuth credential is already linked to another account), `onUpgradeFailure` receives the original error and the anonymous user's `oldUserId` so your app can decide whether to migrate anonymous user data into the existing account. Return `"handled"` from `onUpgradeFailure` to suppress the default FirebaseUI error. Return `undefined`, omit the callback, or throw from the callback to preserve the default error behavior. ```ts import { autoUpgradeAnonymousUsers } from '@firebase-oss/ui-core'; @@ -346,7 +348,12 @@ const ui = initializeUI({ behaviors: [autoUpgradeAnonymousUsers({ async onUpgrade(ui, oldUserId, credential) { // Some account upgrade logic. - } + }, + async onUpgradeFailure({ ui, oldUserId, error, credential, provider }) { + // Optional merge-conflict handling. + // Return "handled" if your app handled the failure and FirebaseUI + // should not show the default error. + }, })], }); ``` From 08deffb5de0f7661b4c7e4c9799691a4675a2213 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 6 May 2026 15:22:24 +0100 Subject: [PATCH 4/6] fix: ensure redirect linking preserves fbui:upgrade:oldUserId --- packages/core/src/behaviors/anonymous-upgrade.ts | 14 ++++++++++---- packages/core/src/behaviors/provider-strategy.ts | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/core/src/behaviors/anonymous-upgrade.ts b/packages/core/src/behaviors/anonymous-upgrade.ts index e80c0e0d5..7699a4c38 100644 --- a/packages/core/src/behaviors/anonymous-upgrade.ts +++ b/packages/core/src/behaviors/anonymous-upgrade.ts @@ -95,21 +95,27 @@ export const autoUpgradeAnonymousProviderHandler = async ( window.localStorage.setItem("fbui:upgrade:oldUserId", oldUserId); - let result: UserCredential; + let result: UserCredential | void; try { result = await getBehavior(ui, "providerLinkStrategy")(ui, currentUser, provider); } catch (error) { + window.localStorage.removeItem("fbui:upgrade:oldUserId"); + if (await handleUpgradeFailure({ ui, oldUserId, error, provider }, onUpgradeFailure)) { return; } throw error; - } finally { - // When the link attempt settles locally, the stored ID is no longer needed. - window.localStorage.removeItem("fbui:upgrade:oldUserId"); } + // Redirect strategies complete later, so keep oldUserId for the redirect handler. + if (!result) { + return; + } + + window.localStorage.removeItem("fbui:upgrade:oldUserId"); + if (onUpgrade) { await onUpgrade(ui, oldUserId, result); } diff --git a/packages/core/src/behaviors/provider-strategy.ts b/packages/core/src/behaviors/provider-strategy.ts index a0f02ab9c..95cb66481 100644 --- a/packages/core/src/behaviors/provider-strategy.ts +++ b/packages/core/src/behaviors/provider-strategy.ts @@ -30,7 +30,7 @@ export type ProviderLinkStrategyHandler = ( ui: FirebaseUI, user: User, provider: AuthProvider -) => Promise; +) => Promise; export const signInWithRediectHandler: ProviderSignInStrategyHandler = async (ui, provider) => { return signInWithRedirect(ui.auth, provider); From fd835ca179015ec06c4004977efa874ee759f205 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 6 May 2026 15:22:42 +0100 Subject: [PATCH 5/6] test: preserve oldUserId in localStorage when provider linking redirects --- .../src/behaviors/anonymous-upgrade.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/core/src/behaviors/anonymous-upgrade.test.ts b/packages/core/src/behaviors/anonymous-upgrade.test.ts index 34d679eeb..eed58eed1 100644 --- a/packages/core/src/behaviors/anonymous-upgrade.test.ts +++ b/packages/core/src/behaviors/anonymous-upgrade.test.ts @@ -239,6 +239,23 @@ describe("autoUpgradeAnonymousProviderHandler", () => { await expect(autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, onUpgrade)).rejects.toThrow( "Callback error" ); + expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBeNull(); + }); + + it("should preserve oldUserId in localStorage when provider linking redirects", async () => { + const mockUser = { isAnonymous: true, uid: "anonymous-123" } as User; + const mockAuth = { currentUser: mockUser } as Auth; + const mockUI = createMockUI({ auth: mockAuth }); + const mockProvider = { providerId: "google.com" } as AuthProvider; + const onUpgrade = vi.fn(); + const mockProviderLinkStrategy = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockProviderLinkStrategy); + + const result = await autoUpgradeAnonymousProviderHandler(mockUI, mockProvider, onUpgrade); + + expect(result).toBeUndefined(); + expect(onUpgrade).not.toHaveBeenCalled(); + expect(window.localStorage.getItem("fbui:upgrade:oldUserId")).toBe("anonymous-123"); }); it("should call onUpgradeFailure and rethrow when provider linking fails", async () => { From f9c5333cf7798a10da66cc2bdf99be89e7afdf8c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 7 May 2026 17:19:03 +0100 Subject: [PATCH 6/6] chore: stop error surfacing when consumer has "handled" unhappy path --- packages/core/src/auth.test.ts | 188 +++++++++++++++--- packages/core/src/auth.ts | 140 ++++++++----- .../core/src/behaviors/anonymous-upgrade.ts | 4 +- packages/core/src/behaviors/index.ts | 7 + 4 files changed, 261 insertions(+), 78 deletions(-) diff --git a/packages/core/src/auth.test.ts b/packages/core/src/auth.test.ts index 692757355..152a73e66 100644 --- a/packages/core/src/auth.test.ts +++ b/packages/core/src/auth.test.ts @@ -107,7 +107,7 @@ describe("signInWithEmailAndPassword", () => { expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); expect(_signInWithCredential).toHaveBeenCalledTimes(1); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); }); it("should call the autoUpgradeAnonymousCredential behavior if enabled and return a value", async () => { @@ -126,7 +126,7 @@ describe("signInWithEmailAndPassword", () => { expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); @@ -154,6 +154,28 @@ describe("signInWithEmailAndPassword", () => { expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); + it("should stop when autoUpgradeAnonymousCredential handles a linking failure", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const email = "test@example.com"; + const password = "password123"; + + const credential = EmailAuthProvider.credential(email, password); + vi.mocked(hasBehavior).mockReturnValue(true); + vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential); + const mockBehavior = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockBehavior); + + const result = await signInWithEmailAndPassword(mockUI, email, password); + + expect(result).toBeUndefined(); + expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); + expect(_signInWithCredential).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + }); + it("should call handleFirebaseError if an error is thrown", async () => { const mockUI = createMockUI(); const email = "test@example.com"; @@ -196,7 +218,7 @@ describe("createUserWithEmailAndPassword", () => { expect(_createUserWithEmailAndPassword).toHaveBeenCalledWith(mockUI.auth, email, password); expect(_createUserWithEmailAndPassword).toHaveBeenCalledTimes(1); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); }); it("should call the autoUpgradeAnonymousCredential behavior if enabled and return a value", async () => { @@ -219,7 +241,7 @@ describe("createUserWithEmailAndPassword", () => { expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); @@ -251,6 +273,32 @@ describe("createUserWithEmailAndPassword", () => { expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); + it("should stop when autoUpgradeAnonymousCredential handles a create-account linking failure", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const email = "test@example.com"; + const password = "password123"; + + const credential = EmailAuthProvider.credential(email, password); + vi.mocked(hasBehavior).mockImplementation((_, behavior) => { + if (behavior === "autoUpgradeAnonymousCredential") return true; + if (behavior === "requireDisplayName") return false; + return false; + }); + vi.mocked(EmailAuthProvider.credential).mockReturnValue(credential); + const mockBehavior = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockBehavior); + + const result = await createUserWithEmailAndPassword(mockUI, email, password); + + expect(result).toBeUndefined(); + expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); + expect(_createUserWithEmailAndPassword).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + }); + it("should call handleFirebaseError if an error is thrown", async () => { const mockUI = createMockUI(); const email = "test@example.com"; @@ -484,8 +532,8 @@ describe("confirmPhoneNumber", () => { const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode); - // Since currentUser is null, the behavior should not called. - expect(hasBehavior).toHaveBeenCalledTimes(0); + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); + expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); // Calls pending pre-_signInWithCredential call, then idle after. expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); @@ -494,7 +542,7 @@ describe("confirmPhoneNumber", () => { expect(_signInWithCredential).toHaveBeenCalledTimes(1); // Assert that the result is a valid UserCredential. - expect(result.providerId).toBe("phone"); + expect(result).toMatchObject({ providerId: "phone" }); }); it("should call autoUpgradeAnonymousCredential behavior when user is anonymous", async () => { @@ -516,7 +564,7 @@ describe("confirmPhoneNumber", () => { expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(result.providerId).toBe("phone"); + expect(result).toMatchObject({ providerId: "phone" }); // Auth method sets pending at start, then idle in finally block. expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); @@ -530,18 +578,19 @@ describe("confirmPhoneNumber", () => { const verificationCode = "123456"; const credential = PhoneAuthProvider.credential(verificationId, verificationCode); + vi.mocked(hasBehavior).mockReturnValue(false); vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential); vi.mocked(_signInWithCredential).mockResolvedValue({ providerId: "phone" } as UserCredential); const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode); - // Behavior should not be called when user is not anonymous - expect(hasBehavior).not.toHaveBeenCalled(); + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); + expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); // Should proceed with normal sign-in flow expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); - expect(result.providerId).toBe("phone"); + expect(result).toMatchObject({ providerId: "phone" }); }); it("should not call behavior when user is null", async () => { @@ -552,21 +601,22 @@ describe("confirmPhoneNumber", () => { const verificationCode = "123456"; const credential = PhoneAuthProvider.credential(verificationId, verificationCode); + vi.mocked(hasBehavior).mockReturnValue(false); vi.mocked(PhoneAuthProvider.credential).mockReturnValue(credential); vi.mocked(_signInWithCredential).mockResolvedValue({ providerId: "phone" } as UserCredential); const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode); - // Behavior should not be called when user is null - expect(hasBehavior).not.toHaveBeenCalled(); + expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); + expect(getBehavior).not.toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); // Should proceed with normal sign-in flow expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); - expect(result.providerId).toBe("phone"); + expect(result).toMatchObject({ providerId: "phone" }); }); - it("should fall back to normal sign-in when behavior returns undefined", async () => { + it("should stop when autoUpgradeAnonymousCredential returns undefined for an anonymous user", async () => { const mockUI = createMockUI({ auth: { currentUser: { isAnonymous: true } } as Auth, }); @@ -579,17 +629,16 @@ describe("confirmPhoneNumber", () => { const mockBehavior = vi.fn().mockResolvedValue(undefined); vi.mocked(getBehavior).mockReturnValue(mockBehavior); - await confirmPhoneNumber(mockUI, verificationId, verificationCode); + const result = await confirmPhoneNumber(mockUI, verificationId, verificationCode); expect(hasBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); - expect(_signInWithCredential).toHaveBeenCalledTimes(1); - - // Calls pending pre-_signInWithCredential call, then idle after. + expect(result).toBeUndefined(); + expect(_signInWithCredential).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); @@ -798,7 +847,7 @@ describe("signInWithEmailLink", () => { expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); expect(_signInWithCredential).toHaveBeenCalledTimes(1); - expect(result.providerId).toBe("emailLink"); + expect(result).toMatchObject({ providerId: "emailLink" }); }); it("should call the autoUpgradeAnonymousCredential behavior if enabled and return a value", async () => { @@ -820,7 +869,7 @@ describe("signInWithEmailLink", () => { expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(result.providerId).toBe("emailLink"); + expect(result).toMatchObject({ providerId: "emailLink" }); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); @@ -851,6 +900,28 @@ describe("signInWithEmailLink", () => { expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); + it("should stop when autoUpgradeAnonymousCredential returns undefined for an anonymous user", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const email = "test@example.com"; + const link = "https://example.com/auth?oobCode=abc123"; + + const credential = EmailAuthProvider.credentialWithLink(email, link); + vi.mocked(hasBehavior).mockReturnValue(true); + vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(credential); + const mockBehavior = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockBehavior); + + const result = await signInWithEmailLink(mockUI, email, link); + + expect(result).toBeUndefined(); + expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); + expect(_signInWithCredential).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + }); + it("should call handleFirebaseError if an error is thrown", async () => { const mockUI = createMockUI(); const email = "test@example.com"; @@ -893,7 +964,7 @@ describe("signInWithCredential", () => { expect(_signInWithCredential).toHaveBeenCalledWith(mockUI.auth, credential); expect(_signInWithCredential).toHaveBeenCalledTimes(1); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); }); it("should call the autoUpgradeAnonymousCredential behavior if enabled and return a value", async () => { @@ -910,7 +981,7 @@ describe("signInWithCredential", () => { expect(getBehavior).toHaveBeenCalledWith(mockUI, "autoUpgradeAnonymousCredential"); expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); - expect(result.providerId).toBe("password"); + expect(result).toMatchObject({ providerId: "password" }); expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); @@ -936,6 +1007,25 @@ describe("signInWithCredential", () => { expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); }); + it("should stop when autoUpgradeAnonymousCredential handles a linking failure", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const credential = { providerId: "password" } as any; + + vi.mocked(hasBehavior).mockReturnValue(true); + const mockBehavior = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockBehavior); + + const result = await signInWithCredential(mockUI, credential); + + expect(result).toBeUndefined(); + expect(mockBehavior).toHaveBeenCalledWith(mockUI, credential); + expect(_signInWithCredential).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + }); + it("should call handleFirebaseError if an error is thrown", async () => { const mockUI = createMockUI(); const credential = { providerId: "password" } as any; @@ -1146,6 +1236,31 @@ describe("signInWithProvider", () => { expect(result).toBe(mockResult); }); + it("should stop when autoUpgradeAnonymousProvider handles a linking failure", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const provider = { providerId: "google.com" } as AuthProvider; + + vi.mocked(hasBehavior).mockReturnValue(true); + + const mockUpgradeBehavior = vi.fn().mockResolvedValue(undefined); + const mockProviderStrategy = vi.fn(); + vi.mocked(getBehavior).mockImplementation((_ui, behavior) => { + if (behavior === "autoUpgradeAnonymousProvider") return mockUpgradeBehavior; + if (behavior === "providerSignInStrategy") return mockProviderStrategy; + return vi.fn(); + }); + + const result = await signInWithProvider(mockUI, provider); + + expect(result).toBeUndefined(); + expect(mockUpgradeBehavior).toHaveBeenCalledWith(mockUI, provider); + expect(mockProviderStrategy).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + }); + it("should call handleFirebaseError if an error is thrown", async () => { const mockUI = createMockUI(); const provider = { providerId: "google.com" } as AuthProvider; @@ -1370,6 +1485,31 @@ describe("completeEmailLinkSignIn", () => { expect(window.localStorage.getItem("emailForSignIn")).toBeNull(); }); + it("should return null when anonymous email-link auto-upgrade returns undefined", async () => { + const mockUI = createMockUI({ + auth: { currentUser: { isAnonymous: true } } as Auth, + }); + const currentUrl = "https://example.com/auth?oobCode=abc123"; + const email = "test@example.com"; + const emailLinkCredential = { providerId: "emailLink" } as any; + + vi.mocked(_isSignInWithEmailLink).mockReturnValue(true); + window.localStorage.setItem("emailForSignIn", email); + vi.mocked(hasBehavior).mockReturnValue(true); + vi.mocked(EmailAuthProvider.credentialWithLink).mockReturnValue(emailLinkCredential); + const mockBehavior = vi.fn().mockResolvedValue(undefined); + vi.mocked(getBehavior).mockReturnValue(mockBehavior); + + const result = await completeEmailLinkSignIn(mockUI, currentUrl); + + expect(result).toBeNull(); + expect(mockBehavior).toHaveBeenCalledWith(mockUI, emailLinkCredential); + expect(_signInWithCredential).not.toHaveBeenCalled(); + expect(handleFirebaseError).not.toHaveBeenCalled(); + expect(vi.mocked(mockUI.setState).mock.calls).toEqual([["pending"], ["idle"]]); + expect(window.localStorage.getItem("emailForSignIn")).toBeNull(); + }); + it("should propagate error from signInWithEmailLink", async () => { const mockUI = createMockUI(); const currentUrl = "https://example.com/auth?oobCode=abc123"; diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts index 60b0bf08f..b2e7ff0b3 100644 --- a/packages/core/src/auth.ts +++ b/packages/core/src/auth.ts @@ -64,6 +64,47 @@ function setPendingState(ui: FirebaseUI) { ui.setState("pending"); } +type AnonymousUpgradeAttempt = + | { status: "upgraded"; credential: UserCredential } + | { status: "stopped" } + | { status: "skipped" }; + +async function attemptAnonymousCredentialUpgrade( + ui: FirebaseUI, + credential: AuthCredential +): Promise { + if (!hasBehavior(ui, "autoUpgradeAnonymousCredential")) { + return { status: "skipped" }; + } + + const wasAnonymous = ui.auth.currentUser?.isAnonymous === true; + const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); + + if (result) { + return { status: "upgraded", credential: result }; + } + + return wasAnonymous ? { status: "stopped" } : { status: "skipped" }; +} + +async function attemptAnonymousProviderUpgrade( + ui: FirebaseUI, + provider: AuthProvider +): Promise { + if (!hasBehavior(ui, "autoUpgradeAnonymousProvider")) { + return { status: "skipped" }; + } + + const wasAnonymous = ui.auth.currentUser?.isAnonymous === true; + const result = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider); + + if (result) { + return { status: "upgraded", credential: result }; + } + + return wasAnonymous ? { status: "stopped" } : { status: "skipped" }; +} + /** * Signs in with an email and password. * @@ -72,23 +113,23 @@ function setPendingState(ui: FirebaseUI) { * @param ui - The FirebaseUI instance. * @param email - The email to sign in with. * @param password - The password to sign in with. - * @returns {Promise} A promise containing the user credential. + * @returns {Promise} A promise containing the user credential, or void if handled. */ export async function signInWithEmailAndPassword( ui: FirebaseUI, email: string, password: string -): Promise { +): Promise { try { setPendingState(ui); const credential = EmailAuthProvider.credential(email, password); - if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) { - const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); - - if (result) { - return handlePendingCredential(ui, result); - } + const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential); + if (upgrade.status === "upgraded") { + return handlePendingCredential(ui, upgrade.credential); + } + if (upgrade.status === "stopped") { + return; } const result = await _signInWithCredential(ui.auth, credential); @@ -110,14 +151,14 @@ export async function signInWithEmailAndPassword( * @param email - The email address for the new account. * @param password - The password for the new account. * @param displayName - Optional display name for the user. - * @returns {Promise} A promise containing the user credential. + * @returns {Promise} A promise containing the user credential, or void if handled. */ export async function createUserWithEmailAndPassword( ui: FirebaseUI, email: string, password: string, displayName?: string -): Promise { +): Promise { try { setPendingState(ui); const credential = EmailAuthProvider.credential(email, password); @@ -126,16 +167,16 @@ export async function createUserWithEmailAndPassword( 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); + const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential); + if (upgrade.status === "upgraded") { + if (hasBehavior(ui, "requireDisplayName")) { + await getBehavior(ui, "requireDisplayName")(ui, upgrade.credential.user, displayName!); } + + return handlePendingCredential(ui, upgrade.credential); + } + if (upgrade.status === "stopped") { + return; } const result = await _createUserWithEmailAndPassword(ui.auth, email, password); @@ -213,24 +254,23 @@ export async function verifyPhoneNumber( * @param ui - The FirebaseUI instance. * @param verificationId - The verification ID from the phone verification process. * @param verificationCode - The verification code sent to the phone. - * @returns {Promise} A promise containing the user credential. + * @returns {Promise} A promise containing the user credential, or void if handled. */ export async function confirmPhoneNumber( ui: FirebaseUI, verificationId: string, verificationCode: string -): Promise { +): Promise { try { setPendingState(ui); - const currentUser = ui.auth.currentUser; const credential = PhoneAuthProvider.credential(verificationId, verificationCode); - if (currentUser?.isAnonymous && hasBehavior(ui, "autoUpgradeAnonymousCredential")) { - const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); - - if (result) { - return handlePendingCredential(ui, result); - } + const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential); + if (upgrade.status === "upgraded") { + return handlePendingCredential(ui, upgrade.credential); + } + if (upgrade.status === "stopped") { + return; } const result = await _signInWithCredential(ui.auth, credential); @@ -294,9 +334,9 @@ export async function sendSignInLinkToEmail(ui: FirebaseUI, email: string): Prom * @param ui - The FirebaseUI instance. * @param email - The email address associated with the sign-in link. * @param link - The sign-in link from the email. - * @returns {Promise} A promise containing the user credential. + * @returns {Promise} A promise containing the user credential, or void if handled. */ -export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: string): Promise { +export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: string): Promise { const credential = EmailAuthProvider.credentialWithLink(email, link); return signInWithCredential(ui, credential); } @@ -308,19 +348,17 @@ export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: s * * @param ui - The FirebaseUI instance. * @param credential - The authentication credential to sign in with. - * @returns {Promise} A promise containing the user credential. + * @returns {Promise} A promise containing the user credential, or void if handled. */ -export async function signInWithCredential(ui: FirebaseUI, credential: AuthCredential): Promise { +export async function signInWithCredential(ui: FirebaseUI, credential: AuthCredential): Promise { try { setPendingState(ui); - if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) { - const userCredential = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential); - - // If they got here, they're either not anonymous or they've been linked. - // If the credential has been linked, we don't need to sign them in, so return early. - if (userCredential) { - return handlePendingCredential(ui, userCredential); - } + const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential); + if (upgrade.status === "upgraded") { + return handlePendingCredential(ui, upgrade.credential); + } + if (upgrade.status === "stopped") { + return; } const result = await _signInWithCredential(ui.auth, credential); @@ -377,19 +415,17 @@ export async function signInAnonymously(ui: FirebaseUI): Promise * * @param ui - The FirebaseUI instance. * @param provider - The authentication provider to sign in with. - * @returns {Promise} A promise containing the user credential, or never if using redirect strategy. + * @returns {Promise} A promise containing the user credential, or void if handled. */ -export async function signInWithProvider(ui: FirebaseUI, provider: AuthProvider): Promise { +export async function signInWithProvider(ui: FirebaseUI, provider: AuthProvider): Promise { try { setPendingState(ui); - if (hasBehavior(ui, "autoUpgradeAnonymousProvider")) { - const credential = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider); - - // If we got here, the user is either not anonymous, or they have been linked - // via a popup, and the credential has been created. - if (credential) { - return handlePendingCredential(ui, credential); - } + const upgrade = await attemptAnonymousProviderUpgrade(ui, provider); + if (upgrade.status === "upgraded") { + return handlePendingCredential(ui, upgrade.credential); + } + if (upgrade.status === "stopped") { + return; } const strategy = getBehavior(ui, "providerSignInStrategy"); @@ -424,9 +460,9 @@ export async function completeEmailLinkSignIn(ui: FirebaseUI, currentUrl: string const email = window.localStorage.getItem("emailForSignIn"); if (!email) return null; - // signInWithEmailLink handles behavior checks, credential creation, and error handling + // signInWithEmailLink handles behavior checks, credential creation, and error handling. const result = await signInWithEmailLink(ui, email, currentUrl); - return handlePendingCredential(ui, result); + return result ?? null; } finally { window.localStorage.removeItem("emailForSignIn"); } diff --git a/packages/core/src/behaviors/anonymous-upgrade.ts b/packages/core/src/behaviors/anonymous-upgrade.ts index 7699a4c38..7aaaaa4a5 100644 --- a/packages/core/src/behaviors/anonymous-upgrade.ts +++ b/packages/core/src/behaviors/anonymous-upgrade.ts @@ -51,7 +51,7 @@ export const autoUpgradeAnonymousCredentialHandler = async ( credential: AuthCredential, onUpgrade?: OnUpgradeCallback, onUpgradeFailure?: OnUpgradeFailureCallback -) => { +): Promise => { const currentUser = ui.auth.currentUser; if (!currentUser?.isAnonymous) { @@ -84,7 +84,7 @@ export const autoUpgradeAnonymousProviderHandler = async ( provider: AuthProvider, onUpgrade?: OnUpgradeCallback, onUpgradeFailure?: OnUpgradeFailureCallback -) => { +): Promise => { const currentUser = ui.auth.currentUser; if (!currentUser?.isAnonymous) { diff --git a/packages/core/src/behaviors/index.ts b/packages/core/src/behaviors/index.ts index ada2e251f..8f005ffc2 100644 --- a/packages/core/src/behaviors/index.ts +++ b/packages/core/src/behaviors/index.ts @@ -32,6 +32,13 @@ import { type RedirectBehavior, } from "./utils"; +export type { + OnUpgradeCallback, + OnUpgradeFailureCallback, + OnUpgradeFailureContext, + OnUpgradeFailureResult, +} from "./anonymous-upgrade"; + type Registry = { autoAnonymousLogin: InitBehavior; autoUpgradeAnonymousCredential: CallableBehavior<