iOS Notification Tap Not Working for Local Notifications Created via showLocalAgoraMessage()
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
- Go to '...'
- Click on '....'
- Scroll down to '....'
- 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
/*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
/// Initialize local notification plugin
static Future
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
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
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<String> 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<AgoraChatService>(); await agoraChatService.init();
if (await ChatClient.getInstance.isConnected()) { await agoraChatService.registerAgoraPushToken(); } runApp(const EntryPoint()); }
do we have a fix already for this one? @riyapateluex