import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/api/api_client_provider.dart'; import 'core/auth/auth_notifier.dart'; import 'core/chat/mitra_chat_notifier.dart'; import 'core/status/status_notifier.dart'; import 'core/chat/chat_request_notifier.dart'; import 'core/chat/widgets/chat_request_overlay.dart'; import 'core/notifications/notification_service.dart'; import 'core/theme/halo_theme.dart'; import 'router.dart'; /// Shared app bootstrap used by every flavor entrypoint /// (main_dev / main_staging / main_prod). Each entrypoint passes the /// flavor's own [FirebaseOptions] and a [flavor] tag. Future bootstrap({ required FirebaseOptions firebaseOptions, required String flavor, }) async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: firebaseOptions); final messaging = FirebaseMessaging.instance; await messaging.requestPermission(); runApp(const ProviderScope(child: App())); } class App extends ConsumerStatefulWidget { const App({super.key}); @override ConsumerState createState() => _AppState(); } class _AppState extends ConsumerState with WidgetsBindingObserver { bool _fcmRegistered = false; // Session the chat WS was on at the moment we backgrounded. Restored on // resume so a backgrounded mitra reconnects to the same chat once they // foreground the app. Mirrors the customer-app fix (main.dart on the // client side) — backend's sendMessage checks recipient WS readyState // before falling back to FCM, so leaving the WS open while paused makes // FCM never fire and the mitra misses customer messages in background. String? _pausedChatSessionId; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { ref.read(onlineStatusProvider.notifier).onAppPaused(); // Close the chat WS so backend `sendMessage` falls back to FCM when // the customer sends a message. Stash the active session_id so we // can rejoin it on resume. final chatNotifier = ref.read(mitraChatProvider.notifier); final sid = chatNotifier.connectedSessionId; if (sid != null) { _pausedChatSessionId = sid; chatNotifier.disconnect(); } } else if (state == AppLifecycleState.resumed) { ref.read(onlineStatusProvider.notifier).onAppResumed(); // Reconnect to the chat we backgrounded out of, if any. final saved = _pausedChatSessionId; _pausedChatSessionId = null; if (saved != null) { // ignore: discarded_futures ref.read(mitraChatProvider.notifier).connect(saved); } } } void _registerFcmToken() { if (_fcmRegistered) return; _fcmRegistered = true; Future(() async { try { final token = await FirebaseMessaging.instance.getToken(); if (token != null) { await ref.read(apiClientProvider).post('/api/shared/device-token', data: {'token': token}); } } catch (_) { _fcmRegistered = false; } }); } @override Widget build(BuildContext context) { // Listen for auth changes to load status and register FCM ref.listen(mitraAuthProvider, (prev, next) { final data = next.valueOrNull; if (data is MitraAuthAuthenticatedData) { ref.read(onlineStatusProvider.notifier).load(); _registerFcmToken(); } }); final router = ref.watch(routerProvider); NotificationService.initialize(router); NotificationService.onChatRequestTapped = (sessionId) { ref.read(chatRequestProvider.notifier).setIncomingFromNotification(sessionId); }; return ChatRequestOverlay( child: MaterialApp.router( title: 'Halo Bestie Mitra', theme: haloThemeData(), routerConfig: router, ), ); } }