diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 2cfdb487..81109398 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -45,7 +45,7 @@ export { useDeleteUserMutation } from "./useDeleteUserMutation"; // useLinkWithPhoneNumberMutation // useLinkWithPopupMutation // useLinkWithRedirectMutation -// useReauthenticateWithPhoneNumberMutation +export { useReauthenticateWithPhoneNumberMutation } from "./useReauthenticateWithPhoneNumberMutation"; // useReauthenticateWithCredentialMutation // useReauthenticateWithPopupMutation // useReauthenticateWithRedirectMutation diff --git a/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.test.tsx b/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.test.tsx new file mode 100644 index 00000000..cf9d36f5 --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.test.tsx @@ -0,0 +1,155 @@ +import { renderHook, waitFor } from "@testing-library/react"; +import { + reauthenticateWithPhoneNumber, + type User, + type ApplicationVerifier, + type ConfirmationResult, +} from "firebase/auth"; +import { useReauthenticateWithPhoneNumberMutation } from "./useReauthenticateWithPhoneNumberMutation"; +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, + reauthenticateWithPhoneNumber: vi.fn(), + }; +}); + +describe("useReauthenticateWithPhoneNumberMutation", () => { + let user: User; + let appVerifier: ApplicationVerifier; + + const phoneNumber = "+16505550101"; + + beforeEach(async () => { + queryClient.clear(); + vi.resetAllMocks(); + user = { uid: "test-user" } as User; + appVerifier = { type: "recaptcha" } as ApplicationVerifier; + }); + + test("should successfully reauthenticate with correct phone number", async () => { + const confirmationResult = { + verificationId: "123456", + } as ConfirmationResult; + vi.mocked(reauthenticateWithPhoneNumber).mockResolvedValue( + confirmationResult + ); + + const { result } = renderHook( + () => useReauthenticateWithPhoneNumberMutation(appVerifier), + { wrapper } + ); + + result.current.mutate({ user, phoneNumber }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + expect(result.current.data).toBe(confirmationResult); + }); + }); + + test("should fail with incorrect phone number", async () => { + vi.mocked(reauthenticateWithPhoneNumber).mockRejectedValue({ + code: "auth/invalid-phone-number", + }); + + const { result } = renderHook( + () => useReauthenticateWithPhoneNumberMutation(appVerifier), + { wrapper } + ); + + result.current.mutate({ user, phoneNumber: "+123" }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + expect(result.current.error?.code).toBe("auth/invalid-phone-number"); + }); + }); + + test("should call onSuccess callback after successful reauthentication", async () => { + let callbackCalled = false; + const confirmationResult = { + verificationId: "123456", + } as ConfirmationResult; + + vi.mocked(reauthenticateWithPhoneNumber).mockResolvedValue( + confirmationResult + ); + + const { result } = renderHook( + () => + useReauthenticateWithPhoneNumberMutation(appVerifier, { + onSuccess: () => { + callbackCalled = true; + }, + }), + { wrapper } + ); + + result.current.mutate({ user, phoneNumber }); + + 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; + + vi.mocked(reauthenticateWithPhoneNumber).mockRejectedValue({ + code: "auth/invalid-verification-code", + }); + + const { result } = renderHook( + () => + useReauthenticateWithPhoneNumberMutation(appVerifier, { + onError: (error) => { + errorCode = error.code; + }, + }), + { wrapper } + ); + + result.current.mutate({ user, phoneNumber }); + + await waitFor(() => { + expect(result.current.isError).toBe(true); + expect(errorCode).toBe("auth/invalid-verification-code"); + }); + }); + + test("should handle multiple reauthentication attempts", async () => { + const confirmationResult = { + verificationId: "123456", + } as ConfirmationResult; + + vi.mocked(reauthenticateWithPhoneNumber).mockResolvedValue( + confirmationResult + ); + + const { result } = renderHook( + () => useReauthenticateWithPhoneNumberMutation(appVerifier), + { wrapper } + ); + + // First attempt + result.current.mutate({ user, phoneNumber }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + // Second attempt + result.current.mutate({ user, phoneNumber }); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + }); +}); diff --git a/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.ts b/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.ts new file mode 100644 index 00000000..9c8bfd83 --- /dev/null +++ b/packages/react/src/auth/useReauthenticateWithPhoneNumberMutation.ts @@ -0,0 +1,39 @@ +import { useMutation, type UseMutationOptions } from "@tanstack/react-query"; +import { + type AuthError, + type User, + type ApplicationVerifier, + type ConfirmationResult, + reauthenticateWithPhoneNumber, +} from "firebase/auth"; + +type AuthMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useReauthenticateWithPhoneNumberMutation( + appVerifier: ApplicationVerifier, + options?: AuthMutationOptions< + ConfirmationResult, + AuthError, + { + user: User; + phoneNumber: string; + } + > +) { + return useMutation< + ConfirmationResult, + AuthError, + { + user: User; + phoneNumber: string; + } + >({ + ...options, + mutationFn: ({ user, phoneNumber }) => + reauthenticateWithPhoneNumber(user, phoneNumber, appVerifier), + }); +}