invertase / notifee

⚛️ A feature rich notifications library for React Native.
https://notifee.app
Apache License 2.0
1.84k stars 222 forks source link

Integration with react-native-firebase #154

Closed luisfuertes closed 1 year ago

luisfuertes commented 4 years ago

Hi, I have purchased a license from Notifee and am trying to integrate it with react-native-firebase messaging.

I have first added all the react-native-firebase settings.

Then i see the integration guide of your page

But it has not worked well.

On integration guide say:

import messaging from '@react-native-firebase/messaging';

function onMessageReceived(message) {
  // Do something
}

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

But on react-native-firebase say to add this on root index.js:

import messaging from '@react-native-firebase/messaging'
import AsyncStorage from '@react-native-community/async-storage'
// Register background handler
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Message handled in the background!', remoteMessage)

  let messageArray = []
  const currentMessages = await AsyncStorage.getItem('messages')
  if (currentMessages) {
    messageArray = JSON.parse(currentMessages)
  }

  messageArray.push(remoteMessage)
  await AsyncStorage.setItem('messages', JSON.stringify(messageArray))
})

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

  return <App />
}

HeadlessCheck.propTypes = {
  isHeadless: PropTypes.bool
}

AppRegistry.registerComponent(appName, () => HeadlessCheck)

With messaging().setBackgroundMessageHandler(onMessageReceived); in my App component it doesnt work.

With react-native-firebase documentation it works, but always display his own notification. And i cant catch notification press event.

I need catch background notification press, or hide it and show with notifee.

EDIT: I added this and now i can get the bg or closed app notif data

 const initialNotification = await messaging().getInitialNotification()
      if (initialNotification) {
        console.log('getInitialNotification initialNotification', initialNotification)
      }
Ehesp commented 4 years ago

Hey, what payload are you sending via FCM?

luisfuertes commented 4 years ago

Push example (Withouth data.notifee added) asads

Now my code is: In Index.js (I dont use this events for nothing, but i have to add to delete warnings)

import { AppRegistry } from 'react-native'
import { App } from './src/components/system'
import { name as appName } from './app.json'

import notifee from '@notifee/react-native'
import messaging from '@react-native-firebase/messaging'

notifee.onBackgroundEvent(async localMessage => {
  console.log('notifee setBackgroundMessageHandler localMessage', localMessage)
})

messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('messaging setBackgroundMessageHandler remoteMessage', remoteMessage)
})

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

And App.js

  _enablePushNotifications = async () => {
    try {
      // CHECK PERMISSIONS
      const hasPermission = await messaging().hasPermission()
      if (!hasPermission) {
        await messaging().requestPermission()
      }

      // REGISTER FOR REMOTE NOTIFICATIONS
      await messaging().registerDeviceForRemoteMessages()

      // CREATE DEFAULT ANDROID CHANNEL ID TO LOCAL NOTIFICATIONS
      this.channelId = await notifee.createChannel({
        id: 'default',
        name: 'Default Channel'
      })

      // GET FCM TOKEN
      const fcmToken = await messaging().getToken()
      store.dispatch(notificationsOperations.updateFCMToken(fcmToken))
      console.log('FCM fcmToken:', fcmToken)

      // SET REFRESH TOKEN EVENT LISTENER
      this.onTokenRefreshListener = messaging().onTokenRefresh(fcmToken => {
        console.log('onTokenRefresh fcmToken:', fcmToken)
        store.dispatch(notificationsOperations.updateFCMToken(fcmToken))
        store.dispatch(notificationsOperations.postDeviceToken())
      })

      // REMOTE NOTIFICATION TAPPED WITH APP CLOSED OR IN BG
      const initialRemoteNotification = await messaging().getInitialNotification()
      if (initialRemoteNotification) {
        const data = JSON.parse(_.get(initialRemoteNotification, 'data.data', {}))
        if (data) {
          this._onNotifTapped(data)
        }
      }

      // REMOTE NOTIFICATION RECEIVED WITH APP FOREGROUND
      this.onMessageListener = messaging().onMessage(message => {
        const notification = _.get(message, 'notification')
        if (notification) {
          notifee.displayNotification({
            ...message,
            ...notification,
            android: { channelId: this.channelId },
            timestamp: Date.now().toString()
          })
        }
      })

      // LOCAL NOTIFICATION TAPPED WITH APP CLOSED OR IN BG (IS NOT BEING CALLED)
      const initialLocalNotification = await notifee.getInitialNotification()
      if (initialLocalNotification) {
        console.log('notifee getInitialLocalNotification notification', initialLocalNotification.notification)
        console.log('notifee getInitialLocalNotification pressAction', initialLocalNotification.pressAction)
      }

      // LOCAL NOTIFICATIONS TAPPED WITH APP OPENED
      this.onForegroundEventListener = notifee.onForegroundEvent(({ type, detail }) => {
        console.log('onForegroundEvent type: ', type)
        console.log('onForegroundEvent detail: ', detail)
        const data = JSON.parse(_.get(detail, 'notification.data.data', {}))
        if (type === EventType.PRESS && data) {
          this._onNotifTapped(data)
        }
      })
    } catch (e) {
      console.log('_enablePushNotifications e: ', e)
    }
  }

  componentWillUnmount() {
    this.onMessageListener && this.onMessageListener()
    this.onTokenRefreshListener && this.onTokenRefreshListener()
    this.onForegroundEventListener && this.onForegroundEventListener()
  }

  _onNotifTapped = notifData => {
    console.log('_onNotifTapped notifData: ', notifData)
  }

