A comprehensive guide to implementing push notifications in your mobile app using Firebase Cloud Messaging (FCM)
Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably send notifications at no cost. Using FCM, you can notify a client app that new email or other data is available to sync. You can send notification messages to drive user re-engagement and retention.
Before implementing FCM, ensure you have:
First, you need to add Firebase to your project. The process varies depending on your platform:
google-services.json file and place it in your app's module directorydependencies {
// Add the Firebase SDK for Google Analytics
implementation 'com.google.firebase:firebase-analytics:21.3.0'
// Add the Firebase Cloud Messaging SDK
implementation 'com.google.firebase:firebase-messaging:23.2.0'
}
And at the bottom of the file, add:
apply plugin: 'com.google.gms.google-services'
In your project-level build.gradle file, add:
buildscript {
repositories {
google()
// ...
}
dependencies {
// ...
classpath 'com.google.gms:google-services:4.3.15'
}
}
GoogleService-Info.plist filepod 'Firebase/Analytics'
pod 'Firebase/Messaging'
Then run:
pod install
In your AppDelegate, initialize Firebase:
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
<!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging.js"></script>
<script>
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "your-api-key",
authDomain: "your-auth-domain",
projectId: "your-project-id",
storageBucket: "your-storage-bucket",
messagingSenderId: "your-messaging-sender-id",
appId: "your-app-id"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
Before you can send notifications to a device, you need to request permission from the user and get an FCM token.
In your application class or main activity:
import com.google.firebase.messaging.FirebaseMessaging;
// Get FCM token
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.w(TAG, "Fetching FCM registration token failed", task.getException());
return;
}
// Get new FCM registration token
String token = task.getResult();
// Log and toast
Log.d(TAG, "FCM Token: " + token);
// Send token to your server
sendRegistrationToServer(token);
});
In your AppDelegate:
import Firebase
import FirebaseMessaging
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
// Request permission for notifications
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { _, _ in }
)
application.registerForRemoteNotifications()
// Set messaging delegate
Messaging.messaging().delegate = self
return true
}
// Receive FCM token
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
// Send token to your server
if let token = fcmToken {
sendRegistrationToServer(token)
}
}
}
// Request permission
const messaging = firebase.messaging();
messaging.requestPermission()
.then(() => {
console.log('Notification permission granted.');
return messaging.getToken();
})
.then((token) => {
console.log('FCM Token:', token);
// Send token to your server
sendTokenToServer(token);
})
.catch((err) => {
console.log('Unable to get permission to notify.', err);
});
First, add the Firebase Messaging package:
flutter pub add firebase_messaging
Then in your app:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void initState() {
super.initState();
setupFirebaseMessaging();
}
Future setupFirebaseMessaging() async {
// Request permission
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
print('User granted permission: ${settings.authorizationStatus}');
// Get token
String? token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
// Send token to server
if (token != null) {
sendTokenToServer(token);
}
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
print('FCM Token refreshed: $newToken');
sendTokenToServer(newToken);
});
}
void sendTokenToServer(String token) {
// Implement your server communication here
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FCM Example')),
body: Center(child: Text('Firebase Cloud Messaging Example')),
),
);
}
}
First, install the required packages:
npm install @react-native-firebase/app @react-native-firebase/messaging
Then in your app:
import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
function App() {
useEffect(() => {
// Request permission
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);
getFcmToken();
}
}
// Get FCM token
const getFcmToken = async () => {
const fcmToken = await messaging().getToken();
if (fcmToken) {
console.log('FCM Token:', fcmToken);
// Send token to your server
sendTokenToServer(fcmToken);
}
}
requestUserPermission();
// Listen to token refresh
const unsubscribe = messaging().onTokenRefresh(token => {
console.log('FCM Token refreshed:', token);
sendTokenToServer(token);
});
return unsubscribe;
}, []);
const sendTokenToServer = (token) => {
// Implement your server communication here
};
return (
// Your app component
);
}
export default App;
Now that you have set up FCM and obtained a token, you need to handle incoming messages.
Create a service that extends FirebaseMessagingService:
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// Check if message contains a notification payload
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
// Show notification
sendNotification(remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody());
}
// Check if message contains a data payload
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
// Handle data payload
handleDataMessage(remoteMessage.getData());
}
}
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
// Send token to your app server
sendRegistrationToServer(token);
}
private void sendNotification(String title, String messageBody) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE);
String channelId = "fcm_default_channel";
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
"Channel human readable title",
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(0, notificationBuilder.build());
}
private void handleDataMessage(Map data) {
// Process data message here
}
private void sendRegistrationToServer(String token) {
// Implement this method to send token to your app server
}
}
Don't forget to register the service in your AndroidManifest.xml:
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
In your AppDelegate, implement the UNUserNotificationCenterDelegate methods:
// Handle notification when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
// Change this to your preferred presentation option
completionHandler([[.alert, .sound]])
}
// Handle notification when user taps on it
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler()
}
// Handle receiving message
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
// Handle foreground messages
messaging.onMessage((payload) => {
console.log('Message received in foreground:', payload);
// Display notification
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/firebase-logo.png'
};
if (Notification.permission === 'granted') {
new Notification(notificationTitle, notificationOptions);
}
});
// Handle background messages
// This requires a service worker
// Create a file named firebase-messaging-sw.js in your web root:
/*
// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging.js');
firebase.initializeApp({
apiKey: "your-api-key",
authDomain: "your-auth-domain",
projectId: "your-project-id",
storageBucket: "your-storage-bucket",
messagingSenderId: "your-messaging-sender-id",
appId: "your-app-id"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
console.log('Background message received:', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/firebase-logo.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
*/
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// Define a top-level function to handle background messages
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
@override
void initState() {
super.initState();
setupFirebaseMessaging();
setupLocalNotifications();
}
Future setupFirebaseMessaging() async {
// Set the background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print("Got a message whilst in the foreground!");
print("Message data: ${message.data}");
if (message.notification != null) {
print("Message also contained a notification: ${message.notification}");
showNotification(message);
}
});
// Handle when the app is opened from a notification
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('A new onMessageOpenedApp event was published!');
// Navigate to relevant page based on message data
handleNotificationClick(message.data);
});
// Check if the app was opened from a notification
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
handleNotificationClick(initialMessage.data);
}
}
void setupLocalNotifications() {
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
final IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings();
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String? payload) async {
if (payload != null) {
// Handle notification click with payload
Map data = json.decode(payload);
handleNotificationClick(data);
}
});
}
void showNotification(RemoteMessage message) async {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null) {
await flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'channel_description',
importance: Importance.max,
priority: Priority.high,
showWhen: false,
),
iOS: IOSNotificationDetails(),
),
payload: json.encode(message.data),
);
}
}
void handleNotificationClick(Map data) {
// Navigate to relevant page based on notification data
print('Handling notification click with data: $data');
// Example: Navigator.pushNamed(context, '/details', arguments: data);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FCM Example')),
body: Center(child: Text('Firebase Cloud Messaging Example')),
),
);
}
}
import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import PushNotification from 'react-native-push-notification';
function App() {
useEffect(() => {
// Configure local notifications
PushNotification.configure({
onNotification: function (notification) {
console.log('NOTIFICATION:', notification);
// Process the notification
// Required on iOS only
notification.finish(PushNotificationIOS.FetchResult.NoData);
},
// IOS ONLY
permissions: {
alert: true,
badge: true,
sound: true,
},
popInitialNotification: true,
requestPermissions: true,
});
// Create notification channel for Android
PushNotification.createChannel(
{
channelId: "fcm_default_channel",
channelName: "Default Channel",
channelDescription: "A default channel for notifications",
soundName: "default",
importance: 4,
vibrate: true,
},
(created) => console.log(`Channel created: ${created}`)
);
// Handle foreground messages
const unsubscribe = messaging().onMessage(async remoteMessage => {
console.log('A new FCM message arrived!', remoteMessage);
// Display local notification
PushNotification.localNotification({
channelId: "fcm_default_channel",
title: remoteMessage.notification?.title,
message: remoteMessage.notification?.body || '',
data: remoteMessage.data,
});
});
// Handle background/quit state messages
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});
// 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,
);
// Navigate based on the notification data
handleNotificationOpen(remoteMessage);
}
});
// Handle notification open when app is in background
messaging().onNotificationOpenedApp(remoteMessage => {
console.log(
'Notification caused app to open from background state:',
remoteMessage,
);
// Navigate based on the notification data
handleNotificationOpen(remoteMessage);
});
return unsubscribe;
}, []);
const handleNotificationOpen = (remoteMessage) => {
// Navigate to relevant screen based on notification data
console.log('Handling notification open with data:', remoteMessage.data);
// Example: navigation.navigate('Details', { id: remoteMessage.data.id });
};
return (
// Your app component
);
}
export default App;
When a client app registers with FCM, it receives a registration token. You should store this token on your server to send messages to specific devices.
// Example server code (Node.js with Express and MongoDB)
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// Define a schema for storing FCM tokens
const tokenSchema = new mongoose.Schema({
userId: { type: String, required: true },
token: { type: String, required: true },
platform: { type: String, enum: ['android', 'ios', 'web'], required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
const Token = mongoose.model('Token', tokenSchema);
// API endpoint to register/update FCM token
app.post('/api/register-token', async (req, res) => {
try {
const { userId, token, platform } = req.body;
// Validate input
if (!userId || !token || !platform) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Update token if it exists, otherwise create a new one
const result = await Token.findOneAndUpdate(
{ userId, platform },
{ token, updatedAt: Date.now() },
{ upsert: true, new: true }
);
res.status(200).json({ success: true, data: result });
} catch (error) {
console.error('Error registering token:', error);
res.status(500).json({ error: 'Server error' });
}
});
You can send messages to devices using the FCM HTTP v1 API or the Firebase Admin SDK.
Using the Firebase Admin SDK:
const admin = require('firebase-admin');
const serviceAccount = require('./path/to/serviceAccountKey.json');
// Initialize the app with a service account
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
// Send a message to a specific device
async function sendNotificationToDevice(token, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
token
};
const response = await admin.messaging().send(message);
console.log('Successfully sent message:', response);
return response;
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}
// Send a message to multiple devices
async function sendNotificationToDevices(tokens, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
tokens // Array of tokens
};
const response = await admin.messaging().sendMulticast(message);
console.log(`${response.successCount} messages were sent successfully`);
return response;
} catch (error) {
console.error('Error sending messages:', error);
throw error;
}
}
// Send a message to a topic
async function sendNotificationToTopic(topic, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
topic
};
const response = await admin.messaging().send(message);
console.log('Successfully sent message to topic:', response);
return response;
} catch (error) {
console.error('Error sending message to topic:', error);
throw error;
}
}
// Example API endpoint to send notification
app.post('/api/send-notification', async (req, res) => {
try {
const { userId, title, body, data } = req.body;
// Get the user's FCM token from the database
const tokenDoc = await Token.findOne({ userId });
if (!tokenDoc) {
return res.status(404).json({ error: 'User token not found' });
}
const response = await sendNotificationToDevice(tokenDoc.token, title, body, data);
res.status(200).json({ success: true, response });
} catch (error) {
console.error('Error sending notification:', error);
res.status(500).json({ error: 'Server error' });
}
});
Using the Firebase Admin SDK:
import firebase_admin
from firebase_admin import credentials, messaging
# Initialize the app with a service account
cred = credentials.Certificate('path/to/serviceAccountKey.json')
firebase_admin.initialize_app(cred)
# Send a message to a specific device
def send_notification_to_device(token, title, body, data=None):
if data is None:
data = {}
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
token=token
)
try:
response = messaging.send(message)
print('Successfully sent message:', response)
return response
except Exception as e:
print('Error sending message:', e)
raise e
# Send a message to multiple devices
def send_notification_to_devices(tokens, title, body, data=None):
if data is None:
data = {}
message = messaging.MulticastMessage(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
tokens=tokens
)
try:
response = messaging.send_multicast(message)
print(f'{response.success_count} messages were sent successfully')
return response
except Exception as e:
print('Error sending messages:', e)
raise e
# Send a message to a topic
def send_notification_to_topic(topic, title, body, data=None):
if data is None:
data = {}
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
topic=topic
)
try:
response = messaging.send(message)
print('Successfully sent message to topic:', response)
return response
except Exception as e:
print('Error sending message to topic:', e)
raise e
# Example Flask API endpoint to send notification
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/send-notification', methods=['POST'])
def send_notification():
try:
data = request.json
user_id = data.get('userId')
title = data.get('title')
body = data.get('body')
custom_data = data.get('data', {})
# Get the user's FCM token from the database
# This is a placeholder - implement your database logic
token = get_user_token(user_id)
if not token:
return jsonify({'error': 'User token not found'}), 404
response = send_notification_to_device(token, title, body, custom_data)
return jsonify({'success': True, 'response': response}), 200
except Exception as e:
print('Error sending notification:', e)
return jsonify({'error': 'Server error'}), 500
def get_user_token(user_id):
# Implement your database logic to retrieve the token
# This is a placeholder
return "user_fcm_token"
if __name__ == '__main__':
app.run(debug=True)
Using the Firebase Admin SDK:
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootApplication
@RestController
public class FcmApplication {
public static void main(String[] args) throws IOException {
// Initialize Firebase Admin SDK
FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json");
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
FirebaseApp.initializeApp(options);
SpringApplication.run(FcmApplication.class, args);
}
// Send a message to a specific device
public String sendNotificationToDevice(String token, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
Message message = Message.builder()
.setNotification(notification)
.putAllData(data)
.setToken(token)
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message: " + response);
return response;
}
// Send a message to multiple devices
public BatchResponse sendNotificationToDevices(List tokens, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
MulticastMessage message = MulticastMessage.builder()
.setNotification(notification)
.putAllData(data)
.addAllTokens(tokens)
.build();
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
System.out.println(response.getSuccessCount() + " messages were sent successfully");
return response;
}
// Send a message to a topic
public String sendNotificationToTopic(String topic, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
Message message = Message.builder()
.setNotification(notification)
.putAllData(data)
.setTopic(topic)
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message to topic: " + response);
return response;
}
// Example API endpoint to send notification
@PostMapping("/api/send-notification")
public ResponseEntity> sendNotification(@RequestBody NotificationRequest request) {
try {
// Get the user's FCM token from the database
// This is a placeholder - implement your database logic
String token = getUserToken(request.getUserId());
if (token == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User token not found");
}
String response = sendNotificationToDevice(token, request.getTitle(), request.getBody(), request.getData());
return ResponseEntity.ok(response);
} catch (FirebaseMessagingException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending notification");
}
}
private String getUserToken(String userId) {
// Implement your database logic to retrieve the token
// This is a placeholder
return "user_fcm_token";
}
// Request model
public static class NotificationRequest {
private String userId;
private String title;
private String body;
private Map data;
// Getters and setters
}
}
FCM topic messaging allows you to send a message to multiple devices that have opted in to a particular topic.
Subscribe to a topic on the client:
// Android
FirebaseMessaging.getInstance().subscribeToTopic("news")
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d(TAG, "Subscribed to news topic");
} else {
Log.e(TAG, "Failed to subscribe to news topic", task.getException());
}
});
// iOS
Messaging.messaging().subscribe(toTopic: "news") { error in
if let error = error {
print("Failed to subscribe to news topic: \(error)")
} else {
print("Subscribed to news topic")
}
}
// Flutter
FirebaseMessaging.instance.subscribeToTopic('news')
.then((value) => print('Subscribed to news topic'))
.catchError((error) => print('Failed to subscribe to news topic: $error'));
// React Native
messaging()
.subscribeToTopic('news')
.then(() => console.log('Subscribed to news topic'))
.catch(error => console.log('Failed to subscribe to news topic:', error));
Send a message to a topic from the server:
// Node.js
const message = {
notification: {
title: 'Breaking News',
body: 'New article available'
},
topic: 'news'
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message to topic:', response);
})
.catch((error) => {
console.error('Error sending message to topic:', error);
});
// Python
message = messaging.Message(
notification=messaging.Notification(
title='Breaking News',
body='New article available'
),
topic='news'
)
response = messaging.send(message)
print('Successfully sent message to topic:', response)
// Java
Message message = Message.builder()
.setNotification(Notification.builder()
.setTitle("Breaking News")
.setBody("New article available")
.build())
.setTopic("news")
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message to topic: " + response);
The easiest way to test your FCM implementation is to send a test message from the Firebase Console:
You can also test your FCM implementation using cURL commands:
curl -X POST -H "Authorization: key=YOUR_SERVER_KEY" -H "Content-Type: application/json" \
-d '{
"to": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Test Notification",
"body": "This is a test notification from cURL"
},
"data": {
"key1": "value1",
"key2": "value2"
}
}' \
https://fcm.googleapis.com/fcm/send
Postman provides a user-friendly interface for testing API calls:
https://fcm.googleapis.com/fcm/send{
"to": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Test Notification",
"body": "This is a test notification from Postman"
},
"data": {
"key1": "value1",
"key2": "value2"
}
}
To thoroughly test how users interact with your notifications, we've created a Notification Interaction Recorder. This tool captures detailed information about the notification lifecycle and user interactions.
Record and analyze how users interact with push notifications to improve your notification strategy and fix issues.
Implement the Notification Interaction Recorder in your Android app:
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.messaging.RemoteMessage;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class NotificationRecorder {
private static final String TAG = "NotificationRecorder";
private static NotificationRecorder instance;
private boolean isRecording = false;
private final List<NotificationEvent> events = new ArrayList<>();
private final Map<String, Object> appState = new HashMap<>();
private final Map<String, Object> networkRequests = new HashMap<>();
private final List<String> systemLogs = new ArrayList<>();
private final OkHttpClient httpClient = new OkHttpClient();
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
private Activity currentActivity;
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks;
private NotificationRecorder(Context context) {
// Initialize the recorder
lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
currentActivity = activity;
if (isRecording) {
captureAppState("Activity created: " + activity.getClass().getSimpleName());
}
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
currentActivity = activity;
if (isRecording) {
captureAppState("Activity started: " + activity.getClass().getSimpleName());
}
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
currentActivity = activity;
if (isRecording) {
captureAppState("Activity resumed: " + activity.getClass().getSimpleName());
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
if (isRecording) {
captureAppState("Activity paused: " + activity.getClass().getSimpleName());
}
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
if (isRecording) {
captureAppState("Activity stopped: " + activity.getClass().getSimpleName());
}
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
// Not needed for recording
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
if (isRecording && currentActivity == activity) {
captureAppState("Activity destroyed: " + activity.getClass().getSimpleName());
currentActivity = null;
}
}
};
// Register activity lifecycle callbacks
if (context instanceof Application) {
((Application) context).registerActivityLifecycleCallbacks(lifecycleCallbacks);
}
}
public static synchronized NotificationRecorder getInstance(Context context) {
if (instance == null) {
instance = new NotificationRecorder(context.getApplicationContext());
}
return instance;
}
public void startRecording() {
if (!isRecording) {
isRecording = true;
events.clear();
appState.clear();
networkRequests.clear();
systemLogs.clear();
// Capture initial app state
captureAppState("Recording started");
// Log the event
addEvent("RECORDING_STARTED", "Notification interaction recording started");
Log.d(TAG, "Notification recording started");
}
}
public void stopRecording() {
if (isRecording) {
isRecording = false;
// Capture final app state
captureAppState("Recording stopped");
// Log the event
addEvent("RECORDING_STOPPED", "Notification interaction recording stopped");
Log.d(TAG, "Notification recording stopped with " + events.size() + " events");
}
}
public void onNotificationReceived(RemoteMessage remoteMessage) {
if (isRecording) {
try {
JSONObject notificationData = new JSONObject();
// Add notification details
if (remoteMessage.getNotification() != null) {
notificationData.put("title", remoteMessage.getNotification().getTitle());
notificationData.put("body", remoteMessage.getNotification().getBody());
}
// Add data payload
JSONObject dataPayload = new JSONObject();
for (Map.Entry<String, String> entry : remoteMessage.getData().entrySet()) {
dataPayload.put(entry.getKey(), entry.getValue());
}
notificationData.put("data", dataPayload);
// Capture app state before notification processing
captureAppState("Before notification processing");
// Log the event
addEvent("NOTIFICATION_RECEIVED", "Notification received", notificationData);
Log.d(TAG, "Recorded notification received: " + notificationData.toString());
} catch (JSONException e) {
Log.e(TAG, "Error recording notification", e);
}
}
}
public void onNotificationClicked(RemoteMessage remoteMessage) {
if (isRecording) {
try {
JSONObject notificationData = new JSONObject();
// Add notification details
if (remoteMessage.getNotification() != null) {
notificationData.put("title", remoteMessage.getNotification().getTitle());
notificationData.put("body", remoteMessage.getNotification().getBody());
}
// Add data payload
JSONObject dataPayload = new JSONObject();
for (Map.Entry<String, String> entry : remoteMessage.getData().entrySet()) {
dataPayload.put(entry.getKey(), entry.getValue());
}
notificationData.put("data", dataPayload);
// Capture app state before notification click handling
captureAppState("Before notification click handling");
// Log the event
addEvent("NOTIFICATION_CLICKED", "Notification clicked", notificationData);
Log.d(TAG, "Recorded notification click: " + notificationData.toString());
} catch (JSONException e) {
Log.e(TAG, "Error recording notification click", e);
}
}
}
public void captureUIChange(String description, String fromScreen, String toScreen) {
if (isRecording) {
try {
JSONObject uiData = new JSONObject();
uiData.put("description", description);
uiData.put("fromScreen", fromScreen);
uiData.put("toScreen", toScreen);
// Log the event
addEvent("UI_CHANGE", "UI changed", uiData);
Log.d(TAG, "Recorded UI change: " + uiData.toString());
} catch (JSONException e) {
Log.e(TAG, "Error recording UI change", e);
}
}
}
public void captureNetworkRequest(String url, String method, Map<String, String> headers, String requestBody, int responseCode, String responseBody) {
if (isRecording) {
try {
JSONObject networkData = new JSONObject();
networkData.put("url", url);
networkData.put("method", method);
// Add headers
JSONObject headersJson = new JSONObject();
for (Map.Entry<String, String> entry : headers.entrySet()) {
headersJson.put(entry.getKey(), entry.getValue());
}
networkData.put("headers", headersJson);
// Add request and response
networkData.put("requestBody", requestBody);
networkData.put("responseCode", responseCode);
networkData.put("responseBody", responseBody);
// Store in network requests map
String requestId = String.valueOf(System.currentTimeMillis());
networkRequests.put(requestId, networkData);
// Log the event
addEvent("NETWORK_REQUEST", "Network request made", networkData);
Log.d(TAG, "Recorded network request: " + networkData.toString());
} catch (JSONException e) {
Log.e(TAG, "Error recording network request", e);
}
}
}
public void captureSystemLog(String logMessage) {
if (isRecording) {
systemLogs.add(logMessage);
// Log the event
addEvent("SYSTEM_LOG", logMessage);
Log.d(TAG, "Recorded system log: " + logMessage);
}
}
private void captureAppState(String reason) {
try {
JSONObject stateData = new JSONObject();
stateData.put("timestamp", dateFormat.format(new Date()));
stateData.put("reason", reason);
// Capture current activity
if (currentActivity != null) {
stateData.put("currentActivity", currentActivity.getClass().getSimpleName());
stateData.put("activityState", getActivityState(currentActivity));
}
// Capture memory usage
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
stateData.put("memoryUsage", usedMemory);
// Store in app state map
String stateId = String.valueOf(System.currentTimeMillis());
appState.put(stateId, stateData);
// Log the event
addEvent("APP_STATE_CAPTURED", "App state captured: " + reason, stateData);
Log.d(TAG, "Captured app state: " + stateData.toString());
} catch (JSONException e) {
Log.e(TAG, "Error capturing app state", e);
}
}
private String getActivityState(Activity activity) {
if (activity.isFinishing()) {
return "FINISHING";
} else if (activity.hasWindowFocus()) {
return "FOCUSED";
} else {
return "CREATED";
}
}
private void addEvent(String type, String description) {
addEvent(type, description, null);
}
private void addEvent(String type, String description, JSONObject data) {
NotificationEvent event = new NotificationEvent(type, description, data);
events.add(event);
}
public JSONObject getRecordingAsJson() {
try {
JSONObject recording = new JSONObject();
recording.put("recordingId", String.valueOf(System.currentTimeMillis()));
recording.put("timestamp", dateFormat.format(new Date()));
// Add events
JSONArray eventsArray = new JSONArray();
for (NotificationEvent event : events) {
eventsArray.put(event.toJson());
}
recording.put("events", eventsArray);
// Add app states
JSONObject appStatesJson = new JSONObject();
for (Map.Entry<String, Object> entry : appState.entrySet()) {
appStatesJson.put(entry.getKey(), entry.getValue());
}
recording.put("appStates", appStatesJson);
// Add network requests
JSONObject networkRequestsJson = new JSONObject();
for (Map.Entry<String, Object> entry : networkRequests.entrySet()) {
networkRequestsJson.put(entry.getKey(), entry.getValue());
}
recording.put("networkRequests", networkRequestsJson);
// Add system logs
JSONArray systemLogsArray = new JSONArray();
for (String log : systemLogs) {
systemLogsArray.put(log);
}
recording.put("systemLogs", systemLogsArray);
return recording;
} catch (JSONException e) {
Log.e(TAG, "Error creating recording JSON", e);
return new JSONObject();
}
}
public void saveRecordingToFile(File directory, String filename) {
if (!directory.exists()) {
directory.mkdirs();
}
File file = new File(directory, filename);
try (FileWriter writer = new FileWriter(file)) {
writer.write(getRecordingAsJson().toString(2));
Log.d(TAG, "Recording saved to " + file.getAbsolutePath());
} catch (IOException | JSONException e) {
Log.e(TAG, "Error saving recording to file", e);
}
}
public List<NotificationEvent> getEvents() {
return new ArrayList<>(events);
}
public boolean isRecording() {
return isRecording;
}
// Inner class to represent a notification event
public static class NotificationEvent {
private final String type;
private final String description;
private final JSONObject data;
private final long timestamp;
public NotificationEvent(String type, String description, JSONObject data) {
this.type = type;
this.description = description;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public JSONObject toJson() throws JSONException {
JSONObject json = new JSONObject();
json.put("type", type);
json.put("description", description);
json.put("timestamp", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).format(new Date(timestamp)));
if (data != null) {
json.put("data", data);
}
return json;
}
public String getType() {
return type;
}
public String getDescription() {
return description;
}
public JSONObject getData() {
return data;
}
public long getTimestamp() {
return timestamp;
}
}
}
To use the recorder in your Firebase Messaging Service:
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
private NotificationRecorder recorder;
@Override
public void onCreate() {
super.onCreate();
recorder = NotificationRecorder.getInstance(this);
}
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// Record the notification
recorder.onNotificationReceived(remoteMessage);
// Your existing notification handling code
if (remoteMessage.getNotification() != null) {
sendNotification(remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody());
}
if (remoteMessage.getData().size() > 0) {
handleDataMessage(remoteMessage.getData());
}
}
// Add this to your activity where you handle notification clicks
private void handleNotificationClick(RemoteMessage remoteMessage) {
// Record the notification click
NotificationRecorder.getInstance(this).onNotificationClicked(remoteMessage);
// Your existing notification click handling code
}
}
Add a UI to control the recorder:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Notification Interaction Recorder"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnStartRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Start Recording"
android:backgroundTint="@color/colorSuccess"/>
<Button
android:id="@+id/btnStopRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Stop Recording"
android:backgroundTint="@color/colorError"
android:enabled="false"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/btnReplayRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replay Recording"
android:enabled="false"/>
<Button
android:id="@+id/btnDownloadRecording"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Download JSON"
android:enabled="false"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp"
android:gravity="center_vertical">
<View
android:id="@+id/statusIndicator"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/status_indicator"/>
<TextView
android:id="@+id/tvStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Not recording"
android:layout_marginStart="8dp"/>
</LinearLayout>
</LinearLayout>
Implement the UI controls:
public class NotificationTestActivity extends AppCompatActivity {
private NotificationRecorder recorder;
private Button btnStartRecording, btnStopRecording, btnReplayRecording, btnDownloadRecording;
private View statusIndicator;
private TextView tvStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notification_test);
// Initialize the recorder
recorder = NotificationRecorder.getInstance(this);
// Initialize UI components
btnStartRecording = findViewById(R.id.btnStartRecording);
btnStopRecording = findViewById(R.id.btnStopRecording);
btnReplayRecording = findViewById(R.id.btnReplayRecording);
btnDownloadRecording = findViewById(R.id.btnDownloadRecording);
statusIndicator = findViewById(R.id.statusIndicator);
tvStatus = findViewById(R.id.tvStatus);
// Set up button click listeners
btnStartRecording.setOnClickListener(v -> startRecording());
btnStopRecording.setOnClickListener(v -> stopRecording());
btnReplayRecording.setOnClickListener(v -> replayRecording());
btnDownloadRecording.setOnClickListener(v -> downloadRecording());
// Update UI based on recorder state
updateUI();
}
private void startRecording() {
recorder.startRecording();
updateUI();
}
private void stopRecording() {
recorder.stopRecording();
updateUI();
}
private void replayRecording() {
// Show the recording in a dialog
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Notification Interaction Recording");
// Create a WebView to display the JSON in a formatted way
WebView webView = new WebView(this);
webView.loadData(
"<html><head><style>pre { white-space: pre-wrap; word-wrap: break-word; }</style></head>" +
"<body><pre>" + recorder.getRecordingAsJson().toString(2) + "</pre></body></html>",
"text/html", "UTF-8");
builder.setView(webView);
builder.setPositiveButton("Close", null);
builder.show();
}
private void downloadRecording() {
// Save the recording to a file
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
String filename = "notification_recording_" + System.currentTimeMillis() + ".json";
recorder.saveRecordingToFile(downloadsDir, filename);
// Show a toast
Toast.makeText(this, "Recording saved to Downloads/" + filename, Toast.LENGTH_LONG).show();
}
private void updateUI() {
boolean isRecording = recorder.isRecording();
btnStartRecording.setEnabled(!isRecording);
btnStopRecording.setEnabled(isRecording);
btnReplayRecording.setEnabled(!isRecording && !recorder.getEvents().isEmpty());
btnDownloadRecording.setEnabled(!isRecording && !recorder.getEvents().isEmpty());
if (isRecording) {
statusIndicator.setBackgroundResource(R.drawable.status_indicator_recording);
tvStatus.setText("Recording...");
} else {
statusIndicator.setBackgroundResource(R.drawable.status_indicator_idle);
tvStatus.setText("Not recording");
}
}
}
Implement the Notification Interaction Recorder in your iOS app:
import Foundation
import UIKit
import FirebaseMessaging
class NotificationRecorder {
static let shared = NotificationRecorder()
private var isRecording = false
private var events: [NotificationEvent] = []
private var appState: [String: Any] = [:]
private var networkRequests: [String: Any] = [:]
private var systemLogs: [String] = []
private let dateFormatter = ISO8601DateFormatter()
private init() {
// Initialize the recorder
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func applicationDidBecomeActive() {
if isRecording {
captureAppState(reason: "Application did become active")
}
}
@objc private func applicationWillResignActive() {
if isRecording {
captureAppState(reason: "Application will resign active")
}
}
@objc private func applicationDidEnterBackground() {
if isRecording {
captureAppState(reason: "Application did enter background")
}
}
@objc private func applicationWillEnterForeground() {
if isRecording {
captureAppState(reason: "Application will enter foreground")
}
}
func startRecording() {
if !isRecording {
isRecording = true
events.removeAll()
appState.removeAll()
networkRequests.removeAll()
systemLogs.removeAll()
// Capture initial app state
captureAppState(reason: "Recording started")
// Log the event
addEvent(type: "RECORDING_STARTED", description: "Notification interaction recording started")
print("Notification recording started")
}
}
func stopRecording() {
if isRecording {
isRecording = false
// Capture final app state
captureAppState(reason: "Recording stopped")
// Log the event
addEvent(type: "RECORDING_STOPPED", description: "Notification interaction recording stopped")
print("Notification recording stopped with \(events.count) events")
}
}
func onNotificationReceived(userInfo: [AnyHashable: Any]) {
if isRecording {
do {
var notificationData: [String: Any] = [:]
// Add notification details
if let aps = userInfo["aps"] as? [String: Any], let alert = aps["alert"] as? [String: Any] {
notificationData["title"] = alert["title"]
notificationData["body"] = alert["body"]
}
// Add data payload
var dataPayload: [String: Any] = [:]
for (key, value) in userInfo {
if let key = key as? String, key != "aps" {
dataPayload[key] = value
}
}
notificationData["data"] = dataPayload
// Capture app state before notification processing
captureAppState(reason: "Before notification processing")
// Log the event
addEvent(type: "NOTIFICATION_RECEIVED", description: "Notification received", data: notificationData)
print("Recorded notification received: \(notificationData)")
}
}
}
func onNotificationClicked(userInfo: [AnyHashable: Any]) {
if isRecording {
do {
var notificationData: [String: Any] = [:]
// Add notification details
if let aps = userInfo["aps"] as? [String: Any], let alert = aps["alert"] as? [String: Any] {
notificationData["title"] = alert["title"]
notificationData["body"] = alert["body"]
}
// Add data payload
var dataPayload: [String: Any] = [:]
for (key, value) in userInfo {
if let key = key as? String, key != "aps" {
dataPayload[key] = value
}
}
notificationData["data"] = dataPayload
// Capture app state before notification click handling
captureAppState(reason: "Before notification click handling")
// Log the event
addEvent(type: "NOTIFICATION_CLICKED", description: "Notification clicked", data: notificationData)
print("Recorded notification click: \(notificationData)")
}
}
}
func captureUIChange(description: String, fromScreen: String, toScreen: String) {
if isRecording {
let uiData: [String: Any] = [
"description": description,
"fromScreen": fromScreen,
"toScreen": toScreen
]
// Log the event
addEvent(type: "UI_CHANGE", description: "UI changed", data: uiData)
print("Recorded UI change: \(uiData)")
}
}
func captureNetworkRequest(url: URL, method: String, headers: [String: String], requestBody: String?, responseCode: Int, responseBody: String?) {
if isRecording {
var networkData: [String: Any] = [
"url": url.absoluteString,
"method": method,
"headers": headers,
"responseCode": responseCode
]
if let requestBody = requestBody {
networkData["requestBody"] = requestBody
}
if let responseBody = responseBody {
networkData["responseBody"] = responseBody
}
// Store in network requests map
let requestId = String(Date().timeIntervalSince1970)
networkRequests[requestId] = networkData
// Log the event
addEvent(type: "NETWORK_REQUEST", description: "Network request made", data: networkData)
print("Recorded network request: \(networkData)")
}
}
func captureSystemLog(_ logMessage: String) {
if isRecording {
systemLogs.append(logMessage)
// Log the event
addEvent(type: "SYSTEM_LOG", description: logMessage)
print("Recorded system log: \(logMessage)")
}
}
private func captureAppState(reason: String) {
var stateData: [String: Any] = [
"timestamp": dateFormatter.string(from: Date()),
"reason": reason
]
// Capture current view controller
if let rootVC = UIApplication.shared.windows.first?.rootViewController {
stateData["rootViewController"] = String(describing: type(of: rootVC))
// Get the currently visible view controller
if let visibleVC = getVisibleViewController(from: rootVC) {
stateData["visibleViewController"] = String(describing: type(of: visibleVC))
}
}
// Capture app state
stateData["applicationState"] = UIApplication.shared.applicationState.rawValue
// Capture memory usage
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout.size)/4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if kerr == KERN_SUCCESS {
stateData["memoryUsage"] = info.resident_size
}
// Store in app state map
let stateId = String(Date().timeIntervalSince1970)
appState[stateId] = stateData
// Log the event
addEvent(type: "APP_STATE_CAPTURED", description: "App state captured: \(reason)", data: stateData)
print("Captured app state: \(stateData)")
}
private func getVisibleViewController(from rootViewController: UIViewController) -> UIViewController? {
if let presentedViewController = rootViewController.presentedViewController {
return getVisibleViewController(from: presentedViewController)
}
if let navigationController = rootViewController as? UINavigationController {
return navigationController.visibleViewController
}
if let tabBarController = rootViewController as? UITabBarController {
return tabBarController.selectedViewController
}
return rootViewController
}
private func addEvent(type: String, description: String, data: [String: Any]? = nil) {
let event = NotificationEvent(type: type, description: description, data: data)
events.append(event)
}
func getRecordingAsJson() -> [String: Any] {
var recording: [String: Any] = [
"recordingId": String(Date().timeIntervalSince1970),
"timestamp": dateFormatter.string(from: Date())
]
// Add events
recording["events"] = events.map { $0.toJson() }
// Add app states
recording["appStates"] = appState
// Add network requests
recording["networkRequests"] = networkRequests
// Add system logs
recording["systemLogs"] = systemLogs
return recording
}
func saveRecordingToFile(filename: String) -> URL? {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsDirectory.appendingPathComponent(filename)
do {
let jsonData = try JSONSerialization.data(withJSONObject: getRecordingAsJson(), options: .prettyPrinted)
try jsonData.write(to: fileURL)
print("Recording saved to \(fileURL.path)")
return fileURL
} catch {
print("Error saving recording to file: \(error)")
return nil
}
}
func getEvents() -> [NotificationEvent] {
return events
}
var recording: Bool {
return isRecording
}
// Inner class to represent a notification event
class NotificationEvent {
let type: String
let description: String
let data: [String: Any]?
let timestamp: Date
init(type: String, description: String, data: [String: Any]? = nil) {
self.type = type
self.description = description
self.data = data
self.timestamp = Date()
}
func toJson() -> [String: Any] {
var json: [String: Any] = [
"type": type,
"description": description,
"timestamp": ISO8601DateFormatter().string(from: timestamp)
]
if let data = data {
json["data"] = data
}
return json
}
}
}
To use the recorder in your AppDelegate:
import UIKit
import Firebase
import FirebaseMessaging
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
// Request permission for notifications
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { _, _ in }
)
application.registerForRemoteNotifications()
// Set messaging delegate
Messaging.messaging().delegate = self
return true
}
// Handle notification when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// Record the notification
NotificationRecorder.shared.onNotificationReceived(userInfo: userInfo)
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
// Change this to your preferred presentation option
completionHandler([[.alert, .sound]])
}
// Handle notification when user taps on it
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Record the notification click
NotificationRecorder.shared.onNotificationClicked(userInfo: userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler()
}
// Handle receiving message
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Record the notification
NotificationRecorder.shared.onNotificationReceived(userInfo: userInfo)
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
// Receive FCM token
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
// Send token to your server
if let token = fcmToken {
sendRegistrationToServer(token)
}
}
func sendRegistrationToServer(_ token: String) {
// Implement your server communication here
}
}
Create a UI to control the recorder:
import UIKit
class NotificationTestViewController: UIViewController {
private let titleLabel: UILabel = {
let label = UILabel()
label.text = "Notification Interaction Recorder"
label.font = UIFont.boldSystemFont(ofSize: 18)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let startRecordingButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Start Recording", for: .normal)
button.backgroundColor = UIColor.systemGreen
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let stopRecordingButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Stop Recording", for: .normal)
button.backgroundColor = UIColor.systemRed
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.isEnabled = false
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let replayRecordingButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Replay Recording", for: .normal)
button.backgroundColor = UIColor.systemBlue
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.isEnabled = false
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let downloadRecordingButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Download JSON", for: .normal)
button.backgroundColor = UIColor.lightGray
button.setTitleColor(.white, for: .normal)
button.layer.cornerRadius = 8
button.isEnabled = false
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
private let statusIndicator: UIView = {
let view = UIView()
view.backgroundColor = UIColor.lightGray
view.layer.cornerRadius = 6
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let statusLabel: UILabel = {
let label = UILabel()
label.text = "Not recording"
label.font = UIFont.systemFont(ofSize: 14)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupUI()
updateUI()
}
private func setupUI() {
// Add subviews
view.addSubview(titleLabel)
view.addSubview(startRecordingButton)
view.addSubview(stopRecordingButton)
view.addSubview(replayRecordingButton)
view.addSubview(downloadRecordingButton)
view.addSubview(statusIndicator)
view.addSubview(statusLabel)
// Set up constraints
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
startRecordingButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
startRecordingButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
startRecordingButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
startRecordingButton.heightAnchor.constraint(equalToConstant: 44),
stopRecordingButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
stopRecordingButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stopRecordingButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
stopRecordingButton.heightAnchor.constraint(equalToConstant: 44),
replayRecordingButton.topAnchor.constraint(equalTo: startRecordingButton.bottomAnchor, constant: 12),
replayRecordingButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
replayRecordingButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
replayRecordingButton.heightAnchor.constraint(equalToConstant: 44),
downloadRecordingButton.topAnchor.constraint(equalTo: stopRecordingButton.bottomAnchor, constant: 12),
downloadRecordingButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
downloadRecordingButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.45),
downloadRecordingButton.heightAnchor.constraint(equalToConstant: 44),
statusIndicator.topAnchor.constraint(equalTo: replayRecordingButton.bottomAnchor, constant: 20),
statusIndicator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusIndicator.widthAnchor.constraint(equalToConstant: 12),
statusIndicator.heightAnchor.constraint(equalToConstant: 12),
statusLabel.centerYAnchor.constraint(equalTo: statusIndicator.centerYAnchor),
statusLabel.leadingAnchor.constraint(equalTo: statusIndicator.trailingAnchor, constant: 8),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
// Add button actions
startRecordingButton.addTarget(self, action: #selector(startRecording), for: .touchUpInside)
stopRecordingButton.addTarget(self, action: #selector(stopRecording), for: .touchUpInside)
replayRecordingButton.addTarget(self, action: #selector(replayRecording), for: .touchUpInside)
downloadRecordingButton.addTarget(self, action: #selector(downloadRecording), for: .touchUpInside)
}
@objc private func startRecording() {
NotificationRecorder.shared.startRecording()
updateUI()
}
@objc private func stopRecording() {
NotificationRecorder.shared.stopRecording()
updateUI()
}
@objc private func replayRecording() {
// Create a text view to display the JSON
let textView = UITextView()
textView.isEditable = false
textView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
// Convert recording to JSON string
do {
let jsonData = try JSONSerialization.data(withJSONObject: NotificationRecorder.shared.getRecordingAsJson(), options: .prettyPrinted)
if let jsonString = String(data: jsonData, encoding: .utf8) {
textView.text = jsonString
}
} catch {
textView.text = "Error converting recording to JSON: \(error)"
}
// Create and present a view controller with the text view
let viewController = UIViewController()
viewController.view = textView
viewController.title = "Recording Replay"
viewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissReplay))
let navigationController = UINavigationController(rootViewController: viewController)
present(navigationController, animated: true)
}
@objc private func dismissReplay() {
dismiss(animated: true)
}
@objc private func downloadRecording() {
// Save the recording to a file
let filename = "notification_recording_\(Int(Date().timeIntervalSince1970)).json"
if let fileURL = NotificationRecorder.shared.saveRecordingToFile(filename: filename) {
// Show activity view controller to share the file
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
present(activityViewController, animated: true)
}
}
private func updateUI() {
let isRecording = NotificationRecorder.shared.recording
startRecordingButton.isEnabled = !isRecording
stopRecordingButton.isEnabled = isRecording
replayRecordingButton.isEnabled = !isRecording && !NotificationRecorder.shared.getEvents().isEmpty
downloadRecordingButton.isEnabled = !isRecording && !NotificationRecorder.shared.getEvents().isEmpty
if isRecording {
statusIndicator.backgroundColor = UIColor.systemRed
// Add pulsing animation
let pulseAnimation = CABasicAnimation(keyPath: "opacity")
pulseAnimation.duration = 1.0
pulseAnimation.fromValue = 1.0
pulseAnimation.toValue = 0.5
pulseAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = Float.infinity
statusIndicator.layer.add(pulseAnimation, forKey: "pulse")
statusLabel.text = "Recording..."
} else {
statusIndicator.backgroundColor = UIColor.lightGray
statusIndicator.layer.removeAnimation(forKey: "pulse")
statusLabel.text = "Not recording"
}
}
}
Implement the Notification Interaction Recorder for web applications:
// notification-recorder.js
class NotificationRecorder {
constructor() {
if (NotificationRecorder.instance) {
return NotificationRecorder.instance;
}
NotificationRecorder.instance = this;
this.isRecording = false;
this.events = [];
this.appState = {};
this.networkRequests = {};
this.systemLogs = [];
this.originalFetch = window.fetch;
this.originalXHR = window.XMLHttpRequest.prototype.open;
// Set up network request monitoring
this.setupNetworkMonitoring();
// Set up performance monitoring
this.setupPerformanceMonitoring();
return this;
}
setupNetworkMonitoring() {
// Override fetch
window.fetch = async (...args) => {
const request = args[0];
const options = args[1] || {};
// Create a unique ID for this request
const requestId = Date.now().toString();
// Record the request
const requestInfo = {
url: typeof request === 'string' ? request : request.url,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body || null,
timestamp: new Date().toISOString()
};
if (this.isRecording) {
this.captureNetworkRequest(requestId, requestInfo);
}
try {
// Make the actual request
const response = await this.originalFetch.apply(window, args);
// Clone the response to read its body
const clonedResponse = response.clone();
try {
// Try to read the response body as text
const responseBody = await clonedResponse.text();
// Record the response
if (this.isRecording) {
this.updateNetworkRequest(requestId, {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries([...response.headers.entries()]),
body: responseBody,
timestamp: new Date().toISOString()
});
}
} catch (error) {
console.error('Error reading response body:', error);
}
return response;
} catch (error) {
// Record the error
if (this.isRecording) {
this.updateNetworkRequest(requestId, {
error: error.message,
timestamp: new Date().toISOString()
});
}
throw error;
}
};
// Override XMLHttpRequest
window.XMLHttpRequest.prototype.open = function(...args) {
const method = args[0];
const url = args[1];
// Create a unique ID for this request
const requestId = Date.now().toString();
// Store the request info
const requestInfo = {
url,
method,
timestamp: new Date().toISOString()
};
// Add event listeners to track the request
this.addEventListener('load', function() {
if (NotificationRecorder.instance.isRecording) {
NotificationRecorder.instance.updateNetworkRequest(requestId, {
status: this.status,
statusText: this.statusText,
responseText: this.responseText,
timestamp: new Date().toISOString()
});
}
});
this.addEventListener('error', function() {
if (NotificationRecorder.instance.isRecording) {
NotificationRecorder.instance.updateNetworkRequest(requestId, {
error: 'Network error',
timestamp: new Date().toISOString()
});
}
});
// Record the request
if (NotificationRecorder.instance.isRecording) {
NotificationRecorder.instance.captureNetworkRequest(requestId, requestInfo);
}
// Call the original open method
return NotificationRecorder.instance.originalXHR.apply(this, args);
};
}
setupPerformanceMonitoring() {
// Monitor page navigation
window.addEventListener('popstate', () => {
if (this.isRecording) {
this.captureUIChange('Navigation', document.referrer, window.location.href);
}
});
// Monitor DOM mutations
const observer = new MutationObserver((mutations) => {
if (this.isRecording) {
// Only record significant DOM changes
const significantChanges = mutations.filter(mutation => {
// Filter out style changes and minor text changes
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
return false;
}
if (mutation.type === 'characterData' && mutation.target.textContent.length < 10) {
return false;
}
return true;
});
if (significantChanges.length > 0) {
this.captureUIChange('DOM mutation', 'N/A', 'N/A');
}
}
});
observer.observe(document.body, {
childList: true,
attributes: true,
characterData: true,
subtree: true
});
// Monitor performance metrics
if ('PerformanceObserver' in window) {
const performanceObserver = new PerformanceObserver((list) => {
if (this.isRecording) {
for (const entry of list.getEntries()) {
this.captureSystemLog(`Performance: ${entry.name} - ${entry.startTime.toFixed(2)}ms`);
}
}
});
performanceObserver.observe({ entryTypes: ['resource', 'navigation', 'paint'] });
}
}
startRecording() {
if (!this.isRecording) {
this.isRecording = true;
this.events = [];
this.appState = {};
this.networkRequests = {};
this.systemLogs = [];
// Capture initial app state
this.captureAppState('Recording started');
// Log the event
this.addEvent('RECORDING_STARTED', 'Notification interaction recording started');
console.log('Notification recording started');
// Dispatch event
window.dispatchEvent(new CustomEvent('notification-recorder-started'));
}
}
stopRecording() {
if (this.isRecording) {
this.isRecording = false;
// Capture final app state
this.captureAppState('Recording stopped');
// Log the event
this.addEvent('RECORDING_STOPPED', 'Notification interaction recording stopped');
console.log('Notification recording stopped with ' + this.events.length + ' events');
// Dispatch event
window.dispatchEvent(new CustomEvent('notification-recorder-stopped'));
}
}
onNotificationReceived(notification) {
if (this.isRecording) {
// Capture app state before notification processing
this.captureAppState('Before notification processing');
// Log the event
this.addEvent('NOTIFICATION_RECEIVED', 'Notification received', notification);
console.log('Recorded notification received:', notification);
}
}
onNotificationClicked(notification) {
if (this.isRecording) {
// Capture app state before notification click handling
this.captureAppState('Before notification click handling');
// Log the event
this.addEvent('NOTIFICATION_CLICKED', 'Notification clicked', notification);
console.log('Recorded notification click:', notification);
}
}
captureUIChange(description, fromScreen, toScreen) {
if (this.isRecording) {
const uiData = {
description,
fromScreen,
toScreen,
url: window.location.href,
timestamp: new Date().toISOString()
};
// Log the event
this.addEvent('UI_CHANGE', 'UI changed', uiData);
console.log('Recorded UI change:', uiData);
}
}
captureNetworkRequest(requestId, requestInfo) {
if (this.isRecording) {
// Store in network requests map
this.networkRequests[requestId] = {
request: requestInfo
};
// Log the event
this.addEvent('NETWORK_REQUEST_STARTED', 'Network request started', requestInfo);
console.log('Recorded network request start:', requestInfo);
}
}
updateNetworkRequest(requestId, responseInfo) {
if (this.isRecording && this.networkRequests[requestId]) {
// Update the network request with response info
this.networkRequests[requestId].response = responseInfo;
// Log the event
this.addEvent('NETWORK_REQUEST_COMPLETED', 'Network request completed', {
requestId,
...responseInfo
});
console.log('Recorded network request completion:', responseInfo);
}
}
captureSystemLog(logMessage) {
if (this.isRecording) {
this.systemLogs.push({
message: logMessage,
timestamp: new Date().toISOString()
});
// Log the event
this.addEvent('SYSTEM_LOG', logMessage);
console.log('Recorded system log:', logMessage);
}
}
captureAppState(reason) {
const stateData = {
timestamp: new Date().toISOString(),
reason,
url: window.location.href,
title: document.title,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
userAgent: navigator.userAgent,
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize
} : null
};
// Store in app state map
const stateId = Date.now().toString();
this.appState[stateId] = stateData;
// Log the event
this.addEvent('APP_STATE_CAPTURED', 'App state captured: ' + reason, stateData);
console.log('Captured app state:', stateData);
}
addEvent(type, description, data = null) {
const event = {
type,
description,
timestamp: new Date().toISOString(),
data
};
this.events.push(event);
}
getRecordingAsJson() {
const recording = {
recordingId: Date.now().toString(),
timestamp: new Date().toISOString(),
events: this.events,
appStates: this.appState,
networkRequests: this.networkRequests,
systemLogs: this.systemLogs
};
return recording;
}
downloadRecording() {
const recording = this.getRecordingAsJson();
const json = JSON.stringify(recording, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `notification_recording_${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
replayRecording() {
// Create a modal to display the recording
const modal = document.createElement('div');
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
modal.style.zIndex = '9999';
modal.style.display = 'flex';
modal.style.justifyContent = 'center';
modal.style.alignItems = 'center';
const content = document.createElement('div');
content.style.backgroundColor = 'white';
content.style.padding = '20px';
content.style.borderRadius = '8px';
content.style.width = '80%';
content.style.height = '80%';
content.style.overflow = 'auto';
const closeButton = document.createElement('button');
closeButton.textContent = 'Close';
closeButton.style.padding = '8px 16px';
closeButton.style.backgroundColor = '#3F51B5';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.borderRadius = '4px';
closeButton.style.cursor = 'pointer';
closeButton.style.marginBottom = '20px';
closeButton.addEventListener('click', () => {
document.body.removeChild(modal);
});
const pre = document.createElement('pre');
pre.style.whiteSpace = 'pre-wrap';
pre.style.wordBreak = 'break-all';
pre.textContent = JSON.stringify(this.getRecordingAsJson(), null, 2);
content.appendChild(closeButton);
content.appendChild(pre);
modal.appendChild(content);
document.body.appendChild(modal);
}
}
// Create a singleton instance
const notificationRecorder = new NotificationRecorder();
// Export the instance
window.notificationRecorder = notificationRecorder;
Create a UI for the recorder:
<!-- notification-recorder-ui.html -->
<div class="notification-recorder-container"&
Firebase Cloud Messaging Implementation Guide
Firebase Cloud Messaging Implementation Guide
A comprehensive guide to implementing push notifications in your mobile app using Firebase Cloud Messaging (FCM)
Introduction to Firebase Cloud Messaging
Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably send notifications at no cost. Using FCM, you can notify a client app that new email or other data is available to sync. You can send notification messages to drive user re-engagement and retention.
Note: FCM is the successor to Google Cloud Messaging (GCM), and it inherits the reliable and scalable GCM infrastructure, plus new features.
Key Capabilities
- Send notification messages or data messages
- Versatile message targeting - to single devices, groups of devices, or topics
- Send messages from client apps - A client app can send messages to devices that have subscribed to a topic
Prerequisites
Before implementing FCM, ensure you have:
- A Firebase project created in the Firebase Console
- Your app added to the Firebase project
- Firebase SDK integrated into your app
Setting Up Firebase Cloud Messaging
Step 1: Configure Firebase in Your Project
First, you need to add Firebase to your project. The process varies depending on your platform:
- In the Firebase console, add an Android app to your project
- Register the app with your package name
- Download the
google-services.json file and place it in your app's module directory
- Add the Firebase SDK to your app-level build.gradle file:
dependencies {
// Add the Firebase SDK for Google Analytics
implementation 'com.google.firebase:firebase-analytics:21.3.0'
// Add the Firebase Cloud Messaging SDK
implementation 'com.google.firebase:firebase-messaging:23.2.0'
}
And at the bottom of the file, add:
apply plugin: 'com.google.gms.google-services'
In your project-level build.gradle file, add:
buildscript {
repositories {
google()
// ...
}
dependencies {
// ...
classpath 'com.google.gms:google-services:4.3.15'
}
}
- In the Firebase console, add an iOS app to your project
- Register the app with your bundle ID
- Download the
GoogleService-Info.plist file
- Add the file to your Xcode project (drag and drop into the project navigator)
- Install the Firebase SDK using CocoaPods. Add to your Podfile:
pod 'Firebase/Analytics'
pod 'Firebase/Messaging'
Then run:
pod install
In your AppDelegate, initialize Firebase:
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
}
- In the Firebase console, add a Web app to your project
- Register the app with a nickname
- Add the Firebase SDK to your web app:
<!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging.js"></script>
<script>
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "your-api-key",
authDomain: "your-auth-domain",
projectId: "your-project-id",
storageBucket: "your-storage-bucket",
messagingSenderId: "your-messaging-sender-id",
appId: "your-app-id"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
Step 2: Enable Cloud Messaging API
- Go to the Firebase Console and select your project
- Navigate to Project Settings (gear icon) > Cloud Messaging
- Make sure the Cloud Messaging API is enabled
- Note down the Server key for later use when sending messages from your server
Client-Side Implementation
Requesting Permission and Getting FCM Token
Before you can send notifications to a device, you need to request permission from the user and get an FCM token.
In your application class or main activity:
import com.google.firebase.messaging.FirebaseMessaging;
// Get FCM token
FirebaseMessaging.getInstance().getToken()
.addOnCompleteListener(task -> {
if (!task.isSuccessful()) {
Log.w(TAG, "Fetching FCM registration token failed", task.getException());
return;
}
// Get new FCM registration token
String token = task.getResult();
// Log and toast
Log.d(TAG, "FCM Token: " + token);
// Send token to your server
sendRegistrationToServer(token);
});
In your AppDelegate:
import Firebase
import FirebaseMessaging
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
// Request permission for notifications
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { _, _ in }
)
application.registerForRemoteNotifications()
// Set messaging delegate
Messaging.messaging().delegate = self
return true
}
// Receive FCM token
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
// Send token to your server
if let token = fcmToken {
sendRegistrationToServer(token)
}
}
}
// Request permission
const messaging = firebase.messaging();
messaging.requestPermission()
.then(() => {
console.log('Notification permission granted.');
return messaging.getToken();
})
.then((token) => {
console.log('FCM Token:', token);
// Send token to your server
sendTokenToServer(token);
})
.catch((err) => {
console.log('Unable to get permission to notify.', err);
});
First, add the Firebase Messaging package:
flutter pub add firebase_messaging
Then in your app:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
@override
void initState() {
super.initState();
setupFirebaseMessaging();
}
Future setupFirebaseMessaging() async {
// Request permission
NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
alert: true,
badge: true,
sound: true,
);
print('User granted permission: ${settings.authorizationStatus}');
// Get token
String? token = await FirebaseMessaging.instance.getToken();
print('FCM Token: $token');
// Send token to server
if (token != null) {
sendTokenToServer(token);
}
// Listen for token refresh
FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
print('FCM Token refreshed: $newToken');
sendTokenToServer(newToken);
});
}
void sendTokenToServer(String token) {
// Implement your server communication here
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FCM Example')),
body: Center(child: Text('Firebase Cloud Messaging Example')),
),
);
}
}
First, install the required packages:
npm install @react-native-firebase/app @react-native-firebase/messaging
Then in your app:
import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
function App() {
useEffect(() => {
// Request permission
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);
getFcmToken();
}
}
// Get FCM token
const getFcmToken = async () => {
const fcmToken = await messaging().getToken();
if (fcmToken) {
console.log('FCM Token:', fcmToken);
// Send token to your server
sendTokenToServer(fcmToken);
}
}
requestUserPermission();
// Listen to token refresh
const unsubscribe = messaging().onTokenRefresh(token => {
console.log('FCM Token refreshed:', token);
sendTokenToServer(token);
});
return unsubscribe;
}, []);
const sendTokenToServer = (token) => {
// Implement your server communication here
};
return (
// Your app component
);
}
export default App;
Handling Incoming Messages
Now that you have set up FCM and obtained a token, you need to handle incoming messages.
Create a service that extends FirebaseMessagingService:
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
private static final String TAG = "MyFirebaseMsgService";
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// Check if message contains a notification payload
if (remoteMessage.getNotification() != null) {
Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
// Show notification
sendNotification(remoteMessage.getNotification().getTitle(),
remoteMessage.getNotification().getBody());
}
// Check if message contains a data payload
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
// Handle data payload
handleDataMessage(remoteMessage.getData());
}
}
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
// Send token to your app server
sendRegistrationToServer(token);
}
private void sendNotification(String title, String messageBody) {
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE);
String channelId = "fcm_default_channel";
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
"Channel human readable title",
NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(0, notificationBuilder.build());
}
private void handleDataMessage(Map data) {
// Process data message here
}
private void sendRegistrationToServer(String token) {
// Implement this method to send token to your app server
}
}
Don't forget to register the service in your AndroidManifest.xml:
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
In your AppDelegate, implement the UNUserNotificationCenterDelegate methods:
// Handle notification when app is in foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
// Change this to your preferred presentation option
completionHandler([[.alert, .sound]])
}
// Handle notification when user taps on it
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler()
}
// Handle receiving message
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// With swizzling disabled you must let Messaging know about the message
Messaging.messaging().appDidReceiveMessage(userInfo)
// Print message ID
if let messageID = userInfo["gcm.message_id"] {
print("Message ID: \(messageID)")
}
// Print full message
print(userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
// Handle foreground messages
messaging.onMessage((payload) => {
console.log('Message received in foreground:', payload);
// Display notification
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/firebase-logo.png'
};
if (Notification.permission === 'granted') {
new Notification(notificationTitle, notificationOptions);
}
});
// Handle background messages
// This requires a service worker
// Create a file named firebase-messaging-sw.js in your web root:
/*
// firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging.js');
firebase.initializeApp({
apiKey: "your-api-key",
authDomain: "your-auth-domain",
projectId: "your-project-id",
storageBucket: "your-storage-bucket",
messagingSenderId: "your-messaging-sender-id",
appId: "your-app-id"
});
const messaging = firebase.messaging();
messaging.onBackgroundMessage((payload) => {
console.log('Background message received:', payload);
const notificationTitle = payload.notification.title;
const notificationOptions = {
body: payload.notification.body,
icon: '/firebase-logo.png'
};
return self.registration.showNotification(notificationTitle, notificationOptions);
});
*/
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// Define a top-level function to handle background messages
Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print("Handling a background message: ${message.messageId}");
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
@override
void initState() {
super.initState();
setupFirebaseMessaging();
setupLocalNotifications();
}
Future setupFirebaseMessaging() async {
// Set the background message handler
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Handle foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print("Got a message whilst in the foreground!");
print("Message data: ${message.data}");
if (message.notification != null) {
print("Message also contained a notification: ${message.notification}");
showNotification(message);
}
});
// Handle when the app is opened from a notification
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
print('A new onMessageOpenedApp event was published!');
// Navigate to relevant page based on message data
handleNotificationClick(message.data);
});
// Check if the app was opened from a notification
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
handleNotificationClick(initialMessage.data);
}
}
void setupLocalNotifications() {
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
final IOSInitializationSettings initializationSettingsIOS =
IOSInitializationSettings();
final InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String? payload) async {
if (payload != null) {
// Handle notification click with payload
Map data = json.decode(payload);
handleNotificationClick(data);
}
});
}
void showNotification(RemoteMessage message) async {
RemoteNotification? notification = message.notification;
AndroidNotification? android = message.notification?.android;
if (notification != null && android != null) {
await flutterLocalNotificationsPlugin.show(
notification.hashCode,
notification.title,
notification.body,
NotificationDetails(
android: AndroidNotificationDetails(
'channel_id',
'channel_name',
channelDescription: 'channel_description',
importance: Importance.max,
priority: Priority.high,
showWhen: false,
),
iOS: IOSNotificationDetails(),
),
payload: json.encode(message.data),
);
}
}
void handleNotificationClick(Map data) {
// Navigate to relevant page based on notification data
print('Handling notification click with data: $data');
// Example: Navigator.pushNamed(context, '/details', arguments: data);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('FCM Example')),
body: Center(child: Text('Firebase Cloud Messaging Example')),
),
);
}
}
import React, { useEffect } from 'react';
import { Alert } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import PushNotification from 'react-native-push-notification';
function App() {
useEffect(() => {
// Configure local notifications
PushNotification.configure({
onNotification: function (notification) {
console.log('NOTIFICATION:', notification);
// Process the notification
// Required on iOS only
notification.finish(PushNotificationIOS.FetchResult.NoData);
},
// IOS ONLY
permissions: {
alert: true,
badge: true,
sound: true,
},
popInitialNotification: true,
requestPermissions: true,
});
// Create notification channel for Android
PushNotification.createChannel(
{
channelId: "fcm_default_channel",
channelName: "Default Channel",
channelDescription: "A default channel for notifications",
soundName: "default",
importance: 4,
vibrate: true,
},
(created) => console.log(`Channel created: ${created}`)
);
// Handle foreground messages
const unsubscribe = messaging().onMessage(async remoteMessage => {
console.log('A new FCM message arrived!', remoteMessage);
// Display local notification
PushNotification.localNotification({
channelId: "fcm_default_channel",
title: remoteMessage.notification?.title,
message: remoteMessage.notification?.body || '',
data: remoteMessage.data,
});
});
// Handle background/quit state messages
messaging().setBackgroundMessageHandler(async remoteMessage => {
console.log('Message handled in the background!', remoteMessage);
});
// 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,
);
// Navigate based on the notification data
handleNotificationOpen(remoteMessage);
}
});
// Handle notification open when app is in background
messaging().onNotificationOpenedApp(remoteMessage => {
console.log(
'Notification caused app to open from background state:',
remoteMessage,
);
// Navigate based on the notification data
handleNotificationOpen(remoteMessage);
});
return unsubscribe;
}, []);
const handleNotificationOpen = (remoteMessage) => {
// Navigate to relevant screen based on notification data
console.log('Handling notification open with data:', remoteMessage.data);
// Example: navigation.navigate('Details', { id: remoteMessage.data.id });
};
return (
// Your app component
);
}
export default App;
Server-Side Implementation
Storing FCM Tokens
When a client app registers with FCM, it receives a registration token. You should store this token on your server to send messages to specific devices.
// Example server code (Node.js with Express and MongoDB)
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.json());
// Define a schema for storing FCM tokens
const tokenSchema = new mongoose.Schema({
userId: { type: String, required: true },
token: { type: String, required: true },
platform: { type: String, enum: ['android', 'ios', 'web'], required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
const Token = mongoose.model('Token', tokenSchema);
// API endpoint to register/update FCM token
app.post('/api/register-token', async (req, res) => {
try {
const { userId, token, platform } = req.body;
// Validate input
if (!userId || !token || !platform) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Update token if it exists, otherwise create a new one
const result = await Token.findOneAndUpdate(
{ userId, platform },
{ token, updatedAt: Date.now() },
{ upsert: true, new: true }
);
res.status(200).json({ success: true, data: result });
} catch (error) {
console.error('Error registering token:', error);
res.status(500).json({ error: 'Server error' });
}
});
Sending Messages from Your Server
You can send messages to devices using the FCM HTTP v1 API or the Firebase Admin SDK.
Using the Firebase Admin SDK:
const admin = require('firebase-admin');
const serviceAccount = require('./path/to/serviceAccountKey.json');
// Initialize the app with a service account
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
// Send a message to a specific device
async function sendNotificationToDevice(token, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
token
};
const response = await admin.messaging().send(message);
console.log('Successfully sent message:', response);
return response;
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}
// Send a message to multiple devices
async function sendNotificationToDevices(tokens, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
tokens // Array of tokens
};
const response = await admin.messaging().sendMulticast(message);
console.log(`${response.successCount} messages were sent successfully`);
return response;
} catch (error) {
console.error('Error sending messages:', error);
throw error;
}
}
// Send a message to a topic
async function sendNotificationToTopic(topic, title, body, data = {}) {
try {
const message = {
notification: {
title,
body
},
data,
topic
};
const response = await admin.messaging().send(message);
console.log('Successfully sent message to topic:', response);
return response;
} catch (error) {
console.error('Error sending message to topic:', error);
throw error;
}
}
// Example API endpoint to send notification
app.post('/api/send-notification', async (req, res) => {
try {
const { userId, title, body, data } = req.body;
// Get the user's FCM token from the database
const tokenDoc = await Token.findOne({ userId });
if (!tokenDoc) {
return res.status(404).json({ error: 'User token not found' });
}
const response = await sendNotificationToDevice(tokenDoc.token, title, body, data);
res.status(200).json({ success: true, response });
} catch (error) {
console.error('Error sending notification:', error);
res.status(500).json({ error: 'Server error' });
}
});
Using the Firebase Admin SDK:
import firebase_admin
from firebase_admin import credentials, messaging
# Initialize the app with a service account
cred = credentials.Certificate('path/to/serviceAccountKey.json')
firebase_admin.initialize_app(cred)
# Send a message to a specific device
def send_notification_to_device(token, title, body, data=None):
if data is None:
data = {}
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
token=token
)
try:
response = messaging.send(message)
print('Successfully sent message:', response)
return response
except Exception as e:
print('Error sending message:', e)
raise e
# Send a message to multiple devices
def send_notification_to_devices(tokens, title, body, data=None):
if data is None:
data = {}
message = messaging.MulticastMessage(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
tokens=tokens
)
try:
response = messaging.send_multicast(message)
print(f'{response.success_count} messages were sent successfully')
return response
except Exception as e:
print('Error sending messages:', e)
raise e
# Send a message to a topic
def send_notification_to_topic(topic, title, body, data=None):
if data is None:
data = {}
message = messaging.Message(
notification=messaging.Notification(
title=title,
body=body
),
data=data,
topic=topic
)
try:
response = messaging.send(message)
print('Successfully sent message to topic:', response)
return response
except Exception as e:
print('Error sending message to topic:', e)
raise e
# Example Flask API endpoint to send notification
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/send-notification', methods=['POST'])
def send_notification():
try:
data = request.json
user_id = data.get('userId')
title = data.get('title')
body = data.get('body')
custom_data = data.get('data', {})
# Get the user's FCM token from the database
# This is a placeholder - implement your database logic
token = get_user_token(user_id)
if not token:
return jsonify({'error': 'User token not found'}), 404
response = send_notification_to_device(token, title, body, custom_data)
return jsonify({'success': True, 'response': response}), 200
except Exception as e:
print('Error sending notification:', e)
return jsonify({'error': 'Server error'}), 500
def get_user_token(user_id):
# Implement your database logic to retrieve the token
# This is a placeholder
return "user_fcm_token"
if __name__ == '__main__':
app.run(debug=True)
Using the Firebase Admin SDK:
import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.*;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@SpringBootApplication
@RestController
public class FcmApplication {
public static void main(String[] args) throws IOException {
// Initialize Firebase Admin SDK
FileInputStream serviceAccount = new FileInputStream("path/to/serviceAccountKey.json");
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
FirebaseApp.initializeApp(options);
SpringApplication.run(FcmApplication.class, args);
}
// Send a message to a specific device
public String sendNotificationToDevice(String token, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
Message message = Message.builder()
.setNotification(notification)
.putAllData(data)
.setToken(token)
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message: " + response);
return response;
}
// Send a message to multiple devices
public BatchResponse sendNotificationToDevices(List tokens, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
MulticastMessage message = MulticastMessage.builder()
.setNotification(notification)
.putAllData(data)
.addAllTokens(tokens)
.build();
BatchResponse response = FirebaseMessaging.getInstance().sendMulticast(message);
System.out.println(response.getSuccessCount() + " messages were sent successfully");
return response;
}
// Send a message to a topic
public String sendNotificationToTopic(String topic, String title, String body, Map data) throws FirebaseMessagingException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.build();
Message message = Message.builder()
.setNotification(notification)
.putAllData(data)
.setTopic(topic)
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message to topic: " + response);
return response;
}
// Example API endpoint to send notification
@PostMapping("/api/send-notification")
public ResponseEntity> sendNotification(@RequestBody NotificationRequest request) {
try {
// Get the user's FCM token from the database
// This is a placeholder - implement your database logic
String token = getUserToken(request.getUserId());
if (token == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User token not found");
}
String response = sendNotificationToDevice(token, request.getTitle(), request.getBody(), request.getData());
return ResponseEntity.ok(response);
} catch (FirebaseMessagingException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error sending notification");
}
}
private String getUserToken(String userId) {
// Implement your database logic to retrieve the token
// This is a placeholder
return "user_fcm_token";
}
// Request model
public static class NotificationRequest {
private String userId;
private String title;
private String body;
private Map data;
// Getters and setters
}
}
Topic Messaging
FCM topic messaging allows you to send a message to multiple devices that have opted in to a particular topic.
Subscribe to a topic on the client:
// Android
FirebaseMessaging.getInstance().subscribeToTopic("news")
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d(TAG, "Subscribed to news topic");
} else {
Log.e(TAG, "Failed to subscribe to news topic", task.getException());
}
});
// iOS
Messaging.messaging().subscribe(toTopic: "news") { error in
if let error = error {
print("Failed to subscribe to news topic: \(error)")
} else {
print("Subscribed to news topic")
}
}
// Flutter
FirebaseMessaging.instance.subscribeToTopic('news')
.then((value) => print('Subscribed to news topic'))
.catchError((error) => print('Failed to subscribe to news topic: $error'));
// React Native
messaging()
.subscribeToTopic('news')
.then(() => console.log('Subscribed to news topic'))
.catch(error => console.log('Failed to subscribe to news topic:', error));
Send a message to a topic from the server:
// Node.js
const message = {
notification: {
title: 'Breaking News',
body: 'New article available'
},
topic: 'news'
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message to topic:', response);
})
.catch((error) => {
console.error('Error sending message to topic:', error);
});
// Python
message = messaging.Message(
notification=messaging.Notification(
title='Breaking News',
body='New article available'
),
topic='news'
)
response = messaging.send(message)
print('Successfully sent message to topic:', response)
// Java
Message message = Message.builder()
.setNotification(Notification.builder()
.setTitle("Breaking News")
.setBody("New article available")
.build())
.setTopic("news")
.build();
String response = FirebaseMessaging.getInstance().send(message);
System.out.println("Successfully sent message to topic: " + response);
Testing Your FCM Implementation
Testing from Firebase Console
The easiest way to test your FCM implementation is to send a test message from the Firebase Console:
- Go to the Firebase Console and select your project
- Navigate to Messaging (in the left sidebar)
- Click "Send your first message"
- Compose your notification (title, text, image)
- Under "Target", select one of the following:
- User segment - to target specific user groups
- Topic - to send to devices subscribed to a topic
- Single device - to test with a specific FCM token
- Complete the setup and send the test message
Testing with cURL
You can also test your FCM implementation using cURL commands:
curl -X POST -H "Authorization: key=YOUR_SERVER_KEY" -H "Content-Type: application/json" \
-d '{
"to": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Test Notification",
"body": "This is a test notification from cURL"
},
"data": {
"key1": "value1",
"key2": "value2"
}
}' \
https://fcm.googleapis.com/fcm/send
Testing with Postman
Postman provides a user-friendly interface for testing API calls:
- Create a new POST request to
https://fcm.googleapis.com/fcm/send
- Add headers:
- Authorization: key=YOUR_SERVER_KEY
- Content-Type: application/json
- Add the request body:
{
"to": "DEVICE_FCM_TOKEN",
"notification": {
"title": "Test Notification",
"body": "This is a test notification from Postman"
},
"data": {
"key1": "value1",
"key2": "value2"
}
}
- Send the request and check if the notification is received on the device
Best Practices for FCM Implementation
Token Management
- Store tokens securely - FCM tokens should be stored securely on your server
- Handle token refreshes - Tokens can change, so implement logic to update tokens when they refresh
- Clean up invalid tokens - Remove tokens that are no longer valid to keep your database clean
Message Payload
- Keep payloads small - FCM has payload size limits (4KB for notification messages, 4KB for data messages)
- Use data messages for custom handling - Data messages give you more control over how notifications are displayed
- Include relevant information - Add enough data to allow the app to navigate to the right screen when a notification is tapped
User Experience
- Respect user preferences - Allow users to opt out of non-essential notifications
- Don't spam - Send notifications only when they provide value to the user
- Localize notifications - Send notifications in the user's preferred language
- Use rich notifications - Include images or action buttons when appropriate
Security
- Protect your server key - Never include your FCM server key in client-side code
- Validate message sources - Ensure that notification requests come from trusted sources
- Use HTTPS - Always use HTTPS for communication between your server and FCM
Performance
- Batch messages - Use multicast messaging to send to multiple devices at once
- Use topics for broadcast messages - Topics are more efficient than sending individual messages to many devices
- Implement retry logic - Handle temporary failures with appropriate retry strategies
Troubleshooting Common Issues
Notifications Not Received
- Check device settings - Ensure notifications are enabled for your app in the device settings
- Verify token registration - Make sure the FCM token is correctly registered on your server
- Check for token refresh - The token might have changed; implement proper token refresh handling
- Verify server key - Ensure you're using the correct server key in your API calls
- Check payload format - Make sure your notification payload is correctly formatted
Notification Displayed Incorrectly
- Check notification channel - On Android 8.0+, ensure you've created the notification channel correctly
- Verify notification builder - Make sure you're setting all required properties in the notification builder
- Check for custom handling - If you're using data messages, ensure your custom handling logic is correct
Background/Foreground Handling Issues
- Implement both handlers - Make sure you've implemented handlers for both foreground and background messages
- Check background restrictions - Some devices have aggressive battery optimization that can affect background message delivery
- Verify service registration - On Android, ensure your FirebaseMessagingService is correctly registered in the manifest
API Errors
Common Error Codes:
- 400 Bad Request - Check your payload format and ensure it's valid JSON
- 401 Unauthorized - Verify your server key is correct and has not expired
- 403 Forbidden - Your server key might not have permission to send messages
- 404 Not Found - The FCM token might be invalid or the device might have uninstalled your app
- 429 Too Many Requests - You're sending too many messages too quickly; implement rate limiting
Conclusion
Firebase Cloud Messaging provides a reliable and scalable solution for sending push notifications to your users across different platforms. By following the implementation steps and best practices outlined in this guide, you can effectively integrate FCM into your application and enhance user engagement.
Remember to:
- Properly handle FCM tokens on both client and server sides
- Implement appropriate message handling for foreground and background states
- Follow best practices for security, performance, and user experience
- Test your implementation thoroughly before deploying to production
With FCM, you can keep your users informed and engaged with timely and relevant notifications, ultimately improving user retention and satisfaction.