Phone Number Authentication

This guide builds upon our Firebase Auth Reference to implement phone number authentication in your Flutter app.

Prerequisites

  1. Firebase project setup with Phone Authentication enabled
  2. SHA-1 and SHA-256 fingerprints added to your Firebase project
  3. Firebase configuration files (google-services.json for Android, GoogleService-Info.plist for iOS)

Implementation Steps

1. Update IAuthService Interface

First, extend the IAuthService interface to include phone authentication methods:

abstract class IAuthService {
  // ... existing methods ...
  
  Future<void> verifyPhoneNumber(String phoneNumber);
  Future<void> verifyOTP(String verificationId, String otp);
  Future<void> resendOTP(String phoneNumber);
}

2. Firebase Implementation

Here’s how to implement phone authentication in your FirebaseAuthService:

class FirebaseAuthService implements IAuthService {
  // ... existing code ...

  String? _verificationId;
  int? _resendToken;

  
  Future<void> verifyPhoneNumber(String phoneNumber) async {
    try {
      await _firebaseAuth.verifyPhoneNumber(
        phoneNumber: phoneNumber,
        verificationCompleted: (PhoneAuthCredential credential) async {
          await _firebaseAuth.signInWithCredential(credential);
        },
        verificationFailed: (FirebaseAuthException e) {
          debugPrint('Verification failed: ${e.message}');
          throw Exception('Phone verification failed: ${e.message}');
        },
        codeSent: (String verificationId, int? resendToken) {
          _verificationId = verificationId;
          _resendToken = resendToken;
        },
        codeAutoRetrievalTimeout: (String verificationId) {
          _verificationId = verificationId;
        },
        timeout: const Duration(seconds: 60),
      );
    } catch (e) {
      debugPrint('Error verifying phone number: $e');
      throw Exception('Failed to verify phone number');
    }
  }

  
  Future<void> verifyOTP(String verificationId, String otp) async {
    try {
      final credential = PhoneAuthProvider.credential(
        verificationId: verificationId,
        smsCode: otp,
      );
      await _firebaseAuth.signInWithCredential(credential);
    } catch (e) {
      debugPrint('Error verifying OTP: $e');
      throw Exception('Invalid OTP');
    }
  }

  
  Future<void> resendOTP(String phoneNumber) async {
    if (_resendToken == null) {
      throw Exception('No resend token available');
    }
    
    try {
      await _firebaseAuth.verifyPhoneNumber(
        phoneNumber: phoneNumber,
        forceResendingToken: _resendToken,
        verificationCompleted: (PhoneAuthCredential credential) async {
          await _firebaseAuth.signInWithCredential(credential);
        },
        verificationFailed: (FirebaseAuthException e) {
          debugPrint('Resend verification failed: ${e.message}');
          throw Exception('Failed to resend OTP: ${e.message}');
        },
        codeSent: (String verificationId, int? resendToken) {
          _verificationId = verificationId;
          _resendToken = resendToken;
        },
        codeAutoRetrievalTimeout: (String verificationId) {
          _verificationId = verificationId;
        },
        timeout: const Duration(seconds: 60),
      );
    } catch (e) {
      debugPrint('Error resending OTP: $e');
      throw Exception('Failed to resend OTP');
    }
  }
}

Testing Considerations

1. Test Phone Numbers

Firebase provides test phone numbers that don’t require actual SMS verification:

  • For Android: Use test phone numbers with the format +1 650-555-XXXX
  • For iOS: Use test phone numbers with the format +1 650-555-XXXX

Example test numbers:

+1 650-555-1234
+1 650-555-5678

2. Emulator Testing

When testing on emulators:

  1. Android Emulator:

    • Use the emulator’s built-in SMS receiver
    • OTPs will be automatically detected and filled
    • No need for actual SMS verification
  2. iOS Simulator:

    • Use test phone numbers
    • OTPs will be logged in the console
    • No need for actual SMS verification

3. Production Testing

Before going to production:

  1. Enable phone authentication in Firebase Console
  2. Add your app’s SHA-1 and SHA-256 fingerprints
  3. Test with real phone numbers in a staging environment
  4. Monitor Firebase Console for any verification issues

