import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:go_router/go_router.dart'; import '../models/notification_item.dart'; import '../providers/notifications_provider.dart'; import '../providers/notification_navigation_provider.dart'; /// Wraps the app and installs both a Supabase realtime listener and the /// FCM handlers described in the frontend design. /// /// The navigator key is required so that snackbars and navigation can be /// triggered from outside the widget tree (e.g. from realtime callbacks). class NotificationBridge extends ConsumerStatefulWidget { const NotificationBridge({ required this.navigatorKey, required this.child, super.key, }); final GlobalKey navigatorKey; final Widget child; @override ConsumerState createState() => _NotificationBridgeState(); } class _NotificationBridgeState extends ConsumerState with WidgetsBindingObserver { // store previous notifications to diff List _prevList = []; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _setupFcmHandlers(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); // App lifecycle is now monitored, but individual streams handle their own // recovery via StreamRecoveryWrapper. This no longer forces a global reconnect, // which was the blocking behavior users complained about. if (state == AppLifecycleState.resumed) { // Future: Could trigger stream-specific recovery hints if needed. } } void _showBanner(String type, NotificationItem item) { final ctx = widget.navigatorKey.currentState?.overlay?.context; if (ctx == null) return; ScaffoldMessenger.of(ctx).showSnackBar( SnackBar( content: Text('New $type received!'), action: SnackBarAction( label: 'View', onPressed: () => _navigateToNotification(item), ), ), ); } void _navigateToNotification(NotificationItem item) { widget.navigatorKey.currentState?.pushNamed( '/notification-detail', arguments: item, ); } void _setupFcmHandlers() { // ignore foreground messages; realtime websocket will surface them FirebaseMessaging.onMessage.listen((_) {}); // handle taps when the app is backgrounded FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageTap); // handle a tap that launched the app from a terminated state FirebaseMessaging.instance.getInitialMessage().then((msg) { if (msg != null) _handleMessageTap(msg); }); } void _handleMessageTap(RemoteMessage message) { final data = message.data.cast(); final item = NotificationItem.fromMap(data); _navigateToNotification(item); } @override Widget build(BuildContext context) { // listen inside build; safe with ConsumerState ref.listen>>(notificationsProvider, ( previous, next, ) { final prevList = _prevList; final nextList = next.maybeWhen( data: (d) => d, orElse: () => [], ); if (nextList.length > prevList.length) { final newItem = nextList.last; _showBanner(newItem.type, newItem); } _prevList = nextList; }); // Listen for pending navigation from notification taps ref.listen(pendingNotificationNavigationProvider, ( previous, next, ) { if (next != null) { final type = next.type; final id = next.id; if (type == 'ticket') { GoRouter.of(context).go('/tickets/$id'); } else if (type == 'task') { GoRouter.of(context).go('/tasks/$id'); } // Clear the pending navigation after handling ref.read(pendingNotificationNavigationProvider.notifier).state = null; } }); return widget.child; } }