Skip to content

[🐛] Broken change with PR #8786 - completionHandler is called twice when native implementation exists #9049

@jdcumpson

Description

@jdcumpson

Issue

When #8786 was merged, it broke the semantics of willPresent messaging completionHandler contract.

Before

if (_originalDelegate != nil && originalDelegateRespondsTo.willPresentNotification) {
    [_originalDelegate userNotificationCenter:center
                      willPresentNotification:notification
                        withCompletionHandler:completionHandler];
  } else {
    completionHandler(presentationOptions);
  }

Now

if (_originalDelegate != nil && originalDelegateRespondsTo.willPresentNotification) {
    [_originalDelegate userNotificationCenter:center
                      willPresentNotification:notification
                        withCompletionHandler:completionHandler];
  }

  // Don't consume completionHandler before the _originalDelegate has been
  // processed
  completionHandler(presentationOptions);

The issue is that completionHandler is always called regardless of whether the original delegate calls the method.

This results in completionHandler being called twice in the case where the original app delegate overrides willPresentNotification.

The real issue for me is that this means that my willPresentNotification cannot do async work in the original delegate handler now to resolve notification options without blocking the main UI thread.

Example:

  • Original delegate implements willPresentNotification and calls completionHandler to customize the response
  • If the original delegate willPresentNotification callback is synchronous, the original delegate completionHandler call wins
  • If the original delegate willPresentNotification is async, the default completion handler is called
  • In both cases completionHandler is called twice which is incorrect

