Contact Management
Contact Management
Section titled “Contact Management”Summary
Section titled “Summary”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
Contact Details Load Flow
Section titled “Contact Details Load Flow”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 HistoryUpdate Contact Flow
Section titled “Update Contact Flow”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 MessageReal-time Contact Update Flow
Section titled “Real-time Contact Update Flow”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 ActiveEdge Cases:
- Contact deleted while viewing
- Conflicting concurrent updates
- Missing required fields
- Invalid email/phone format
- Network error during update
API/Contracts
Section titled “API/Contracts”Fetch Contact Conversations
Section titled “Fetch Contact Conversations”Request:
GET /api/v1/accounts/{accountId}/contacts/{contactId}/conversationsResponse:
{ data: { payload: Conversation[]; meta: { count: number; current_page: number; }; }}Update Contact
Section titled “Update Contact”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;}Redux Actions
Section titled “Redux Actions”Fetch Contact Conversations:
contactConversationActions.fetchConversations({ contactId: number})Update Contact:
contactActions.updateContact({ contactId: number, name?: string, email?: string, phoneNumber?: string, customAttributes?: Record<string, any>})Redux Selectors
Section titled “Redux Selectors”// Get contact by IDconst contact = useAppSelector(state => selectContactById(state, contactId));
// Get contact conversationsconst conversations = useAppSelector(state => selectContactConversations(state, contactId));
// Get contact custom attributesconst attributes = useAppSelector(state => selectContactCustomAttributes(state, contactId));Error Handling and Retries
Section titled “Error Handling and Retries”Validation Errors
Section titled “Validation Errors”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;};Update Conflicts
Section titled “Update Conflicts”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' }, ] ); }}Telemetry
Section titled “Telemetry”Tracked Events
Section titled “Tracked Events”// Contact viewedAnalyticsHelper.track('contact_viewed', { contact_id: contactId, source: 'conversation',});
// Contact updatedAnalyticsHelper.track('contact_updated', { contact_id: contactId, fields_updated: Object.keys(updates),});
// Conversation history viewedAnalyticsHelper.track('contact_history_viewed', { contact_id: contactId, conversation_count: conversations.length,});Example
Section titled “Example”Contact Details Screen
Section titled “Contact Details Screen”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> );};Contact Basic Info Component
Section titled “Contact Basic Info Component”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> );};Further Reading
Section titled “Further Reading”- Conversations - Conversation management
- State Management - Redux architecture