main.dart
1 import 'package:flutter/material.dart'; 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 4 import 'core/router.dart'; 5 import 'core/theme/app_theme.dart'; 6 import 'core/utils/logger.dart'; 7 import 'core/utils/platform_utils.dart'; 8 import 'providers/theme_provider.dart'; 9 import 'providers/ble_provider.dart' show bleEnabledProvider, bleInitializedProvider; 10 import 'providers/transport_provider.dart' show dhtPollingProvider, irohP2PProvider, torP2PProvider, networkModeProvider, NetworkMode; 11 import 'screens/auth/passphrase_screen.dart'; 12 import 'package:background_fetch/background_fetch.dart'; 13 14 import 'services/background_fetch_service.dart'; 15 import 'services/notification_service.dart'; 16 import 'src/rust/frb_generated.dart'; 17 18 final _navigatorKey = GlobalKey<NavigatorState>(); 19 20 Future<void> main() async { 21 WidgetsFlutterBinding.ensureInitialized(); 22 23 // Initialize the Rust FFI bridge 24 await RustLib.init(); 25 26 // Initialize logger 27 await LoggerService.instance.initialize(); 28 29 // Initialize notification service (replaces broken workmanager approach) 30 try { 31 await NotificationService.instance.initialize(); 32 NotificationService.navigatorKey = _navigatorKey; 33 } catch (e) { 34 LoggerService.instance.info('Main', 'Notification service init failed: $e'); 35 } 36 37 // Configure background fetch for DHT polling when app is backgrounded (mobile only) 38 if (PlatformUtils.supportsBackgroundFetch) { 39 try { 40 await BackgroundFetchService.instance.configure(); 41 } catch (e) { 42 LoggerService.instance.info('Main', 'Background fetch config failed: $e'); 43 } 44 45 // Register headless task for when app is terminated by OS 46 BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask); 47 } 48 49 // Lock phones to portrait, allow tablets/desktops all orientations 50 await AppResponsive.configureOrientations(); 51 52 // Note: DeadDropService initialization moved to PassphraseScreen 53 // which handles passphrase retrieval/creation 54 55 runApp(const ProviderScope(child: DeadDropApp())); 56 } 57 58 class DeadDropApp extends ConsumerStatefulWidget { 59 const DeadDropApp({super.key}); 60 61 @override 62 ConsumerState<DeadDropApp> createState() => _DeadDropAppState(); 63 } 64 65 class _DeadDropAppState extends ConsumerState<DeadDropApp> 66 with WidgetsBindingObserver { 67 bool _isUnlocked = false; 68 69 @override 70 void initState() { 71 super.initState(); 72 WidgetsBinding.instance.addObserver(this); 73 } 74 75 @override 76 void dispose() { 77 WidgetsBinding.instance.removeObserver(this); 78 super.dispose(); 79 } 80 81 @override 82 void didChangeAppLifecycleState(AppLifecycleState state) { 83 // On iOS, the app stays alive in background due to BLE background modes. 84 // The existing DHT polling timer continues running. Notifications will 85 // display because the app is backgrounded (not in foreground). 86 // 87 // When the app resumes, activeContactHex tracking is handled by ChatScreen 88 // init/dispose — no action needed here. 89 } 90 91 void _onUnlocked() { 92 setState(() { 93 _isUnlocked = true; 94 }); 95 } 96 97 @override 98 Widget build(BuildContext context) { 99 final themeMode = ref.watch(themeModeProvider); 100 final scale = AppResponsive.scaleFromView(View.of(context)); 101 102 return MaterialApp( 103 title: 'Ekko', 104 debugShowCheckedModeBanner: false, 105 106 // Theme — scaled for tablets 107 theme: AppTheme.light(scaleFactor: scale), 108 darkTheme: AppTheme.dark(scaleFactor: scale), 109 themeMode: themeMode, 110 111 // Show passphrase screen until unlocked, then show main app 112 home: _isUnlocked 113 ? const _MainApp() 114 : PassphraseScreen(onSuccess: _onUnlocked), 115 ); 116 } 117 } 118 119 /// Main app content after unlocking 120 class _MainApp extends ConsumerStatefulWidget { 121 const _MainApp(); 122 123 @override 124 ConsumerState<_MainApp> createState() => _MainAppState(); 125 } 126 127 class _MainAppState extends ConsumerState<_MainApp> { 128 bool _initialized = false; 129 130 @override 131 void initState() { 132 super.initState(); 133 // Auto-bootstrap network on first build 134 WidgetsBinding.instance.addPostFrameCallback((_) { 135 if (!_initialized) { 136 _initialized = true; 137 _initializeNetwork(); 138 } 139 }); 140 } 141 142 Future<void> _initializeNetwork() async { 143 // Auto-start with balanced mode (BLE + DHT) on app launch 144 await ref.read(networkModeProvider.notifier).setMode(NetworkMode.balanced); 145 146 // Start iroh P2P (will auto-start DHT if needed for peer discovery) 147 try { 148 await ref.read(irohP2PProvider.notifier).start(); 149 } catch (e) { 150 debugPrint('[Main] iroh P2P startup failed (non-fatal): $e'); 151 } 152 153 // Start Tor P2P (onion service + peer connections) 154 if (PlatformUtils.supportsTor) { 155 try { 156 await ref.read(torP2PProvider.notifier).start(); 157 } catch (e) { 158 debugPrint('[Main] Tor P2P startup failed (non-fatal): $e'); 159 } 160 } 161 162 // Auto-initialize BLE if enabled (starts background scan timer for gossip relay) 163 // BLE is mobile-only — skip on desktop platforms 164 if (PlatformUtils.supportsBle) { 165 final bleEnabled = ref.read(bleEnabledProvider); 166 LoggerService.instance.info('Main', 'BLE auto-init: enabled=$bleEnabled'); 167 if (bleEnabled) { 168 try { 169 await ref.read(bleInitializedProvider.future); 170 LoggerService.instance.info('Main', 'BLE auto-init: success'); 171 } catch (e) { 172 LoggerService.instance.info('Main', 'BLE initialization failed (non-fatal): $e'); 173 } 174 } 175 } 176 } 177 178 @override 179 Widget build(BuildContext context) { 180 final router = ref.watch(routerProvider); 181 182 // Watch providers with DHT-dependent listeners to ensure they activate 183 ref.watch(dhtPollingProvider); 184 ref.watch(irohP2PProvider); 185 186 final scale = AppResponsive.scaleFromView(View.of(context)); 187 188 return MaterialApp.router( 189 title: 'Ekko', 190 debugShowCheckedModeBanner: false, 191 192 // Theme — scaled for tablets 193 theme: AppTheme.light(scaleFactor: scale), 194 darkTheme: AppTheme.dark(scaleFactor: scale), 195 themeMode: ref.watch(themeModeProvider), 196 197 // Router 198 routerConfig: router, 199 ); 200 } 201 }