aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.41k stars 2.11k forks source link

iOS: onNotificationReceivedInForeground is always triggered when Push Notification is clicked #11338

Closed tyndria closed 9 months ago

tyndria commented 1 year ago

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:

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 #import @interface AppDelegate : UIResponder @property(nonatomic, strong) UIWindow *window; @end ```

AppDelegate.mm

``` #import "AppDelegate.h" #import "AmplifyPushNotification.h" #import #import #import #import "RNSplashScreen.h" #import #import #if RCT_NEW_ARCH_ENABLED #import #import #import #import #import #import #import static NSString *const kRNConcurrentRoot = @"concurrentRoot"; @interface AppDelegate () { RCTTurboModuleManager *_turboModuleManager; RCTSurfacePresenterBridgeAdapter *_bridgeAdapter; std::shared_ptr _reactNativeConfig; facebook::react::ContextContainer::Shared _contextContainer; } @end #endif @implementation AppDelegate // Required for deep linking - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)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(); _reactNativeConfig = std::make_shared(); _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)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)getTurboModule:(const std::string &)name jsInvoker:(std::shared_ptr)jsInvoker { return nullptr; } - (std::shared_ptr)getTurboModule:(const std::string &)name initParams: (const facebook::react::ObjCTurboModule::InitParams &)params { return nullptr; } - (id)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(); const foregroundListener = useRef(); const notificationOpenedListener = useRef(); 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:

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

cshfang commented 1 year ago

Hi @tyndria

The onNotificationReceivedInForeground is actually quite simple in its implementation logic. Whenever the AppDelegate's didReceiveRemoteNotification is triggered, we take a look at the app state. If the app state is active, we forward the notification to the JS layer as a foreground notification event. In other words, it should not fire in all cases, only in foreground cases.

Looking at your video, it seems like there is a second notification being displayed after the app was initially launched. This is unexpected and I'm wondering if you can help me understand where that "inner notification" is coming from.

cshfang commented 1 year ago

Hi again @tyndria

I tried building a new RN app using:

Unfortunately, I was unable to reproduce what you are seeing.

When I had my app active in the foreground:

Do you have any further details you can share with us? Would it be possible for you to share a project repo where this behavior can be seen so that we can clone it and further debug?

tyndria commented 1 year ago

@cshfang hi, big thanks for such a quick response!

Please, give me several days to give you more info. For now I'm a bit confused about this problem, because I thought I fixed it by defining an empty onNotificationReceivedInBackground (I know it sounds crazy):

Notifications.Push.onNotificationReceivedInBackground(noop);

But now this problem is just reproduced randomly, I can't catch a consistency here, so let me just recheck everything. I only have a thought that maybe it's kind of a race or asynchronicity problem (because of the fact that this bug is floating for me now, sth onNotificationReceivedInForeground invokes, sometimes doesn't).

Again, sorry for crazy ideas / solutions, I'm just fighting with push notifications for the second week

cshfang commented 1 year ago

No problem! Please keep us posted.

cwomack commented 1 year ago

@tyndria, were you able to find any way to reproduce the issue reliably or do any further testing on this?

tyndria commented 1 year ago

@cwomack hi!

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

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

cwomack commented 1 year ago

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

aalindQuantel commented 1 year ago

@tyndria what version of aws-amplify are you using as i am trying to use the latest 5.2.1 but its not working for me ?

tyndria commented 1 year ago

@aalindQuantel hi! There is Environment information in the issue description, check it, please aws-amplify version was 5.1.4

Screen Shot 2023-05-19 at 12 33 29 pm

what isn't working exactly?

aalindQuantel commented 1 year ago

@tyndria https://github.com/aws-amplify/amplify-js/issues/11388

aalindQuantel commented 1 year ago

Notifications.Push.enable(), Notifications.Push.onNotificationReceivedInBackground are not working

tyndria commented 1 year ago

@aalindQuantel sorry, this issue is about redundant events, not about absence of events

banv commented 1 year ago

I send a push notification when my app is active, onNotificationReceivedInForeground was raised => Ok. The notification also display to user, but when I click to push notification (my app still active), onNotificationOpened does not raise (instead onNotificationReceivedInForeground was raised again).

My device is Iphone 11, iOS 16.4.1. To allow iOS display notification when my app is active, here my code

//Called when a notification is delivered to a foreground app.
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}


I see the [document](https://docs.amplify.aws/lib/push-notifications/interact-with-notifications/q/platform/react-native/#onnotificationopened), it said: 

> Add onNotificationOpened listeners to respond to a push notification being opened while your app is in a foreground or background state.

But when my app in foreground (active) state, onNotificationOpened is not call.
MensurRasic commented 1 year ago

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 commented 1 year ago

Yeah, @MensurRasic try to check this library: react-native-bootsplash

cwomack commented 10 months ago

@tyndria, @aalindQuantel, or @banv, are any of you still experiencing issues with this that seem to be outside of the expected default behavior for onNotificationReceivedInForeground()? If not we'll close this issue, but I don't think there appears to be any bugs with this at this time.

tyndria commented 10 months ago

@cwomack I wasn't working closely with push notifications, but I think it could be closed if there are no other bugs thanks for you help!

cwomack commented 9 months ago

@tyndria, appreciate the follow up and we can reopen if needed. I'll close the issue for now. Thanks!