When user press on REMOTE NOTIFICATION i can get data and event with:

const initialRemoteNotification = await messaging().getInitialNotification()

When remote notification receive on foreground i get it with messaging onMessage event, and try to send local notification to show it:

this.onMessageListener = messaging().onMessage(message => {
        const notification = _.get(message, 'notification')
        if (notification) {
          notifee.displayNotification({
            ...message,
            ...notification,
            android: { channelId: this.channelId },
            timestamp: Date.now().toString()
          })
        }
      })

QUESTION: If i send LOCAL NOTIFICATION in foreground, when local notif is tapped on background or with app closed, i need similar behavior than with REMOTE NOTIFICATIONS, but app not open and const initialLocalNotification = await notifee.getInitialNotification() isnt called

luisfuertes commented 4 years ago

New update, i forgot add to AndroidManifest.xml this prop in activity: android:launchMode="singleTop"

this causes when you press a notification with the app in the background, the event "onNotificationOpenedApp" is not called.

however the messaging().getInitialNotification() event if it was called in its place, and left the app without redux state and in the initial navigation screen instead last loaded screen before go to bg.

With singleTop prop that problems are fixed. Now i need fix when i press notifee local notification in background or with app closed, app is not opened on android. How can i fix it? I need use notifee onNotificationOpenedApp and getInitialNotification events too for local notif with app closed or with app in bg like firebase messaging

luisfuertes commented 4 years ago

