invertase / react-native-firebase

🔥 A well-tested feature-rich modular Firebase implementation for React Native. Supports both iOS & Android platforms for all Firebase services.
https://rnfirebase.io
Other
11.63k stars 2.2k forks source link

[🐛] 🔥 onMessage callback gets triggered multiple times on iOS 18 #7979

Open nikitasigal opened 3 weeks ago

nikitasigal commented 3 weeks ago

Issue

On devices and simulators running iOS 18, the onMessage() callback gets called two or more times. The issue does not appear on Android or on iOS devices running 17.5 or below.

I've verified, that the onMessage() call itself is only done once, so there should not be multiple listeners active. The remoteMessage payload is notification+data.

Devices tested, where the bug is present:

Devices tested, where the bug is not present:


Project Files

Javascript

Click To Expand

#### `package.json`: ```json "dependencies": { "@gorhom/bottom-sheet": "^4.5.1", "@hmscore/react-native-hms-push": "^6.12.0-301", "@react-native-clipboard/clipboard": "^1.14.1", "@react-native-community/netinfo": "^11.3.2", "@react-native-firebase/app": "^20.4.0", "@react-native-firebase/messaging": "^20.4.0", "@react-navigation/bottom-tabs": "^6.5.9", "@react-navigation/native": "^6.1.8", "@react-navigation/native-stack": "^6.10.0", "@sentry/react-native": "^5.25.0", "@shopify/flash-list": "^1.6.1", "@types/semver": "^7.5.8", "axios": "^1.5.1", "mobx": "^6.10.2", "mobx-persist-store": "^1.1.3", "mobx-react-lite": "^4.0.5", "moment": "^2.29.4", "patch-package": "^8.0.0", "qs": "^6.12.2", "react": "18.2.0", "react-native": "0.73.8", "react-native-bootsplash": "^5.5.3", "react-native-calendars": "^1.1306.0", "react-native-code-push": "^8.3.1", "react-native-config": "^1.5.1", "react-native-device-info": "^10.11.0", "react-native-gesture-handler": "^2.17.0", "react-native-linear-gradient": "^2.8.3", "react-native-mask-input": "^1.2.3", "react-native-mmkv": "^2.10.2", "react-native-modal": "^13.0.1", "react-native-permissions": "^4.1.5", "react-native-reanimated": "^3.6.0", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "^3.25.0", "react-native-svg": "^13.14.0", "react-native-svg-transformer": "^1.1.0", "react-native-tab-view": "^3.5.2", "react-native-toast-notifications": "^3.4.0", "semver": "^7.6.3" }, ``` #### `firebase.json` for react-native-firebase v6: ```json # N/A ```

iOS

Click To Expand

#### `ios/Podfile`: - [ ] I'm not using Pods - [x] I'm using Pods and my Podfile looks like: ```ruby def node_require(script) # Resolve script with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', "require.resolve( '#{script}', {paths: [process.argv[1]]}, )", __dir__]).strip end node_require('react-native/scripts/react_native_pods.rb') node_require('react-native-permissions/scripts/setup.rb') platform :ios, min_ios_version_supported prepare_react_native_project! setup_permissions([ 'Notifications', ]) # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded # # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` # ```js # module.exports = { # dependencies: { # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), # ``` # flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled flipper_config = FlipperConfiguration.disabled use_frameworks! :linkage => :static $RNFirebaseAsStaticFramework = true target 'iteco_employee' do config = use_native_modules! use_react_native!( :path => config[:reactNativePath], # Enables Flipper. # # Note that if you have use_frameworks! enabled, Flipper will not work and # you should disable the next line. :flipper_configuration => flipper_config, # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) target 'iteco_employeeTests' do inherit! :complete # Pods for testing end post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) end end ``` #### `AppDelegate.m`: ```objc #import "AppDelegate.h" #import "RNBootSplash.h" #import #import #import #import #import @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.moduleName = @"iteco_employee"; // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. self.initialProps = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions]; [FIRApp configure]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [CodePush bundleURL]; #endif } - (NSURL *)getBundleURLf { #if DEBUG return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; #else return [CodePush bundleURL]; #endif } - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps { UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps]; [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView]; // ⬅️ initialize the splash screen return rootView; } - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { return [RCTLinkingManager application:application openURL:url options:options]; } @end ```


Android

Click To Expand

#### Have you converted to AndroidX? - [ ] my application is an AndroidX application? - [ ] I am using `android/gradle.settings` `jetifier=true` for Android compatibility? - [ ] I am using the NPM package `jetifier` for react-native compatibility? #### `android/build.gradle`: ```groovy // N/A ``` #### `android/app/build.gradle`: ```groovy // N/A ``` #### `android/settings.gradle`: ```groovy // N/A ``` #### `MainApplication.java`: ```java // N/A ``` #### `AndroidManifest.xml`: ```xml ```


Environment

Click To Expand

