@parlr/react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +918 -0
- package/lib/commonjs/components/AttachmentPicker.js +292 -0
- package/lib/commonjs/components/AttachmentPicker.js.map +1 -0
- package/lib/commonjs/components/AttachmentPreview.js +200 -0
- package/lib/commonjs/components/AttachmentPreview.js.map +1 -0
- package/lib/commonjs/components/ChatBubble.js +391 -0
- package/lib/commonjs/components/ChatBubble.js.map +1 -0
- package/lib/commonjs/components/EmptyState.js +115 -0
- package/lib/commonjs/components/EmptyState.js.map +1 -0
- package/lib/commonjs/components/ParlrChat.js +745 -0
- package/lib/commonjs/components/ParlrChat.js.map +1 -0
- package/lib/commonjs/components/ParlrConversationList.js +509 -0
- package/lib/commonjs/components/ParlrConversationList.js.map +1 -0
- package/lib/commonjs/components/PreChatForm.js +263 -0
- package/lib/commonjs/components/PreChatForm.js.map +1 -0
- package/lib/commonjs/components/RichMessage.js +284 -0
- package/lib/commonjs/components/RichMessage.js.map +1 -0
- package/lib/commonjs/components/SatisfactionSurvey.js +292 -0
- package/lib/commonjs/components/SatisfactionSurvey.js.map +1 -0
- package/lib/commonjs/components/TypingIndicator.js +86 -0
- package/lib/commonjs/components/TypingIndicator.js.map +1 -0
- package/lib/commonjs/core/api.js +310 -0
- package/lib/commonjs/core/api.js.map +1 -0
- package/lib/commonjs/core/config.js +40 -0
- package/lib/commonjs/core/config.js.map +1 -0
- package/lib/commonjs/core/errors.js +73 -0
- package/lib/commonjs/core/errors.js.map +1 -0
- package/lib/commonjs/core/offlineQueue.js +89 -0
- package/lib/commonjs/core/offlineQueue.js.map +1 -0
- package/lib/commonjs/core/pushNotifications.js +21 -0
- package/lib/commonjs/core/pushNotifications.js.map +1 -0
- package/lib/commonjs/core/session.js +130 -0
- package/lib/commonjs/core/session.js.map +1 -0
- package/lib/commonjs/core/theme.js +110 -0
- package/lib/commonjs/core/theme.js.map +1 -0
- package/lib/commonjs/core/types.js +6 -0
- package/lib/commonjs/core/types.js.map +1 -0
- package/lib/commonjs/core/websocket.js +245 -0
- package/lib/commonjs/core/websocket.js.map +1 -0
- package/lib/commonjs/hooks/useChat.js +462 -0
- package/lib/commonjs/hooks/useChat.js.map +1 -0
- package/lib/commonjs/hooks/useParlr.js +44 -0
- package/lib/commonjs/hooks/useParlr.js.map +1 -0
- package/lib/commonjs/index.js +185 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/provider/ParlrContext.js +38 -0
- package/lib/commonjs/provider/ParlrContext.js.map +1 -0
- package/lib/commonjs/provider/ParlrProvider.js +256 -0
- package/lib/commonjs/provider/ParlrProvider.js.map +1 -0
- package/lib/module/components/AttachmentPicker.js +287 -0
- package/lib/module/components/AttachmentPicker.js.map +1 -0
- package/lib/module/components/AttachmentPreview.js +195 -0
- package/lib/module/components/AttachmentPreview.js.map +1 -0
- package/lib/module/components/ChatBubble.js +386 -0
- package/lib/module/components/ChatBubble.js.map +1 -0
- package/lib/module/components/EmptyState.js +110 -0
- package/lib/module/components/EmptyState.js.map +1 -0
- package/lib/module/components/ParlrChat.js +740 -0
- package/lib/module/components/ParlrChat.js.map +1 -0
- package/lib/module/components/ParlrConversationList.js +504 -0
- package/lib/module/components/ParlrConversationList.js.map +1 -0
- package/lib/module/components/PreChatForm.js +258 -0
- package/lib/module/components/PreChatForm.js.map +1 -0
- package/lib/module/components/RichMessage.js +280 -0
- package/lib/module/components/RichMessage.js.map +1 -0
- package/lib/module/components/SatisfactionSurvey.js +287 -0
- package/lib/module/components/SatisfactionSurvey.js.map +1 -0
- package/lib/module/components/TypingIndicator.js +81 -0
- package/lib/module/components/TypingIndicator.js.map +1 -0
- package/lib/module/core/api.js +305 -0
- package/lib/module/core/api.js.map +1 -0
- package/lib/module/core/config.js +36 -0
- package/lib/module/core/config.js.map +1 -0
- package/lib/module/core/errors.js +64 -0
- package/lib/module/core/errors.js.map +1 -0
- package/lib/module/core/offlineQueue.js +82 -0
- package/lib/module/core/offlineQueue.js.map +1 -0
- package/lib/module/core/pushNotifications.js +16 -0
- package/lib/module/core/pushNotifications.js.map +1 -0
- package/lib/module/core/session.js +122 -0
- package/lib/module/core/session.js.map +1 -0
- package/lib/module/core/theme.js +105 -0
- package/lib/module/core/theme.js.map +1 -0
- package/lib/module/core/types.js +4 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/core/websocket.js +241 -0
- package/lib/module/core/websocket.js.map +1 -0
- package/lib/module/hooks/useChat.js +458 -0
- package/lib/module/hooks/useChat.js.map +1 -0
- package/lib/module/hooks/useParlr.js +40 -0
- package/lib/module/hooks/useParlr.js.map +1 -0
- package/lib/module/index.js +58 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/provider/ParlrContext.js +35 -0
- package/lib/module/provider/ParlrContext.js.map +1 -0
- package/lib/module/provider/ParlrProvider.js +251 -0
- package/lib/module/provider/ParlrProvider.js.map +1 -0
- package/lib/typescript/commonjs/components/AttachmentPicker.d.ts +23 -0
- package/lib/typescript/commonjs/components/AttachmentPicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AttachmentPreview.d.ts +16 -0
- package/lib/typescript/commonjs/components/AttachmentPreview.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ChatBubble.d.ts +14 -0
- package/lib/typescript/commonjs/components/ChatBubble.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/EmptyState.d.ts +10 -0
- package/lib/typescript/commonjs/components/EmptyState.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ParlrChat.d.ts +34 -0
- package/lib/typescript/commonjs/components/ParlrChat.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ParlrConversationList.d.ts +17 -0
- package/lib/typescript/commonjs/components/ParlrConversationList.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/PreChatForm.d.ts +20 -0
- package/lib/typescript/commonjs/components/PreChatForm.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/RichMessage.d.ts +41 -0
- package/lib/typescript/commonjs/components/RichMessage.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts +17 -0
- package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/TypingIndicator.d.ts +7 -0
- package/lib/typescript/commonjs/components/TypingIndicator.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/api.d.ts +37 -0
- package/lib/typescript/commonjs/core/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/config.d.ts +9 -0
- package/lib/typescript/commonjs/core/config.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/errors.d.ts +35 -0
- package/lib/typescript/commonjs/core/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/offlineQueue.d.ts +16 -0
- package/lib/typescript/commonjs/core/offlineQueue.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/pushNotifications.d.ts +6 -0
- package/lib/typescript/commonjs/core/pushNotifications.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/session.d.ts +15 -0
- package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/theme.d.ts +43 -0
- package/lib/typescript/commonjs/core/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/types.d.ts +185 -0
- package/lib/typescript/commonjs/core/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/core/websocket.d.ts +17 -0
- package/lib/typescript/commonjs/core/websocket.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useChat.d.ts +35 -0
- package/lib/typescript/commonjs/hooks/useChat.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useParlr.d.ts +11 -0
- package/lib/typescript/commonjs/hooks/useParlr.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +30 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/provider/ParlrContext.d.ts +13 -0
- package/lib/typescript/commonjs/provider/ParlrContext.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider/ParlrProvider.d.ts +5 -0
- package/lib/typescript/commonjs/provider/ParlrProvider.d.ts.map +1 -0
- package/lib/typescript/module/components/AttachmentPicker.d.ts +23 -0
- package/lib/typescript/module/components/AttachmentPicker.d.ts.map +1 -0
- package/lib/typescript/module/components/AttachmentPreview.d.ts +16 -0
- package/lib/typescript/module/components/AttachmentPreview.d.ts.map +1 -0
- package/lib/typescript/module/components/ChatBubble.d.ts +14 -0
- package/lib/typescript/module/components/ChatBubble.d.ts.map +1 -0
- package/lib/typescript/module/components/EmptyState.d.ts +10 -0
- package/lib/typescript/module/components/EmptyState.d.ts.map +1 -0
- package/lib/typescript/module/components/ParlrChat.d.ts +34 -0
- package/lib/typescript/module/components/ParlrChat.d.ts.map +1 -0
- package/lib/typescript/module/components/ParlrConversationList.d.ts +17 -0
- package/lib/typescript/module/components/ParlrConversationList.d.ts.map +1 -0
- package/lib/typescript/module/components/PreChatForm.d.ts +20 -0
- package/lib/typescript/module/components/PreChatForm.d.ts.map +1 -0
- package/lib/typescript/module/components/RichMessage.d.ts +41 -0
- package/lib/typescript/module/components/RichMessage.d.ts.map +1 -0
- package/lib/typescript/module/components/SatisfactionSurvey.d.ts +17 -0
- package/lib/typescript/module/components/SatisfactionSurvey.d.ts.map +1 -0
- package/lib/typescript/module/components/TypingIndicator.d.ts +7 -0
- package/lib/typescript/module/components/TypingIndicator.d.ts.map +1 -0
- package/lib/typescript/module/core/api.d.ts +37 -0
- package/lib/typescript/module/core/api.d.ts.map +1 -0
- package/lib/typescript/module/core/config.d.ts +9 -0
- package/lib/typescript/module/core/config.d.ts.map +1 -0
- package/lib/typescript/module/core/errors.d.ts +35 -0
- package/lib/typescript/module/core/errors.d.ts.map +1 -0
- package/lib/typescript/module/core/offlineQueue.d.ts +16 -0
- package/lib/typescript/module/core/offlineQueue.d.ts.map +1 -0
- package/lib/typescript/module/core/pushNotifications.d.ts +6 -0
- package/lib/typescript/module/core/pushNotifications.d.ts.map +1 -0
- package/lib/typescript/module/core/session.d.ts +15 -0
- package/lib/typescript/module/core/session.d.ts.map +1 -0
- package/lib/typescript/module/core/theme.d.ts +43 -0
- package/lib/typescript/module/core/theme.d.ts.map +1 -0
- package/lib/typescript/module/core/types.d.ts +185 -0
- package/lib/typescript/module/core/types.d.ts.map +1 -0
- package/lib/typescript/module/core/websocket.d.ts +17 -0
- package/lib/typescript/module/core/websocket.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useChat.d.ts +35 -0
- package/lib/typescript/module/hooks/useChat.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useParlr.d.ts +11 -0
- package/lib/typescript/module/hooks/useParlr.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +30 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/provider/ParlrContext.d.ts +13 -0
- package/lib/typescript/module/provider/ParlrContext.d.ts.map +1 -0
- package/lib/typescript/module/provider/ParlrProvider.d.ts +5 -0
- package/lib/typescript/module/provider/ParlrProvider.d.ts.map +1 -0
- package/package.json +120 -0
- package/src/components/AttachmentPicker.tsx +310 -0
- package/src/components/AttachmentPreview.tsx +209 -0
- package/src/components/ChatBubble.tsx +424 -0
- package/src/components/EmptyState.tsx +118 -0
- package/src/components/ParlrChat.tsx +863 -0
- package/src/components/ParlrConversationList.tsx +559 -0
- package/src/components/PreChatForm.tsx +313 -0
- package/src/components/RichMessage.tsx +353 -0
- package/src/components/SatisfactionSurvey.tsx +333 -0
- package/src/components/TypingIndicator.tsx +89 -0
- package/src/core/api.ts +406 -0
- package/src/core/config.ts +39 -0
- package/src/core/errors.ts +68 -0
- package/src/core/offlineQueue.ts +94 -0
- package/src/core/pushNotifications.ts +22 -0
- package/src/core/session.ts +156 -0
- package/src/core/theme.ts +133 -0
- package/src/core/types.ts +237 -0
- package/src/core/websocket.ts +270 -0
- package/src/hooks/useChat.ts +534 -0
- package/src/hooks/useParlr.ts +43 -0
- package/src/index.ts +98 -0
- package/src/provider/ParlrContext.ts +40 -0
- package/src/provider/ParlrProvider.tsx +338 -0
package/src/core/api.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parlr React Native SDK - REST API Client
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Wraps all widget-facing REST endpoints with automatic retry, exponential
|
|
6
|
+
// backoff, session-token injection, and response validation.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import axios, {
|
|
10
|
+
type AxiosInstance,
|
|
11
|
+
type AxiosError,
|
|
12
|
+
type InternalAxiosRequestConfig,
|
|
13
|
+
} from 'axios';
|
|
14
|
+
import {
|
|
15
|
+
ParlrAuthError,
|
|
16
|
+
ParlrNetworkError,
|
|
17
|
+
ParlrValidationError,
|
|
18
|
+
} from './errors';
|
|
19
|
+
import type {
|
|
20
|
+
Attachment,
|
|
21
|
+
Conversation,
|
|
22
|
+
ParlrUser,
|
|
23
|
+
Message,
|
|
24
|
+
PaginatedResponse,
|
|
25
|
+
ResolvedConfig,
|
|
26
|
+
Session,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Retry configuration
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const MAX_RETRIES = 3;
|
|
34
|
+
const BASE_DELAY_MS = 500;
|
|
35
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
36
|
+
|
|
37
|
+
function delay(ms: number): Promise<void> {
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isRetryable(error: AxiosError): boolean {
|
|
42
|
+
if (!error.response) return true; // network error
|
|
43
|
+
return RETRYABLE_STATUS.has(error.response.status);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Response validators
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function validateSession(data: unknown): Session {
|
|
51
|
+
if (!data || typeof data !== 'object') {
|
|
52
|
+
throw new ParlrValidationError(
|
|
53
|
+
'Invalid session response: expected an object',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const d = data as Record<string, unknown>;
|
|
57
|
+
// Accept both "token" and "sessionToken" from the server.
|
|
58
|
+
const token = (typeof d.token === 'string' ? d.token : d.sessionToken) as
|
|
59
|
+
| string
|
|
60
|
+
| undefined;
|
|
61
|
+
if (
|
|
62
|
+
typeof token !== 'string' ||
|
|
63
|
+
typeof d.contactId !== 'string' ||
|
|
64
|
+
typeof d.workspaceId !== 'string' ||
|
|
65
|
+
typeof d.expiresAt !== 'string'
|
|
66
|
+
) {
|
|
67
|
+
throw new ParlrValidationError(
|
|
68
|
+
'Invalid session response: missing required fields (token, contactId, workspaceId, expiresAt)',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
token,
|
|
73
|
+
contactId: d.contactId,
|
|
74
|
+
workspaceId: d.workspaceId,
|
|
75
|
+
expiresAt: d.expiresAt,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateMessage(data: unknown): Message {
|
|
80
|
+
if (!data || typeof data !== 'object') {
|
|
81
|
+
throw new ParlrValidationError(
|
|
82
|
+
'Invalid message response: expected an object',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const d = data as Record<string, unknown>;
|
|
86
|
+
if (
|
|
87
|
+
typeof d.id !== 'string' ||
|
|
88
|
+
typeof d.conversationId !== 'string' ||
|
|
89
|
+
typeof d.senderType !== 'string'
|
|
90
|
+
) {
|
|
91
|
+
throw new ParlrValidationError(
|
|
92
|
+
'Invalid message response: missing required fields (id, conversationId, senderType)',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
id: d.id,
|
|
97
|
+
conversationId: d.conversationId,
|
|
98
|
+
content: typeof d.content === 'string' ? d.content : '',
|
|
99
|
+
contentType: typeof d.contentType === 'string' ? (d.contentType as Message['contentType']) : 'text',
|
|
100
|
+
senderType: d.senderType as Message['senderType'],
|
|
101
|
+
senderName: typeof d.senderName === 'string' ? d.senderName : undefined,
|
|
102
|
+
senderAvatarUrl: typeof d.senderAvatarUrl === 'string' ? d.senderAvatarUrl : undefined,
|
|
103
|
+
createdAt: typeof d.createdAt === 'string' ? d.createdAt : new Date().toISOString(),
|
|
104
|
+
clientId: typeof d.clientId === 'string' ? d.clientId : undefined,
|
|
105
|
+
attachments: Array.isArray(d.attachments) ? (d.attachments as Attachment[]) : [],
|
|
106
|
+
status: 'sent',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validateConversation(data: unknown): Conversation {
|
|
111
|
+
if (!data || typeof data !== 'object') {
|
|
112
|
+
throw new ParlrValidationError(
|
|
113
|
+
'Invalid conversation response: expected an object',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const d = data as Record<string, unknown>;
|
|
117
|
+
if (
|
|
118
|
+
typeof d.id !== 'string' ||
|
|
119
|
+
typeof d.status !== 'string'
|
|
120
|
+
) {
|
|
121
|
+
throw new ParlrValidationError(
|
|
122
|
+
'Invalid conversation response: missing required fields (id, status)',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id: d.id,
|
|
127
|
+
status: d.status as Conversation['status'],
|
|
128
|
+
subject: typeof d.subject === 'string' ? d.subject : null,
|
|
129
|
+
lastMessageAt: typeof d.lastMessageAt === 'string' ? d.lastMessageAt : null,
|
|
130
|
+
unreadCount: typeof d.unreadCount === 'number' ? d.unreadCount : 0,
|
|
131
|
+
assignee: d.assignee && typeof d.assignee === 'object' ? d.assignee as Conversation['assignee'] : null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Error mapper
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function mapAxiosError(error: AxiosError): Error {
|
|
140
|
+
if (!error.response) {
|
|
141
|
+
return new ParlrNetworkError(
|
|
142
|
+
error.message || 'Network request failed',
|
|
143
|
+
undefined,
|
|
144
|
+
{ cause: error },
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const status = error.response.status;
|
|
149
|
+
|
|
150
|
+
if (status === 401 || status === 403) {
|
|
151
|
+
return new ParlrAuthError(
|
|
152
|
+
`Authentication failed (HTTP ${status})`,
|
|
153
|
+
{ cause: error },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return new ParlrNetworkError(
|
|
158
|
+
`HTTP ${status}: ${error.message}`,
|
|
159
|
+
status,
|
|
160
|
+
{ cause: error },
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Client factory
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
export interface ParlrApiClient {
|
|
169
|
+
/** Create a new anonymous session. */
|
|
170
|
+
createSession(): Promise<Session>;
|
|
171
|
+
|
|
172
|
+
/** Identify (or re-identify) the contact behind the current session. */
|
|
173
|
+
identify(user: ParlrUser): Promise<void>;
|
|
174
|
+
|
|
175
|
+
/** List conversations for the current contact. */
|
|
176
|
+
listConversations(page?: number): Promise<PaginatedResponse<Conversation>>;
|
|
177
|
+
|
|
178
|
+
/** Create a new conversation (optionally with a first message). */
|
|
179
|
+
createConversation(initialMessage?: string): Promise<Conversation>;
|
|
180
|
+
|
|
181
|
+
/** Fetch messages for a conversation. */
|
|
182
|
+
getMessages(
|
|
183
|
+
conversationId: string,
|
|
184
|
+
page?: number,
|
|
185
|
+
): Promise<PaginatedResponse<Message>>;
|
|
186
|
+
|
|
187
|
+
/** Send a message within a conversation. */
|
|
188
|
+
sendMessage(
|
|
189
|
+
conversationId: string,
|
|
190
|
+
content: string,
|
|
191
|
+
clientId: string,
|
|
192
|
+
): Promise<Message>;
|
|
193
|
+
|
|
194
|
+
/** Notify the server that the contact is typing. */
|
|
195
|
+
sendTypingIndicator(
|
|
196
|
+
conversationId: string,
|
|
197
|
+
isTyping: boolean,
|
|
198
|
+
): Promise<void>;
|
|
199
|
+
|
|
200
|
+
/** Close a conversation. */
|
|
201
|
+
closeConversation(conversationId: string): Promise<Conversation>;
|
|
202
|
+
|
|
203
|
+
/** Reopen a closed conversation. */
|
|
204
|
+
reopenConversation(conversationId: string): Promise<Conversation>;
|
|
205
|
+
|
|
206
|
+
/** Upload an attachment to a conversation. */
|
|
207
|
+
uploadAttachment(conversationId: string, formData: FormData): Promise<Attachment>;
|
|
208
|
+
|
|
209
|
+
/** Subscribe to push notifications. */
|
|
210
|
+
subscribePush(token: string, platform: 'ios' | 'android'): Promise<void>;
|
|
211
|
+
|
|
212
|
+
/** Unsubscribe from push notifications. */
|
|
213
|
+
unsubscribePush(token: string): Promise<void>;
|
|
214
|
+
|
|
215
|
+
/** Submit a CSAT rating for a conversation. */
|
|
216
|
+
submitRating(conversationId: string, score: number, comment?: string): Promise<void>;
|
|
217
|
+
|
|
218
|
+
/** Search messages in a conversation. */
|
|
219
|
+
searchMessages(conversationId: string, query: string): Promise<PaginatedResponse<Message>>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a configured API client bound to a resolved config and an optional
|
|
224
|
+
* session token accessor (so the token can be refreshed externally).
|
|
225
|
+
*/
|
|
226
|
+
export function createApiClient(
|
|
227
|
+
config: ResolvedConfig,
|
|
228
|
+
getToken: () => string | null,
|
|
229
|
+
): ParlrApiClient {
|
|
230
|
+
const http: AxiosInstance = axios.create({
|
|
231
|
+
baseURL: config.apiBaseUrl,
|
|
232
|
+
timeout: 15_000,
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
Accept: 'application/json',
|
|
236
|
+
'X-Workspace-Id': config.workspaceId,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// --- Inject session token and identity token on every request --------------
|
|
241
|
+
http.interceptors.request.use((req: InternalAxiosRequestConfig) => {
|
|
242
|
+
const token = getToken();
|
|
243
|
+
if (token) {
|
|
244
|
+
req.headers.set('X-Session-Token', token);
|
|
245
|
+
}
|
|
246
|
+
if (config.identityToken) {
|
|
247
|
+
req.headers.set('X-Identity-Token', config.identityToken);
|
|
248
|
+
}
|
|
249
|
+
return req;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// --- Retry interceptor -----------------------------------------------------
|
|
253
|
+
http.interceptors.response.use(undefined, async (error: AxiosError) => {
|
|
254
|
+
const requestConfig = error.config as InternalAxiosRequestConfig & {
|
|
255
|
+
_retryCount?: number;
|
|
256
|
+
};
|
|
257
|
+
if (!requestConfig) throw mapAxiosError(error);
|
|
258
|
+
|
|
259
|
+
requestConfig._retryCount = requestConfig._retryCount ?? 0;
|
|
260
|
+
|
|
261
|
+
if (requestConfig._retryCount >= MAX_RETRIES || !isRetryable(error)) {
|
|
262
|
+
throw mapAxiosError(error);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
requestConfig._retryCount += 1;
|
|
266
|
+
const jitter = Math.random() * 200;
|
|
267
|
+
const backoff =
|
|
268
|
+
BASE_DELAY_MS * Math.pow(2, requestConfig._retryCount - 1) + jitter;
|
|
269
|
+
|
|
270
|
+
if (config.debug) {
|
|
271
|
+
console.log(
|
|
272
|
+
`[@parlr/react-native] Retry #${requestConfig._retryCount} after ${Math.round(backoff)}ms`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await delay(backoff);
|
|
277
|
+
return http.request(requestConfig);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// --- Public API methods ----------------------------------------------------
|
|
281
|
+
return {
|
|
282
|
+
async createSession(): Promise<Session> {
|
|
283
|
+
const { data } = await http.post('/sessions', {
|
|
284
|
+
workspaceId: config.workspaceId,
|
|
285
|
+
});
|
|
286
|
+
return validateSession(data);
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
async identify(user: ParlrUser): Promise<void> {
|
|
290
|
+
await http.post('/identify', user);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async listConversations(
|
|
294
|
+
page = 1,
|
|
295
|
+
): Promise<PaginatedResponse<Conversation>> {
|
|
296
|
+
const { data } = await http.get(
|
|
297
|
+
'/conversations',
|
|
298
|
+
{ params: { page } },
|
|
299
|
+
);
|
|
300
|
+
// Handle both paginated ({ data: [...], meta: {...} }) and plain array responses.
|
|
301
|
+
if (Array.isArray(data)) {
|
|
302
|
+
const convs = data.map(validateConversation);
|
|
303
|
+
return { data: convs, meta: { total: convs.length, page: 1, perPage: convs.length, hasMore: false } };
|
|
304
|
+
}
|
|
305
|
+
return data as PaginatedResponse<Conversation>;
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
async createConversation(initialMessage?: string): Promise<Conversation> {
|
|
309
|
+
const { data } = await http.post('/conversations', {
|
|
310
|
+
...(initialMessage ? { message: initialMessage } : {}),
|
|
311
|
+
});
|
|
312
|
+
return validateConversation(data);
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
async getMessages(
|
|
316
|
+
conversationId: string,
|
|
317
|
+
page = 1,
|
|
318
|
+
): Promise<PaginatedResponse<Message>> {
|
|
319
|
+
const { data } = await http.get(
|
|
320
|
+
`/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
321
|
+
{ params: { page } },
|
|
322
|
+
);
|
|
323
|
+
// Handle both paginated ({ data: [...], meta: {...} }) and plain array responses.
|
|
324
|
+
if (Array.isArray(data)) {
|
|
325
|
+
const messages = data.map(validateMessage);
|
|
326
|
+
return { data: messages, meta: { total: messages.length, page: 1, perPage: messages.length, hasMore: false } };
|
|
327
|
+
}
|
|
328
|
+
return data as PaginatedResponse<Message>;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async sendMessage(
|
|
332
|
+
conversationId: string,
|
|
333
|
+
content: string,
|
|
334
|
+
clientId: string,
|
|
335
|
+
): Promise<Message> {
|
|
336
|
+
const { data } = await http.post(
|
|
337
|
+
`/conversations/${encodeURIComponent(conversationId)}/messages`,
|
|
338
|
+
{ content, clientId },
|
|
339
|
+
);
|
|
340
|
+
return validateMessage(data);
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
async sendTypingIndicator(
|
|
344
|
+
conversationId: string,
|
|
345
|
+
isTyping: boolean,
|
|
346
|
+
): Promise<void> {
|
|
347
|
+
await http.post(
|
|
348
|
+
`/conversations/${encodeURIComponent(conversationId)}/typing`,
|
|
349
|
+
{ isTyping },
|
|
350
|
+
);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
async closeConversation(conversationId: string): Promise<Conversation> {
|
|
354
|
+
const { data } = await http.post(
|
|
355
|
+
`/conversations/${encodeURIComponent(conversationId)}/close`,
|
|
356
|
+
);
|
|
357
|
+
return validateConversation(data);
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async reopenConversation(conversationId: string): Promise<Conversation> {
|
|
361
|
+
const { data } = await http.post(
|
|
362
|
+
`/conversations/${encodeURIComponent(conversationId)}/reopen`,
|
|
363
|
+
);
|
|
364
|
+
return validateConversation(data);
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
async uploadAttachment(conversationId: string, formData: FormData): Promise<Attachment> {
|
|
368
|
+
const { data } = await http.post(
|
|
369
|
+
`/conversations/${encodeURIComponent(conversationId)}/attachments`,
|
|
370
|
+
formData,
|
|
371
|
+
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
|
372
|
+
);
|
|
373
|
+
return data as Attachment;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async subscribePush(token: string, platform: 'ios' | 'android'): Promise<void> {
|
|
377
|
+
await http.post('/push/subscribe', { token, platform });
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
async unsubscribePush(token: string): Promise<void> {
|
|
381
|
+
await http.post('/push/unsubscribe', { token });
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async submitRating(conversationId: string, score: number, comment?: string): Promise<void> {
|
|
385
|
+
await http.post(
|
|
386
|
+
`/conversations/${encodeURIComponent(conversationId)}/rating`,
|
|
387
|
+
{ score, comment },
|
|
388
|
+
);
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
async searchMessages(
|
|
392
|
+
conversationId: string,
|
|
393
|
+
query: string,
|
|
394
|
+
): Promise<PaginatedResponse<Message>> {
|
|
395
|
+
const { data } = await http.get(
|
|
396
|
+
`/conversations/${encodeURIComponent(conversationId)}/messages/search`,
|
|
397
|
+
{ params: { q: query } },
|
|
398
|
+
);
|
|
399
|
+
if (Array.isArray(data)) {
|
|
400
|
+
const messages = data.map(validateMessage);
|
|
401
|
+
return { data: messages, meta: { total: messages.length, page: 1, perPage: messages.length, hasMore: false } };
|
|
402
|
+
}
|
|
403
|
+
return data as PaginatedResponse<Message>;
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parlr React Native SDK - Configuration
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { ParlrConfig, ResolvedConfig } from './types';
|
|
6
|
+
|
|
7
|
+
const DEFAULTS = {
|
|
8
|
+
apiBaseUrl: 'https://api.parlr.chat/api/v1/widget',
|
|
9
|
+
// WebSocket disabled by default — the Java backend uses STOMP protocol,
|
|
10
|
+
// not the custom JSON protocol this SDK expects. Enable when the Rust
|
|
11
|
+
// Gateway is deployed: wsUrl: 'wss://ws.parlr.chat/ws'
|
|
12
|
+
wsUrl: '',
|
|
13
|
+
locale: 'fr',
|
|
14
|
+
debug: false,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a partial user-supplied config into a fully-qualified config with
|
|
19
|
+
* all defaults applied.
|
|
20
|
+
*
|
|
21
|
+
* @throws {Error} if `workspaceId` is missing or empty.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveConfig(partial: ParlrConfig): ResolvedConfig {
|
|
24
|
+
if (!partial.workspaceId || partial.workspaceId.trim().length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'[@parlr/react-native] workspaceId is required. ' +
|
|
27
|
+
'Pass it as a prop to <ParlrProvider workspaceId="ws_xxx" />.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
workspaceId: partial.workspaceId.trim(),
|
|
33
|
+
apiBaseUrl: (partial.apiBaseUrl ?? DEFAULTS.apiBaseUrl).replace(/\/+$/, ''),
|
|
34
|
+
wsUrl: (partial.wsUrl ?? DEFAULTS.wsUrl).replace(/\/+$/, ''),
|
|
35
|
+
locale: partial.locale ?? DEFAULTS.locale,
|
|
36
|
+
debug: partial.debug ?? DEFAULTS.debug,
|
|
37
|
+
identityToken: partial.identityToken,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parlr React Native SDK - Error Hierarchy
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Typed error classes for structured error handling throughout the SDK.
|
|
6
|
+
// Consumers can use `instanceof` checks to differentiate error types.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base error class for all Parlr SDK errors.
|
|
11
|
+
*/
|
|
12
|
+
export class ParlrError extends Error {
|
|
13
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
14
|
+
super(message, options);
|
|
15
|
+
this.name = 'ParlrError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Network-level error (timeout, DNS failure, no internet).
|
|
21
|
+
*/
|
|
22
|
+
export class ParlrNetworkError extends ParlrError {
|
|
23
|
+
public readonly statusCode?: number;
|
|
24
|
+
|
|
25
|
+
constructor(message: string, statusCode?: number, options?: ErrorOptions) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.name = 'ParlrNetworkError';
|
|
28
|
+
this.statusCode = statusCode;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authentication / session error (expired token, invalid session).
|
|
34
|
+
*/
|
|
35
|
+
export class ParlrAuthError extends ParlrError {
|
|
36
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
37
|
+
super(message, options);
|
|
38
|
+
this.name = 'ParlrAuthError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validation error (invalid payload from API or WebSocket).
|
|
44
|
+
*/
|
|
45
|
+
export class ParlrValidationError extends ParlrError {
|
|
46
|
+
public readonly field?: string;
|
|
47
|
+
|
|
48
|
+
constructor(message: string, field?: string, options?: ErrorOptions) {
|
|
49
|
+
super(message, options);
|
|
50
|
+
this.name = 'ParlrValidationError';
|
|
51
|
+
this.field = field;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* WebSocket connection error (failed to connect, unexpected close).
|
|
57
|
+
*/
|
|
58
|
+
export class ParlrConnectionError extends ParlrError {
|
|
59
|
+
public readonly code?: number;
|
|
60
|
+
public readonly reason?: string;
|
|
61
|
+
|
|
62
|
+
constructor(message: string, code?: number, reason?: string, options?: ErrorOptions) {
|
|
63
|
+
super(message, options);
|
|
64
|
+
this.name = 'ParlrConnectionError';
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.reason = reason;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parlr React Native SDK - Offline Message Queue
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// AsyncStorage-based queue for messages sent while offline. Messages are
|
|
6
|
+
// persisted and flushed automatically when connectivity is restored.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import type { ParlrApiClient } from './api';
|
|
10
|
+
|
|
11
|
+
export interface QueuedMessage {
|
|
12
|
+
conversationId: string;
|
|
13
|
+
content: string;
|
|
14
|
+
clientId: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const QUEUE_KEY = '@parlr/offline_queue';
|
|
19
|
+
|
|
20
|
+
// In-memory queue that mirrors persistent storage.
|
|
21
|
+
let memoryQueue: QueuedMessage[] = [];
|
|
22
|
+
|
|
23
|
+
// Lazy AsyncStorage accessor — returns null when unavailable.
|
|
24
|
+
function getAsyncStorage(): {
|
|
25
|
+
getItem: (key: string) => Promise<string | null>;
|
|
26
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
27
|
+
} | null {
|
|
28
|
+
try {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
return require('@react-native-async-storage/async-storage').default;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function persist(): Promise<void> {
|
|
37
|
+
try {
|
|
38
|
+
const storage = getAsyncStorage();
|
|
39
|
+
if (storage) {
|
|
40
|
+
await storage.setItem(QUEUE_KEY, JSON.stringify(memoryQueue));
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Persistence is best-effort — the in-memory queue is always available.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function hydrate(): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const storage = getAsyncStorage();
|
|
50
|
+
if (storage) {
|
|
51
|
+
const raw = await storage.getItem(QUEUE_KEY);
|
|
52
|
+
if (raw) {
|
|
53
|
+
memoryQueue = JSON.parse(raw) as QueuedMessage[];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Start with an empty queue if hydration fails.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hydrate on module load (non-blocking).
|
|
62
|
+
hydrate();
|
|
63
|
+
|
|
64
|
+
/** Enqueue a message for later delivery. */
|
|
65
|
+
export async function queueMessage(msg: QueuedMessage): Promise<void> {
|
|
66
|
+
memoryQueue.push(msg);
|
|
67
|
+
await persist();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Return all currently queued messages. */
|
|
71
|
+
export async function getQueuedMessages(): Promise<QueuedMessage[]> {
|
|
72
|
+
return [...memoryQueue];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Remove a single message from the queue by clientId. */
|
|
76
|
+
export async function removeFromQueue(clientId: string): Promise<void> {
|
|
77
|
+
memoryQueue = memoryQueue.filter((m) => m.clientId !== clientId);
|
|
78
|
+
await persist();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Attempt to send all queued messages via the API, removing successful ones. */
|
|
82
|
+
export async function flushQueue(api: ParlrApiClient): Promise<void> {
|
|
83
|
+
const snapshot = [...memoryQueue];
|
|
84
|
+
|
|
85
|
+
for (const msg of snapshot) {
|
|
86
|
+
try {
|
|
87
|
+
await api.sendMessage(msg.conversationId, msg.content, msg.clientId);
|
|
88
|
+
await removeFromQueue(msg.clientId);
|
|
89
|
+
} catch {
|
|
90
|
+
// Leave failed messages in the queue for the next flush attempt.
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parlr React Native SDK - Push Notification Helpers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
import type { ParlrApiClient } from './api';
|
|
6
|
+
|
|
7
|
+
/** Register a device push token with the Parlr backend. */
|
|
8
|
+
export async function registerPushToken(
|
|
9
|
+
api: ParlrApiClient,
|
|
10
|
+
token: string,
|
|
11
|
+
platform: 'ios' | 'android',
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
await api.subscribePush(token, platform);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Unregister a previously registered push token. */
|
|
17
|
+
export async function unregisterPushToken(
|
|
18
|
+
api: ParlrApiClient,
|
|
19
|
+
token: string,
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
await api.unsubscribePush(token);
|
|
22
|
+
}
|