Push notifications are an important feature of mobile applications. They keep users engaged with an app over time. Finding a reliable package to integrate this feature on iOS and Android for React Native can be tricky. As phone software updates, many React Native packages become deprecated while more packages get added over time. This happened to my previous go-to package, react-native-push-notification, which recommended Notifee or react-native-notifications as a replacement.
I landed with Notifee as my go to push notifications package for a couple reasons. The ability to create repeating reminders and connect to Firebase Remote Notifications was needed. The creators Invertase, have made some great Firebase integrations with React Native and continue to support their packages long term.
Before any notifications can be created, an app requires push notification permissions on the device. Notifee permissions require separate checks between the two platforms. The notifee.requestPermission function should request notification permissions for iOS and Android 13 or higher. Android 12 and earlier have this permission enabled by default unless a user explicitly turns it off. Android also requires a channel to assign the notification. I used the function below to return a Boolean value to check whether push notifications were allowed.
import { useCallback } from 'react'
import { Platform } from 'react-native';
import notifee, { AuthorizationStatus} from '@notifee/react-native';
const checkPermissions = async () => {
if (Platform.OS === 'ios') {
const settings = await notifee.requestPermission();
return Boolean(
settings.authorizationStatus === AuthorizationStatus.AUTHORIZED ||
settings.authorizationStatus === AuthorizationStatus.PROVISIONAL,
);
}
const settings =
Platform.OS === 'android' && Platform.Version >= 33
? await notifee.requestPermission()
: await notifee.getNotificationSettings();
const channel = await notifee.getChannel('MyChannelID');
return (
settings.authorizationStatus === AuthorizationStatus.AUTHORIZED &&
!channel?.blocked
);
};
When a user wants to enable notifications, I would check this permissions value above to determine if I should navigate the user to the app permissions screen or turn notifications on in the app.
To navigate the user to the correct permissions I have used this:
const enableReminders = async () => {
const hasPermissions = await checkPermissions();
if (hasPermissions) {
// Enable push notification settings
} else {
Alert.alert(
'Enable Notifications',
'To receive notifications opt in from your Settings.',
[{ text: 'Cancel' },{ text: 'Settings', onPress: openPermissionSettings}],
);
}
};
const openPermissionSettings = async () => {
if (Platform.OS === 'ios') {
await Linking.openSettings();
} else {
await notifee.openNotificationSettings();
}
};
Please note, these permission checks do not include Android's AlarmManager API. This would require a different logical flow. You can find more details on iOS and Android permissions, respectively.
Separating System Permissions and App Settings
For a faster, simpler implementation, it is easier to let system permissions determine if push notifications are enabled; however, this forces the user to navigate out of the app each time they would like to re-enable push notifications. An additional check can be created to control whether push notifications are enabled or disabled within an app. Instead of relying on the permissions check, we need to think through these scenarios:
Essentially, the permission and app settings must be enabled to add notifications. If permissions are removed, this also turns off the app settings. The benefit to this implementation allows the user to control notifications without needing to leave the app after permissions have been granted.
Tying this together, I used this code in order to watch for permissions with AppState.
// App State hook watches for if the app is active
import React from 'react';
import { AppState, AppStateStatus } from 'react-native';
export const useAppState = () => {
const [appState, setAppState] = React.useState<AppStateStatus>('active');
React.useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) =>
setAppState(nextAppState),
);
return () => {
subscription.remove();
};
}, []);
return {
appState,
};
};
// Using app state to check permissions
const { appState } = useAppState();
const [watchAppStateToEnable, setWatchAppStateToEnable] = useState(false);
const openPermissionSettings = useCallback(async () => {
if (Platform.OS === 'ios') {
await Linking.openSettings();
} else {
await notifee.openNotificationSettings();
}
setWatchAppStateToEnable(true);
}, []);
const checkAppStatePermissions = useCallback(async () => {
const hasPermissions = await checkPermissions();
if (hasPermissions && watchAppStateToEnable) {
// Enable in-app push notification setting
} else if (!hasPermissions) {
// Disable in-app push notification setting
}
setWatchAppState(false);
}, [checkPermissions, watchAppStateToEnable]);
useEffect(() => {
// Called each time the app is opened
if (appState === 'active') {
checkAppStatePermissions();
}
}, [appState, checkAppStatePermissions]);
In this example, we need the watchAppStateToEnable value to only enable the notifications when the user is expecting to enable them. Otherwise, it matches device settings completely. This will also disable the setting if permissions are ever revoked. Disabling reminders is much easier. notifee.cancelAllNotifications will fully delete all Notifee notifications.
const disableAllReminders = async () => {
// Disable in-app push notification setting
await notifee.cancelAllNotifications();
};
The in-app push notification setting state may be stored however you like. Once you have this setup, you can use it to determine if notifications are enabled or disabled in the app.
Trigger notifications allow users to schedule out notifications at specific timestamps or intervals.
At first, the timestamp required for creating trigger reminders tripped me up. The value just needs to be in the future from the current time. I highly recommend adding the package dayjs as it has makes handling dates in js much easier to implement.
For daily reminders, I added this condition to secure a valid timestamp for a repeating reminder from a date-picker. If the timestamp has already passed, it simply targets the selected time for tomorrow. The second(0) is added for the notification to show at the beginning of the selected minute. If a user wants to show a notification weekly, .add(1, 'week') would also work.
const validTimestamp =
dayjs(timestamp).second(0).valueOf() > new Date().getTime()
? dayjs(timestamp).second(0).valueOf()
: dayjs(timestamp).add(1, 'day').second(0).valueOf();
I created this utility function to make creating trigger notifications easier. This was created to suit my needs but can easily be customized. Please note creating many trigger notifications with iOS media attachments may affect app performance.
import notifee, { RepeatFrequency } from '@notifee/react-native';
type NotifeeTriggerReminder = {
title: string;
body: string;
timestamp: number;
id?: string;
image?: string;
repeatFrequency?: RepeatFrequency;
};
const setupNotifeeTimestampReminder = async ({ id, title, body, timestamp, image, repeatFrequency }: NotifeeTriggerReminder) => {
const idContent = id ? { id } : {};
const iOSMedia = image ? { attachments: [{ url: image }] } : {};
const androidMedia = image
? {
style: {
type: AndroidStyle.BIGPICTURE,
picture: image,
} as AndroidBigPictureStyle,
}
: {};
await notifee.createTriggerNotification(
{
...idContent,
title,
body,
android: {
channelId: ChannelId,
smallIcon: 'ic_small_icon',
pressAction: {
id: 'default',
},
...androidMedia,
},
ios: {
sound: 'default',
...iOSMedia,
},
},
{
repeatFrequency,
timestamp,
type: TriggerType.TIMESTAMP,
},
);
};
Notifee interacts well with Firebase Cloud Messaging and provides plenty of documentation. Remote notifications handled in the foreground and background are handled separately, but both need to be displayed. I added this utility function for both instances in and outside of the app.
import notifee from '@notifee/react-native';
import { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
export const onRemoteMessageReceived = async ({
notification,
messageId,
}: FirebaseMessagingTypes.RemoteMessage) => {
if (notification) {
try {
await notifee.displayNotification({
id: messageId,
title: notification.title,
body: notification.body,
android: {
channelId: 'ChannelID',
smallIcon: 'ic_small_icon',
pressAction: {
id: 'default',
},
},
ios: {
sound: 'default',
},
});
} catch (e) {
// Report Error
}
}
};
To receive notifications while the app is in the background, I added this function above the AppRegistry on the index.js file. On Android, adding the utility function created a duplicated notification bug. To prevent a duplicated push notification, I am only calling the utility function on iOS.
import { AppRegistry, Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import { onRemoteMessageReceived } from './app/utils/onRemoteMessageRecieved';
messaging().setBackgroundMessageHandler(
Platform.OS === 'ios' ? onRemoteMessageReceived : () => Promise.resolve(),
);
And then to display foreground events, I added a similar function to the App.tsx file.
useEffect(() => {
const unsubscribe = messaging().onMessage(onRemoteMessageReceived);
return unsubscribe;
}, []);
Here are a couple of limitations I have come across using Notifee and ways to work around them.
One bug on recently updated Android 13 devices may cause the app to crash. If a user creates scheduled trigger reminders on Android 12 or earlier and then updates their device to Android 13, notifee.getTriggerNotifications throws an error that needs to be handled. To bypass this, I am clearing all scheduled notifications with await notifee.cancelAllNotifications() one time on all Android 13 devices.
One limitation prevents delaying a repeating reminder on iOS. For example, I have an app with daily objectives. The app should remind users everyday unless the objective has already been completed. I originally planned to reschedule a daily repeating reminder if the objectives were completed. This works on Android but shows every day regardless on iOS.
As a workaround, I added weekly repeating reminders. On app load, these reminders all get recreated. After completing the objective, I am removing the reminder for that day of the week. It is not a perfect solution, but in this case this works fine.
const scheduleRepeatingReminder = async (timestamp: number) => {
const week = new Array(7).fill('');
for await (const [index, _day] of week.entries()) {
const dayTimestamp = dayjs(timestamp).add(index, 'day').second(0).valueOf();
const validTimestamp =
dayTimestamp > new Date().getTime()
? dayTimestamp
: dayjs(dayTimestamp).add(1, 'week').valueOf();
await setupNotifeeTimestampReminder({
id: dayjs(validTimestamp).format('ddd'),
title: 'Reminder',
body: 'This is a reminder',
timestamp: validTimestamp,
repeatFrequency: RepeatFrequency.WEEKLY,
});
}
};
const removeRepeatingReminder = async () => {
const reminderId = dayjs().format('ddd');
await notifee.cancelTriggerNotification(reminderId);
};
Using Notifee to integrate push notifications within an app can really enhance user engagement. This package provides a flexible, reliable approach for adding notifications to both iOS and Android with React Native. Thank you for taking the time to read this, if you have any suggestions or questions, please let us know!