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/notification_navigation_provider.dart'; import '../routing/app_router.dart'; /// Wraps the app and installs both a Supabase realtime listener and the /// FCM handlers described in the frontend design. /// /// Navigation is performed via the [GoRouter] instance obtained from /// [appRouterProvider], so this widget does **not** require an external /// navigator key. class NotificationBridge extends ConsumerStatefulWidget { const NotificationBridge({required this.child, super.key}); 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. } } /// Navigate to the task or ticket detail screen using GoRouter. void _goToDetail({String? taskId, String? ticketId}) { final router = ref.read(appRouterProvider); if (taskId != null && taskId.isNotEmpty) { router.go('/tasks/$taskId'); } else if (ticketId != null && ticketId.isNotEmpty) { router.go('/tickets/$ticketId'); } } void _showBanner(String type, NotificationItem item) { // Use a post-frame callback so that the ScaffoldMessenger from // MaterialApp is available in the element tree. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; try { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('New $type received!'), action: SnackBarAction( label: 'View', onPressed: () => _goToDetail(taskId: item.taskId, ticketId: item.ticketId), ), ), ); } catch (_) { // ScaffoldMessenger not available yet (before first frame) — ignore. } }); } 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); }); } /// Extract task/ticket IDs from the FCM data payload and navigate. /// /// The FCM data map contains keys such as `task_id` / `taskId` / /// `ticket_id` / `ticketId` — these are the same keys used in the /// background handler ([_firebaseMessagingBackgroundHandler]). void _handleMessageTap(RemoteMessage message) { final data = message.data; final String? taskId = (data['task_id'] ?? data['taskId'] ?? data['task']) ?.toString(); final String? ticketId = (data['ticket_id'] ?? data['ticketId'] ?? data['ticket'])?.toString(); _goToDetail(taskId: taskId, ticketId: ticketId); } @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 local-notification taps ref.listen(pendingNotificationNavigationProvider, ( previous, next, ) { if (next != null) { _goToDetail( taskId: next.type == 'task' ? next.id : null, ticketId: next.type == 'ticket' ? next.id : null, ); // Clear the pending navigation after handling ref.read(pendingNotificationNavigationProvider.notifier).state = null; } }); return widget.child; } }