diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 2cfdb487..257b8940 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -46,7 +46,7 @@ export { useDeleteUserMutation } from "./useDeleteUserMutation"; // useLinkWithPopupMutation // useLinkWithRedirectMutation // useReauthenticateWithPhoneNumberMutation -// useReauthenticateWithCredentialMutation +export { useReauthenticateWithCredentialMutation } from "./useReauthenticateWithCredentialMutation"; // useReauthenticateWithPopupMutation // useReauthenticateWithRedirectMutation export { useReloadMutation } from "./useReloadMutation"; diff --git a/packages/react/src/auth/useReauthenticateWithCredentialMutation.test.tsx b/packages/react/src/auth/useReauthenticateWithCredentialMutation.test.tsx new file mode 100644 index 00000000..f1ee3094 --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithCredentialMutation.test.tsx @@ -0,0 +1,164 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { + createUserWithEmailAndPassword, + signOut, + EmailAuthProvider, + type User, +} from "firebase/auth"; +import { useReauthenticateWithCredentialMutation } from "./useReauthenticateWithCredentialMutation"; +import { describe, test, expect, beforeEach, afterEach } from "vitest"; +import { wrapper, queryClient } from "../../utils"; +import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils"; + +describe("useReauthenticateWithCredentialMutation", () => { + let user: User; + + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; + + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + const userCredential = await createUserWithEmailAndPassword( + auth, + email, + password + ); + user = userCredential.user; + }); + + afterEach(async () => { + await signOut(auth); + }); + + test("should successfully reauthenticate with correct credentials", async () => { + const { result } = renderHook( + () => useReauthenticateWithCredentialMutation(), + { wrapper } + ); + + const credential = EmailAuthProvider.credential(email, password); + + result.current.mutate({ + user, + credential, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data?.user.email).toBe(email); + }); + + test("should fail with incorrect credentials", async () => { + const { result } = renderHook( + () => useReauthenticateWithCredentialMutation(), + { wrapper } + ); + + const wrongCredential = EmailAuthProvider.credential( + email, + "wrongPassword" + ); + + result.current.mutate({ + user, + credential: wrongCredential, + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + }); + + expectFirebaseError(result.current.error, "auth/wrong-password"); + }); + + test("should call onSuccess callback after successful reauthentication", async () => { + let callbackCalled = false; + + const { result } = renderHook( + () => + useReauthenticateWithCredentialMutation({ + onSuccess: () => { + callbackCalled = true; + }, + }), + { wrapper } + ); + + const credential = EmailAuthProvider.credential(email, password); + + result.current.mutate({ + user, + credential, + }); + + await waitFor(() => { + expect(callbackCalled).toBe(true); + expect(result.current.isSuccess).toBe(true); + }); + }); + + test("should call onError callback on authentication failure", async () => { + let errorCode: string | undefined; + + const { result } = renderHook( + () => + useReauthenticateWithCredentialMutation({ + onError: (error) => { + errorCode = error.code; + }, + }), + { wrapper } + ); + + const wrongCredential = EmailAuthProvider.credential( + email, + "wrongPassword" + ); + + result.current.mutate({ + user, + credential: wrongCredential, + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + expectFirebaseError(result.current.error, "auth/wrong-password"); + }); + }); + + test("should handle multiple reauthentication attempts", async () => { + const { result } = renderHook( + () => useReauthenticateWithCredentialMutation(), + { wrapper } + ); + + // First attempt - successful + const correctCredential = EmailAuthProvider.credential(email, password); + result.current.mutate({ + user, + credential: correctCredential, + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Second attempt - with wrong password + const wrongCredential = EmailAuthProvider.credential( + email, + "wrongPassword" + ); + result.current.mutate({ + user, + credential: wrongCredential, + }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + expectFirebaseError(result.current.error, "auth/wrong-password"); + }); + }); +}); diff --git a/packages/react/src/auth/useReauthenticateWithCredentialMutation.ts b/packages/react/src/auth/useReauthenticateWithCredentialMutation.ts new file mode 100644 index 00000000..92ffd0d1 --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithCredentialMutation.ts @@ -0,0 +1,29 @@ +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { + type AuthCredential, + reauthenticateWithCredential, + type AuthError, + type User, + type UserCredential, +} from "firebase/auth"; + +type Variables = { + user: User; + credential: AuthCredential; +}; + +type AuthMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useReauthenticateWithCredentialMutation( + options?: AuthMutationOptions +) { + return useMutation({ + ...options, + mutationFn: ({ user, credential }) => + reauthenticateWithCredential(user, credential), + }); +}