Common Issues and Solutions

  1. Verification Failed

    • Check if phone authentication is enabled in Firebase Console
    • Verify SHA-1/SHA-256 fingerprints are correctly added
    • Ensure proper Firebase configuration files are included
  2. OTP Not Received

    • Check if the phone number format is correct (include country code)
    • Verify the phone number is not blocked by Firebase
    • Check Firebase Console for any rate limiting issues
  3. Auto-retrieval Not Working

    • Ensure proper permissions in Android/iOS configuration
    • Check if the app has SMS reading permissions
    • Verify the phone number format matches the device’s region

Best Practices

  1. Error Handling

    • Implement proper error handling for all authentication flows
    • Show user-friendly error messages
    • Log errors for debugging purposes
  2. User Experience

    • Show loading states during verification
    • Implement proper timeout handling
    • Provide clear instructions for OTP entry
  3. Security

    • Implement rate limiting on your backend
    • Use reCAPTCHA for suspicious activities
    • Monitor for abuse patterns

Example Usage

First, add the flutter_otp_text_field package to your pubspec.yaml:

dependencies:
  flutter_otp_text_field: ^1.5.1+1  # Check for latest version

📦 Package Reference: flutter_otp_text_field on pub.dev

Then run:

flutter pub get

Here’s how to implement the OTP input field using flutter_otp_text_field:

class PhoneAuthScreen extends StatefulWidget {
  
  _PhoneAuthScreenState createState() => _PhoneAuthScreenState();
}

class _PhoneAuthScreenState extends State<PhoneAuthScreen> {
  final _phoneController = TextEditingController();
  final _otpController = TextEditingController();
  bool _isVerifying = false;
  String? _verificationId;

  Future<void> _verifyPhoneNumber() async {
    setState(() => _isVerifying = true);
    try {
      await context.read<IAuthService>().verifyPhoneNumber(_phoneController.text);
      // Show OTP input UI
    } catch (e) {
      // Show error message
    } finally {
      setState(() => _isVerifying = false);
    }
  }

  Future<void> _verifyOTP(String otp) async {
    setState(() => _isVerifying = true);
    try {
      await context.read<IAuthService>().verifyOTP(
        _verificationId!,
        otp,
      );
      // Navigate to home screen
    } catch (e) {
      // Show error message
    } finally {
      setState(() => _isVerifying = false);
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: _isVerifying
            ? CircularProgressIndicator()
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  TextField(
                    controller: _phoneController,
                    decoration: InputDecoration(
                      hintText: 'Enter phone number',
                    ),
                  ),
                  ElevatedButton(
                    onPressed: _verifyPhoneNumber,
                    child: Text('Send OTP'),
                  ),
                  if (_verificationId != null) ...[
                    SizedBox(height: 20),
                    OTPTextField(
                      length: 6,
                      width: MediaQuery.of(context).size.width,
                      fieldWidth: 40,
                      style: TextStyle(fontSize: 17),
                      textFieldAlignment: MainAxisAlignment.spaceAround,
                      fieldStyle: FieldStyle.box,
                      onCompleted: (pin) {
                        _verifyOTP(pin);
                      },
                    ),
                  ],
                ],
              ),
      ),
    );
  }
}

OTPTextField Properties

The OTPTextField widget provides several customization options:

  1. Basic Properties:

    • length: Number of OTP digits (default: 6)
    • width: Total width of the OTP field
    • fieldWidth: Width of each digit field
    • style: Text style for the digits
    • fieldStyle: Style of the input fields (box, underline, or none)
  2. Appearance:

    • outlineBorderRadius: Border radius for box style
    • fieldStyle: Choose between box, underline, or none
    • inputDecoration: Custom decoration for each field
    • keyboardType: Type of keyboard to show
  3. Functionality:

    • onCompleted: Callback when all digits are entered
    • onChanged: Callback when any digit changes
    • autoFocus: Whether to focus the first field automatically
  4. Validation:

    • validator: Custom validation function
    • errorText: Error message to display

Example with custom styling:

OTPTextField(
  length: 6,
  width: MediaQuery.of(context).size.width,
  fieldWidth: 40,
  style: TextStyle(fontSize: 17),
  textFieldAlignment: MainAxisAlignment.spaceAround,
  fieldStyle: FieldStyle.box,
  outlineBorderRadius: 10,
  inputDecoration: InputDecoration(
    filled: true,
    fillColor: Colors.grey[200],
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: BorderSide.none,
    ),
  ),
  onCompleted: (pin) {
    _verifyOTP(pin);
  },
)

Additional Resources