Firebase Auth Service Methods

Let’s setup the interface that our implementation class would be using to connect to firebase auth.

Features

  • Anonymous Sign In
  • Sign in with social providers like Google, Apple.
  • Email signIn
  • Utility to fetch the user access token
  • Utility to fetch the user ID
  • Update user profile on firebase user
abstract class IAuthService {
  Future<void> signInAnonymously();
  Future<void> signInWithGoogle();
  Future<void> signInWithApple();
  Future<void> signupWithEmailAndPassword(String email, String password);
  Future<void> loginWithEmailAndPassword(String email, String password);
  Future<String> getUserAccessToken();
  String? getUserId();
  Future<void> updateUserDetails({String? photoURL, String? displayName});
}

Example implementation of the firebase

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class FirebaseAuthService implements IAuthService {
  final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

  
  Future<void> signInAnonymously() async {
    if (_firebaseAuth.currentUser != null) {
      final token = await _firebaseAuth.currentUser?.getIdToken();
      debugPrint('TOKEN: $token');
      return;
    }

    try {
      await _firebaseAuth.signInAnonymously();
      debugPrint('Signed in with temporary account.');
    } on FirebaseAuthException catch (e) {
      switch (e.code) {
        case 'operation-not-allowed':
          debugPrint("Anonymous auth hasn't been enabled for this project.");
          break;
        default:
          debugPrint('Unknown error.');
      }
    }
  }

  
  Future<void> signInWithGoogle() async {
    try {
      final googleUser = await GoogleSignIn().signIn();
      final googleAuth = await googleUser?.authentication;

      final credential = GoogleAuthProvider.credential(
        accessToken: googleAuth?.accessToken,
        idToken: googleAuth?.idToken,
      );

      if (_firebaseAuth.currentUser == null) {
        await _firebaseAuth.signInWithCredential(credential);
      } else {
        await _linkOrSignInWithCredential(credential);
      }

      await updateUserDetails(
        photoURL: googleUser?.photoUrl,
        displayName: googleUser?.displayName,
      );
    } catch (error) {
      debugPrint('error while google sign-in $error');
      throw Exception('google sign-in failed');
    }
  }

  
  Future<void> signInWithApple() async {
    try {
      final userCredential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
      );
      final credential = OAuthProvider('apple.com').credential(
        idToken: userCredential.identityToken,
        accessToken: userCredential.authorizationCode,
      );

      if (_firebaseAuth.currentUser == null) {
        await _firebaseAuth.signInWithCredential(credential);
      } else {
        await _linkOrSignInWithCredential(credential);
      }
    } catch (error) {
      debugPrint('error while apple sign-in $error');
      throw Exception('apple sign-in failed');
    }
  }

  
  Future<void> signupWithEmailAndPassword(String email, String password) async {
    try {
      await _firebaseAuth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      switch (e.code) {
        case 'weak-password':
          throw Exception('The password provided is too weak.');
        case 'email-already-in-use':
          throw Exception('The account already exists for that email.');
        case 'invalid-email':
          throw Exception('Please provide a valid email');
        default:
          throw Exception('Email Signup Failed');
      }
    }
  }

  
  Future<void> loginWithEmailAndPassword(String email, String password) async {
    try {
      await _firebaseAuth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      debugPrint(e.code);
      switch (e.code) {
        case 'invalid-credential':
          throw Exception('Invalid credentials');
        case 'wrong-password':
          throw Exception('Wrong password provided for that user.');
        default:
          throw Exception('Email Login Failed');
      }
    }
  }

  
  Future<String> getUserAccessToken() async {
    return await _firebaseAuth.currentUser?.getIdToken() ?? '';
  }

  
  String? getUserId() {
    return _firebaseAuth.currentUser?.uid;
  }

  
  Future<void> updateUserDetails({
    String? photoURL,
    String? displayName,
  }) async {
    final currentUser = _firebaseAuth.currentUser;
    if (currentUser == null) return;

    if (photoURL != null) {
      await currentUser.updatePhotoURL(photoURL);
    }
    if (displayName != null) {
      await currentUser.updateDisplayName(displayName);
    }
  }

  Future<void> _linkOrSignInWithCredential(AuthCredential credential) async {
    try {
      await _firebaseAuth.currentUser?.linkWithCredential(credential);
    } on FirebaseAuthException catch (error) {
      switch (error.code) {
        case 'provider-already-linked':
          throw Exception('The provider has already been linked to the user.');
        case 'invalid-credential':
          throw Exception("The provider's credential is not valid.");
        case 'credential-already-in-use':
          debugPrint(
              'The account corresponding to the credential already exists, '
              'or is already linked to a Firebase User.');
          try {
            await _firebaseAuth.signInWithCredential(error.credential!);
          } catch (e) {
            debugPrint('Error signing in with credentials: $e');
          }
          break;
        default:
          debugPrint('Unknown error.');
      }
    }
  }
}

State Management with Bloc

State

In the bloc state we will track whether the user is authenticated on or not. That is the single responsibility of the auth bloc. When the user is signed in you will have access to the user object from the bloc’s state.

part of 'auth_bloc.dart';


class AuthState with _$AuthState {
  const factory AuthState.signedOut() = _SignedOut;
  const factory AuthState.signedIn({required User user}) = _SignedIn;
}

Events

The AuthEvent class defines two main events that can be triggered in the authentication flow:

  1. signedInWithProvider: This event is triggered when a user successfully signs in through any authentication provider (Google, Apple, Email, or Anonymous). It carries the Firebase User object which contains the user’s authentication details. When this event is processed, the bloc will transition to the signedIn state with the provided user.

  2. signOut: This event is triggered when the user wants to sign out of the application. When processed, it will transition the bloc to the signedOut state.

