0

I'm developing a Flutter app using Firebase Auth, and I'm experiencing a persistent issue where users get logged out every time they close the app completely (kill from recent apps) and reopen it. The authentication should persist automatically on mobile platforms, but it's not working.

Problem:

  • User logs in successfully ✅
  • User closes app normally (minimize) - stays logged in ✅
  • User kills app completely and reopens - gets logged out and redirected to login❌

main.dart:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  try {
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    await SecurityService.initialize();
    AntiDebugService.startMonitoring();
    ObfuscationUtils.executeDummyOperations();
  } catch (error) {
    debugPrint('Initialization error: $error');
  }

  runApp(const KhedamApp());

  _initializeBackgroundServices();
}

void _initializeBackgroundServices() async {
  try {
    FlutterError.onError = (FlutterErrorDetails details) {
      RuntimeReportService.reportCrash(
          details.exception, details.stack ?? StackTrace.current);
    };

    runZonedGuarded(() {}, (error, stackTrace) {
      RuntimeReportService.reportCrash(error, stackTrace);
    });

    await RuntimeReportService.init();
    AppLifecycleManager().initialize();

    final notificationService = NotificationService();
    await notificationService.initPushNotifications();

    SanctionCleanupService.startPeriodicCleanup();
  } catch (error) {
    debugPrint('Background initialization error: $error');
  }
}

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

  @override
  State<KhedamApp> createState() => _KhedamAppState();
}

class _KhedamAppState extends State<KhedamApp> with WidgetsBindingObserver {
  final LocaleProvider _localeProvider = LocaleProvider();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.resumed) {
      DeviceSessionService.updateActivity();
    }
  }

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
        providers: [
          Provider<AuthService>(create: (_) => AuthService()),
          ChangeNotifierProvider<LocaleProvider>.value(value: _localeProvider),
        ],
        child: Consumer<LocaleProvider>(
          builder: (context, localeProvider, _) => MaterialApp(
            title: 'Khedam',
            routes: {'/login': (context) => const LoginScreen()},
            debugShowCheckedModeBanner: false,
            localizationsDelegates: const [
              AppLocalizations.delegate,
              GlobalMaterialLocalizations.delegate,
              GlobalWidgetsLocalizations.delegate,
              GlobalCupertinoLocalizations.delegate,
            ],
            supportedLocales: const [
              Locale('fr'),
              Locale('ar'),
              Locale('en'),
            ],
            locale: localeProvider.locale,
            theme: ThemeData(
              colorScheme: ColorScheme.fromSeed(
                seedColor: AppColors.primary,
                brightness: Brightness.light,
              ),
              textTheme: GoogleFonts.notoSansTextTheme(),
              useMaterial3: true,
              appBarTheme: const AppBarTheme(
                backgroundColor: AppColors.primary,
                foregroundColor: Colors.white,
                elevation: 0,
              ),
              elevatedButtonTheme: ElevatedButtonThemeData(
                style: ElevatedButton.styleFrom(
                  backgroundColor: AppColors.primary,
                  foregroundColor: Colors.white,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
              cardTheme: CardTheme(
                elevation: 2,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
            ),
            home: const SplashScreen(),
          ),
        ));
  }
}

splash_screen.dart:

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

  @override
  State<SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<SplashScreen> {
  bool _hasNavigated = false;

  @override
  void initState() {
    super.initState();
    _initializeApp();
    Future.delayed(const Duration(seconds: 10), () {
      if (!_hasNavigated && mounted) {
        if (FirebaseAuth.instance.currentUser != null) {
          RuntimeReportService.reportSplashTimeout();
        }
      }
    });
  }

  void _initializeApp() async {
    final splashStart = DateTime.now();
    const minSplashDuration = Duration(milliseconds: 2000);

    try {
      _initializeSplashServices();

      final user = FirebaseAuth.instance.currentUser;

      final elapsed = DateTime.now().difference(splashStart);
      if (elapsed < minSplashDuration) {
        await Future.delayed(minSplashDuration - elapsed);
      }

      if (mounted && !_hasNavigated) {
        _hasNavigated = true;
        _navigateBasedOnAuthState(user);
      }
    } catch (e) {
      final elapsed = DateTime.now().difference(splashStart);
      if (elapsed < minSplashDuration) {
        await Future.delayed(minSplashDuration - elapsed);
      }

      if (mounted && !_hasNavigated) {
        _hasNavigated = true;
        _navigateBasedOnAuthState(null);
      }
    }
  }

  void _initializeSplashServices() async {
    try {
      SecurityService.performRuntimeCheck();
      AntiDebugService.checkPoint();

      _startSessionHeartbeat();
    } catch (e) {
      debugPrint('Splash initialization error: $e');
    }
  }

  void _startSessionHeartbeat() {
    Future.delayed(const Duration(minutes: 5), () {
      DeviceSessionService.updateActivity();
      if (mounted) _startSessionHeartbeat();
    });
  }

  void _navigateBasedOnAuthState(User? user) async {
    if (user != null) {
      debugPrint('✅ User IS authenticated redirect to home');
      DeviceSessionService.updateActivity();

      if (mounted) {
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (_) => const HomeScreen()),
        );
      }
    } else {
      debugPrint('NO authenticated user redirect to Login');

      if (mounted) {
        Navigator.of(context).pushReplacement(
          MaterialPageRoute(builder: (_) => const LoginScreen()),
        );
      }
    }
  }

auth_service.dart:

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  
  User? get currentUser => _auth.currentUser;
  Stream<User?> get authStateChanges => _auth.authStateChanges();
}
Future<UserModel?> signInWithEmailAndPassword(String email, String password) async {
  UserCredential result = await _auth.signInWithEmailAndPassword(
    email: email,
    password: password,
  );

  if (result.user != null) {
    return userModel; // Return user data
  }
  return null;
}
Future<void> signOut() async {
  await DeviceSessionService.endSession();
  await _googleSignIn.signOut();
  await _auth.signOut();
}

i've tried using StreamBuilder with authStateChanges(), but failed i've tried using authStateChanges().first in splash screen, but failed Both methods fail - the auth state returns null on cold app startup even though the user was previously authenticated, the logs say:

Connection state: ConnectionState.active
Has data: false
User: NULL (NULL)

it shows that firebase auth is actively returning null even though the user was authenticated before killing the app.

1 Answer 1

2

Please never do something like this:

Future.delayed(const Duration(seconds: 10), () {
  if (!_hasNavigated && mounted) {
    if (FirebaseAuth.instance.currentUser != null) {
      RuntimeReportService.reportSplashTimeout();
    }
  }
});

Instead of waiting for 10 seconds, you can listen for auth state changes as shown in the first example in the documentation on getting the current user. From there:

FirebaseAuth.instance
  .authStateChanges()
  .listen((User? user) {
    if (user == null) {
      print('User is currently signed out!');
    } else {
      print('User is signed in!');
    }
  });

The first time the listen callback gets called is after Firebase has restored the user state, so user will have the correct value at that point (which is typically much faster than 10 seconds).

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you Frank, my mistake is I was checking FirebaseAuth.instance.currentUser immediately in the splash screen and i wasn't waiting for Firebase to complete the auth restoration process.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.