@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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +918 -0
  3. package/lib/commonjs/components/AttachmentPicker.js +292 -0
  4. package/lib/commonjs/components/AttachmentPicker.js.map +1 -0
  5. package/lib/commonjs/components/AttachmentPreview.js +200 -0
  6. package/lib/commonjs/components/AttachmentPreview.js.map +1 -0
  7. package/lib/commonjs/components/ChatBubble.js +391 -0
  8. package/lib/commonjs/components/ChatBubble.js.map +1 -0
  9. package/lib/commonjs/components/EmptyState.js +115 -0
  10. package/lib/commonjs/components/EmptyState.js.map +1 -0
  11. package/lib/commonjs/components/ParlrChat.js +745 -0
  12. package/lib/commonjs/components/ParlrChat.js.map +1 -0
  13. package/lib/commonjs/components/ParlrConversationList.js +509 -0
  14. package/lib/commonjs/components/ParlrConversationList.js.map +1 -0
  15. package/lib/commonjs/components/PreChatForm.js +263 -0
  16. package/lib/commonjs/components/PreChatForm.js.map +1 -0
  17. package/lib/commonjs/components/RichMessage.js +284 -0
  18. package/lib/commonjs/components/RichMessage.js.map +1 -0
  19. package/lib/commonjs/components/SatisfactionSurvey.js +292 -0
  20. package/lib/commonjs/components/SatisfactionSurvey.js.map +1 -0
  21. package/lib/commonjs/components/TypingIndicator.js +86 -0
  22. package/lib/commonjs/components/TypingIndicator.js.map +1 -0
  23. package/lib/commonjs/core/api.js +310 -0
  24. package/lib/commonjs/core/api.js.map +1 -0
  25. package/lib/commonjs/core/config.js +40 -0
  26. package/lib/commonjs/core/config.js.map +1 -0
  27. package/lib/commonjs/core/errors.js +73 -0
  28. package/lib/commonjs/core/errors.js.map +1 -0
  29. package/lib/commonjs/core/offlineQueue.js +89 -0
  30. package/lib/commonjs/core/offlineQueue.js.map +1 -0
  31. package/lib/commonjs/core/pushNotifications.js +21 -0
  32. package/lib/commonjs/core/pushNotifications.js.map +1 -0
  33. package/lib/commonjs/core/session.js +130 -0
  34. package/lib/commonjs/core/session.js.map +1 -0
  35. package/lib/commonjs/core/theme.js +110 -0
  36. package/lib/commonjs/core/theme.js.map +1 -0
  37. package/lib/commonjs/core/types.js +6 -0
  38. package/lib/commonjs/core/types.js.map +1 -0
  39. package/lib/commonjs/core/websocket.js +245 -0
  40. package/lib/commonjs/core/websocket.js.map +1 -0
  41. package/lib/commonjs/hooks/useChat.js +462 -0
  42. package/lib/commonjs/hooks/useChat.js.map +1 -0
  43. package/lib/commonjs/hooks/useParlr.js +44 -0
  44. package/lib/commonjs/hooks/useParlr.js.map +1 -0
  45. package/lib/commonjs/index.js +185 -0
  46. package/lib/commonjs/index.js.map +1 -0
  47. package/lib/commonjs/package.json +1 -0
  48. package/lib/commonjs/provider/ParlrContext.js +38 -0
  49. package/lib/commonjs/provider/ParlrContext.js.map +1 -0
  50. package/lib/commonjs/provider/ParlrProvider.js +256 -0
  51. package/lib/commonjs/provider/ParlrProvider.js.map +1 -0
  52. package/lib/module/components/AttachmentPicker.js +287 -0
  53. package/lib/module/components/AttachmentPicker.js.map +1 -0
  54. package/lib/module/components/AttachmentPreview.js +195 -0
  55. package/lib/module/components/AttachmentPreview.js.map +1 -0
  56. package/lib/module/components/ChatBubble.js +386 -0
  57. package/lib/module/components/ChatBubble.js.map +1 -0
  58. package/lib/module/components/EmptyState.js +110 -0
  59. package/lib/module/components/EmptyState.js.map +1 -0
  60. package/lib/module/components/ParlrChat.js +740 -0
  61. package/lib/module/components/ParlrChat.js.map +1 -0
  62. package/lib/module/components/ParlrConversationList.js +504 -0
  63. package/lib/module/components/ParlrConversationList.js.map +1 -0
  64. package/lib/module/components/PreChatForm.js +258 -0
  65. package/lib/module/components/PreChatForm.js.map +1 -0
  66. package/lib/module/components/RichMessage.js +280 -0
  67. package/lib/module/components/RichMessage.js.map +1 -0
  68. package/lib/module/components/SatisfactionSurvey.js +287 -0
  69. package/lib/module/components/SatisfactionSurvey.js.map +1 -0
  70. package/lib/module/components/TypingIndicator.js +81 -0
  71. package/lib/module/components/TypingIndicator.js.map +1 -0
  72. package/lib/module/core/api.js +305 -0
  73. package/lib/module/core/api.js.map +1 -0
  74. package/lib/module/core/config.js +36 -0
  75. package/lib/module/core/config.js.map +1 -0
  76. package/lib/module/core/errors.js +64 -0
  77. package/lib/module/core/errors.js.map +1 -0
  78. package/lib/module/core/offlineQueue.js +82 -0
  79. package/lib/module/core/offlineQueue.js.map +1 -0
  80. package/lib/module/core/pushNotifications.js +16 -0
  81. package/lib/module/core/pushNotifications.js.map +1 -0
  82. package/lib/module/core/session.js +122 -0
  83. package/lib/module/core/session.js.map +1 -0
  84. package/lib/module/core/theme.js +105 -0
  85. package/lib/module/core/theme.js.map +1 -0
  86. package/lib/module/core/types.js +4 -0
  87. package/lib/module/core/types.js.map +1 -0
  88. package/lib/module/core/websocket.js +241 -0
  89. package/lib/module/core/websocket.js.map +1 -0
  90. package/lib/module/hooks/useChat.js +458 -0
  91. package/lib/module/hooks/useChat.js.map +1 -0
  92. package/lib/module/hooks/useParlr.js +40 -0
  93. package/lib/module/hooks/useParlr.js.map +1 -0
  94. package/lib/module/index.js +58 -0
  95. package/lib/module/index.js.map +1 -0
  96. package/lib/module/package.json +1 -0
  97. package/lib/module/provider/ParlrContext.js +35 -0
  98. package/lib/module/provider/ParlrContext.js.map +1 -0
  99. package/lib/module/provider/ParlrProvider.js +251 -0
  100. package/lib/module/provider/ParlrProvider.js.map +1 -0
  101. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts +23 -0
  102. package/lib/typescript/commonjs/components/AttachmentPicker.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts +16 -0
  104. package/lib/typescript/commonjs/components/AttachmentPreview.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/ChatBubble.d.ts +14 -0
  106. package/lib/typescript/commonjs/components/ChatBubble.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/EmptyState.d.ts +10 -0
  108. package/lib/typescript/commonjs/components/EmptyState.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/ParlrChat.d.ts +34 -0
  110. package/lib/typescript/commonjs/components/ParlrChat.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts +17 -0
  112. package/lib/typescript/commonjs/components/ParlrConversationList.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/components/PreChatForm.d.ts +20 -0
  114. package/lib/typescript/commonjs/components/PreChatForm.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/components/RichMessage.d.ts +41 -0
  116. package/lib/typescript/commonjs/components/RichMessage.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts +17 -0
  118. package/lib/typescript/commonjs/components/SatisfactionSurvey.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/components/TypingIndicator.d.ts +7 -0
  120. package/lib/typescript/commonjs/components/TypingIndicator.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/core/api.d.ts +37 -0
  122. package/lib/typescript/commonjs/core/api.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/core/config.d.ts +9 -0
  124. package/lib/typescript/commonjs/core/config.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/core/errors.d.ts +35 -0
  126. package/lib/typescript/commonjs/core/errors.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/core/offlineQueue.d.ts +16 -0
  128. package/lib/typescript/commonjs/core/offlineQueue.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/core/pushNotifications.d.ts +6 -0
  130. package/lib/typescript/commonjs/core/pushNotifications.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/core/session.d.ts +15 -0
  132. package/lib/typescript/commonjs/core/session.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/core/theme.d.ts +43 -0
  134. package/lib/typescript/commonjs/core/theme.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/core/types.d.ts +185 -0
  136. package/lib/typescript/commonjs/core/types.d.ts.map +1 -0
  137. package/lib/typescript/commonjs/core/websocket.d.ts +17 -0
  138. package/lib/typescript/commonjs/core/websocket.d.ts.map +1 -0
  139. package/lib/typescript/commonjs/hooks/useChat.d.ts +35 -0
  140. package/lib/typescript/commonjs/hooks/useChat.d.ts.map +1 -0
  141. package/lib/typescript/commonjs/hooks/useParlr.d.ts +11 -0
  142. package/lib/typescript/commonjs/hooks/useParlr.d.ts.map +1 -0
  143. package/lib/typescript/commonjs/index.d.ts +30 -0
  144. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  145. package/lib/typescript/commonjs/package.json +1 -0
  146. package/lib/typescript/commonjs/provider/ParlrContext.d.ts +13 -0
  147. package/lib/typescript/commonjs/provider/ParlrContext.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts +5 -0
  149. package/lib/typescript/commonjs/provider/ParlrProvider.d.ts.map +1 -0
  150. package/lib/typescript/module/components/AttachmentPicker.d.ts +23 -0
  151. package/lib/typescript/module/components/AttachmentPicker.d.ts.map +1 -0
  152. package/lib/typescript/module/components/AttachmentPreview.d.ts +16 -0
  153. package/lib/typescript/module/components/AttachmentPreview.d.ts.map +1 -0
  154. package/lib/typescript/module/components/ChatBubble.d.ts +14 -0
  155. package/lib/typescript/module/components/ChatBubble.d.ts.map +1 -0
  156. package/lib/typescript/module/components/EmptyState.d.ts +10 -0
  157. package/lib/typescript/module/components/EmptyState.d.ts.map +1 -0
  158. package/lib/typescript/module/components/ParlrChat.d.ts +34 -0
  159. package/lib/typescript/module/components/ParlrChat.d.ts.map +1 -0
  160. package/lib/typescript/module/components/ParlrConversationList.d.ts +17 -0
  161. package/lib/typescript/module/components/ParlrConversationList.d.ts.map +1 -0
  162. package/lib/typescript/module/components/PreChatForm.d.ts +20 -0
  163. package/lib/typescript/module/components/PreChatForm.d.ts.map +1 -0
  164. package/lib/typescript/module/components/RichMessage.d.ts +41 -0
  165. package/lib/typescript/module/components/RichMessage.d.ts.map +1 -0
  166. package/lib/typescript/module/components/SatisfactionSurvey.d.ts +17 -0
  167. package/lib/typescript/module/components/SatisfactionSurvey.d.ts.map +1 -0
  168. package/lib/typescript/module/components/TypingIndicator.d.ts +7 -0
  169. package/lib/typescript/module/components/TypingIndicator.d.ts.map +1 -0
  170. package/lib/typescript/module/core/api.d.ts +37 -0
  171. package/lib/typescript/module/core/api.d.ts.map +1 -0
  172. package/lib/typescript/module/core/config.d.ts +9 -0
  173. package/lib/typescript/module/core/config.d.ts.map +1 -0
  174. package/lib/typescript/module/core/errors.d.ts +35 -0
  175. package/lib/typescript/module/core/errors.d.ts.map +1 -0
  176. package/lib/typescript/module/core/offlineQueue.d.ts +16 -0
  177. package/lib/typescript/module/core/offlineQueue.d.ts.map +1 -0
  178. package/lib/typescript/module/core/pushNotifications.d.ts +6 -0
  179. package/lib/typescript/module/core/pushNotifications.d.ts.map +1 -0
  180. package/lib/typescript/module/core/session.d.ts +15 -0
  181. package/lib/typescript/module/core/session.d.ts.map +1 -0
  182. package/lib/typescript/module/core/theme.d.ts +43 -0
  183. package/lib/typescript/module/core/theme.d.ts.map +1 -0
  184. package/lib/typescript/module/core/types.d.ts +185 -0
  185. package/lib/typescript/module/core/types.d.ts.map +1 -0
  186. package/lib/typescript/module/core/websocket.d.ts +17 -0
  187. package/lib/typescript/module/core/websocket.d.ts.map +1 -0
  188. package/lib/typescript/module/hooks/useChat.d.ts +35 -0
  189. package/lib/typescript/module/hooks/useChat.d.ts.map +1 -0
  190. package/lib/typescript/module/hooks/useParlr.d.ts +11 -0
  191. package/lib/typescript/module/hooks/useParlr.d.ts.map +1 -0
  192. package/lib/typescript/module/index.d.ts +30 -0
  193. package/lib/typescript/module/index.d.ts.map +1 -0
  194. package/lib/typescript/module/package.json +1 -0
  195. package/lib/typescript/module/provider/ParlrContext.d.ts +13 -0
  196. package/lib/typescript/module/provider/ParlrContext.d.ts.map +1 -0
  197. package/lib/typescript/module/provider/ParlrProvider.d.ts +5 -0
  198. package/lib/typescript/module/provider/ParlrProvider.d.ts.map +1 -0
  199. package/package.json +120 -0
  200. package/src/components/AttachmentPicker.tsx +310 -0
  201. package/src/components/AttachmentPreview.tsx +209 -0
  202. package/src/components/ChatBubble.tsx +424 -0
  203. package/src/components/EmptyState.tsx +118 -0
  204. package/src/components/ParlrChat.tsx +863 -0
  205. package/src/components/ParlrConversationList.tsx +559 -0
  206. package/src/components/PreChatForm.tsx +313 -0
  207. package/src/components/RichMessage.tsx +353 -0
  208. package/src/components/SatisfactionSurvey.tsx +333 -0
  209. package/src/components/TypingIndicator.tsx +89 -0
  210. package/src/core/api.ts +406 -0
  211. package/src/core/config.ts +39 -0
  212. package/src/core/errors.ts +68 -0
  213. package/src/core/offlineQueue.ts +94 -0
  214. package/src/core/pushNotifications.ts +22 -0
  215. package/src/core/session.ts +156 -0
  216. package/src/core/theme.ts +133 -0
  217. package/src/core/types.ts +237 -0
  218. package/src/core/websocket.ts +270 -0
  219. package/src/hooks/useChat.ts +534 -0
  220. package/src/hooks/useParlr.ts +43 -0
  221. package/src/index.ts +98 -0
  222. package/src/provider/ParlrContext.ts +40 -0
  223. package/src/provider/ParlrProvider.tsx +338 -0
