Push Notifications
Push Notifications
Section titled “Push Notifications”Problem and Rationale
Section titled “Problem and Rationale”Customer support teams need immediate notification of:
- New messages in conversations
- Conversation assignments
- Mentions and team activity
- System notifications
- Even when the app is closed or backgrounded
Why Firebase Cloud Messaging:
- Reliable cross-platform delivery (iOS & Android)
- Background notification handling
- Rich notification support (images, actions)
- Deep linking integration
- Free tier sufficient for most use cases
Libraries:
@react-native-firebase/messaging- FCM integration@notifee/react-native- Local notification display (iOS)
Design
Section titled “Design”Notification Architecture
Section titled “Notification Architecture”Zelta Chat Server ↓Firebase Cloud Messaging ↓├─ App Foreground → onMessage│ ↓│ Display In-App Notification│├─ App Background → onNotificationOpenedApp│ ↓│ Navigate to Conversation│└─ App Closed → getInitialNotification ↓ Launch App → Navigate to ConversationNotification Types
Section titled “Notification Types”Supported Notification Types:
const NOTIFICATION_TYPES = [ 'conversation_creation', 'conversation_assignment', 'assigned_conversation_new_message', 'conversation_mention', 'participating_conversation_new_message',];Notification Payload Structure
Section titled “Notification Payload Structure”FCM Message:
{ "data": { "payload": { "data": { "notification": { "notification_type": "assigned_conversation_new_message", "primary_actor": { "id": 123, "conversation_id": 456 }, "primary_actor_type": "Message", "primary_actor_id": 123 } } } }}Lifecycle and Data Flow
Section titled “Lifecycle and Data Flow”Notification Permission Flow
Section titled “Notification Permission Flow”App First Launch ↓Request Notification Permission ↓├─ Granted│ ↓│ Get FCM Token│ ↓│ Register Token with Zelta Chat Server│ ↓│ Subscribe to Topics│└─ Denied ↓ Show Settings PromptNotification Received Flow (App Foreground)
Section titled “Notification Received Flow (App Foreground)”FCM Message Received ↓onMessage Handler ↓Parse Notification Data ↓Extract Conversation ID ↓Display In-App Notification (Notifee) ↓User Taps Notification ↓Navigate to ConversationNotification Opened Flow (App Background)
Section titled “Notification Opened Flow (App Background)”User Taps Notification ↓App Brought to Foreground ↓onNotificationOpenedApp Handler ↓Extract FCM Message Data ↓Transform to Conversation Link ↓Navigate via Deep Link System ↓Load ConversationNotification Opened Flow (App Closed)
Section titled “Notification Opened Flow (App Closed)”User Taps Notification ↓App Launches ↓getInitialNotification ↓Extract Message Data ↓Store Deep Link Intent ↓App Initialization Complete ↓Navigate to ConversationImplementation Notes
Section titled “Implementation Notes”FCM Setup
Section titled “FCM Setup”Message Handler Registration (src/navigation/index.tsx):
import messaging from '@react-native-firebase/messaging';
// Background message handler (must be at top level)messaging().setBackgroundMessageHandler(async remoteMessage => { console.log('Message handled in the background!', remoteMessage); // Process notification data});Permission Request:
const requestNotificationPermission = async () => { const authStatus = await messaging().requestPermission(); const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED || authStatus === messaging.AuthorizationStatus.PROVISIONAL;
if (enabled) { const token = await messaging().getToken(); // Send token to Zelta Chat server await registerFCMToken(token); }};Notification Handling
Section titled “Notification Handling”Extract Notification Data (src/utils/pushUtils.ts):
export const findNotificationFromFCM = ({ message }: { message: any }) => { let notification = null;
// FCM HTTP v1 if (message?.data?.payload) { const parsedPayload = JSON.parse(message.data.payload); notification = parsedPayload.data.notification; } // FCM legacy else { notification = JSON.parse(message.data.notification); }
return notification;};Build Conversation Link:
export const findConversationLinkFromPush = ({ notification, installationUrl,}: { notification: Notification; installationUrl: string;}) => { const { notificationType, primaryActor, primaryActorId, primaryActorType } = notification;
if (NOTIFICATION_TYPES.includes(notificationType)) { let conversationId = null;
if (primaryActorType === 'Conversation') { conversationId = primaryActor.id; } else if (primaryActorType === 'Message') { conversationId = primaryActor.conversationId; }
if (conversationId) { return `${installationUrl}/app/accounts/1/conversations/${conversationId}/${primaryActorId}/${primaryActorType}`; } }
return undefined;};Deep Link Integration
Section titled “Deep Link Integration”Initial URL Handling (src/navigation/index.tsx):
const linking = { prefixes: [installationUrl],
async getInitialURL() { // Check deep link first const url = await Linking.getInitialURL(); if (url) return url;
// Check FCM notification const message = await messaging().getInitialNotification(); if (message) { const notification = findNotificationFromFCM({ message }); const camelCaseNotification = transformNotification(notification); const conversationLink = findConversationLinkFromPush({ notification: camelCaseNotification, installationUrl, }); return conversationLink; }
return undefined; },
subscribe(listener: (url: string) => void) { // Listen for notifications when app in background const unsubscribe = messaging().onNotificationOpenedApp(message => { if (message) { const notification = findNotificationFromFCM({ message }); const camelCaseNotification = transformNotification(notification); const conversationLink = findConversationLinkFromPush({ notification: camelCaseNotification, installationUrl, }); if (conversationLink) { listener(conversationLink); } } });
return unsubscribe; },};Local Notifications (iOS)
Section titled “Local Notifications (iOS)”Display Local Notification (iOS only):
import notifee from '@notifee/react-native';
const displayLocalNotification = async (notification: Notification) => { if (Platform.OS === 'ios') { await notifee.displayNotification({ title: notification.pushMessageTitle, body: notification.primaryActor.content, data: notification, ios: { sound: 'default', }, }); }};Clear Notifications:
export const clearAllDeliveredNotifications = async () => { if (Platform.OS === 'ios') { await notifee.cancelAllNotifications(); }};Update Badge Count:
export const updateBadgeCount = async ({ count = 0 }) => { if (Platform.OS === 'ios' && count >= 0) { await notifee.setBadgeCount(count); }};Patterns and Anti-Patterns
Section titled “Patterns and Anti-Patterns”✅ Good Patterns:
// Always check notification typeif (NOTIFICATION_TYPES.includes(notificationType)) { // Handle notification}
// Clear notifications on app launchuseEffect(() => { clearAllDeliveredNotifications();}, []);
// Update badge count on unread changesuseEffect(() => { updateBadgeCount({ count: unreadCount });}, [unreadCount]);❌ Anti-Patterns:
// DON'T: Forget to handle null/undefinedconst link = findConversationLinkFromPush(notification);navigate(link); // ❌ link might be undefined
// DON'T: Block the message handlermessaging().setBackgroundMessageHandler(async message => { await slowAsyncOperation(); // ❌ Handler should be fast});
// DON'T: Show notifications when app is focused on that conversationif (activeConversationId === notificationConversationId) { return; // ✅ Don't show duplicate notification}Performance Considerations
Section titled “Performance Considerations”Notification Throttling
Section titled “Notification Throttling”Prevent notification spam:
const recentNotifications = new Set<string>();const THROTTLE_WINDOW = 5000; // 5 seconds
const shouldShowNotification = (notificationId: string): boolean => { if (recentNotifications.has(notificationId)) { return false; }
recentNotifications.add(notificationId); setTimeout(() => { recentNotifications.delete(notificationId); }, THROTTLE_WINDOW);
return true;};Batching Notifications
Section titled “Batching Notifications”Group notifications by conversation:
const groupNotifications = (notifications: Notification[]) => { const grouped = new Map<number, Notification[]>();
notifications.forEach(notif => { const convId = notif.primaryActor.conversationId; if (!grouped.has(convId)) { grouped.set(convId, []); } grouped.get(convId)!.push(notif); });
return grouped;};Testing Strategy
Section titled “Testing Strategy”Mock FCM
Section titled “Mock FCM”jest.mock('@react-native-firebase/messaging', () => ({ default: jest.fn(() => ({ requestPermission: jest.fn(() => Promise.resolve(1)), getToken: jest.fn(() => Promise.resolve('mock-token')), getInitialNotification: jest.fn(() => Promise.resolve(null)), onNotificationOpenedApp: jest.fn(), setBackgroundMessageHandler: jest.fn(), })),}));Test Notification Handling
Section titled “Test Notification Handling”describe('Push Notifications', () => { it('should extract conversation link from FCM message', () => { const message = { data: { payload: JSON.stringify({ data: { notification: { notification_type: 'assigned_conversation_new_message', primary_actor: { id: 123, conversation_id: 456 }, primary_actor_type: 'Message', }, }, }), }, };
const notification = findNotificationFromFCM({ message }); const link = findConversationLinkFromPush({ notification, installationUrl: 'https://app.Zelta Chat.com', });
expect(link).toContain('/conversations/456'); });});Extending This Concept
Section titled “Extending This Concept”Adding Notification Actions
Section titled “Adding Notification Actions”await notifee.displayNotification({ title: 'New Message', body: message.content, ios: { categoryId: 'message', foregroundPresentationOptions: { badge: true, sound: true, banner: true, }, },});
// Register category with actionsawait notifee.setNotificationCategories([ { id: 'message', actions: [ { id: 'reply', title: 'Reply', input: true, }, { id: 'mark-read', title: 'Mark as Read', }, ], },]);Adding Rich Notifications
Section titled “Adding Rich Notifications”await notifee.displayNotification({ title: 'New Message', body: message.content, ios: { attachments: [ { url: message.imageUrl, thumbnailTime: 3, }, ], },});Topic Subscriptions
Section titled “Topic Subscriptions”const subscribeToTopics = async (accountId: number, userId: number) => { await messaging().subscribeToTopic(`account_${accountId}`); await messaging().subscribeToTopic(`user_${userId}`);};
const unsubscribeFromTopics = async (accountId: number, userId: number) => { await messaging().unsubscribeFromTopic(`account_${accountId}`); await messaging().unsubscribeFromTopic(`user_${userId}`);};Further Reading
Section titled “Further Reading”- Navigation System - Deep linking integration
- Real-time Communication - WebSocket notifications
- Firebase Messaging Documentation
- Notifee Documentation