fixed notification taps being ignored when they cold-started the app

This commit is contained in:
2026-09-27 14:38:48 +02:00
parent 2ca9feb49a
commit 1fd6264d10
7 changed files with 146 additions and 59 deletions
+27 -19
View File
@@ -1,24 +1,22 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'push_actions.dart';
import 'push_target.dart';
/// Routes foreground notification interactions from the single
/// flutter_local_notifications response callback. Action responses (reply /
/// mark-read) are dispatched straight to [PushActions]; a plain tap publishes
/// the target chat token via [pendingChatToken] for [App] to navigate to.
/// its target via [pendingTarget] for [App] to navigate to.
class PushTapRouter {
PushTapRouter._();
/// Chat token of the most recently tapped Talk notification, or null. [App]
/// listens to this and opens the chat, then resets it to null.
static final ValueNotifier<String?> pendingChatToken = ValueNotifier(null);
/// Newsletter id of the most recently tapped Marianum-Message notification,
/// or null. [App] listens to this and opens the message, then resets it.
static final ValueNotifier<String?> pendingNewsletterId = ValueNotifier(null);
/// Target of the most recently tapped notification, or null. [App] listens
/// to this and navigates, then resets it to null.
static final ValueNotifier<PushTarget?> pendingTarget = ValueNotifier(null);
static void handleResponse(NotificationResponse response) {
final actionId = response.actionId;
@@ -29,13 +27,28 @@ class PushTapRouter {
}
final map = _payloadMap(response.payload);
if (map == null) return;
final newsletterId = _stringValue(map, 'newsletterId');
if (newsletterId != null) {
pendingNewsletterId.value = newsletterId;
return;
final target = resolvePushTarget(map);
if (target != null) pendingTarget.value = target;
}
static bool _launchHandled = false;
/// Routes the tap that cold-started the app. The plugin reports such a tap
/// only through its launch details, never through the response callback;
/// the details stay set for the whole process, hence the one-shot guard.
static Future<void> handleAppLaunch(
FlutterLocalNotificationsPlugin plugin,
) async {
if (_launchHandled) return;
_launchHandled = true;
try {
final details = await plugin.getNotificationAppLaunchDetails();
final response = details?.notificationResponse;
if (details?.didNotificationLaunchApp != true || response == null) return;
handleResponse(response);
} on Object catch (e) {
log('Reading the notification launch details failed: $e');
}
final token = _stringValue(map, 'chatToken');
if (token != null) pendingChatToken.value = token;
}
static Map<String, dynamic>? _payloadMap(String? payload) {
@@ -46,9 +59,4 @@ class PushTapRouter {
return null;
}
}
static String? _stringValue(Map<String, dynamic> map, String key) {
final value = map[key];
return value is String && value.isNotEmpty ? value : null;
}
}
+32
View File
@@ -0,0 +1,32 @@
/// Where a tapped notification leads.
sealed class PushTarget {
const PushTarget();
}
class NewsletterTarget extends PushTarget {
final String newsletterId;
const NewsletterTarget(this.newsletterId);
}
class ChatTarget extends PushTarget {
final String chatToken;
const ChatTarget(this.chatToken);
}
/// Resolves the data of a tapped notification — the payload of a locally
/// rendered one as well as the data of an FCM message. Null when it names no
/// known target.
PushTarget? resolvePushTarget(Map<String, dynamic> data) {
String? value(String key) {
final value = data[key];
return value is String && value.isNotEmpty ? value : null;
}
if (value('newsletterId') case final newsletterId?) {
return NewsletterTarget(newsletterId);
}
for (final key in const ['chatToken', 'token', 'roomToken']) {
if (value(key) case final chatToken?) return ChatTarget(chatToken);
}
return null;
}