/ app / lib / main.dart
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  }