@experiaapp/webchat-react-native 2.0.1

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 (101) hide show
  1. package/README.md +254 -0
  2. package/app.plugin.js +6 -0
  3. package/lib/adapters/audio.d.ts +74 -0
  4. package/lib/adapters/audio.js +39 -0
  5. package/lib/adapters/audioRoute.d.ts +57 -0
  6. package/lib/adapters/audioRoute.js +77 -0
  7. package/lib/adapters/expoDefaults.d.ts +77 -0
  8. package/lib/adapters/expoDefaults.js +539 -0
  9. package/lib/adapters/picker.d.ts +67 -0
  10. package/lib/adapters/picker.js +37 -0
  11. package/lib/adapters/webrtc.d.ts +131 -0
  12. package/lib/adapters/webrtc.js +70 -0
  13. package/lib/core/VideoCallClient.d.ts +106 -0
  14. package/lib/core/VideoCallClient.js +302 -0
  15. package/lib/core/WebchatClient.d.ts +34 -0
  16. package/lib/core/WebchatClient.js +132 -0
  17. package/lib/core/configClient.d.ts +42 -0
  18. package/lib/core/configClient.js +302 -0
  19. package/lib/core/greet.d.ts +11 -0
  20. package/lib/core/greet.js +17 -0
  21. package/lib/core/ice.d.ts +31 -0
  22. package/lib/core/ice.js +48 -0
  23. package/lib/core/linkify.d.ts +11 -0
  24. package/lib/core/linkify.js +25 -0
  25. package/lib/core/logger.d.ts +17 -0
  26. package/lib/core/logger.js +53 -0
  27. package/lib/core/media.d.ts +52 -0
  28. package/lib/core/media.js +115 -0
  29. package/lib/core/mediaType.d.ts +21 -0
  30. package/lib/core/mediaType.js +66 -0
  31. package/lib/core/messagesReducer.d.ts +36 -0
  32. package/lib/core/messagesReducer.js +58 -0
  33. package/lib/core/persistence.d.ts +45 -0
  34. package/lib/core/persistence.js +63 -0
  35. package/lib/core/socketFactory.d.ts +16 -0
  36. package/lib/core/socketFactory.js +82 -0
  37. package/lib/core/types.d.ts +320 -0
  38. package/lib/core/types.js +30 -0
  39. package/lib/core/unread.d.ts +2 -0
  40. package/lib/core/unread.js +5 -0
  41. package/lib/i18n/ar.json +1 -0
  42. package/lib/i18n/en.json +1 -0
  43. package/lib/i18n/index.d.ts +7 -0
  44. package/lib/i18n/index.js +43 -0
  45. package/lib/index.d.ts +59 -0
  46. package/lib/index.js +142 -0
  47. package/lib/plugin/withWebchat.d.ts +53 -0
  48. package/lib/plugin/withWebchat.js +164 -0
  49. package/lib/state/WebchatProvider.d.ts +132 -0
  50. package/lib/state/WebchatProvider.js +906 -0
  51. package/lib/state/useWebchat.d.ts +1 -0
  52. package/lib/state/useWebchat.js +12 -0
  53. package/lib/theme/dir.d.ts +14 -0
  54. package/lib/theme/dir.js +20 -0
  55. package/lib/theme/themeFactory.d.ts +219 -0
  56. package/lib/theme/themeFactory.js +182 -0
  57. package/lib/ui/AttachButton.d.ts +35 -0
  58. package/lib/ui/AttachButton.js +26 -0
  59. package/lib/ui/AudioRecorder.d.ts +25 -0
  60. package/lib/ui/AudioRecorder.js +228 -0
  61. package/lib/ui/Bubble.d.ts +1 -0
  62. package/lib/ui/Bubble.js +265 -0
  63. package/lib/ui/CallControls.d.ts +27 -0
  64. package/lib/ui/CallControls.js +92 -0
  65. package/lib/ui/CallPlaceholder.d.ts +16 -0
  66. package/lib/ui/CallPlaceholder.js +73 -0
  67. package/lib/ui/Composer.d.ts +5 -0
  68. package/lib/ui/Composer.js +272 -0
  69. package/lib/ui/FileTile.d.ts +9 -0
  70. package/lib/ui/FileTile.js +31 -0
  71. package/lib/ui/Header.d.ts +52 -0
  72. package/lib/ui/Header.js +236 -0
  73. package/lib/ui/Icon.d.ts +21 -0
  74. package/lib/ui/Icon.js +110 -0
  75. package/lib/ui/ImageBubble.d.ts +11 -0
  76. package/lib/ui/ImageBubble.js +16 -0
  77. package/lib/ui/MediaUploadMenu.d.ts +23 -0
  78. package/lib/ui/MediaUploadMenu.js +68 -0
  79. package/lib/ui/MessageList.d.ts +1 -0
  80. package/lib/ui/MessageList.js +46 -0
  81. package/lib/ui/PoweredBy.d.ts +8 -0
  82. package/lib/ui/PoweredBy.js +14 -0
  83. package/lib/ui/PrechatForm.d.ts +1 -0
  84. package/lib/ui/PrechatForm.js +230 -0
  85. package/lib/ui/QuickReplies.d.ts +1 -0
  86. package/lib/ui/QuickReplies.js +24 -0
  87. package/lib/ui/TypingIndicator.d.ts +9 -0
  88. package/lib/ui/TypingIndicator.js +88 -0
  89. package/lib/ui/VideoBubble.d.ts +10 -0
  90. package/lib/ui/VideoBubble.js +130 -0
  91. package/lib/ui/VideoCall.d.ts +34 -0
  92. package/lib/ui/VideoCall.js +191 -0
  93. package/lib/ui/VideoTile.d.ts +25 -0
  94. package/lib/ui/VideoTile.js +13 -0
  95. package/lib/ui/VoiceMessage.d.ts +19 -0
  96. package/lib/ui/VoiceMessage.js +127 -0
  97. package/lib/ui/WebChat.d.ts +10 -0
  98. package/lib/ui/WebChat.js +386 -0
  99. package/lib/ui/openLink.d.ts +1 -0
  100. package/lib/ui/openLink.js +16 -0
  101. package/package.json +94 -0
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PrechatForm = PrechatForm;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ // src/ui/PrechatForm.tsx
6
+ //
7
+ // Pre-chat form at web parity (audit Section 3, Tier B #22). Renders the
8
+ // server-configured (or host-supplied) `fields[]` with per-field type support
9
+ // (text / email / phone / dropdown), `{en,ar}` localized label/placeholder/
10
+ // errorMessage, a localized headline + submit label, RTL via `dirStyles(dir)`,
11
+ // an in-form language toggle, inline error-clear on change, and a
12
+ // submitting/disabled state. Validation (required + email/phone regex) is ported
13
+ // verbatim from the web widget's `redux/setting/actions.ts`.
14
+ //
15
+ // On a valid submit it calls `onSubmit(values)` with the collected values keyed
16
+ // by `field.key` — exactly the shape `core/greet.ts` (`buildGreet`) consumes, so
17
+ // the greet plumbing stays untouched.
18
+ const react_1 = require("react");
19
+ const react_native_1 = require("react-native");
20
+ const dir_1 = require("../theme/dir");
21
+ // ---- Validation regexes (PORTED VERBATIM from web actions.ts:84-85) ----
22
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
23
+ const PHONE_RE = /^\+?[\d\s\-()]{6,}$/;
24
+ // Localized-text resolver: web reads `text[lang] || text.en`. We additionally
25
+ // accept a plain string (back-compat with hosts/tests that pass `label: "Name"`)
26
+ // and tolerate an absent value.
27
+ function localize(text, lang) {
28
+ if (text == null)
29
+ return "";
30
+ if (typeof text === "string")
31
+ return text;
32
+ return text[lang] || text.en || "";
33
+ }
34
+ // Default per-field error (web parity: actions.ts validatePrechatField). Used
35
+ // only when the field has no `errorMessage[lang]` override.
36
+ function defaultError(kind, lang) {
37
+ if (kind === "email") {
38
+ return lang === "ar" ? "بريد إلكتروني غير صالح" : "Invalid email address";
39
+ }
40
+ if (kind === "phone") {
41
+ return lang === "ar" ? "رقم هاتف غير صالح" : "Invalid phone number";
42
+ }
43
+ return lang === "ar" ? "هذا الحقل مطلوب" : "This field is required";
44
+ }
45
+ // Per-field validation mirroring web validatePrechatField (actions.ts:87-116):
46
+ // required (default true -> required !== false) empty-check first, then the
47
+ // type-specific regex on a non-empty trimmed value. Returns the localized
48
+ // error string or null when valid.
49
+ function validateField(field, value, lang) {
50
+ const trimmed = (value !== null && value !== void 0 ? value : "").trim();
51
+ const required = field.required !== false;
52
+ const custom = (kind) => localize(field.errorMessage, lang) || defaultError(kind, lang);
53
+ if (required && trimmed === "")
54
+ return custom("required");
55
+ if (trimmed === "")
56
+ return null; // optional + empty -> valid
57
+ if (field.type === "email" && !EMAIL_RE.test(trimmed))
58
+ return custom("email");
59
+ if (field.type === "phone" && !PHONE_RE.test(trimmed))
60
+ return custom("phone");
61
+ return null;
62
+ }
63
+ // RN keyboardType per field type (web parity: inputMode email/tel/text).
64
+ function keyboardTypeFor(type) {
65
+ if (type === "email")
66
+ return "email-address";
67
+ if (type === "phone")
68
+ return "phone-pad";
69
+ return "default";
70
+ }
71
+ // Custom RN dropdown: a Pressable anchor opening a Modal bottom-sheet listing the
72
+ // localized options (reuses the MediaUploadMenu sheet pattern — NO native picker
73
+ // peer). The selected option's localized label shows in the anchor; the
74
+ // placeholder shows when nothing is selected.
75
+ function Dropdown({ field, value, lang, dir, theme, hasError, onSelect }) {
76
+ var _a, _b, _c, _d, _e, _f, _g;
77
+ const [open, setOpen] = (0, react_1.useState)(false);
78
+ const d = (0, dir_1.dirStyles)(dir);
79
+ const options = (_a = field.options) !== null && _a !== void 0 ? _a : [];
80
+ const placeholder = localize(field.placeholder, lang);
81
+ const selected = options.find((o) => o.value === value);
82
+ const display = selected ? localize(selected.label, lang) : placeholder;
83
+ const textColor = (_c = (_b = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : "#161616";
84
+ const muted = "#9DA4AE";
85
+ const borderColor = hasError ? "#B42318" : "#9DA4AE";
86
+ const surface = (_e = (_d = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _d === void 0 ? void 0 : _d.surface) !== null && _e !== void 0 ? _e : "#FFFFFF";
87
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { testID: `prechat-field-${field.key}`, accessibilityRole: "button", accessibilityLabel: localize(field.label, lang), onPress: () => setOpen(true), style: {
88
+ flexDirection: d.rowDirection,
89
+ alignItems: "center",
90
+ justifyContent: "space-between",
91
+ borderWidth: 1,
92
+ borderColor,
93
+ borderRadius: 4,
94
+ minHeight: 40,
95
+ paddingHorizontal: 12,
96
+ backgroundColor: surface,
97
+ }, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
98
+ flex: 1,
99
+ color: selected ? textColor : muted,
100
+ textAlign: d.textAlign,
101
+ fontSize: (_g = (_f = theme === null || theme === void 0 ? void 0 : theme.font) === null || _f === void 0 ? void 0 : _f.size) !== null && _g !== void 0 ? _g : 16,
102
+ }, children: display }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: muted, marginHorizontal: 4 }, children: "\u25BE" })] }), (0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: open, transparent: true, animationType: "fade", onRequestClose: () => setOpen(false), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-dropdown-backdrop-${field.key}`, accessibilityRole: "button", onPress: () => setOpen(false), style: {
103
+ flex: 1,
104
+ backgroundColor: "rgba(0,0,0,0.35)",
105
+ justifyContent: "flex-end",
106
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-dropdown-sheet-${field.key}`, accessibilityViewIsModal: true, accessibilityRole: "menu", onPress: () => { }, style: {
107
+ backgroundColor: surface,
108
+ borderTopLeftRadius: 12,
109
+ borderTopRightRadius: 12,
110
+ paddingVertical: 8,
111
+ }, children: options.map((opt) => {
112
+ var _a, _b;
113
+ const isSel = opt.value === value;
114
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-option-${field.key}-${opt.value}`, accessibilityRole: "menuitem", accessibilityState: { selected: isSel }, accessibilityLabel: localize(opt.label, lang), onPress: () => {
115
+ onSelect(opt.value);
116
+ setOpen(false);
117
+ }, style: {
118
+ paddingVertical: 14,
119
+ paddingHorizontal: 20,
120
+ backgroundColor: isSel ? "#F3F4F6" : "transparent",
121
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
122
+ color: textColor,
123
+ textAlign: d.textAlign,
124
+ fontSize: (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.font) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 16,
125
+ fontWeight: isSel ? "600" : "400",
126
+ }, children: localize(opt.label, lang) }) }, opt.value));
127
+ }) }) }) })] }));
128
+ }
129
+ function PrechatForm({ fields, onSubmit, dir, theme, headline, submitLabel,
130
+ // Initial language: ar when dir is rtl (web derives both from the same source);
131
+ // the in-form toggle then drives `lang` independently of the host dir.
132
+ language, }) {
133
+ var _a, _b, _c, _d, _e, _f, _g, _h;
134
+ const initialLang = language === "ar" || (language == null && dir === "rtl") ? "ar" : "en";
135
+ const [lang, setLang] = (0, react_1.useState)(initialLang);
136
+ const [values, setValues] = (0, react_1.useState)({});
137
+ const [errors, setErrors] = (0, react_1.useState)({});
138
+ const [submitting, setSubmitting] = (0, react_1.useState)(false);
139
+ // The toggle flips the displayed language; direction follows it (web parity —
140
+ // the form direction tracks the form language, not the host dir).
141
+ const effectiveDir = lang === "ar" ? "rtl" : "ltr";
142
+ const d = (0, dir_1.dirStyles)(effectiveDir);
143
+ const fieldList = Array.isArray(fields) ? fields : [];
144
+ const primary = (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _a === void 0 ? void 0 : _a.primary) !== null && _b !== void 0 ? _b : "#1B8354";
145
+ const textColor = (_d = (_c = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : "#161616";
146
+ const surface = (_f = (_e = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _e === void 0 ? void 0 : _e.surface) !== null && _f !== void 0 ? _f : "#FFFFFF";
147
+ const fontSize = (_h = (_g = theme === null || theme === void 0 ? void 0 : theme.font) === null || _g === void 0 ? void 0 : _g.size) !== null && _h !== void 0 ? _h : 16;
148
+ const headlineText = localize(headline, lang);
149
+ const submitText = localize(submitLabel, lang) || (lang === "ar" ? "ابدأ المحادثة" : "Start chat");
150
+ const toggleLabel = lang === "ar" ? "EN" : "AR";
151
+ const setValue = (key, v) => {
152
+ setValues((prev) => ({ ...prev, [key]: v }));
153
+ // Inline error-clear on change (web ChatContainer parity).
154
+ setErrors((prev) => {
155
+ if (!prev[key])
156
+ return prev;
157
+ const next = { ...prev };
158
+ delete next[key];
159
+ return next;
160
+ });
161
+ };
162
+ const submit = () => {
163
+ if (submitting)
164
+ return;
165
+ const nextErrors = {};
166
+ fieldList.forEach((f) => {
167
+ var _a;
168
+ const err = validateField(f, (_a = values[f.key]) !== null && _a !== void 0 ? _a : "", lang);
169
+ if (err)
170
+ nextErrors[f.key] = err;
171
+ });
172
+ setErrors(nextErrors);
173
+ if (Object.keys(nextErrors).length > 0)
174
+ return; // block submit until valid
175
+ setSubmitting(true);
176
+ onSubmit(values);
177
+ };
178
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { testID: "prechat-form", style: { padding: 16, flexDirection: "column" }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection, marginBottom: 8 }, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "prechat-language-toggle", accessibilityRole: "button", accessibilityLabel: lang === "ar" ? "Switch to English" : "التبديل إلى العربية", onPress: () => setLang((l) => (l === "ar" ? "en" : "ar")), style: {
179
+ borderWidth: 1,
180
+ borderColor: primary,
181
+ borderRadius: 999,
182
+ paddingVertical: 4,
183
+ paddingHorizontal: 10,
184
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: primary, fontWeight: "600", fontSize: fontSize - 2 }, children: toggleLabel }) }) }), headlineText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { testID: "prechat-headline", style: {
185
+ color: textColor,
186
+ fontSize: fontSize + 4,
187
+ fontWeight: "600",
188
+ textAlign: d.textAlign,
189
+ marginBottom: 16,
190
+ }, children: headlineText })) : null, fieldList.map((f) => {
191
+ var _a, _b, _c;
192
+ const label = localize(f.label, lang);
193
+ const placeholder = localize(f.placeholder, lang);
194
+ const required = f.required !== false;
195
+ const error = (_a = errors[f.key]) !== null && _a !== void 0 ? _a : null;
196
+ const isDropdown = f.type === "dropdown";
197
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { marginBottom: 16 }, children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { testID: `prechat-label-${f.key}`, style: {
198
+ color: textColor,
199
+ fontSize,
200
+ fontWeight: "500",
201
+ textAlign: d.textAlign,
202
+ marginBottom: 6,
203
+ }, children: [label, required ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#B42318" }, children: " *" }) : null] }), isDropdown ? ((0, jsx_runtime_1.jsx)(Dropdown, { field: f, value: (_b = values[f.key]) !== null && _b !== void 0 ? _b : "", lang: lang, dir: effectiveDir, theme: theme, hasError: Boolean(error), onSelect: (v) => setValue(f.key, v) })) : ((0, jsx_runtime_1.jsx)(react_native_1.TextInput, { testID: `prechat-field-${f.key}`, accessibilityLabel: label, value: (_c = values[f.key]) !== null && _c !== void 0 ? _c : "", onChangeText: (t) => setValue(f.key, t), placeholder: placeholder, placeholderTextColor: "#9DA4AE", editable: !submitting, keyboardType: keyboardTypeFor(f.type), autoCapitalize: f.type === "email" ? "none" : "sentences", autoCorrect: f.type !== "email", style: {
204
+ borderWidth: 1,
205
+ borderColor: error ? "#B42318" : "#9DA4AE",
206
+ borderRadius: 4,
207
+ minHeight: 40,
208
+ paddingHorizontal: 12,
209
+ paddingVertical: 8,
210
+ color: textColor,
211
+ fontSize,
212
+ textAlign: d.textAlign,
213
+ backgroundColor: surface,
214
+ writingDirection: effectiveDir,
215
+ } })), error ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { testID: `prechat-error-${f.key}`, style: {
216
+ color: "#B42318",
217
+ fontSize: fontSize - 2,
218
+ textAlign: d.textAlign,
219
+ marginTop: 4,
220
+ }, children: error })) : null] }, f.key));
221
+ }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection }, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "prechat-submit", accessibilityRole: "button", accessibilityLabel: submitText, accessibilityState: { disabled: submitting, busy: submitting }, disabled: submitting, onPress: submit, style: {
222
+ backgroundColor: primary,
223
+ borderRadius: 4,
224
+ paddingVertical: 10,
225
+ paddingHorizontal: 16,
226
+ minWidth: 90,
227
+ alignItems: "center",
228
+ opacity: submitting ? 0.6 : 1,
229
+ }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#FFFFFF", fontWeight: "500", fontSize }, children: submitText }) }) })] }));
230
+ }
@@ -0,0 +1 @@
1
+ export declare function QuickReplies({ items, onSelect, theme, dir }: any): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QuickReplies = QuickReplies;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_native_1 = require("react-native");
6
+ const dir_1 = require("../theme/dir");
7
+ const themeFactory_1 = require("../theme/themeFactory");
8
+ // Quick-reply chips for a bot message (audit #3). The row honours the resolved
9
+ // direction (audit #10): rowDirection flips to row-reverse under RTL so the
10
+ // chips read leading-edge first. Selecting a chip calls onSelect(q) — the caller
11
+ // (MessageList) routes it through send({ text: title, payload }).
12
+ function QuickReplies({ items, onSelect, theme, dir = "ltr" }) {
13
+ var _a, _b, _c, _d, _e;
14
+ const d = (0, dir_1.dirStyles)(dir);
15
+ // Chip typography (web quickReplyFontSize / quickReplyFontWeight + defaultFontWeight).
16
+ // Fall back to the global font size / weight when the chat tokens are unset.
17
+ const chatStyle = (_a = theme === null || theme === void 0 ? void 0 : theme.chat) !== null && _a !== void 0 ? _a : {};
18
+ const fontSize = (_b = chatStyle.quickReplyFontSize) !== null && _b !== void 0 ? _b : theme.font.size;
19
+ const fontWeight = (_c = chatStyle.quickReplyFontWeight) !== null && _c !== void 0 ? _c : (_d = theme === null || theme === void 0 ? void 0 : theme.font) === null || _d === void 0 ? void 0 : _d.weight;
20
+ // Tenant custom font (item 28): loaded RN family for the chip weight; undefined =>
21
+ // omit fontFamily (system font).
22
+ const fontFamily = (0, themeFactory_1.resolveFontFamily)((_e = theme === null || theme === void 0 ? void 0 : theme.font) === null || _e === void 0 ? void 0 : _e.familyMap, fontWeight);
23
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection, flexWrap: "wrap", gap: 8, marginTop: 6 }, children: items.map((q) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: q.title, onPress: () => onSelect(q), style: { borderWidth: 1, borderColor: theme.colors.primary, borderRadius: 15, paddingHorizontal: 12, paddingVertical: 6 }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: theme.colors.primary, fontSize, fontWeight, fontFamily }, children: q.title }) }, q.payload))) }));
24
+ }
@@ -0,0 +1,9 @@
1
+ import { type Dir } from "../theme/dir";
2
+ interface TypingIndicatorProps {
3
+ theme: any;
4
+ dir?: Dir;
5
+ /** testID hook for the container (defaults to "typing-indicator"). */
6
+ testID?: string;
7
+ }
8
+ export declare function TypingIndicator({ theme, dir, testID }: TypingIndicatorProps): import("react/jsx-runtime").JSX.Element;
9
+ export default TypingIndicator;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TypingIndicator = TypingIndicator;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ // src/ui/TypingIndicator.tsx
9
+ //
10
+ // Animated three-dot "agent is typing" bubble (web parity — Body/index.tsx:134-191
11
+ // renders an avatar + a white bubble with three pulsing dots while
12
+ // `displayTypingIndication` is on). The web build wires it to a CSS `@keyframes wave`
13
+ // opacity pulse; RN has no CSS keyframes, so we drive the same staggered fade with an
14
+ // Animated.loop over three dots (each offset so the wave reads left → right).
15
+ //
16
+ // Themed: the bubble background + the leading-edge squared "tail" follow the received
17
+ // bubble tokens (theme.bubble.receivedBg / receivedRadius), and the dots take the
18
+ // bubble text color so they read on any tenant palette. Laid out with the logical
19
+ // `dir` so the bubble sits on the leading edge (left in LTR, right in RTL).
20
+ const react_1 = __importDefault(require("react"));
21
+ const react_native_1 = require("react-native");
22
+ const dir_1 = require("../theme/dir");
23
+ /** One pulsing dot — a looped opacity fade with a per-dot start delay. */
24
+ function Dot({ color, delay }) {
25
+ // 0.3 → 1 → 0.3 opacity wave (web uses opacity:0.4 idle); RN drives it with a loop.
26
+ const opacity = react_1.default.useRef(new react_native_1.Animated.Value(0.3)).current;
27
+ react_1.default.useEffect(() => {
28
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
29
+ react_native_1.Animated.delay(delay),
30
+ react_native_1.Animated.timing(opacity, {
31
+ toValue: 1,
32
+ duration: 400,
33
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
34
+ useNativeDriver: true,
35
+ }),
36
+ react_native_1.Animated.timing(opacity, {
37
+ toValue: 0.3,
38
+ duration: 400,
39
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
40
+ useNativeDriver: true,
41
+ }),
42
+ ]));
43
+ loop.start();
44
+ return () => loop.stop();
45
+ }, [opacity, delay]);
46
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: {
47
+ width: 6,
48
+ height: 6,
49
+ borderRadius: 3,
50
+ marginHorizontal: 2,
51
+ backgroundColor: color,
52
+ opacity,
53
+ } }));
54
+ }
55
+ function TypingIndicator({ theme, dir = "ltr", testID = "typing-indicator" }) {
56
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
57
+ const d = (0, dir_1.dirStyles)(dir);
58
+ const bubble = (_a = theme === null || theme === void 0 ? void 0 : theme.bubble) !== null && _a !== void 0 ? _a : {};
59
+ const bg = (_d = (_b = bubble.receivedBg) !== null && _b !== void 0 ? _b : (_c = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _c === void 0 ? void 0 : _c.surface) !== null && _d !== void 0 ? _d : "#FFFFFF";
60
+ // dots take the received text color (web uses black @ 0.4 opacity on the white bubble).
61
+ const dotColor = (_g = (_e = bubble.receivedText) !== null && _e !== void 0 ? _e : (_f = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _f === void 0 ? void 0 : _f.text) !== null && _g !== void 0 ? _g : "#000000";
62
+ const radius = (_j = (_h = bubble.receivedRadius) !== null && _h !== void 0 ? _h : bubble.radius) !== null && _j !== void 0 ? _j : 8;
63
+ const marginX = (_k = bubble.marginX) !== null && _k !== void 0 ? _k : 15;
64
+ // Squared leading-edge tail to match a received bubble (web 0 1em 1em 1em).
65
+ const tail = d.bubbleTail("response");
66
+ const corner = tail === "bottomLeft"
67
+ ? { borderBottomLeftRadius: 0 }
68
+ : { borderBottomRightRadius: 0 };
69
+ const shadow = {
70
+ shadowColor: (_l = bubble.shadowColor) !== null && _l !== void 0 ? _l : "#000",
71
+ shadowOpacity: (_m = bubble.shadowOpacity) !== null && _m !== void 0 ? _m : 0.12,
72
+ shadowRadius: (_o = bubble.shadowRadius) !== null && _o !== void 0 ? _o : 6,
73
+ shadowOffset: (_p = bubble.shadowOffset) !== null && _p !== void 0 ? _p : { width: 0, height: 2 },
74
+ elevation: (_q = bubble.elevation) !== null && _q !== void 0 ? _q : 2,
75
+ };
76
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: testID, accessibilityLabel: "Agent is typing", style: { alignSelf: "flex-start", maxWidth: "85%" }, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
77
+ flexDirection: d.rowDirection,
78
+ alignItems: "center",
79
+ backgroundColor: bg,
80
+ borderRadius: radius,
81
+ ...corner,
82
+ marginHorizontal: marginX,
83
+ paddingHorizontal: 14,
84
+ paddingVertical: 12,
85
+ ...shadow,
86
+ }, children: [(0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 0 }), (0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 150 }), (0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 300 })] }) }));
87
+ }
88
+ exports.default = TypingIndicator;
@@ -0,0 +1,10 @@
1
+ export interface VideoBubbleProps {
2
+ /** hosted URL (mediaUrl) or local/data URI for the video */
3
+ uri: string;
4
+ /** display name (used by the FileTile fallback) */
5
+ name?: string;
6
+ theme: any;
7
+ testID?: string;
8
+ }
9
+ export declare function VideoBubble({ uri, name, theme, testID }: VideoBubbleProps): import("react/jsx-runtime").JSX.Element;
10
+ export default VideoBubble;
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VideoBubble = VideoBubble;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ // src/ui/VideoBubble.tsx
9
+ // Inline video player for a received/sent message file with a uri/mediaUrl.
10
+ //
11
+ // Drawn with `expo-video` (useVideoPlayer + <VideoView/>). The dep is OPTIONAL at
12
+ // runtime (chat-only consumers may not have the native side, and jest mocks it):
13
+ // we lazy-require it inside a guarded helper (mirroring Icon.tsx loadSvg +
14
+ // VideoTile.tsx). When it is absent — or fails to resolve under metro/jest — the
15
+ // bubble degrades to a <FileTile> download link instead of throwing at render.
16
+ //
17
+ // HOOK-SAFETY: `useVideoPlayer` is a React hook and MUST NOT be called
18
+ // conditionally. So this file never calls it directly. Instead it (a) resolves
19
+ // expo-video once at render; (b) when it is ABSENT renders <FileTile> (a path
20
+ // that calls no hook); (c) when it is PRESENT renders an inner <ExpoVideoPlayer>
21
+ // component that UNCONDITIONALLY calls useVideoPlayer. Because the present/absent
22
+ // decision picks a whole component (never a branch inside one component's body),
23
+ // the hook order in each component stays stable across renders.
24
+ const react_1 = __importDefault(require("react"));
25
+ const react_native_1 = require("react-native");
26
+ const FileTile_1 = require("./FileTile");
27
+ /**
28
+ * Lazily resolve `expo-video`. Returns `null` when the optional dep is not
29
+ * installed (or fails to resolve) so the caller renders the <FileTile> fallback
30
+ * instead of crashing. Mirrors Icon.tsx's loadSvg() / VideoTile's pattern.
31
+ */
32
+ function loadExpoVideo() {
33
+ var _a;
34
+ try {
35
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
36
+ const ev = require("expo-video");
37
+ if (ev && ev.useVideoPlayer && ev.VideoView) {
38
+ return { useVideoPlayer: ev.useVideoPlayer, VideoView: ev.VideoView };
39
+ }
40
+ if (globalThis.__DEV__ === true) {
41
+ // eslint-disable-next-line no-console
42
+ console.log("[webchat adapters] expo-video loaded but missing useVideoPlayer/VideoView");
43
+ }
44
+ return null;
45
+ }
46
+ catch (e) {
47
+ // DEV diagnostic: native expo-video absent from the running build (rebuild) -> link fallback.
48
+ if (globalThis.__DEV__ === true) {
49
+ // eslint-disable-next-line no-console
50
+ console.log(`[webchat adapters] require("expo-video") FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e} (native missing → rebuild)`);
51
+ }
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Inner player — only mounted when expo-video resolved, so the useVideoPlayer
57
+ * hook here is ALWAYS called (never conditionally). Sized like ImageBubble:
58
+ * full width up to 100%, a bounded height (~200), rounded, contain-fit.
59
+ */
60
+ function ExpoVideoPlayer({ useVideoPlayer, VideoView, uri, name, theme, testID, }) {
61
+ var _a, _b;
62
+ const [failed, setFailed] = react_1.default.useState(false);
63
+ const player = useVideoPlayer(uri, (p) => {
64
+ p.loop = false;
65
+ });
66
+ // ASYNC playback failures (an unreachable / non-video URL, an unsupported codec,
67
+ // or a `data:` video URL iOS AVPlayer can't open) surface via the player's
68
+ // 'statusChange' event with status 'error' — they are NOT render throws, so the
69
+ // VideoErrorBoundary can't catch them and the bubble would otherwise sit as a dead
70
+ // empty box. Listen for the error status and degrade to a tappable <FileTile>.
71
+ react_1.default.useEffect(() => {
72
+ if (!(player === null || player === void 0 ? void 0 : player.addListener))
73
+ return;
74
+ const sub = player.addListener("statusChange", (payload) => {
75
+ if ((payload === null || payload === void 0 ? void 0 : payload.status) === "error")
76
+ setFailed(true);
77
+ });
78
+ return () => {
79
+ var _a;
80
+ try {
81
+ (_a = sub === null || sub === void 0 ? void 0 : sub.remove) === null || _a === void 0 ? void 0 : _a.call(sub);
82
+ }
83
+ catch { /* already removed */ }
84
+ };
85
+ }, [player]);
86
+ if (failed) {
87
+ return (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID });
88
+ }
89
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: testID, children: (0, jsx_runtime_1.jsx)(VideoView, { testID: testID ? `${testID}-view` : undefined, player: player, nativeControls: true, allowsFullscreen: true, contentFit: "contain", style: {
90
+ // EXPLICIT pixel dimensions (like ImageBubble's 200x150). The previous
91
+ // width:"100%" collapsed to ~0px because the wrapping <View> has NO defined
92
+ // width — a 0-width VideoView is mounted in the tree but invisible on screen
93
+ // (the exact symptom: React DevTools shows VideoView present, nothing renders).
94
+ width: 240,
95
+ height: 180,
96
+ borderRadius: 8,
97
+ backgroundColor: (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _a === void 0 ? void 0 : _a.surface) !== null && _b !== void 0 ? _b : "#000",
98
+ } }) }));
99
+ }
100
+ /**
101
+ * Catches a render throw from <ExpoVideoPlayer>. expo-video's JS can be present
102
+ * (Metro bundled it) while the NATIVE module is absent — e.g. a dev client built
103
+ * BEFORE expo-video was bundled. In that case useVideoPlayer/VideoView throw at
104
+ * render; rather than a dead/empty tall box we degrade to the <FileTile> link.
105
+ * After a dev-client rebuild the native module is present and the player renders.
106
+ */
107
+ class VideoErrorBoundary extends react_1.default.Component {
108
+ constructor() {
109
+ super(...arguments);
110
+ this.state = { failed: false };
111
+ }
112
+ static getDerivedStateFromError() {
113
+ return { failed: true };
114
+ }
115
+ render() {
116
+ return this.state.failed ? this.props.fallback : this.props.children;
117
+ }
118
+ }
119
+ function VideoBubble({ uri, name, theme, testID = "video-bubble" }) {
120
+ const ev = loadExpoVideo();
121
+ // Graceful fallback: no expo-video JS at all (chat-only consumer / test without
122
+ // the mock) -> a plain link tile (no native dep, no hook).
123
+ if (!ev) {
124
+ return (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID });
125
+ }
126
+ // expo-video JS is present but the NATIVE module may be missing (dev client not
127
+ // yet rebuilt) -> if the player throws, the boundary degrades to the link tile.
128
+ return ((0, jsx_runtime_1.jsx)(VideoErrorBoundary, { fallback: (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID }), children: (0, jsx_runtime_1.jsx)(ExpoVideoPlayer, { useVideoPlayer: ev.useVideoPlayer, VideoView: ev.VideoView, uri: uri, name: name, theme: theme, testID: testID }) }));
129
+ }
130
+ exports.default = VideoBubble;
@@ -0,0 +1,34 @@
1
+ import { type VideoCallClient, type VideoCallClientOptions, type WebRTCFactory } from "../core/VideoCallClient";
2
+ import type { Dir } from "../theme/dir";
3
+ /** The audio-route surface the screen drives — `AudioRoute` satisfies it. */
4
+ export interface AudioRouteLike {
5
+ start(opts?: {
6
+ media?: "video" | "audio";
7
+ }): void;
8
+ stop(): void;
9
+ setSpeaker?(on: boolean): void;
10
+ }
11
+ export interface VideoCallProps {
12
+ /** existing chat socket (the call rides it; no second connection) */
13
+ socket: VideoCallClientOptions["socket"];
14
+ /** web-parity call scoping (C8): the active chat session id, sent on join */
15
+ getSessionId: () => string;
16
+ /** called once the call ends (leave / inbound hangup / chat-close S6) */
17
+ onEnd?: () => void;
18
+ /** WebRTC seam — defaults to the real react-native-webrtc factory */
19
+ webrtcFactory?: WebRTCFactory;
20
+ /** audio-route seam — defaults to the real react-native-incall-manager wrapper */
21
+ audioRoute?: AudioRouteLike;
22
+ /** client factory seam — defaults to the real createVideoCallClient */
23
+ createClient?: (opts: VideoCallClientOptions) => VideoCallClient;
24
+ /** layout direction (controls mirror under rtl) */
25
+ dir?: Dir;
26
+ /** resolved language — localizes the error copy + control labels (audit #8). */
27
+ language?: string;
28
+ /** agent display name — shown on the "connecting" placeholder before the remote joins */
29
+ agentName?: string;
30
+ /** agent avatar URL — shown on the "connecting" placeholder (falls back to initials) */
31
+ agentAvatarUrl?: string;
32
+ theme: any;
33
+ }
34
+ export declare function VideoCall({ socket, getSessionId, onEnd, webrtcFactory, audioRoute, createClient, dir, language, agentName, agentAvatarUrl, theme, }: VideoCallProps): import("react/jsx-runtime").JSX.Element;