Proposals:

    1. If the original delegate implements willPresentNotification it must call completionHandler
    1. Wrap completion handler in a block, using __block BOOL handled = false, e.g./
     __block BOOL handled = NO;
    
    void (^completionHandlerProxy)(UNNotificationPresentationOptions options) = ^{
       if (handled == YES) {
          return;
       }
       handled = YES;
       completionHandler(options);
    };
    // async timeout to call 
    // 1. Define the delay in seconds
    NSTimeInterval delayInSeconds = 3.0; 
    
    __weak typeof(self) weakSelf = self;
    
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    
    if (_originalDelegate != nil && originalDelegateRespondsTo.willPresentNotification) {
      [_originalDelegate userNotificationCenter:center
                        willPresentNotification:notification
                          withCompletionHandler:completionHandlerProxy];
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_after(popTime, dispatch_get_main_queue(), ^{
          completionHandlerProxy(UNNotificationPresentationOptionsNone);
    }




<!-- Please describe your issue here --^ and provide as much detail as you can. -->
<!-- Include code snippets that show your usages of the library in the context of your project. -->
<!-- Snippets that also show how and where the library is imported in JS are useful to debug issues relating to importing or methods not found issues -->

Describe your issue here

---

## Project Files

<!-- Provide the contents of key project files which will help to debug -->
<!--     For Example: -->
<!--        - iOS: `Podfile` contents. -->
<!--        - Android: `android/build.gradle` contents. -->
<!--        - Android: `android/app/build.gradle` contents. -->
<!--        - Android: `AndroidManifest.xml` contents. -->

<!-- ADD THE CONTENTS OF THE FILES IN THE PROVIDED CODE BLOCKS BELOW -->

### Javascript

<details><summary>Click To Expand</summary>
<p>

#### `package.json`:

```json
# N/A

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:
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")

# CocoaPods 1.16.2 uses xcodeproj 1.27.0, which can read objectVersion 70
# projects but lacks the compatibility label needed when creating Pods.xcodeproj.
unless Xcodeproj::Constants::COMPATIBILITY_VERSION_BY_OBJECT_VERSION.key?(70)
  compatibility_versions = Xcodeproj::Constants::COMPATIBILITY_VERSION_BY_OBJECT_VERSION.merge(70 => 'Xcode 16.0')
  Xcodeproj::Constants.send(:remove_const, :COMPATIBILITY_VERSION_BY_OBJECT_VERSION)
  Xcodeproj::Constants.const_set(:COMPATIBILITY_VERSION_BY_OBJECT_VERSION, compatibility_versions.freeze)
end

require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}

ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']

platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
install! 'cocoapods',
  :deterministic_uuids => false

prepare_react_native_project!

target 'legacyreactnativeexpo' do
  use_expo_modules!

  if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
    config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
  else
    config_command = [
      'node',
      '--no-warnings',
      '--eval',
      'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
      'react-native-config',
      '--json',
      '--platform',
      'ios'
    ]
  end

  config = use_native_modules!(config_command)
  
  # FirebaseCoreInternal is Swift. When Firebase is integrated with static
  # linkage, CocoaPods needs a module map for GoogleUtilities.
  pod 'GoogleUtilities', :modular_headers => true


  # use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
  # use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
  use_frameworks! :linkage => :static
  $RNFirebaseAsStaticFramework = true

  use_react_native!(
    :path => config[:reactNativePath],
    :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/..",
    :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
  )

  # target 'notificationservice' do
  #   inherit! :search_paths

  #   # Add only extension-safe pods here, if any.
  #   # Example:
  #   # pod 'Firebase/Messaging'
  # end

  post_install do |installer|
    react_native_post_install(
      installer,
      config[:reactNativePath],
      :mac_catalyst_enabled => false,
      :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
    )

    # RCT-Folly's podspec sets these as compiler flags, but framework module
    # builds also need them as target-level preprocessor definitions.
    folly_definitions = %w[
      FOLLY_NO_CONFIG
      FOLLY_MOBILE=1
      FOLLY_USE_LIBCPP=1
      FOLLY_CFG_NO_COROUTINES=1
      FOLLY_HAVE_CLOCK_GETTIME=1
      FOLLY_HAVE_PTHREAD=1
    ]

    installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
        definitions = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS']
        definitions = [definitions].compact if definitions.nil? || definitions.is_a?(String)
        definitions = ['$(inherited)'] if definitions.empty?
        config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] =
          (definitions + folly_definitions).uniq
      end
    end

    Dir.glob(File.join(installer.sandbox.root.to_s, 'Target Support Files', '*', '*.xcconfig')).each do |xcconfig_path|
      lines = File.readlines(xcconfig_path)
      definition_index = lines.index { |line| line.start_with?('GCC_PREPROCESSOR_DEFINITIONS =') }

      if definition_index
        existing_definitions = lines[definition_index].split('=', 2).last.strip.split(/\s+/)
        lines[definition_index] =
          "GCC_PREPROCESSOR_DEFINITIONS = #{(existing_definitions + folly_definitions).uniq.join(' ')}\n"
      else
        lines << "GCC_PREPROCESSOR_DEFINITIONS = $(inherited) #{folly_definitions.join(' ')}\n"
      end

      File.write(xcconfig_path, lines.join)
    end

    # Xcode 26.4 / Apple Clang rejects fmt 11's consteval path. Keep the
    # workaround scoped to fmt; React Native pods still need their C++ standard.
    installer.pods_project.targets.each do |target|
      next unless target.name == 'fmt'

      target.build_configurations.each do |config|
        config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17'
      end
    end

    # This is necessary for Xcode 14, because it signs resource bundles by default
    # when building for devices.
    installer.target_installation_results.pod_target_installation_results
      .each do |pod_name, target_installation_result|
      target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
        resource_bundle_target.build_configurations.each do |config|
          config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
        end
      end
    end
  end
end

AppDelegate.m:

#import "AppDelegate.h"
#import "ExpoModulesCore-Swift.h"
#include <Foundation/NSObjCRuntime.h>
#include <UserNotifications/UNUserNotificationCenter.h>
#import <UserNotifications/UserNotifications.h>
#include <objc/objc.h>

#import "legacyreactnativeexpo-Swift.h"
#import <FirebaseCore/FirebaseCore.h>
#import <FirebaseMessaging/FirebaseMessaging.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTLog.h>

#import <Signotif/SignotifObjCBridge.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  self.moduleName = @"main";
  self.pendingPresentationHandlers = [NSMutableDictionary new];

  [FIRApp configure];

  [UNUserNotificationCenter currentNotificationCenter].delegate = self;

  RCTLogInfo(@"[AppDelegate] Setup UNUserNotificationCenter and FIRApp");

  // 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 = @{};

  return [super application:application
      didFinishLaunchingWithOptions:launchOptions];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
  return [self bundleURL];
}

- (NSURL *)bundleURL {
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings]
      jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main"
                                 withExtension:@"jsbundle"];
#endif
}

// Linking API
- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:
                (NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
  BOOL handled;

  RCTLogInfo(@"[AppDelegate] openURL %@ - %@", url, options);
  handled = [RCTLinkingManager application:application
                                   openURL:url
                                   options:options];
  if (!handled) {
    return [super application:application openURL:url options:options];
  }

  return YES;
}

// Universal Links
- (BOOL)application:(UIApplication *)application
    continueUserActivity:(nonnull NSUserActivity *)userActivity
      restorationHandler:
          (nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))
              restorationHandler {
  BOOL result = [RCTLinkingManager application:application
                          continueUserActivity:userActivity
                            restorationHandler:restorationHandler];
  return [super application:application
             continueUserActivity:userActivity
               restorationHandler:restorationHandler] ||
         result;
}

// Explicitly define remote notification delegates to ensure compatibility
// with some third-party libraries
- (void)application:(UIApplication *)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  RCTLogInfo(
      @"[AppDelegate] didRegisterForRemoteNotificationsWithDeviceToken %@",
      deviceToken);
  [FIRMessaging messaging].APNSToken = deviceToken;
  [super application:application
      didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

// Explicitly define remote notification delegates to ensure compatibility
// with some third-party libraries
- (void)application:(UIApplication *)application
    didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
  RCTLogInfo(@"[AppDelegate] Failed to register for remote notifications %@",
             error);
  [super application:application
      didFailToRegisterForRemoteNotificationsWithError:error];
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
    didReceiveNotificationResponse:(UNNotificationResponse *)response
             withCompletionHandler:(void (^)(void))completionHandler {
  RCTLogInfo(@"[AppDelegate] Received notification response %@", response);
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:
             (void (^)(UNNotificationPresentationOptions options))
                 completionHandler {

  RCTLogInfo(@"[AppDelegate] Received notification willPresent %@",
             notification);
  // UUID(void (^)(UNNotificationPresentationOptions options))
  // [SignotifObjCBridge handleWillPresentNotification:notification
  //                                 completionHandler:completionHandler];
}

// Explicitly define remote notification delegates to ensure compatibility
// with some third-party libraries
- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
          fetchCompletionHandler:
              (void (^)(UIBackgroundFetchResult))completionHandler {
  RCTLogInfo(@"[AppDelegate] Received remote notification %@", userInfo);
  // completionHandler(UIBackgroundFetchResultNoData);
  return [super application:application
      didReceiveRemoteNotification:userInfo
            fetchCompletionHandler:completionHandler];
}

@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 26.3.1
  CPU: (11) arm64 Apple M3 Pro
  Memory: 105.58 MB / 36.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 20.19.2
    path: /private/var/folders/6q/rsr9ppvj54d_7_zlk3tx_7n40000gn/T/xfs-b165d19a/node
  Yarn:
    version: 4.11.0
    path: /private/var/folders/6q/rsr9ppvj54d_7_zlk3tx_7n40000gn/T/xfs-b165d19a/yarn
  npm:
    version: 10.8.2
    path: /Users/jd/.asdf/plugins/nodejs/shims/npm
  Watchman: Not Found
Managers:
  CocoaPods:
    version: 1.16.2
    path: /Users/jd/.asdf/shims/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.4
      - iOS 26.4
      - macOS 26.4
      - tvOS 26.4
      - visionOS 26.4
      - watchOS 26.4
  Android SDK:
    API Levels:
      - "29"
      - "31"
      - "33"
      - "34"
      - "35"
    Build Tools:
      - 30.0.3
      - 31.0.0
      - 33.0.0
      - 34.0.0
      - 35.0.0
      - 36.1.0
    System Images:
      - android-33 | Google APIs ARM 64 v8a
      - android-34 | Google APIs ARM 64 v8a
      - android-35 | Google Play ARM 64 v8a
      - android-35 | Pre-Release 16 KB Page Size Google APIs ARM 64 v8a
      - android-37.0 | Pre-Release 16 KB Page Size Google Play ARM 64 v8a
      - android-VanillaIceCream | Google Play ARM 64 v8a
      - android-VanillaIceCream | Pre-Release 16 KB Page Size Google APIs ARM 64
        v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2025.3 AI-253.30387.90.2532.14935130
  Xcode:
    version: 26.4/17E192
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.14
    path: /opt/homebrew/opt/openjdk@17/bin/javac
  Ruby:
    version: 3.1.0
    path: /Users/jd/.asdf/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.1.3
    wanted: ^20.1.3
  react:
    installed: 18.3.1
    wanted: 18.3.1
  react-native:
    installed: 0.77.3
    wanted: 0.77.3
  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:
    • 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


Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions