Skip to content

Notifications

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
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 Count

Push 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 Messages
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 Toast
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 State

Edge Cases:

  • Notification for deleted conversation
  • Duplicate notifications
  • Out-of-order notifications
  • Notification permission denied
  • FCM token registration failure

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';

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;
}

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
})
// Get all notifications
const notifications = useAppSelector(selectAllNotifications);
// Get filtered notifications
const filteredNotifications = useAppSelector(selectFilteredNotifications);
// Get unread count
const unreadCount = useAppSelector(selectUnreadNotificationCount);
// Get current filter
const filter = useAppSelector(selectNotificationFilter);
try {
await dispatch(notificationActions.fetchNotifications()).unwrap();
} catch (error) {
if (error.code === 'NETWORK_ERROR') {
// Retry once after delay
setTimeout(() => {
dispatch(notificationActions.fetchNotifications());
}, 3000);
}
}
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()
},
]
);
}
};
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
}
};
// Notification received
AnalyticsHelper.track('notification_received', {
notification_type: notificationType,
app_state: AppState.currentState,
});
// Notification opened
AnalyticsHelper.track('notification_opened', {
notification_id: notificationId,
notification_type: notificationType,
time_since_received: timeSinceReceived,
});
// Notification marked as read
AnalyticsHelper.track('notification_marked_read', {
notification_id: notificationId,
source: 'list' | 'direct_tap',
});
  • Notification delivery latency
  • Time to mark as read
  • Badge update latency
  • Deep link navigation time
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()}
/>
);
};
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>
);
};
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);
});
};
// Rich notifications with images
if (featureFlags.richNotifications) {
return <RichNotificationItem notification={notification} />;
}
// Notification grouping
if (featureFlags.notificationGrouping) {
return <GroupedNotificationList />;
}
  1. Phase 1: Conversation assignments
  2. Phase 2: New messages in assigned conversations
  3. Phase 3: Mentions
  4. Phase 4: Participating conversations
  5. Phase 5: System notifications