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

Prerequisites

Before implementing FCM, ensure you have:

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:

  1. In the Firebase console, add an Android app to your project
  2. Register the app with your package name
  3. Download the google-services.json file and place it in your app's module directory
  4. 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'
    }
}
  1. In the Firebase console, add an iOS app to your project
  2. Register the app with your bundle ID
  3. Download the GoogleService-Info.plist file
  4. Add the file to your Xcode project (drag and drop into the project navigator)
  5. 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
    }
}
  1. In the Firebase console, add a Web app to your project
  2. Register the app with a nickname
  3. 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

  1. Go to the Firebase Console and select your project
  2. Navigate to Project Settings (gear icon) > Cloud Messaging
  3. Make sure the Cloud Messaging API is enabled
  4. 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:

  1. Go to the Firebase Console and select your project
  2. Navigate to Messaging (in the left sidebar)
  3. Click "Send your first message"
  4. Compose your notification (title, text, image)
  5. 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
  6. 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:

  1. Create a new POST request to https://fcm.googleapis.com/fcm/send
  2. Add headers:
    • Authorization: key=YOUR_SERVER_KEY
    • Content-Type: application/json
  3. 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"
      }
    }
  4. Send the request and check if the notification is received on the device

Notification Interaction Recorder

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.

Notification Interaction Recorder

Record and analyze how users interact with push notifications to improve your notification strategy and fix issues.

Not recording

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:

  1. In the Firebase console, add an Android app to your project
  2. Register the app with your package name
  3. Download the google-services.json file and place it in your app's module directory
  4. 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'
    }
}
  1. In the Firebase console, add an iOS app to your project
  2. Register the app with your bundle ID
  3. Download the GoogleService-Info.plist file
  4. Add the file to your Xcode project (drag and drop into the project navigator)
  5. 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
    }
}
  1. In the Firebase console, add a Web app to your project
  2. Register the app with a nickname
  3. 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

  1. Go to the Firebase Console and select your project
  2. Navigate to Project Settings (gear icon) > Cloud Messaging
  3. Make sure the Cloud Messaging API is enabled
  4. 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:

  1. Go to the Firebase Console and select your project
  2. Navigate to Messaging (in the left sidebar)
  3. Click "Send your first message"
  4. Compose your notification (title, text, image)
  5. 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
  6. 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:

  1. Create a new POST request to https://fcm.googleapis.com/fcm/send
  2. Add headers:
    • Authorization: key=YOUR_SERVER_KEY
    • Content-Type: application/json
  3. 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"
      }
    }
  4. 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.