From dc2a3a4868e0076838c8b24224a1e895476fdf52 Mon Sep 17 00:00:00 2001 From: HassanBahati Date: Fri, 21 Feb 2025 16:37:18 +0300 Subject: [PATCH] feat(react/auth): add useReauthenticateWithPopupMutation --- packages/react/src/auth/index.ts | 1 + ...seReauthenticateWithPopupMutation.test.tsx | 142 ++++++++++++++++++ .../useReauthenticateWithPopupMutation.ts | 40 +++++ 3 files changed, 183 insertions(+) create mode 100644 packages/react/src/auth/useReauthenticateWithPopupMutation.test.tsx create mode 100644 packages/react/src/auth/useReauthenticateWithPopupMutation.ts diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 2cfdb487..874db29a 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -48,6 +48,7 @@ export { useDeleteUserMutation } from "./useDeleteUserMutation"; // useReauthenticateWithPhoneNumberMutation // useReauthenticateWithCredentialMutation // useReauthenticateWithPopupMutation +export { useReauthenticateWithPopupMutation } from "./useReauthenticateWithPopupMutation"; // useReauthenticateWithRedirectMutation export { useReloadMutation } from "./useReloadMutation"; // useSendEmailVerificationMutation diff --git a/packages/react/src/auth/useReauthenticateWithPopupMutation.test.tsx b/packages/react/src/auth/useReauthenticateWithPopupMutation.test.tsx new file mode 100644 index 00000000..b2bdfcef --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithPopupMutation.test.tsx @@ -0,0 +1,142 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { + GoogleAuthProvider, + type User, + type UserCredential, + reauthenticateWithPopup, + AuthError, +} from "firebase/auth"; +import { useReauthenticateWithPopupMutation } from "./useReauthenticateWithPopupMutation"; +import { describe, test, expect, beforeEach, vi } from "vitest"; +import { wrapper, queryClient } from "../../utils"; + +vi.mock("firebase/auth", async () => { + const actual = await vi.importActual( + "firebase/auth" + ); + return { + ...actual, + reauthenticateWithPopup: vi.fn(), + }; +}); + +describe("useReauthenticateWithPopupMutation", () => { + const mockUser = { uid: "test-uid", email: "test@example.com" } as User; + const mockCredential = { + user: mockUser, + providerId: "google.com", + operationType: "reauthenticate", + } as UserCredential; + + beforeEach(async () => { + queryClient.clear(); + vi.clearAllMocks(); + }); + + test("should successfully reauthenticate with popup", async () => { + const provider = new GoogleAuthProvider(); + vi.mocked(reauthenticateWithPopup).mockResolvedValueOnce(mockCredential); + + const { result } = renderHook( + () => useReauthenticateWithPopupMutation(provider), + { wrapper } + ); + + result.current.mutate({ + user: mockUser, + }); + + await waitFor(() => { + expect(reauthenticateWithPopup).toHaveBeenCalledWith( + mockUser, + provider, + undefined + ); + expect(result.current.data).toBe(mockCredential); + }); + }); + + test("should handle popup closed by user", async () => { + const provider = new GoogleAuthProvider(); + const mockError: Partial = { + code: "auth/popup-closed-by-user", + message: "The popup has been closed by the user", + name: "AuthError", + }; + + vi.mocked(reauthenticateWithPopup).mockRejectedValueOnce(mockError); + + const { result } = renderHook( + () => useReauthenticateWithPopupMutation(provider), + { wrapper } + ); + + result.current.mutate({ + user: mockUser, + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + expect(result.current.error?.code).toBe("auth/popup-closed-by-user"); + }); + }); + + test("should handle optional resolver parameter", async () => { + const provider = new GoogleAuthProvider(); + const mockResolver = { + _popupRedirectResolver: "mock", + }; + + vi.mocked(reauthenticateWithPopup).mockResolvedValueOnce(mockCredential); + + const { result } = renderHook( + () => useReauthenticateWithPopupMutation(provider), + { wrapper } + ); + + result.current.mutate({ + user: mockUser, + resolver: mockResolver, + }); + + await waitFor(() => { + expect(reauthenticateWithPopup).toHaveBeenCalledWith( + mockUser, + provider, + mockResolver + ); + }); + }); + + test("should call lifecycle hooks in correct order", async () => { + const provider = new GoogleAuthProvider(); + const lifecycleCalls: string[] = []; + + vi.mocked(reauthenticateWithPopup).mockResolvedValueOnce(mockCredential); + + const { result } = renderHook( + () => + useReauthenticateWithPopupMutation(provider, { + onSettled: () => { + lifecycleCalls.push("onSettled"); + }, + onSuccess: () => { + lifecycleCalls.push("onSuccess"); + }, + onError: () => { + lifecycleCalls.push("onError"); + }, + }), + { wrapper } + ); + + result.current.mutate({ + user: mockUser, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(lifecycleCalls).toEqual(["onSuccess", "onSettled"]); + }); + }); +}); diff --git a/packages/react/src/auth/useReauthenticateWithPopupMutation.ts b/packages/react/src/auth/useReauthenticateWithPopupMutation.ts new file mode 100644 index 00000000..0124a239 --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithPopupMutation.ts @@ -0,0 +1,40 @@ +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { + type AuthProvider, + type AuthError, + type User, + type PopupRedirectResolver, + UserCredential, + reauthenticateWithPopup, +} from "firebase/auth"; + +type AuthMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useReauthenticateWithPopupMutation( + provider: AuthProvider, + options?: AuthMutationOptions< + UserCredential, + AuthError, + { + user: User; + resolver?: PopupRedirectResolver; + } + > +) { + return useMutation< + UserCredential, + AuthError, + { + user: User; + resolver?: PopupRedirectResolver; + } + >({ + ...options, + mutationFn: ({ user, resolver }) => + reauthenticateWithPopup(user, provider, resolver), + }); +}