How Push Notifications works internally at Web, Android & iOS?
?? Sai Ashish
Full Stack Engineer | ex- Tekion, VideoVerse, LambdaTest, Paytm, ?????????????? MintTea (SwiftRobotics, Stewards, Velvet Video, ???? SearchVaccines), ???? PadhAI.ai | 10 internships | System Design | Problem Solver
Push notifications are messages sent by a server to a client application. These messages can be displayed even when the client application is not actively running.
Push notifications are called "push" notifications because the messages are "pushed" from the server to the client device, rather than the client device having to "pull" the information from the server. Here's a more detailed explanation:
Push vs. Pull Model
Push Model:
Pull Model:
How Push Notifications Work
Benefits of Push Notifications
Example
If a push notification is missed by the server or fails to reach the client device, the impact and subsequent actions depend on the underlying push notification service (e.g., FCM for Android, APNs for iOS) and the design of the application. Here's what generally happens and the mechanisms in place to handle such scenarios:
Factors Leading to Missed Notifications
Mechanisms to Handle Missed Notifications
Retry Mechanisms
Message Queueing and Storage
User Experience Considerations
Example Scenarios
Here's a breakdown of how push notifications work on the Web, Android, and iOS, including the internal mechanisms:
Web Push Notifications:
1. User Subscription:
2. Service Worker:
3. Push Service:
4. Sending a Notification:
Android Push Notifications:
1. Firebase Cloud Messaging (FCM):
2. App Registration:
3. Sending a Notification:
iOS Push Notifications
1. Apple Push Notification Service (APNs):
2. App Registration:
3. Sending a Notification:
When an app is in the background or closed, push notifications are handled differently by each operating system (Android and iOS). Here’s a detailed look at how it works internally for both platforms:
Android
1. Push Notification Reception
2. System-Level Handling
3. Notification Display
4. User Interaction
public class MyFirebaseMessagingService extends FirebaseMessagingService {
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
// Handle FCM messages here.
// If the message contains a data payload, process it.
if (remoteMessage.getData().size > 0) {
// Handle message data
}
// If the message contains a notification payload, display it.
if (remoteMessage.getNotification() != null) {
sendNotification(remoteMessage.getNotification().getBody());
}
}
private void sendNotification(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_ONE_SHOT);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id")
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("My Notification")
.setContentText(messageBody)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(0, notificationBuilder.build());
}
}
iOS
1. Push Notification Reception
2. System-Level Handling
3. Notification Display
4. User Interaction
领英推荐
import UserNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UNUserNotificationCenter.current().delegate = self
// Request notification permissions
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
// Handle permission granted/denied
}
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// Handle remote notification
if let aps = userInfo["aps"] as? [String: AnyObject] {
handleNotification(aps: aps)
}
completionHandler(.newData)
}
private func handleNotification(aps: [String: AnyObject]) {
// Process notification payload
if let alert = aps["alert"] as? String {
// Display alert
}
}
// Handle notification when app is in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound])
}
}
We can use batch processing and distributed processing to deliver notifications to millions of devices.
For a comprehensive guide on sending notifications from the backend using Kafka, SNS, and SQS, check out the following article:
Steps to View Push Notification Connections in Chrome Network Tab
Here's how to test at the Application storage:
Firebase Cloud Messaging (FCM) does not use WebSockets for push notifications. Instead, it relies on a different set of technologies to deliver messages to devices:
So, while WebSockets are a popular choice for real-time communication in some scenarios, FCM uses a combination of HTTP/2 and XMPP to handle push notifications and message delivery.
Amazon SNS (Simple Notification Service) and Amazon SQS (Simple Queue Service) do not use WebSockets for push notifications or messaging. Here's how each service typically operates:
Amazon SNS (Simple Notification Service):
Amazon SQS (Simple Queue Service):
Hence, both SNS and SQS rely on standard HTTP/HTTPS protocols rather than WebSockets for their communication and messaging operations.
Example at React Native and React Web using Firebase Cloud Messaging and Push.js (Web):
React Native (Android & iOS):
import messaging from '@react-native-firebase/messaging';
import logger from './log';
import Storage from './storage';
import { getUniqueDeviceId } from './device';
import { successNotification } from './notifications';
import { db, FieldValue, auth } from './firebase';
import { Collections } from 'constants/Firestore';
import StorageKeys from 'constants/Storage';
async function storeFCMToken(token) {
const uid = auth.currentUser?.uid;
if (!(uid && token)) return;
const deviceId = getUniqueDeviceId();
let dbToken = null;
try {
dbToken = await Storage.load({ key: StorageKeys.fcmToken });
} catch (e) {}
if (dbToken !== token) {
Storage.save({ key: StorageKeys.fcmToken, data: token });
await db
.collection(Collections.users)
.doc(uid)
.collection(Collections.private)
.doc('private')
.update({
[`fcmTokens.${deviceId}`]: token,
});
}
}
export async function removeFCMToken() {
const uid = auth.currentUser?.uid;
await messaging().deleteToken(); // TODO: test this
if (!uid) return;
const deviceId = getUniqueDeviceId();
Storage.remove({ key: StorageKeys.fcmToken });
await db
.collection(Collections.users)
.doc(uid)
.collection(Collections.private)
.doc('private')
.update({
[`fcmTokens.${deviceId}`]: FieldValue.delete(),
});
}
// Only for iOS. On Android it will always revolve successfully
export async function initMessaging() {
const authStatus = await messaging().requestPermission();
const enabled =
authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) {
try {
const fcmToken = await messaging().getToken();
await storeFCMToken(fcmToken);
} catch (e) {
logger('FCM::INIT_MESSAGING ', e);
}
}
}
export function onTokenRefresh() {
return messaging().onTokenRefresh(async token => storeFCMToken(token));
}
export function onMessage() {
return messaging().onMessage(async remoteMessage => {
const { title, body } = remoteMessage.notification;
successNotification(title, body, 6000);
});
}
useEffect(() => {
const unsubscribeOnTokenRefresh = onTokenRefresh();
const unsubscribeOnMessage = onMessage();
return function cleanup() {
unsubscribeOnTokenRefresh?.();
unsubscribeOnMessage?.();
};
}, []);
export async function initUser(uid) {
const publicUserSnap = await db.collection(Collections.users).doc(uid).get();
const privateUserSnap = await db
.collection(Collections.users)
.doc(uid)
.collection(Collections.private)
.doc('private')
.get();
const userData = {
...publicUserSnap.data(),
...privateUserSnap.data(),
id: uid,
};
return userData;
}
useEffect(() => {
const uid = getCurrentUserId();
if (uid) {
const user = await initUser(uid);
await updateUser(user);
initMessaging().catch(e => logger('INITIALIZE_MESSAGING', e));
}
}, []);
React Web:
import { isSupported, getToken, onMessage, getMessaging } from 'firebase/messaging';
import Push from 'push.js';
import { storeFCMToken } from './token';
import Config from 'config/config.json';
import { getNotificationPermission } from 'utils/notifications';
export async function initMessaging() {
let unsubsOnMessage;
if ((await isSupported()) && (await getNotificationPermission())) {
const messaging = getMessaging();
const token = await getToken(messaging, { vapidKey: Config.firebase.vapidKey });
await storeFCMToken(token);
unsubsOnMessage = onMessage(messaging, (payload) => {
const { title, body } = payload.notification;
Push.create(title, {
body,
icon: '../assets/images/header/logo.png',
onClick() {
window.focus();
this.close();
},
});
});
}
return unsubsOnMessage;
}
import { deleteField } from 'firebase/firestore';
import { nanoid } from 'nanoid';
import { updateUserDocument, updateUserPrivateDocument } from './users';
import { Storage } from 'constants/index';
import { auth } from 'utils/firebase';
export async function removeFCMToken() {
const uid = auth?.currentUser?.uid;
if (!uid) return;
const deviceId = localStorage.getItem(Storage.deviceId);
localStorage.removeItem(Storage.token);
await deleteToken(uid, deviceId);
}
export const deleteToken = async (uid, deviceId) => {
await updateUserDocument(uid, { [`tokens.${deviceId}`]: deleteField() });
};
export async function storeFCMToken(token) {
const uid = auth?.currentUser?.uid;
if (!(uid && token)) return;
let deviceId = localStorage.getItem(Storage.deviceId);
if (!deviceId) {
deviceId = nanoid();
localStorage.setItem(Storage.deviceId, deviceId);
}
const dbToken = localStorage.getItem(Storage.token);
if (dbToken !== token) {
localStorage.setItem(Storage.token, token);
await addToken(uid, deviceId, token);
}
}
export const addToken = async (uid, deviceId, token) => {
await updateUserPrivateDocument(uid, { [`fcmTokens.${deviceId}`]: token });
};
const onLogout = async () => {
await removeFCMToken();
await signOut(auth);
updateUser(null);
clearFreshChatUser();
history.replace('home');
};
<ProfileButton onLogin={onLogin} onLogout={onLogout} />
useEffect(() => {
let unsubsInitMessage;
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
const userData = await getCurrentUser();
unsubsInitMessage = await initMessaging();
updateUser(userData);
setFreshChatUser(userData);
if (!userData.userName || !userData.displayName) {
setStep(1);
setLoginModalVisibility(true);
}
} else updateUser(null);
});
return function cleanup() {
if (unsubscribe) unsubscribe();
if (unsubsInitMessage) unsubsInitMessage();
};
}, []);
Node.js Backend (Firebase Cloud Messaging):
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';
import { PubSub } from '@google-cloud/pubsub';
admin.initializeApp();
const pubSubClient = new PubSub();
export const db = admin.firestore();
export const adminAuth = admin.auth();
export const config = functions.config();
export const bucket = admin.storage().bucket();
export const messaging = admin.messaging();
const functionsInRegion = functions.region('europe-west1');
export const { onCall, onRequest } = functionsInRegion.https;
export const firestoreTrigger = functionsInRegion.firestore;
export const authTrigger = functionsInRegion.auth.user();
export const { firestore } = admin;
export const { HttpsError } = functions.https;
export const { logger } = functions;
export const { pubsub } = functionsInRegion;
export function runWithOnRequest(options, fn) {
return functionsInRegion.runWith(options).https.onRequest(fn);
}
export async function sendNotifications(tokens, notification = {}, data = {}) {
try {
await messaging.sendToDevice(tokens, { data, notification }, { contentAvailable: true, priority: 'high' });
} catch (e) {
logger.log('Sending notification failed', e);
console.error(e);
}
}
export async function publishNotiEvent(lsId) {
await pubSubClient.topic('notifyUsers').publish(Buffer.from(JSON.stringify({ lsId })));
}
import { Collections } from 'constants';
import { db, firestore, sendNotifications } from 'utils/firebase';
export default async function notifyUsers(data /* , context */) {
const { lsId } = data.json;
const livestreamDocRef = db.collection(Collections.liveStreams).doc(lsId);
const tokens = [];
const lsData = (await livestreamDocRef.get()).data();
const { subscribedUsers, userId, title } = lsData;
const batch = db.batch();
if (!subscribedUsers) return;
for (const subscribedUser of subscribedUsers) {
const userDocRef = db
.collection(Collections.users)
.doc(subscribedUser)
.collection(Collections.private)
.doc('private');
const docData = (await userDocRef.get()).data();
if (docData?.fcmTokens) tokens.push(...Object.values(docData.fcmTokens));
batch.update(userDocRef, { subscribedLiveStreams: firestore.FieldValue.arrayRemove(lsId) });
}
batch.update(livestreamDocRef, { subscribedUsers: firestore.FieldValue.delete() });
await batch.commit();
const userData = (
await db
.collection(Collections.users)
.doc(userId)
.get()
).data();
const notiConfig = { title: `${title} will start soon`, body: `${userData.displayName} will start the livestream` };
await sendNotifications(tokens, notiConfig);
}
async function movePrivateUserData() {
const { docs } = await db.collection('users').get();
let i = 0;
const batch = db.batch();
for (const user of docs) {
const { id } = user;
console.log(id);
const data = user.data();
const { displayName, photoURL, userName = `user${i++}`, bio = '', author = false, tokens, ...privateData } = data;
const publicData = { displayName, photoURL, userName, bio, author };
privateData.fcmTokens = tokens ?? [];
const privateRef = db
.collection('users')
.doc(id)
.collection('private')
.doc('private');
batch.set(user.ref, publicData);
batch.set(privateRef, privateData, { merge: true });
}
await batch.commit();
return 'success';
}
If a server delivers a message to Firebase Cloud Messaging (FCM) but the Android device misses it, several scenarios and mechanisms can come into play to handle such situations:
Potential Reasons for Missing Messages
Mechanisms to Handle Missing Messages
Best Practices
By employing these strategies, you can mitigate the impact of missed messages and ensure a more reliable push notification experience for users.
If an Android device receives a push notification but the notification is missed or not acted upon, there are several factors and potential solutions to consider:
Potential Reasons for Missed Notifications:
1. App in Background or Closed:
- Background Processing: If the app is in the background or closed, the notification may not be immediately visible or actionable. The notification might get dismissed or overlooked by the user.
2. Notification Overwrites:
- Notification Overwriting: If multiple notifications are sent, newer ones might overwrite older ones if they share the same notification ID. This can cause earlier notifications to be missed.
3. User Actions:
- Accidental Dismissal: Users might accidentally swipe away or dismiss notifications before viewing them.
4. Device Settings:
- Notification Settings: Device settings or app-specific notification settings might be configured to limit the visibility or importance of notifications.
5. Notification Channels:
- Channel Configuration: Notifications sent to a particular channel might not be configured to show prominently, or the user might have disabled notifications for that channel.
Mechanisms to Address Missed Notifications
1. Notification Management:
- Persistent Notifications: Use persistent notifications to ensure important messages remain visible until the user interacts with them. Set appropriate priority levels to make notifications more noticeable.
- Custom Actions: Include actionable buttons in your notifications to encourage users to interact with them.
2. In-App Notifications:
- In-App Messaging: Implement in-app notifications or messages within the app to inform users of missed updates or actions required. This can be useful if the user opens the app later.
3. Local Storage and Sync:
- Message Caching: Cache important messages or updates locally within the app. When the user opens the app, they can see any missed information or notifications.
- Sync Mechanisms: Use data sync mechanisms to fetch missed notifications or updates from the server when the app starts or when the user reconnects to the internet.
4. Notification Channels:
- Channel Configuration: Ensure that notification channels are configured properly, allowing users to customize their notification preferences. Provide clear and actionable notification settings to enhance user engagement.
5. Analytics and Monitoring:
- Track Delivery: Implement analytics to track notification delivery and interaction rates. This helps identify patterns of missed notifications and improves future notification strategies. Best Practices
- Provide Clear Context: Ensure notifications provide enough context and actionable content so users understand the importance and can act upon them quickly.
- User Preferences: Respect user preferences and settings regarding notifications. Provide settings within the app for users to customize their notification experience.
- Engagement Strategies: Consider using additional engagement strategies such as in-app messages, reminders, or follow-up notifications to address missed or ignored notifications.
By incorporating these practices, you can improve the effectiveness of push notifications and minimize the impact of missed notifications on user experience.