From d2e6daa0ae5fb34f07c0d979d7d1930d75fb9fde Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Tue, 9 Dec 2025 15:21:54 +0000 Subject: [PATCH 1/3] fix(firebase_messaging): enhance iOS scene delegate support and improve notification handling --- .../example/ios/Runner/AppDelegate.h | 2 +- .../example/ios/Runner/AppDelegate.m | 6 +- .../example/ios/Runner/Info.plist | 21 +++++ .../firebase_messaging/example/lib/main.dart | 20 +++-- .../FLTFirebaseMessagingPlugin.m | 85 ++++++++++++++++++- 5 files changed, 120 insertions(+), 14 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.h b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..01e6e1d4793a 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.h +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.h @@ -1,6 +1,6 @@ #import #import -@interface AppDelegate : FlutterAppDelegate +@interface AppDelegate : FlutterAppDelegate @end diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m index 70e83933db14..e243e6627852 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/AppDelegate.m @@ -5,9 +5,13 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; + // [GeneratedPluginRegistrant registerWithRegistry:self]; // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)didInitializeImplicitFlutterEngine:(NSObject *)engineBridge { + [GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry]; +} + @end diff --git a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/Info.plist b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/Info.plist index b8293061bd33..cd5c989988e6 100644 --- a/packages/firebase_messaging/firebase_messaging/example/ios/Runner/Info.plist +++ b/packages/firebase_messaging/firebase_messaging/example/ios/Runner/Info.plist @@ -50,5 +50,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/firebase_messaging/firebase_messaging/example/lib/main.dart b/packages/firebase_messaging/firebase_messaging/example/lib/main.dart index 112a02db5783..128d68951a0b 100644 --- a/packages/firebase_messaging/firebase_messaging/example/lib/main.dart +++ b/packages/firebase_messaging/firebase_messaging/example/lib/main.dart @@ -93,6 +93,7 @@ Future setupFlutterNotifications() async { } void showFlutterNotification(RemoteMessage message) { + print('foreground message received: ${message.messageId}'); RemoteNotification? notification = message.notification; AndroidNotification? android = message.notification?.android; if (notification != null && android != null && !kIsWeb) { @@ -179,14 +180,17 @@ class _Application extends State { void initState() { super.initState(); - FirebaseMessaging.instance.getInitialMessage().then( - (value) => setState( - () { - _resolved = true; - initialMessage = value?.data.toString(); - }, - ), - ); + // Delay getInitialMessage call by 3 seconds + Future.delayed(const Duration(seconds: 3), () { + FirebaseMessaging.instance.getInitialMessage().then( + (value) => setState( + () { + _resolved = true; + initialMessage = value?.data.toString(); + }, + ), + ); + }); FirebaseMessaging.onMessage.listen(showFlutterNotification); diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m index 7af15f16070f..a3fda5ac4f4e 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m @@ -542,8 +542,14 @@ - (BOOL)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { // Handle launch notification if present - NSDictionary *remoteNotification = - connectionOptions.notificationResponse.notification.request.content.userInfo; + // With scene delegates, the notification can be in notificationResponse if user tapped it + + NSDictionary *remoteNotification = nil; + if (connectionOptions.notificationResponse != nil) { + remoteNotification = + connectionOptions.notificationResponse.notification.request.content.userInfo; + } + if (remoteNotification != nil) { // If remoteNotification exists, it is the notification that opened the app. _initialNotification = @@ -551,11 +557,82 @@ - (BOOL)scene:(UIScene *)scene _initialNotificationID = remoteNotification[@"gcm.message_id"]; } + // Mark that initial notification has been gathered (even if nil) + _initialNotificationGathered = YES; + [self initialNotificationCallback]; + + [GULAppDelegateSwizzler registerAppDelegateInterceptor:self]; + [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; + + SEL didReceiveRemoteNotificationWithCompletionSEL = + NSSelectorFromString(@"application:didReceiveRemoteNotification:fetchCompletionHandler:"); + if ([[GULAppDelegateSwizzler sharedApplication].delegate + respondsToSelector:didReceiveRemoteNotificationWithCompletionSEL]) { + // noop - user has own implementation of this method in their AppDelegate, this + // means GULAppDelegateSwizzler will have already replaced it with a donor method + } else { + // add our own donor implementation of + // application:didReceiveRemoteNotification:fetchCompletionHandler: + Method donorMethod = class_getInstanceMethod(object_getClass(self), + didReceiveRemoteNotificationWithCompletionSEL); + class_addMethod(object_getClass([GULAppDelegateSwizzler sharedApplication].delegate), + didReceiveRemoteNotificationWithCompletionSEL, + method_getImplementation(donorMethod), method_getTypeEncoding(donorMethod)); + } + +#if !TARGET_OS_OSX + // `[_registrar addApplicationDelegate:self];` alone doesn't work for notifications to be received + // without the above swizzling This commit: + // https://github.com/google/GoogleUtilities/pull/162/files#diff-6bb6d1c46632fc66405a524071cc4baca5fc6a1a6c0eefef81d8c3e2c89cbc13L520-L533 + // broke notifications which was released with firebase-ios-sdk v11.0.0 + [_registrar addApplicationDelegate:self]; +#endif + + // Ensure UNUserNotificationCenter delegate is set up for scene delegates + // This is critical for foreground notifications to work with scene delegates + if (@available(iOS 10.0, *)) { + BOOL shouldReplaceDelegate = YES; + UNUserNotificationCenter *notificationCenter = + [UNUserNotificationCenter currentNotificationCenter]; + + if (notificationCenter.delegate != nil) { + // If a UNUserNotificationCenterDelegate is set and it conforms to + // FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier + // call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls + // to this plugin. If we replace it, it will cause a stack overflow. See + // https://github.com/firebasefire/issues/4026. + if ([notificationCenter.delegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { + // Note this one only executes if Firebase swizzling is **enabled**. + shouldReplaceDelegate = NO; + } + + if (shouldReplaceDelegate && _originalNotificationCenterDelegate == nil) { + // Preserve original delegate if it exists + _originalNotificationCenterDelegate = notificationCenter.delegate; + _originalNotificationCenterDelegateRespondsTo.openSettingsForNotification = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]; + _originalNotificationCenterDelegateRespondsTo.willPresentNotification = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter: + willPresentNotification:withCompletionHandler:)]; + _originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse = + (unsigned int)[_originalNotificationCenterDelegate + respondsToSelector:@selector(userNotificationCenter: + didReceiveNotificationResponse:withCompletionHandler:)]; + } + } + + if (shouldReplaceDelegate && notificationCenter.delegate != self) { + // Set our delegate + __strong FLTFirebasePlugin *strongSelf = self; + notificationCenter.delegate = strongSelf; + } + } + // Register for remote notifications in scene delegate // This is critical for getting APNS token when using UISceneDelegate [[UIApplication sharedApplication] registerForRemoteNotifications]; - - return YES; } #endif From 56c1262bd0533f8719bc8ff84150e51403bfaa9a Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Wed, 10 Dec 2025 16:08:27 +0000 Subject: [PATCH 2/3] fix(firebase_messaging): refactor notification handling in scene delegate methods --- .../FLTFirebaseMessagingPlugin.m | 108 +++--------------- 1 file changed, 16 insertions(+), 92 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m index a3fda5ac4f4e..c64e6468d5a8 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m @@ -216,14 +216,8 @@ - (void)messaging:(nonnull FIRMessaging *)messaging #pragma mark - NSNotificationCenter Observers -- (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)notification { - // Setup UIApplicationDelegate. -#if TARGET_OS_OSX - NSDictionary *remoteNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey]; -#else - NSDictionary *remoteNotification = - notification.userInfo[UIApplicationLaunchOptionsRemoteNotificationKey]; -#endif +- (void)setupNotificationHandlingWithRemoteNotification: + (nullable NSDictionary *)remoteNotification { if (remoteNotification != nil) { // If remoteNotification exists, it is the notification that opened the app. _initialNotification = @@ -314,6 +308,17 @@ - (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)n #endif } +- (void)application_onDidFinishLaunchingNotification:(nonnull NSNotification *)notification { + // Setup UIApplicationDelegate. +#if TARGET_OS_OSX + NSDictionary *remoteNotification = notification.userInfo[NSApplicationLaunchUserNotificationKey]; +#else + NSDictionary *remoteNotification = + notification.userInfo[UIApplicationLaunchOptionsRemoteNotificationKey]; +#endif + [self setupNotificationHandlingWithRemoteNotification:remoteNotification]; +} + #pragma mark - UNUserNotificationCenter Delegate Methods #ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM @@ -538,7 +543,7 @@ - (BOOL)application:(UIApplication *)application #pragma mark - SceneDelegate Methods #if !TARGET_OS_OSX -- (BOOL)scene:(UIScene *)scene +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { // Handle launch notification if present @@ -546,93 +551,12 @@ - (BOOL)scene:(UIScene *)scene NSDictionary *remoteNotification = nil; if (connectionOptions.notificationResponse != nil) { + // User tapped the notification. remoteNotification = connectionOptions.notificationResponse.notification.request.content.userInfo; } - if (remoteNotification != nil) { - // If remoteNotification exists, it is the notification that opened the app. - _initialNotification = - [FLTFirebaseMessagingPlugin remoteMessageUserInfoToDict:remoteNotification]; - _initialNotificationID = remoteNotification[@"gcm.message_id"]; - } - - // Mark that initial notification has been gathered (even if nil) - _initialNotificationGathered = YES; - [self initialNotificationCallback]; - - [GULAppDelegateSwizzler registerAppDelegateInterceptor:self]; - [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; - - SEL didReceiveRemoteNotificationWithCompletionSEL = - NSSelectorFromString(@"application:didReceiveRemoteNotification:fetchCompletionHandler:"); - if ([[GULAppDelegateSwizzler sharedApplication].delegate - respondsToSelector:didReceiveRemoteNotificationWithCompletionSEL]) { - // noop - user has own implementation of this method in their AppDelegate, this - // means GULAppDelegateSwizzler will have already replaced it with a donor method - } else { - // add our own donor implementation of - // application:didReceiveRemoteNotification:fetchCompletionHandler: - Method donorMethod = class_getInstanceMethod(object_getClass(self), - didReceiveRemoteNotificationWithCompletionSEL); - class_addMethod(object_getClass([GULAppDelegateSwizzler sharedApplication].delegate), - didReceiveRemoteNotificationWithCompletionSEL, - method_getImplementation(donorMethod), method_getTypeEncoding(donorMethod)); - } - -#if !TARGET_OS_OSX - // `[_registrar addApplicationDelegate:self];` alone doesn't work for notifications to be received - // without the above swizzling This commit: - // https://github.com/google/GoogleUtilities/pull/162/files#diff-6bb6d1c46632fc66405a524071cc4baca5fc6a1a6c0eefef81d8c3e2c89cbc13L520-L533 - // broke notifications which was released with firebase-ios-sdk v11.0.0 - [_registrar addApplicationDelegate:self]; -#endif - - // Ensure UNUserNotificationCenter delegate is set up for scene delegates - // This is critical for foreground notifications to work with scene delegates - if (@available(iOS 10.0, *)) { - BOOL shouldReplaceDelegate = YES; - UNUserNotificationCenter *notificationCenter = - [UNUserNotificationCenter currentNotificationCenter]; - - if (notificationCenter.delegate != nil) { - // If a UNUserNotificationCenterDelegate is set and it conforms to - // FlutterAppLifeCycleProvider then we don't want to replace it on iOS as the earlier - // call to `[_registrar addApplicationDelegate:self];` will automatically delegate calls - // to this plugin. If we replace it, it will cause a stack overflow. See - // https://github.com/firebasefire/issues/4026. - if ([notificationCenter.delegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { - // Note this one only executes if Firebase swizzling is **enabled**. - shouldReplaceDelegate = NO; - } - - if (shouldReplaceDelegate && _originalNotificationCenterDelegate == nil) { - // Preserve original delegate if it exists - _originalNotificationCenterDelegate = notificationCenter.delegate; - _originalNotificationCenterDelegateRespondsTo.openSettingsForNotification = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter:openSettingsForNotification:)]; - _originalNotificationCenterDelegateRespondsTo.willPresentNotification = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter: - willPresentNotification:withCompletionHandler:)]; - _originalNotificationCenterDelegateRespondsTo.didReceiveNotificationResponse = - (unsigned int)[_originalNotificationCenterDelegate - respondsToSelector:@selector(userNotificationCenter: - didReceiveNotificationResponse:withCompletionHandler:)]; - } - } - - if (shouldReplaceDelegate && notificationCenter.delegate != self) { - // Set our delegate - __strong FLTFirebasePlugin *strongSelf = self; - notificationCenter.delegate = strongSelf; - } - } - - // Register for remote notifications in scene delegate - // This is critical for getting APNS token when using UISceneDelegate - [[UIApplication sharedApplication] registerForRemoteNotifications]; + [self setupNotificationHandlingWithRemoteNotification:remoteNotification]; } #endif From 1375b057632bdb67aec29c983fa5618ee8b88f27 Mon Sep 17 00:00:00 2001 From: Jude Kwashie Date: Thu, 11 Dec 2025 12:32:48 +0000 Subject: [PATCH 3/3] fix(firebase_messaging): improve scene delegate support for notification handling --- .../FLTFirebaseMessagingPlugin.m | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m index c64e6468d5a8..8a89462afbe6 100644 --- a/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m +++ b/packages/firebase_messaging/firebase_messaging/ios/firebase_messaging/Sources/firebase_messaging/FLTFirebaseMessagingPlugin.m @@ -40,6 +40,9 @@ @implementation FLTFirebaseMessagingPlugin { NSString *_notificationOpenedAppID; NSString *_foregroundUniqueIdentifier; + // Track if scene delegate connected (for iOS 13+ scene delegate support) + BOOL _sceneDidConnect; + #ifdef __FF_NOTIFICATIONS_SUPPORTED_PLATFORM API_AVAILABLE(ios(10), macosx(10.14)) __weak id _originalNotificationCenterDelegate; @@ -59,6 +62,7 @@ - (instancetype)initWithFlutterMethodChannel:(FlutterMethodChannel *)channel self = [super init]; if (self) { _initialNotificationGathered = NO; + _sceneDidConnect = NO; _channel = channel; _registrar = registrar; // Application @@ -223,9 +227,24 @@ - (void)setupNotificationHandlingWithRemoteNotification: _initialNotification = [FLTFirebaseMessagingPlugin remoteMessageUserInfoToDict:remoteNotification]; _initialNotificationID = remoteNotification[@"gcm.message_id"]; + _initialNotificationGathered = YES; + [self initialNotificationCallback]; + } else if (_sceneDidConnect) { + // For scene delegates, if no notification was found in connectionOptions, + // delay marking as gathered to allow didReceiveRemoteNotification to fire first + // for contentAvailable notifications that caused the app to launch + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + if (!self->_initialNotificationGathered) { + self->_initialNotificationGathered = YES; + [self initialNotificationCallback]; + } + }); + } else { + // For non-scene delegate apps, mark as gathered immediately + _initialNotificationGathered = YES; + [self initialNotificationCallback]; } - _initialNotificationGathered = YES; - [self initialNotificationCallback]; [GULAppDelegateSwizzler registerAppDelegateInterceptor:self]; [GULAppDelegateSwizzler proxyOriginalDelegateIncludingAPNSMethods]; @@ -478,6 +497,15 @@ - (BOOL)application:(UIApplication *)application [FLTFirebaseMessagingPlugin remoteMessageUserInfoToDict:userInfo]; // Only handle notifications from FCM. if (userInfo[@"gcm.message_id"]) { + // For scene delegate apps: if this notification arrives during cold launch + // (before initial notification gathering is complete) and no notification was found + // in connectionOptions, this is the notification that caused the launch. + if (_sceneDidConnect && !_initialNotificationGathered && _initialNotification == nil) { + _initialNotification = notificationDict; + _initialNotificationID = userInfo[@"gcm.message_id"]; + _initialNotificationGathered = YES; + [self initialNotificationCallback]; + } if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { __block BOOL completed = NO; @@ -548,6 +576,7 @@ - (void)scene:(UIScene *)scene options:(UISceneConnectionOptions *)connectionOptions { // Handle launch notification if present // With scene delegates, the notification can be in notificationResponse if user tapped it + _sceneDidConnect = YES; NSDictionary *remoteNotification = nil; if (connectionOptions.notificationResponse != nil) {