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 appropriate screen using GoRouter. /// /// Supports generic [route] for new notification types, plus legacy /// [taskId], [ticketId], and [itServiceRequestId] fallbacks. void _goToDetail({ String? taskId, String? ticketId, String? itServiceRequestId, String? route, }) { final router = ref.read(appRouterProvider); if (route != null && route.isNotEmpty) { router.go(route); } else if (taskId != null && taskId.isNotEmpty) { router.go('/tasks/$taskId'); } else if (ticketId != null && ticketId.isNotEmpty) { router.go('/tickets/$ticketId'); } else if (itServiceRequestId != null && itServiceRequestId.isNotEmpty) { router.go('/it-service-requests/$itServiceRequestId'); } } 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 navigation target from FCM data payload and navigate. /// /// Prefers the `navigate_to` field (used by scheduled notification /// reminders) and falls back to legacy `task_id` / `ticket_id` keys. void _handleMessageTap(RemoteMessage message) { final data = message.data; // New: use navigate_to route if present (scheduled reminder notifications) final String? navigateTo = data['navigate_to']?.toString(); if (navigateTo != null && navigateTo.isNotEmpty) { _goToDetail(route: navigateTo); return; } // Legacy fallback for task_id / ticket_id final String? taskId = (data['task_id'] ?? data['taskId'] ?? data['task'])?.toString(); final String? ticketId = (data['ticket_id'] ?? data['ticketId'] ?? data['ticket'])?.toString(); final String? isrId = (data['it_service_request_id'] ?? data['itServiceRequestId']) ?.toString(); _goToDetail( taskId: taskId, ticketId: ticketId, itServiceRequestId: isrId, ); } @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( route: next.route, taskId: next.type == 'task' ? next.id : null, ticketId: next.type == 'ticket' ? next.id : null, itServiceRequestId: next.type == 'it_service_request' ? next.id : null, ); // Clear the pending navigation after handling ref.read(pendingNotificationNavigationProvider.notifier).state = null; } }); return widget.child; } }