Skip to content

Contact Management

The Contact Management feature provides agents with comprehensive customer information, conversation history, and contact metadata to deliver personalized support.

User Story: As a support agent, I want to view customer contact details and conversation history so that I can provide contextual and personalized assistance.

Acceptance Criteria:

  • ✅ Display contact basic information (name, email, phone)
  • ✅ Show contact avatar
  • ✅ Display conversation history with contact
  • ✅ Show custom attributes
  • ✅ Display contact labels/tags
  • ✅ Edit contact information
  • ✅ View contact social profiles
  • ✅ Show contact activity timeline

Inputs:

  • Contact ID or conversation ID
  • Contact update data

Outputs:

  • Contact details
  • Conversation history
  • Custom attributes
  • Contact labels
User Taps Contact from Conversation
Navigate to ContactDetailsScreen({ conversationId })
Extract Contact from Conversation Metadata
Dispatch: fetchContactConversations(contactId)
API Request: GET /contacts/{id}/conversations
Transform Response Data
Store in Redux
Render Contact Details Screen
Display:
- Contact Information
- Custom Attributes
- Labels
- Conversation History
User Edits Contact Information
Validate Input
Dispatch: updateContact({ contactId, updates })
Optimistically Update Redux
API Request: PUT /contacts/{id}
├─ Success
│ ↓
│ Confirm Update
│ ↓
│ Show Success Toast
└─ Failure
Revert Optimistic Update
Show Error Message
WebSocket Event: contact.updated
ActionCable Receives Event
Extract Contact Data
Transform to camelCase
Dispatch: updateContact(contact)
Reducer Merges Updates into contacts.byId
Re-render Contact Details if Active

Edge Cases:

  • Contact deleted while viewing
  • Conflicting concurrent updates
  • Missing required fields
  • Invalid email/phone format
  • Network error during update

Request:

GET /api/v1/accounts/{accountId}/contacts/{contactId}/conversations

Response:

{
data: {
payload: Conversation[];
meta: {
count: number;
current_page: number;
};
}
}

Request:

PUT /api/v1/accounts/{accountId}/contacts/{contactId}
Body:
{
name?: string;
email?: string;
phone_number?: string;
custom_attributes?: Record<string, any>;
}

Response:

{
data: {
payload: {
contact: Contact;
};
}
}
interface Contact {
id: number;
name: string;
email?: string;
phone_number?: string;
thumbnail?: string;
additional_attributes?: Record<string, any>;
custom_attributes?: Record<string, any>;
last_activity_at: number;
created_at: string;
}

Fetch Contact Conversations:

contactConversationActions.fetchConversations({
contactId: number
})

Update Contact:

contactActions.updateContact({
contactId: number,
name?: string,
email?: string,
phoneNumber?: string,
customAttributes?: Record<string, any>
})
// Get contact by ID
const contact = useAppSelector(state =>
selectContactById(state, contactId)
);
// Get contact conversations
const conversations = useAppSelector(state =>
selectContactConversations(state, contactId)
);
// Get contact custom attributes
const attributes = useAppSelector(state =>
selectContactCustomAttributes(state, contactId)
);
const validateContactData = (data: UpdateContactPayload) => {
const errors: string[] = [];
if (data.email && !isValidEmail(data.email)) {
errors.push(I18n.t('ERRORS.INVALID_EMAIL'));
}
if (data.phoneNumber && !isValidPhone(data.phoneNumber)) {
errors.push(I18n.t('ERRORS.INVALID_PHONE'));
}
return errors;
};
try {
await dispatch(contactActions.updateContact(updates)).unwrap();
} catch (error) {
if (error.code === 'CONFLICT') {
Alert.alert(
I18n.t('CONTACT.UPDATE_CONFLICT_TITLE'),
I18n.t('CONTACT.UPDATE_CONFLICT_MESSAGE'),
[
{ text: I18n.t('RELOAD'), onPress: () => reloadContact() },
{ text: I18n.t('CANCEL'), style: 'cancel' },
]
);
}
}
// Contact viewed
AnalyticsHelper.track('contact_viewed', {
contact_id: contactId,
source: 'conversation',
});
// Contact updated
AnalyticsHelper.track('contact_updated', {
contact_id: contactId,
fields_updated: Object.keys(updates),
});
// Conversation history viewed
AnalyticsHelper.track('contact_history_viewed', {
contact_id: contactId,
conversation_count: conversations.length,
});
import React, { useEffect } from 'react';
import { ScrollView, View, Text } from 'react-native';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { contactConversationActions } from '@/store/contact/contactConversationActions';
interface ContactDetailsScreenProps {
route: { params: { conversationId: number } };
}
export const ContactDetailsScreen: React.FC<ContactDetailsScreenProps> = ({
route,
}) => {
const dispatch = useAppDispatch();
const { conversationId } = route.params;
const conversation = useAppSelector(state =>
selectConversationById(state, conversationId)
);
const contact = conversation?.meta?.sender;
const contactId = contact?.id;
const contactConversations = useAppSelector(state =>
selectContactConversations(state, contactId)
);
useEffect(() => {
if (contactId) {
dispatch(contactConversationActions.fetchConversations({ contactId }));
}
}, [contactId, dispatch]);
if (!contact) return null;
return (
<ScrollView>
<ContactBasicInfo contact={contact} />
<ContactMetaInformation contact={contact} />
<ContactConversationHistory conversations={contactConversations} />
</ScrollView>
);
};
import { Avatar } from '@/components-next';
export const ContactBasicInfo = ({ contact }: { contact: Contact }) => {
return (
<View style={tailwind('p-4 items-center')}>
<Avatar
source={{ uri: contact.thumbnail }}
size={80}
name={contact.name}
/>
<Text style={tailwind('text-gray-12 font-inter-medium-24 text-lg mt-4')}>
{contact.name}
</Text>
{contact.email && (
<Text style={tailwind('text-gray-11 text-md mt-1')}>
{contact.email}
</Text>
)}
{contact.phoneNumber && (
<Text style={tailwind('text-gray-11 text-md mt-1')}>
{contact.phoneNumber}
</Text>
)}
</View>
);
};