Skip to content

iOS Notification Tap Not Working for Local Notifications Created via showLocalAgoraMessage()Β #2713

@riyapateluex

Description

@riyapateluex

Describe the bug
When showing a local notification on iOS using FlutterLocalNotificationsPlugin.show() (inside showLocalAgoraMessage()), tapping the notification does not trigger any callback (onDidReceiveNotificationResponse or onDidReceiveBackgroundNotificationResponse).
This issue only occurs on iOS.
On Android, the tap callback works correctly and navigation is triggered.
To Reproduce

  1. Go to '...'
  2. Click on '....'
  3. Scroll down to '....'
  4. See error

Expected behavior
When a user taps a local notification (created from showLocalAgoraMessage()):
The app should open (if terminated or backgrounded).
The onDidReceiveNotificationResponse or onDidReceiveBackgroundNotificationResponse callback should be invoked.
The payload should be accessible and used for navigation.
Sample code to reproduce the problem
import Flutter
import UIKit
import GoogleMaps
import Firebase
import UserNotifications

@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("AIzaSyAAKACXwaUU8azzjgUcDgd381g1cA7Al1Q")
GeneratedPluginRegistrant.register(with: self)

// Clear badge
UIApplication.shared.applicationIconBadgeNumber = 0

// Set notification delegate
if #available(iOS 10.0, *) {
  UNUserNotificationCenter.current().delegate = self
}

// Handle notification when app is launched from terminated state
if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] {
  Messaging.messaging().appDidReceiveMessage(remoteNotification)
}

return super.application(application, didFinishLaunchingWithOptions: launchOptions)

}

override func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
Messaging.messaging().appDidReceiveMessage(userInfo)
completionHandler(.newData)
}

// Handle notification when app is in foreground
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound, .badge])
}

// Handle notification tap
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
Messaging.messaging().appDidReceiveMessage(userInfo)
completionHandler()
}
}

// import 'dart:convert';
import 'dart:io';

import 'package:agora_chat_sdk/agora_chat_sdk.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:logger/logger.dart';
import 'package:platinum_acres/core/app_life_cycle_observer.dart';

import '../core/cache/preference_store.dart';
import '../core/constants.dart';
import '../core/logging.dart';
import '../core/utils/colors.dart';
import '../core/utils/styles.dart';
import '../main.dart';

@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) async {
print('πŸ”” Background notification tapped: ${notificationResponse.payload}');
if (notificationResponse.payload != null && notificationResponse.payload!.isNotEmpty) {
try {
final payloadMap = json.decode(notificationResponse.payload!);
FCMNotificationService.navigateScreen(payloadMap);
} catch (e) {
print('❌ Failed to parse payload: $e');
}
}
}

class FCMNotificationService {
static final FirebaseMessaging _messaging = FirebaseMessaging.instance;
static final FlutterLocalNotificationsPlugin _localNotificationsPlugin =
FlutterLocalNotificationsPlugin();

static const AndroidNotificationChannel _channel = AndroidNotificationChannel(
'high_importance_channel', // id
'High Importance Notifications', // title
description: 'This channel is used for important notifications.',
// description
importance: Importance.high,
);

/// Initialize both Firebase Messaging and Local Notification Plugins
static Future initialize() async {
// Request permissions
await _requestPermissions();

/*if (Platform.isIOS) {
  await FirebaseMessaging.instance
      .setForegroundNotificationPresentationOptions(
    alert: true,
    badge: true,
    sound: true,
  );
}*/

// Get and store FCM token
final token = await _messaging.getToken();
log(message: 'πŸ“² FCM Token: $token');

final preferenceStore = PreferenceStore();
await preferenceStore.init();
await preferenceStore.setStringData(PreferenceData.FCM_TOKEN, token ?? '');

/*FirebaseMessaging.instance.onTokenRefresh.listen((newToken) async {
  print('πŸ”„ FCM token refreshed: $newToken');
  if (await ChatClient.getInstance.isConnected()) {
    final agoraChatService = Injector.resolve<AgoraChatService>();
    await agoraChatService.registerAgoraPushToken();
  }
});*/

// Init local notification plugin
await _initializeLocalNotifications();

// Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
  Logger().d("πŸ“© Foreground FCM message received: $message");
  Logger().d("πŸ“© Foreground FCM message received: ${message.notification}");
  Logger().d("πŸ“© Foreground FCM message received: ${message.data}");

  // Show local notification
  await _showLocalNotification(message);
});
// Handle background-tap (when app is opened from a notification)
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  log(message: 'πŸš€ App opened from notification: ${message}');
  log(message: 'πŸš€ App opened from notification1: ${message.messageId}');
  log(message: 'πŸš€ App opened from notification2: ${message.data}');

  //Done
  /* navigatorKey.currentState?.pushNamed(
    '/dashBoardScreen',
    arguments: message.data,
  );*/
  // Handle navigation if needed
});

