Skip to content

Commit 5e62532

Browse files
HassanBahatiEhesp
andauthored
feat(react): add useCheckActionCodeMutation (#151)
Co-authored-by: Elliot Hesp <[email protected]>
1 parent 4683a20 commit 5e62532

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

packages/react/src/auth/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
// useMultiFactorUserUnenrollMutation (MultiFactorUser)
1010
// useMultiFactorUserGetSessionMutation (MultiFactorUser)
1111
// useMultiFactorResolverResolveSignInMutation (MultiFactorResolver)
12+
// useApplyActionCodeMutation
13+
export { useCheckActionCodeMutation } from "./useCheckActionCodeMutation";
1214
export { useApplyActionCodeMutation } from "./useApplyActionCodeMutation";
1315
// useCheckActionCodeMutation
1416
// useConfirmPasswordResetMutation
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { act, renderHook, waitFor } from "@testing-library/react";
2+
import {
3+
createUserWithEmailAndPassword,
4+
sendPasswordResetEmail,
5+
type ActionCodeInfo,
6+
} from "firebase/auth";
7+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
8+
import { auth, expectFirebaseError, wipeAuth } from "~/testing-utils";
9+
import { useCheckActionCodeMutation } from "./useCheckActionCodeMutation";
10+
import { waitForPasswordResetCode } from "./utils";
11+
import { queryClient, wrapper } from "../../utils";
12+
13+
describe("useCheckActionCodeMutation", () => {
14+
const email = "[email protected]";
15+
const password = "TanstackQueryFirebase#123";
16+
17+
beforeEach(async () => {
18+
queryClient.clear();
19+
await wipeAuth();
20+
await createUserWithEmailAndPassword(auth, email, password);
21+
});
22+
23+
afterEach(async () => {
24+
vi.clearAllMocks();
25+
await auth.signOut();
26+
});
27+
28+
test("successfully checks password reset action code", async () => {
29+
await sendPasswordResetEmail(auth, email);
30+
const oobCode = await waitForPasswordResetCode(email);
31+
32+
if (!oobCode) {
33+
throw new Error("oobCode is null");
34+
}
35+
36+
const { result } = renderHook(() => useCheckActionCodeMutation(auth), {
37+
wrapper,
38+
});
39+
40+
await act(async () => {
41+
await result.current.mutateAsync(oobCode);
42+
});
43+
44+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
45+
46+
const actionCodeInfo = result.current.data as ActionCodeInfo;
47+
expect(actionCodeInfo.operation).toBe("PASSWORD_RESET");
48+
expect(actionCodeInfo.data.email).toBe(email);
49+
});
50+
51+
test("handles invalid action code", async () => {
52+
const invalidCode = "invalid-action-code";
53+
54+
const { result } = renderHook(() => useCheckActionCodeMutation(auth), {
55+
wrapper,
56+
});
57+
58+
await act(async () => {
59+
try {
60+
await result.current.mutateAsync(invalidCode);
61+
} catch (error) {
62+
expectFirebaseError(error, "auth/invalid-action-code");
63+
}
64+
});
65+
66+
await waitFor(() => expect(result.current.isError).toBe(true));
67+
expect(result.current.error).toBeDefined();
68+
expectFirebaseError(result.current.error, "auth/invalid-action-code");
69+
});
70+
71+
test("handles empty action code", async () => {
72+
const { result } = renderHook(() => useCheckActionCodeMutation(auth), {
73+
wrapper,
74+
});
75+
76+
await act(async () => {
77+
try {
78+
await result.current.mutateAsync("");
79+
} catch (error) {
80+
expectFirebaseError(error, "auth/internal-error");
81+
}
82+
});
83+
84+
await waitFor(() => expect(result.current.isError).toBe(true));
85+
expect(result.current.error).toBeDefined();
86+
expectFirebaseError(result.current.error, "auth/internal-error");
87+
});
88+
89+
test("executes onSuccess callback", async () => {
90+
await sendPasswordResetEmail(auth, email);
91+
const oobCode = await waitForPasswordResetCode(email);
92+
const onSuccess = vi.fn();
93+
94+
if (!oobCode) {
95+
throw new Error("oobCode is null");
96+
}
97+
98+
const { result } = renderHook(
99+
() => useCheckActionCodeMutation(auth, { onSuccess }),
100+
{ wrapper }
101+
);
102+
103+
await act(async () => {
104+
await result.current.mutateAsync(oobCode);
105+
});
106+
107+
await waitFor(() => expect(onSuccess).toHaveBeenCalled());
108+
109+
const actionCodeInfo = onSuccess.mock.calls[0][0] as ActionCodeInfo;
110+
expect(actionCodeInfo.operation).toBe("PASSWORD_RESET");
111+
expect(actionCodeInfo.data.email).toBe(email);
112+
});
113+
114+
test("executes onError callback", async () => {
115+
const invalidCode = "invalid-action-code";
116+
const onError = vi.fn();
117+
118+
const { result } = renderHook(
119+
() => useCheckActionCodeMutation(auth, { onError }),
120+
{ wrapper }
121+
);
122+
123+
await act(async () => {
124+
try {
125+
await result.current.mutateAsync(invalidCode);
126+
} catch (error) {
127+
expectFirebaseError(error, "auth/invalid-action-code");
128+
}
129+
});
130+
131+
await waitFor(() => expect(onError).toHaveBeenCalled());
132+
expect(onError.mock.calls[0][0]).toBeDefined();
133+
expectFirebaseError(result.current.error, "auth/invalid-action-code");
134+
});
135+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type UseMutationOptions, useMutation } from "@tanstack/react-query";
2+
import {
3+
type Auth,
4+
type ActionCodeInfo,
5+
checkActionCode,
6+
type AuthError,
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 useCheckActionCodeMutation(
16+
auth: Auth,
17+
options?: AuthUseMutationOptions<ActionCodeInfo, AuthError, string>
18+
) {
19+
return useMutation<ActionCodeInfo, AuthError, string>({
20+
...options,
21+
mutationFn: (oobCode) => checkActionCode(auth, oobCode),
22+
});
23+
}

0 commit comments

Comments
 (0)