amplify-js: iOS: onNotificationReceivedInForeground is always triggered when Push Notification is clicked

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Authentication, GraphQL API, Push Notifications

Amplify Categories

auth, function, api, notifications

Environment information

  System:
    OS: macOS 12.6.2
    CPU: (8) x64 Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
    Memory: 34.70 MB / 16.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 18.16.0 - ~/.nvm/versions/node/v18.16.0/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 9.5.1 - ~/.nvm/versions/node/v18.16.0/bin/npm
    Watchman: 2023.02.13.00 - /usr/local/bin/watchman
  Browsers:
    Chrome: 112.0.5615.137
    Safari: 15.6.1
    Safari Technology Preview: 14.2
  npmPackages:
    @apollo/client: ^3.7.7 => 3.7.13 
    @apollo/client/cache:  undefined ()
    @apollo/client/core:  undefined ()
    @apollo/client/errors:  undefined ()
    @apollo/client/link/batch:  undefined ()
    @apollo/client/link/batch-http:  undefined ()
    @apollo/client/link/context:  undefined ()
    @apollo/client/link/core:  undefined ()
    @apollo/client/link/error:  undefined ()
    @apollo/client/link/http:  undefined ()
    @apollo/client/link/persisted-queries:  undefined ()
    @apollo/client/link/retry:  undefined ()
    @apollo/client/link/schema:  undefined ()
    @apollo/client/link/subscriptions:  undefined ()
    @apollo/client/link/utils:  undefined ()
    @apollo/client/link/ws:  undefined ()
    @apollo/client/react:  undefined ()
    @apollo/client/react/components:  undefined ()
    @apollo/client/react/context:  undefined ()
    @apollo/client/react/hoc:  undefined ()
    @apollo/client/react/hooks:  undefined ()
    @apollo/client/react/parser:  undefined ()
    @apollo/client/react/ssr:  undefined ()
    @apollo/client/testing:  undefined ()
    @apollo/client/testing/core:  undefined ()
    @apollo/client/utilities:  undefined ()
    @apollo/client/utilities/globals:  undefined ()
    @aws-amplify/rtn-push-notification: ^1.1.1 => 1.1.1 
    @aws-amplify/ui-react-native: ^1.2.14 => 1.2.14 
    @aws-sdk/client-lex-runtime-v2: ^3.262.0 => 3.321.1 (3.186.1)
    @babel/core: ^7.12.9 => 7.21.5 
    @babel/runtime: ^7.12.5 => 7.21.5 
    @formatjs/intl-datetimeformat: ^6.5.1 => 6.7.0 
    @formatjs/intl-getcanonicallocales: ^2.1.0 => 2.1.0 
    @formatjs/intl-locale: ^3.1.1 => 3.2.1 
    @formatjs/intl-numberformat: ^8.3.5 => 8.4.1 
    @formatjs/intl-pluralrules: ^5.1.10 => 5.2.1 
    @react-native-async-storage/async-storage: ^1.18.1 => 1.18.1 
    @react-native-community/eslint-config: ^2.0.0 => 2.0.0 
    @react-native-community/netinfo: ^9.3.9 => 9.3.9 
    @react-native-masked-view/masked-view: ^0.2.8 => 0.2.9 
    @react-native-picker/picker: ^2.4.8 => 2.4.10 
    @react-navigation/bottom-tabs: ^6.5.5 => 6.5.7 
    @react-navigation/native: ^6.0.14 => 6.1.6 
    @react-navigation/native-stack: ^6.9.2 => 6.9.12 
    @react-navigation/stack: ^6.3.13 => 6.3.16 
    @reduxjs/toolkit: ^1.9.3 => 1.9.5 
    @reduxjs/toolkit-query:  1.0.0 
    @reduxjs/toolkit-query-react:  1.0.0 
    @rneui/base: ^4.0.0-rc.7 => 4.0.0-rc.7 
    @rneui/themed: ^4.0.0-rc.7 => 4.0.0-rc.7 
    @rtk-query/graphql-request-base-query: ^2.2.0 => 2.2.0 
    @tsconfig/react-native: ^2.0.2 => 2.0.3 
    @types/jest: ^26.0.23 => 26.0.24 
    @types/lodash: ^4.14.191 => 4.14.194 
    @types/react: ^18.0.21 => 18.2.0 
    @types/react-native: ^0.70.6 => 0.70.13 
    @types/react-native-vector-icons: ^6.4.13 => 6.4.13 
    @types/react-test-renderer: ^18.0.0 => 18.0.0 
    @types/styled-components: ^5.1.26 => 5.1.26 
    @types/styled-components-react-native: ^5.2.1 => 5.2.1 
    @types/uuid: ^9.0.0 => 9.0.1 
    @typescript-eslint/eslint-plugin: ^5.37.0 => 5.59.1 (3.10.1)
    @typescript-eslint/parser: ^5.37.0 => 5.59.1 (3.10.1)
    HelloWorld:  0.0.1 
    amazon-cognito-identity-js: ^6.2.0 => 6.2.0 
    apollo3-cache-persist: ^0.14.1 => 0.14.1 
    aws-amplify: ^5.1.4 => 5.1.4 
    babel-jest: ^26.6.3 => 26.6.3 
    babel-plugin-module-resolver: ^5.0.0 => 5.0.0 
    date-fns: ^2.29.3 => 2.30.0 
    eslint: ^7.32.0 => 7.32.0 
    eslint-config-prettier: ^8.6.0 => 8.8.0 (6.15.0)
    eslint-plugin-import: ^2.27.5 => 2.27.5 
    eslint-plugin-prettier: ^4.2.1 => 4.2.1 (3.1.2)
    eslint-plugin-react-hooks: ^4.6.0 => 4.6.0 
    example:  0.0.1 
    graphql: ^16.6.0 => 16.6.0 (15.8.0)
    hermes-inspector-msggen:  1.0.0 
    husky: ^8.0.0 => 8.0.3 
    jest: ^26.6.3 => 26.6.3 
    lint-staged: ^13.1.0 => 13.2.2 
    lodash: ^4.17.21 => 4.17.21 
    metro-react-native-babel-preset: 0.72.3 => 0.72.3 
    prettier: ^2.8.3 => 2.8.8 
    react: ^18.2.0 => 18.2.0 
    react-intl: ^6.2.8 => 6.4.1 
    react-native: 0.70.6 => 0.70.6 
    react-native-autoheight-webview: ^1.6.5 => 1.6.5 
    react-native-config: ^1.5.0 => 1.5.0 
    react-native-device-info: ^10.4.0 => 10.6.0 
    react-native-flash-message: ^0.4.0 => 0.4.1 
    react-native-gesture-handler: ^2.9.0 => 2.9.0 
    react-native-get-random-values: ^1.8.0 => 1.8.0 
    react-native-gifted-chat: ^2.0.1 => 2.0.1 
    react-native-iap: ^12.10.0 => 12.10.5 
    react-native-linear-gradient: ^2.6.2 => 2.6.2 
    react-native-localize: ^2.2.6 => 2.2.6 
    react-native-render-html: ^6.3.4 => 6.3.4 
    react-native-safe-area-context: ^4.5.0 => 4.5.2 
    react-native-screens: ^3.18.2 => 3.20.0 
    react-native-secure-key-store: ^2.0.10 => 2.0.10 
    react-native-splash-screen: ^3.3.0 => 3.3.0 
    react-native-svg: ^13.8.0 => 13.9.0 
    react-native-svg-transformer: ^1.0.0 => 1.0.0 
    react-native-url-polyfill: ^1.3.0 => 1.3.0 
    react-native-vector-icons: ^9.2.0 => 9.2.0 
    react-native-webview: ^11.26.1 => 11.26.1 
    react-redux: ^8.0.5 => 8.0.5 
    react-test-renderer: 18.1.0 => 18.1.0 
    reactotron-react-native: ^5.0.3 => 5.0.3 
    redux-persist: ^6.0.0 => 6.0.0 
    redux-persist/integration/react:  undefined ()
    styled-components: ^5.3.9 => 5.3.10 
    styled-components/macro:  undefined ()
    styled-components/native:  undefined ()
    styled-components/primitives:  undefined ()
    typescript: ^4.8.3 => 4.9.5 
    uuid: ^9.0.0 => 9.0.0 (3.4.0, 8.3.2, 7.0.3)
  npmGlobalPackages:
    corepack: 0.17.0
    npm: 9.5.1


