The APNS recieved from sendbird is getting handled by OS and we want it to be handled by our explicit notifee code written on our client side in order to format the notification

We wanted both android and ios push notifications to be handled by our client code and followed sendbird’s official documentation to achieve it.
https://sendbird.com/docs/chat/v4/ios/push-notifications/configuring-preferences/register-push-notification-credentials#2-step-6-enable-multi-device-support-on-sendbird-dashboard

Fortunately we were able to achieve it for android in a certain format we wanted using notifee, but for IOS somehow it didn’t trigger any of our event handlers. ( We want it to be handled in both background and quit state in our customized format ).

The code that we wrote on client side to handle the backgroundMessageEvent is as follows:-

messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  // Request permissions (required for iOS)
  if (Platform.OS === "ios") {
    await notifee.requestPermission();
  }
  console.log("remoteMessage", remoteMessage);
  const isSendbirdNotification = Boolean(remoteMessage?.data?.sendbird);
  if (!isSendbirdNotification) return;

  const text = remoteMessage?.data?.message;
  const payload = JSON.parse(remoteMessage?.data?.sendbird);

  // Create a channel (required for Android)
  const channelId = await notifee.createChannel({
    id: `binks_chat_support`,
    name: "Binks Chat Support",
    importance: AndroidImportance.HIGH,
    vibration: true,
    vibrationPattern: [300, 500],
  });

  // display notification
  await notifee.displayNotification({
    id: `binks_chat_support`,
    title: `You have new messages!`,
    subtitle: `Total unread messages: ${payload.unread_message_count}`,
    body: text,
    // data: payload,
    android: {
      channelId,
      importance: AndroidImportance.HIGH,
    },
    ios: {
      foregroundPresentationOptions: {
        alert: true,
        badge: true,
        sound: true,
      },
    },
  });
});

We are receiving a notification but the notifications that’s getting displayed is getting handled by our OS and not by our client code written to handle and customize it before it get’s displayed.

For what i understood so far,

We have two kinds of messages that we can send:

A notification-message which is displayed and handled by the operating system directly.

A data-message which is handled by the application.

We are unaware of as to why the push message notification received is handled by the OS instead of getting handled by the client code for notifee that we have written. We want it to trigger messaging().setBackgroundMessageHandler so that we can modify it as per our needs before displaying it.

Please help us to know, what should we do to achieve our use-case.

I think so too.

messaging().getInitialNotification() is not working too. ToT

Are you sure that you should be calling notifee.requestPermission() inside the background handler? I think you should be requesting permission while the app is foregrounded.

This is the documentation that I have been using to do the same thing you want to do: Push notifications | Chat JavaScript SDK | Sendbird Docs

It looks like Notifee v7.0 is not compatible with @react-native-firebase/messaging, because Notifee intercepts the background event on iOS before RNFB does. (This also affects @react-native-community/push-notification-ios.)

I’m going to look at using Notifee for the background event handling and if I can’t get that to work, then I’ll downgrade Notifee to the latest 6.x.

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
  )
}