@@ -0,0 +1,313 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Pre-Chat Form
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Collects visitor identity (name, email) before starting a conversation.
6
+ // Shown when the user has not been identified yet.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React, { useCallback, useState } from 'react';
10
+ import {
11
+ ActivityIndicator,
12
+ KeyboardAvoidingView,
13
+ Platform,
14
+ Pressable,
15
+ StyleSheet,
16
+ Text,
17
+ TextInput,
18
+ View,
19
+ useColorScheme,
20
+ } from 'react-native';
21
+ import type { ParlrUser } from '../core/types';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Props
25
+ // ---------------------------------------------------------------------------
26
+
27
+ export interface PreChatFormProps {
28
+ /** Called when the form is submitted with user data. */
29
+ onSubmit: (user: ParlrUser) => Promise<void>;
30
+ /** Fields to collect. Default: ['name', 'email']. */
31
+ fields?: Array<'name' | 'email' | 'phone'>;
32
+ /** Title text. */
33
+ title?: string;
34
+ /** Description text. */
35
+ description?: string;
36
+ /** Submit button label. */
37
+ submitLabel?: string;
38
+ /** Accent color for the submit button. */
39
+ accentColor?: string;
40
+ /** Locale for default text. */
41
+ locale?: string;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // i18n
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function getI18n(locale?: string) {
49
+ if (locale?.startsWith('en')) {
50
+ return {
51
+ title: 'Welcome!',
52
+ description: 'Please introduce yourself so we can help you better.',
53
+ name: 'Your name',
54
+ email: 'Email address',
55
+ phone: 'Phone number',
56
+ submit: 'Start chat',
57
+ nameRequired: 'Name is required',
58
+ emailInvalid: 'Please enter a valid email',
59
+ };
60
+ }
61
+ return {
62
+ title: 'Bienvenue !',
63
+ description: 'Pr\u00e9sentez-vous pour que nous puissions mieux vous aider.',
64
+ name: 'Votre nom',
65
+ email: 'Adresse e-mail',
66
+ phone: 'Num\u00e9ro de t\u00e9l\u00e9phone',
67
+ submit: 'D\u00e9marrer le chat',
68
+ nameRequired: 'Le nom est requis',
69
+ emailInvalid: 'Veuillez entrer un e-mail valide',
70
+ };
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Validation
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function isValidEmail(email: string): boolean {
78
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Component
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export function PreChatForm({
86
+ onSubmit,
87
+ fields = ['name', 'email'],
88
+ title,
89
+ description,
90
+ submitLabel,
91
+ accentColor: accentColorProp,
92
+ locale,
93
+ }: PreChatFormProps) {
94
+ const isDark = useColorScheme() === 'dark';
95
+ const accentColor = accentColorProp ?? '#6366f1';
96
+ const i18n = getI18n(locale);
97
+
98
+ const [name, setName] = useState('');
99
+ const [email, setEmail] = useState('');
100
+ const [phone, setPhone] = useState('');
101
+ const [errors, setErrors] = useState<Record<string, string>>({});
102
+ const [isSubmitting, setIsSubmitting] = useState(false);
103
+
104
+ const validate = useCallback((): boolean => {
105
+ const newErrors: Record<string, string> = {};
106
+
107
+ if (fields.includes('name') && !name.trim()) {
108
+ newErrors.name = i18n.nameRequired;
109
+ }
110
+
111
+ if (fields.includes('email') && email.trim() && !isValidEmail(email.trim())) {
112
+ newErrors.email = i18n.emailInvalid;
113
+ }
114
+
115
+ setErrors(newErrors);
116
+ return Object.keys(newErrors).length === 0;
117
+ }, [fields, name, email, i18n]);
118
+
119
+ const handleSubmit = useCallback(async () => {
120
+ if (!validate()) return;
121
+
122
+ setIsSubmitting(true);
123
+ try {
124
+ const user: ParlrUser = {};
125
+ if (name.trim()) user.name = name.trim();
126
+ if (email.trim()) user.email = email.trim();
127
+ if (phone.trim()) user.phone = phone.trim();
128
+
129
+ await onSubmit(user);
130
+ } finally {
131
+ setIsSubmitting(false);
132
+ }
133
+ }, [validate, name, email, phone, onSubmit]);
134
+
135
+ return (
136
+ <KeyboardAvoidingView
137
+ style={[styles.root, isDark && styles.rootDark]}
138
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
139
+ >
140
+ <View style={styles.container}>
141
+ <Text style={[styles.title, isDark && styles.titleDark]}>
142
+ {title ?? i18n.title}
143
+ </Text>
144
+ <Text style={[styles.description, isDark && styles.descriptionDark]}>
145
+ {description ?? i18n.description}
146
+ </Text>
147
+
148
+ {fields.includes('name') && (
149
+ <View style={styles.fieldContainer}>
150
+ <TextInput
151
+ style={[
152
+ styles.input,
153
+ isDark ? styles.inputDark : styles.inputLight,
154
+ errors.name ? styles.inputError : undefined,
155
+ ]}
156
+ value={name}
157
+ onChangeText={setName}
158
+ placeholder={i18n.name}
159
+ placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
160
+ autoCapitalize="words"
161
+ autoComplete="name"
162
+ textContentType="name"
163
+ accessibilityLabel={i18n.name}
164
+ />
165
+ {errors.name && (
166
+ <Text style={styles.errorText}>{errors.name}</Text>
167
+ )}
168
+ </View>
169
+ )}
170
+
171
+ {fields.includes('email') && (
172
+ <View style={styles.fieldContainer}>
173
+ <TextInput
174
+ style={[
175
+ styles.input,
176
+ isDark ? styles.inputDark : styles.inputLight,
177
+ errors.email ? styles.inputError : undefined,
178
+ ]}
179
+ value={email}
180
+ onChangeText={setEmail}
181
+ placeholder={i18n.email}
182
+ placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
183
+ keyboardType="email-address"
184
+ autoCapitalize="none"
185
+ autoComplete="email"
186
+ textContentType="emailAddress"
187
+ accessibilityLabel={i18n.email}
188
+ />
189
+ {errors.email && (
190
+ <Text style={styles.errorText}>{errors.email}</Text>
191
+ )}
192
+ </View>
193
+ )}
194
+
195
+ {fields.includes('phone') && (
196
+ <View style={styles.fieldContainer}>
197
+ <TextInput
198
+ style={[
199
+ styles.input,
200
+ isDark ? styles.inputDark : styles.inputLight,
201
+ ]}
202
+ value={phone}
203
+ onChangeText={setPhone}
204
+ placeholder={i18n.phone}
205
+ placeholderTextColor={isDark ? '#64748b' : '#94a3b8'}
206
+ keyboardType="phone-pad"
207
+ autoComplete="tel"
208
+ textContentType="telephoneNumber"
209
+ accessibilityLabel={i18n.phone}
210
+ />
211
+ </View>
212
+ )}
213
+
214
+ <Pressable
215
+ onPress={handleSubmit}
216
+ disabled={isSubmitting}
217
+ style={[
218
+ styles.submitButton,
219
+ { backgroundColor: isSubmitting ? '#cbd5e1' : accentColor },
220
+ ]}
221
+ accessibilityRole="button"
222
+ accessibilityLabel={submitLabel ?? i18n.submit}
223
+ accessibilityState={{ disabled: isSubmitting }}
224
+ >
225
+ {isSubmitting ? (
226
+ <ActivityIndicator size="small" color="#ffffff" />
227
+ ) : (
228
+ <Text style={styles.submitText}>{submitLabel ?? i18n.submit}</Text>
229
+ )}
230
+ </Pressable>
231
+ </View>
232
+ </KeyboardAvoidingView>
233
+ );
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Styles
238
+ // ---------------------------------------------------------------------------
239
+
240
+ const styles = StyleSheet.create({
241
+ root: {
242
+ flex: 1,
243
+ backgroundColor: '#ffffff',
244
+ justifyContent: 'center',
245
+ },
246
+ rootDark: {
247
+ backgroundColor: '#0f172a',
248
+ },
249
+ container: {
250
+ paddingHorizontal: 24,
251
+ paddingVertical: 32,
252
+ },
253
+ title: {
254
+ fontSize: 24,
255
+ fontWeight: '700',
256
+ color: '#0f172a',
257
+ marginBottom: 8,
258
+ textAlign: 'center',
259
+ },
260
+ titleDark: {
261
+ color: '#f1f5f9',
262
+ },
263
+ description: {
264
+ fontSize: 15,
265
+ color: '#64748b',
266
+ textAlign: 'center',
267
+ lineHeight: 22,
268
+ marginBottom: 28,
269
+ },
270
+ descriptionDark: {
271
+ color: '#94a3b8',
272
+ },
273
+ fieldContainer: {
274
+ marginBottom: 16,
275
+ },
276
+ input: {
277
+ fontSize: 15,
278
+ paddingHorizontal: 16,
279
+ paddingVertical: 14,
280
+ borderRadius: 12,
281
+ borderWidth: 1,
282
+ },
283
+ inputLight: {
284
+ backgroundColor: '#f8fafc',
285
+ borderColor: '#e2e8f0',
286
+ color: '#0f172a',
287
+ },
288
+ inputDark: {
289
+ backgroundColor: '#1e293b',
290
+ borderColor: '#334155',
291
+ color: '#f1f5f9',
292
+ },
293
+ inputError: {
294
+ borderColor: '#ef4444',
295
+ },
296
+ errorText: {
297
+ fontSize: 12,
298
+ color: '#ef4444',
299
+ marginTop: 4,
300
+ marginLeft: 4,
301
+ },
302
+ submitButton: {
303
+ paddingVertical: 14,
304
+ borderRadius: 12,
305
+ alignItems: 'center',
306
+ marginTop: 8,
307
+ },
308
+ submitText: {
309
+ color: '#ffffff',
310
+ fontSize: 16,
311
+ fontWeight: '600',
312
+ },
313
+ });
@@ -0,0 +1,353 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parlr React Native SDK - Rich Message Renderer
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Renders rich message content types sent by automations or agents:
6
+ // cards, buttons, carousels, quick replies, etc.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import React from 'react';
10
+ import {
11
+ FlatList,
12
+ Image,
13
+ Linking,
14
+ Pressable,
15
+ StyleSheet,
16
+ Text,
17
+ View,
18
+ useColorScheme,
19
+ } from 'react-native';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface RichCard {
26
+ title?: string;
27
+ description?: string;
28
+ imageUrl?: string;
29
+ actions?: RichAction[];
30
+ }
31
+
32
+ export interface RichAction {
33
+ type: 'link' | 'postback' | 'reply';
34
+ label: string;
35
+ value: string;
36
+ }
37
+
38
+ export interface RichCarousel {
39
+ cards: RichCard[];
40
+ }
41
+
42
+ export interface RichQuickReplies {
43
+ text?: string;
44
+ replies: Array<{ label: string; value: string }>;
45
+ }
46
+
47
+ export interface RichContent {
48
+ type: 'card' | 'carousel' | 'quick_replies' | 'buttons';
49
+ card?: RichCard;
50
+ carousel?: RichCarousel;
51
+ quickReplies?: RichQuickReplies;
52
+ buttons?: RichAction[];
53
+ }
54
+
55
+ export interface RichMessageProps {
56
+ /** The rich content to render. Parsed from message metadata. */
57
+ content: RichContent;
58
+ /** Called when a postback/reply action is triggered. */
59
+ onAction?: (action: RichAction) => void;
60
+ /** Accent color for buttons. */
61
+ accentColor?: string;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Helpers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ function tryParseRichContent(metadata?: Record<string, unknown>): RichContent | null {
69
+ if (!metadata || typeof metadata !== 'object') return null;
70
+ if (metadata.richContent && typeof metadata.richContent === 'object') {
71
+ return metadata.richContent as RichContent;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ export { tryParseRichContent };
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Action Button
80
+ // ---------------------------------------------------------------------------
81
+
82
+ function ActionButton({
83
+ action,
84
+ onAction,
85
+ accentColor,
86
+ isDark: _isDark,
87
+ }: {
88
+ action: RichAction;
89
+ onAction?: (action: RichAction) => void;
90
+ accentColor: string;
91
+ isDark: boolean;
92
+ }) {
93
+ const handlePress = () => {
94
+ if (action.type === 'link') {
95
+ Linking.openURL(action.value).catch(() => {});
96
+ } else {
97
+ onAction?.(action);
98
+ }
99
+ };
100
+
101
+ return (
102
+ <Pressable
103
+ onPress={handlePress}
104
+ style={[styles.actionButton, { borderColor: accentColor }]}
105
+ accessibilityRole="button"
106
+ accessibilityLabel={action.label}
107
+ >
108
+ <Text style={[styles.actionLabel, { color: accentColor }]}>
109
+ {action.label}
110
+ </Text>
111
+ </Pressable>
112
+ );
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Card
117
+ // ---------------------------------------------------------------------------
118
+
119
+ function CardView({
120
+ card,
121
+ onAction,
122
+ accentColor,
123
+ isDark,
124
+ }: {
125
+ card: RichCard;
126
+ onAction?: (action: RichAction) => void;
127
+ accentColor: string;
128
+ isDark: boolean;
129
+ }) {
130
+ return (
131
+ <View style={[styles.card, isDark && styles.cardDark]}>
132
+ {card.imageUrl && (
133
+ <Image
134
+ source={{ uri: card.imageUrl }}
135
+ style={styles.cardImage}
136
+ resizeMode="cover"
137
+ accessibilityLabel={card.title ?? 'Card image'}
138
+ />
139
+ )}
140
+ {card.title && (
141
+ <Text style={[styles.cardTitle, isDark && styles.cardTitleDark]}>
142
+ {card.title}
143
+ </Text>
144
+ )}
145
+ {card.description && (
146
+ <Text style={[styles.cardDescription, isDark && styles.cardDescriptionDark]}>
147
+ {card.description}
148
+ </Text>
149
+ )}
150
+ {card.actions && card.actions.length > 0 && (
151
+ <View style={styles.cardActions}>
152
+ {card.actions.map((action, idx) => (
153
+ <ActionButton
154
+ key={idx}
155
+ action={action}
156
+ onAction={onAction}
157
+ accentColor={accentColor}
158
+ isDark={isDark}
159
+ />
160
+ ))}
161
+ </View>
162
+ )}
163
+ </View>
164
+ );
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Main component
169
+ // ---------------------------------------------------------------------------
170
+
171
+ export function RichMessage({
172
+ content,
173
+ onAction,
174
+ accentColor: accentColorProp,
175
+ }: RichMessageProps) {
176
+ const isDark = useColorScheme() === 'dark';
177
+ const accentColor = accentColorProp ?? '#6366f1';
178
+
179
+ switch (content.type) {
180
+ case 'card':
181
+ if (!content.card) return null;
182
+ return (
183
+ <View style={styles.container}>
184
+ <CardView
185
+ card={content.card}
186
+ onAction={onAction}
187
+ accentColor={accentColor}
188
+ isDark={isDark}
189
+ />
190
+ </View>
191
+ );
192
+
193
+ case 'carousel':
194
+ if (!content.carousel?.cards?.length) return null;
195
+ return (
196
+ <FlatList
197
+ horizontal
198
+ data={content.carousel.cards}
199
+ renderItem={({ item }) => (
200
+ <View style={styles.carouselItem}>
201
+ <CardView
202
+ card={item}
203
+ onAction={onAction}
204
+ accentColor={accentColor}
205
+ isDark={isDark}
206
+ />
207
+ </View>
208
+ )}
209
+ keyExtractor={(_, idx) => String(idx)}
210
+ showsHorizontalScrollIndicator={false}
211
+ contentContainerStyle={styles.carouselContainer}
212
+ snapToAlignment="start"
213
+ decelerationRate="fast"
214
+ />
215
+ );
216
+
217
+ case 'quick_replies':
218
+ if (!content.quickReplies) return null;
219
+ return (
220
+ <View style={styles.container}>
221
+ {content.quickReplies.text && (
222
+ <Text style={[styles.quickReplyText, isDark && styles.quickReplyTextDark]}>
223
+ {content.quickReplies.text}
224
+ </Text>
225
+ )}
226
+ <View style={styles.quickRepliesRow}>
227
+ {content.quickReplies.replies.map((reply, idx) => (
228
+ <Pressable
229
+ key={idx}
230
+ onPress={() => onAction?.({ type: 'reply', label: reply.label, value: reply.value })}
231
+ style={[styles.quickReply, { borderColor: accentColor }]}
232
+ accessibilityRole="button"
233
+ accessibilityLabel={reply.label}
234
+ >
235
+ <Text style={[styles.quickReplyLabel, { color: accentColor }]}>
236
+ {reply.label}
237
+ </Text>
238
+ </Pressable>
239
+ ))}
240
+ </View>
241
+ </View>
242
+ );
243
+
244
+ case 'buttons':
245
+ if (!content.buttons?.length) return null;
246
+ return (
247
+ <View style={styles.container}>
248
+ {content.buttons.map((action, idx) => (
249
+ <ActionButton
250
+ key={idx}
251
+ action={action}
252
+ onAction={onAction}
253
+ accentColor={accentColor}
254
+ isDark={isDark}
255
+ />
256
+ ))}
257
+ </View>
258
+ );
259
+
260
+ default:
261
+ return null;
262
+ }
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Styles
267
+ // ---------------------------------------------------------------------------
268
+
269
+ const styles = StyleSheet.create({
270
+ container: {
271
+ marginTop: 6,
272
+ },
273
+ card: {
274
+ backgroundColor: '#f8fafc',
275
+ borderRadius: 12,
276
+ overflow: 'hidden',
277
+ borderWidth: StyleSheet.hairlineWidth,
278
+ borderColor: '#e2e8f0',
279
+ },
280
+ cardDark: {
281
+ backgroundColor: '#1e293b',
282
+ borderColor: '#334155',
283
+ },
284
+ cardImage: {
285
+ width: '100%',
286
+ height: 140,
287
+ },
288
+ cardTitle: {
289
+ fontSize: 15,
290
+ fontWeight: '600',
291
+ color: '#0f172a',
292
+ paddingHorizontal: 12,
293
+ paddingTop: 10,
294
+ },
295
+ cardTitleDark: {
296
+ color: '#f1f5f9',
297
+ },
298
+ cardDescription: {
299
+ fontSize: 13,
300
+ color: '#64748b',
301
+ paddingHorizontal: 12,
302
+ paddingTop: 4,
303
+ lineHeight: 18,
304
+ },
305
+ cardDescriptionDark: {
306
+ color: '#94a3b8',
307
+ },
308
+ cardActions: {
309
+ padding: 10,
310
+ gap: 6,
311
+ },
312
+ actionButton: {
313
+ paddingVertical: 8,
314
+ paddingHorizontal: 14,
315
+ borderRadius: 8,
316
+ borderWidth: 1,
317
+ alignItems: 'center',
318
+ },
319
+ actionLabel: {
320
+ fontSize: 14,
321
+ fontWeight: '500',
322
+ },
323
+ carouselContainer: {
324
+ paddingHorizontal: 12,
325
+ gap: 10,
326
+ },
327
+ carouselItem: {
328
+ width: 240,
329
+ },
330
+ quickReplyText: {
331
+ fontSize: 14,
332
+ color: '#0f172a',
333
+ marginBottom: 8,
334
+ },
335
+ quickReplyTextDark: {
336
+ color: '#e2e8f0',
337
+ },
338
+ quickRepliesRow: {
339
+ flexDirection: 'row',
340
+ flexWrap: 'wrap',
341
+ gap: 6,
342
+ },
343
+ quickReply: {
344
+ paddingVertical: 6,
345
+ paddingHorizontal: 14,
346
+ borderRadius: 16,
347
+ borderWidth: 1,
348
+ },
349
+ quickReplyLabel: {
350
+ fontSize: 13,
351
+ fontWeight: '500',
352
+ },
353
+ });