@parlr/react-native
v0.1.7
Published
Official Parlr live chat SDK for React Native
Maintainers
Readme
Features
| Feature | Description |
|---|---|
| Real-time messaging | WebSocket with automatic reconnection and exponential backoff |
| Optimistic UI | Messages appear instantly, sync in the background |
| Typing indicators | Show when agents are typing, send visitor typing events |
| Read receipts | Delivery status tracking (sending → sent → read) |
| Pre-built UI components | Drop-in <ParlrChat> and <ParlrConversationList> |
| Pre-chat forms | Collect visitor info (name, email, phone) before the first message |
| CSAT surveys | Satisfaction rating after conversation resolution |
| Speech-to-text | Voice dictation via microphone with streaming transcription (iOS & Android) |
| File attachments | Images and documents via expo-image-picker / expo-document-picker |
| Rich messages | Cards, carousels, quick replies rendered natively |
| Offline queue | Messages are queued and retried when connectivity returns |
| Push notifications | FCM (Android) and APNs (iOS) support |
| Device metadata | Platform, OS, screen size, locale, timezone — sent automatically |
| User identification | Flexible: name, firstName/lastName, email, externalId |
| HMAC verification | Prevent contact impersonation with server-side tokens |
| Dark mode | Automatic system detection + full theme customization |
| TypeScript-first | Complete type definitions for all APIs, hooks, and components |
| Tiny footprint | ~160 KB published size, 1 runtime dependency (axios) |
Works with Expo (managed & bare) and bare React Native projects.
Installation
npm install @parlr/react-native
# or
yarn add @parlr/react-nativeRequired peer dependency
npm install react-native-reanimatedOptional dependencies
# Secure session persistence (recommended)
npm install expo-secure-store
# SVG icons (recommended — emoji fallback without it)
npm install react-native-svg
# File & image attachments
npm install expo-image-picker expo-document-picker
# Speech-to-text (voice dictation)
npm install expo-speech-recognitionNote: Without
expo-secure-store, sessions are stored in memory and won't persist across app restarts. Users will get a new session each time.
Compatibility matrix
| Package | Minimum version | Required |
|---|---|---|
| react | 18.0.0 | Yes |
| react-native | 0.72.0 | Yes |
| react-native-reanimated | 3.0.0 | Yes |
| expo-secure-store | 13.0.0 | No |
| expo-image-picker | 15.0.0 | No |
| react-native-svg | 13.0.0 | No |
| expo-document-picker | 12.0.0 | No |
| expo-speech-recognition | 1.0.0 | No |
Quick Start
1. Wrap your app with the provider
import { ParlrProvider } from '@parlr/react-native';
export default function App() {
return (
<ParlrProvider workspaceId="your-workspace-id">
<Navigation />
</ParlrProvider>
);
}2. Add the chat screen
import { ParlrChat } from '@parlr/react-native';
function SupportScreen({ navigation }) {
return (
<ParlrChat
user={{
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
}}
onBack={() => navigation.goBack()}
/>
);
}3. Show unread count anywhere
import { useParlr } from '@parlr/react-native';
function SupportButton() {
const { unreadCount } = useParlr();
return (
<TouchableOpacity onPress={() => navigation.navigate('Support')}>
<Text>Support</Text>
{unreadCount > 0 && <Badge count={unreadCount} />}
</TouchableOpacity>
);
}That's it. The SDK handles session management, WebSocket connections, message delivery, and UI rendering automatically.
Components
<ParlrProvider>
Root wrapper that initializes the SDK, manages the session, and opens the WebSocket connection. Place it near the top of your component tree (above any screen that uses Parlr hooks or components).
<ParlrProvider
workspaceId="your-workspace-id"
locale="en"
debug={__DEV__}
theme={{ colors: { primary: '#E91E63' } }}
onError={(err) => Sentry.captureException(err)}
>
<App />
</ParlrProvider>| Prop | Type | Default | Description |
|---|---|---|---|
| workspaceId | string | required | Your Parlr workspace ID (find it in Settings > Channels > Mobile SDK) |
| apiBaseUrl | string | https://api.parlr.chat/api/v1/widget | REST API base URL |
| wsUrl | string | wss://ws.parlr.chat/ws | WebSocket endpoint |
| locale | string | "fr" | BCP-47 locale code ("fr", "en", "es", "de", "ar") |
| debug | boolean | false | Enable verbose console logging (API calls, WS events, session lifecycle) |
| theme | Partial<ParlrTheme> | auto | Theme overrides merged with system light/dark theme (see Theming) |
| identityToken | string | — | HMAC token for secure identity verification (see HMAC Verification) |
| onError | (error: ParlrError) => void | — | Global error callback for monitoring |
<ParlrChat>
Full-featured chat screen. Handles session initialization, WebSocket, message sending/receiving, typing indicators, attachments, and CSAT surveys — all in one component.
<ParlrChat
user={{
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
company: 'Acme Inc',
}}
onBack={() => navigation.goBack()}
headerTitle="Help & Support"
showPreChatForm={true}
preChatFields={['name', 'email']}
showSatisfactionSurvey={true}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| user | ParlrUser | — | Identify the user on mount (see User Identification) |
| conversationId | string | — | Resume an existing conversation (omit to start fresh) |
| onBack | () => void | — | Back button handler (shows back arrow when provided) |
| headerTitle | string | "Support" | Header bar title |
| placeholder | string | "Write a message..." | Input placeholder text |
| accentColor | string | theme.colors.primary | Accent color override |
| showHeader | boolean | true | Show/hide the header bar |
| showPreChatForm | boolean | false | Collect user info before the first message |
| preChatFields | Array<'name' \| 'email' \| 'phone'> | ['name', 'email'] | Which fields to show in the pre-chat form |
| showSatisfactionSurvey | boolean | true | Show CSAT survey after conversation closes |
| safeAreaBottom | number | 0 | Bottom safe-area inset so the input bar sits above the keyboard |
| allowSpeechToText | boolean | true | Enable microphone button for voice dictation |
| emptyStateTitle | string | auto | Custom title for the empty state (no messages yet) |
| emptyStateDescription | string | auto | Custom description for the empty state |
| onConversationClosed | () => void | — | Callback fired when a conversation is closed |
Built-in features: online/offline status indicator, three-dot menu (close/reopen), optimistic message sending, agent typing animation, attachment picker with preview, speech-to-text dictation, message pagination (infinite scroll), auto-scroll to latest, closed conversation banner, CSAT star rating.
<ParlrConversationList>
Displays a list of conversations with last message preview, unread badges, assignee avatar, and status indicator.
<ParlrConversationList
onSelectConversation={(id) => navigation.navigate('Chat', { id })}
onNewConversation={() => navigation.navigate('Chat')}
statusFilter="open"
/>| Prop | Type | Default | Description |
|---|---|---|---|
| onSelectConversation | (id: string) => void | required | Callback when tapping a conversation |
| onNewConversation | () => void | — | "New conversation" button handler |
| headerTitle | string | "Conversations" | Header title |
| statusFilter | 'open' \| 'closed' \| 'pending' | — | Filter conversations by status |
| showHeader | boolean | true | Show/hide header |
| accentColor | string | "#6366f1" | Accent color |
Lower-level components
These are used internally by ParlrChat but are exported for building custom UIs:
| Component | Description |
|---|---|
| <ChatBubble message={msg} /> | Single message bubble (agent left, contact right) |
| <TypingIndicator /> | Animated three-dot typing indicator |
| <EmptyState /> | Welcome screen shown when no messages exist |
| <PreChatForm onSubmit={fn} /> | Pre-chat information collection form |
| <SatisfactionSurvey onSubmit={fn} /> | CSAT 1–5 star rating survey |
| <AttachmentPicker onFilePicked={fn} /> | Photo / camera / document file picker |
| <AttachmentPreview file={f} onSend={fn} /> | Attachment preview before sending |
| <RichMessage content={rich} /> | Cards, carousels, quick replies |
| <SpeechButton onTranscript={fn} /> | Microphone button with pulse animation for voice dictation |
Hooks
useParlr()
Access SDK state from any component inside <ParlrProvider>.
const {
isReady, // true when SDK is initialized and session is active
isConnected, // true when WebSocket is connected
session, // Current session (token, contactId, workspaceId)
conversations, // All conversations for this contact
unreadCount, // Total unread messages across all conversations
identify, // Identify or update the current user
refreshConversations, // Force-refresh the conversation list
theme, // Resolved theme (light/dark + user overrides)
} = useParlr();Identifying a user:
await identify({
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
company: 'Acme Inc',
customAttributes: { plan: 'pro', mrr: 299 },
});useChat(conversationId?)
Manage a single conversation with real-time updates.
const {
messages, // Message array (oldest first)
isLoading, // true during initial load
hasError, // true if an error occurred
conversation, // Active conversation metadata
agentTyping, // true when an agent is typing (auto-clears after 5s)
sendMessage, // Send a text message (appears instantly via optimistic UI)
retryMessage, // Retry a failed message by clientId
notifyTyping, // Send typing indicator to the server (debounced 3s)
loadMore, // Load older messages (pagination)
hasMore, // true if more pages are available
closeConversation, // Close the conversation
reopenConversation, // Reopen a closed conversation
} = useChat(conversationId);Sending messages:
// Appears instantly with status "sending", then updates to "sent" on server confirmation
await sendMessage('Hello, I need help with my order');
// Retry a failed message
const failed = messages.find(m => m.status === 'failed');
if (failed?.clientId) {
await retryMessage(failed.clientId);
}How optimistic sending works:
sendMessage()inserts the message locally with statussending- The REST API is called in the background
- On success → status updates to
sent - On failure → status updates to
failed(retryable up to 3 times with exponential backoff: 1s, 2s, 4s) - When the same message arrives via WebSocket, it's deduplicated by
clientId - If no
conversationIdis provided, a new conversation is created automatically on the firstsendMessage()
User Identification
The SDK supports flexible user identification through the ParlrUser interface. You can identify users in two ways:
Option 1: Using name (simple)
Pass a single name field. The SDK automatically splits it into firstName and lastName before sending to the server.
<ParlrChat
user={{
email: '[email protected]',
name: 'Alice Martin', // Automatically split: firstName="Alice", lastName="Martin"
}}
/>Splitting rules:
"Alice Martin"→firstName: "Alice",lastName: "Martin""Alice"→firstName: "Alice"(no lastName)"Alice Marie Martin"→firstName: "Alice",lastName: "Marie Martin"" Alice Martin "→firstName: "Alice",lastName: "Martin"(trimmed)
Option 2: Using firstName / lastName (explicit)
For precise control, pass firstName and lastName directly. These take priority over name.
<ParlrChat
user={{
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
}}
/>Important: If neither
name,firstName, norlastNameare provided, the contact name in your Parlr dashboard will fall back to the email prefix (e.g.,[email protected]→alice.martin).
Full ParlrUser interface
interface ParlrUser {
email?: string; // Contact email
name?: string; // Full name (auto-split into firstName/lastName)
firstName?: string; // First name (takes priority over name)
lastName?: string; // Last name (takes priority over name)
externalId?: string; // Your internal user ID
phone?: string; // Phone number (E.164 format recommended)
company?: string; // Company name
customAttributes?: Record<string, unknown>; // Custom key-value pairs visible in the dashboard
}Custom attributes
Custom attributes appear in the Contact Details panel of your Parlr dashboard:
await identify({
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
company: 'Acme Inc',
customAttributes: {
plan: 'enterprise',
mrr: 2990,
signupDate: '2025-06-15',
accountManager: 'Bob',
},
});Device Metadata
The SDK automatically collects and sends device metadata when a session is created. No configuration needed.
| Data | Example | Dashboard panel |
|---|---|---|
| Platform | iOS, Android | Visitor Device |
| OS version | 18.2, 35 (Android API) | Visitor Device |
| Screen resolution | 430x932 | Visitor Device |
| Locale | fr-FR | Location |
| Timezone | Europe/Paris | Location |
| SDK version | ParlrSDK/1.0 | User-Agent |
This data appears in the Visitor Device and Location sections of each contact's profile in the Parlr dashboard.
Theming
The SDK automatically detects your system's dark/light mode preference. You can override any theme value.
Brand color only (most common)
<ParlrProvider
workspaceId="your-workspace-id"
theme={{
colors: {
primary: '#E91E63',
contactBubble: '#E91E63',
},
}}
>Full custom theme
<ParlrProvider
workspaceId="your-workspace-id"
theme={{
colors: {
primary: '#FF6B00',
primaryText: '#FFFFFF',
background: '#1A1A2E',
surface: '#16213E',
text: '#EAEAEA',
textSecondary: '#A0A0B0',
border: '#2A2A4A',
agentBubble: '#16213E',
agentText: '#EAEAEA',
contactBubble: '#FF6B00',
contactText: '#FFFFFF',
},
borderRadius: { bubble: 20, input: 16, button: 12, avatar: 24 },
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 },
typography: { headerSize: 20, bodySize: 16, captionSize: 12 },
}}
>Programmatic theme access
import { useParlr, defaultLightTheme, defaultDarkTheme, mergeTheme } from '@parlr/react-native';
// Access resolved theme in any component
const { theme } = useParlr();
<View style={{ backgroundColor: theme.colors.background }} />
// Build a theme from scratch
const custom = mergeTheme(defaultDarkTheme, { colors: { primary: '#FF6B00' } });interface ParlrTheme {
colors: {
primary: string; // Accent / brand color
primaryText: string; // Text on primary color
background: string; // Screen background
surface: string; // Card / input background
surfaceDark: string; // Darker surface (avatars, headers)
text: string; // Primary text color
textSecondary: string; // Secondary text, timestamps
border: string; // Borders, dividers
success: string; // Online indicator, success states
error: string; // Error states, failed messages
agentBubble: string; // Agent message bubble background
agentBubbleDark: string; // Reserved
agentText: string; // Agent message text color
agentTextDark: string; // Reserved
contactBubble: string; // Contact message bubble background
contactText: string; // Contact message text color
};
borderRadius: {
bubble: number; // Message bubbles (default: 16)
input: number; // Text input (default: 12)
button: number; // Buttons (default: 8)
avatar: number; // Avatars (default: 20)
};
spacing: {
xs: number; // 4
sm: number; // 8
md: number; // 16
lg: number; // 24
xl: number; // 32
};
typography: {
headerSize: number; // 18
bodySize: number; // 15
captionSize: number; // 12
};
}Error Handling
The SDK exports a typed error hierarchy for precise error handling:
import {
ParlrError, // Base class for all SDK errors
ParlrNetworkError, // HTTP errors (includes statusCode property)
ParlrAuthError, // 401/403 — expired or invalid session
ParlrValidationError, // Malformed server response (includes field property)
ParlrConnectionError, // WebSocket errors (includes code, reason properties)
} from '@parlr/react-native';Example: global error monitoring
<ParlrProvider
workspaceId="your-workspace-id"
onError={(error) => {
if (error instanceof ParlrAuthError) {
// Session expired — the SDK auto-refreshes, no action needed
} else if (error instanceof ParlrNetworkError) {
console.warn(`HTTP error: ${error.statusCode}`);
} else if (error instanceof ParlrConnectionError) {
console.warn(`WebSocket closed: ${error.code} ${error.reason}`);
}
// Send to your error tracking service
Sentry.captureException(error);
}}
>Offline Support
Messages sent while offline are queued locally and automatically retried when connectivity returns.
import {
queueMessage,
getQueuedMessages,
removeFromQueue,
flushQueue,
} from '@parlr/react-native';
// Manually queue a message
queueMessage({
conversationId: 'conv_123',
content: 'Hello',
clientId: 'uuid-v4',
timestamp: Date.now(),
});
// Inspect the queue
const queued = await getQueuedMessages();
console.log(`${queued.length} messages pending`);
// Flush (send all queued messages)
await flushQueue(api);Uses AsyncStorage when available, falls back to in-memory storage.
Push Notifications
Register device tokens for FCM (Android) or APNs (iOS) push notifications:
import { registerPushToken, unregisterPushToken } from '@parlr/react-native';
// After obtaining the device token from expo-notifications or @react-native-firebase/messaging
await registerPushToken(api, deviceToken, 'ios'); // or 'android'
// On logout or when the user disables notifications
await unregisterPushToken(api, deviceToken);Speech-to-Text
The SDK supports voice dictation via the device microphone, powered by expo-speech-recognition. When installed, a microphone button appears in the input bar between the text field and the send button.
Installation
npm install expo-speech-recognitioniOS — add to your Info.plist:
<key>NSSpeechRecognitionUsageDescription</key>
<string>Used for voice dictation in the chat</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used for voice dictation in the chat</string>Android — add to your AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" />Usage (integrated)
The microphone button is shown by default in <ParlrChat>. Disable it with:
<ParlrChat allowSpeechToText={false} />Usage (standalone)
import { SpeechButton, isSpeechAvailable } from '@parlr/react-native';
function MyCustomInput() {
const [text, setText] = useState('');
return (
<View style={{ flexDirection: 'row' }}>
<TextInput value={text} onChangeText={setText} />
<SpeechButton
onTranscript={setText}
locale="en-US"
accentColor="#6366f1"
theme={theme}
/>
</View>
);
}Graceful degradation
When expo-speech-recognition is not installed, <SpeechButton> returns null and the microphone button is automatically hidden. No error, no crash — the input bar works exactly as before.
import { isSpeechAvailable } from '@parlr/react-native';
if (isSpeechAvailable) {
console.log('Speech-to-text is available');
}Identity Verification (HMAC)
For production apps, use HMAC verification to prevent contact impersonation.
1. Generate the token server-side using your workspace secret:
// Node.js example
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', WORKSPACE_SECRET);
hmac.update(user.email);
const identityToken = hmac.digest('hex');2. Pass it to the provider:
<ParlrProvider
workspaceId="your-workspace-id"
identityToken={identityTokenFromYourBackend}
>The token is sent as X-Identity-Token on every request. The Parlr server validates it against your workspace secret before accepting the identification.
API Reference
Types
interface ParlrUser {
email?: string; // Contact email address
name?: string; // Full name (auto-split into firstName/lastName)
firstName?: string; // First name (takes priority over name)
lastName?: string; // Last name (takes priority over name)
externalId?: string; // Your internal user ID
phone?: string; // Phone number
company?: string; // Company name
customAttributes?: Record<string, unknown>; // Custom key-value pairs
}Name resolution order:
firstName/lastNameif provided → used directlynameif provided → auto-split on whitespace ("Alice Martin"→firstName: "Alice",lastName: "Martin")- Neither → dashboard shows email prefix as fallback
interface Session {
token: string; // JWT session token
contactId: string; // Unique contact ID
workspaceId: string; // Workspace ID
expiresAt: string; // ISO 8601 expiration timestamp
}interface Conversation {
id: string;
status: 'open' | 'resolved' | 'pending' | 'closed';
subject: string | null;
lastMessageAt: string | null; // ISO 8601
unreadCount: number;
assignee?: {
id: string;
name: string;
avatarUrl?: string;
} | null;
}interface Message {
id: string;
conversationId: string;
senderType: 'contact' | 'agent';
senderName?: string;
senderAvatarUrl?: string;
content: string | null; // Null for image/attachment-only messages
createdAt: string; // ISO 8601
clientId?: string; // For optimistic deduplication
status?: 'sending' | 'sent' | 'failed' | 'read';
attachments?: Attachment[];
contentType?: 'text' | 'image' | 'file' | 'rich';
metadata?: Record<string, unknown>;
}interface Attachment {
id: string;
name: string; // Original filename
url: string; // Download URL
contentType: string; // MIME type (e.g., "image/png")
size: number; // Size in bytes
}interface RichContent {
type: 'card' | 'carousel' | 'quick_replies' | 'buttons';
card?: RichCard;
carousel?: { cards: RichCard[] };
quickReplies?: { text?: string; replies: Array<{ label: string; value: string }> };
buttons?: RichAction[];
}
interface RichCard {
title?: string;
description?: string;
imageUrl?: string;
actions?: RichAction[];
}
interface RichAction {
type: 'link' | 'postback' | 'reply';
label: string;
value: string;
}interface WsEventMap {
auth_ok: { contactId: string };
auth_error: { code?: number; reason?: string; message?: string };
new_message: WsNewMessagePayload;
typing_start: { conversationId: string; senderType?: 'agent' | 'contact'; senderName?: string };
typing_stop: { conversationId: string; senderType?: 'agent' | 'contact'; senderName?: string };
message_seen: { conversationId: string; messageId: string; seenBy: string };
conversation_updated: { conversationId: string; status?: string; assignee?: object | null };
pong: {};
disconnected: { code?: number; reason?: string };
error: { code?: string; message?: string };
}Connection behavior:
- Auto-reconnect with exponential backoff (1s → 30s max)
- Keep-alive ping every 25s, server timeout at 50s
- Clean listener teardown on component unmount
All exports
// Components
import {
ParlrProvider,
ParlrChat,
ParlrConversationList,
ChatBubble,
TypingIndicator,
EmptyState,
PreChatForm,
SatisfactionSurvey,
AttachmentPicker,
AttachmentPreview,
RichMessage,
SpeechButton,
} from '@parlr/react-native';
// Hooks
import { useParlr, useChat } from '@parlr/react-native';
// Errors
import {
ParlrError,
ParlrNetworkError,
ParlrAuthError,
ParlrValidationError,
ParlrConnectionError,
} from '@parlr/react-native';
// Theme utilities
import { defaultLightTheme, defaultDarkTheme, mergeTheme } from '@parlr/react-native';
// Offline queue
import { queueMessage, getQueuedMessages, removeFromQueue, flushQueue } from '@parlr/react-native';
// Push notifications
import { registerPushToken, unregisterPushToken } from '@parlr/react-native';
// Speech-to-text
import { SpeechButton, isSpeechAvailable } from '@parlr/react-native';Examples
Minimal integration
import { ParlrProvider, ParlrChat } from '@parlr/react-native';
export default function App() {
return (
<ParlrProvider workspaceId="your-workspace-id">
<ParlrChat />
</ParlrProvider>
);
}With React Navigation
// App.tsx
import { ParlrProvider } from '@parlr/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<ParlrProvider workspaceId="your-workspace-id" locale="en">
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Support"
component={SupportScreen}
options={{ headerShown: false }}
/>
</Stack.Navigator>
</NavigationContainer>
</ParlrProvider>
);
}
// HomeScreen.tsx
import { useParlr } from '@parlr/react-native';
function HomeScreen({ navigation }) {
const { unreadCount } = useParlr();
return (
<View>
<TouchableOpacity onPress={() => navigation.navigate('Support')}>
<Text>Contact Support {unreadCount > 0 ? `(${unreadCount})` : ''}</Text>
</TouchableOpacity>
</View>
);
}
// SupportScreen.tsx
import { ParlrChat } from '@parlr/react-native';
function SupportScreen({ navigation }) {
return (
<ParlrChat
user={{
email: '[email protected]',
firstName: 'Alice',
lastName: 'Martin',
}}
onBack={() => navigation.goBack()}
headerTitle="Help & Support"
showPreChatForm={true}
/>
);
}With Expo Router
// app/_layout.tsx
import { ParlrProvider } from '@parlr/react-native';
import { Stack } from 'expo-router';
export default function Layout() {
return (
<ParlrProvider workspaceId="your-workspace-id" locale="fr">
<Stack />
</ParlrProvider>
);
}
// app/support.tsx
import { ParlrChat } from '@parlr/react-native';
import { useRouter } from 'expo-router';
export default function SupportScreen() {
const router = useRouter();
return (
<ParlrChat
user={{
email: '[email protected]',
name: 'Alice Martin',
}}
onBack={() => router.back()}
/>
);
}Conversation list + detail
import { ParlrConversationList, ParlrChat } from '@parlr/react-native';
// List screen
function ConversationsScreen({ navigation }) {
return (
<ParlrConversationList
onSelectConversation={(id) => navigation.navigate('Chat', { conversationId: id })}
onNewConversation={() => navigation.navigate('Chat')}
/>
);
}
// Chat screen
function ChatScreen({ route, navigation }) {
return (
<ParlrChat
conversationId={route.params?.conversationId}
onBack={() => navigation.goBack()}
/>
);
}Custom chat UI with hooks
import { useChat, ChatBubble, TypingIndicator } from '@parlr/react-native';
import { FlatList, TextInput, TouchableOpacity, View, Text } from 'react-native';
import { useState } from 'react';
function CustomChat({ conversationId }) {
const [text, setText] = useState('');
const {
messages, agentTyping, sendMessage, notifyTyping, loadMore, hasMore,
} = useChat(conversationId);
const handleSend = async () => {
if (!text.trim()) return;
await sendMessage(text.trim());
setText('');
};
return (
<View style={{ flex: 1 }}>
<FlatList
data={messages}
inverted
keyExtractor={(m) => m.id}
renderItem={({ item }) => <ChatBubble message={item} />}
onEndReached={() => hasMore && loadMore()}
/>
{agentTyping && <TypingIndicator />}
<View style={{ flexDirection: 'row', padding: 8 }}>
<TextInput
value={text}
onChangeText={(t) => { setText(t); notifyTyping(true); }}
style={{ flex: 1, borderWidth: 1, borderRadius: 8, padding: 8 }}
placeholder="Type a message..."
/>
<TouchableOpacity onPress={handleSend} style={{ padding: 8 }}>
<Text>Send</Text>
</TouchableOpacity>
</View>
</View>
);
}Advanced Configuration
Self-hosted Parlr
Point the SDK to your own Parlr instance:
<ParlrProvider
workspaceId="your-workspace-id"
apiBaseUrl="https://api.your-domain.com/api/v1/widget"
wsUrl="wss://ws.your-domain.com/ws"
>Debug mode
<ParlrProvider workspaceId="your-workspace-id" debug={true}>Logs to the console:
- REST API calls and responses
- WebSocket events (connect, disconnect, messages)
- Session lifecycle (create, refresh, expire)
- Retry attempts with backoff timing
FAQ
Sign up at app.parlr.chat, create a workspace, and find your workspace ID in Settings > Channels > Mobile SDK.
Yes. The SDK works with both Expo managed workflow and bare React Native. All optional dependencies (expo-secure-store, expo-image-picker, expo-document-picker) are standard Expo packages.
Messages are queued locally and automatically retried when connectivity returns. The WebSocket reconnects with exponential backoff (1s up to 30s). The UI continues to work normally — messages show with sending status until confirmed.
Yes. Use useParlr() and useChat() hooks to build a completely custom UI. The SDK handles all networking, state management, and real-time events for you.
Generate an HMAC-SHA256 token server-side using your workspace secret and the user's email. Pass it as identityToken to <ParlrProvider>. The server validates the token before accepting the identification — this prevents visitors from impersonating other contacts.
Make sure you're passing firstName/lastName (or name) in the user prop. If only email is provided, the dashboard falls back to displaying the email prefix (e.g., alice from [email protected]).
Changelog
0.1.7
- Fix: Speech-to-text API — corrected
expo-speech-recognitionintegration to useExpoSpeechRecognitionModule.addListener()instead of the non-existentaddSpeechRecognitionListenerexport. This fixes theaddSpeechRecognitionListener is not a functioncrash when pressing the microphone button.
0.1.6
- Speech-to-text — voice dictation via
expo-speech-recognition(optional peer dependency). A microphone button with pulse animation appears in the input bar. Streaming interim results are displayed in real-time. - New
<SpeechButton>component for standalone usage. - New
isSpeechAvailableutility to check availability at runtime. - New
allowSpeechToTextprop on<ParlrChat>(default:true). - New
MicrophoneIconSVG icon with emoji fallback.
0.1.5
- Fix: ChatBubble crash on image messages — fixed
Cannot read property 'length' of nullwhen a message contains only an image attachment (no text). TheMessage.contenttype is corrected fromstringtostring | nullto reflect the actual API contract.ChatBubblenow handlesnullcontent gracefully.
0.1.4
- SVG icons — all UI icons now use inline SVGs via
react-native-svg(optional) instead of emoji characters. Falls back to emoji gracefully ifreact-native-svgis not installed.
0.1.3
- Name auto-split — when passing
name(e.g.,"Alice Martin") withoutfirstName/lastName, the SDK now automatically splits it intofirstNameandlastNamebefore sending to the server. This fixes an issue where the Parlr dashboard would show only the email prefix instead of the full name.
0.1.2
- Documentation improvements and updated metadata.
0.1.1
- Device metadata — the SDK now automatically collects and sends device information (platform, OS version, screen resolution, locale, timezone) when creating a session. This data appears in the Visitor Device and Location panels of your Parlr dashboard.
- Internal improvements to session creation payload.
0.1.0
- Initial release with full chat UI, WebSocket real-time messaging, theming system, hooks API, offline queue, push notifications, pre-chat forms, CSAT surveys, file attachments, rich messages, and TypeScript support.
Support
- Documentation: parlr.chat/docs/sdk/react-native
- Issues & feature requests: [email protected]
License
MIT © Parlr