/*final NotificationAppLaunchDetails? notificationAppLaunchDetails =
await _localNotificationsPlugin.getNotificationAppLaunchDetails();

if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
  final payload = notificationAppLaunchDetails!.notificationResponse?.payload;
  Logger().d("πŸ›‘ App launched from terminated via local notification: $payload");

  if (payload != null && payload.isNotEmpty) {
    Future.delayed(const Duration(milliseconds: 500), () {
      try {
        final Map<String, dynamic> payloadMap = json.decode(payload);
        navigateScreen(payloadMap);
      } catch (e) {
        print('❌ Error parsing launch notification payload: $e');
      }
    });
  }
}*/

// (Optional) handle messages when app is opened via terminated state
/*final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
  Logger().d(
    "πŸ›‘ App launched from terminated via notification: $initialMessage ::: ${initialMessage.data}",
  );
  */ /* navigatorKey.currentState?.pushNamed(
    '/dashBoardScreen',
    arguments: initialMessage.data,
  );*/ /*
  // Handle deep link or navigation here
}*/

}

/// Request user permission (iOS + Android 13+)
static Future _requestPermissions() async {
NotificationSettings settings = await _messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
print('Authorization status: ${settings.authorizationStatus}');
switch (settings.authorizationStatus) {
case AuthorizationStatus.authorized:
print('βœ… User granted permission');
break;
case AuthorizationStatus.provisional:
print('⚠️ User granted provisional permission');
break;
default:
print('❌ User declined or has not accepted permission');
break;
}
}

/// Initialize local notification plugin
static Future _initializeLocalNotifications() async {
const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);

final iOSSettings = DarwinInitializationSettings(
  requestSoundPermission: true,
  requestBadgePermission: true,
  requestAlertPermission: true,
);

final initSettings = InitializationSettings(
  android: androidSettings,
  iOS: iOSSettings,
);

await _localNotificationsPlugin.initialize(
  initSettings,
  onDidReceiveBackgroundNotificationResponse: notificationTapBackground,

  onDidReceiveNotificationResponse: (NotificationResponse response) {
    print('πŸ”” User tapped notification: ${response.payload}');
    print('πŸ”” User tapped notification: ${response.data}:::${response}');
    // Navigate based on payload if needed
    final payload = response.payload;
    Map<String, dynamic> payloadMap = {};
    // Done
    if (payload != null && payload.isNotEmpty) {
      try {
        payloadMap = json.decode(payload);
      } catch (e) {
        print('❌ Failed to parse payload: $e');
      }
      print(
        'πŸ”” User tapped notification1: ${response.payload} :: $payloadMap',
      );
      navigateScreen(payloadMap);
    }
  },
);

if (Platform.isAndroid) {
  await _localNotificationsPlugin
      .resolvePlatformSpecificImplementation<
        AndroidFlutterLocalNotificationsPlugin
      >()
      ?.createNotificationChannel(_channel);
}

}

/// Show local notification
static Future _showLocalNotification(RemoteMessage message) async {
final notification = message.notification;
final android = message.notification?.android;
Logger().d(
"πŸ“© Foreground FCM message received _showLocalNotification: ${message.data}",
);
if (notification == null) return;
Logger().d(
"πŸ“© Foreground FCM message received _showLocalNotification: ${notification.title} ::: ${notification.body}",
);
final details = NotificationDetails(
android: AndroidNotificationDetails(
_channel.id,
_channel.name,
channelDescription: _channel.description,
icon: android?.smallIcon ?? '@mipmap/ic_launcher',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
);

await _localNotificationsPlugin.show(
  notification.hashCode,
  notification.title,
  notification.body,
  details,
  payload: json.encode(message.data),
);

}

static navigateScreen(Map<String, dynamic> message) {
log(message: 'πŸš€ Navigate Screen notification2: $message');

switch (message['screen']) {
  case 'chatScreen':
    navigatorKey.currentState?.pushNamed('/chatScreen', arguments: message);
    break;
  default:
    navigatorKey.currentState?.pushNamed(
      '/notificationScreen',
      arguments: message,
    );
    break;
}

}

static void _showForegroundPopup(RemoteMessage message) {
final context = navigatorKey.currentContext;
if (context == null) return;

final title = message.notification?.title ?? 'Notification';
final body = message.notification?.body ?? '';

showDialog(
  context: context,
  builder:
      (context) => AlertDialog(
        title: Text(title, style: TextStyles.textFormStyle),
        content: Text(body, style: TextStyles.textNormal),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text(
              'CANCEL',
              style: TextStyles.textFormStyle.copyWith(
                color: AppColors.red,
              ),
            ),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              navigateScreen(message.data);
            },
            child: Text(
              'OPEN',
              style: TextStyles.textFormStyle.copyWith(
                color: AppColors.red,
              ),
            ),
          ),
        ],
      ),
);

}

