ReactJS

Google, Facebook, and OTP Login Integration using Firebase in React : Part 3

Ridham Kansara
Ridham KansaraNov 25, 2025

Introduction

This final part completes our authentication system by adding phone/SMS verification. Building on the architecture from Part 1 (Google Auth) and Part 2 (Facebook Auth), we’ll implement secure phone number authentication with OTP verification.

Phone authentication provides :

  • Universal accessibility (no social media accounts required).
  • High security through device possession.
  • Global reach across all countries.
  • Two-factor authentication foundation.

Prerequisites

  • Completed authentication foundation from Part 1.
  • Firebase project with phone authentication enabled.
  • Understanding of reCAPTCHA integration.

Firebase Phone Authentication Setup

Step 1 : Enable Phone Authentication

  1. Firebase Console → Authentication → Sign-in method.
  2. Click “Phone” → Enable.
  3. Add authorized domains (localhost + your domain).

Step 2 : Configure Test Phone Numbers (Optional)

For development testing:

  1. Scroll to “Phone numbers for testing”.
  2. Add: Phone +917575757575, Code 123456.
  3. Use these credentials during development.

Update Components

Update Authentication Service

Extend src/services/authService.ts with phone authentication :

1// Add this import after other imports
2import { signInWithPhoneNumber, RecaptchaVerifier, ConfirmationResult } from 'firebase/auth';
3
4class AuthService {
5  private recaptchaVerifier: RecaptchaVerifier | null = null;
6  private confirmationResult: ConfirmationResult | null = null;
7
8  // ... existing Google/Facebook methods
9
10  // Initialize reCAPTCHA verifier
11  initializeRecaptcha(containerId: string): void {
12    if (this.recaptchaVerifier) {
13      this.recaptchaVerifier.clear();
14    }
15
16    this.recaptchaVerifier = new RecaptchaVerifier(auth, containerId, {
17      size: 'invisible',
18      callback: () => {
19        console.log('reCAPTCHA solved');
20      },
21      'expired-callback': () => {
22        console.log('reCAPTCHA expired');
23        throw new Error('reCAPTCHA expired, please try again');
24      }
25    });
26  }
27
28// Send OTP to phone number
29  async sendOTP(phoneNumber: string): Promise<void> {
30    if (!this.recaptchaVerifier) {
31      throw new Error('reCAPTCHA not initialized');
32    }
33
34    // Validate phone number format
35    const phoneRegex = /^\+[1-9]\d{1,14}$/;
36    if (!phoneRegex.test(phoneNumber)) {
37      throw new Error('Please enter a valid phone number with country code');
38    }
39
40    try {
41      this.confirmationResult = await signInWithPhoneNumber(
42        auth,
43        phoneNumber,
44        this.recaptchaVerifier
45      );
46    } catch (error: any) {
47      console.error('Send OTP error:', error);
48      //You can handle specific errors here
49  }
50
51  // Verify OTP code
52  async verifyOTP(code: string): Promise<AuthUser | null> {
53    if (!this.confirmationResult) {
54      throw new Error('No OTP request found. Please request a new code');
55    }
56
57    // Validate OTP format
58    if (!/^\d{6}$/.test(code)) {
59      throw new Error('OTP must be exactly 6 digits');
60    }
61
62    try {
63      const result = await this.confirmationResult.confirm(code);
64      this.cleanup(); // Clear after successful verification
65      return this.formatUser(result.user);
66    } catch (error: any) {
67      console.error('Verify OTP error:', error);
68      }
69  }
70
71  // Cleanup phone auth resources
72  private cleanup(): void {
73    if (this.recaptchaVerifier) {
74      this.recaptchaVerifier.clear();
75      this.recaptchaVerifier = null;
76    }
77    this.confirmationResult = null;
78  }
79
80// ... rest of existing methods remain same
81}

Update Authentication Service

Extend src/services/authService.ts with phone authentication :

1// Update AuthContextType interface
2
3interface AuthContextType {
4  // ... existing properties
5  sendOTP: (phoneNumber: string) => Promise<void>;
6  verifyOTP: (code: string) => Promise<void>;
7  initializeRecaptcha: (containerId: string) => void;
8}
9
10// Add to AuthProvider component
11export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
12  // ... existing state and effects
13const sendOTP = async (phoneNumber: string): Promise<void> => {
14    try {
15      setError(null);
16      await authService.sendOTP(phoneNumber);
17    } catch (error: any) {
18      setError(error.message);
19      throw error;
20    }
21  };
22
23  const verifyOTP = async (code: string): Promise<void> => {
24    try {
25      setError(null);
26      setLoading(true);
27      await authService.verifyOTP(code);
28    } catch (error: any) {
29      setError(error.message);
30      throw error;
31    } finally {
32      setLoading(false);
33    }
34  };
35
36  const initializeRecaptcha = (containerId: string): void => {
37    authService.initializeRecaptcha(containerId);
38  };
39
40  const value = {
41    // ... existing values
42    sendOTP,
43    verifyOTP,
44    initializeRecaptcha
45  };
46
47  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
48};

