Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
2 of 10 tasks
nikitasigal opened this issue Aug 17, 2024 · 18 comments
Closed
2 of 10 tasks
Labels
platform: ios plugin: messaging FCM only - ( messaging() ) - do not use for Notifications type: bug New bug report

Comments

@nikitasigal
Copy link

nikitasigal commented Aug 17, 2024

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:

  • iPhone 12 (Physical), iOS 18.1 beta 2
  • iPhone 15 Pro (Simulator), iOS 18.1 beta 2
  • iPad mini (Simulator), iOS 18 beta 5

Devices tested, where the bug is not present:

  • iPhone 11 (Physical), iOS 16.0.2
  • iPhone X (Simulator), iOS 16.4
  • iPhone XR (Simulator), iOS 17.5

Project Files

Javascript

Click To Expand

package.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:

# N/A

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
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:

#import "AppDelegate.h"
#import "RNBootSplash.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>

#import <Firebase.h>
#import <CodePush/CodePush.h>
#import <RNFBMessagingModule.h>

@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<UIApplicationOpenURLOptionsKey,id> *)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:

// N/A

android/app/build.gradle:

// N/A

android/settings.gradle:

// N/A

MainApplication.java:

// N/A

AndroidManifest.xml:

<!-- N/A -->


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:
    • 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


@nikitasigal nikitasigal changed the title [🐛] 🔥 onMessage callback gets triggered multiple times on iOS 18.1 [🐛] 🔥 onMessage callback gets triggered multiple times on iOS 18 Aug 19, 2024
@russellwheatley
Copy link
Member

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.

@russellwheatley russellwheatley added plugin: messaging FCM only - ( messaging() ) - do not use for Notifications and removed Needs Attention labels Aug 19, 2024
@smfunder
Copy link

smfunder commented Aug 21, 2024

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
Copy link

smfunder commented Aug 21, 2024

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);
}

Copy link

Hello 👋, to help manage issues we automatically close stale issues.

This issue has been automatically marked as stale because it has not had activity for quite some time.Has this issue been fixed, or does it still require attention?

This issue will be closed in 15 days if no further activity occurs.

Thank you for your contributions.

@github-actions github-actions bot added the Stale label Sep 18, 2024
@justChris
Copy link

I have the exact same issue since I upgraded to xcode16.0 and iOS18 on my physical device. I ensured that my listener only subscribes once, but it still fires twice every time. On other devices with iOS17 it is working as expected.

@github-actions github-actions bot removed the Stale label Sep 18, 2024
@pratikliftoff
Copy link

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);
}

@smfunder Hello mate. I have pasted this code inside AppDelegate.m file and build the app. not working in my case can you please help me in this?

Thanks in Advance 🫡

@VariabileAleatoria
Copy link

I'm facing the same issue, any update?

@coyksdev
Copy link

I am also facing this issue.

macOS: 15.0.1 (24A348)
XCode: 16.0 (16A242d)

"react-native": "0.74.5",
"expo": "~51.0.37",
"@react-native-firebase/app": "21.0.0",
"@react-native-firebase/messaging": "21.0.0",

@puerschel93
Copy link

Same issue ... thats driving me crazy. I almost lost my mind because I was searching for the issue on my side the whole time.

@Ajmal0197
Copy link

Ajmal0197 commented Oct 17, 2024

I am also using ios 18 for me in iOS foreground notification was mutiple times. Had fixed using mmkv workaround.
categoryId comes from remote notification.

index.js

export const storage = new MMKV({
  id: `engage-mmkv-storage`,
  //  encryptionKey: Config.env
});

async function onMessageReceived(message) {
  console.log('onMessageReceived_🙌' + Platform.OS, message);
  const { title, body } = message.data?.title ? message.data : message.notification;

//////////-----
// Here we check to see if the last categoryId we saw is the same as the one we are currently posting
  const categoryId = `${message?.messageId}`;
  const categoryIdStored = storage.getString('categoryId');
  // If they are the same, this is the iOS 18.0 double-posting bug, just skip it
  if (categoryIdStored === categoryId) {
    return;
  }
  // if this is a new message determined by categoryId, we'll post it but remember in case it double-posts
  storage.set('categoryId', `${categoryId}`);
///////////------

  await displayNotification(title, body, `${message?.messageId}`, message?.data);

  // Extract badge count
  const badgeCount = Platform.OS === 'ios' ? message.notification?.ios?.badge : message.data?.badge;
  await addBadgeCount(badgeCount);
}

messaging().onMessage(onMessageReceived);
messaging().setBackgroundMessageHandler(onMessageReceived);