Describe the bug

onNotificationReceivedInForeground is triggered not only for Push notifications received in foreground iOS app. But also when:

  • Push notification is clicked when app is in background (onNotificationOpened also works)
  • Push notification is clicked when app is terminated (Notifications.Push.getLaunchNotification also works)

So I only need onNotificationReceivedInForeground if app is foreground, moreover, schemas in documentation doesn’t contains trigger of this listener for background or terminated states.

Is it expected that onNotificationReceivedInForeground triggers always?

Expected behavior

onNotificationReceivedInForeground triggers only when push notification comes when app is in foreground mode

Reproduction steps

aws-exports.js

/* eslint-disable */
// WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten.

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_mobile_analytics_app_id": "xxx",
    "aws_mobile_analytics_app_region": "us-east-1",
    "Analytics": {
        "AWSPinpoint": {
            "appId": "xxx",
            "region": "us-east-1"
        }
    },
    "Notifications": {
        "Push": {
            "AWSPinpoint": {
                "appId": "xxx",
                "region": "us-east-1"
            }
        }
    },
    "aws_appsync_graphqlEndpoint": "https://xxx/graphql",
    "aws_appsync_region": "us-east-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "xxx",
    "aws_cognito_identity_pool_id": "xxx",
    "aws_cognito_region": "us-east-1",
    "aws_user_pools_id": "xxx",
    "aws_user_pools_web_client_id": "xxx"",
    "oauth": {},
    "aws_cognito_username_attributes": [
        "EMAIL"
    ],
    "aws_cognito_social_providers": [],
    "aws_cognito_signup_attributes": [
        "NAME",
        "FAMILY_NAME"
    ],
    "aws_cognito_mfa_configuration": "OFF",
    "aws_cognito_mfa_types": [
        "SMS"
    ],
    "aws_cognito_password_protection_settings": {
        "passwordPolicyMinLength": 8,
        "passwordPolicyCharacters": []
    },
    "aws_cognito_verification_mechanisms": [
        "EMAIL"
    ]
};