static void _showForegroundDialog(
Map<String, dynamic> payload,
String title,
String body,
) {
final context = navigatorKey.currentContext;
if (context == null) return;

showDialog(
  context: context,
  builder:
      (context) => AlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('Dismiss'),
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              navigateScreen(payload);
            },
            child: Text('Open'),
          ),
        ],
      ),
);

}

static Future showLocalAgoraMessage(ChatMessage msg) async {
String? pushTitle;
String? pushTitleNotification = '';
String? pushContent;

try {
  final attrs = msg.attributes;
  if (attrs!.containsKey('em_push_ext')) {
    final ext = attrs['em_push_ext'];
    if (ext is Map) {
      pushTitle = ext['em_push_title']?.toString();
      pushTitleNotification = ext['em_push_title']?.toString();
      pushContent = ext['em_push_content']?.toString();
    } else if (ext is String) {
      final Map<String, dynamic> parsed = jsonDecode(ext);
      pushTitle = parsed['em_push_title']?.toString();
      pushTitleNotification = parsed['em_push_title']?.toString();
      pushContent = parsed['em_push_content']?.toString();
    }
  }
} catch (e) {
  print('⚠️ Error parsing em_push_ext: $e');
}

pushTitle ??= 'Message from ${msg.from}';
pushContent ??=
    (msg.body is ChatTextMessageBody)
        ? (msg.body as ChatTextMessageBody).content
        : 'New message';

final details = NotificationDetails(
  android: AndroidNotificationDetails(
    _channel.id,
    _channel.name,
    channelDescription: _channel.description,
    importance: Importance.high,
    priority: Priority.high,
    icon: '@mipmap/ic_launcher',
  ),
  iOS: const DarwinNotificationDetails(
    presentAlert: true,
    presentSound: true,
    presentBadge: true,
  ),
);

Map<String, dynamic> payload = {
  'screen': 'chatScreen',
  'chatId': msg.conversationId,
  'title': pushTitleNotification,
};

if (Platform.isIOS && navigatorKey.currentContext != null) {
  if (AppLifecycleObserver.isAppInForeground) {
    _showForegroundDialog(payload, pushTitle, pushContent);
  } else {
    await _localNotificationsPlugin.show(
      DateTime.now().millisecondsSinceEpoch ~/ 1000,
      pushTitle,
      pushContent,
      details,
      payload: json.encode(payload),
    );
  }

  print('πŸ”” Local notification shown1: $pushTitle β†’ $pushContent');
} else {
  await _localNotificationsPlugin.show(
    DateTime.now().millisecondsSinceEpoch ~/ 1000,
    pushTitle,
    pushContent,
    details,
    payload: json.encode(payload),
  );
  print('πŸ”” Local notification shown2: $pushTitle β†’ $pushContent');
}

print('πŸ”” Local notification shown: $pushTitle β†’ $pushContent');

}

Future getTitle(ChatConversation conv) async {
try {
if (conv.type == ChatConversationType.Chat) {
final result = await ChatClient.getInstance.userInfoManager
.fetchUserInfoById([conv.id]);
final user = result[conv.id];
Logger().d("mn13user$user");
return user?.nickName?.isNotEmpty == true
? user!.nickName!
: user?.phone?.isNotEmpty == true
? user!.phone!
: conv.id;
} else if (conv.type == ChatConversationType.GroupChat) {
final group = await ChatClient.getInstance.groupManager.getGroupWithId(
conv.id,
);
return group?.name ?? conv.id;
} else {
return conv.id;
}
} catch (e) {
print("Error fetching title for ${conv.id}: $e");
return conv.id;
}
}
}

//

void main() async {
WidgetsFlutterBinding.ensureInitialized();

WidgetsBinding.instance.addObserver(AppLifecycleObserver());

SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);

SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: AppColors.scaffoldBackgroundColor,
statusBarIconBrightness: Brightness.dark,
),
);
FirebaseApp app = await Firebase.initializeApp();
print('πŸ”₯ Firebase initialized: ${app.name}');
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
); // <== move this up!
await FCMNotificationService.initialize();
await Injector.setup();
final agoraChatService = Injector.resolve();
await agoraChatService.init();

if (await ChatClient.getInstance.isConnected()) {
await agoraChatService.registerAgoraPushToken();
}
runApp(const EntryPoint());
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions