I was able to get foreground and background notifications working with Notifee v7.0. Additionally, when a notification is tapped, the user is taken directly to that channel inside the app. I’ve shared all the relevant code below. The exported usePushNotificationHandlers()
just needs to be called once inside your app hierarchy and registerBackgroundNotifeeEventHandler()
called from index.js
.
Here is a summary of the approach:
- App in background
- iOS: Allow system to show standard push notification without any interception or local Notifee notification.
Notifee.onForegroundEvent
receives the push payload and handles it.
- Android: Use
setBackgroundMessageHandler
from react-native-firebase/messaging
to intercept the push and generate a local Notifee notification
- We only need to handle push notifications while the app is in the background because (at least for my app’s configuration) Sendbird does not send push notifications to applications with an active connection.
- App in foreground
- When the app is active, we listen to Sendbird events for new messages, then create a local notification to show the user.
- Notification is tapped
- Set up Notifee event listeners for
EventType.PRESS
using Notifee.onForegroundEvent
and Notifee.onBackgroundEvent
. When one of these events is observed, you can then navigate to the channelUrl
in the local notification payload.
import { useEffect } from 'react'
import { Platform } from 'react-native'
import Notifee, {
AndroidImportance,
Event as AndroidNotificationEvent,
EventType,
Notification as NotifeeNotification,
} from '@notifee/react-native'
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging'
import { getFocusedRouteNameFromRoute } from '@react-navigation/native'
import { SendbirdDataPayload } from '@sendbird/uikit-utils'
import { navigationActions, navigationRef } from '@navigation/navigationContainer'
import { GroupChannelParams } from '@screens'
import { selectApiRequestsEnabled, store } from '@store'
/**
* Sets up foreground and background handlers for push notifications from Sendbird.
* Note that Sendbird is configured to not send push notifications to app instances
* that are in the foreground. For new message events in the foreground, we use the Sendbird groupChannelHandler.
*/
export const usePushNotificationHandlers = () => {
useEffect(() => {
const onMessageReceived = async (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
const payload = getSendbirdPayloadFromRemoteMessage(remoteMessage)
console.log('on message received')
if (payload && shouldShowNotificationForPayload(payload)) {
await showLocalNotificationForSendbirdPayload(payload)
}
}
// On iOS, this approach is deprecated in favor of the onForegroundEvent handler below
if (Platform.OS === 'android') {
// Check and handle initial notification when tapping the notification launched app from quit state
handleInitialNotification().catch(console.error)
// Convert foreground push notifications to local notifications
messaging().onMessage(onMessageReceived)
// Convert background push notifications to local notifications
messaging().setBackgroundMessageHandler(onMessageReceived)
}
}, [])
useEffect(() => {
// The foreground event handler is used on iOS to get the event that triggered the app to open
// This is not intended to handle notifications received when the app is already open, because Sendbird
// doesn't send push notifications when the app is in the foreground.
const unsubscribe = Notifee.onForegroundEvent(handleNotifeeOnPress)
return () => {
unsubscribe()
}
}, [])
}
export const shouldShowNotificationForPayload = (payload: MinimalSendbirdDataPayload) => {
const currentRoute = navigationRef.current?.getCurrentRoute()
if (!currentRoute) return true
const routeName = getFocusedRouteNameFromRoute(currentRoute) ?? currentRoute.name
console.log('shouldShowNotificationForPayload', routeName, currentRoute.params)
switch (routeName) {
case 'GroupChannel':
return (currentRoute.params as GroupChannelParams).channelUrl !== payload.channelUrl
default:
return true
}
}
const handleNotifeeOnPress = async (event: AndroidNotificationEvent) => {
const channelUrl = getChannelUrlFromEvent(event)
if (channelUrl && event.type === EventType.PRESS) {
console.log('Notifee on press event, will navigate to', channelUrl)
navigateToChat(channelUrl)
}
}
const handleInitialNotification = async () => {
const initialNotification = await Notifee.getInitialNotification()
if (!initialNotification) return
const payload = getSendbirdPayloadFromNotification(initialNotification.notification)
if (!payload) return
console.log('handleInitialNotification will navigate to', payload.channelUrl)
navigateToChat(payload.channelUrl)
}
const navigateToChat = (channelUrl: string) => {
const state = store.getState()
const isAuthenticated = selectApiRequestsEnabled(state)
if (!isAuthenticated) return
const navigatedAt = new Date().toISOString()
const currentRoute = navigationRef.current?.getCurrentRoute()?.name ?? ''
switch (currentRoute) {
case 'GroupChannelList':
case 'GroupChannel':
// If we're already on the channel list or channel screen, go directly to the channel screen
navigationActions.navigate('GroupChannel', { channelUrl, navigatedAt })
break
default:
// If the ChatTab hasn't yet been visited during the current app session, then it hasn't been loaded into memory and the
// root navigator is unaware of its child screens. So we need to navigate to the ChatTab first, then to the GroupChannelList.
navigationActions.navigate('ChatTab', {
screen: 'GroupChannelList',
params: { channelUrl, navigatedAt },
})
}
}
/**
* The associated data may either come from a push notification or a Sendbird event, hence the different types.
*/
const getChannelUrlFromEvent = (event: AndroidNotificationEvent): string | undefined => {
if (isMinimalSendbirdDataPayload(event.detail.notification?.data)) {
return event.detail.notification?.data?.channelUrl
} else {
const payload = getSendbirdPayloadFromEvent(event)
return payload?.channelUrl
}
}
/**
* Notifee recommends registering the background handler as soon as possible in the app lifecycle, i.e. index.js.
* Note that this is for responding to background events on local Notifee notifications, not remote push notifications.
*/
export const registerBackgroundNotifeeEventHandler = () => {
Notifee.onBackgroundEvent(handleNotifeeOnPress)
}
const SendbirdChatChannelId = 'SENDBIRD_CHAT_CHANNEL_ID'
export const showLocalNotificationForSendbirdPayload = async (payload: MinimalSendbirdDataPayload) => {
const title = payload.senderName ? `New message from ${payload.senderName}` : 'New message'
const channelId = await Notifee.createChannel({
id: SendbirdChatChannelId,
name: 'Chat Notifications',
importance: AndroidImportance.HIGH,
})
console.log('Will display notifee notification')
await Notifee.displayNotification({
id: payload.messageId.toString(),
title,
// no body for privacy reasons
data: payload,
android: {
channelId,
smallIcon: 'notification_icon',
importance: AndroidImportance.HIGH,
pressAction: {
id: 'default',
},
},
ios: {
foregroundPresentationOptions: {
alert: true,
badge: true,
sound: true,
},
},
})
}
// Utility functions for extracting typed Sendbird data from events and notifications
const getSendbirdPayloadFromEvent = (
event: AndroidNotificationEvent,
): MinimalSendbirdDataPayload | undefined => {
// Sendbird wraps the payload in a `sendbird` object on iOS, but not on Android
const sendbirdData =
Platform.OS === 'android'
? event.detail.notification?.data
: event.detail.notification?.data?.sendbird
return isSendbirdPayload(sendbirdData) ? minimizePayload(sendbirdData) : undefined
}
const getSendbirdPayloadFromNotification = (
notification: NotifeeNotification,
): MinimalSendbirdDataPayload | undefined => {
const payload = notification?.data?.sendbird
return isSendbirdPayload(payload) ? minimizePayload(payload) : undefined
}
const getSendbirdPayloadFromRemoteMessage = (
remoteMessage: FirebaseMessagingTypes.RemoteMessage,
): MinimalSendbirdDataPayload | undefined => {
const sendbirdString = remoteMessage?.data?.sendbird
if (!sendbirdString) return undefined
const payload = JSON.parse(sendbirdString)
return isSendbirdPayload(payload) ? minimizePayload(payload) : undefined
}
export type MinimalSendbirdDataPayload = {
messageId: string
channelUrl: string
senderName?: string
}
const isMinimalSendbirdDataPayload = (object: any | undefined): object is MinimalSendbirdDataPayload => {
return !!object?.messageId && !!object?.channelUrl
}
/**
* Notifee validation inside displayNotification() doesn't like null values or arrays,
* so only include necessary fields in payload for Notifee
*/
const minimizePayload = (payload: SendbirdDataPayload): MinimalSendbirdDataPayload | undefined => {
if (!payload.channel?.channel_url) return undefined
return {
messageId: payload.message_id.toString(),
channelUrl: payload.channel.channel_url,
senderName: payload.sender?.name,
}
}
const isSendbirdPayload = (object: any | undefined): object is SendbirdDataPayload => {
if (!object) return false
return (
!!object.message_id &&
!!object.channel &&
!!object.channel.channel_url &&
!!object.sender &&
!!object.sender.name
)
}