diff --git a/packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx b/packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx index 761e9514..ce7a4d71 100644 --- a/packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx +++ b/packages/react/src/auth/forms/mfa/sms-multi-factor-assertion-form.tsx @@ -110,7 +110,9 @@ function SmsMultiFactorAssertionPhoneForm(props: SmsMultiFactorAssertionPhoneFor onSubmit={async (e) => { e.preventDefault(); e.stopPropagation(); - await form.handleSubmit(); + if (recaptchaVerifier) { + await form.handleSubmit(); + } }} > @@ -127,7 +129,9 @@ function SmsMultiFactorAssertionPhoneForm(props: SmsMultiFactorAssertionPhoneFor
- {getTranslation(ui, "labels", "sendCode")} + + {getTranslation(ui, "labels", "sendCode")} +
diff --git a/packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.tsx b/packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.tsx index 3b383955..ae54edca 100644 --- a/packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.tsx +++ b/packages/react/src/auth/forms/mfa/sms-multi-factor-enrollment-form.tsx @@ -114,7 +114,9 @@ function MultiFactorEnrollmentPhoneNumberForm(props: MultiFactorEnrollmentPhoneN onSubmit={async (e) => { e.preventDefault(); e.stopPropagation(); - await form.handleSubmit(); + if (recaptchaVerifier) { + await form.handleSubmit(); + } }} > @@ -138,7 +140,9 @@ function MultiFactorEnrollmentPhoneNumberForm(props: MultiFactorEnrollmentPhoneN
- {getTranslation(ui, "labels", "sendCode")} + + {getTranslation(ui, "labels", "sendCode")} +
diff --git a/packages/react/src/auth/forms/phone-auth-form.test.tsx b/packages/react/src/auth/forms/phone-auth-form.test.tsx index 7bd9d6fb..ffb83d20 100644 --- a/packages/react/src/auth/forms/phone-auth-form.test.tsx +++ b/packages/react/src/auth/forms/phone-auth-form.test.tsx @@ -78,6 +78,7 @@ import { verifyPhoneNumber, confirmPhoneNumber } from "@firebase-oss/ui-core"; import { createFirebaseUIProvider, createMockUI } from "~/tests/utils"; import { registerLocale } from "@firebase-oss/ui-translations"; import { FirebaseUIProvider } from "~/context"; +import { useRecaptchaVerifier } from "~/hooks"; vi.mock("~/components/country-selector", () => ({ CountrySelector: vi.fn().mockImplementation(({ value, onChange, ref }: any) => { @@ -578,6 +579,18 @@ describe("", () => { const mockVerificationId = "test-verification-id"; vi.mocked(verifyPhoneNumber).mockResolvedValue(mockVerificationId); + // Create a mock verifier that simulates async render() + const mockVerifier = { + render: vi.fn().mockResolvedValue(123), + clear: vi.fn(), + verify: vi.fn().mockResolvedValue("verification-token"), + }; + + // Override the global mock to return our specific verifier + vi.mocked(useRecaptchaVerifier).mockReturnValue( + mockVerifier as unknown as import("firebase/auth").RecaptchaVerifier + ); + const { container } = render( @@ -591,9 +604,34 @@ describe("", () => { await act(async () => { fireEvent.change(phoneInput, { target: { value: "1234567890" } }); + }); + + // Check if there are any validation errors before submitting + const errorBeforeSubmit = screen.queryByTestId("error-message"); + if (errorBeforeSubmit) { + throw new Error(`Form has validation error before submit: ${errorBeforeSubmit.textContent}`); + } + + await act(async () => { fireEvent.click(sendCodeButton); }); + // Wait for the async form submission to complete + await waitFor( + () => { + expect(verifyPhoneNumber).toHaveBeenCalled(); + }, + { timeout: 3000 } + ); + + // Verify that verifyPhoneNumber was called with the verifier + // Note: The phone number gets formatted, so we check for the formatted version + expect(verifyPhoneNumber).toHaveBeenCalledWith( + expect.anything(), + expect.stringMatching(/1.*234.*567.*890/), // Matches formatted phone number like "1(234)567-890" or "+11234567890" + mockVerifier + ); + const verificationInput = await waitFor(() => { return screen.getByRole("textbox", { name: /verificationCode/i }); }); diff --git a/packages/react/src/auth/forms/phone-auth-form.tsx b/packages/react/src/auth/forms/phone-auth-form.tsx index 0553c457..8ba05ed5 100644 --- a/packages/react/src/auth/forms/phone-auth-form.tsx +++ b/packages/react/src/auth/forms/phone-auth-form.tsx @@ -103,6 +103,7 @@ export function PhoneNumberForm(props: PhoneNumberFormProps) { const recaptchaContainerRef = useRef(null); const recaptchaVerifier = useRecaptchaVerifier(recaptchaContainerRef); const countrySelector = useRef(null); + const form = usePhoneNumberForm({ recaptchaVerifier: recaptchaVerifier!, onSuccess: props.onSubmit, @@ -115,7 +116,9 @@ export function PhoneNumberForm(props: PhoneNumberFormProps) { onSubmit={async (e) => { e.preventDefault(); e.stopPropagation(); - await form.handleSubmit(); + if (recaptchaVerifier) { + await form.handleSubmit(); + } }} > @@ -135,7 +138,9 @@ export function PhoneNumberForm(props: PhoneNumberFormProps) {
- {getTranslation(ui, "labels", "sendCode")} + + {getTranslation(ui, "labels", "sendCode")} +
diff --git a/packages/react/src/hooks.ts b/packages/react/src/hooks.ts index 75cb777c..e4531564 100644 --- a/packages/react/src/hooks.ts +++ b/packages/react/src/hooks.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useContext, useMemo, useEffect, useRef } from "react"; +import { useContext, useMemo, useEffect, useRef, useState } from "react"; import type { RecaptchaVerifier, User } from "firebase/auth"; import { createEmailLinkAuthFormSchema, @@ -200,7 +200,7 @@ export function useMultiFactorTotpAuthVerifyFormSchema() { */ export function useRecaptchaVerifier(ref: React.RefObject) { const ui = useUI(); - const verifierRef = useRef(null); + const [verifier, setVerifier] = useState(null); const uiRef = useRef(ui); const prevElementRef = useRef(null); @@ -213,13 +213,19 @@ export function useRecaptchaVerifier(ref: React.RefObject if (currentElement !== prevElementRef.current) { prevElementRef.current = currentElement; if (currentElement) { - verifierRef.current = getBehavior(currentUI, "recaptchaVerification")(currentUI, currentElement); - verifierRef.current.render(); + try { + const newVerifier = getBehavior(currentUI, "recaptchaVerification")(currentUI, currentElement); + newVerifier.render(); + setVerifier(newVerifier); + } catch (error) { + console.error("[useRecaptchaVerifier] Failed to create/render verifier:", error); + setVerifier(null); + } } else { - verifierRef.current = null; + setVerifier(null); } } }, [ref]); - return verifierRef.current; + return verifier; }