Notifications
Notifications
Section titled “Notifications”Summary
Section titled “Summary”The Notifications feature provides agents with real-time alerts about conversation activity, assignments, and system events through both in-app notifications and push notifications.
User Story: As a support agent, I want to receive notifications about important events so that I can respond promptly to customer needs and team activities.
Acceptance Criteria:
- ✅ Display in-app notification list
- ✅ Filter notifications by type (assigned, mentioned, participating)
- ✅ Mark notifications as read
- ✅ Navigate to conversation from notification
- ✅ Receive push notifications when app is backgrounded
- ✅ Badge count on app icon (iOS)
- ✅ Clear all notifications
- ✅ Notification settings and preferences
Inputs:
- Notification type filter
- User preferences
- FCM push notification data
Outputs:
- Filtered notification list
- Unread notification count
- Navigation to related conversation
Notification List Load Flow
Section titled “Notification List Load Flow”User Opens Inbox/Notifications Tab ↓Dispatch: fetchNotifications() ↓API Request: GET /notification_subscriptions ↓Transform Response Data ↓Store in Redux (notifications.byId, allIds) ↓Calculate Unread Count ↓Render Notification List ↓Update Badge Count (iOS)Push Notification Received Flow (App Foreground)
Section titled “Push Notification Received Flow (App Foreground)”FCM Message Received ↓onMessage Handler ↓Extract Notification Data ↓Dispatch: addNotification(notification) ↓Update Redux Store ↓Increment Unread Count ↓Show In-App Notification Banner ↓Update Badge CountPush Notification Tapped Flow (App Background)
Section titled “Push Notification Tapped Flow (App Background)”User Taps Push Notification ↓App Brought to Foreground ↓onNotificationOpenedApp Handler ↓Extract Conversation ID ↓Build Deep Link URL ↓Navigate to ChatScreen({ conversationId }) ↓Mark Notification as Read ↓Load Conversation MessagesMark as Read Flow
Section titled “Mark as Read Flow”User Taps Notification Item ↓Dispatch: markAsRead({ notificationId, primaryActorId, primaryActorType }) ↓Optimistically Update Redux (read: true) ↓API Request: POST /notification_subscriptions/{id}/mark_as_read ↓├─ Success│ ↓│ Decrement Unread Count│ ↓│ Update Badge Count│ ↓│ Navigate to Conversation│└─ Failure ↓ Revert Optimistic Update ↓ Show Error ToastClear All Notifications Flow
Section titled “Clear All Notifications Flow”User Taps "Clear All" ↓Dispatch: clearAllNotifications() ↓API Request: DELETE /notification_subscriptions/all ↓Clear Redux Store ↓Reset Unread Count to 0 ↓Update Badge Count to 0 ↓Clear iOS Notification Center ↓Show Empty StateEdge Cases:
- Notification for deleted conversation
- Duplicate notifications
- Out-of-order notifications
- Notification permission denied
- FCM token registration failure
API/Contracts
Section titled “API/Contracts”Fetch Notifications
Section titled “Fetch Notifications”Request:
GET /api/v1/notification_subscriptions
Query Parameters:{ page?: number}Response:
{ data: { meta: { unread_count: number; count: number; current_page: number; }; payload: Notification[]; }}
interface Notification { id: number; notification_type: NotificationType; primary_actor_type: 'Conversation' | 'Message'; primary_actor_id: number; primary_actor: { id: number; conversation_id?: number; content?: string; }; read_at: string | null; created_at: string; push_message_title: string; account_id: number;}
type NotificationType = | 'conversation_creation' | 'conversation_assignment' | 'assigned_conversation_new_message' | 'conversation_mention' | 'participating_conversation_new_message';Mark as Read
Section titled “Mark as Read”Request:
POST /api/v1/notification_subscriptions/{id}/mark_as_read
Body:{ primary_actor_id: number; primary_actor_type: 'Conversation' | 'Message';}Response:
{ id: number; read_at: string;}Redux Actions
Section titled “Redux Actions”Fetch Notifications:
notificationActions.fetchNotifications({ page?: number})Mark as Read:
notificationActions.markAsRead({ notificationId: number, primaryActorId: number, primaryActorType: 'Conversation' | 'Message'})Add Notification (Real-time):
addNotification({ notification: NotificationCreatedResponse})Remove Notification (Real-time):
removeNotification({ notification: NotificationRemovedResponse})Redux Selectors
Section titled “Redux Selectors”// Get all notificationsconst notifications = useAppSelector(selectAllNotifications);
// Get filtered notificationsconst filteredNotifications = useAppSelector(selectFilteredNotifications);
// Get unread countconst unreadCount = useAppSelector(selectUnreadNotificationCount);
// Get current filterconst filter = useAppSelector(selectNotificationFilter);Error Handling and Retries
Section titled “Error Handling and Retries”Network Errors
Section titled “Network Errors”try { await dispatch(notificationActions.fetchNotifications()).unwrap();} catch (error) { if (error.code === 'NETWORK_ERROR') { // Retry once after delay setTimeout(() => { dispatch(notificationActions.fetchNotifications()); }, 3000); }}Push Notification Permission
Section titled “Push Notification Permission”const requestNotificationPermission = async () => { const authStatus = await messaging().requestPermission();
if (authStatus === messaging.AuthorizationStatus.DENIED) { Alert.alert( I18n.t('NOTIFICATIONS.PERMISSION_DENIED_TITLE'), I18n.t('NOTIFICATIONS.PERMISSION_DENIED_MESSAGE'), [ { text: I18n.t('CANCEL'), style: 'cancel' }, { text: I18n.t('OPEN_SETTINGS'), onPress: () => Linking.openSettings() }, ] ); }};FCM Token Registration
Section titled “FCM Token Registration”const registerFCMToken = async () => { try { const token = await messaging().getToken();
await apiService.post('notification_subscriptions', { fcm_token: token, device_type: Platform.OS, }); } catch (error) { console.error('Failed to register FCM token:', error); // Retry later setTimeout(registerFCMToken, 60000); // Retry after 1 minute }};Telemetry
Section titled “Telemetry”Tracked Events
Section titled “Tracked Events”// Notification receivedAnalyticsHelper.track('notification_received', { notification_type: notificationType, app_state: AppState.currentState,});
// Notification openedAnalyticsHelper.track('notification_opened', { notification_id: notificationId, notification_type: notificationType, time_since_received: timeSinceReceived,});
// Notification marked as readAnalyticsHelper.track('notification_marked_read', { notification_id: notificationId, source: 'list' | 'direct_tap',});Performance Metrics
Section titled “Performance Metrics”- Notification delivery latency
- Time to mark as read
- Badge update latency
- Deep link navigation time
Example
Section titled “Example”Notification List Component
Section titled “Notification List Component”import React, { useEffect } from 'react';import { FlatList, TouchableOpacity, Text, View } from 'react-native';import { useAppDispatch, useAppSelector } from '@/hooks';import { notificationActions } from '@/store/notification/notificationAction';import { selectAllNotifications } from '@/store/notification/notificationSelectors';import { navigate } from '@/utils/navigationUtils';
export const NotificationList = () => { const dispatch = useAppDispatch(); const notifications = useAppSelector(selectAllNotifications);
useEffect(() => { dispatch(notificationActions.fetchNotifications()); }, [dispatch]);
const handleNotificationPress = async (notification: Notification) => { // Mark as read await dispatch(notificationActions.markAsRead({ notificationId: notification.id, primaryActorId: notification.primaryActorId, primaryActorType: notification.primaryActorType, }));
// Navigate to conversation const conversationId = notification.primaryActorType === 'Conversation' ? notification.primaryActor.id : notification.primaryActor.conversationId;
if (conversationId) { navigate('ChatScreen', { conversationId }); } };
return ( <FlatList data={notifications} renderItem={({ item }) => ( <NotificationItem notification={item} onPress={() => handleNotificationPress(item)} /> )} keyExtractor={item => item.id.toString()} /> );};Notification Item Component
Section titled “Notification Item Component”import { tailwind } from '@/theme';
interface NotificationItemProps { notification: Notification; onPress: () => void;}
export const NotificationItem: React.FC<NotificationItemProps> = ({ notification, onPress,}) => { const isUnread = !notification.readAt;
return ( <TouchableOpacity onPress={onPress} style={tailwind.style( 'p-4 border-b border-gray-3', isUnread && 'bg-blue-2' )}> <View style={tailwind('flex-row items-center')}> {isUnread && ( <View style={tailwind('w-2 h-2 rounded-full bg-blue-9 mr-3')} /> )} <View style={tailwind('flex-1')}> <Text style={tailwind('text-gray-12 font-inter-medium-24')}> {notification.pushMessageTitle} </Text> <Text style={tailwind('text-gray-11 text-xs mt-1')}> {formatTimeAgo(notification.createdAt)} </Text> </View> </View> </TouchableOpacity> );};Push Notification Setup
Section titled “Push Notification Setup”import messaging from '@react-native-firebase/messaging';import { updateBadgeCount } from '@/utils/pushUtils';
export const setupPushNotifications = () => { // Request permission requestNotificationPermission();
// Register FCM token registerFCMToken();
// Handle foreground messages messaging().onMessage(async remoteMessage => { const notification = findNotificationFromFCM({ message: remoteMessage });
store.dispatch(addNotification(notification));
// Update badge count const unreadCount = selectUnreadNotificationCount(store.getState()); updateBadgeCount({ count: unreadCount });
// Show in-app notification showInAppNotification(notification); });
// Handle token refresh messaging().onTokenRefresh(async token => { await registerFCMToken(token); });};Rollout
Section titled “Rollout”Feature Flags
Section titled “Feature Flags”// Rich notifications with imagesif (featureFlags.richNotifications) { return <RichNotificationItem notification={notification} />;}
// Notification groupingif (featureFlags.notificationGrouping) { return <GroupedNotificationList />;}Notification Types Rollout
Section titled “Notification Types Rollout”- Phase 1: Conversation assignments
- Phase 2: New messages in assigned conversations
- Phase 3: Mentions
- Phase 4: Participating conversations
- Phase 5: System notifications
Further Reading
Section titled “Further Reading”- Conversations - Conversation management
- Push Notifications Concept - FCM integration
- Navigation - Deep linking from notifications