Push notification tap redirect to corressponding ticket or task
This commit is contained in:
parent
eb49329b16
commit
5713581992
|
|
@ -482,8 +482,13 @@ Future<void> main() async {
|
||||||
>()
|
>()
|
||||||
?.createNotificationChannel(channel);
|
?.createNotificationChannel(channel);
|
||||||
|
|
||||||
// global navigator key used for snackbars/navigation from notification
|
// Create the global provider container BEFORE initializing local
|
||||||
final navigatorKey = GlobalKey<NavigatorState>();
|
// notifications, because the tap callback needs to write to it and
|
||||||
|
// flutter_local_notifications may fire the callback synchronously if
|
||||||
|
// the app was launched by tapping a notification.
|
||||||
|
_globalProviderContainer = ProviderContainer(
|
||||||
|
observers: [NotificationSoundObserver()],
|
||||||
|
);
|
||||||
|
|
||||||
// initialize the local notifications plugin so we can post alerts later
|
// initialize the local notifications plugin so we can post alerts later
|
||||||
await NotificationService.initialize(
|
await NotificationService.initialize(
|
||||||
|
|
@ -505,36 +510,32 @@ Future<void> main() async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the pending navigation provider
|
// Update the pending navigation provider.
|
||||||
if (ticketId != null && ticketId.isNotEmpty) {
|
// Prefer task over ticket — assignment notifications include both
|
||||||
_globalProviderContainer
|
// IDs but the primary entity is the task.
|
||||||
.read(pendingNotificationNavigationProvider.notifier)
|
if (taskId != null && taskId.isNotEmpty) {
|
||||||
.state = (
|
|
||||||
type: 'ticket',
|
|
||||||
id: ticketId,
|
|
||||||
);
|
|
||||||
} else if (taskId != null && taskId.isNotEmpty) {
|
|
||||||
_globalProviderContainer
|
_globalProviderContainer
|
||||||
.read(pendingNotificationNavigationProvider.notifier)
|
.read(pendingNotificationNavigationProvider.notifier)
|
||||||
.state = (
|
.state = (
|
||||||
type: 'task',
|
type: 'task',
|
||||||
id: taskId,
|
id: taskId,
|
||||||
);
|
);
|
||||||
|
} else if (ticketId != null && ticketId.isNotEmpty) {
|
||||||
|
_globalProviderContainer
|
||||||
|
.read(pendingNotificationNavigationProvider.notifier)
|
||||||
|
.state = (
|
||||||
|
type: 'ticket',
|
||||||
|
id: ticketId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the global provider container
|
|
||||||
_globalProviderContainer = ProviderContainer();
|
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
UncontrolledProviderScope(
|
||||||
observers: [NotificationSoundObserver()],
|
container: _globalProviderContainer,
|
||||||
child: NotificationBridge(
|
child: const NotificationBridge(child: TasqApp()),
|
||||||
navigatorKey: navigatorKey,
|
|
||||||
child: const TasqApp(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -984,6 +984,7 @@ class TasksController {
|
||||||
|
|
||||||
final dataPayload = <String, dynamic>{
|
final dataPayload = <String, dynamic>{
|
||||||
'type': 'created',
|
'type': 'created',
|
||||||
|
'task_id': taskId,
|
||||||
'task_number': ?taskNumber,
|
'task_number': ?taskNumber,
|
||||||
'office_id': ?officeId,
|
'office_id': ?officeId,
|
||||||
'office_name': ?officeName,
|
'office_name': ?officeName,
|
||||||
|
|
@ -1548,6 +1549,7 @@ class TasksController {
|
||||||
|
|
||||||
final dataPayload = <String, dynamic>{
|
final dataPayload = <String, dynamic>{
|
||||||
'type': 'assignment',
|
'type': 'assignment',
|
||||||
|
'task_id': taskId,
|
||||||
'task_number': ?taskNumber,
|
'task_number': ?taskNumber,
|
||||||
'office_id': ?officeId,
|
'office_id': ?officeId,
|
||||||
'office_name': ?officeName,
|
'office_name': ?officeName,
|
||||||
|
|
@ -1781,6 +1783,7 @@ class TaskAssignmentsController {
|
||||||
|
|
||||||
final dataPayload = <String, dynamic>{
|
final dataPayload = <String, dynamic>{
|
||||||
'type': 'assignment',
|
'type': 'assignment',
|
||||||
|
'task_id': taskId,
|
||||||
'task_number': ?taskNumber,
|
'task_number': ?taskNumber,
|
||||||
'ticket_id': ?ticketId,
|
'ticket_id': ?ticketId,
|
||||||
'office_id': ?officeId,
|
'office_id': ?officeId,
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,21 @@
|
||||||
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';
|
import '../providers/notification_navigation_provider.dart';
|
||||||
|
import '../routing/app_router.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.
|
||||||
///
|
///
|
||||||
/// The navigator key is required so that snackbars and navigation can be
|
/// Navigation is performed via the [GoRouter] instance obtained from
|
||||||
/// triggered from outside the widget tree (e.g. from realtime callbacks).
|
/// [appRouterProvider], so this widget does **not** require an external
|
||||||
|
/// navigator key.
|
||||||
class NotificationBridge extends ConsumerStatefulWidget {
|
class NotificationBridge extends ConsumerStatefulWidget {
|
||||||
const NotificationBridge({
|
const NotificationBridge({required this.child, super.key});
|
||||||
required this.navigatorKey,
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> navigatorKey;
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -55,26 +51,36 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showBanner(String type, NotificationItem item) {
|
/// Navigate to the task or ticket detail screen using GoRouter.
|
||||||
final ctx = widget.navigatorKey.currentState?.overlay?.context;
|
void _goToDetail({String? taskId, String? ticketId}) {
|
||||||
if (ctx == null) return;
|
final router = ref.read(appRouterProvider);
|
||||||
|
if (taskId != null && taskId.isNotEmpty) {
|
||||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
router.go('/tasks/$taskId');
|
||||||
SnackBar(
|
} else if (ticketId != null && ticketId.isNotEmpty) {
|
||||||
content: Text('New $type received!'),
|
router.go('/tickets/$ticketId');
|
||||||
action: SnackBarAction(
|
}
|
||||||
label: 'View',
|
|
||||||
onPressed: () => _navigateToNotification(item),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToNotification(NotificationItem item) {
|
void _showBanner(String type, NotificationItem item) {
|
||||||
widget.navigatorKey.currentState?.pushNamed(
|
// Use a post-frame callback so that the ScaffoldMessenger from
|
||||||
'/notification-detail',
|
// MaterialApp is available in the element tree.
|
||||||
arguments: item,
|
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() {
|
void _setupFcmHandlers() {
|
||||||
|
|
@ -90,10 +96,20 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
void _handleMessageTap(RemoteMessage message) {
|
||||||
final data = message.data.cast<String, dynamic>();
|
final data = message.data;
|
||||||
final item = NotificationItem.fromMap(data);
|
|
||||||
_navigateToNotification(item);
|
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
|
@override
|
||||||
|
|
@ -115,19 +131,16 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||||
_prevList = nextList;
|
_prevList = nextList;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for pending navigation from notification taps
|
// Listen for pending navigation from local-notification taps
|
||||||
ref.listen<PendingNavigation?>(pendingNotificationNavigationProvider, (
|
ref.listen<PendingNavigation?>(pendingNotificationNavigationProvider, (
|
||||||
previous,
|
previous,
|
||||||
next,
|
next,
|
||||||
) {
|
) {
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
final type = next.type;
|
_goToDetail(
|
||||||
final id = next.id;
|
taskId: next.type == 'task' ? next.id : null,
|
||||||
if (type == 'ticket') {
|
ticketId: next.type == 'ticket' ? next.id : null,
|
||||||
GoRouter.of(context).go('/tickets/$id');
|
);
|
||||||
} else if (type == 'task') {
|
|
||||||
GoRouter.of(context).go('/tasks/$id');
|
|
||||||
}
|
|
||||||
// Clear the pending navigation after handling
|
// Clear the pending navigation after handling
|
||||||
ref.read(pendingNotificationNavigationProvider.notifier).state = null;
|
ref.read(pendingNotificationNavigationProvider.notifier).state = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user