/// Application entry point: wires services, Provider, deep links, and [MaterialApp]. library; import 'dart:async'; import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'providers/auth_provider.dart'; import 'providers/bank_provider.dart'; import 'providers/preferences_provider.dart'; import 'providers/sync_provider.dart'; import 'providers/theme_provider.dart'; import 'providers/tx_meta_provider.dart'; import 'screens/splash_screen.dart'; import 'services/storage_service.dart'; import 'utils/app_config.dart'; import 'utils/app_theme.dart'; import 'utils/constants.dart'; import 'widgets/skip_build_when_covered.dart'; import 'widgets/sync_feedback_listener.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await AppConfig.load(); final storage = StorageService(); await storage.init(); final auth = AuthController(storage); await auth.init(); runApp(MyApp(storage: storage, auth: auth)); } /// Root widget. Theme and bank controllers live here so every route /// can `context.watch` them. class MyApp extends StatefulWidget { const MyApp({super.key, required this.storage, required this.auth}); final StorageService storage; final AuthController auth; @override State createState() => _MyAppState(); } class _MyAppState extends State { late final BankController _bank; late final TxMetaController _meta; late final PreferencesController _prefs; late final SyncController _sync; late final AppLinks _appLinks; StreamSubscription? _linkSub; @override void initState() { super.initState(); _meta = TxMetaController(widget.storage); _prefs = PreferencesController(widget.storage); _bank = BankController(storage: widget.storage)..refresh(); _sync = SyncController( auth: widget.auth, meta: _meta, bank: _bank, prefs: _prefs, ); _appLinks = AppLinks(); _listenForBankRedirects(); } Future _listenForBankRedirects() async { try { final initial = await _appLinks.getInitialLink(); if (initial != null) { await _bank.handleRedirect(initial); } } catch (error, stackTrace) { debugPrint('Initial deep link failed: $error\n$stackTrace'); } _linkSub = _appLinks.uriLinkStream.listen( (uri) { unawaited(_bank.handleRedirect(uri)); }, onError: (Object error, StackTrace stackTrace) { debugPrint('Deep link stream error: $error\n$stackTrace'); }, ); } @override void dispose() { _linkSub?.cancel(); _sync.dispose(); _bank.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider.value(value: widget.auth), ChangeNotifierProvider(create: (_) => ThemeController(widget.storage)), ChangeNotifierProvider.value(value: _prefs), ChangeNotifierProvider.value(value: _meta), ChangeNotifierProvider.value(value: _sync), ChangeNotifierProvider.value(value: _bank), ], child: const _MaterialRoot(), ); } } class _MaterialRoot extends StatelessWidget { const _MaterialRoot(); @override Widget build(BuildContext context) { final themeMode = context.watch().themeMode; return MaterialApp( title: kAppName, debugShowCheckedModeBanner: false, theme: AppTheme.light(), darkTheme: AppTheme.dark(), themeMode: themeMode, navigatorObservers: [appRouteObserver], builder: (context, child) { return SyncFeedbackListener( child: child ?? const SizedBox.shrink(), ); }, home: const SplashScreen(), ); } }