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

[🐛] IOS - Background notification received as foreground notification too #7836

Open
2 of 10 tasks
sanduluca opened this issue Jun 11, 2024 · 20 comments
Open
2 of 10 tasks
Labels
Needs Attention platform: ios plugin: messaging FCM only - ( messaging() ) - do not use for Notifications type: bug New bug report

Comments

@sanduluca
Copy link

sanduluca commented Jun 11, 2024

Issue

We are encountering an issue where iOS users occasionally see twice the push notifications. First time the notification is displayed by the OS as we send a push notification with the notification key (not data only). The second time the notification is displayed using notifee but the problem is that we receive the same notification in messaging().onMessage after the app is opened.

We have configured a setBackgroundMessageHandler. We send a push notification with firebase admin sdk from our backend. The notification has title, body and data, content_available is true.
We also have a listener for foreground push notification which looks like this:

useEffect(() => {
const unsubscribeFCMNotification = messaging().onMessage(message => {
    // ... some loging
    dispath(increment())

    // display the notification
    notifee.displayNotification({
        title: message.notification?.title,
        body: message.notification?.body,
        data: message.data,
        android: {
            sound: 'ding',
            channelId: 'general',
            smallIcon: 'ic_notification',
            color: 'red'
        },
        ios: {
            sound: 'ding.wav'
        }
        })
        .catch(() => {});
        });

return () => {
    // Clean up the event listeners
    unsubscribeFCMNotification();
};
}, [dispatch]);


// index.js
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
	await notifee.incrementBadgeCount()

	const displayedNotifications = await notifee.getDisplayedNotifications().catch(() => []);
	if (Platform.OS === 'ios') {
		// some async logic (workaroud for long sound on ios)
	}

	if (Platform.OS === 'andorid') {
		// some async logic (workaround: cancel older notification than X)
	}

	await keycloak.init(...)

	const { data, notification } = remoteMessage;

	if (data?.type === 'NEW_ORDER') {
		await api()
			.markOrderAsReceived({ orderId: data.orderId })
			.catch(() => {}); // <-------- here we make the http request
		return;
	}

	if (notification) {
		// If the message contains a notification property, it is automatically
		// handled by the OS when the app is not active. There is nothing you
		// can do to prevent that.
		return;
	}

	// Custom notification
	if (data?.notifee) {
		notifee.displayNotification(JSON.parse(data.notifee));
		return;
	}
});

Steps we use to reproduce (not consistently):

  1. Close the app (quit state)
  2. Send a push notification with title, body, data, content_available true
  3. Wait about 5-30 seconds (the sound ends and the notification goes to notification center) (our notification is 25s)
  4. Open the app (the notification may be shown again)

Remarks:
If you close the phone screen on step 1, the chance to reproduce it seems higher


Project Files

Javascript

Click To Expand

package.json:

