From 527986fdb1de811ce8b72c46e44843432a00d3f2 Mon Sep 17 00:00:00 2001 From: "Mr.Jebelli" Date: Sun, 21 Jun 2026 13:40:52 +0330 Subject: [PATCH] =?UTF-8?q?wire=20background-tap=20=E2=86=92=20route=20via?= =?UTF-8?q?=20cross-isolate=20payload=20stash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main.dart | 3 + .../notification/notification_service.dart | 73 +++++++++++++++++++ lib/views/splash/splash.dart | 7 ++ 3 files changed, 83 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 262172f..5598ec0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -238,6 +238,9 @@ class _DidvanState extends State with WidgetsBindingObserver { if (HomeWidgetRepository.data != null) { await HomeWidgetRepository.decideWhereToGoNotif(); } + // Drain anything the background-isolate notification-tap handler + // stashed while the app was killed/backgrounded. + await NotificationService.consumePendingNotificationTap(); } } } diff --git a/lib/services/notification/notification_service.dart b/lib/services/notification/notification_service.dart index b4f5a41..4f299fb 100644 --- a/lib/services/notification/notification_service.dart +++ b/lib/services/notification/notification_service.dart @@ -9,9 +9,33 @@ import 'package:didvan/services/storage/storage.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; +import 'package:home_widget/home_widget.dart'; import 'package:http/http.dart' as http; 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 { static final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -26,6 +50,10 @@ class NotificationService { await _flutterLocalNotificationsPlugin.initialize( initializationSettings, 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) { @@ -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 loadPendingNotificationPayload() async { + try { + final stashed = await HomeWidget.getWidgetData( + 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 consumePendingNotificationTap() async { + try { + final stashed = await HomeWidget.getWidgetData( + 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 _onNotificationResponse( NotificationResponse response) async { if (kDebugMode) { diff --git a/lib/views/splash/splash.dart b/lib/views/splash/splash.dart index ced0137..32dab6b 100644 --- a/lib/views/splash/splash.dart +++ b/lib/views/splash/splash.dart @@ -15,6 +15,7 @@ import 'package:didvan/services/auth/token_storage.dart'; import 'package:didvan/services/migration/app_migration.dart'; import 'package:didvan/services/network/request.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:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -337,6 +338,12 @@ class _SplashState extends State with SingleTickerProviderStateMixin { void _navigateToNextScreen(String? token) async { 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 == '/' ? Routes.home : (initialURI?.path ?? Routes.home);