With pressAction in displayNotification.android data app is opened. (Solution in this issue https://github.com/notifee/react-native-notifee/issues/31)

notifee.displayNotification({
        ...message,
        ...notification,
        android: {
          channelId: this.channelId,
          pressAction: {
            launchActivity: 'default',
            id: 'default'
          }
          //smallIcon: 'ic_notification'
        }
      })

Now all works fine, only need a notifee event to local notifications tapped on app background.

Event like messaging().onNotificationOpenedApp(remoteMessage => {}) but with Notifee

luisfuertes commented 4 years ago

Any update?

roni-castro commented 4 years ago

@Ehesp If possible, it would be nice to have an example of the full integration between react-native-firebase/messaging and react-native-notifee, because it is too difficult implement an app that works fot ALL use cases below using both these libs. We never know if one of this libs have some bugs or our app have some native problem, so I suggest the owners to create a full example that works to all use cases.

The testing cases must be:

I have got almost all cases using both the libs, but on the specific case of the application is killed/quit both the onBackgroundEvent and notifee.getInitialNotification are called and I can't use the onNotificationOpenedApp as it is never called on Android.

mikehardy commented 4 years ago

I did mostly similar to your request with react-native-firebase only in https://github.com/mikehardy/rnfbdemo/blob/master/make-demo-v6.sh but this would be a great user utility for notifee. It has a combination of copied in items and shell-scripted-edited items, it's not dogmatic about getting it done. If you want to fork that off and have react-native-firebase-notifee.sh and put in whatever you have learned so far, I could put in what I'm learning (I'm going through the integration now) and I can work with the Invertase folks to finish the rest and we'll have the ability to give anyone a "throwaway example" project at will, that's easy to keep updated

Ehesp commented 4 years ago

We do have a repo internally which does this - we'll need to rework it a bit as it's got some private stuff in there but no reason we can't make it open.

netbull commented 4 years ago

@Ehesp that will be great!

Any plans to make onBackgroundEvent work on iOS?

mikehardy commented 4 years ago

@netbull there is no way, on iOS, if the app is force quit, for onBackgroundEvent to work, it is not possible on the platform. In all other cases, it does work?

netbull commented 4 years ago

Yes, it works. It works also in background /if the FCM message is Notification + Data/. Because of this the backend should send always Notification + Data as message in order to be sure that the notification will be always displayed, but this rise the question why then do we need Notifee library?

mikehardy commented 4 years ago

Foreground Background Force-killed

These are 3 different states, you specified force-killed. Data-only doesn't work force-killed

The SDKs will pop a local notification if you send notification + data, but then you have no control - your code doesn't execute. Notification + data payload are only delivered if user interacts with the SDK-popped local notification

You may not need the Notifee library, depends on your needs. Do what you need.

netbull commented 4 years ago

@mikehardy I need /for now/ 2 types of notifications. 1- not so important notifications, like memo reminders and 2- very important notifications for chat messages. When the user press over the notification, the app should open and route him to the corresponding Memo/Chat. Most of the logic /should a notification be send or when to which device/ is handled by the backend.

If I understood correctly, I should send always Notification + Data in order to be sure that the notification will be displayed. I am right?

Ooh, one more thing.. I use the library with react-native-firebase which is great, but if I use setBackgroundMessageHandler and include the option content-available in the FCM it just awakes the App, but the setBackgroundMessageHandler it's not called :/ And on the second message the handler is triggered.

mikehardy commented 4 years ago

In a force-quit situation I believe (that should always be read as: test it!) you have to send notification payload (with or without data) for it to show up. Although of course there are still other cases where it won't be delivered (APNs throttles you, the device power throttles you, etc). But to have a chance even, I believe it needs the notification payload.

Unknown about the background message handler, that's not expected. You might check that it really is very early in the startup sequence, make sure you test release build on a real device but plugged into Xcode and watching the console. Perhaps the app takes too long to startup before the method does it's work and you got power throttled? But then the second time since it's already alive it works? Just a hunch. If so, that can vary per-device even depending on device speed. I believe (read as: test it!) that Xcode logs on a connected device will show you if the handler was timed out?

netbull commented 4 years ago

@mikehardy Yes, in the production build it works...

/**
 * @format
 */
import React from 'react';
import {AppRegistry} from 'react-native';
import messaging from '@react-native-firebase/messaging';

import notificationService from 'services/notificationService';
import {name as appName} from './app.json';
import App from './App';

messaging().setBackgroundMessageHandler(async remoteMessage => {
  await notificationService.onMessageReceived(remoteMessage);
});

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

  return <App />;
}

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

I think this code is executed as earlier as possible.. However, I continue to use Notifee to display the messages as the issue with Data only messages is solved.

Thank you for your help!

luisfuertes commented 4 years ago

And how about method like messaging().onNotificationOpenedApp(remoteMessage => {}) but with Notifee?

netbull commented 4 years ago

@luisfuertes there is such listener in Notifee.

Here is what I use: notifee.getInitialNotification() is used when the app is in killed state /not just background/ and the notification is pressed. notifee.onForegroundEvent() is used with EventType.PRESS to handle pressed notifications when the app is in background or foreground mode.

It was a bit confusing from the docs /at least for me/, but finally I made it to work, without using messaging().onNotificationOpenedApp(remoteMessage => {})

luisfuertes commented 4 years ago

But if i send a local notification with notifee, kill the app, and open app tappin local notif, i think getInitialNotification and onForegroundEvent doesnt work in that flow

netbull commented 4 years ago

notifee.getInitialNotification() works for me when the app is killed and after tapping the notification displayed by notifee.displayNotification()

luisfuertes commented 4 years ago

great i will try that

ANIBIT14 commented 4 years ago

Hi, @luisfuertes the onMessage() is never triggered for me when I trigger notifications from firebase console, is there any specific case in which it will be triggered, I used the code that you specified above to create a local notification. Hoping you can provide some insight.

axeljeremy7 commented 4 years ago

for ios has anyone try:

firebase.messaging().getAPNSToken();

and have issues triggering notifications.

mahpat16 commented 4 years ago

Any update on messaging().onNotificationOpenedApp(remoteMessage => {}) being added to Notifee. From my testing.

notifee.getInitialNotification() - is called when the app is not running at all. notifee.onForegroundEvent() - is called when the app is in foreground.

When the app is running and minimized. We get the notifee.onBackgroupEvent() - But this event does not run in the react native context. What we want is a callback that is in the recat native context so we can show a specific screen to the user. Without a callback how do we find out when the app comes back in the foreground?