{
	"name": "app",
	"version": "0.0.1",
	"private": true,
	"dependencies": {
		"@gorhom/bottom-sheet": "^4.4.7",
		"@loadsmart/rn-salesforce-chat": "^3.4.1",
		"@miblanchard/react-native-slider": "^2.6.0",
		"@notifee/react-native": "^7.8.0",
		"@react-keycloak/native": "^0.6.4",
		"@react-native-async-storage/async-storage": "^1.19.3",
		"@react-native-clipboard/clipboard": "^1.12.1",
		"@react-native-community/art": "^1.2.0",
		"@react-native-community/netinfo": "^9.4.1",
		"@react-native-firebase/analytics": "^17.4.2",
		"@react-native-firebase/app": "^17.4.2",
		"@react-native-firebase/crashlytics": "^17.4.2",
		"@react-native-firebase/messaging": "^17.4.2",
		"@react-native-picker/picker": "^2.5.0",
		"@react-navigation/drawer": "^6.6.3",
		"@react-navigation/native": "^6.1.7",
		"@react-navigation/native-stack": "^6.9.13",
		"@react-navigation/stack": "^6.3.17",
		"@reduxjs/toolkit": "^1.9.7",
		"axios": "^1.3.6",
		"dotenv": "^16.3.1",
		"formik": "^2.4.3",
		"i18next": "^23.4.6",
		"immer": "^10.0.2",
		"jwt-decode": "^3.1.2",
		"lodash": "^4.17.21",
		"lottie-ios": "4.2.0",
		"lottie-react-native": "^6.2.0",
		"moment": "^2.29.1",
		"patch-package": "^8.0.0",
		"postinstall-postinstall": "^2.1.0",
		"react": "18.2.0",
		"react-error-boundary": "^4.0.11",
		"react-i18next": "^13.2.0",
		"react-native": "0.72.12",
		"react-native-background-fetch": "^4.2.0",
		"react-native-background-geolocation": "^4.12.1",
		"react-native-barcode-builder": "^2.0.0",
		"react-native-calendars": "^1.1300.0",
		"react-native-circular-progress": "^1.3.9",
		"react-native-code-push": "^8.1.0",
		"react-native-config": "^1.5.1",
		"react-native-device-info": "^10.8.0",
		"react-native-document-scanner-plugin": "^0.9.1",
		"react-native-encrypted-storage": "^4.0.2",
		"react-native-gesture-handler": "^2.16.0",
		"react-native-get-random-values": "^1.9.0",
		"react-native-image-crop-picker": "0.38.1",
		"react-native-inappbrowser-reborn": "^3.7.0",
		"react-native-keyboard-aware-scroll-view": "^0.9.5",
		"react-native-map-link": "^2.11.2",
		"react-native-maps": "^1.7.1",
		"react-native-material-ripple": "^0.9.1",
		"react-native-modal-dropdown": "^1.0.2",
		"react-native-native-log": "^0.1.3",
		"react-native-notificated": "^0.1.5",
		"react-native-permissions": "^4.1.0",
		"react-native-picker-select": "^8.0.4",
		"react-native-reanimated": "^3.8.1",
		"react-native-safe-area-context": "^4.7.1",
		"react-native-screens": "^3.24.0",
		"react-native-simple-toast": "^3.1.0",
		"react-native-snap-carousel": "^3.9.1",
		"react-native-splash-screen": "^3.2.0",
		"react-native-svg": "^13.13.0",
		"react-native-svg-transformer": "^1.1.0",
		"react-native-swipe-list-view": "^3.2.9",
		"react-native-video": "^5.2.0",
		"react-redux": "^8.1.2",
		"reactotron-react-native": "^5.0.1",
		"reactotron-redux": "^3.1.3",
		"redux": "^4.1.0",
		"redux-persist": "^6.0.0",
		"redux-thunk": "^2.3.0",
		"reselect": "^4.1.8",
		"styled-components": "^6.0.7",
		"yarn": "^1.22.17",
		"yup": "^1.2.0"
	},
	"devDependencies": {
		"@babel/core": "^7.22.11",
		"@babel/preset-env": "^7.20.0",
		"@babel/runtime": "^7.22.11",
		"@react-native/eslint-config": "^0.72.2",
		"@react-native/metro-config": "^0.72.12",
		"@tsconfig/react-native": "^3.0.0",
		"@types/detox": "^18.1.0",
		"@types/jest": "^29.5.4",
		"@types/lodash": "^4.14.197",
		"@types/react": "^18.2.21",
		"@types/react-native": "^0.72.2",
		"@types/react-native-material-ripple": "^0.9.2",
		"@types/react-native-modal-dropdown": "^1.0.2",
		"@types/react-native-snap-carousel": "^3.8.5",
		"@types/react-native-vector-icons": "^6.4.14",
		"@types/react-native-video": "^5.0.15",
		"@types/react-test-renderer": "^18.0.0",
		"@types/styled-components": "^5.1.34",
		"@types/uuid": "^9.0.2",
		"@typescript-eslint/eslint-plugin": "^6.4.1",
		"@typescript-eslint/parser": "^6.4.1",
		"babel-jest": "^29.6.4",
		"babel-plugin-module-resolver": "^5.0.0",
		"detox": "^20.18.5",
		"eslint": "^8.48.0",
		"eslint-config-prettier": "^9.0.0",
		"eslint-import-resolver-alias": "^1.1.2",
		"eslint-import-resolver-typescript": "^3.6.0",
		"eslint-plugin-flowtype": "^8.0.3",
		"eslint-plugin-ft-flow": "^3.0.7",
		"eslint-plugin-import": "^2.28.1",
		"eslint-plugin-jest": "^27.2.3",
		"eslint-plugin-prettier": "^5.0.0",
		"eslint-plugin-react": "^7.33.2",
		"eslint-plugin-react-hooks": "^4.3.0",
		"eslint-plugin-react-native": "^4.0.0",
		"form-data": "^4.0.0",
		"husky": "^8.0.3",
		"jest": "^29.6.4",
		"metro-react-native-babel-preset": "^0.76.9",
		"prettier": "3.0.2",
		"react-devtools": "^4.28.0",
		"react-test-renderer": "18.2.0",
		"typescript": "^5.1.6",
		"typescript-styled-plugin": "^0.18.3"
	},
	"engines": {
		"node": ">=16"
	}
}

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:
# Resolve react_native_pods.rb with node to allow for hoisting
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')

