After migrating a Flutter iOS app to use UISceneDelegation (via FlutterSceneDelegate in Info.plist), FirebaseMessaging.onMessage in Dart never fires for foreground messages. Background banners appear correctly via APNs, but the Dart layer receives nothing — no onMessage, no onMessageOpenedApp logs, and no getInitialMessage data.
What works:
-
FCM token is generated and non-null
-
Push notification banners appear when the app is backgrounded
-
didRegisterForRemoteNotificationsWithDeviceTokenfires correctly -
UNUserNotificationCenterwillPresent fires correctly (confirmed via Swift print) -
Messaging.messaging().appDidReceiveMessage(userInfo)is called successfully from willPresent
What breaks:
-
FirebaseMessaging.onMessagenever fires in Dart -
FirebaseMessaging.onMessageOpenedAppnever fires in Dart -
No Dart-side logs appear for any incoming notification
-
Root Cause (Diagnosed): When using
FlutterImplicitEngineDelegate, plugins are registered via:func didInitializeImplicitFlutterEngine(\_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) }, After GeneratedPluginRegistrant.register runs, FLTFirebaseMessagingPlugin is expected to set itself as Messaging.messaging().delegate. However, printing the delegate immediately after registration reveals: print(Messaging.messaging().delegate) // → nil
This did not occur before the UIScene migration, when GeneratedPluginRegistrant.register(with: self) was called in didFinishLaunchingWithOptions and FlutterAppDelegate (which lives for the app’s lifetime) held the plugins strongly.
This is my appdelegate.swift file:
import Flutter
import Firebase
import UserNotifications
import flutter_local_notifications
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in }
application.registerForRemoteNotifications()
// GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
// This is required to make any communication available in the action isolate.
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
GeneratedPluginRegistrant.register(with: registry)
}
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
override func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
// print("✅ APNs token: \(deviceToken.map { String(format: "%02x", $0) }.joined())")
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
// override func application(_ application: UIApplication,
// didReceiveRemoteNotification userInfo: [AnyHashable : Any],
// fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Messaging.messaging().appDidReceiveMessage(userInfo)
// completionHandler(.newData)
// }
}
And this is my info.plist file:
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
UISceneConfigurations
UIWindowSceneSessionRoleApplication
UISceneClassName
UIWindowScene
UISceneDelegateClassName
FlutterSceneDelegate
UISceneConfigurationName
flutter
UISceneStoryboardFile
Main
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
Bongo Payments
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
bongo_pay
CFBundlePackageType
APPL
CFBundleShortVersionString
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
UIBackgroundModes
remote-notification
FirebaseAppDelegateProxyEnabled
NSAppTransportSecurity
NSAllowsArbitraryLoads
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
NSLocationWhenInUseUsageDescription
We need your location to provide better service.
NSLocationAlwaysAndWhenInUseUsageDescription
This app uses notifications
NSLocationAlwaysUsageDescription
We need your location even when the app is in the background.
NSUserTrackingUsageDescription
This app uses notifications
LSRequiresIPhoneOS
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
NSFaceIDUsageDescription
This app requires Face ID authentication to secure your personal data and ensure only you can access your account.
NSCameraUsageDescription
We need access to your camera to take photos.
NSPhotoLibraryUsageDescription
We need access to your photo library to upload images.
NSAppleMusicUsageDescription
This app does not use Apple Music but requires limited media library access for system features.
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
CADisableMinimumFrameDurationOnPhone
UIApplicationSupportsIndirectInputEvents
io.flutter.embedded_views_preview
NSMicrophoneUsageDescription
We need access to your microphone for video or audio capture in web pages.
NSPhotoLibraryAddUsageDescription
We need access to save images to your photo library.
UIFileSharingEnabled
LSSupportsOpeningDocumentsInPlace
UIStatusBarHidden