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
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
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) {
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;
}