Push notifications are essential for user engagement and retention. This guide covers implementation for iOS, Android, React Native, and Flutter, including rich notifications and best practices.
How Push Notifications Work
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Your App │───▶│ Your Server │───▶│ FCM/APNs │
│ (Client) │ │ (Backend) │ │ (Gateway) │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ Device │
└─────────────┘
- App registers with FCM/APNs
- App receives device token
- Token sent to your backend
- Backend sends notification via FCM/APNs
- Device receives and displays notification
Firebase Cloud Messaging (FCM)
FCM works for both Android and iOS.
Setup
React Native:
npm install @react-native-firebase/app @react-native-firebase/messaging
Flutter:
flutter pub add firebase_core firebase_messaging
Request Permission
React Native:
import messaging from '@react-native-firebase/messaging';
async function requestPermission() {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
console.log('Notification permission granted');
return true;
}
return false;
}
Flutter:
import 'package:firebase_messaging/firebase_messaging.dart';
Future<bool> requestPermission() async {
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
return settings.authorizationStatus == AuthorizationStatus.authorized;
}
Get Device Token
React Native:
async function getToken() {
// Get token
const token = await messaging().getToken();
console.log('FCM Token:', token);
// Save to backend
await saveTokenToServer(token);
// Listen for token refresh
messaging().onTokenRefresh(async newToken => {
await saveTokenToServer(newToken);
});
return token;
}
Flutter:
Future<String?> getToken() async {
final token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
// Save to backend
await saveTokenToServer(token);
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
saveTokenToServer(newToken);
});
return token;
}
Handle Notifications
React Native:
import { useEffect } from 'react';
import messaging from '@react-native-firebase/messaging';
import notifee, { AndroidImportance } from '@notifee/react-native';
// Background handler (must be outside component)
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Background message:', remoteMessage);
// Handle background notification
});
function App() {
useEffect(() => {
// Foreground messages
const unsubscribe = messaging().onMessage(async remoteMessage => {
// Display notification using notifee
await notifee.displayNotification({
title: remoteMessage.notification?.title,
body: remoteMessage.notification?.body,
android: {
channelId: 'default',
importance: AndroidImportance.HIGH,
pressAction: { id: 'default' },
},
});
});
// Notification opened app (background)
messaging().onNotificationOpenedApp(remoteMessage => {
console.log('Notification opened app:', remoteMessage);
handleNotificationNavigation(remoteMessage.data);
});
// Check if app opened from notification (quit state)
messaging()
.getInitialNotification()
.then(remoteMessage => {
if (remoteMessage) {
handleNotificationNavigation(remoteMessage.data);
}
});
return unsubscribe;
}, []);
return <YourApp />;
}
Flutter:
import 'package:firebase_messaging/firebase_messaging.dart';
// Background handler (must be top-level)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print('Background message: ${message.messageId}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
runApp(MyApp());
}
class NotificationService {
void initialize() {
// Foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Foreground message: ${message.notification?.title}');
// Show local notification
_showLocalNotification(message);
});
// Notification opened app
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('Notification opened app: ${message.data}');
_handleNotificationNavigation(message.data);
});
// Check initial notification
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {
_handleNotificationNavigation(message.data);
}
});
}
}
Backend Implementation
Node.js with Firebase Admin SDK
const admin = require('firebase-admin');
// Initialize
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
// Send to single device
async function sendNotification(token, title, body, data = {}) {
const message = {
notification: {
title,
body,
},
data,
token,
};
try {
const response = await admin.messaging().send(message);
console.log('Successfully sent:', response);
return response;
} catch (error) {
console.error('Error sending:', error);
throw error;
}
}
// Send to multiple devices
async function sendToMultiple(tokens, title, body, data = {}) {
const message = {
notification: { title, body },
data,
tokens, // Array of tokens
};
const response = await admin.messaging().sendEachForMulticast(message);
console.log(`Success: ${response.successCount}, Failure: ${response.failureCount}`);
// Handle failed tokens
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(tokens[idx]);
}
});
// Remove failed tokens from database
await removeInvalidTokens(failedTokens);
}
return response;
}
// Send to topic
async function sendToTopic(topic, title, body, data = {}) {
const message = {
notification: { title, body },
data,
topic,
};
return admin.messaging().send(message);
}
// Send with condition
async function sendWithCondition(condition, title, body) {
// Condition example: "'news' in topics && 'sports' in topics"
const message = {
notification: { title, body },
condition,
};
return admin.messaging().send(message);
}
Rich Notifications (Images, Actions)
// Android rich notification
const androidMessage = {
notification: {
title: 'New Photo',
body: 'Someone shared a photo with you',
imageUrl: 'https://example.com/image.jpg',
},
android: {
notification: {
channelId: 'photos',
priority: 'high',
defaultSound: true,
defaultVibrateTimings: true,
imageUrl: 'https://example.com/image.jpg',
actions: [
{
action: 'like',
title: 'Like',
icon: 'ic_like',
},
{
action: 'reply',
title: 'Reply',
icon: 'ic_reply',
},
],
},
},
token: deviceToken,
};
// iOS rich notification
const iosMessage = {
notification: {
title: 'New Photo',
body: 'Someone shared a photo with you',
},
apns: {
payload: {
aps: {
'mutable-content': 1,
'content-available': 1,
sound: 'default',
badge: 1,
category: 'PHOTO_CATEGORY',
},
},
fcm_options: {
image: 'https://example.com/image.jpg',
},
},
token: deviceToken,
};
// Combined platform message
const message = {
notification: {
title: 'New Message',
body: 'You have a new message',
},
android: {
notification: {
channelId: 'messages',
priority: 'high',
imageUrl: 'https://example.com/image.jpg',
},
},
apns: {
payload: {
aps: {
'mutable-content': 1,
sound: 'default',
badge: 1,
},
},
fcm_options: {
image: 'https://example.com/image.jpg',
},
},
data: {
type: 'message',
chatId: '123',
senderId: '456',
},
token: deviceToken,
};
Android Notification Channels
Android 8.0+ requires notification channels:
React Native (with Notifee):
import notifee, { AndroidImportance } from '@notifee/react-native';
async function createChannels() {
// Messages channel (high priority)
await notifee.createChannel({
id: 'messages',
name: 'Messages',
description: 'Chat message notifications',
importance: AndroidImportance.HIGH,
sound: 'message_sound',
vibration: true,
});
// Updates channel (default priority)
await notifee.createChannel({
id: 'updates',
name: 'Updates',
description: 'App updates and news',
importance: AndroidImportance.DEFAULT,
});
// Promotions channel (low priority)
await notifee.createChannel({
id: 'promotions',
name: 'Promotions',
description: 'Deals and offers',
importance: AndroidImportance.LOW,
});
}
Flutter:
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
Future<void> createNotificationChannels() async {
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const messagesChannel = AndroidNotificationChannel(
'messages',
'Messages',
description: 'Chat message notifications',
importance: Importance.high,
playSound: true,
);
const updatesChannel = AndroidNotificationChannel(
'updates',
'Updates',
description: 'App updates and news',
importance: Importance.defaultImportance,
);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(messagesChannel);
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(updatesChannel);
}
iOS-Specific Configuration
APNs Setup in Xcode
- Enable Push Notifications capability
- Enable Background Modes > Remote notifications
- Add APNs key to Firebase Console
iOS Notification Categories (Actions)
Swift (iOS Native):
import UserNotifications
func registerNotificationCategories() {
let likeAction = UNNotificationAction(
identifier: "LIKE_ACTION",
title: "Like",
options: []
)
let replyAction = UNTextInputNotificationAction(
identifier: "REPLY_ACTION",
title: "Reply",
options: [],
textInputButtonTitle: "Send",
textInputPlaceholder: "Type a message..."
)
let messageCategory = UNNotificationCategory(
identifier: "MESSAGE_CATEGORY",
actions: [likeAction, replyAction],
intentIdentifiers: [],
options: .customDismissAction
)
UNUserNotificationCenter.current().setNotificationCategories([messageCategory])
}
Topic Subscriptions
// React Native - Subscribe to topic
await messaging().subscribeToTopic('news');
await messaging().subscribeToTopic('sports');
// Unsubscribe
await messaging().unsubscribeFromTopic('news');
// Flutter
await FirebaseMessaging.instance.subscribeToTopic('news');
await FirebaseMessaging.instance.unsubscribeFromTopic('news');
Scheduled Notifications
For scheduled notifications, use local notifications:
React Native (Notifee):
import notifee, { TimestampTrigger, TriggerType } from '@notifee/react-native';
async function scheduleNotification(date, title, body) {
const trigger: TimestampTrigger = {
type: TriggerType.TIMESTAMP,
timestamp: date.getTime(),
};
await notifee.createTriggerNotification(
{
title,
body,
android: {
channelId: 'reminders',
},
},
trigger
);
}
// Schedule for specific time
await scheduleNotification(
new Date(Date.now() + 3600000), // 1 hour from now
'Reminder',
"Don't forget to check your tasks!"
);
// Repeating notification
const intervalTrigger = {
type: TriggerType.INTERVAL,
interval: 24,
timeUnit: TimeUnit.HOURS,
};
Best Practices
1. Request Permission at Right Time
// Don't request immediately on app launch
// Instead, request when user understands the value
function showNotificationPrompt() {
Alert.alert(
'Stay Updated',
'Enable notifications to get real-time updates about your orders and messages.',
[
{ text: 'Not Now', style: 'cancel' },
{
text: 'Enable',
onPress: async () => {
await requestPermission();
},
},
]
);
}
2. Handle Token Changes
// Save token with user data
async function saveTokenToServer(token) {
const userId = getCurrentUserId();
if (!userId) return;
await api.post('/users/push-token', {
userId,
token,
platform: Platform.OS,
deviceId: getUniqueDeviceId(),
});
}
// Remove token on logout
async function onLogout() {
const token = await messaging().getToken();
await api.delete('/users/push-token', { token });
await messaging().deleteToken();
}
3. Group Notifications
// Backend - Send with group key
const message = {
notification: {
title: 'New Message',
body: 'You have 5 new messages',
},
android: {
notification: {
tag: 'messages', // Groups notifications with same tag
notificationCount: 5,
},
},
token: deviceToken,
};
4. Silent Notifications for Data Sync
// Send data-only notification
const dataMessage = {
data: {
type: 'sync',
entity: 'messages',
timestamp: Date.now().toString(),
},
// No notification field = silent
token: deviceToken,
};
// Handle in app
messaging().onMessage(async message => {
if (message.data?.type === 'sync') {
// Trigger background sync
await syncData(message.data.entity);
}
});
5. Analytics and Testing
// Track notification metrics
async function trackNotificationMetrics(notificationId, action) {
await analytics.logEvent('notification_interaction', {
notification_id: notificationId,
action, // 'received', 'opened', 'dismissed'
timestamp: Date.now(),
});
}
Troubleshooting
Common Issues
-
Token not received
- Check internet connection
- Verify Firebase configuration
- iOS: Check APNs setup
-
Notification not displayed
- Android: Create notification channel
- iOS: Check permission status
- Foreground: Implement local notification display
-
Background handler not called
- Must be top-level function
- Check Firebase configuration
-
Invalid token errors
- Token expired - refresh
- App uninstalled - remove from DB
Conclusion
Push notifications are crucial for user engagement. Start with basic notifications, then add rich media, actions, and scheduling. Always respect user preferences and provide value with each notification.
Need help implementing push notifications? Contact Hevcode for professional mobile app development with push notification integration.