These events work in conjunction with the IAuthService implementation:

  • When a user successfully authenticates through any of the service methods (signInWithGoogle, signInWithApple, etc.), the bloc should receive a signedInWithProvider event with the authenticated user.
  • When the user signs out, the bloc should receive a signOut event.

The bloc’s state management ensures that the UI always reflects the current authentication status of the user, whether they are signed in (with their user details) or signed out.

part of 'auth_bloc.dart';


class AuthEvent with _$AuthEvent {
  const factory AuthEvent.signedInwithProvider({
    required User user,
  }) = _SignedInWithProvider;
  const factory AuthEvent.signOut() = _SignOut;
}

Bloc Implementation Code

The AuthBloc implementation manages the authentication state and handles various authentication-related operations. Here’s a breakdown of its key features:

Initialization and State Management

  • The bloc initializes with a signedOut state
  • It sets up a listener for Firebase Auth state changes using authStateChanges()
  • When auth state changes, it automatically dispatches appropriate events:
    • signOut when user is null
    • signedInWithProvider when a user is present

Event Handlers

  1. Sign Out Handler (_SignOut)

    • Transitions to signedOut state
    • Handles RevenueCat (Purchases) logout if configured and not anonymous
    • Cleans up user session
  2. Sign In Handler (_SignedInWithProvider)

    • Transitions to signedIn state with the user object
    • Sets up user tracking across various services:
      • Firebase Crashlytics user identifier
      • Firebase Analytics user ID
      • Facebook App Events user ID
      • RevenueCat user login
    • Logs user details for debugging

Utility Methods

  • getUser(): Returns the current user if signed in, null otherwise
  • isLoggedIn(): Returns a boolean indicating if the user is currently logged in
  • close(): Properly cleans up the auth state stream subscription when the bloc is closed

Integration with External Services

The bloc integrates with multiple services to provide a complete authentication solution:

  • Firebase Authentication for core auth functionality
  • Firebase Analytics for user tracking
  • Firebase Crashlytics for error reporting
  • Facebook App Events for analytics
  • RevenueCat for subscription management

Each integrated service requires a user identifier to function properly. After successful authentication, we associate the user ID with these services to enable features like analytics tracking and purchase monitoring. For instance, when a user makes a purchase or triggers an analytics event, the system needs their ID to properly attribute that activity.

import 'dart:async';
import 'dart:developer';

import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

part 'auth_bloc.freezed.dart';
part 'auth_event.dart';
part 'auth_state.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  AuthBloc({
    required IAuthService authService,
    required FirebaseAnalytics analytics,
    required FirebaseCrashlytics crashlytics,
    required FacebookAppEvents facebookEvents,
  })  : _authService = authService,
        _analytics = analytics,
        _crashlytics = crashlytics,
        _facebookEvents = facebookEvents,
        super(const AuthState.signedOut()) {
    // Listen to auth state changes
    _authStateStreamSubscription = _authService.authStateChanges.listen((user) {
      if (user == null) {
        add(const AuthEvent.signOut());
      } else {
        add(AuthEvent.signedInwithProvider(user: user));
      }
    });

    // Handle Sign Out
    on<_SignOut>((event, emit) async {
      debugPrint('DEBUG: AuthStateChange: Sign Out');
      emit(const AuthState.signedOut());
      
      // Handle Initial logout
      final isConfigured = await Purchases.isConfigured;
      if (isConfigured) {
        final isAnon = await Purchases.isAnonymous;
        if (!isAnon) {
          await Purchases.logOut();
        }
      }
    });

    // Handle Sign In
    on<_SignedInWithProvider>((event, emit) async {
      final token = await event.user.getIdToken();
      final user = event.user;
      log('DEBUG: AuthStateChange: Sign In: $token');
      
      emit(AuthState.signedIn(user: user));
      
      // Set up user tracking across services
      await _crashlytics.setUserIdentifier(user.uid);
      await _analytics.setUserId(id: user.uid);
      await _facebookEvents.setUserID(user.uid);
      await Purchases.logIn(user.uid);

      debugPrint(
        [
          'DEBUG: user details ${user.photoURL}, ${user.displayName},',
          ' ${user.email} ',
        ].join(),
      );
    });
  }

  final IAuthService _authService;
  final FirebaseAnalytics _analytics;
  final FirebaseCrashlytics _crashlytics;
  final FacebookAppEvents _facebookEvents;
  StreamSubscription<User?>? _authStateStreamSubscription;

  User? getUser() {
    if (state is _SignedIn) {
      final signedInState = state as _SignedIn;
      return signedInState.user;
    }
    return null;
  }

  bool isLoggedIn() {
    if (state is _SignedIn) {
      return true;
    }
    return false;
  }

  
  Future<void> close() {
    _authStateStreamSubscription?.cancel();
    return super.close();
  }
}

Registering the auth bloc as dependency

  getIt
    ..registerSingleton(AnalyticsCore()) // Other dependencies
    ..registerSingleton(AuthBloc()) // Inject dependencies according to your integrations

Navigation and Authentication Flow

For a comprehensive guide on implementing navigation with authentication in your Flutter app, including route protection, deep linking, and best practices, please refer to our Navigation Guide.

The navigation guide covers:

  • Basic navigation setup with GoRouter
  • Authentication flow implementation
  • Route protection and guards
  • State management integration
  • Deep linking configuration
  • Common navigation patterns
  • Troubleshooting common issues