export default awsmobile;

Manual configuration

No response

Additional configuration

AppDelegate.h

#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>

@property(nonatomic, strong) UIWindow *window;

@end

AppDelegate.mm

#import "AppDelegate.h"
#import "AmplifyPushNotification.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "RNSplashScreen.h"

#import <React/RCTAppSetupUtils.h>

#import <React/RCTLinkingManager.h>

#if RCT_NEW_ARCH_ENABLED
#import <React/CoreModulesPlugins.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <ReactCommon/RCTTurboModuleManager.h>

#import <react/config/ReactNativeConfig.h>

static NSString *const kRNConcurrentRoot = @"concurrentRoot";

@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
  RCTTurboModuleManager *_turboModuleManager;
  RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
  std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
  facebook::react::ContextContainer::Shared _contextContainer;
}
@end
#endif

@implementation AppDelegate

// Required for deep linking
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

// Required for Push Notifications
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  [AmplifyPushNotification didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
  [AmplifyPushNotification didReceiveRemoteNotification:userInfo withCompletionHandler:completionHandler];
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{ 
  RCTAppSetupPrepareApp(application);

  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

#if RCT_NEW_ARCH_ENABLED
  _contextContainer = std::make_shared<facebook::react::ContextContainer const>();
  _reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
  _contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
  _bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
  bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
#endif

  NSDictionary *initProps = [self prepareInitialProps];
  UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"AstuvetMobile", initProps);

  if (@available(iOS 13.0, *)) {
    rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
    rootView.backgroundColor = [UIColor whiteColor];
  }

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  [RNSplashScreen show];

  return YES;
}


/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
///
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
/// @return: `true` if the `concurrentRoot` feture is enabled. Otherwise, it returns `false`.
- (BOOL)concurrentRootEnabled
{
  // Switch this bool to turn on and off the concurrent root
  return true;
}

- (NSDictionary *)prepareInitialProps
{
  NSMutableDictionary *initProps = [NSMutableDictionary new];

#ifdef RCT_NEW_ARCH_ENABLED
  initProps[kRNConcurrentRoot] = @([self concurrentRootEnabled]);
#endif

  return initProps;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
}

#if RCT_NEW_ARCH_ENABLED

#pragma mark - RCTCxxBridgeDelegate

- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
  _turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
                                                             delegate:self
                                                            jsInvoker:bridge.jsCallInvoker];
  return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
}

#pragma mark RCTTurboModuleManagerDelegate

- (Class)getModuleClassFromName:(const char *)name
{
  return RCTCoreModulesClassProvider(name);
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                      jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
  return nullptr;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
                                                     initParams:
                                                         (const facebook::react::ObjCTurboModule::InitParams &)params
{
  return nullptr;
}

- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
  return RCTAppSetupDefaultModuleFromClass(moduleClass);
}

#endif

@end

Podfile

require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, '13.0'
install! 'cocoapods', :deterministic_uuids => false