min_ios_version = "13.0"

platform :ios, min_ios_version
prepare_react_native_project!

setup_permissions([
  'Camera',
])

source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/goinstant/pods-specs-public'

# 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

linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
  Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
  use_frameworks! :linkage => linkage.to_sym
end

target 'App' do

  # React Native Maps dependencies
  rn_maps_path = '../node_modules/react-native-maps'
  pod 'react-native-google-maps', :path => rn_maps_path

  config = use_native_modules!

  # use_frameworks! :linkage => :static
  # Workaround for Firebase and Flipper to work together
  # https://github.com/invertase/react-native-firebase/issues/6425#issuecomment-1527949355
  pod 'FirebaseCore', :modular_headers => true
  pod 'FirebaseCoreExtension', :modular_headers => true
  pod 'FirebaseInstallations', :modular_headers => true
  pod 'GoogleDataTransport', :modular_headers => true
  pod 'GoogleUtilities', :modular_headers => true
  pod 'nanopb', :modular_headers => true
  $RNFirebaseAsStaticFramework = true

  # Flags change depending on the env values.
  flags = get_default_flags()

  use_react_native!(
    :path => config[:reactNativePath],
    # Hermes is now enabled by default. Disable by setting this flag to false.
    # Upcoming versions of React Native may rely on get_default_flags(), but
    # we make it explicit here to aid in the React Native upgrade process.
    :hermes_enabled => flags[:hermes_enabled],
    :fabric_enabled => flags[:fabric_enabled],
    # 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}/.."
  )

  # Workaround that lets us use both react-native-maps and firebase
  # Ref: https://github.com/react-native-maps/react-native-maps/pull/4446
  $static_library = [
   'React',
   'GoogleMaps',
   'Google-Maps-iOS-Utils',
   'react-native-maps',
   'react-native-google-maps',
  ]

  # Workaround that lets us use both react-native-maps and firebase
  # Ref: https://github.com/react-native-maps/react-native-maps/pull/4446
  pre_install do |installer|
    Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {}
    installer.pod_targets.each do |pod|
      bt = pod.send(:build_type)
      if $static_library.include?(pod.name)
        puts "Overriding the build_type to static_library from static_framework for #{pod.name}"
        def pod.build_type;
          Pod::BuildType.static_library
        end
      end
    end
    installer.pod_targets.each do |pod|
      bt = pod.send(:build_type)
      puts "#{pod.name} (#{bt})"
      puts "  linkage: #{bt.send(:linkage)} packaging: #{bt.send(:packaging)}"
    end
  end

  target 'AppTests' do
    inherit! :complete
    # Pods for testing
  end

  post_install do |installer|
    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        # Disable arm64 builds for the simulator
        config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'

        current_target = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
        minimum_target = min_ios_version
        if current_target.to_f < minimum_target.to_f
          config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = minimum_target
        end

        # Workaround for having to manually assign team for certain pods
        if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
          target.build_configurations.each do |config|
              config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
          end
        end
      end
    end
    react_native_post_install(
      installer,
      config[:reactNativePath],
      :mac_catalyst_enabled => false
    )
    __apply_Xcode_12_5_M1_post_install_workaround(installer)
  end
end

AppDelegate.m:

#import "AppDelegate.h"


#import <React/RCTBundleURLProvider.h>

#import <React/RCTLinkingManager.h>

#import <Firebase.h>
#import "RNFBMessagingModule.h"

#import <CodePush/CodePush.h>
#import <GoogleMaps/GoogleMaps.h>
#import <TSBackgroundFetch/TSBackgroundFetch.h>