or (Note that if you don't use MMKV, you can just use a global variable)

// Use a global variable to store the last categoryId
global.lastCategoryId = null;

async function onMessageReceived(message) {
  console.log('onMessageReceived_🙌' + Platform.OS, message);
  const { title, body } = message.data?.title ? message.data : message.notification;

  const categoryId = `${message?.messageId}`;

  // Skip if this is a duplicate message
  if (global.lastCategoryId === categoryId) {
    return;
  }

  // Update the global variable with the new categoryId
  global.lastCategoryId = categoryId;

  await displayNotification(title, body, categoryId, message?.data);

  const badgeCount = Platform.OS === 'ios' ? message.notification?.ios?.badge : message.data?.badge;
  await addBadgeCount(badgeCount);
}

messaging().onMessage(onMessageReceived);
messaging().setBackgroundMessageHandler(onMessageReceived);

This global variable will only retain data during the app’s runtime, so it will be reset if the app restarts.

@xerdnu
Copy link

xerdnu commented Oct 19, 2024

I've created a temporary fix for this issue, and hope it will help react-native-firebase-ios18-fix

Check the comments in the code if you are using bare react-native (not expo)

@mikehardy
Copy link
Collaborator

@xerdnu definitely appreciate the help, however:

The problem arises because of onMessage being triggered multiple times on iOS 18 devices

That is not the problem, that is the symptom :-), what is the problem? Have you checked in to firebase-ios-sdk to see if anyone is chatting about this on their issues list? Have you tried instrumenting the native code in the messaging module here with extra log statements and then watch Console.app on a real device as it receives messages to see what is going on ?

Would be very interesting to know the actual cause - likely there is some easy fix in the code if someone has time to add the log statements and watch a message delivery to see what code is running. I'd love to merge a PR for this but haven't had time to look at it myself and won't for a while unfortunately

@xerdnu
Copy link

xerdnu commented Oct 19, 2024

@mikehardy

You are 100% correct and i did not mean to make it sound that rnfirebase is the core issue for certain. I updated the readme to make this very clear :-)

Unfortunately i dont have the time to dive into this any deeper right now as i am just in the release stage of my own app so i needed a "quick fix" for the problem that i decided to share as a temporary solution as this issue caused big problems for me on iOS 18.

Hopefully it can help someone else aswell until it gets fixed.

Best regards!

@mikehardy
Copy link
Collaborator

I actually suspect RNFB could be the core issue 🧐😅

@rawatnaresh
Copy link
Contributor

rawatnaresh commented Oct 30, 2024

This worked for me because only the last delivered notification was being displayed twice.

// This holds last processed notification
const lastDisplayedMessageIdRef = useRef < string | undefined > (undefined);

useEffect(() => {
  const unsubscribeOnMessage = messaging().onMessage(remoteMessage => {
    if (!remoteMessage) {
      return;
    }

    // Message is already processed
    if (lastDisplayedMessageIdRef.current === remoteMessage.messageId) {
      return;
    }

    lastDisplayedMessageIdRef.current === remoteMessage.messageId;

    displayNotification(remoteMessage);
  });

  return () => {
    unsubscribeOnMessage();
  };
}, []);

@VariabileAleatoria
Copy link

This worked for me because only the last delivered notification was being displayed twice.

// This holds last processed notification
const lastDisplayedMessageIdRef = useRef < string | undefined > (undefined);

useEffect(() => {
  const unsubscribeOnMessage = messaging().onMessage(remoteMessage => {
    if (!remoteMessage) {
      return;
    }

    // Message is already processed
    if (lastDisplayedMessageIdRef.current === remoteMessage.messageId) {
      return;
    }

    displayNotification(remoteMessage);
  });

  return () => {
    unsubscribeOnMessage();
  };
}, []);

you are not setting the last message id.
By the way is a useRef really needed? Why not a variable outside the component at es-module level scope?

@mikehardy
Copy link
Collaborator

mikehardy commented Oct 30, 2024

Testing on the related PR to address this in FlutterFire indicates that iOS 18.1 fixes this.
If that is the case - given that iOS users update their devices very quickly historically and there will be no devices that got 18 but cannot get 18.1, it is likely best to just close this.

If anyone tests on 18.1 and still reproduces the behavior, we can reopen

If this is of vital importance and your users are significantly stuck on iOS18 such that you can't just let it go away on it's own, I recommend a workaround such as this posted above #7979 (comment)

@ankitpoplify
Copy link

This worked for me because only the last delivered notification was being displayed twice.

// This holds last processed notification
const lastDisplayedMessageIdRef = useRef < string | undefined > (undefined);

useEffect(() => {
  const unsubscribeOnMessage = messaging().onMessage(remoteMessage => {
    if (!remoteMessage) {
      return;
    }

    // Message is already processed
    if (lastDisplayedMessageIdRef.current === remoteMessage.messageId) {
      return;
    }

    displayNotification(remoteMessage);
  });

  return () => {
    unsubscribeOnMessage();
  };
}, []);

@rawatnaresh Can you please tell us where are you saving the value in the ref?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
platform: ios plugin: messaging FCM only - ( messaging() ) - do not use for Notifications type: bug New bug report
Projects
None yet
Development

No branches or pull requests