diff --git a/android/gradle.properties b/android/gradle.properties index 9fb38428..d18a7fb5 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,6 +3,9 @@ android.useAndroidX=true android.enableJetifier=true dev.steenbakker.mobile_scanner.useUnbundled=true +# Java 21 for Android Gradle Plugin +org.gradle.java.home=/Users/devsikander/Library/Java/JavaVirtualMachines/corretto-21.0.8/Contents/Home + # Gradle optimization and caching org.gradle.caching=true org.gradle.parallel=true diff --git a/lib/controllers/notification/firebase_notification_manger.dart b/lib/controllers/notification/firebase_notification_manger.dart index 9866be33..f750c3bb 100644 --- a/lib/controllers/notification/firebase_notification_manger.dart +++ b/lib/controllers/notification/firebase_notification_manger.dart @@ -1,9 +1,10 @@ import 'dart:convert'; -import 'dart:developer'; import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:google_api_availability/google_api_availability.dart'; import 'package:huawei_push/huawei_push.dart' as h_push; import 'package:test_sa/controllers/notification/notification_manger.dart'; @@ -19,13 +20,108 @@ import 'package:test_sa/modules/tm_module/gas_refill/gas_refill_details.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; +/// Background notification handler - runs in separate isolate +/// Cannot access BuildContext or UI, must use flutter_local_notifications directly @pragma('vm:entry-point') -Future firebaseMessagingBackgroundHandler(RemoteMessage message) async {} +Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { + if (kDebugMode) { + print('Background message received: ${message.messageId}'); + print('Has notification field: ${message.notification != null}'); + print('Data: ${message.data}'); + } + + // Skip if it's a notification that should not be shown + if (message.data["notificationType"] == 'NurseConfirmArrive') { + return; + } + + // For Android: Show notification manually + // Even if android.notification config exists in payload, if message.notification is null, + // we need to show it using data from the data field + if (Platform.isAndroid) { + await _showBackgroundNotification(message); + } + // For iOS, system handles background notifications automatically + // if the payload includes the 'notification' field +} + +/// Show background notification with grouping support +/// This runs in background isolate, so we use flutter_local_notifications directly +Future _showBackgroundNotification(RemoteMessage message) async { + final FlutterLocalNotificationsPlugin notificationsPlugin = FlutterLocalNotificationsPlugin(); + + // Initialize plugin in background isolate + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@drawable/ic_stat_name'); + const InitializationSettings initializationSettings = + InitializationSettings(android: initializationSettingsAndroid); + + await notificationsPlugin.initialize(initializationSettings); + + // Create notification channel in background handler + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + notificationsPlugin.resolvePlatformSpecificImplementation(); + + if (androidImplementation != null) { + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'com.hmg.atoms', + 'ATOMS', + description: 'Push notification service for ATOMS', + importance: Importance.max, + playSound: true, + enableVibration: true, + showBadge: true, + ); + + await androidImplementation.createNotificationChannel(channel); + } + + // Extract data from message + final String title = message.notification?.title ?? message.data['title']?.toString() ?? 'ATOMS'; + final String body = message.notification?.body ?? message.data['body']?.toString() ?? ''; + final String? groupId = message.data['group-id']?.toString(); + final int notificationId = DateTime.now().millisecondsSinceEpoch % 100000; + + if (kDebugMode) { + print('Background notification - Title: $title, Body: $body, GroupId: $groupId'); + print('message.data[title]: ${message.data['title']}'); + print('message.data[body]: ${message.data['body']}'); + print('message.data[group-id]: ${message.data['group-id']}'); + } + + // Create notification details with grouping if group-id is provided + final AndroidNotificationDetails androidDetails = AndroidNotificationDetails( + 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: '@drawable/ic_stat_name', + playSound: true, + enableVibration: true, + groupKey: groupId, // Use group-id to separate different notification groups + setAsGroupSummary: false, // Individual notification, not summary + ); + + final NotificationDetails notificationDetails = NotificationDetails(android: androidDetails); + + await notificationsPlugin.show( + notificationId, + title, + body, + notificationDetails, + payload: json.encode(message.data), + ); +} + class FirebaseNotificationManger { static FirebaseMessaging messaging = FirebaseMessaging.instance; static String? token; + // Store initial notification payload when app opened from killed/background state + static Map? _pendingNotificationPayload; + static Future getToken() async { NotificationSettings settings = await messaging.requestPermission(alert: true, announcement: false, badge: true, carPlay: false, criticalAlert: false, provisional: false, sound: true); @@ -33,7 +129,7 @@ class FirebaseNotificationManger { try { if (!(await isGoogleServicesAvailable())) { h_push.Push.enableLogger(); - final result = await h_push.Push.setAutoInitEnabled(true); + await h_push.Push.setAutoInitEnabled(true); h_push.Push.onMessageReceivedStream.listen(_onMessageReceived, onError: _onMessageReceiveError); h_push.Push.getTokenStream.listen((hToken) { @@ -65,7 +161,27 @@ class FirebaseNotificationManger { } static void _onMessageReceived(h_push.RemoteMessage remoteMessage) { - print("onMessageReceivedStream:${remoteMessage.toMap()}"); + if (kDebugMode) { + print("onMessageReceivedStream:${remoteMessage.toMap()}"); + } + + // Handle Huawei push notification display with grouping support + // Extract notification data and group-id from the message + try { + final messageMap = remoteMessage.toMap(); + final dataPayload = messageMap['dataPayload'] as Map?; + + if (dataPayload != null && dataPayload["notificationType"] != 'NurseConfirmArrive') { + String? groupId = dataPayload["group-id"]?.toString(); + + // You may need to show notification here for Huawei devices + // depending on your Huawei Push implementation + } + } catch (e) { + if (kDebugMode) { + print("Error processing Huawei message: $e"); + } + } } static void _onMessageReceiveError(Object error) { @@ -199,10 +315,10 @@ class FirebaseNotificationManger { static initialized(BuildContext context) async { //TOD0 add platform check here also if (!(await isGoogleServicesAvailable()) && Platform.isAndroid) { - var initialNotification = await h_push.Push.getInitialNotification(); - if (initialNotification != null) { - Map remoteData = Map.from(initialNotification["extras"] as Map); - handleMessage(context, remoteData); + // Huawei device - initial notification already captured in main.dart + // Only set up the listener for when app is opened from background + if (kDebugMode) { + print('[FCM] Huawei device - setting up onNotificationOpenedApp listener'); } h_push.Push.onNotificationOpenedApp.listen((message) { @@ -243,17 +359,44 @@ class FirebaseNotificationManger { await FirebaseMessaging.instance.setAutoInitEnabled(true); await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(alert: true, badge: true, sound: true); - FirebaseMessaging.instance.getInitialMessage().then((initialMessage) { - if (initialMessage != null) { - handleMessage(context, initialMessage.data); - } - }); + // Note: getInitialMessage() is now called in main.dart BEFORE runApp() + // This ensures we capture the notification that launched the app + if (kDebugMode) { + print('[FCM] initialized() - listeners set up. Initial message already captured in main.dart'); + } FirebaseMessaging.onMessage.listen((RemoteMessage message) { if (Platform.isAndroid) { if (message.data["notificationType"] != 'NurseConfirmArrive') { + // Extract group-id from data payload + String? groupId = message.data["group-id"]?.toString(); + NotificationManger.showNotification( - title: message.notification?.title ?? "", subtext: message.notification?.body ?? "", hashcode: int.tryParse("1234" ?? "") ?? 1, payload: json.encode(message.data), context: context); + title: message.notification?.title ?? message.data["title"] ?? "", + subtext: message.notification?.body ?? message.data["body"] ?? "", + hashcode: DateTime.now().millisecondsSinceEpoch % 100000, // Generate unique ID + payload: json.encode(message.data), + context: context, + groupId: groupId, // Pass group-id for notification grouping + ); + } + } else if (Platform.isIOS) { + // For iOS data-only notifications, manually show notification with thread-id + if (message.data["notificationType"] != 'NurseConfirmArrive') { + String? groupId = message.data["group-id"]?.toString(); + + // Only show notification manually if it's a data-only notification + // iOS system handles notification display for notifications with 'notification' payload + if (message.notification == null && message.data.isNotEmpty) { + NotificationManger.showNotification( + title: message.data["title"]?.toString() ?? "ATOMS", + subtext: message.data["body"]?.toString() ?? "", + hashcode: message.data.hashCode, + payload: json.encode(message.data), + context: context, + groupId: groupId, // Will be used as threadIdentifier on iOS + ); + } } } return; @@ -261,6 +404,24 @@ class FirebaseNotificationManger { FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { handleMessage(context, message.data); }); - FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); + // Background message handler is registered in main.dart before runApp() + } + + /// Check if there's a pending notification payload from app launch + /// Returns the payload and clears it + static Map? getPendingNotificationPayload() { + final payload = _pendingNotificationPayload; + _pendingNotificationPayload = null; // Clear after retrieving + return payload; + } + + /// Set the initial notification payload (called from main.dart) + static void setInitialNotification(Map payload) { + _pendingNotificationPayload = payload; + } + + /// Check if there's a pending notification without consuming it + static bool hasPendingNotification() { + return _pendingNotificationPayload != null; } } diff --git a/lib/controllers/notification/notification_manger.dart b/lib/controllers/notification/notification_manger.dart index ab1df81a..4cb2b3f0 100644 --- a/lib/controllers/notification/notification_manger.dart +++ b/lib/controllers/notification/notification_manger.dart @@ -11,6 +11,13 @@ class NotificationManger { NotificationManger._(); static late FlutterLocalNotificationsPlugin localNotificationsPlugin; + + // Track notification groups for Android grouping + static final Map>> _notificationGroups = {}; + static final Map _groupSummaryIds = {}; + + // Store initial notification details when app launched by tapping notification + static NotificationResponse? _initialNotificationResponse; /// initialisation setting for all platform /// onNotificationPressed action when notification pressed to open tap @@ -30,6 +37,20 @@ class NotificationManger { await localNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { final AndroidFlutterLocalNotificationsPlugin? androidImplementation = localNotificationsPlugin.resolvePlatformSpecificImplementation(); + + // Create notification channel explicitly for Android + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'com.hmg.atoms', // id + 'ATOMS', // title + description: 'Push notification service for ATOMS', + importance: Importance.max, + playSound: true, + enableVibration: true, + showBadge: true, + ); + + await androidImplementation?.createNotificationChannel(channel); + final bool granted = await androidImplementation?.requestNotificationsPermission() ?? false; if (!granted) { if (kDebugMode) { @@ -40,9 +61,31 @@ class NotificationManger { } } + // CRITICAL: Get the notification that launched the app (if any) + // This must be done BEFORE setting up the callback + final notificationAppLaunchDetails = await localNotificationsPlugin.getNotificationAppLaunchDetails(); + if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + _initialNotificationResponse = notificationAppLaunchDetails!.notificationResponse; + if (kDebugMode) { + print('[NOTIFICATION_MANAGER] App launched from notification tap!'); + print('[NOTIFICATION_MANAGER] Initial notification payload: ${_initialNotificationResponse?.payload}'); + } + } + await localNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: onNotificationPressed); } // push new notification + /// Get the initial notification response if app was launched by tapping a notification + /// Returns null if app was opened normally + static NotificationResponse? getInitialNotificationResponse() { + final response = _initialNotificationResponse; + _initialNotificationResponse = null; // Clear after retrieving + if (kDebugMode) { + print('[NOTIFICATION_MANAGER] getInitialNotificationResponse() returning: ${response?.payload}'); + } + return response; + } + static const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( 'com.hmg.atoms', 'ATOMS', @@ -61,28 +104,211 @@ class NotificationManger { categoryIdentifier: "atoms", ); - static Future showNotification({required BuildContext context, required String title, required String subtext, required int hashcode, String? payload}) async { - // const AndroidNotificationDetails androidChannel = AndroidNotificationDetails( - // 'com.hmg.atoms', - // 'ATOMS', - // channelDescription: 'Push notification service for ATOMS', - // importance: Importance.max, - // priority: Priority.max, - // icon: "@drawable/ic_stat_name", - // playSound: true, - // channelShowBadge: true, - // visibility: NotificationVisibility.public, - // enableVibration: true, - // groupKey: 'com.hmg.atoms', - // ); - - // const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails( - // categoryIdentifier: "atoms", - // ); - - const NotificationDetails platformChannel = NotificationDetails(android: androidNotificationDetails, iOS: iosNotificationDetails, macOS: iosNotificationDetails); - - await localNotificationsPlugin.show(hashcode, title, subtext, platformChannel, payload: payload); + static Future showNotification({ + required BuildContext context, + required String title, + required String subtext, + required int hashcode, + String? payload, + String? groupId, // New parameter for grouping + }) async { + if (groupId != null && groupId.isNotEmpty) { + // Show grouped notification + await _showGroupedNotification( + context: context, + title: title, + subtext: subtext, + hashcode: hashcode, + payload: payload, + groupId: groupId, + ); + } else { + // Show regular notification without grouping + const NotificationDetails platformChannel = NotificationDetails( + android: androidNotificationDetails, + iOS: iosNotificationDetails, + macOS: iosNotificationDetails, + ); + await localNotificationsPlugin.show(hashcode, title, subtext, platformChannel, payload: payload); + } + } + + /// Show grouped notification with Android grouping and iOS thread support + static Future _showGroupedNotification({ + required BuildContext context, + required String title, + required String subtext, + required int hashcode, + String? payload, + required String groupId, + }) async { + // Store notification in group + _notificationGroups.putIfAbsent(groupId, () => []); + _notificationGroups[groupId]!.add({ + 'title': title, + 'body': subtext, + 'id': hashcode, + 'payload': payload, + 'timestamp': DateTime.now().millisecondsSinceEpoch, + }); + + if (Platform.isAndroid) { + // Android: Show individual notification + summary notification + await _showAndroidGroupedNotification( + title: title, + subtext: subtext, + hashcode: hashcode, + payload: payload, + groupId: groupId, + ); + } else if (Platform.isIOS) { + // iOS: Use thread identifier for grouping + await _showIOSGroupedNotification( + title: title, + subtext: subtext, + hashcode: hashcode, + payload: payload, + groupId: groupId, + ); + } + } + + /// Android-specific grouped notification implementation + static Future _showAndroidGroupedNotification({ + required String title, + required String subtext, + required int hashcode, + String? payload, + required String groupId, + }) async { + final notifications = _notificationGroups[groupId]!; + final notificationCount = notifications.length; + + // Create individual notification with group key + final AndroidNotificationDetails androidDetails = AndroidNotificationDetails( + 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: "@drawable/ic_stat_name", + playSound: true, + channelShowBadge: true, + visibility: NotificationVisibility.public, + enableVibration: true, + groupKey: groupId, // Use dynamic group key + setAsGroupSummary: false, + ); + + const DarwinNotificationDetails iosDetails = DarwinNotificationDetails( + categoryIdentifier: "atoms", + ); + + final NotificationDetails notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + macOS: iosDetails, + ); + + // Show individual notification + await localNotificationsPlugin.show( + hashcode, + title, + subtext, + notificationDetails, + payload: payload, + ); + + // Create and show summary notification + await _showAndroidSummaryNotification(groupId, notificationCount); + } + + /// Create summary notification for Android group + static Future _showAndroidSummaryNotification(String groupId, int count) async { + // Generate or retrieve summary notification ID + if (!_groupSummaryIds.containsKey(groupId)) { + _groupSummaryIds[groupId] = groupId.hashCode.abs(); + } + final summaryId = _groupSummaryIds[groupId]!; + + final notifications = _notificationGroups[groupId]!; + + // Build inbox style summary + final InboxStyleInformation inboxStyle = InboxStyleInformation( + notifications.map((n) => '${n['title']}: ${n['body']}').toList(), + contentTitle: '$count new notifications', + summaryText: 'ATOMS', + ); + + final AndroidNotificationDetails summaryDetails = AndroidNotificationDetails( + 'com.hmg.atoms', + 'ATOMS', + channelDescription: 'Push notification service for ATOMS', + importance: Importance.max, + priority: Priority.max, + icon: "@drawable/ic_stat_name", + styleInformation: inboxStyle, + groupKey: groupId, + setAsGroupSummary: true, // This is the summary notification + playSound: false, // Don't play sound for summary + enableVibration: false, // Don't vibrate for summary + ); + + final NotificationDetails summaryNotificationDetails = NotificationDetails( + android: summaryDetails, + ); + + await localNotificationsPlugin.show( + summaryId, + 'ATOMS', + '$count new notifications', + summaryNotificationDetails, + ); + } + + /// iOS-specific grouped notification with thread identifier + static Future _showIOSGroupedNotification({ + required String title, + required String subtext, + required int hashcode, + String? payload, + required String groupId, + }) async { + final DarwinNotificationDetails iosDetails = DarwinNotificationDetails( + categoryIdentifier: "atoms", + threadIdentifier: groupId, // iOS uses threadIdentifier for grouping + ); + + const AndroidNotificationDetails androidDetails = androidNotificationDetails; + + final NotificationDetails notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + macOS: iosDetails, + ); + + await localNotificationsPlugin.show( + hashcode, + title, + subtext, + notificationDetails, + payload: payload, + ); + } + + /// Clear notification group when tapped (optional utility) + static void clearNotificationGroup(String groupId) { + _notificationGroups.remove(groupId); + if (_groupSummaryIds.containsKey(groupId)) { + final summaryId = _groupSummaryIds[groupId]!; + localNotificationsPlugin.cancel(summaryId); + _groupSummaryIds.remove(groupId); + } + } + + /// Get notifications in a group (for debugging or UI display) + static List>? getNotificationsInGroup(String groupId) { + return _notificationGroups[groupId]; } static Future scheduleNotification({int? id, String? title, String? body, String? payLoad, required DateTime scheduledNotificationDateTime}) async { diff --git a/lib/main.dart b/lib/main.dart index fcb78f3e..24438d6c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:localization/localization.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:huawei_push/huawei_push.dart' as h_push; import 'package:provider/provider.dart'; import 'package:test_sa/controllers/providers/api/all_requests_provider.dart'; import 'package:test_sa/controllers/providers/api/comments_provider.dart'; @@ -139,6 +140,8 @@ import 'providers/lookups/request_type_lookup_provider.dart'; import 'providers/lookups/yes_no_lookup_provider.dart'; import 'providers/service_request_providers/loan_availability_provider.dart'; import 'providers/service_request_providers/reject_reason_provider.dart'; +import 'controllers/notification/firebase_notification_manger.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; // class MyHttpOverrides extends HttpOverrides { // @override @@ -166,6 +169,46 @@ void main() async { } else { await Firebase.initializeApp(); } + + // Register background message handler - CRITICAL: Must be done in main() before runApp() + // This allows notifications to be shown when app is in background or terminated + FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); + + // CRITICAL: Capture initial notification IMMEDIATELY after Firebase init, before runApp() + // This is the ONLY place where getInitialMessage() works reliably + if (Platform.isAndroid) { + // Check if device has Google Play Services + final hasGoogleServices = await FirebaseNotificationManger.isGoogleServicesAvailable(); + + if (hasGoogleServices) { + // Firebase/Google device + final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + + if (initialMessage != null) { + FirebaseNotificationManger.setInitialNotification(initialMessage.data); + } + } else { + // Huawei device + try { + final huaweiPush = await h_push.Push.getInitialNotification(); + + if (huaweiPush != null) { + Map remoteData = Map.from(huaweiPush["extras"] as Map); + FirebaseNotificationManger.setInitialNotification(remoteData); + } + } catch (e) { + // Huawei push check failed, continue without notification + } + } + } else if (Platform.isIOS) { + // iOS device + final initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + + if (initialMessage != null) { + FirebaseNotificationManger.setInitialNotification(initialMessage.data); + } + } + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.white, @@ -197,6 +240,7 @@ final navigatorKey = GlobalKey(); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); + // This widget is the root of your application. @override Widget build(BuildContext context) { diff --git a/lib/new_views/pages/land_page/land_page.dart b/lib/new_views/pages/land_page/land_page.dart index b024e501..e4cf0fa8 100644 --- a/lib/new_views/pages/land_page/land_page.dart +++ b/lib/new_views/pages/land_page/land_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -53,11 +54,15 @@ class _LandPageState extends State { } void checkLocalAuth() async { + await checkLocalAuthWithCallback(null); + } + + Future checkLocalAuthWithCallback(VoidCallback? onComplete) async { if (await Provider.of(context, listen: false).auth.isDeviceSupported()) { SharedPreferences prefs = await SharedPreferences.getInstance(); if (!prefs.containsKey(ASettings.localAuth)) { (await Provider.of(context, listen: false)).setAuth(false); - showModalBottomSheet( + await showModalBottomSheet( context: context, useSafeArea: true, backgroundColor: Colors.transparent, @@ -100,6 +105,20 @@ class _LandPageState extends State { )), ), ); + // After bottom sheet is dismissed, call the callback if provided + if (onComplete != null) { + onComplete(); + } + } else { + // No bottom sheet shown, call callback immediately if provided + if (onComplete != null) { + onComplete(); + } + } + } else { + // Device doesn't support biometric, call callback immediately if provided + if (onComplete != null) { + onComplete(); } } } @@ -132,9 +151,47 @@ class _LandPageState extends State { FirebaseNotificationManger.handleMessage(context, json.decode(notificationDetails.payload!)); }, (id, title, body, payload) async {}); + // Check if there's a pending notification from app launch (killed/background state) + // This handles the case when user taps notification, app opens, asks for login, + // and after successful login we need to navigate to the correct screen + Map? pendingPayload = FirebaseNotificationManger.getPendingNotificationPayload(); + + // If no Firebase notification, check local notification (for data-only notifications shown by background handler) + if (pendingPayload == null) { + final initialLocalNotification = NotificationManger.getInitialNotificationResponse(); + if (initialLocalNotification != null && initialLocalNotification.payload != null) { + try { + pendingPayload = json.decode(initialLocalNotification.payload!) as Map; + } catch (e) { + // Silent fail - invalid payload + } + } + } + + if (pendingPayload != null) { + // Capture the payload in a final variable for use in callback + final notificationPayload = pendingPayload; + + // Show biometric prompt on dashboard FIRST, THEN navigate after user interaction + WidgetsBinding.instance.addPostFrameCallback((_) async { + // Show biometric bottom sheet on dashboard + await checkLocalAuthWithCallback(() { + // After biometric interaction completes (bottom sheet dismissed), + // navigate to notification detail screen + Future.delayed(const Duration(milliseconds: 300), () { + if (mounted) { + FirebaseNotificationManger.handleMessage(context, notificationPayload); + } + }); + }); + }); + } else { + // No pending notification, show biometric prompt normally + checkLocalAuth(); + } + isFCM = false; } - checkLocalAuth(); } return WillPopScope(