import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import '../models/notification_item.dart'; import '../providers/notifications_provider.dart'; import '../providers/realtime_controller.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); if (state == AppLifecycleState.resumed) { try { // Trigger a best-effort realtime reconnection when the app resumes. ref.read(realtimeControllerProvider).recoverConnection(); } catch (_) {} } } 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; }); return widget.child; } }