Building a Dating App in Flutter with Firebase

0

Flutter Dating App

Building a dating app in Flutter with Firebase is not complex. It started with a simple idea—what does it really take to build a modern dating app from scratch?

Every day, millions of people swipe, match, and chat without thinking about the technology behind it. But as a developer, I was curious. How do these apps handle real-time conversations? How does the swipe system feel so smooth? And how can all of this be built into one seamless experience?

So I decided to build one myself.

In this blog, I’ll walk you through my journey of creating a fully functional dating app using Flutter and Firebase. From setting up authentication to implementing swipe features, real-time chat, notifications, and location-based discovery, this project was all about turning a complex idea into a clean and user-friendly app.

Why Flutter for a Dating App?

I’ve worked with React Native before, and while it’s capable, on the other hand, Flutter kept pulling me back in. The single codebase that compiles to native Android and iOS is one thing, but what really sold me was performance. Flutter renders its own UI — it doesn’t rely on platform-native components bridged through JavaScript. That matters enormously in a swipe-based interface where animations need to feel buttery and instant.

However, Dart also has a learning curve, but once you’re comfortable, you move fast. The widget tree model forces you to think about UI composition in a way that actually makes large apps easier to maintain than they might sound.

Here’s the stack I chose and why it crushed it:

  • Frontend: Flutter – Handles everything from silky swipes to video calls.

  • Backend: Firebase – Authentication, Firestore for user data/matches, Cloud Functions for match logic, and FCM for notifications. Zero servers to manage.

  • State Management: Riverpod – Lightweight, testable, and beats Provider for scoped providers. Kept my swipe states and chat streams reactive without boilerplate hell.

  • Maps: Google Maps Flutter plugin – For nearby users on a map view.

  • Extras: Zegocloud SDK for audio/video calls, image_picker for profiles, geolocator for distance filters, and some other packages.

Key Features of the App

1. Multiple Authentication Options(with source code)

The app begins with a flexible and secure authentication system, which is a core part of any modern application. Since authentication plays such an important role, I decided to also share the source code for this module. The idea is to make it reusable, so you can easily integrate it into your own projects with only a few modifications.

This screen contains all the login methods login_option_screen.dart

import 'package:dating_app/common/route.dart';
import 'package:dating_app/common/widget/background.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/feature/auth/screen/google_login_screen.dart';
import 'package:dating_app/feature/auth/screen/login_screen.dart';
import 'package:dating_app/feature/auth/screen/phone/verify_number_screen.dart';
import 'package:flutter/material.dart';

