Skip to content

Commit 37ea606

Browse files
authored
feat(react/auth): add useGetRedirectResultQuery (#162)
1 parent 8d9b259 commit 37ea606

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

packages/react/src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export { useApplyActionCodeMutation } from "./useApplyActionCodeMutation";
1616
// useConfirmPasswordResetMutation
1717
// useCreateUserWithEmailAndPasswordMutation
1818
// useFetchSignInMethodsForEmailQuery
19-
// useGetRedirectResultQuery
19+
export { useGetRedirectResultQuery } from "./useGetRedirectResultQuery";
2020
// useRevokeAccessTokenMutation
2121
// useSendPasswordResetEmailMutation
2222
export { useSendSignInLinkToEmailMutation } from "./useSendSignInLinkToEmailMutation";
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { renderHook, waitFor } from "@testing-library/react";
2+
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
3+
import {
4+
type UserCredential,
5+
type PopupRedirectResolver,
6+
getRedirectResult,
7+
} from "firebase/auth";
8+
import { auth, wipeAuth } from "~/testing-utils";
9+
import { useGetRedirectResultQuery } from "./useGetRedirectResultQuery";
10+
import { queryClient, wrapper } from "../../utils";
11+
12+
vi.mock("firebase/auth", async () => {
13+
const actual = await vi.importActual("firebase/auth");
14+
return {
15+
...actual,
16+
getRedirectResult: vi.fn(),
17+
};
18+
});
19+
20+
describe("useGetRedirectResultQuery", () => {
21+
beforeEach(async () => {
22+
queryClient.clear();
23+
await wipeAuth();
24+
});
25+
26+
afterEach(async () => {
27+
vi.clearAllMocks();
28+
});
29+
30+
test("returns user credential on successful redirect", async () => {
31+
const mockUserCredential = {
32+
user: {
33+
uid: "test-uid",
34+
35+
},
36+
operationType: "signIn",
37+
providerId: "google.com",
38+
} as unknown as UserCredential;
39+
40+
vi.mocked(getRedirectResult).mockResolvedValueOnce(mockUserCredential);
41+
42+
const { result } = renderHook(
43+
() =>
44+
useGetRedirectResultQuery(auth, {
45+
queryKey: ["redirectResult"],
46+
}),
47+
{ wrapper }
48+
);
49+
50+
expect(result.current.isLoading).toBe(true);
51+
52+
await waitFor(() => {
53+
expect(result.current.isSuccess).toBe(true);
54+
});
55+
56+
expect(result.current.data).toEqual(mockUserCredential);
57+
expect(getRedirectResult).toHaveBeenCalledWith(auth, undefined);
58+
});
59+
60+
test("uses custom resolver when provided", async () => {
61+
const mockResolver = {} as PopupRedirectResolver;
62+
const mockUserCredential = {
63+
user: {
64+
uid: "test-uid",
65+
66+
},
67+
} as unknown as UserCredential;
68+
69+
vi.mocked(getRedirectResult).mockResolvedValueOnce(mockUserCredential);
70+
71+
const { result } = renderHook(
72+
() =>
73+
useGetRedirectResultQuery(auth, {
74+
queryKey: ["redirectResult"],
75+
auth: { resolver: mockResolver },
76+
}),
77+
{ wrapper }
78+
);
79+
80+
await waitFor(() => {
81+
expect(result.current.isSuccess).toBe(true);
82+
});
83+
84+
expect(getRedirectResult).toHaveBeenCalledWith(auth, mockResolver);
85+
});
86+
87+
test("uses different query keys for different configs", async () => {
88+
const mockUserCredential1 = {
89+
user: { uid: "user1" },
90+
} as unknown as UserCredential;
91+
92+
const mockUserCredential2 = {
93+
user: { uid: "user2" },
94+
} as unknown as UserCredential;
95+
96+
vi.mocked(getRedirectResult)
97+
.mockResolvedValueOnce(mockUserCredential1)
98+
.mockResolvedValueOnce(mockUserCredential2);
99+
100+
// Render first hook with default key
101+
const { result: result1 } = renderHook(
102+
() =>
103+
useGetRedirectResultQuery(auth, {
104+
queryKey: ["redirectResult", "config1"],
105+
}),
106+
{ wrapper }
107+
);
108+
109+
// Render second hook with different key
110+
const { result: result2 } = renderHook(
111+
() =>
112+
useGetRedirectResultQuery(auth, {
113+
queryKey: ["redirectResult", "config2"],
114+
}),
115+
{ wrapper }
116+
);
117+
118+
await waitFor(() => {
119+
expect(result1.current.isSuccess).toBe(true);
120+
expect(result2.current.isSuccess).toBe(true);
121+
});
122+
123+
expect(result1.current.data).toEqual(mockUserCredential1);
124+
expect(result2.current.data).toEqual(mockUserCredential2);
125+
expect(getRedirectResult).toHaveBeenCalledTimes(2);
126+
});
127+
128+
test("adheres to enabled option", async () => {
129+
const mockUserCredential = {
130+
user: { uid: "test-uid" },
131+
} as unknown as UserCredential;
132+
133+
vi.mocked(getRedirectResult).mockResolvedValueOnce(mockUserCredential);
134+
135+
const { result } = renderHook(
136+
() =>
137+
useGetRedirectResultQuery(auth, {
138+
queryKey: ["redirectResult"],
139+
enabled: false,
140+
}),
141+
{ wrapper }
142+
);
143+
144+
expect(result.current.isLoading).toBe(false);
145+
expect(getRedirectResult).not.toHaveBeenCalled();
146+
});
147+
148+
test("shares data between hooks with same query key", async () => {
149+
const mockUserCredential = {
150+
user: { uid: "test-uid" },
151+
} as unknown as UserCredential;
152+
153+
vi.mocked(getRedirectResult).mockResolvedValueOnce(mockUserCredential);
154+
155+
// Render first instance
156+
const { result: result1 } = renderHook(
157+
() =>
158+
useGetRedirectResultQuery(auth, {
159+
queryKey: ["redirectResult"],
160+
}),
161+
{ wrapper }
162+
);
163+
164+
// Render second instance with same key
165+
const { result: result2 } = renderHook(
166+
() =>
167+
useGetRedirectResultQuery(auth, {
168+
queryKey: ["redirectResult"],
169+
}),
170+
{ wrapper }
171+
);
172+
173+
await waitFor(() => {
174+
expect(result1.current.isSuccess).toBe(true);
175+
expect(result2.current.isSuccess).toBe(true);
176+
});
177+
178+
expect(result1.current.data).toEqual(result2.current.data);
179+
expect(getRedirectResult).toHaveBeenCalledTimes(1);
180+
});
181+
182+
test("stale time prevents refetch", async () => {
183+
const mockUserCredential = {
184+
user: { uid: "test-uid" },
185+
} as unknown as UserCredential;
186+
187+
vi.mocked(getRedirectResult).mockResolvedValue(mockUserCredential);
188+
189+
const { result, rerender } = renderHook(
190+
() =>
191+
useGetRedirectResultQuery(auth, {
192+
queryKey: ["redirectResult"],
193+
staleTime: 1000,
194+
}),
195+
{ wrapper }
196+
);
197+
198+
await waitFor(() => {
199+
expect(result.current.isSuccess).toBe(true);
200+
});
201+
202+
// Rerender while data is still fresh
203+
rerender();
204+
205+
expect(getRedirectResult).toHaveBeenCalledTimes(1);
206+
});
207+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
2+
import {
3+
type Auth,
4+
type AuthError,
5+
getRedirectResult,
6+
type PopupRedirectResolver,
7+
type UserCredential,
8+
} from "firebase/auth";
9+
10+
type AuthUseQueryOptions<TData = unknown, TError = Error> = Omit<
11+
UseQueryOptions<TData, TError, void>,
12+
"queryFn"
13+
> & { auth?: { resolver?: PopupRedirectResolver } };
14+
15+
export function useGetRedirectResultQuery(
16+
auth: Auth,
17+
options: AuthUseQueryOptions<UserCredential | null, AuthError>
18+
) {
19+
const { auth: authOptions, ...queryOptions } = options;
20+
const resolver = authOptions?.resolver;
21+
22+
return useQuery<UserCredential | null, AuthError, void>({
23+
...queryOptions,
24+
queryFn: () => getRedirectResult(auth, resolver),
25+
});
26+
}

0 commit comments

Comments
 (0)