Initial implementation of navigation from notification(not yet working)
This commit is contained in:
parent
ed078f24ec
commit
294d3f7470
112
lib/main.dart
112
lib/main.dart
|
|
@ -11,6 +11,7 @@ import 'firebase_options.dart';
|
||||||
// removed unused imports
|
// removed unused imports
|
||||||
import 'app.dart';
|
import 'app.dart';
|
||||||
import 'providers/notifications_provider.dart';
|
import 'providers/notifications_provider.dart';
|
||||||
|
import 'providers/notification_navigation_provider.dart';
|
||||||
import 'utils/app_time.dart';
|
import 'utils/app_time.dart';
|
||||||
import 'utils/notification_permission.dart';
|
import 'utils/notification_permission.dart';
|
||||||
import 'services/notification_service.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
|
// audio player not used at top-level; instantiate where needed
|
||||||
StreamSubscription<String?>? _fcmTokenRefreshSub;
|
StreamSubscription<String?>? _fcmTokenRefreshSub;
|
||||||
|
late ProviderContainer _globalProviderContainer;
|
||||||
|
|
||||||
Map<String, String> _formatNotificationFromData(Map<String, dynamic> data) {
|
Map<String, String> _formatNotificationFromData(Map<String, dynamic> data) {
|
||||||
String actor = '';
|
String actor = '';
|
||||||
if (data['actor_name'] != null) {
|
if (data['actor_name'] != null) {
|
||||||
|
|
@ -186,6 +189,29 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
// Create a unique ID for the notification display
|
// Create a unique ID for the notification display
|
||||||
final int id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final int id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
|
// Build payload string with ticket/task information for navigation
|
||||||
|
final payloadParts = <String>[];
|
||||||
|
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
|
// 3. Define the exact same channel specifics
|
||||||
const androidDetails = AndroidNotificationDetails(
|
const androidDetails = AndroidNotificationDetails(
|
||||||
'tasq_custom_sound_channel_3',
|
'tasq_custom_sound_channel_3',
|
||||||
|
|
@ -202,7 +228,7 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
title: title,
|
title: title,
|
||||||
body: body,
|
body: body,
|
||||||
notificationDetails: const NotificationDetails(android: androidDetails),
|
notificationDetails: const NotificationDetails(android: androidDetails),
|
||||||
payload: message.data['type'], // Or whatever payload you need for routing
|
payload: payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,11 +421,35 @@ Future<void> main() async {
|
||||||
if (!recent.containsKey(stableId)) {
|
if (!recent.containsKey(stableId)) {
|
||||||
recent[stableId] = now;
|
recent[stableId] = now;
|
||||||
await prefs.setString('recent_notifs', jsonEncode(recent));
|
await prefs.setString('recent_notifs', jsonEncode(recent));
|
||||||
|
|
||||||
|
// Build payload string with ticket/task information for navigation
|
||||||
|
final payloadParts = <String>[];
|
||||||
|
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(
|
NotificationService.show(
|
||||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
title: formatted['title']!,
|
title: formatted['title']!,
|
||||||
body: formatted['body']!,
|
body: formatted['body']!,
|
||||||
payload: message.data['payload'],
|
payload: payload,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -408,24 +458,11 @@ Future<void> main() async {
|
||||||
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
title: formatted['title']!,
|
title: formatted['title']!,
|
||||||
body: formatted['body']!,
|
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)
|
// 1. Define the High Importance Channel (This MUST match your manifest exactly)
|
||||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||||
'tasq_custom_sound_channel', // id
|
'tasq_custom_sound_channel', // id
|
||||||
|
|
@ -448,6 +485,49 @@ Future<void> main() async {
|
||||||
// global navigator key used for snackbars/navigation from notification
|
// global navigator key used for snackbars/navigation from notification
|
||||||
final navigatorKey = GlobalKey<NavigatorState>();
|
final navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
// 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(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
observers: [NotificationSoundObserver()],
|
observers: [NotificationSoundObserver()],
|
||||||
|
|
|
||||||
7
lib/providers/notification_navigation_provider.dart
Normal file
7
lib/providers/notification_navigation_provider.dart
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
typedef PendingNavigation = ({String type, String id})?;
|
||||||
|
|
||||||
|
final pendingNotificationNavigationProvider = StateProvider<PendingNavigation>(
|
||||||
|
(ref) => null,
|
||||||
|
);
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../models/notification_item.dart';
|
import '../models/notification_item.dart';
|
||||||
import '../providers/notifications_provider.dart';
|
import '../providers/notifications_provider.dart';
|
||||||
|
import '../providers/notification_navigation_provider.dart';
|
||||||
|
|
||||||
/// Wraps the app and installs both a Supabase realtime listener and the
|
/// Wraps the app and installs both a Supabase realtime listener and the
|
||||||
/// FCM handlers described in the frontend design.
|
/// FCM handlers described in the frontend design.
|
||||||
|
|
@ -112,6 +114,25 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||||
}
|
}
|
||||||
_prevList = nextList;
|
_prevList = nextList;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for pending navigation from notification taps
|
||||||
|
ref.listen<PendingNavigation?>(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;
|
return widget.child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user