Phone Authentication Component

Create src/components/PhoneAuth.tsx :

1// PhoneAuth.tsx
2import { useAuth } from "../../contexts/AuthContext";
3import authService from "../../services/authService";
4
5// Phone Authentication with OTP using Firebase
6const PhoneAuth: React.FC = () => {
7  const { sendOTP, verifyOTP } = useAuth(); // functions from AuthContext
8  const [loading, setLoading] = useState(false);
9  const [error, setError] = useState<string | null>(null);
10  const [phoneNumber, setPhoneNumber] = useState("");
11  const [otpCode, setOtpCode] = useState("");
12  const [step, setStep] = useState<0 | 1>(0); // 0 = enter phone, 1 = enter OTP
13
14  useEffect(() => {
15    // Initialize invisible reCAPTCHA once
16    authService.initializeRecaptcha("recaptcha-container");
17  }, []);
18
19  // Step 1: Send OTP to the entered phone number
20  const handleSendOTP = async () => {
21    if (!/^\+[1-9]\d{1,14}$/.test(phoneNumber)) {
22      return setError("Enter a valid phone number with country code (e.g., +1234567890)");
23    }
24    setLoading(true); setError(null);
25    try {
26      await sendOTP(phoneNumber);
27      setStep(1); // move to OTP step
28    } catch (e: any) {
29      setError(e?.message || "Failed to send OTP");
30    } finally {
31      setLoading(false);
32    }
33  };
34
35  // Step 2: Verify the OTP entered by user
36  const handleVerifyOTP = async () => {
37    if (otpCode.length !== 6) return setError("OTP must be 6 digits");
38    setLoading(true); setError(null);
39    try {
40      await verifyOTP(otpCode);
41      // At this point user is authenticated
42    } catch (e: any) {
43      setError(e?.message || "Invalid OTP code");
44    } finally {
45      setLoading(false);
46    }
47  };
48
49  return (
50    <Box>
51      {step === 0 ? (
52        // Phone Number Input
53        <>
54          <TextField
55            label="Phone Number"
56            placeholder="+1234567890"
57            value={phoneNumber}
58            onChange={(e) => setPhoneNumber(e.target.value)}
59            disabled={loading}
60          />
61          <Button
62            variant="contained"
63            onClick={handleSendOTP}
64            disabled={loading || !phoneNumber.trim()}
65          >
66            {loading ? "Sending OTP..." : "Send OTP"}
67          </Button>
68        </>
69      ) : (
70        // OTP Verification Input
71        <>
72          <Typography variant="body2" sx={{ mb: 2, textAlign: "center" }}>
73            Enter the code sent to <strong>{phoneNumber}</strong>
74          </Typography>
75          <TextField
76            label="OTP Code"
77            value={otpCode}
78            onChange={(e) => setOtpCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
79            disabled={loading}
80            />
81          <Button
82            variant="contained"
83            onClick={handleVerifyOTP}
84            disabled={loading || otpCode.length < 6}
85          >
86            {loading ? "Verifying..." : "Verify"}
87          </Button>
88          <Button size="small" onClick={handleSendOTP} disabled={loading}>
89            Resend OTP
90          </Button>
91        </>
92      )}
93      {/*  reCAPTCHA container (required by Firebase) */}
94      <div id="recaptcha-container" style={{ display: "none" }} />
95    </Box>
96  );
97};
98
99export default PhoneAuth;

Update Authentication Container

Update src/components/auth/AuthenticationContainer.tsx :

1// AuthenticationContainer.tsx
2//Existing imports..
3import PhoneAuth from "./PhoneAuth";
4
5const AuthenticationContainer: React.FC = () => {
6  return (
7      //In this after FacebookAuth add Below line
8      <PhoneAuth />
9  );
10};
11//Other things remain as it is

These are the primary alterations in your code to instantiate phone authentication logic in your application as it currently exists. You may customize the Dashboard and main App files as per your design, but the code logic should remain as established.

Phone Authentication Key Concepts

  • reCAPTCHA Verification :

Firebase requires reCAPTCHA to prevent spam and abuse :

  • Invisible : Runs automatically in background.
  • Visible : Shows challenge when needed.
  • Required : Cannot be disabled for phone auth.
  • Two-Step Process
    1. Send OTP : Validates phone number and sends SMS.
    2. Verify OTP : Confirms user has access to the device.
  • Phone Number Format :

Always use E.164 format : +[country code] [phone number]

  • US : +1234567890
  • UK : +441234567890
  • India : +911234567890

Security Considerations

  • Rate Limiting :

Firebase automatically limits OTP requests to prevent abuse :

  • Maximum 5 SMS per phone number per hour.
  • Exponential backoff for repeated failures.
  • Test Phone Numbers

Use Firebase test numbers in development :

1// Add to Firebase console for testing
2testNumbers: {
3  '+15555555555': '123456',
4  '+15555555556': '654321'
5}

Production Best Practices :

  1. Monitor Usage : Track SMS costs and usage patterns.
  2. Implement Retry Logic : Handle network failures gracefully.
  3. User Education : Clear instructions for international numbers.
  4. Fallback Options : Provide alternative auth methods.

Common Issues and Solutions

reCAPTCHA Not Loading :

1// Ensure proper initialization
2useEffect(() => {
3  const timer = setTimeout(() => {
4    initializeRecaptcha('recaptcha-container');
5  }, 1000);
6  return () => clearTimeout(timer);
7}, []);

SMS Not Received :

  • Check phone number format.
  • Verify carrier compatibility.
  • Consider regional restrictions.
  • Implement resend functionality.

Complete Authentication System :

You now have a comprehensive authentication system with :

  1. Google OAuth : Social login with Google accounts.
  2. Facebook OAuth : Social login with Facebook accounts.
  3. Phone/SMS : Universal authentication via phone numbers.
  4. Unified Architecture : Consistent error handling and user experience.

You can check out the complete code for this part in my GitHub repository here :

https://github.com/Ridham-K/Blog-Code-Examples/tree/main/firebase-auth-demo

Conclusion

The modular architecture created in Part 1 provided for a seamless addition of phone authentication, demonstrating the importance of properly structured authentication services. Your application now offers three of the main authentication methods available while remaining clean and maintainable.

This three-part series has provided a production-ready authentication system able to provide for the differing kinds of requirements of modern web applications.

© 2026 IGNEK. All rights reserved.

Ignek on LinkedInIgnek on InstagramIgnek on FacebookIgnek on YouTubeIgnek on X