import "package:flutter/material.dart"; import "api.dart"; import "app_settings.dart"; import "auth.dart"; import "biometric.dart"; import "theme.dart"; import "screens/lock_screen.dart"; import "screens/login_screen.dart"; import "screens/root_shell.dart"; // Simple app-wide singletons (small app — no DI framework needed). final apiClient = ApiClient(); final authService = AuthService(apiClient); final appSettings = AppSettings(); Future main() async { WidgetsFlutterBinding.ensureInitialized(); await apiClient.loadServerUrl(); await appSettings.loadFromStorage(); await authService.loadFromStorage(); // Prime the biometric-enabled cache; if a session survived from a previous // run and biometric login is on, start locked so the app requires an unlock // instead of jumping straight to the dashboard. final bioEnabled = await biometricAuth.isEnabled(); if (authService.isAuthenticated && bioEnabled) { authService.lock(); } // If a token survived from a previous run, refresh the profile so the saved // appearance prefs (theme/font/date format) apply on boot. if (authService.isAuthenticated) { apiClient.getMe().then((p) => appSettings.applyFromProfile(p)).catchError((_) {}); } runApp(const CarControlApp()); } class CarControlApp extends StatefulWidget { const CarControlApp({super.key}); @override State createState() => _CarControlAppState(); } class _CarControlAppState extends State with WidgetsBindingObserver { // Grace period: a quick app-switch (returning within this window) does NOT // re-lock; staying in the background longer does. static const _lockGrace = Duration(seconds: 30); DateTime? _backgroundedAt; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // Re-lock when the app returns from the background, but only if it was away // longer than the grace period — so quick app-switches don't re-lock. // Handle only 'paused'/'resumed' (not 'inactive', which also fires // transiently while the OS biometric prompt is showing). if (state == AppLifecycleState.paused) { _backgroundedAt = DateTime.now(); } else if (state == AppLifecycleState.resumed) { final since = _backgroundedAt; _backgroundedAt = null; if (since != null && DateTime.now().difference(since) > _lockGrace && authService.isAuthenticated && biometricAuth.enabledCached) { authService.lock(); } } } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: appSettings, builder: (context, _) => MaterialApp( title: "DriverVault", debugShowCheckedModeBanner: false, theme: DriverVault.theme(Brightness.light), darkTheme: DriverVault.theme(Brightness.dark), themeMode: appSettings.themeMode, // Apply the user's font-size preference app-wide. builder: (context, child) => MediaQuery( data: MediaQuery.of(context).copyWith( textScaler: TextScaler.linear(appSettings.textScale), ), child: child!, ), home: ListenableBuilder( listenable: authService, builder: (context, _) { if (!authService.isAuthenticated) return const LoginScreen(); if (authService.locked) return const LockScreen(); return const RootShell(); }, ), ), ); } }