@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{

  ClearKeychainIfNecessary(); // react-native-encrypted-storage

  [GMSServices provideAPIKey:@""]; // google maps
  [FIRApp configure]; // firebase

  self.moduleName = @"App";
  // 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];

  bool didFinish=[super application:application didFinishLaunchingWithOptions:launchOptions];

  [UIDevice currentDevice].batteryMonitoringEnabled = true; // react-native-device-info


  return didFinish;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
  #else
    return [CodePush bundleURL];
  #endif
}


// Deep linking
- (BOOL)application:(UIApplication *)application openURL:(nonnull NSURL *)url options:(nonnull NSDictionary<NSString *,id> *)options {

  if ([RCTLinkingManager application:application openURL:url options:options]) {
    return YES;
  }

  return NO;
}

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

 OUTPUT GOES HERE
  • 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:
    • e.g. 5.4.3
  • Firebase module(s) you're using that has the issue:
    • e.g. Instance ID
  • Are you using TypeScript?
    • Y/N & VERSION


@sanduluca sanduluca added help: needs-triage Issue needs additional investigation/triaging. type: bug New bug report labels Jun 11, 2024
@sanduluca
Copy link
Author

Also had the case of getting a white screen on ios. I guess is it because of HeadlessCheck

function HeadlessCheck({ isHeadless }) {
	if (isHeadless) {
		// App has been launched in the background by iOS, ignore
		return null;
	}

	return <App />;
}
AppRegistry.registerComponent(appName, () => HeadlessCheck);

As a user, i open the app the same time the iOS launch it in background to run the setBackgroundMessageHandler

@mikehardy
Copy link
Collaborator

🤔 would be interesting to see all the handlers in play, also the JSON (or code that generates it) for the FCMs that trigger the problem - The notification has title, body and data, content_available is true. is a little vague unfortunately

Note that in all cases I'm aware of, if an iOS app is not foreground and JSON has notification key, the firebase-ios-sdk library will post a notification. So you must not post your own in that case later or you will double notify.

If you want to take control of behavior of notification posting for FCM with notification block while iOS app is in background you need to implement a notification extension helper, as documented in notifee repo

@sanduluca
Copy link
Author

sanduluca commented Jun 13, 2024

I have the both handlers posted in the first comment.
Here is how we generate the push notification on backend (java)

private Message getMessage(String token, String title, String text, Map<String, String> data, long ttl,
			String sound, String channelId) {

		Notification notification = null;
		AndroidNotification androidNotification = null;

		if (text != null || title != null) {
			notification = Notification.builder().setBody(text).setTitle(title).build();

			androidNotification = AndroidNotification.builder()
				.setBody(text)
				.setTitle(title)
				.setSound(sound)
				.setColor(COLOR)
				.setIcon(ICON)
				.setChannelId(channelId)
				.setPriority(AndroidNotification.Priority.HIGH)
				.build();
		}

		HashMap<String, String> apnsHeaders = new HashMap<>();
		apnsHeaders.put("apns-expiration", String.valueOf(ZonedDateTime.now().plusSeconds(ttl).toEpochSecond()));
		ApnsConfig apnsConfig = ApnsConfig.builder()
			.setAps(Aps.builder()
				.setContentAvailable(true)
				.setSound(sound)
				.setAlert(ApsAlert.builder().setTitle(title).setBody(text).build())
				.build())
			.putAllHeaders(apnsHeaders)
			.build();

		AndroidConfig androidConfig = AndroidConfig.builder()
			.setTtl(ttl)
			.setPriority(AndroidConfig.Priority.HIGH)
			.setNotification(androidNotification)
			.build();

		if (data == null) {
			data = new HashMap<>();
		}
		data.put("reload", "true");

		return Message.builder()
			.setNotification(notification)
			.setToken(token)
			.setAndroidConfig(androidConfig)
			.setApnsConfig(apnsConfig)
			.putAllData(data)
			.build();
	}



So you must not post your own in that case later or you will double notify.

The idea is that I dont want to post it if the the firebase sdk already posted it. But in reality the firebase sdk post it and when the user opens the app the onMessage is triggered with the same notification and i have no idea how do i know that the firebase sdk didnt showed it already. I was expecting that ones it was posted its done with it but it seems that I get a race condition somehow (the notification is not discarded from some internal queue and it sends it to onMessage listeners)