mikehardy commented 4 years ago

@mahpat16 if a user taps a notification for me on android at least, with the app in the background, I do get an onBackgroundEvent with event type of "press", and since I have set the launch action to default, it pops my app to the front. As part of my onBackgroundEvent handler I also pull relevant info out of the "data" section of the notification that was tapped and I use that to navigate my app to a specific screen with specific data loading/loaded.

This sounds like what you are thinking, and it works for me. Am I perhaps misunderstanding your use case and there is a feature gap that isn't in my use case?

mahpat16 commented 4 years ago

Yes this is what I am thinking. I am missing the specific callback or event where I can put the 'navigate' code in. I hope that makes sense, i.e. if you save the relevant data in the onBackgroundEvent handler. we still need another event that gets triggered where we can act on that data and navigate. Are you using regular react state change callback?

mikehardy commented 4 years ago

I use ReSub / https://github.com/microsoft/resub - and I have a "NotificationStore". In getBackgroundHandler I push the Notification object I get from the Notifee Event into NotificationStore as an "initialNotification" (the same place I put an actual initialNotification if one opens the app...). So now I have the data stuffed away. The NotificationStore then "triggers" which causes any mounted components to re-build state if they are listening to NotificationStore.

In my "Dashboard" screen which loads by default and thus is always mounted (via react-navigation) I have subscribed to NotificationStore events, so it gets the new initialNotification to handle (if there was one) and I go into my notification handling / maybe forwarding logic

In that way it all hooks up pretty well, with the same state handler shared between actual initial notifications and background notifications even.

There are of course infinite ways to do this. Although I think it's more popular to use redux, and now everyone is using hooks so my codebase seems prehistoric 😅

mahpat16 commented 4 years ago

Thanks for the detailed explanation it certainly helps. Overall looks like you were able to leverage some of your existing triggers/event handler. And as you mentioned probably multiple other ways to achieve this. I guess adding this extra color to the notifee documentation would help the users. And I feel the original request to add a onNotificationOpenedApp still makes sense for users who dont have or want to use any other event/trigger system. Especially since FCM based notification already provides this trigger.

mikehardy commented 4 years ago

I agree it does seem like it would be a useful API to have

meliodev commented 3 years ago

Yes this is what I am thinking. I am missing the specific callback or event where I can put the 'navigate' code in. I hope that makes sense, i.e. if you save the relevant data in the onBackgroundEvent handler. we still need another event that gets triggered where we can act on that data and navigate. Are you using regular react state change callback?

Hi @mahpat16, did you manage to navigate from onBackgroundEvent handler ? I am trying to do the same thing. But from index.js, we are outside of react context. Then I couldn't find any way to invoke react-navigation. As you mentioned, onNotificationOpenedApp would be helpful to be added on notifee. Is there any work around to accomplish this ?

mikehardy commented 3 years ago

You can always navigate in react-navigation can't you? I have no problem navigating directly (call setTopLevelNavigator as part of your first app render, call navigate where ever you need it...)

import { NavigationActions } from 'react-navigation';

let _navigator: any;

function setTopLevelNavigator(navigatorRef: any): void {
  _navigator = navigatorRef;
}

function navigate(routeName: string, params: any = {}): void {
  _navigator.dispatch(
    NavigationActions.navigate({
      routeName,
      params,
    })
  );
}

// add other navigation functions that you need and export them

export default {
  navigate,
  setTopLevelNavigator,
};
meliodev commented 3 years ago

Hi @mikehardy. Thank you for your support. I really need this feature, I have just migrated to react-native-firebase v6 to use notifee. Now to explain clearly my issue. onBackgroundEvent is mounted before my first app render (as suggested by notifee documentation). That's why I could not get a reference to my navigator. Is there no issue to initialize onBackgroundEvent after rendering my app ?

Please find below my index.js

import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import { gestureHandlerRootHOC } from 'react-native-gesture-handler'

import notifee, { EventType, AndroidImportance } from '@notifee/react-native'
import messaging from '@react-native-firebase/messaging'

//background & quit state: messages listener   
messaging().setBackgroundMessageHandler(onBackgroundMessageReceived)

async function onBackgroundMessageReceived(message) {
    const channelId = await notifee.createChannel({
        id: 'projects',
        name: 'projects',
        lights: false,
        vibration: true,
        importance: AndroidImportance.DEFAULT,
    })
    await notifee.displayNotification(JSON.parse(message.data.notifee))
}