**`react-native info` output:** ``` System: OS: macOS 15.0 CPU: (10) arm64 Apple M2 Pro Memory: 88.00 MB / 16.00 GB Shell: version: "5.9" path: /bin/zsh Binaries: Node: version: 22.5.1 path: /opt/homebrew/bin/node Yarn: Not Found npm: version: 10.8.2 path: /opt/homebrew/bin/npm Watchman: version: 2024.07.15.00 path: /opt/homebrew/bin/watchman Managers: CocoaPods: version: 1.15.2 path: /opt/homebrew/bin/pod SDKs: iOS SDK: Platforms: - DriverKit 24.1 - iOS 18.1 - macOS 15.1 - tvOS 18.0 - visionOS 2.0 - watchOS 11.0 Android SDK: API Levels: - "31" - "33" - "34" Build Tools: - 30.0.3 - 31.0.0 - 33.0.0 - 33.0.1 - 34.0.0 - 35.0.0 System Images: - android-34 | Google APIs ARM 64 v8a Android NDK: Not Found IDEs: Android Studio: 2024.1 AI-241.18034.62.2411.12071903 Xcode: version: 16.1/16B5001e path: /usr/bin/xcodebuild Languages: Java: version: 17.0.12 path: /usr/bin/javac Ruby: version: 3.3.4 path: /opt/homebrew/opt/ruby/bin/ruby npmPackages: "@react-native-community/cli": Not Found react: installed: 18.2.0 wanted: 18.2.0 react-native: installed: 0.73.8 wanted: 0.73.8 react-native-macos: Not Found npmGlobalPackages: "*react-native*": Not Found Android: hermesEnabled: true newArchEnabled: false iOS: hermesEnabled: true newArchEnabled: false ``` - **Platform that you're experiencing the issue on**: - [x] iOS - [ ] Android - [ ] **iOS** but have not tested behavior on Android - [ ] **Android** but have not tested behavior on iOS - [ ] Both - **`react-native-firebase` version you're using that has this issue:** - `20.4.0` - **`Firebase` module(s) you're using that has the issue:** - `messaging` - **Are you using `TypeScript`?** - `Y`, `5.0.4`


russellwheatley commented 3 weeks ago

Haven't been able to test on iOS 18 yet, I don't have Xcode 16. I tested on iOS 17.6, and I only received one message. I'll circle back to this once I have Xcode 16.

smfunder commented 3 weeks ago

Hello! Yes, I experienced the same running on iOS 18. I've tried some workarounds like this one:

//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  // HACK for iOS 18: We got duplicated push. One with an uppercase identifier and the other lowercase.
  // So we report only one push.
  NSString *versionString = [[UIDevice currentDevice] systemVersion];
  if ([versionString floatValue] < 18.0 || ![self.latestPushId isEqualToString:notification.request.identifier]) {
    // Still call the JS onNotification handler so it can display the new message right away
    NSDictionary *userInfo = notification.request.content.userInfo;
    [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo
                                     fetchCompletionHandler:^void (UIBackgroundFetchResult result){}];
  }
  [self setLatestPushId:notification.request.identifier];

  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

Basically it saves the latest received identifier of the push notification and skip it if it is the same, but I've received some other non-ui pushes in-between that changed the latest identifier, like:

UI Push - Identifier: A Non-UI Push - Identifier: B (Duplicated) UI Push: Identifier A

So the push with 'B' identifier will the logic above to fail.

Another workaround, would be to have in memory the list of all the push identifiers and check if they were previously reported. But this is not a good idea because it can be a huge list in memory.

But this is more a patch than fixing the real bug.

Any other suggestions would be welcome!

Ah! This is happening when receiving the push notification while the app is in foreground.

smfunder commented 3 weeks ago

Btw, looks like to be an issue in iOS 18 Beta investigated by Apple: https://developer.apple.com/forums/thread/762412?answerId=800720022#800720022

And here is a possible patch/workaround: https://developer.apple.com/forums/thread/762126?answerId=800296022#800296022

So here is the updated code to filter out pushes that we don't use to show in the UI and to save the latest id to filter duplicated pushes:

   //Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  // HACK for iOS 18: We got duplicated push. One with an uppercase identifier and the other lowercase.
  // So we report only one push.
  // https://github.com/invertase/react-native-firebase/issues/7979
  bool shouldDisplayPush = notification.request.content.title.length > 0 && notification.request.content.body.length > 0 && [notification.request.content.badge intValue] > 0 && notification.request.content.sound != nil;
  NSString *versionString = [[UIDevice currentDevice] systemVersion];
  if ([versionString floatValue] < 18.0 || ![self.latestPushId isEqualToString:notification.request.identifier]) {
    // Still call the JS onNotification handler so it can display the new message right away
    NSDictionary *userInfo = notification.request.content.userInfo;
    [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo
                                     fetchCompletionHandler:^void (UIBackgroundFetchResult result){}];
  }
  // Save only if we should display it
  if (shouldDisplayPush) {
    [self setLatestPushId:notification.request.identifier];
  }

  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}