Notifications with Firebase
Hey team! Let’s dive into implementing push notifications in our React Native apps using Firebase Cloud Messaging (FCM). This guide will help you set up notifications that work seamlessly across iOS and Android platforms.
Overview
Firebase Cloud Messaging (FCM) is the go-to solution for sending push notifications to mobile apps. It handles the complexity of different platforms and provides a unified API for both iOS and Android.
Setup
1. Firebase Project Configuration
First, set up your Firebase project:
- Go to the Firebase Console
- Create a new project or select an existing one
- Add Android and iOS apps to your project
- Download the configuration files:
google-services.json
for AndroidGoogleService-Info.plist
for iOS
2. Install Dependencies
# For Expo managed workflow
expo install expo-notifications
expo install expo-device
expo install expo-constants
# For bare React Native
npm install @react-native-firebase/app
npm install @react-native-firebase/messaging
npm install @react-native-async-storage/async-storage
3. Android Configuration
Add to android/app/google-services.json
Place the downloaded google-services.json
file in the android/app/
directory.
Update android/build.gradle
buildscript {
dependencies {
// ... other dependencies
classpath 'com.google.gms:google-services:4.3.15'
}
}
Update android/app/build.gradle
apply plugin: 'com.google.gms.google-services'
dependencies {
// ... other dependencies
implementation 'com.google.firebase:firebase-messaging:23.2.1'
}
4. iOS Configuration
Add to iOS project
- Add
GoogleService-Info.plist
to your iOS project - Enable push notifications in your app capabilities
- Add the following to your
Info.plist
:
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
Implementation
Expo Managed Workflow
1. Configure Notifications
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export const registerForPushNotificationsAsync = async () => {
let token;
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig.extra.eas.projectId,
})).data;
} else {
alert('Must use physical device for Push Notifications');
}
if (Platform.OS === 'android') {
Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C',
});
}
return token;
};
2. React Hook
import { useState, useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { registerForPushNotificationsAsync } from './notificationUtils';
export const useNotifications = () => {
const [expoPushToken, setExpoPushToken] = useState('');
const [notification, setNotification] = useState(false);
const notificationListener = useRef();
const responseListener = useRef();
useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token));
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
setNotification(notification);
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
console.log(response);
});
return () => {
Notifications.removeNotificationSubscription(notificationListener.current);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);
return { expoPushToken, notification };
};
Bare React Native
1. Initialize Firebase
import messaging from '@react-native-firebase/messaging';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const requestUserPermission = async () => {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Authorization status:', authStatus);
return true;
}
return false;
};
export const getFCMToken = async () => {
try {
const fcmToken = await AsyncStorage.getItem('fcmToken');
if (!fcmToken) {
const token = await messaging().getToken();
if (token) {
await AsyncStorage.setItem('fcmToken', token);
return token;
}
}
return fcmToken;
} catch (error) {
console.log('Error getting FCM token:', error);
return null;
}
};
2. Handle Notifications
import messaging from '@react-native-firebase/messaging';
export const onMessageReceived = async (remoteMessage) => {
console.log('A new FCM message arrived!', JSON.stringify(remoteMessage));
// Handle foreground messages
// You can show a local notification here
};
export const onNotificationOpenedApp = (remoteMessage) => {
console.log('Notification caused app to open from background state:', remoteMessage);
// Navigate to specific screen based on notification data
if (remoteMessage.data?.screen) {
// Navigate to the specified screen
}
};
export const onNotificationReceived = (remoteMessage) => {
console.log('Notification received in background:', remoteMessage);
};
3. Setup Listeners
import { useEffect } from 'react';
import messaging from '@react-native-firebase/messaging';
import { onMessageReceived, onNotificationOpenedApp, onNotificationReceived } from './notificationHandlers';
export const useNotificationListeners = () => {
useEffect(() => {
// Foreground message handler
const unsubscribe = messaging().onMessage(onMessageReceived);
// Background message handler
messaging().setBackgroundMessageHandler(onNotificationReceived);
// App opened from background state
messaging().onNotificationOpenedApp(onNotificationOpenedApp);
// Check if app was opened from a notification
messaging()
.getInitialNotification()
.then(remoteMessage => {
if (remoteMessage) {
console.log('Notification caused app to open from quit state:', remoteMessage);
}
});
return unsubscribe;
}, []);
};
Best Practices
1. Permission Handling
- Always request permission explicitly
- Handle all permission states gracefully
- Provide clear explanations of why notifications are needed
2. Token Management
const updateFCMToken = async () => {
try {
const token = await messaging().getToken();
await AsyncStorage.setItem('fcmToken', token);
// Send token to your backend
await fetch('/api/notifications/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, userId: currentUser.id })
});
} catch (error) {
console.error('Error updating FCM token:', error);
}
};
3. Channel Configuration (Android)
import { Platform } from 'react-native';
import messaging from '@react-native-firebase/messaging';
const createNotificationChannels = () => {
if (Platform.OS === 'android') {
messaging().createChannel({
id: 'default',
name: 'Default Channel',
importance: 4,
sound: 'default',
vibration: true,
});
messaging().createChannel({
id: 'high-priority',
name: 'High Priority',
importance: 5,
sound: 'default',
vibration: true,
});
}
};
4. Error Handling
const handleNotificationError = (error) => {
switch (error.code) {
case 'messaging/permission-blocked':
console.log('User blocked notifications');
break;
case 'messaging/permission-default':
console.log('User hasn\'t made a choice yet');
break;
case 'messaging/token-registry-error':
console.log('Token registry error');
break;
default:
console.error('Notification error:', error);
}
};
Testing
Local Testing
- Use Firebase Console to send test messages
- Test on physical devices (not simulators)
- Test both foreground and background scenarios
- Verify token generation and storage
Production Testing
- Test on different devices and OS versions
- Verify notification delivery across networks
- Monitor error rates and user engagement
- Test notification actions and deep linking
Useful Links
- Firebase Cloud Messaging Documentation
- React Native Firebase Messaging
- Expo Notifications
- Firebase Console
Example Component
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert } from 'react-native';
import { useNotifications } from './hooks/useNotifications';
const NotificationComponent = () => {
const { expoPushToken, notification } = useNotifications();
const [isSubscribed, setIsSubscribed] = useState(false);
const handleSubscribe = async () => {
if (expoPushToken) {
try {
// Send token to your backend
await fetch('/api/notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: expoPushToken })
});
setIsSubscribed(true);
Alert.alert('Success', 'Subscribed to notifications!');
} catch (error) {
console.error('Error subscribing to notifications:', error);
Alert.alert('Error', 'Failed to subscribe to notifications');
}
}
};
return (
<View style=>
<Text style=>
Push Notifications
</Text>
{expoPushToken ? (
<View>
<Text style=>
Token: {expoPushToken.substring(0, 50)}...
</Text>
{!isSubscribed && (
<Button title="Subscribe to Notifications" onPress={handleSubscribe} />
)}
{isSubscribed && (
<Text style=>✅ Subscribed to notifications!</Text>
)}
</View>
) : (
<Text>Loading notification token...</Text>
)}
{notification && (
<View style=>
<Text style=>Last Notification:</Text>
<Text>{notification.request.content.title}</Text>
<Text>{notification.request.content.body}</Text>
</View>
)}
</View>
);
};
export default NotificationComponent;
Last updated: July 10, 2025
Remember to replace placeholder values with your actual Firebase configuration. Keep your service account keys secure and never commit them to version control. For production apps, consider using environment variables for sensitive configuration.