diff --git a/packages/react/src/auth/index.ts b/packages/react/src/auth/index.ts index 363308f3..2cfdb487 100644 --- a/packages/react/src/auth/index.ts +++ b/packages/react/src/auth/index.ts @@ -13,6 +13,9 @@ export { useCheckActionCodeMutation } from "./useCheckActionCodeMutation"; export { useApplyActionCodeMutation } from "./useApplyActionCodeMutation"; // useCheckActionCodeMutation +// useConfirmPasswordResetMutation +export { useCreateUserWithEmailAndPasswordMutation } from "./useCreateUserWithEmailAndPasswordMutation"; +// useFetchSignInMethodsForEmailQuery export { useConfirmPasswordResetMutation } from "./useConfirmPasswordResetMutation"; // useCreateUserWithEmailAndPasswordMutation // useGetRedirectResultQuery diff --git a/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.test.tsx b/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.test.tsx new file mode 100644 index 00000000..82089255 --- /dev/null +++ b/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.test.tsx @@ -0,0 +1,141 @@ +import { act, renderHook, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, test, vi, beforeEach } from "vitest"; +import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils"; +import { useCreateUserWithEmailAndPasswordMutation } from "./useCreateUserWithEmailAndPasswordMutation"; +import { queryClient, wrapper } from "../../utils"; + +describe("useCreateUserWithEmailAndPasswordMutation", () => { + const email = "tqf@invertase.io"; + const password = "TanstackQueryFirebase#123"; + + beforeEach(async () => { + queryClient.clear(); + await wipeAuth(); + }); + + afterEach(async () => { + vi.clearAllMocks(); + }); + + test("successfully creates user", async () => { + const { result } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth), + { + wrapper, + } + ); + + await act(async () => { + const userCredential = await result.current.mutateAsync({ + email, + password, + }); + expect(userCredential.user.email).toBe(email); + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + }); + + test("handles existing user", async () => { + const { result: firstAttempt } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth), + { + wrapper, + } + ); + + await act(async () => { + await firstAttempt.current.mutateAsync({ email, password }); + }); + + const { result } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth), + { + wrapper, + } + ); + + await act(async () => { + try { + await result.current.mutateAsync({ email, password }); + } catch (error) { + expectFirebaseError(error, "auth/email-already-in-use"); + } + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expectFirebaseError(result.current.error, "auth/email-already-in-use"); + }); + + test("handles weak password", async () => { + const weakPassword = "weak"; + const { result } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth), + { + wrapper, + } + ); + + await act(async () => { + try { + await result.current.mutateAsync({ email, password: weakPassword }); + } catch (error) { + expectFirebaseError(error, "auth/weak-password"); + } + }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expectFirebaseError(result.current.error, "auth/weak-password"); + }); + + test("executes onSuccess callback", async () => { + const onSuccess = vi.fn(); + + const { result } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth, { onSuccess }), + { wrapper } + ); + + await act(async () => { + await result.current.mutateAsync({ email, password }); + }); + + await waitFor(() => expect(onSuccess).toHaveBeenCalled()); + }); + + test("executes onError callback", async () => { + const existingEmail = "tqf@invertase.io"; + const onError = vi.fn(); + + const { result: firstAttempt } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth), + { + wrapper, + } + ); + + await act(async () => { + await firstAttempt.current.mutateAsync({ + email: existingEmail, + password, + }); + }); + + const { result } = renderHook( + () => useCreateUserWithEmailAndPasswordMutation(auth, { onError }), + { wrapper } + ); + + await act(async () => { + try { + await result.current.mutateAsync({ email: existingEmail, password }); + } catch (error) { + expectFirebaseError(error, "auth/email-already-in-use"); + } + }); + + await waitFor(() => expect(onError).toHaveBeenCalled()); + expect(onError.mock.calls[0][0]).toBeDefined(); + expectFirebaseError(result.current.error, "auth/email-already-in-use"); + }); +}); diff --git a/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.ts b/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.ts new file mode 100644 index 00000000..d7c0aa8c --- /dev/null +++ b/packages/react/src/auth/useCreateUserWithEmailAndPasswordMutation.ts @@ -0,0 +1,32 @@ +import { type UseMutationOptions, useMutation } from "@tanstack/react-query"; +import { + type Auth, + type AuthError, + type UserCredential, + createUserWithEmailAndPassword, +} from "firebase/auth"; + +type AuthUseMutationOptions< + TData = unknown, + TError = Error, + TVariables = void +> = Omit, "mutationFn">; + +export function useCreateUserWithEmailAndPasswordMutation( + auth: Auth, + options?: AuthUseMutationOptions< + UserCredential, + AuthError, + { email: string; password: string } + > +) { + return useMutation< + UserCredential, + AuthError, + { email: string; password: string } + >({ + ...options, + mutationFn: ({ email, password }) => + createUserWithEmailAndPassword(auth, email, password), + }); +}