Edit: Not sure if it is important but we use 2 custom sounds. One is 1 second long and one is 27 seconds long

@matsura
Copy link

matsura commented Jun 25, 2024

Any news here - I have the same exact issue?

@camboYY
Copy link

camboYY commented Jul 18, 2024

i faced the same issue

@sourabhsharmait
Copy link

I am also facing the same issue.

@jmada
Copy link

jmada commented Aug 1, 2024

I'm facing the same problem.

@russellwheatley russellwheatley added the plugin: messaging FCM only - ( messaging() ) - do not use for Notifications label Aug 12, 2024
@Iamivan1996
Copy link

Iamivan1996 commented Aug 15, 2024

I have facing this issue before in 4 Apr, i am just using redux to save the remote message from the onNotificationOpenedApp function and then compare with the remote message from the onMessage function, if the messageId are the same, then not to show the notification when the app is opening.

After few months, my client reported that they encounter the same issue again, but when i trying to encounter the issue, everything is normal. They was just sent the push notification on the last day and open it on the next day, the notification will show again when the app is opening

However, i have update the react native version from 0.71.5 to 0.72.13 for my apps before, so it seems the issue still happening on different version. And i am using "@react-native-firebase/messaging": "18.3.0",

@russellwheatley
Copy link
Member

I just tested this, I didn't see this behaviour. Need an mcve to demonstrate this issue.

@russellwheatley russellwheatley added blocked: customer-response and removed help: needs-triage Issue needs additional investigation/triaging. labels Aug 19, 2024
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 16, 2024
@sanduluca
Copy link
Author

Didnt have time to make a mcve, but its still actual

@Deepali-Rai
Copy link

I am facing the same issue , did any one find the solution?

@xLorena
Copy link

xLorena commented Oct 15, 2024

I am facing the same issue, but I noticed this happens only on iOS 17. On iOS 16 and iOS 18 it works fine. I'm desperate. Did anyone find a workaround by any chance?

@ArunKumar-webdev
Copy link

ArunKumar-webdev commented Oct 21, 2024

I am facing the same issue, but I noticed this happens only on iOS 17. On iOS 16 and iOS 18 it works fine. I'm desperate. Did anyone find a workaround by any chance?

@xLorena same issue I am facing. iOS 18 works fine but iOS 17 has issue receiving notifications until the app is opened.

@ankitpoplify
Copy link

I am also facing the same issue. I have app in background and notification come. When I open the app after notification then onmessage fired and show foreground notification again.

@mikehardy
Copy link
Collaborator

If this is only happening on iOS 17 but working on iOS 18 / 18.1+ it sounds like a platform bug that is resolved (and iOS users typically update quickly so it should actually resolve in user base as well). This likely won't receive priority here, and a workaround such as that proposed for the recent "iOS 18 delivers message twice" issue (which resolved in iOS 18.1) may be the best way to move forward

Here is an example workaround that follows the basic idea of "let's save the last sent message, now when we get a message, is it the same as the last sent one? If it is the same skip it, if it is not, then save this one and post it". The idea may be extended to save multiple messages as needed #7979 (comment)

@ankitpoplify
Copy link

@mikehardy I can confirm that it is not working on iOS 17 and iOS 18. Because I tested it on iOS 18.0.1 and 18.1 and it is not working. I also tested it on iOS 17.6 and same results. I am aware of the saving last message workaround but again the issue is we are still struggling with background event handler issue. Sometimes when I get notifications when app is killed then background event handler didn't fired.

@mikehardy
Copy link
Collaborator

@ankitpoplify going to have to echo Russell here. This looks tough to reproduce, but certainly no progress can be made without a concrete reproduction as it is pretty subtle

#7836 (comment)

Need an MCVE

Can use https://github.com/mikehardy/rnfbdemo/blob/main/make-demo.sh to create a skeleton. Make all code fit in App.tsx, include a shell script that uses the FCM REST API to post a specific message given a firebase project API key and device token so the behavior can be triggered for inspection

@JunnieLee
Copy link

JunnieLee commented Nov 12, 2024

facing the same issue on IOS 18.2. Has anyone found a solution yet?

@JAY-Winter
Copy link

Is there anyone who handling this issue by iOS Native Code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Attention 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