Skip to content

Commit fd4b470

Browse files
committed
feat(useSignInWithEmailAndPasswordMutation): add useSignInWithEmailAndPasswordMutation hook
1 parent 8a9daa1 commit fd4b470

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

0 commit comments

Comments
 (0)