class LoginOptionScreen extends StatelessWidget {
  const LoginOptionScreen({super.key});

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return Scaffold(
      backgroundColor: secondaryColor,
      body: Stack(
        children: [
          SoftGradientBackground(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: SafeArea(
              child: Column(
                children: [
                  SizedBox(
                    height: size.height * 0.28,
                    width: double.maxFinite,
                    child: Image.asset('assets/logo.png', fit: BoxFit.cover),
                  ),
                  Text(
                    "Signup to Continue",
                    style: TextStyle(fontSize: 30, fontWeight: FontWeight.w900),
                  ),
                  SizedBox(height: 10),
                  Text(
                    "Please login to continue",
                    style: TextStyle(color: secondaryTextColor),
                  ),
                  SizedBox(height: size.height * 0.05),
                  GoogleLoginScreen(),
                  SizedBox(height: 20),
                  SizedBox(
                    width: double.maxFinite,
                    child: OutlinedButton(
                      style: OutlinedButton.styleFrom(
                        backgroundColor: Colors.white,

                        side: BorderSide(color: primaryColor, width: 1),

                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(10),
                        ),
                      ),
                      onPressed: () {
                        NavigationHelper.push(context, LoginScreen());
                      },
                      child: Padding(
                        padding: const EdgeInsets.all(13.0),
                        child: Text(
                          "Continue with Email",
                          style: TextStyle(color: primaryColor, fontSize: 19),
                        ),
                      ),
                    ),
                  ),

                  SizedBox(height: size.height * 0.045),
                  Row(
                    children: [
                      Expanded(
                        child: Divider(color: secondaryTextColor.withAlpha(40)),
                      ),
                      Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 15),
                        child: Text(
                          "Or Signup with",
                          style: TextStyle(
                            fontSize: 16,
                            color: secondaryTextColor,
                          ),
                        ),
                      ),
                      Expanded(
                        child: Divider(color: secondaryTextColor.withAlpha(40)),
                      ),
                    ],
                  ),
                  SizedBox(height: size.height * 0.045),
                  Row(
                    children: [
                      Expanded(
                        child: Container(
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(10),
                            border: Border.all(
                              color: secondaryTextColor.withAlpha(40),
                              width: 1,
                            ),
                          ),
                          child: Padding(
                            padding: const EdgeInsets.all(10.0),
                            child: GestureDetector(
                              onTap: () {
                                NavigationHelper.push(
                                  context,
                                  VerifyNumberScreen(),
                                );
                              },
                              child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Icon(
                                    Icons.phone,
                                    color: Colors.green,
                                    size: 35,
                                  ),
                                  SizedBox(width: 10),
                                  Text("Phone", style: TextStyle(fontSize: 18)),
                                ],
                              ),
                            ),
                          ),
                        ),
                      ),
                      SizedBox(width: 15),
                      Expanded(
                        child: Container(
                          decoration: BoxDecoration(
                            borderRadius: BorderRadius.circular(10),
                            border: Border.all(
                              color: secondaryTextColor.withAlpha(40),
                              width: 1,
                            ),
                          ),
                          child: Padding(
                            padding: const EdgeInsets.all(10.0),
                            child: Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: [
                                Icon(
                                  Icons.facebook,
                                  color: Colors.blueAccent,
                                  size: 35,
                                ),
                                SizedBox(width: 10),
                                Text(
                                  "Facebook",
                                  style: TextStyle(fontSize: 18),
                                ),
                              ],
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                  Spacer(),
                  Text.rich(
                    textAlign: TextAlign.center,
                    TextSpan(
                      text: 'I accept all the\n',

                      style: TextStyle(color: primaryTextColor, fontSize: 15),
                      children: [
                        TextSpan(
                          text: 'Terms & condition',
                          style: TextStyle(color: primaryColor),
                        ),
                        TextSpan(text: " & "),
                        TextSpan(
                          text: 'Privacy policy',
                          style: TextStyle(color: primaryColor),
                        ),
                      ],
                    ),
                  ),
                  SizedBox(height: 10),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}


Users can sign up or log in using:

1.Phone number
I will start with phone_auth_model.dart

/// Authentication state model
class AuthState {
  final bool isLoading;
  final String? error;
  final String? phoneNumber;
  final String? verificationId;
  final int? forceResendingToken;
  final bool isVerified;
  final bool codeSent;

  const AuthState({
    this.isLoading = false,
    this.error,
    this.phoneNumber,
    this.verificationId,
    this.forceResendingToken,
    this.isVerified = false,
    this.codeSent = false,
  });

  factory AuthState.initial() => const AuthState();

  AuthState copyWith({
    bool? isLoading,
    String? error,
    String? phoneNumber,
    String? verificationId,
    int? forceResendingToken,
    bool? isVerified,
    bool? codeSent,
  }) {
    return AuthState(
      isLoading: isLoading ?? this.isLoading,
      error: error, // Always set error (null clears it)
      phoneNumber: phoneNumber ?? this.phoneNumber,
      verificationId: verificationId ?? this.verificationId,
      forceResendingToken: forceResendingToken ?? this.forceResendingToken,
      isVerified: isVerified ?? this.isVerified,
      codeSent: codeSent ?? this.codeSent,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;

    return other is AuthState &&
        other.isLoading == isLoading &&
        other.error == error &&
        other.phoneNumber == phoneNumber &&
        other.verificationId == verificationId &&
        other.forceResendingToken == forceResendingToken &&
        other.isVerified == isVerified &&
        other.codeSent == codeSent;
  }

  @override
  int get hashCode {
    return Object.hash(
      isLoading,
      error,
      phoneNumber,
      verificationId,
      forceResendingToken,
      isVerified,
      codeSent,
    );
  }

  @override
  String toString() {
    return 'AuthState(isLoading: $isLoading, error: $error, phoneNumber: $phoneNumber, '
        'verificationId: $verificationId, isVerified: $isVerified, codeSent: $codeSent)';
  }
}

Next, we need a service file where we have achieved a phone authentication service phone_auth_service.dart

import 'package:dating_app/feature/auth/model/phone_auth_model.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/legacy.dart';

class AuthNotifier extends StateNotifier<AuthState> {
  AuthNotifier() : super(AuthState.initial());

  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final FirebaseAuth _auth = FirebaseAuth.instance;

  /// Save or update user data in Firestore
  Future<void> _saveUserToFirestore(User user) async {
    try {
      final userDoc = _firestore.collection('users').doc(user.uid);
      final docSnapshot = await userDoc.get();
      final userData = {
        'uid': user.uid,
        'name': user.displayName ?? '',
        'email': user.email ?? '',
        'phoneNumber': user.phoneNumber ?? '',
        'photoURL': user.photoURL ?? '',
        'provider': 'phone',
        'lastSeen': FieldValue.serverTimestamp(),
        'isOnline': false,
        'blockedUsers': [],
      };

      if (!docSnapshot.exists) {
        await userDoc.set({
          ...userData,
          'createdAt': FieldValue.serverTimestamp(),
        });
        debugPrint('New user created in Firestore: ${user.uid}');
      } else {
        await userDoc.update({
          ...userData,
          // Preserve tru creation date
          'isOnline': true,
          'createdAt': docSnapshot.data()?['createdAt'],
        });
        debugPrint('User updated in Firestore: ${user.uid}');
      }
    } catch (e, stackTrace) {
      debugPrint('Error saving user to Firestore: $e');
      debugPrint('Stack trace: $stackTrace');
      // Don't throw - we don't want to block auth for Firestore errors
      // The user is still authenticated even if Firestore save fails
    }
  }

  /// Validate phone number format
  bool _isValidPhoneNumber(String phone) {
    // Must start with + followed by country code and number
    // E.164 format: +[country code][number] (8-15 digits total after +)
    final phoneRegex = RegExp(r'^\+[1-9]\d{7,14}$');
    return phoneRegex.hasMatch(phone);
  }

  /// Send verification code to phone number
  Future<void> sendVerificationCode({required String phoneNumber}) async {
    final cleanedPhone = phoneNumber.trim();

    // Validate phone number
    if (cleanedPhone.isEmpty || !_isValidPhoneNumber(cleanedPhone)) {
      state = state.copyWith(
        error:
            "Please enter a valid phone number with country code (e.g., +1234567890)",
        isLoading: false,
      );
      return;
    }

    state = state.copyWith(
      isLoading: true,
      error: null,
      phoneNumber: cleanedPhone,
    );

    try {
      await _auth.verifyPhoneNumber(
        phoneNumber: cleanedPhone,
        timeout: const Duration(seconds: 60),
        forceResendingToken: state.forceResendingToken,

        // Auto-verification (Android instant verification)
        verificationCompleted: (PhoneAuthCredential credential) async {
          debugPrint('Auto-verification started');
          await _handleAutoVerification(credential);
        },

        // Verification failed
        verificationFailed: (FirebaseAuthException error) {
          debugPrint('Verification failed: ${error.code} - ${error.message}');
          _handleVerificationFailed(error);
        },

        // Code sent successfully
        codeSent: (String verificationId, int? forceResendingToken) {
          debugPrint(
            'Code sent successfully. Verification ID: $verificationId',
          );
          state = state.copyWith(
            isLoading: false,
            verificationId: verificationId,
            forceResendingToken: forceResendingToken,
            error: null,
            codeSent: true,
          );
        },

        // Auto-retrieval timeout
        codeAutoRetrievalTimeout: (String verificationId) {
          debugPrint(
            'Auto-retrieval timeout. Verification ID: $verificationId',
          );
          state = state.copyWith(
            verificationId: verificationId,
            isLoading: false,
          );
        },
      );
    } catch (e, stackTrace) {
      debugPrint('Error in sendVerificationCode: $e');
      debugPrint('Stack trace: $stackTrace');

      state = state.copyWith(
        isLoading: false,
        error: 'Failed to send verification code. Please try again.',
      );
    }
  }

  /// Handle auto-verification (instant verification on Android)
  Future<void> _handleAutoVerification(PhoneAuthCredential credential) async {
    try {
      state = state.copyWith(isLoading: true, error: null);

      final userCredential = await _auth.signInWithCredential(credential);

      if (userCredential.user != null) {
        await _saveUserToFirestore(userCredential.user!);

        state = state.copyWith(isLoading: false, isVerified: true, error: null);

        debugPrint('Auto-verification successful');
      }
    } catch (e, stackTrace) {
      debugPrint('Error in auto-verification: $e');
      debugPrint('Stack trace: $stackTrace');

      state = state.copyWith(
        isLoading: false,
        error: 'Verification failed. Please try again.',
      );
    }
  }

  /// Handle verification failure
  void _handleVerificationFailed(FirebaseAuthException error) {
    String errorMessage;

    switch (error.code) {
      case 'invalid-phone-number':
        errorMessage = 'The phone number format is invalid.';
        break;
      case 'too-many-requests':
        errorMessage = 'Too many attempts. Please try again later.';
        break;
      case 'quota-exceeded':
        errorMessage = 'SMS quota exceeded. Please try again later.';
        break;
      case 'operation-not-allowed':
        errorMessage = 'Phone authentication is not enabled.';
        break;
      case 'network-request-failed':
        errorMessage = 'Network error. Please check your connection.';
        break;
      default:
        errorMessage = error.message ?? 'Failed to send verification code.';
    }

    state = state.copyWith(isLoading: false, error: errorMessage);
  }

  /// Verify OTP code
  Future<bool> verifyOTP({required String otp}) async {
    final cleanedOtp = otp.trim();

    // Validate OTP
    if (cleanedOtp.isEmpty || cleanedOtp.length != 6) {
      state = state.copyWith(error: "Please enter a valid 6-digit OTP");
      return false;
    }

    // Check if we have a verification ID
    if (state.verificationId == null || state.verificationId!.isEmpty) {
      state = state.copyWith(
        error: "Session expired. Please request a new OTP.",
      );
      return false;
    }

    state = state.copyWith(isLoading: true, error: null);

    try {
      // Create credential
      final credential = PhoneAuthProvider.credential(
        verificationId: state.verificationId!,
        smsCode: cleanedOtp,
      );

      debugPrint('Attempting to sign in with OTP...');

      // Sign in with credential
      final userCredential = await _auth.signInWithCredential(credential);

      if (userCredential.user == null) {
        throw Exception('Authentication failed: No user returned');
      }

      debugPrint('Sign in successful. User ID: ${userCredential.user!.uid}');

      // Save to Firestore
      await _saveUserToFirestore(userCredential.user!);

      // Update state to verified
      state = state.copyWith(isLoading: false, isVerified: true, error: null);

      return true;
    } on FirebaseAuthException catch (e) {
      debugPrint('Firebase Auth error: ${e.code} - ${e.message}');

      final errorMessage = _getOTPErrorMessage(e.code);

      state = state.copyWith(isLoading: false, error: errorMessage);

      return false;
    } catch (e, stackTrace) {
      debugPrint('Unexpected error in verifyOTP: $e');
      debugPrint('Stack trace: $stackTrace');

      state = state.copyWith(
        isLoading: false,
        error: "Verification failed. Please try again.",
      );

      return false;
    }
  }

  /// Get user-friendly error message for OTP errors
  String _getOTPErrorMessage(String errorCode) {
    switch (errorCode) {
      case 'invalid-verification-code':
        return "Invalid OTP. Please check and try again.";
      case 'session-expired':
        return "Session expired. Please request a new OTP.";
      case 'invalid-verification-id':
        return "Invalid session. Please restart verification.";
      case 'code-expired':
        return "OTP has expired. Please request a new code.";
      case 'network-request-failed':
        return "Network error. Please check your connection.";
      default:
        return "Verification failed. Please try again.";
    }
  }

  /// Clear error state
  void clearError() {
    if (state.error != null) {
      state = state.copyWith(error: null);
    }
  }

  /// Reset to initial state
  void resetState() {
    state = AuthState.initial();
  }

  /// Clear verified flag after navigation
  void clearVerified() {
    state = state.copyWith(isVerified: false);
  }

  /// Clear code sent flag
  void clearCodeSent() {
    state = state.copyWith(codeSent: false);
  }
}

final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  return AuthNotifier();
});

After the mode and service file. Next, we need a screen(UI components) otp_screen.dart

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dating_app/common/route.dart';
import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/core/utils/utils.dart';
import 'package:dating_app/feature/account%20setup/account_setup_screen.dart';
import 'package:dating_app/feature/auth/model/phone_auth_model.dart';
import 'package:dating_app/feature/auth/service/phone_auth_service.dart';
import 'package:dating_app/feature/home/home_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class OtpScreen extends ConsumerStatefulWidget {
  final String? phoneNumber;

  const OtpScreen({super.key, this.phoneNumber});

  @override
  ConsumerState<OtpScreen> createState() => _OtpScreenState();
}

class _OtpScreenState extends ConsumerState<OtpScreen> {
  late List<TextEditingController> otpControllers;
  late List<FocusNode> focusNodes;
  Timer? _timer;
  int _remainingSeconds = 60;
  bool _canResend = false;
  bool _isVerifying = false;

  String get otpCode => otpControllers.map((c) => c.text).join();

  @override
  void initState() {
    super.initState();
    _initializeOtpFields();
    _startTimer();
  }

  void _initializeOtpFields() {
    otpControllers = List.generate(6, (_) => TextEditingController());
    focusNodes = List.generate(6, (_) => FocusNode());

    // Auto-focus first field after build
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        focusNodes[0].requestFocus();
      }
    });
  }

  @override
  void dispose() {
    _timer?.cancel();
    for (var controller in otpControllers) {
      controller.dispose();
    }
    for (var node in focusNodes) {
      node.dispose();
    }
    super.dispose();
  }

  void _startTimer() {
    _timer?.cancel(); // Cancel any existing timer
    _canResend = false;
    _remainingSeconds = 60;

    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (!mounted) {
        timer.cancel();
        return;
      }

      setState(() {
        if (_remainingSeconds > 0) {
          _remainingSeconds--;
        } else {
          _canResend = true;
          timer.cancel();
        }
      });
    });
  }

  void _onOtpChanged(int index, String value) {
    if (value.isNotEmpty) {
      // Move to next field
      if (index < 5) {
        FocusScope.of(context).requestFocus(focusNodes[index + 1]);
      } else {
        // Last field - unfocus and auto-verify
        focusNodes[index].unfocus();

        if (otpCode.length == 6 && !_isVerifying) {
          // Small delay for better UX
          Future.delayed(const Duration(milliseconds: 300), () {
            if (mounted && !_isVerifying) {
              _verifyOtp();
            }
          });
        }
      }
    } else if (index > 0) {
      // Backspace - move to previous field
      FocusScope.of(context).requestFocus(focusNodes[index - 1]);
    }
  }

  Future<void> _verifyOtp() async {
    // Prevent multiple simultaneous calls
    if (_isVerifying) return;

    if (otpCode.length != 6) {
      // In _verifyOtp()
      _showError("Please enter complete 6-digit OTP");
      return;
    }

    setState(() => _isVerifying = true);

    try {
      final success = await ref
          .read(authProvider.notifier)
          .verifyOTP(otp: otpCode);

      if (!mounted) return;

      if (success) {
        // Show success message
        showAppSnackbar(
          context: context,
          type: SnackbarType.success,
          description: "Phone number verified successfully",
        );

        // Small delay before navigation
        await Future.delayed(const Duration(milliseconds: 500));

        if (!mounted) return;

        // Clear verified flag
        ref.read(authProvider.notifier).clearVerified();
        // Invalidate and reload providers
        // ref.invalidate(favouriteProvider);
        // NavigationHelper.pushAndRemoveUntil(context, AccountSetupScreen());
        final user = FirebaseAuth.instance.currentUser;
        if (user != null) {
          // Check Firestore for profileCompleted flag
          final doc = await FirebaseFirestore.instance
              .collection('users')
              .doc(user.uid)
              .get();

          final profileCompleted =
              doc.exists && (doc.data()?['profileCompleted'] == true);
          if (mounted) {
            if (profileCompleted) {
              NavigationHelper.pushReplacement(context, HomeScreen());
            } else {
              NavigationHelper.pushReplacement(context, AccountSetupScreen());
            }
          }
        }
      } else {
        // Clear OTP on failure
        _clearOtp();
      }
    } finally {
      if (mounted) {
        setState(() => _isVerifying = false);
      }
    }
  }

  void _clearOtp() {
    for (var controller in otpControllers) {
      controller.clear();
    }
    if (mounted) {
      focusNodes[0].requestFocus();
    }
  }

  Future<void> _resendOtp() async {
    if (!_canResend || !mounted) return;

    _clearOtp();
    _startTimer();

    final phoneNumber =
        widget.phoneNumber ?? ref.read(authProvider).phoneNumber ?? '';

    if (phoneNumber.isEmpty) {
      _showError("Phone number not found. Please go back and try again.");
      return;
    }

    await ref
        .read(authProvider.notifier)
        .sendVerificationCode(phoneNumber: phoneNumber);
  }

  void _showError(String message) {
    if (!mounted) return;

    showAppSnackbar(
      context: context,
      type: SnackbarType.error,
      description: message,
    );
  }

  void _showSuccess(String message) {
    if (!mounted) return;

    showAppSnackbar(
      context: context,
      type: SnackbarType.success,
      description: message,
    );
  }

  String _formatTime(int seconds) {
    final minutes = seconds ~/ 60;
    final secs = seconds % 60;
    return '$minutes:${secs.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    final authState = ref.watch(authProvider);
    // Listen for state changes - THIS IS THE CORRECT WAY
    ref.listen<AuthState>(authProvider, (previous, next) {
      // Show errors
      if (next.error != null && next.error != previous?.error) {
        _showError(next.error!);
      }

      // Show success when code is sent (for resend)
      if (next.codeSent && !previous!.codeSent) {
        _showSuccess("OTP sent to your phone number");
        // Clear the flag
        ref.read(authProvider.notifier).clearCodeSent();
      }
    });

    return Scaffold(
      backgroundColor: backgroundColor,
      appBar: AppBar(
        title: Text(
          "Verify Number",
          style: TextStyle(fontSize: 18, color: primaryTextColor),
        ),
        backgroundColor: secondaryColor,
        elevation: 0,
        iconTheme: const IconThemeData(color: primaryTextColor),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            const SizedBox(height: 40),

            // Title
            Text(
              "Verify your number",
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 24,
                color: primaryTextColor,
              ),
            ),

            // Subtitle
            Text(
              "Enter your OTP code below",
              textAlign: TextAlign.center,
              style: TextStyle(color: secondaryTextColor, height: 3),
            ),

            const SizedBox(height: 40),

            // OTP Input Fields
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: List.generate(6, (index) {
                return _buildOtpField(index);
              }),
            ),

            const SizedBox(height: 40),

            // Verify Button
            _buildVerifyButton(authState),

            const SizedBox(height: 20),

            // Resend Section
            _buildResendSection(authState),

            const SizedBox(height: 20),
          ],
        ),
      ),
    );
  }

  Widget _buildOtpField(int index) {
    return Container(
      width: 50,
      height: 50,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(
          color: otpControllers[index].text.isNotEmpty
              ? Colors.green
              : Colors.grey.shade300,
          width: 2,
        ),
      ),
      child: TextField(
        controller: otpControllers[index],
        focusNode: focusNodes[index],
        textAlign: TextAlign.center,
        style: const TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
          color: primaryTextColor,
        ),
        keyboardType: TextInputType.number,
        inputFormatters: [
          FilteringTextInputFormatter.digitsOnly,
          LengthLimitingTextInputFormatter(1),
        ],
        decoration: const InputDecoration(
          border: InputBorder.none,
          counterText: '',
        ),
        onChanged: (value) => _onOtpChanged(index, value),
        onTap: () {
          // Place cursor at end
          otpControllers[index].selection = TextSelection.fromPosition(
            TextPosition(offset: otpControllers[index].text.length),
          );
        },
      ),
    );
  }

  Widget _buildVerifyButton(AuthState authState) {
    final isLoading = authState.isLoading || _isVerifying;

    if (isLoading) {
      return const Center(
        child: CircularProgressIndicator(
          valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
        ),
      );
    }

    return MyButton(onTap: _verifyOtp, buttonText: "Verify");
  }

  Widget _buildResendSection(AuthState authState) {
    return Column(
      children: [
        Text(
          "Didn't receive the code?",
          style: TextStyle(color: secondaryTextColor, fontSize: 14),
        ),
        const SizedBox(height: 8),

        if (!_canResend) ...[
          // Show countdown timer
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            decoration: BoxDecoration(
              color: Colors.grey.shade100,
              borderRadius: BorderRadius.circular(20),
            ),
            child: Text(
              "${'Resend code in'} ${_formatTime(_remainingSeconds)}",
              // "Resend code in ${_formatTime(_remainingSeconds)}",
              style: TextStyle(
                color: Colors.grey.shade600,
                fontSize: 14,
                fontWeight: FontWeight.w500,
              ),
            ),
          ),
        ] else ...[
          // Show resend button
          GestureDetector(
            onTap: authState.isLoading ? null : _resendOtp,
            child: Container(
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
              decoration: BoxDecoration(
                color: authState.isLoading
                    ? Colors.grey.shade300
                    : Colors.green,
                borderRadius: BorderRadius.circular(25),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (authState.isLoading) ...[
                    const SizedBox(
                      width: 16,
                      height: 16,
                      child: CircularProgressIndicator(
                        strokeWidth: 2,
                        valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                      ),
                    ),
                    const SizedBox(width: 8),
                  ],
                  const Icon(Icons.refresh, color: Colors.white, size: 18),
                  const SizedBox(width: 6),
                  Text(
                    authState.isLoading ? ("Sending...") : ("Resend Code"),
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 14,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ],
    );
  }
}

and verify_number_screen.dart

import 'package:dating_app/common/route.dart';
import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/core/utils/utils.dart';
import 'package:dating_app/feature/account%20setup/account_setup_screen.dart';
import 'package:dating_app/feature/auth/model/phone_auth_model.dart';
import 'package:dating_app/feature/auth/screen/phone/otp_screen.dart';
import 'package:dating_app/feature/auth/service/phone_auth_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl_phone_field/intl_phone_field.dart';

class VerifyNumberScreen extends ConsumerStatefulWidget {
  const VerifyNumberScreen({super.key});

  @override
  ConsumerState<VerifyNumberScreen> createState() => _VerifyNumberScreenState();
}

class _VerifyNumberScreenState extends ConsumerState<VerifyNumberScreen> {
  late TextEditingController phoneController;
  String countryCode = "+977";
  String fullPhoneNumber = "";
  bool _hasNavigated = false; // Prevent multiple navigations

  @override
  void initState() {
    super.initState();
    phoneController = TextEditingController();
  }

  @override
  void dispose() {
    phoneController.dispose();
    super.dispose();
  }

  Future<void> _sendVerificationCode() async {
    // Validate input
    if (phoneController.text.trim().isEmpty) {
      _showError("Please enter your phone number");
      return;
    }

    // Create full phone number
    fullPhoneNumber = countryCode + phoneController.text.trim();

    // Send verification code
    await ref
        .read(authProvider.notifier)
        .sendVerificationCode(phoneNumber: fullPhoneNumber);
  }

  void _showError(String message) {
    if (!mounted) return;

    showAppSnackbar(
      context: context,
      type: SnackbarType.error,
      description: message,
    );
  }

  void _showSuccess(String message) {
    if (!mounted) return;

    showAppSnackbar(
      context: context,
      type: SnackbarType.success,
      description: message,
    );
  }

  @override
  Widget build(BuildContext context) {
    final authState = ref.watch(authProvider);
    // Listen for state changes
    ref.listen<AuthState>(authProvider, (previous, next) {
      // Handle errors
      if (next.error != null && next.error != previous?.error) {
        _showError(next.error!);
      }

      // Handle successful code sent
      if (next.codeSent && !previous!.codeSent && !_hasNavigated) {
        _hasNavigated = true;

        _showSuccess("OTP sent to your phone number");

        // Clear the flag
        ref.read(authProvider.notifier).clearCodeSent();

        // Navigate to OTP screen
        Navigator.of(context)
            .push(
              MaterialPageRoute(
                builder: (_) => OtpScreen(phoneNumber: fullPhoneNumber),
              ),
            )
            .then((_) {
              // Reset navigation flag when returning
              _hasNavigated = false;
            });
      }

      // Handle auto-verification (rare but possible)
      if (next.isVerified && !previous!.isVerified) {
        _showSuccess("Phone number verified successfully");

        // Navigate to main screen
        // Navigator.of(context).pushAndRemoveUntil(
        //   MaterialPageRoute(builder: (_) => const HomeScreen()),
        //   (route) => false,
        // );
        NavigationHelper.pushAndRemoveUntil(context, AccountSetupScreen());
      }
    });

    return Scaffold(
      backgroundColor: backgroundColor,
      appBar: AppBar(
        backgroundColor: secondaryColor,
        title: Text("Verify Number"),
        centerTitle: true,
        elevation: 0,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 15),
        child: Column(
          children: [
            const SizedBox(height: 40),
            // Title
            Center(
              child: Text(
                "Verify your number",
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 22,
                  color: primaryTextColor,
                  height: 3,
                ),
              ),
            ),
            // Subtitle
            Text(
              "A code will be sent to your phone to complete your registration.",
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 30),
            // Phone input field
            IntlPhoneField(
              keyboardType: TextInputType.number,
              inputFormatters: [FilteringTextInputFormatter.digitsOnly],
              dropdownDecoration: const BoxDecoration(color: Colors.white),
              controller: phoneController,
              decoration: InputDecoration(
                filled: true,
                fillColor: Colors.white,
                hintText: "Phone Number",
                labelStyle: TextStyle(fontSize: 14),
                border: OutlineInputBorder(borderSide: BorderSide.none),
              ),
              initialCountryCode: "NP",
              onCountryChanged: (country) {
                countryCode = "+${country.dialCode}";
              },
              onChanged: (phone) {
                // Update full phone number as user types
                fullPhoneNumber = phone.completeNumber;
              },
            ),
            const SizedBox(height: 10),
            // Send code button
            authState.isLoading
                ? const Center(
                    child: CircularProgressIndicator(
                      valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
                    ),
                  )
                : MyButton(
                    onTap: _sendVerificationCode,
                    buttonText: "Send code",
                  ),
            const SizedBox(height: 10),
          ],
        ),
      ),
    );
  }
}

After that, phone authentication is completed.

2. Email and password
To achieve the mail/password authentication model, let’s start with this auth_model.dart

class AuthFormState {
  final String name;
  final String email;
  final String password;
  final String? nameError;
  final String? emailError;
  final String? passwordError;
  final bool isLoading;
  final bool isPasswordHidden;

  AuthFormState({
    this.name = '',
    this.email = '',
    this.password = '',
    this.nameError,
    this.emailError,
    this.passwordError,
    this.isLoading = false,
    this.isPasswordHidden = true,
  });

  bool get isFormValid =>
      emailError == null &&
      passwordError == null &&
      (name.isEmpty || nameError == null);

  AuthFormState copyWith({
    String? name,
    String? email,
    String? password,
    String? nameError,
    String? emailError,
    String? passwordError,
    bool? isLoading,
    bool? isPasswordHidden,
  }) {
    return AuthFormState(
      name: name ?? this.name,
      email: email ?? this.email,
      password: password ?? this.password,
      nameError: nameError,
      emailError: emailError,
      passwordError: passwordError,
      isLoading: isLoading ?? this.isLoading,
      isPasswordHidden: isPasswordHidden ?? this.isPasswordHidden,
    );
  }
}

Next, the auth provider that validates email, password, and name. Toggle password visibility and for the loading state auth_provider.dart

import 'package:dating_app/feature/auth/model/auth_model.dart';
import 'package:flutter_riverpod/legacy.dart';

class AuthFormNotifier extends StateNotifier<AuthFormState> {
  AuthFormNotifier() : super(AuthFormState());

  void togglePasswordVisibility() {
    state = state.copyWith(isPasswordHidden: !state.isPasswordHidden);
  }

  void updateName(String name) {
    String? nameError;
    if (name.isNotEmpty && name.length < 6) {
      nameError = "Provide your full name";
    }
    state = state.copyWith(name: name, nameError: nameError);
  }

  void updateEmail(String email) {
    String? emailError;
    if (email.isNotEmpty &&
        !RegExp(r'^[\w-\.]+@([\w-]+\.)+com$').hasMatch(email)) {
      emailError = 'Enter a valid email';
    }

    state = state.copyWith(email: email, emailError: emailError);
  }

  void updatePassword(String password) {
    String? passwordError;
    if (password.isNotEmpty && password.length < 6) {
      passwordError = 'Password must be at least 6 characters';
    }
    state = state.copyWith(password: password, passwordError: passwordError);
  }

  void setLoading(bool isLoading) {
    state = state.copyWith(isLoading: isLoading);
  }
}

final authFormProvider = StateNotifierProvider<AuthFormNotifier, AuthFormState>(
  (ref) {
    return AuthFormNotifier();
  },
);

To activate email/password authentication, first we need to sign up the user, then we can log in with this user account auth_service.dart

import 'dart:async';
import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Auth Result class to return both success status and error details
class AuthResult {
  final bool success;
  final String message;
  final dynamic error; // Store the actual exception for detailed error handling

  AuthResult({required this.success, required this.message, this.error});

  factory AuthResult.success([String message = 'success']) {
    return AuthResult(success: true, message: message);
  }

  factory AuthResult.failure(String message, {dynamic error}) {
    return AuthResult(success: false, message: message, error: error);
  }
}

class AuthMethod {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Check internet connectivity
  Future<bool> _hasInternetConnection() async {
    try {
      final result = await InternetAddress.lookup(
        'google.com',
      ).timeout(Duration(seconds: 5));
      return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
    } on SocketException catch (_) {
      return false;
    } on TimeoutException catch (_) {
      return false;
    } catch (_) {
      return false;
    }
  }

  // Check if email exists in Firestore users collection
  Future<bool> _isEmailRegisteredInFirestore(String email) async {
    try {
      final querySnapshot = await _firestore
          .collection('users')
          .where('email', isEqualTo: email)
          .limit(1)
          .get()
          .timeout(Duration(seconds: 10));

      return querySnapshot.docs.isNotEmpty;
    } catch (e) {
      // If we can't check, return false to show generic error
      return false;
    }
  }

  Future<AuthResult> signUpUser({
    required String email,
    required String password,
    required String name,
  }) async {
    try {
      if (email.isEmpty || password.isEmpty || name.isEmpty) {
        return AuthResult.failure("Please enter all fields");
      }

      // Check internet connection first
      if (!await _hasInternetConnection()) {
        return AuthResult.failure(
          "No internet connection",
          error: SocketException("No internet"),
        );
      }

      UserCredential cred = await _auth
          .createUserWithEmailAndPassword(email: email, password: password)
          .timeout(
            Duration(seconds: 15),
            onTimeout: () {
              throw TimeoutException('Request timeout');
            },
          );
      // To Get FCM token
      // final token = await NotificationService().getToken();
      await _firestore.collection("users").doc(cred.user!.uid).set({
        "uid": cred.user!.uid,
        "name": name,
        "email": email,
        "photoURL": null,
        "provider": "email",
        'lastSeen': FieldValue.serverTimestamp(),
        "createdAt": FieldValue.serverTimestamp(),
        'isOnline': false,
        'blockedUsers': [],
        // "token": token,
      });
      // await NotificationService().updateUserToken(cred.user!.uid);

      // If token is null, retry in background
      // if (token == null) {
      //   retryTokenUpdate(cred.user!.uid);
      // }
      return AuthResult.success();
    } on FirebaseAuthException catch (e) {
      return AuthResult.failure(e.message ?? "Sign up failed", error: e);
    } on TimeoutException catch (e) {
      return AuthResult.failure("Request timeout", error: e);
    } on SocketException catch (e) {
      return AuthResult.failure("No internet connection", error: e);
    } catch (e) {
      return AuthResult.failure("An unexpected error occurred", error: e);
    }
  }

  Future<AuthResult> loginUser({
    required String email,
    required String password,
  }) async {
    try {
      if (email.isEmpty || password.isEmpty) {
        return AuthResult.failure("Please enter all fields");
      }

      // Check internet connection first
      if (!await _hasInternetConnection()) {
        return AuthResult.failure(
          "No internet connection",
          error: SocketException("No internet"),
        );
      }

      // Attempt login with timeout
      await _auth
          .signInWithEmailAndPassword(email: email, password: password)
          .timeout(
            Duration(seconds: 15),
            onTimeout: () {
              throw TimeoutException('Request timeout');
            },
          );
      // Update online status after login
      // if (_auth.currentUser != null) {
      //   await NotificationService().updateUserToken(_auth.currentUser!.uid);
      // }

      return AuthResult.success();
    } on FirebaseAuthException catch (e) {
      // Handle specific Firebase Auth errors
      if (e.code == 'user-not-found') {
        return AuthResult.failure(
          "Email not registered",
          error: FirebaseAuthException(code: 'user-not-found'),
        );
      } else if (e.code == 'wrong-password') {
        return AuthResult.failure(
          "Wrong password",
          error: FirebaseAuthException(code: 'wrong-password'),
        );
      } else if (e.code == 'invalid-credential') {
        // For invalid-credential, check Firestore to determine if email exists
        final emailExists = await _isEmailRegisteredInFirestore(email);

        if (!emailExists) {
          // Email not found in our database
          return AuthResult.failure(
            "Email not registered",
            error: FirebaseAuthException(code: 'user-not-found'),
          );
        } else {
          // Email exists, so password must be wrong
          return AuthResult.failure(
            "Wrong password",
            error: FirebaseAuthException(code: 'wrong-password'),
          );
        }
      } else if (e.code == 'invalid-email') {
        return AuthResult.failure("Invalid email", error: e);
      } else if (e.code == 'user-disabled') {
        return AuthResult.failure("Account disabled", error: e);
      } else if (e.code == 'too-many-requests') {
        return AuthResult.failure("Too many requests", error: e);
      }

      return AuthResult.failure(e.message ?? "Login failed", error: e);
    } on TimeoutException catch (e) {
      return AuthResult.failure("Request timeout", error: e);
    } on SocketException catch (e) {
      return AuthResult.failure("No internet connection", error: e);
    } catch (e) {
      return AuthResult.failure("An unexpected error occurred", error: e);
    }
  }

  // Reset password
  Future<AuthResult> resetPassword({required String email}) async {
    try {
      if (email.isEmpty) {
        return AuthResult.failure("Please enter your email");
      }

      // Check internet connection first
      if (!await _hasInternetConnection()) {
        return AuthResult.failure(
          "No internet connection",
          error: SocketException("No internet"),
        );
      }

      await _auth
          .sendPasswordResetEmail(email: email)
          .timeout(
            Duration(seconds: 15),
            onTimeout: () {
              throw TimeoutException('Request timeout');
            },
          );

      return AuthResult.success("Password reset email sent");
    } on FirebaseAuthException catch (e) {
      // Check if email exists in Firestore for better error message
      if (e.code == 'user-not-found' || e.code == 'invalid-email') {
        final emailExists = await _isEmailRegisteredInFirestore(email);
        if (!emailExists) {
          return AuthResult.failure(
            "Email not registered",
            error: FirebaseAuthException(code: 'user-not-found'),
          );
        }
      }

      return AuthResult.failure(
        e.message ?? "Failed to send reset email",
        error: e,
      );
    } on TimeoutException catch (e) {
      return AuthResult.failure("Request timeout", error: e);
    } on SocketException catch (e) {
      return AuthResult.failure("No internet connection", error: e);
    } catch (e) {
      return AuthResult.failure("An unexpected error occurred", error: e);
    }
  }

  // Sign out
  // Future<void> signOut() async {
  //   await _auth.signOut();
  // }

  // Get current user
  User? getCurrentUser() {
    return _auth.currentUser;
  }

  // Check if user is logged in
  bool isUserLoggedIn() {
    return _auth.currentUser != null;
  }
}

final authMethodProvider = Provider<AuthMethod>((ref) {
  return AuthMethod();
});

Now, we need the login screen and the signup screen to implement this auth service signup_screen.dart

import 'package:dating_app/common/route.dart';
import 'package:dating_app/common/widget/background.dart';
import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/core/constants/error_handler.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/core/utils/utils.dart';
import 'package:dating_app/feature/auth/provider/auth_provider.dart';
import 'package:dating_app/feature/auth/screen/login_screen.dart';
import 'package:dating_app/feature/auth/service/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class SignupScreen extends ConsumerWidget {
  const SignupScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final formState = ref.watch(authFormProvider);
    final formNotifer = ref.read(authFormProvider.notifier);
    final authMethod = ref.read(authMethodProvider);

    void sigup() async {
      formNotifer.setLoading(true);
      final res = await authMethod.signUpUser(
        email: formState.email,
        password: formState.password,
        name: formState.name,
      );
      formNotifer.setLoading(false);
      if (res.success && context.mounted) {
        NavigationHelper.pushReplacement(context, LoginScreen());
        showAppSnackbar(
          context: context,
          type: SnackbarType.success,
          description: "Sinup Up Successful. Now turn to login",
        );
      } else {
        String errorMessage;

        if (context.mounted) {
          if (res.error != null) {
            // Get detailed error message from AuthErrorHandler
            errorMessage = AuthErrorHandler.getErrorMessage(res.error, context);
          } else {
            // Fallback to the result message
            errorMessage = res.message;
          }

          showAppSnackbar(
            context: context,
            type: SnackbarType.error,
            description: errorMessage,
          );
        }
      }
    }

    double height = MediaQuery.of(context).size.height;
    return Scaffold(
      // backgroundColor: secondaryColor,
      body: Stack(
        children: [
          SoftGradientBackground(),
          LayoutBuilder(
            builder: (context, constraints) {
              return SingleChildScrollView(
                child: ConstrainedBox(
                  constraints: BoxConstraints(minHeight: constraints.maxHeight),
                  child: IntrinsicHeight(
                    child: Column(
                      children: [
                        SizedBox(height: 50),
                        Stack(
                          children: [
                            Container(
                              height: height / 3,
                              width: double.maxFinite,
                              decoration: BoxDecoration(),
                              child: Image.asset(
                                'assets/09345987.png',
                                fit: BoxFit.cover,
                              ),
                            ),
                            Padding(
                              padding: EdgeInsets.symmetric(
                                horizontal: 10,
                                // vertical: 40,
                              ),
                              child: IconButton(
                                style: IconButton.styleFrom(),
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                                icon: Icon(
                                  Icons.arrow_back_sharp,
                                  color: Colors.black,
                                ),
                              ),
                            ),
                          ],
                        ),
                        SizedBox(height: 20),
                        Expanded(
                          child: SizedBox(
                            // color: backgroundColor,
                            width: double.maxFinite,
                            child: Padding(
                              padding: EdgeInsets.all(15),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    "Create account",
                                    style: TextStyle(
                                      fontSize: 24,
                                      color: primaryTextColor,
                                    ),
                                  ),
                                  Text(
                                    "Quickly create an account",
                                    style: TextStyle(color: secondaryTextColor),
                                  ),
                                  SizedBox(height: 20),
                                  TextField(
                                    // enabled: false,
                                    autocorrect: false,
                                    onChanged: (value) =>
                                        formNotifer.updateName(value),

                                    decoration: InputDecoration(
                                      prefixIcon: Icon(
                                        Icons.person_2_outlined,
                                        color: Colors.black38,
                                      ),
                                      border: OutlineInputBorder(
                                        borderSide: BorderSide.none,
                                      ),
                                      fillColor: Colors.white,
                                      filled: true,
                                      labelText: "Your Name",
                                      labelStyle: TextStyle(
                                        color: secondaryTextColor,
                                      ),
                                      contentPadding: EdgeInsets.all(15),
                                      errorText: formState.nameError,
                                    ),
                                  ),
                                  SizedBox(height: 10),
                                  TextField(
                                    autocorrect: false,
                                    onChanged: (value) =>
                                        formNotifer.updateEmail(value),
                                    keyboardType: TextInputType.emailAddress,
                                    decoration: InputDecoration(
                                      prefixIcon: Icon(
                                        Icons.email_outlined,
                                        color: Colors.black38,
                                      ),
                                      labelText: "Enter your email",
                                      border: OutlineInputBorder(
                                        borderSide: BorderSide.none,
                                      ),
                                      fillColor: Colors.white,
                                      filled: true,
                                      contentPadding: EdgeInsets.all(15),
                                      errorText: formState.emailError,
                                    ),
                                  ),
                                  SizedBox(height: 10),
                                  TextField(
                                    autocorrect: false,
                                    onChanged: (value) =>
                                        formNotifer.updatePassword(value),
                                    keyboardType: TextInputType.visiblePassword,
                                    obscureText: formState.isPasswordHidden,
                                    decoration: InputDecoration(
                                      prefixIcon: Icon(
                                        Icons.lock_outline,
                                        color: Colors.black38,
                                      ),
                                      labelText: "Enter your password",
                                      border: OutlineInputBorder(
                                        borderSide: BorderSide.none,
                                      ),
                                      fillColor: Colors.white,
                                      filled: true,
                                      contentPadding: EdgeInsets.all(15),
                                      errorText: formState.passwordError,
                                      suffixIcon: IconButton(
                                        onPressed: () => formNotifer
                                            .togglePasswordVisibility(),
                                        icon: Icon(
                                          formState.isPasswordHidden
                                              ? Icons.visibility_off
                                              : Icons.visibility,
                                          color: Colors.black38,
                                        ),
                                      ),
                                    ),
                                  ),
                                  SizedBox(height: 18),
                                  formState.isLoading
                                      ? Center(
                                          child: CircularProgressIndicator(),
                                        )
                                      : MyButton(
                                          onTap: formState.isFormValid
                                              ? sigup
                                              : null,
                                          buttonText: "Sign Up",
                                        ),
                                  SizedBox(height: 20),
                                  Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: [
                                      Text(
                                        "Already have an account? ",
                                        style: TextStyle(
                                          color: secondaryTextColor,
                                        ),
                                      ),
                                      GestureDetector(
                                        onTap: () {
                                          NavigationHelper.push(
                                            context,
                                            LoginScreen(),
                                          );
                                        },
                                        child: Text(
                                          "Login",
                                          style: TextStyle(
                                            fontWeight: FontWeight.bold,
                                          ),
                                        ),
                                      ),
                                    ],
                                  ),
                                ],
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

 login_screen.dart

// ignore_for_file: use_build_context_synchronously
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dating_app/common/route.dart';
import 'package:dating_app/common/widget/background.dart';
import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/core/constants/error_handler.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/core/utils/utils.dart';
import 'package:dating_app/feature/account%20setup/account_setup_screen.dart';
import 'package:dating_app/feature/auth/provider/auth_provider.dart';
import 'package:dating_app/feature/auth/screen/forgot_password_screen.dart';
import 'package:dating_app/feature/auth/screen/signup_screen.dart';
import 'package:dating_app/feature/auth/service/auth_service.dart';
import 'package:dating_app/feature/home/home_screen.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class LoginScreen extends ConsumerWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    double height = MediaQuery.of(context).size.height;
    final formState = ref.watch(authFormProvider);
    final formNotifer = ref.read(authFormProvider.notifier);
    final authMethod = ref.read(authMethodProvider);

    void login() async {
      formNotifer.setLoading(true);

      final result = await authMethod.loginUser(
        email: formState.email,
        password: formState.password,
      );

      formNotifer.setLoading(false);

      if (result.success) {
        final user = FirebaseAuth.instance.currentUser;
        // Invalidate and reload providers
        // ref.invalidate(favouriteProvider);
        // NavigationHelper.pushAndRemoveUntil(context, AccountSetupScreen());
        if (user != null) {
          // Check Firestore for profileCompleted flag
          final doc = await FirebaseFirestore.instance
              .collection('users')
              .doc(user.uid)
              .get();

          final profileCompleted =
              doc.exists && (doc.data()?['profileCompleted'] == true);

          if (profileCompleted) {
            NavigationHelper.pushReplacement(
              context,
              HomeScreen(),
            ); // ← already set up
          } else {
            NavigationHelper.pushReplacement(
              context,
              AccountSetupScreen(),
            ); // ← already set up
          }
        }
        showAppSnackbar(
          context: context,
          type: SnackbarType.success,
          description: "Successfully logged in",
        );
      } else {
        // Use the error handler to get a detailed, localized error message
        String errorMessage;

        if (result.error != null) {
          // Get detailed error message from AuthErrorHandler
          errorMessage = AuthErrorHandler.getErrorMessage(
            result.error,
            context,
          );
        } else {
          // Fallback to the result message
          errorMessage = result.message;
        }

        showAppSnackbar(
          context: context,
          type: SnackbarType.error,
          description: errorMessage,
        );
      }
    }

    return Scaffold(
      // backgroundColor: backgroundColor,
      body: Stack(
        children: [
          SoftGradientBackground(),
          SingleChildScrollView(
            child: Column(
              children: [
                Stack(
                  children: [
                    SizedBox(
                      height: height / 2.3,
                      width: double.maxFinite,
                      child: Image.asset("assets/1232075.png"),
                    ),
                    Padding(
                      padding: EdgeInsets.symmetric(
                        horizontal: 10,
                        vertical: 40,
                      ),
                      child: IconButton(
                        style: IconButton.styleFrom(),
                        onPressed: () {
                          Navigator.pop(context);
                        },
                        icon: Icon(Icons.arrow_back_sharp, color: Colors.black),
                      ),
                    ),
                  ],
                ),
                SizedBox(
                  // color: backgroundColor,
                  child: Padding(
                    padding: EdgeInsets.all(15),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        SizedBox(height: 15),
                        Text(
                          "Welcome back!",
                          style: TextStyle(
                            fontSize: 24,
                            color: primaryTextColor,
                          ),
                        ),
                        Text(
                          "Sign in to your account",
                          style: TextStyle(color: secondaryTextColor),
                        ),
                        SizedBox(height: 20),
                        TextField(
                          autocorrect: false,
                          onChanged: (value) => formNotifer.updateEmail(value),
                          keyboardType: TextInputType.emailAddress,
                          decoration: InputDecoration(
                            prefixIcon: Icon(
                              Icons.email_outlined,
                              color: Colors.black38,
                            ),
                            labelText: "Enter your email",
                            fillColor: Colors.white,
                            filled: true,
                            border: OutlineInputBorder(
                              borderSide: BorderSide.none,
                            ),
                            contentPadding: EdgeInsets.all(15),
                            errorText: formState.emailError,
                          ),
                        ),
                        SizedBox(height: 5),
                        SizedBox(height: 10),
                        TextField(
                          autocorrect: false,
                          onChanged: (value) =>
                              formNotifer.updatePassword(value),
                          keyboardType: TextInputType.visiblePassword,
                          obscureText: formState.isPasswordHidden,
                          decoration: InputDecoration(
                            prefixIcon: Icon(
                              Icons.lock_outline,
                              color: Colors.black38,
                            ),
                            labelText: "Enter your password",
                            contentPadding: EdgeInsets.all(15),
                            border: OutlineInputBorder(
                              borderSide: BorderSide.none,
                            ),
                            fillColor: Colors.white,
                            filled: true,
                            errorText: formState.passwordError,
                            suffixIcon: IconButton(
                              onPressed: () =>
                                  formNotifer.togglePasswordVisibility(),
                              icon: Icon(
                                formState.isPasswordHidden
                                    ? Icons.visibility_off
                                    : Icons.visibility,
                                color: secondaryTextColor,
                              ),
                            ),
                          ),
                        ),

                        SizedBox(height: 10),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.end,
                          children: [
                            GestureDetector(
                              onTap: () {
                                NavigationHelper.push(
                                  context,
                                  ForgotPassword(),
                                );
                              },
                              child: Text(
                                "Forgot Password?",
                                style: TextStyle(color: Colors.blueAccent),
                              ),
                            ),
                          ],
                        ),
                        SizedBox(height: 20),
                        formState.isLoading
                            ? Center(
                                child: CircularProgressIndicator(
                                  color: primaryColor2,
                                ),
                              )
                            : MyButton(
                                onTap: formState.isFormValid ? login : null,
                                buttonText: "Login",
                              ),
                        SizedBox(height: 20),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              "Don't have an account? ",
                              style: TextStyle(color: secondaryTextColor),
                            ),
                            GestureDetector(
                              onTap: () {
                                NavigationHelper.push(context, SignupScreen());
                              },
                              child: Text(
                                "Sign Up",
                                style: TextStyle(fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

 forget_password_screen.dart

// forgot_password_screen.dart
import 'package:dating_app/common/widget/background.dart';
import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:dating_app/feature/auth/service/forget_password_controller.dart';
import 'package:flutter/material.dart';

class ForgotPassword extends StatefulWidget {
  const ForgotPassword({super.key});

  @override
  State<ForgotPassword> createState() => _ForgotPasswordState();
}

class _ForgotPasswordState extends State<ForgotPassword> {
  late final ForgotPasswordController _controller;

  @override
  void initState() {
    super.initState();
    _controller = ForgotPasswordController();
    _controller.addListener(_onControllerChanged);
  }

  @override
  void dispose() {
    _controller.removeListener(_onControllerChanged);
    _controller.dispose();
    super.dispose();
  }

  void _onControllerChanged() {
    setState(() {});
  }

  Future<void> _handleSendCode() async {
    final success = await _controller.sendPasswordResetEmail(context);
    if (success && mounted) {
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // backgroundColor: secondaryColor,
      appBar: AppBar(
        backgroundColor: secondaryColor,
        centerTitle: true,
        title: Text("Password Recovery"),
      ),
      body: Stack(
        children: [
          SoftGradientBackground(),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 15),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const SizedBox(height: 40),
                Center(
                  child: Text(
                    "Forgot Password",
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 22,
                      color: primaryTextColor,
                      height: 3,
                    ),
                  ),
                ),
                Text(
                  "We can help you reset your password and account information using your email address",
                  textAlign: TextAlign.center,
                  style: TextStyle(color: secondaryTextColor),
                ),
                const SizedBox(height: 40),
                ListenableBuilder(
                  listenable: _controller.emailController,
                  builder: (context, child) {
                    return TextField(
                      autocorrect: false,
                      controller: _controller.emailController,
                      keyboardType: TextInputType.emailAddress,
                      decoration: InputDecoration(
                        prefixIcon: const Icon(
                          Icons.email_outlined,
                          color: Colors.black38,
                        ),
                        suffixIcon: _buildEmailSuffixIcon(),
                        labelText: "Enter your email",
                        fillColor: Colors.white,
                        filled: true,
                        border: const OutlineInputBorder(
                          borderSide: BorderSide.none,
                        ),
                        contentPadding: const EdgeInsets.all(15),
                        errorText: _getEmailErrorText(
                          'Please enter a valid email address',
                        ),
                      ),
                    );
                  },
                ),
                const SizedBox(height: 40),
                MyButton(
                  onTap: _controller.canSendCode ? _handleSendCode : null,
                  buttonText: _controller.isLoading
                      ? "Sending..."
                      : "Send Code",
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget? _buildEmailSuffixIcon() {
    if (_controller.emailController.text.isEmpty) return null;

    return Icon(
      _controller.isValidEmail ? Icons.check_circle : Icons.error,
      color: _controller.isValidEmail ? Colors.green : Colors.red,
    );
  }

  String? _getEmailErrorText(String message) {
    final hasText = _controller.emailController.text.isNotEmpty;
    return hasText && !_controller.isValidEmail ? message : null;
  }
}


3. Google login
One of the most useful authentication methods at the current time. more  google_auth_model.dart

class GoogleAuthState {
  final bool isLoading;
  final String? error;

  GoogleAuthState({this.isLoading = false, this.error});

  GoogleAuthState copyWith({bool? isLoading, String? error}) {
    return GoogleAuthState(
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

 google_auth_provider.dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dating_app/common/route.dart';
import 'package:dating_app/feature/account%20setup/account_setup_screen.dart';
import 'package:dating_app/feature/auth/model/google_auth_model.dart';
import 'package:dating_app/feature/auth/service/google_auth_service.dart';
import 'package:dating_app/feature/home/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';

class GoogleAuthNotifier extends StateNotifier<GoogleAuthState> {
  final FirebaseServices _firebaseServices;

  GoogleAuthNotifier(this._firebaseServices) : super(GoogleAuthState());

  Future<void> signInWithGoogle(BuildContext context, WidgetRef ref) async {
    state = state.copyWith(isLoading: true, error: null);

    try {
      final result = await _firebaseServices.signInWithGoogle();
      state = state.copyWith(isLoading: false);

      if (result != null && result.user != null) {
        // Invalidate providers to reload data for new user
        // ref.invalidate(favouriteProvider);
        await Future.delayed(const Duration(milliseconds: 300));

        // Check Firestore for profileCompleted flag
        final doc = await FirebaseFirestore.instance
            .collection('users')
            .doc(result.user!.uid)
            .get();

        final profileCompleted =
            doc.exists && (doc.data()?['profileCompleted'] == true);
        if (context.mounted) {
          if (profileCompleted) {
            NavigationHelper.pushReplacement(context, HomeScreen());
            // await PresenceService().onUserLoggedIn();
          } else {
            NavigationHelper.pushReplacement(context, AccountSetupScreen());
            // await PresenceService().onUserLoggedIn();
          }
        }
      } else {
        state = state.copyWith(error: 'Google Sign-In canceled');
      }
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        error: 'Google Sign-In failed: ${e.toString()}',
      );
    }
  }

  void clearError() {
    state = state.copyWith(error: null);
  }
}

final googleAuthProvider =
    StateNotifierProvider<GoogleAuthNotifier, GoogleAuthState>((ref) {
      final firebaseService = ref.read(firebaseServicesProvider);
      return GoogleAuthNotifier(firebaseService);
    });

Last,  google_login_screen.dart

import 'package:dating_app/common/widget/my_button.dart';
import 'package:dating_app/feature/auth/provider/google_auth_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class GoogleLoginScreen extends ConsumerWidget {
  const GoogleLoginScreen({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final authState = ref.watch(googleAuthProvider);
    final authNotifier = ref.read(googleAuthProvider.notifier);
    // Show error snackbar if error exists
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (authState.error != null) {
        // mySnackBar(message: authState.error!, context: context);
        Future.delayed(const Duration(milliseconds: 100), () {
          ref.read(googleAuthProvider.notifier).clearError();
        });
      }
    });
    return MyButton(
      onTap: authState.isLoading
          ? null
          : () {
              authNotifier.clearError();
              authNotifier.signInWithGoogle(context, ref);
            },
      buttonText: authState.isLoading
          ? "Signing In...."
          : "Continue with Google",
    );
  }
}

This ensures convenience and accessibility for different types of users when building a dating app in Flutter with Firebase.

Account Setup and Onboarding

First impressions matter, and the onboarding flow is where users decide if they trust the app. I built a multi-step account setup screen that collected the essentials — name, age, gender, a short bio, and location — before asking users to upload a profile photo and select their interests from a predefined tag list.

The interest selection was actually one of the more satisfying UI problems to solve. I ended up building a horizontally scrollable chip-based selector with toggle states managed through Riverpod. The selected interests get stored as an array in Firestore and later used in the matching and search filtering logic.

Profile photos go through a pick-and-crop flow before upload. I used the image_picker packages to handle this, ensuring photos are reasonably consistent in aspect ratio before they hit Storage. Nobody wants their face cut off in a swipe card.

The Swipe Feature — The Heart of the App

This is the feature people associate most with dating apps, and getting it right required more thought than I initially expected.

On the other hand, I built a card stack UI where the top card responds to drag gestures. As the user swipes, the card tilts and shifts, and a directional indicator fades in — green heart for like, red X for dislike, blue star for super like. The next card beneath it scales up slightly as the top card moves, giving the impression of depth.

The four interactions I implemented are:

Like — Records a like in Firestore. If the other user has also liked this person, a match is created automatically and both users are notified.

Dislike — Records a dislike and removes the card from the feed. That profile won’t appear again.

Super Like — Similar to a like, but the other user receives a push notification immediately informing them someone super liked their profile. This is intentional — it’s a stronger signal and creates a moment of flattery even before a match is confirmed.

Pass — Temporarily skips the profile. Unlike dislikes, passes can theoretically surface again.

Swiped profiles are excluded from future queries using a stored list of seen user IDs in Firestore. For performance, I paginate the discovery feed and pre-fetch the next batch of profiles while the user is still swiping through the current set.

Favorites, Search, and Filters

Beyond swiping, users can browse and search in more deliberate ways.

The favorites list lets users bookmark profiles they’ve already interacted with or want to revisit. This is stored as a subcollection on the user document and renders as a simple scrollable list with basic profile preview cards.

The search feature queries Firestore with a display name prefix match. It’s not full-text search, but it covers the practical use case well. For true full-text search, integrating Algolia would be the right next step in a production environment.

Filters are where things got interesting. I implemented both age range and distance-based filtering. Age range maps directly to Firestore queries using the stored birth date field. Distance filtering is more involved — I store each user’s geohash (derived from their latitude/longitude) and use geohash range queries to find nearby users within a specified radius. The user sets their maximum distance in the filter panel, and that setting persists via Riverpod’s state and is also saved to Firestore so it survives app restarts.

Nearby Users — List View and Google Maps

The nearby feature was one of the more fun parts to build. When a user opens the Nearby tab, they see other users within their configured distance displayed in two ways:

List view renders profile cards in a scrollable list, each showing the user’s name, photo, age, and approximate distance.

Map view switches to a Google Maps widget with custom markers at each nearby user’s location. Also, tapping a marker opens a small info card with the user’s photo and name. I used the google_maps_flutter package for this, and the geolocator package to get the device’s current location.

One UX consideration I’m glad I handled: precise location is never shown. Distances are rounded, and markers are placed at the geohash center point, not the user’s exact coordinates. Privacy in dating apps isn’t optional.

Full-Featured Chat

Chat is where I probably spent the most time, and for good reason — it’s where the actual human connection happens.

Messages are stored as subcollections under each match document in Firestore. Real-time listeners keep the conversation updated without polling. Message types supported include text, images, and emojis. Read receipts are tracked, and a typing indicator shows when the other person is actively composing a message.

Audio calls are implemented using the Zegocloud Flutter SDK. When a user initiates a call, a call request document is written to Firestore. The recipient’s app listens for incoming call documents and triggers a full-screen incoming call UI — even if the app is in the background, thanks to FCM.

Video calls follow the same pattern, with the Zegocloud SDK handling the media streams. I enforced permission checks for microphone and camera access before allowing calls to initiate, with friendly fallback messaging if permissions are denied.

Push Notifications

Push notifications in a dating app are genuinely important — users expect to be notified when they get a match, a super like, or a message, even when the app is closed.

I used Firebase Cloud Messaging with a Cloud Functions backend to trigger notifications. When a match is created, a Cloud Function writes to FCM and sends a notification to both users. When a message is sent, if the recipient’s app is in the background or killed, FCM delivers a notification with the sender’s name and a preview of the message.

For foreground notifications (app open), I used the flutter_local_notifications package to display a banner within the app. Getting the foreground and background notification handling to behave consistently across both Android and iOS took some patience, but the end result is solid.

Profile Editing

Users can write their profile at any time through a dedicated edit screen — updating their bio, changing their interests, swapping their profile photo, or adjusting their discovery preferences. Changes write directly to Firestore and are reflected in the discovery feed for other users almost instantly.

I paid particular attention to image updates here. When a user changes their profile photo, the previous image is deleted from Firebase Storage to prevent orphaned files from accumulating.

State Management with Riverpod

Riverpod was the right call for this project. The app has a lot of interdependent state — auth state, user profile data, swipe feed data, match list, active chat, filter preferences, location — and Riverpod’s provider architecture kept it all organized without turning into a tangled mess.

However, I used StateNotifierProvider for mutable states like the swipe card stack and chat message lists, FutureProvider for one-time async loads like the initial profile fetch, and StreamProvider for real-time Firestore streams.

One pattern I found particularly useful was one currentUserProvider that exposes the logged-in user’s Firestore data as a stream. Almost every other provider that needs user data depends on this one, which keeps the dependency graph clean and predictable.

Common 

Here, I will provide you with a commonly used code during building a dating app in Flutter with Firebase that I have used, such as route, error handling, colors, utils, and more,
route.dart

import 'package:flutter/material.dart';

// // insted of using navigator every every time we have create a common navigation helper
class NavigationHelper {
  static void push(BuildContext context, Widget screen) {
    if (!context.mounted) return;
    Navigator.push(context, MaterialPageRoute(builder: (context) => screen));
  }

  static void pushReplacement(BuildContext context, Widget screen) {
    if (!context.mounted) return;
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => screen),
    );
  }

  static void pushAndRemoveUntil(BuildContext context, Widget screen) {
    if (!context.mounted) return;
    Navigator.pushAndRemoveUntil(
      context,
      MaterialPageRoute(builder: (context) => screen),
      (route) => false,
    );
  }
}

error_handler.dart auth error handler

import 'dart:async';
import 'dart:io';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';

class AuthErrorHandler {
  /// Get user-friendly error message from Firebase Auth exception
  static String getErrorMessage(dynamic error, BuildContext context) {
    // Handle TimeoutException
    if (error is TimeoutException) {
      return 'Request timeout. Please check your connection and try again.';
    }

    // Handle SocketException (No Internet)
    if (error is SocketException) {
      return 'No internet connection. Please check your network.';
    }

    // Handle network errors from error string
    final errorString = error.toString().toLowerCase();
    if (errorString.contains('socketexception') ||
        errorString.contains('no internet')) {
      return 'No internet connection. Please check your network.';
    }

    if (errorString.contains('timeout')) {
      return 'Request timeout. Please try again.';
    }

    // Handle FirebaseAuthException
    if (error is FirebaseAuthException) {
      switch (error.code) {
        // Login specific errors
        case 'user-not-found':
          return 'This email is not registered. Please sign up first.';

        case 'wrong-password':
          return 'Incorrect password. Please try again.';

        case 'invalid-email':
          return 'Invalid email address format.';

        case 'user-disabled':
          return 'This account has been disabled. Contact support.';

        case 'too-many-requests':
          return 'Too many failed attempts. Please try again later.';

        // Sign up specific errors
        case 'email-already-in-use':
          return 'This email is already registered. Please login.';

        case 'weak-password':
          return 'Password is too weak. Use at least 6 characters.';

        case 'operation-not-allowed':
          return 'This operation is not allowed.';

        // General errors
        case 'invalid-credential':
          return 'Invalid login credentials. Please check your email and password.';

        case 'network-request-failed':
          return 'Network error. Please check your connection.';

        default:
          // Return the Firebase error message if it's user-friendly
          if (error.message != null && error.message!.isNotEmpty) {
            return error.message!;
          }
          return 'An unexpected error occurred. Please try again.';
      }
    }

    // Default error
    return 'An unexpected error occurred. Please try again.';
  }

  /// Check if error is network related
  static bool isNetworkError(dynamic error) {
    if (error is SocketException || error is TimeoutException) {
      return true;
    }
    if (error is FirebaseAuthException) {
      return error.code == 'network-request-failed';
    }
    final errorString = error.toString().toLowerCase();
    return errorString.contains('network') ||
        errorString.contains('internet') ||
        errorString.contains('socketexception') ||
        errorString.contains('timeout');
  }

  /// Check if error is credential related (wrong password/email)
  static bool isCredentialError(dynamic error) {
    if (error is FirebaseAuthException) {
      return error.code == 'user-not-found' ||
          error.code == 'wrong-password' ||
          error.code == 'invalid-credential';
    }
    return false;
  }
}

interest_list.dart

// Default fallback interests in case Firestore hasn't loaded yet
const fallbackInterests = [
  {'id': 'gaming', 'label': 'Gaming', 'emoji': '🎮'},
  {'id': 'music', 'label': 'Music', 'emoji': '🎵'},
  {'id': 'dancing', 'label': 'Dancing', 'emoji': '💃'},
  {'id': 'nature', 'label': 'Nature', 'emoji': '🌿'},
  {'id': 'art', 'label': 'Art', 'emoji': '🎨'},
  {'id': 'travelling', 'label': 'Travelling', 'emoji': '✈️'},
  {'id': 'gym', 'label': 'Gym & Fitness', 'emoji': '💪'},
  {'id': 'designing', 'label': 'Designing', 'emoji': '✏️'},
  {'id': 'cooking', 'label': 'Cooking', 'emoji': '🍳'},
  {'id': 'reading', 'label': 'Reading', 'emoji': '📚'},
  {'id': 'photography', 'label': 'Photography', 'emoji': '📸'},
  {'id': 'movies', 'label': 'Movies', 'emoji': '🎬'},
  {'id': 'sports', 'label': 'Sports', 'emoji': '⚽'},
  {'id': 'technology', 'label': 'Technology', 'emoji': '💻'},
  {'id': 'yoga', 'label': 'Yoga', 'emoji': '🧘'},
  {'id': 'coffee', 'label': 'Coffee', 'emoji': '☕'},
];

colors.dart

import 'package:flutter/material.dart';

const Color firstColor = Color(0xffFF84A7);
const Color secondColor = Color(0xffE03368);
const Color thirdColor = Color(0xFFF27121);
Color primaryColor2 = Colors.pink.shade300;
const Color primaryColor = Colors.pink;
const Color secondaryColor = Colors.white;
const Color backgroundColor = Color(0xfff4f5f9);
const Color secondaryTextColor = Colors.black54;
const Color primaryTextColor = Colors.black;

Commonly used button my_button.dart

import 'package:dating_app/core/theme/colors.dart';
import 'package:flutter/material.dart';

class MyButton extends StatelessWidget {
  final VoidCallback? onTap;
  final String buttonText;
  final bool isIcons;
  final Color? color1;
  final Color? color2;
  final Widget? icon;
  const MyButton({
    super.key,
    required this.onTap,
    required this.buttonText,
    this.color1 = primaryColor,
    this.color2 = thirdColor,
    this.isIcons = false,
    this.icon = const Icon(Icons.account_circle_outlined),
  });

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return GestureDetector(
      onTap: onTap,
      child: Container(
        width: size.width,
        height: 60,
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(10),
          gradient: LinearGradient(
            begin: Alignment.centerLeft,
            end: Alignment.bottomRight,
            colors: [color1!, color2!],
          ),
        ),
        child: Center(
          child: Row(
            mainAxisAlignment: isIcons == true
                ? MainAxisAlignment.start
                : MainAxisAlignment.center,
            children: [
              if (isIcons == true) SizedBox(width: 45),
              if (isIcons == true) SizedBox(child: icon),
              if (isIcons == true) SizedBox(width: 25),
              Text(
                buttonText,
                textAlign: TextAlign.center,
                style: const TextStyle(
                  fontSize: 19,
                  fontWeight: FontWeight.w700,
                  color: Colors.white,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

snackbar utils.dart

import 'package:cherry_toast/cherry_toast.dart';
import 'package:cherry_toast/resources/arrays.dart';
import 'package:dating_app/core/theme/colors.dart';
import 'package:flutter/material.dart';

enum SnackbarType { success, error, warning }

void showAppSnackbar({
  required BuildContext context,
  required SnackbarType type,
  required String description,
  int toastDuration = 3000,
}) {
  switch (type) {
    case SnackbarType.success:
      CherryToast.success(
        toastDuration: Duration(milliseconds: toastDuration),
        height: 70,
        toastPosition: Position.top,
        shadowColor: secondaryColor,
        animationType: AnimationType.fromTop,
        displayCloseButton: false,
        backgroundColor: Colors.green.withAlpha(40),
        description: Text(
          description,
          maxLines: 3,
          overflow: TextOverflow.ellipsis,
          style: const TextStyle(color: Colors.green),
        ),
        title: const Text(
          "Successful",
          style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold),
        ),
      ).show(context);
      break;

    case SnackbarType.error:
      CherryToast.error(
        // toastDuration: Duration(milliseconds: 2500),
        // height: 70,
        toastPosition: Position.top,
        shadowColor: Colors.white,
        animationType: AnimationType.fromTop,
        displayCloseButton: false,
        backgroundColor: Colors.red.withAlpha(40),
        description: Text(
          description,
          maxLines: 3,
          overflow: TextOverflow.ellipsis,
          style: const TextStyle(color: Colors.red),
        ),
        title: const Text(
          "Fail",
          style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
        ),
      ).show(context);
      break;
    case SnackbarType.warning:
      CherryToast.warning(
        // height: 80,
        toastPosition: Position.top,
        shadowColor: Colors.white,
        animationType: AnimationType.fromTop,
        displayCloseButton: false,
        backgroundColor: Colors.amberAccent.withAlpha(80),
        description: Text(
          description,
          maxLines: 3,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(color: Colors.orange),
        ),
        title: Text(
          "Warning",
          style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
        ),
      ).show(context);
      break;
  }
}
main.dart contains:-
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dating_app/feature/account%20setup/account_setup_screen.dart';
import 'package:dating_app/feature/home/home_screen.dart';
import 'package:dating_app/feature/splashScreen/splash_screen.dart';
import 'package:dating_app/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:firebase_auth/firebase_auth.dart';

final navigatorKey = GlobalKey<NavigatorState>();
void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends ConsumerStatefulWidget {
  const MyApp({super.key});

  @override
  ConsumerState<MyApp> createState() => _MyAppState();
}

class _MyAppState extends ConsumerState<MyApp> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dating App',
      theme: ThemeData(textTheme: GoogleFonts.aBeeZeeTextTheme()),
      debugShowCheckedModeBanner: false,
      navigatorKey: navigatorKey,
      home: StreamBuilder<User?>(
        stream: FirebaseAuth.instance.authStateChanges(),
        builder: (context, snapshot) {
          // ✅ Bug 1 Fix: Wait for Firebase to restore auth state
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const SplashScreen(); // or a dedicated LoadingScreen
          }

          // Handle errors
          if (snapshot.hasError) {
            return Scaffold(
              body: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const Icon(
                      Icons.error_outline,
                      size: 64,
                      color: Colors.red,
                    ),
                    const SizedBox(height: 16),
                    const Text('Something went wrong'),
                    const SizedBox(height: 24),
                    ElevatedButton.icon(
                      onPressed: () => setState(() {}),
                      icon: const Icon(Icons.refresh),
                      label: const Text('Retry'),
                    ),
                  ],
                ),
              ),
            );
          }

          // No user logged in
          if (!snapshot.hasData || snapshot.data == null) {
            return const SplashScreen();
          }

          // Use a separate widget to avoid FutureBuilder recreation
          return UserHomeRouter(uid: snapshot.data!.uid);
        },
      ),
    );
  }
}

class UserHomeRouter extends StatefulWidget {
  final String uid;
  const UserHomeRouter({super.key, required this.uid});

  @override
  State<UserHomeRouter> createState() => _UserHomeRouterState();
}

class _UserHomeRouterState extends State<UserHomeRouter> {
  late Future<DocumentSnapshot> _userFuture;

  @override
  void initState() {
    super.initState();
    // INITIALIZE HERE - Otherwise FutureBuilder has nothing to watch
    _userFuture = _fetchUser();
  }

  Future<DocumentSnapshot> _fetchUser() {
    return FirebaseFirestore.instance
        .collection('users')
        .doc(widget.uid)
        .get()
        .timeout(
          const Duration(seconds: 10),
          onTimeout: () => throw Exception('Firestore timeout'),
        );
  }

  @override
  void didUpdateWidget(UserHomeRouter oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.uid != widget.uid) {
      setState(() {
        _userFuture = _fetchUser();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<DocumentSnapshot>(
      future: _userFuture,
      builder: (context, userDoc) {
        // If there's an error in the future, show it!
        // Don't just let it stay white.
        if (userDoc.hasError) {
          return Scaffold(body: Center(child: Text("Error: ${userDoc.error}")));
        }

        if (userDoc.connectionState == ConnectionState.waiting) {
          return const SplashScreen();
        }

        final data = userDoc.data?.data() as Map<String, dynamic>?;
        final profileCompleted = data?['profileCompleted'] == true;

        return profileCompleted
            ? const HomeScreen()
            : const AccountSetupScreen();
      },
    );
  }
}

Final Thoughts

Overall, building a dating app in Flutter with Firebase was one of the more complete projects I’ve taken on as a developer. Every feature touched multiple layers — UI, state, Firestore schema, Cloud Functions, notifications — and everything had to work together without feeling fragile.

Flutter and Firebase are genuinely well-suited to this kind of app. The real-time capabilities of Firestore, combined with Flutter’s smooth animation support and Riverpod’s structured state management, made it possible to build something that actually feels polished.

If you’re planning a similar project, the biggest advice I’d give is to design your Firestore data model carefully before writing a single line of feature code. Refactoring Firestore schemas mid-project is painful in a way that refactoring code usually isn’t. Think about your query patterns upfront, and structure your collections around them.

Finally, the core is done. The foundation is solid. What comes next is refinement — and there’s always more refinement to do.

Previous articleGroup Chat App Flutter

LEAVE A REPLY

Please enter your comment!
Please enter your name here