wire background-tap → route via cross-isolate payload stash

This commit is contained in:
Mohamad Mahdi Jebeli 2026-06-21 13:40:52 +03:30
parent 339499501f
commit 527986fdb1
3 changed files with 83 additions and 0 deletions

View File

@ -238,6 +238,9 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
if (HomeWidgetRepository.data != null) { if (HomeWidgetRepository.data != null) {
await HomeWidgetRepository.decideWhereToGoNotif(); await HomeWidgetRepository.decideWhereToGoNotif();
} }
// Drain anything the background-isolate notification-tap handler
// stashed while the app was killed/backgrounded.
await NotificationService.consumePendingNotificationTap();
} }
} }
} }

View File

@ -9,9 +9,33 @@ import 'package:didvan/services/storage/storage.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:home_widget/home_widget.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
/// Storage key shared between the background notification-tap handler
/// (running in a separate isolate) and the main isolate, which reads the
/// payload back on resume and performs the actual navigation.
const String pendingNotificationPayloadKey = 'pending_notif_payload';
/// Top-level handler invoked by flutter_local_notifications when the user
/// taps a notification while the app is in the background or killed. Runs
/// in its own isolate, so it cannot touch the Navigator directly. Instead
/// it stashes the payload via HomeWidget storage (cross-isolate) so the
/// main isolate can pick it up and route on resume.
@pragma('vm:entry-point')
void backgroundNotificationTapHandler(NotificationResponse response) {
final payload = response.payload;
if (payload == null || payload.isEmpty) return;
try {
HomeWidget.saveWidgetData(pendingNotificationPayloadKey, payload);
} catch (e) {
if (kDebugMode) {
print('backgroundNotificationTapHandler: failed to stash payload: $e');
}
}
}
class NotificationService { class NotificationService {
static final FlutterLocalNotificationsPlugin static final FlutterLocalNotificationsPlugin
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
@ -26,6 +50,10 @@ class NotificationService {
await _flutterLocalNotificationsPlugin.initialize( await _flutterLocalNotificationsPlugin.initialize(
initializationSettings, initializationSettings,
onDidReceiveNotificationResponse: _onNotificationResponse, onDidReceiveNotificationResponse: _onNotificationResponse,
// Background tap callback runs in a separate isolate when the user
// taps a notification while the app is in the background or killed.
// It stashes the payload; the main isolate consumes it on resume.
onDidReceiveBackgroundNotificationResponse: backgroundNotificationTapHandler,
); );
if (!kIsWeb) { if (!kIsWeb) {
@ -44,6 +72,51 @@ class NotificationService {
} }
} }
/// Read a pending notification payload (if any) from cross-isolate
/// storage and hand it off to [HomeWidgetRepository.data] without
/// navigating. Use this during splash where you want the existing splash
/// routing logic to make the navigation call.
static Future<void> loadPendingNotificationPayload() async {
try {
final stashed = await HomeWidget.getWidgetData<String>(
pendingNotificationPayloadKey,
defaultValue: '',
);
if (stashed == null || stashed.isEmpty) return;
await HomeWidget.saveWidgetData(pendingNotificationPayloadKey, '');
HomeWidgetRepository.data =
NotificationMessage.fromJson(jsonDecode(stashed));
} catch (e) {
if (kDebugMode) {
print('loadPendingNotificationPayload failed: $e');
}
}
}
/// Pull a pending notification payload (stored by the background tap
/// handler) out of cross-isolate storage and route to the right screen.
/// Called from the main isolate on app resume / startup.
static Future<void> consumePendingNotificationTap() async {
try {
final stashed = await HomeWidget.getWidgetData<String>(
pendingNotificationPayloadKey,
defaultValue: '',
);
if (stashed == null || stashed.isEmpty) return;
// Clear immediately so we don't re-route on every resume.
await HomeWidget.saveWidgetData(pendingNotificationPayloadKey, '');
final data = NotificationMessage.fromJson(jsonDecode(stashed));
HomeWidgetRepository.data = data;
await HomeWidgetRepository.decideWhereToGoNotif();
} catch (e) {
if (kDebugMode) {
print('consumePendingNotificationTap failed: $e');
}
}
}
static Future<void> _onNotificationResponse( static Future<void> _onNotificationResponse(
NotificationResponse response) async { NotificationResponse response) async {
if (kDebugMode) { if (kDebugMode) {

View File

@ -15,6 +15,7 @@ import 'package:didvan/services/auth/token_storage.dart';
import 'package:didvan/services/migration/app_migration.dart'; import 'package:didvan/services/migration/app_migration.dart';
import 'package:didvan/services/network/request.dart'; import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart'; import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/notification/notification_service.dart';
import 'package:didvan/services/storage/storage.dart'; import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -337,6 +338,12 @@ class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
void _navigateToNextScreen(String? token) async { void _navigateToNextScreen(String? token) async {
if (!mounted) return; if (!mounted) return;
// If the user tapped a local notification while the app was killed,
// the payload was stashed in cross-isolate storage by
// backgroundNotificationTapHandler. Pull it into HomeWidgetRepository
// so the routing block below picks it up.
await NotificationService.loadPendingNotificationPayload();
String extractedPath = initialURI?.path == '/' String extractedPath = initialURI?.path == '/'
? Routes.home ? Routes.home
: (initialURI?.path ?? Routes.home); : (initialURI?.path ?? Routes.home);