target 'AstuvetMobile' do
  config = use_native_modules!

  # 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 => false, # false is set for debug purpose: https://github.com/facebook/react-native/issues/34624
    :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 => FlipperConfiguration.enabled,
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

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

  post_install do |installer|
    react_native_post_install(
      installer,
      # Set `mac_catalyst_enabled` to `true` in order to apply patches
      # necessary for Mac Catalyst builds
      :mac_catalyst_enabled => false
    )
    __apply_Xcode_12_5_M1_post_install_workaround(installer)
  end
end

index.js

import 'react-native-gesture-handler';
import 'react-native-get-random-values';
import 'react-native-url-polyfill/auto';

import { AppRegistry } from 'react-native';
import { Amplify, Notifications } from 'aws-amplify';

import App from './src/modules/App/App.tsx';
import { name as appName } from './app.json';

import awsconfig from './src/aws-exports';

Amplify.configure(awsconfig);
Notifications.Push.enable();

AppRegistry.registerComponent(appName, () => App);

App.tsx



const App = () => {
  useRequestNotificationsPermissions();
  useBackgroundAndForegroundNotifications();
  const launchNotificationState = useGetLaunchNotification();

  return (
    <...>
  );
};

export const useBackgroundAndForegroundNotifications = () => {
  const tokenListener = useRef<any>();
  const foregroundListener = useRef<any>();
  const notificationOpenedListener = useRef<any>();

  useEffect(() => {
    tokenListener.current =
      Notifications.Push.onTokenReceived(handleTokenReceived);

    foregroundListener.current =
      Notifications.Push.onNotificationReceivedInForeground(
        handleNotificationReceivedInForeground,
      );

    notificationOpenedListener.current =
      Notifications.Push.onNotificationOpened(handleNotificationOpened);

    return () => {
      tokenListener.current?.remove();
      foregroundListener.current?.remove();
      notificationOpenedListener.current?.remove();
    };
  }, []);
};


export const useRequestNotificationsPermissions = () => {
  useAsyncEffect(async () => {
    checkAndRequestPushNotificationsPermissions();
  }, []);
};

const checkAndRequestPushNotificationsPermissions = async () => {
  const status = await Notifications.Push.getPermissionStatus();

  if (status === 'GRANTED') {
    return;
  }

  if (status === 'DENIED') {
    // further attempts to request permissions will no longer do anything
    return;
  }

  if (status === 'SHOULD_REQUEST') {
    await Notifications.Push.requestPermissions();
  }

  if (status === 'SHOULD_EXPLAIN_THEN_REQUEST') {
    // you should display some explanation to your user before requesting permissions
    // TODO explain
    // then request permissions
    await Notifications.Push.requestPermissions();
  }
};



export const useGetLaunchNotification = (): LaunchNotificationState => {
  const { data: launchNotification, loading: loadingLaunchNotification } =
    useQuery(Notifications.Push.getLaunchNotification);

  return {
    launchNotification,
    loadingLaunchNotification,
  };
};


export default App;

Mobile Device

iPhone 14

Mobile Operating System

iOS 16.4.1

Additional information and screenshots

This video presents the following case:

  • App is terminated
  • User clicks notification and app is launched
  • getLaunchNotification is working because the first screen is a screen opened by deep link url
  • But onNotificationReceivedInForeground triggers as well, because inner notification is visible

https://user-images.githubusercontent.com/17138916/236154585-3249b6b6-8249-4688-9a18-3db22fead140.mov

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 18 (7 by maintainers)

Most upvoted comments

Not sure if it can help, but react-native-splash-screen library was preventing Notifications.Push.onNotificationOpened and Notifications.Push.getLaunchNotification from being triggered when a push notification was pressed.

Without that library it works fine… now gotta find an alternative to the splash screen 😮‍💨

@tyndria, thanks for quick the reply. If you get a chance to properly test everything for Amplify Push Notifications then let us know if the issue persists. I’m curious to hear if replacing (or removing) the react-native-splash-screen library also resolves any issues you saw on the Amplify side.

@cwomack hi!

sorry for the late response, but this is what I’ve done:

  • I’ve moved to Firebase with the same set of other libraries
  • Faced some of the functionality wasn’t working (sort of launch notification)
  • Saw an issue about ‘react-native-splash-screen’ library
  • When I’ve replaced it with another lib everything became working

Then I did the same with amplify push notifications and I stopped receiving additional inner notifications (but launch notifications weren’t working which could be just sth else, because I was in a hurry) So it seems that ‘react-native-splash-screen’ could be a problem, but I haven’t tested everything properly for amplify notifications