Skip to content

Commit 8be837e

Browse files
authored
Merge pull request #1238 from firebase/@invertase/bb-19
2 parents c71378e + f875ba2 commit 8be837e

File tree

2 files changed

+126
-12
lines changed

2 files changed

+126
-12
lines changed

packages/react/src/hooks.test.tsx

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616

1717
import { describe, it, expect, vi, beforeEach } from "vitest";
18-
import { renderHook, act, cleanup } from "@testing-library/react";
18+
import { renderHook, act, cleanup, waitFor } from "@testing-library/react";
19+
import React from "react";
1920
import {
2021
useUI,
2122
useRedirectError,
@@ -25,12 +26,29 @@ import {
2526
useEmailLinkAuthFormSchema,
2627
usePhoneAuthNumberFormSchema,
2728
usePhoneAuthVerifyFormSchema,
29+
useRecaptchaVerifier,
2830
} from "./hooks";
2931
import { createFirebaseUIProvider, createMockUI } from "~/tests/utils";
3032
import { registerLocale, enUs } from "@firebase-ui/translations";
33+
import type { RecaptchaVerifier } from "firebase/auth";
34+
35+
// Mock RecaptchaVerifier from firebase/auth
36+
const mockRender = vi.fn();
37+
const mockVerifier = {
38+
render: mockRender,
39+
} as unknown as RecaptchaVerifier;
40+
41+
vi.mock("firebase/auth", async () => {
42+
const actual = await vi.importActual<typeof import("firebase/auth")>("firebase/auth");
43+
return {
44+
...actual,
45+
RecaptchaVerifier: vi.fn().mockImplementation(() => mockVerifier),
46+
};
47+
});
3148

3249
beforeEach(() => {
3350
vi.clearAllMocks();
51+
mockRender.mockClear();
3452
});
3553

3654
describe("useUI", () => {
@@ -825,3 +843,91 @@ describe("useRedirectError", () => {
825843
expect(result.current).toBeUndefined();
826844
});
827845
});
846+
847+
describe("useRecaptchaVerifier", () => {
848+
beforeEach(() => {
849+
cleanup();
850+
});
851+
852+
it("creates verifier when element is available", async () => {
853+
const mockUI = createMockUI();
854+
const element = document.createElement("div");
855+
const ref = { current: element };
856+
857+
const { result } = renderHook(() => useRecaptchaVerifier(ref), {
858+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
859+
});
860+
861+
await waitFor(() => {
862+
expect(result.current).toBe(mockVerifier);
863+
});
864+
865+
expect(mockRender).toHaveBeenCalledTimes(1);
866+
});
867+
868+
it("returns null when element is not available", () => {
869+
const mockUI = createMockUI();
870+
const ref = { current: null };
871+
872+
const { result } = renderHook(() => useRecaptchaVerifier(ref), {
873+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
874+
});
875+
876+
expect(result.current).toBeNull();
877+
expect(mockRender).not.toHaveBeenCalled();
878+
});
879+
880+
it("does not recreate verifier when ui changes", async () => {
881+
const mockUI = createMockUI();
882+
const element = document.createElement("div");
883+
const ref = { current: element };
884+
885+
const { result, rerender } = renderHook(() => useRecaptchaVerifier(ref), {
886+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
887+
});
888+
889+
await waitFor(() => {
890+
expect(result.current).toBe(mockVerifier);
891+
});
892+
893+
const firstVerifier = result.current;
894+
expect(mockRender).toHaveBeenCalledTimes(1);
895+
896+
act(() => {
897+
mockUI.get().setState("pending");
898+
});
899+
900+
rerender();
901+
902+
expect(result.current).toBe(firstVerifier);
903+
expect(mockRender).toHaveBeenCalledTimes(1);
904+
});
905+
906+
it("recreates verifier when element changes", async () => {
907+
const mockUI = createMockUI();
908+
const element1 = document.createElement("div");
909+
const element2 = document.createElement("div");
910+
911+
const { result, rerender } = renderHook((props) => useRecaptchaVerifier(props.ref), {
912+
initialProps: { ref: { current: element1 } },
913+
wrapper: ({ children }) => createFirebaseUIProvider({ children, ui: mockUI }),
914+
});
915+
916+
await waitFor(() => {
917+
expect(result.current).toBe(mockVerifier);
918+
});
919+
920+
expect(mockRender).toHaveBeenCalledTimes(1);
921+
922+
act(() => {
923+
rerender({ ref: { current: element2 } });
924+
});
925+
926+
// Verifier should be recreated - wait for effect to run
927+
await waitFor(() => {
928+
expect(mockRender).toHaveBeenCalledTimes(2);
929+
});
930+
931+
expect(result.current).toBe(mockVerifier);
932+
});
933+
});

packages/react/src/hooks.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useContext, useMemo, useEffect } from "react";
17+
import { useContext, useMemo, useEffect, useRef } from "react";
18+
import type { RecaptchaVerifier } from "firebase/auth";
1819
import {
1920
createEmailLinkAuthFormSchema,
2021
createForgotPasswordAuthFormSchema,
@@ -27,7 +28,6 @@ import {
2728
createSignInAuthFormSchema,
2829
createSignUpAuthFormSchema,
2930
getBehavior,
30-
hasBehavior,
3131
} from "@firebase-ui/core";
3232
import { FirebaseUIContext } from "./context";
3333

@@ -109,18 +109,26 @@ export function useMultiFactorTotpAuthVerifyFormSchema() {
109109

110110
export function useRecaptchaVerifier(ref: React.RefObject<HTMLDivElement | null>) {
111111
const ui = useUI();
112+
const verifierRef = useRef<RecaptchaVerifier | null>(null);
113+
const uiRef = useRef(ui);
114+
const prevElementRef = useRef<HTMLDivElement | null>(null);
112115

113-
const verifier = useMemo(() => {
114-
return ref.current && hasBehavior(ui, "recaptchaVerification")
115-
? getBehavior(ui, "recaptchaVerification")(ui, ref.current)
116-
: null;
117-
}, [ref, ui]);
116+
uiRef.current = ui;
118117

119118
useEffect(() => {
120-
if (verifier) {
121-
verifier.render();
119+
const currentElement = ref.current;
120+
const currentUI = uiRef.current;
121+
122+
if (currentElement !== prevElementRef.current) {
123+
prevElementRef.current = currentElement;
124+
if (currentElement) {
125+
verifierRef.current = getBehavior(currentUI, "recaptchaVerification")(currentUI, currentElement);
126+
verifierRef.current.render();
127+
} else {
128+
verifierRef.current = null;
129+
}
122130
}
123-
}, [verifier]);
131+
}, [ref]);
124132

125-
return verifier;
133+
return verifierRef.current;
126134
}

0 commit comments

Comments
 (0)