Skip to content

Commit 6bf741d

Browse files
authored
Merge pull request #120 from HassanBahati/ft-add-useSignInWithEmailAndPasswordMutation
feat: add useSignInWithEmailAndPasswordMutation hook
2 parents a21b53a + 4e0470e commit 6bf741d

File tree

3 files changed

+210
-1
lines changed

3 files changed

+210
-1
lines changed

packages/react/src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMuta
2121
export { useSignInAnonymouslyMutation } from "./useSignInAnonymouslyMutation";
2222
export { useSignInWithCredentialMutation } from "./useSignInWithCredentialMutation";
2323
// useSignInWithCustomTokenMutation
24-
// useSignInWithEmailAndPasswordMutation
24+
export { useSignInWithEmailAndPasswordMutation } from "./useSignInWithEmailAndPasswordMutation";
2525
// useSignInWithEmailLinkMutation
2626
// useSignInWithPhoneNumberMutation
2727
// useSignInWithPopupMutation
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import React from "react";
2+
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
3+
import { renderHook, act, waitFor } from "@testing-library/react";
4+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5+
import { useSignInWithEmailAndPasswordMutation } from "./useSignInWithEmailAndPasswordMutation";
6+
import { auth, wipeAuth } from "~/testing-utils";
7+
import { createUserWithEmailAndPassword } from "firebase/auth";
8+
9+
const queryClient = new QueryClient({
10+
defaultOptions: {
11+
queries: { retry: false },
12+
mutations: { retry: false },
13+
},
14+
});
15+
16+
const wrapper = ({ children }: { children: React.ReactNode }) => (
17+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
18+
);
19+
20+
describe("useSignInWithEmailAndPasswordMutation", () => {
21+
beforeEach(async () => {
22+
queryClient.clear();
23+
await wipeAuth();
24+
});
25+
26+
afterEach(async () => {
27+
await auth.signOut();
28+
});
29+
30+
test("successfully signs in with email and password", async () => {
31+
const email = "[email protected]";
32+
const password = "tanstackQueryFirebase#123";
33+
await createUserWithEmailAndPassword(auth, email, password);
34+
35+
const { result } = renderHook(
36+
() => useSignInWithEmailAndPasswordMutation(auth),
37+
{ wrapper }
38+
);
39+
40+
await act(async () => result.current.mutate({ email, password }));
41+
42+
await waitFor(async () => expect(result.current.isSuccess).toBe(true));
43+
44+
expect(result.current.data?.user.email).toBe(email);
45+
});
46+
47+
test("fails to sign in with incorrect password", async () => {
48+
const email = "[email protected]";
49+
const password = "tanstackQueryFirebase#123";
50+
const wrongPassword = "wrongpassword";
51+
52+
await createUserWithEmailAndPassword(auth, email, password);
53+
54+
const { result } = renderHook(
55+
() => useSignInWithEmailAndPasswordMutation(auth),
56+
{ wrapper }
57+
);
58+
59+
await act(async () => {
60+
result.current.mutate({ email, password: wrongPassword });
61+
});
62+
63+
await waitFor(() => expect(result.current.isError).toBe(true));
64+
65+
expect(result.current.error).toBeDefined();
66+
expect(result.current.isSuccess).toBe(false);
67+
// TODO: Assert Firebase error for auth/wrong-password
68+
});
69+
70+
test("fails to sign in with non-existent email", async () => {
71+
const email = "[email protected]";
72+
const password = "tanstackQueryFirebase#123";
73+
74+
const { result } = renderHook(
75+
() => useSignInWithEmailAndPasswordMutation(auth),
76+
{ wrapper }
77+
);
78+
79+
await act(async () => {
80+
result.current.mutate({ email, password });
81+
});
82+
83+
await waitFor(() => expect(result.current.isError).toBe(true));
84+
85+
expect(result.current.error).toBeDefined();
86+
expect(result.current.isSuccess).toBe(false);
87+
// TODO: Assert Firebase error for auth/user-not-found
88+
});
89+
90+
test("handles empty email input", async () => {
91+
const email = "";
92+
const password = "validPassword123";
93+
94+
const { result } = renderHook(
95+
() => useSignInWithEmailAndPasswordMutation(auth),
96+
{ wrapper }
97+
);
98+
99+
await act(async () => {
100+
result.current.mutate({ email, password });
101+
});
102+
103+
await waitFor(() => expect(result.current.isError).toBe(true));
104+
105+
expect(result.current.error).toBeDefined();
106+
expect(result.current.isSuccess).toBe(false);
107+
// TODO: Assert Firebase error for auth/invalid-email
108+
});
109+
110+
test("handles empty password input", async () => {
111+
const email = "[email protected]";
112+
const password = "";
113+
114+
const { result } = renderHook(
115+
() => useSignInWithEmailAndPasswordMutation(auth),
116+
{ wrapper }
117+
);
118+
119+
await act(async () => {
120+
result.current.mutate({ email, password });
121+
});
122+
123+
await waitFor(() => expect(result.current.isError).toBe(true));
124+
125+
expect(result.current.error).toBeDefined();
126+
expect(result.current.isSuccess).toBe(false);
127+
// TODO: Assert Firebase error for auth/missing-password
128+
});
129+
130+
test("handles concurrent sign in attempts", async () => {
131+
const email = "[email protected]";
132+
const password = "tanstackQueryFirebase#123";
133+
134+
await createUserWithEmailAndPassword(auth, email, password);
135+
136+
const { result } = renderHook(
137+
() => useSignInWithEmailAndPasswordMutation(auth),
138+
{ wrapper }
139+
);
140+
141+
// Attempt multiple concurrent sign-ins
142+
await act(async () => {
143+
result.current.mutate({ email, password });
144+
result.current.mutate({ email, password });
145+
});
146+
147+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
148+
149+
expect(result.current.data?.user.email).toBe(email);
150+
});
151+
152+
test("handles sign in with custom mutation options", async () => {
153+
const email = "[email protected]";
154+
const password = "tanstackQueryFirebase#123";
155+
156+
const onSuccessMock = vi.fn();
157+
158+
await createUserWithEmailAndPassword(auth, email, password);
159+
160+
const { result } = renderHook(
161+
() =>
162+
useSignInWithEmailAndPasswordMutation(auth, {
163+
onSuccess: onSuccessMock,
164+
}),
165+
{ wrapper }
166+
);
167+
168+
await act(async () => {
169+
result.current.mutate({ email, password });
170+
});
171+
172+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
173+
174+
expect(onSuccessMock).toHaveBeenCalled();
175+
expect(result.current.data?.user.email).toBe(email);
176+
});
177+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useMutation, type UseMutationOptions } from "@tanstack/react-query";
2+
import {
3+
type Auth,
4+
type AuthError,
5+
signInWithEmailAndPassword,
6+
type UserCredential,
7+
} from "firebase/auth";
8+
9+
type AuthUseMutationOptions<
10+
TData = unknown,
11+
TError = Error,
12+
TVariables = void
13+
> = Omit<UseMutationOptions<TData, TError, TVariables>, "mutationFn">;
14+
15+
export function useSignInWithEmailAndPasswordMutation(
16+
auth: Auth,
17+
options?: AuthUseMutationOptions<
18+
UserCredential,
19+
AuthError,
20+
{ email: string; password: string }
21+
>
22+
) {
23+
return useMutation<
24+
UserCredential,
25+
AuthError,
26+
{ email: string; password: string }
27+
>({
28+
...options,
29+
mutationFn: ({ email, password }) =>
30+
signInWithEmailAndPassword(auth, email, password),
31+
});
32+
}

0 commit comments

Comments
 (0)