diff --git a/lib/main.dart b/lib/main.dart index 36f5887f..d638d190 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,6 +11,7 @@ import 'firebase_options.dart'; // removed unused imports import 'app.dart'; import 'providers/notifications_provider.dart'; +import 'providers/notification_navigation_provider.dart'; import 'utils/app_time.dart'; import 'utils/notification_permission.dart'; import 'services/notification_service.dart'; @@ -21,6 +22,8 @@ import 'package:shared_preferences/shared_preferences.dart'; // audio player not used at top-level; instantiate where needed StreamSubscription? _fcmTokenRefreshSub; +late ProviderContainer _globalProviderContainer; + Map _formatNotificationFromData(Map data) { String actor = ''; if (data['actor_name'] != null) { @@ -186,6 +189,29 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // Create a unique ID for the notification display final int id = DateTime.now().millisecondsSinceEpoch ~/ 1000; + // Build payload string with ticket/task information for navigation + final payloadParts = []; + final taskId = + (message.data['task_id'] ?? + message.data['taskId'] ?? + message.data['task']) + ?.toString(); + final ticketId = + message.data['ticket_id'] ?? + message.data['ticketId'] ?? + message.data['ticket']?.toString() ?? + ''; + + if (taskId != null && taskId.isNotEmpty) { + payloadParts.add('task:$taskId'); + } + if (ticketId.isNotEmpty) { + payloadParts.add('ticket:$ticketId'); + } + final payload = payloadParts.join('|').isNotEmpty + ? payloadParts.join('|') + : message.data['type']?.toString() ?? ''; + // 3. Define the exact same channel specifics const androidDetails = AndroidNotificationDetails( 'tasq_custom_sound_channel_3', @@ -202,7 +228,7 @@ Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { title: title, body: body, notificationDetails: const NotificationDetails(android: androidDetails), - payload: message.data['type'], // Or whatever payload you need for routing + payload: payload, ); } @@ -395,11 +421,35 @@ Future main() async { if (!recent.containsKey(stableId)) { recent[stableId] = now; await prefs.setString('recent_notifs', jsonEncode(recent)); + + // Build payload string with ticket/task information for navigation + final payloadParts = []; + final taskId = + (message.data['task_id'] ?? + message.data['taskId'] ?? + message.data['task']) + ?.toString(); + final ticketId = + message.data['ticket_id'] ?? + message.data['ticketId'] ?? + message.data['ticket']?.toString() ?? + ''; + + if (taskId != null && taskId.isNotEmpty) { + payloadParts.add('task:$taskId'); + } + if (ticketId.isNotEmpty) { + payloadParts.add('ticket:$ticketId'); + } + final payload = payloadParts.join('|').isNotEmpty + ? payloadParts.join('|') + : message.data['payload']?.toString() ?? ''; + NotificationService.show( id: DateTime.now().millisecondsSinceEpoch ~/ 1000, title: formatted['title']!, body: formatted['body']!, - payload: message.data['payload'], + payload: payload, ); } } catch (e) { @@ -408,24 +458,11 @@ Future main() async { id: DateTime.now().millisecondsSinceEpoch ~/ 1000, title: formatted['title']!, body: formatted['body']!, - payload: message.data['payload'], + payload: '', ); } }); - // initialize the local notifications plugin so we can post alerts later - await NotificationService.initialize( - onDidReceiveNotificationResponse: (response) { - // handle user tapping a notification; the payload format is up to us - final payload = response.payload; - if (payload != null && payload.startsWith('ticket:')) { - // ignore if context not mounted; we might use a navigator key in real - // app, but keep this simple for now - // TODO: navigate to ticket/task as appropriate - } - }, - ); - // 1. Define the High Importance Channel (This MUST match your manifest exactly) const AndroidNotificationChannel channel = AndroidNotificationChannel( 'tasq_custom_sound_channel', // id @@ -448,6 +485,49 @@ Future main() async { // global navigator key used for snackbars/navigation from notification final navigatorKey = GlobalKey(); + // initialize the local notifications plugin so we can post alerts later + await NotificationService.initialize( + onDidReceiveNotificationResponse: (response) { + // handle user tapping a notification; the payload format is "ticket:ID", + // "task:ID", "tasknum:NUMBER", or a combination separated by "|" + final payload = response.payload; + if (payload != null && payload.isNotEmpty) { + // Parse the payload to extract ticket and task information + final parts = payload.split('|'); + String? ticketId; + String? taskId; + + for (final part in parts) { + if (part.startsWith('ticket:')) { + ticketId = part.substring('ticket:'.length); + } else if (part.startsWith('task:')) { + taskId = part.substring('task:'.length); + } + } + + // Update the pending navigation provider + if (ticketId != null && ticketId.isNotEmpty) { + _globalProviderContainer + .read(pendingNotificationNavigationProvider.notifier) + .state = ( + type: 'ticket', + id: ticketId, + ); + } else if (taskId != null && taskId.isNotEmpty) { + _globalProviderContainer + .read(pendingNotificationNavigationProvider.notifier) + .state = ( + type: 'task', + id: taskId, + ); + } + } + }, + ); + + // Create the global provider container + _globalProviderContainer = ProviderContainer(); + runApp( ProviderScope( observers: [NotificationSoundObserver()], diff --git a/lib/providers/notification_navigation_provider.dart b/lib/providers/notification_navigation_provider.dart new file mode 100644 index 00000000..7408f959 --- /dev/null +++ b/lib/providers/notification_navigation_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +typedef PendingNavigation = ({String type, String id})?; + +final pendingNotificationNavigationProvider = StateProvider( + (ref) => null, +); diff --git a/lib/services/notification_bridge.dart b/lib/services/notification_bridge.dart index 80a84f3d..1cfc7684 100644 --- a/lib/services/notification_bridge.dart +++ b/lib/services/notification_bridge.dart @@ -1,9 +1,11 @@ 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. @@ -112,6 +114,25 @@ class _NotificationBridgeState extends ConsumerState } _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; } }