notifee.onBackgroundEvent(async ({ type, detail }) => {
    const { notification } = detail
    //const { pressAction } = notification.android

    switch (type) {
        case EventType.PRESS:
            console.log('NOTIFICATION PRESSED !')

            // I want to navigate to a sepcified screen <--- Here is the issue

            await notifee.cancelNotification(notification.id)
            break
    }
})

AppRegistry.registerComponent(appName, () => gestureHandlerRootHOC(App));
mikehardy commented 3 years ago

Now to explain clearly my issue. onBackgroundEvent is mounted before my first app render (as suggested by notifee documentation).

I think you mean registered? React components are "mounted" in a view hierarchy, but handlers / callbacks etc are just registered. But! I understand what you mean. And sure. It's possible on Android to have onBackgroundEvent fire while no App is registered because of headless mode. On iOS it's not really possible as the whole App loads, there is no background.

That's why I could not get a reference to my navigator. Is there no issue to initialize onBackgroundEvent after rendering my app ?

There is not, you will need some sort of storage state like redux or ReSub where onBackgroundEvent can store that it received something, and during app component mount you should check on your welcome screen whether anything was received that it needs to forward for

The same is true of dynamic links received in the background, notifications opening the app etc. Register handlers, have those handlers take note that they were called and store that in state, have your welcome screen check state and forward as necessary. That's the pattern I use anyway, works well for me?

meliodev commented 3 years ago

Now to explain clearly my issue. onBackgroundEvent is mounted before my first app render (as suggested by notifee documentation).

I think you mean registered? React components are "mounted" in a view hierarchy, but handlers / callbacks etc are just registered. But! I understand what you mean. And sure. It's possible on Android to have onBackgroundEvent fire while no App is registered because of headless mode. On iOS it's not really possible as the whole App loads, there is no background.

That's why I could not get a reference to my navigator. Is there no issue to initialize onBackgroundEvent after rendering my app ?

There is not, you will need some sort of storage state like redux or ReSub where onBackgroundEvent can store that it received something, and during app component mount you should check on your welcome screen whether anything was received that it needs to forward for

The same is true of dynamic links received in the background, notifications opening the app etc. Register handlers, have those handlers take note that they were called and store that in state, have your welcome screen check state and forward as necessary. That's the pattern I use anyway, works well for me?

Thank you @mikehardy now I start to understand better how things work. I will try to follow your tips ! Cheers

billnbell commented 3 years ago

Since I purchased Notifee this is still a major issue. We did find a work around, but i was not good, until you see the comment at the end of the message. I am sure this can help others.

  1. When a background message is sent we enter notifee.onBackgroundEvent() which is great. But we are in index.js and not in App.tsx. So we have no way to direct the "click" from the displayed notification to the right page.
  2. We decided to use AsyncStorage() to set something in the index.js, and have App.tsx read it and do a redirect. This works - and based on the AsyncStorage() setting we know what page to go to.

We would like Notifee to fix this issue and it is the same on Android and IOS. We cannot get onForegroundEvent to ever fire on a background notification. We see EVENT type 3, and then 1 fire inside notifee.onBackgroundEvent() when they click, and the onForegroundEvent never sees the message.

Here is the answer.

For your home page - if using React Navigation. Add this not in App.tsx, but add it into your actual page that renders when your site comes up. That was the key for us. notifee.getInitialNotification() can we called for a few seconds when site comes up.... Did not know that.

  1. User Clicks the background message.
  2. We are redirected to the Home page.
  3. Add this code on Home page (the page that you go to on click of the notification)

import {
  AppState,
} from 'react-native';
import notifee from '@notifee/react-native';
...
  const onAppStateChange = (nextAppState: string) => {
    if (nextAppState === 'active') {

      notifee.getInitialNotification().then(initialNotification => {
        console.log('getInitialNotification');
        if (initialNotification) {
          console.log(initialNotification);
        }
      }).catch((e) => {
        console.log(e);
      });
    }
  };

useEffect(() => {
    AppState.addEventListener('change', onAppStateChange);

    return () => {
      AppState.removeEventListener('change', onAppStateChange);
    };

  },[]);
github-actions[bot] commented 1 year ago

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.

pfinazzo commented 1 year ago

@meliodev

I know this post is old, but I believe you have access to AsyncStorage within the onBackgroundEvent, and so you can save the stringified notification and then in App.js grab it in a useEffect for your internal page routing

billnbell commented 1 year ago

Yeah that is what I said, but you can also just use notifee.getInitialNotification()