@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,539 @@
1
+ "use strict";
2
+ // src/adapters/expoDefaults.ts
3
+ //
4
+ // BATTERIES-INCLUDED default adapters. These let `<WebChat config={...} />` work
5
+ // with ZERO host adapter code: when the matching Expo peer is installed they
6
+ // auto-wire image/document picking, file->base64 reads, and audio record/play;
7
+ // when the peer is ABSENT each factory returns `undefined` so the existing
8
+ // feature-detect/`?? ` gating degrades gracefully (attach row hidden, recorder
9
+ // suppressed, throwing-reader fallback) exactly as if no prop were passed.
10
+ //
11
+ // Every Expo lib is an OPTIONAL peer (see package.json peerDependenciesMeta) and
12
+ // is required LAZILY (inside a factory, never at module top-level) behind a
13
+ // try/catch — mirroring src/ui/Icon.tsx loadSvg() / src/adapters/webrtc.ts.
14
+ //
15
+ // CRITICAL: the require MUST use a STATIC LITERAL string — `require("expo-font")`,
16
+ // via `safeRequire(() => require("expo-font"), …)` — NOT a dynamic
17
+ // `require(variable)`. Metro only bundles static requires; a dynamic require throws
18
+ // "Dynamic require … not supported by Metro" AT RUNTIME (node/jest tolerate it, so
19
+ // unit tests won't catch it) and silently disables every adapter on-device. Because
20
+ // these are static, Metro bundles the named packages: a batteries-included build
21
+ // must have the Expo peers installed (the example + README do). At runtime a missing
22
+ // NATIVE module is caught here -> the factory returns undefined and the feature
23
+ // degrades gracefully (button hidden / system font / link fallback).
24
+ //
25
+ // Note (bare React Native, no Expo prebuild): these Expo modules require the
26
+ // `expo-modules-core` runtime. In an Expo-managed/dev-client app that is already
27
+ // present; in a bare RN app the consumer must run `npx install-expo-modules`
28
+ // once so the autolinked native side exists. See README / package.json comment.
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.createExpoPicker = createExpoPicker;
31
+ exports.createExpoReadBase64 = createExpoReadBase64;
32
+ exports.createExpoFontLoader = createExpoFontLoader;
33
+ exports.createExpoAudioAdapter = createExpoAudioAdapter;
34
+ /**
35
+ * Best-effort lazy require of an optional Expo peer. Returns `undefined` when the
36
+ * module is not installed (or fails to resolve) so callers feature-detect on the
37
+ * shape rather than crashing on a missing dependency. Mirrors Icon.tsx loadSvg().
38
+ */
39
+ function safeRequire(load, label) {
40
+ var _a;
41
+ try {
42
+ // `load` MUST contain a STATIC literal require, e.g. () => require("expo-font").
43
+ // Metro only bundles static requires — a dynamic `require(variable)` throws
44
+ // "Dynamic require … not supported by Metro" at runtime, which is what silently
45
+ // disabled EVERY default adapter on-device (node/jest tolerate dynamic require,
46
+ // so the unit tests never caught it).
47
+ return load();
48
+ }
49
+ catch (e) {
50
+ // DEV diagnostic: surface WHY an optional peer didn't resolve (native module
51
+ // missing from the running build, or package not installed). Silent in release.
52
+ adapterLog(`require("${label}") FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
53
+ return undefined;
54
+ }
55
+ }
56
+ /** DEV-only adapter-resolution diagnostic (prints to Metro; silent in release). */
57
+ function adapterLog(...args) {
58
+ if (globalThis.__DEV__ === true) {
59
+ // eslint-disable-next-line no-console
60
+ console.log("[webchat adapters]", ...args);
61
+ }
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Picker (expo-image-picker + expo-document-picker) -> PickerAdapter
65
+ // ---------------------------------------------------------------------------
66
+ /** jpg|png|heic|… inferred from a URI/name tail, lower-cased, query-stripped. */
67
+ function extOf(s) {
68
+ const clean = s.split("?")[0];
69
+ const dot = clean.lastIndexOf(".");
70
+ return dot >= 0 ? clean.slice(dot + 1).toLowerCase() : "";
71
+ }
72
+ /** Infer an image/video mime from a file extension, defaulting to image/jpeg. */
73
+ function imageMimeFromExt(ext) {
74
+ if (ext === "png")
75
+ return "image/png";
76
+ if (ext === "heic" || ext === "heif")
77
+ return "image/heic";
78
+ if (ext === "mp4")
79
+ return "video/mp4";
80
+ if (ext === "m4v")
81
+ return "video/x-m4v";
82
+ return "image/jpeg";
83
+ }
84
+ /**
85
+ * Normalize one `expo-image-picker` asset to the SDK {@link Asset}.
86
+ *
87
+ * expo-image-picker field -> SDK Asset:
88
+ * uri -> uri
89
+ * fileName -> name (synthesized `image_<ts>.<ext>` when null — common on Android/camera-roll)
90
+ * mimeType -> mimeType (inferred from the uri extension when null; `type:'video'` biases mp4)
91
+ * fileSize -> size (0 fallback; the encode-time cap re-checks the real bytes)
92
+ * base64 -> base64 (present because we request `base64:true`, so readBase64 is skipped)
93
+ */
94
+ function normalizeImageAsset(a) {
95
+ var _a, _b, _c, _d, _e, _f;
96
+ const ext = extOf((_a = a === null || a === void 0 ? void 0 : a.uri) !== null && _a !== void 0 ? _a : "") || ((a === null || a === void 0 ? void 0 : a.type) === "video" ? "mp4" : "jpg");
97
+ const mimeType = (_b = a === null || a === void 0 ? void 0 : a.mimeType) !== null && _b !== void 0 ? _b : ((a === null || a === void 0 ? void 0 : a.type) === "video" ? imageMimeFromExt(ext === "jpg" ? "mp4" : ext) : imageMimeFromExt(ext));
98
+ return {
99
+ uri: (_c = a === null || a === void 0 ? void 0 : a.uri) !== null && _c !== void 0 ? _c : "",
100
+ name: (_d = a === null || a === void 0 ? void 0 : a.fileName) !== null && _d !== void 0 ? _d : `image_${Date.now()}.${ext}`,
101
+ mimeType,
102
+ size: (_e = a === null || a === void 0 ? void 0 : a.fileSize) !== null && _e !== void 0 ? _e : 0,
103
+ base64: (_f = a === null || a === void 0 ? void 0 : a.base64) !== null && _f !== void 0 ? _f : undefined,
104
+ };
105
+ }
106
+ /**
107
+ * Normalize one `expo-document-picker` asset to the SDK {@link Asset}.
108
+ *
109
+ * expo-document-picker field -> SDK Asset:
110
+ * uri -> uri (a file:// cache uri because we pass copyToCacheDirectory:true)
111
+ * name -> name (synthesized when null)
112
+ * mimeType -> mimeType (pdf inferred from ext; else application/octet-stream — which
113
+ * validateSelection rejects as unsupported-type, the desired safe default)
114
+ * size -> size (0 fallback)
115
+ * (no base64 — documents carry no eager payload, so readBase64(uri) supplies it)
116
+ */
117
+ function normalizeDocumentAsset(a) {
118
+ var _a, _b, _c, _d, _e, _f;
119
+ const ext = extOf((_a = a === null || a === void 0 ? void 0 : a.name) !== null && _a !== void 0 ? _a : "") || extOf((_b = a === null || a === void 0 ? void 0 : a.uri) !== null && _b !== void 0 ? _b : "");
120
+ return {
121
+ uri: (_c = a === null || a === void 0 ? void 0 : a.uri) !== null && _c !== void 0 ? _c : "",
122
+ name: (_d = a === null || a === void 0 ? void 0 : a.name) !== null && _d !== void 0 ? _d : `file_${Date.now()}${ext ? "." + ext : ""}`,
123
+ mimeType: (_e = a === null || a === void 0 ? void 0 : a.mimeType) !== null && _e !== void 0 ? _e : (ext === "pdf" ? "application/pdf" : "application/octet-stream"),
124
+ size: (_f = a === null || a === void 0 ? void 0 : a.size) !== null && _f !== void 0 ? _f : 0,
125
+ };
126
+ }
127
+ /**
128
+ * Default {@link PickerAdapter} backed by Expo. Returns `undefined` when NEITHER
129
+ * expo-image-picker nor expo-document-picker is installed — so the attach button
130
+ * stays hidden (feature-detect parity with the absent-prop case). When only one
131
+ * lib is present the adapter exposes only that method (a partial picker is valid;
132
+ * the Surface omits the missing row).
133
+ */
134
+ function createExpoPicker() {
135
+ const ImagePicker = safeRequire(() => require("expo-image-picker"), "expo-image-picker");
136
+ const DocumentPicker = safeRequire(() => require("expo-document-picker"), "expo-document-picker");
137
+ // SDK 56's modern file picker (expo-file-system File.pickFileAsync). It is the
138
+ // New-Architecture-native replacement for expo-document-picker, whose
139
+ // getDocumentAsync can HANG on the New Arch (presents nothing, never resolves).
140
+ // Preferred for documents when present; getDocumentAsync is the fallback.
141
+ const FS = safeRequire(() => require("expo-file-system"), "expo-file-system");
142
+ const FilePickCtor = FS === null || FS === void 0 ? void 0 : FS.File;
143
+ const hasFilePick = typeof (FilePickCtor === null || FilePickCtor === void 0 ? void 0 : FilePickCtor.pickFileAsync) === "function";
144
+ const hasImages = typeof (ImagePicker === null || ImagePicker === void 0 ? void 0 : ImagePicker.launchImageLibraryAsync) === "function";
145
+ const hasDocs = typeof (DocumentPicker === null || DocumentPicker === void 0 ? void 0 : DocumentPicker.getDocumentAsync) === "function";
146
+ adapterLog("picker — expo-image-picker:", hasImages ? "READY" : ImagePicker ? "loaded but no launchImageLibraryAsync" : "NOT AVAILABLE (native missing → rebuild)", "| documents:", hasFilePick ? "READY (File.pickFileAsync)" : hasDocs ? "READY (getDocumentAsync)" : "NOT AVAILABLE (native missing → rebuild)");
147
+ if (!hasImages && !hasDocs && !hasFilePick)
148
+ return undefined;
149
+ const adapter = {};
150
+ if (hasImages) {
151
+ adapter.pickImages = async (opts) => {
152
+ var _a, _b, _c;
153
+ // Permission: a denial is treated as a cancel ([] per contract), never a throw.
154
+ const req = ImagePicker.requestMediaLibraryPermissionsAsync;
155
+ if (typeof req === "function") {
156
+ const perm = await req();
157
+ if (perm && perm.granted === false)
158
+ return [];
159
+ }
160
+ // mediaTypes: pass the SDK 51+ string-array form directly. Do NOT reference
161
+ // ImagePicker.MediaTypeOptions — passing its enum member fires the SDK 52+
162
+ // deprecation warning (parseMediaTypes warns on `=== a MediaTypeOptions value`).
163
+ const mediaTypes = ["images"];
164
+ let result;
165
+ try {
166
+ result = await ImagePicker.launchImageLibraryAsync({
167
+ mediaTypes,
168
+ allowsMultipleSelection: (_a = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _a !== void 0 ? _a : true,
169
+ base64: true, // eager base64 => toMediaEntry short-circuits readBase64 for images
170
+ quality: (_b = opts === null || opts === void 0 ? void 0 : opts.quality) !== null && _b !== void 0 ? _b : 0.8,
171
+ });
172
+ }
173
+ catch (e) {
174
+ // A throw is almost always the NATIVE module missing from the running dev
175
+ // client (the JS exists so this method is wired, but the native call fails).
176
+ // Surface it (the Composer's stagePick otherwise swallows it -> the picker
177
+ // silently never opens) and treat as a cancel. Fix: rebuild the dev client.
178
+ adapterLog(`pickImages FAILED — ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
179
+ return [];
180
+ }
181
+ // Modern API: { canceled: boolean; assets }. `canceled` is spelled with one L.
182
+ if (result === null || result === void 0 ? void 0 : result.canceled)
183
+ return [];
184
+ const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets) ? result.assets : [];
185
+ return assets.map(normalizeImageAsset);
186
+ };
187
+ }
188
+ if (hasImages) {
189
+ adapter.pickVideos = async (opts) => {
190
+ var _a, _b, _c;
191
+ // Permission: a denial is treated as a cancel ([] per contract), never a throw.
192
+ const req = ImagePicker.requestMediaLibraryPermissionsAsync;
193
+ if (typeof req === "function") {
194
+ const perm = await req();
195
+ if (perm && perm.granted === false)
196
+ return [];
197
+ }
198
+ // mediaTypes: pass the string-array form directly (warning-free on SDK 56).
199
+ const mediaTypes = ["videos"];
200
+ let result;
201
+ try {
202
+ result = await ImagePicker.launchImageLibraryAsync({
203
+ mediaTypes,
204
+ allowsMultipleSelection: (_a = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _a !== void 0 ? _a : true,
205
+ // Videos can be large; do NOT request eager base64 (it would inflate the
206
+ // payload in memory). readBase64(uri) supplies the payload at encode time,
207
+ // exactly like documents. `type:'video'` biases normalizeImageAsset to mp4.
208
+ quality: (_b = opts === null || opts === void 0 ? void 0 : opts.quality) !== null && _b !== void 0 ? _b : 0.8,
209
+ });
210
+ }
211
+ catch (e) {
212
+ adapterLog(`pickVideos FAILED — ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
213
+ return [];
214
+ }
215
+ // Modern API: { canceled: boolean; assets }. `canceled` is spelled with one L.
216
+ if (result === null || result === void 0 ? void 0 : result.canceled)
217
+ return [];
218
+ const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets) ? result.assets : [];
219
+ return assets.map(normalizeImageAsset);
220
+ };
221
+ }
222
+ if (hasDocs || hasFilePick) {
223
+ adapter.pickDocuments = async (opts) => {
224
+ var _a, _b, _c, _d;
225
+ // PREFERRED — expo-file-system File.pickFileAsync (SDK 56, New-Arch native).
226
+ // expo-document-picker's getDocumentAsync HANGS on the New Architecture (the OS
227
+ // sheet never presents and the promise never resolves), so try the modern picker
228
+ // first and only fall through to getDocumentAsync if it is unavailable/throws.
229
+ if (hasFilePick) {
230
+ try {
231
+ const res = await FilePickCtor.pickFileAsync((opts === null || opts === void 0 ? void 0 : opts.allowsMultiple)
232
+ ? { multipleFiles: true, ...((opts === null || opts === void 0 ? void 0 : opts.mimeTypes) ? { mimeTypes: opts.mimeTypes } : {}) }
233
+ : { ...((opts === null || opts === void 0 ? void 0 : opts.mimeTypes) ? { mimeTypes: opts.mimeTypes } : {}) });
234
+ if (!res || res.canceled)
235
+ return [];
236
+ const files = Array.isArray(res.result)
237
+ ? res.result
238
+ : res.result
239
+ ? [res.result]
240
+ : [];
241
+ // File implements Blob: uri/name/size + `type` (MIME) → normalize to Asset.
242
+ // `type` may be an EMPTY string (Blob.type unknown); coerce to undefined so
243
+ // normalizeDocumentAsset's extension fallback resolves it (.pdf → application/pdf)
244
+ // — otherwise "" slips past its `?? ` and validateSelection rejects a valid PDF.
245
+ return files.map((f) => normalizeDocumentAsset({
246
+ uri: f === null || f === void 0 ? void 0 : f.uri,
247
+ name: f === null || f === void 0 ? void 0 : f.name,
248
+ mimeType: (typeof (f === null || f === void 0 ? void 0 : f.type) === "string" && f.type) || undefined,
249
+ size: f === null || f === void 0 ? void 0 : f.size,
250
+ }));
251
+ }
252
+ catch (e) {
253
+ adapterLog(`pickDocuments: File.pickFileAsync FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}${hasDocs ? " — falling back to getDocumentAsync" : ""}`);
254
+ if (!hasDocs)
255
+ return [];
256
+ // fall through to getDocumentAsync
257
+ }
258
+ }
259
+ let result;
260
+ try {
261
+ result = await DocumentPicker.getDocumentAsync({
262
+ // expo-document-picker accepts a string[] of MIME types directly. iOS maps
263
+ // them to UTIs; Android honors them as EXTRA_MIME_TYPES. Non-standard mimes
264
+ // a chooser ignores are re-enforced post-pick by validateSelection.
265
+ type: (_b = opts === null || opts === void 0 ? void 0 : opts.mimeTypes) !== null && _b !== void 0 ? _b : ["*/*"],
266
+ multiple: (_c = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _c !== void 0 ? _c : false,
267
+ // REQUIRED: yields a stable file:// cache uri readBase64 can read (a raw
268
+ // content:// uri can be revoked before the read runs).
269
+ copyToCacheDirectory: true,
270
+ });
271
+ }
272
+ catch (e) {
273
+ // This method only exists when getDocumentAsync IS present (hasDocs), so a throw
274
+ // here is a RUNTIME error, NOT a missing module — most often "Different document
275
+ // picking in progress" (a concurrent launch; now guarded by Composer.stagePick),
276
+ // or a permissions/UTI issue. Only if the OS sheet NEVER opens at all is the
277
+ // native module missing → add "expo-document-picker" to plugins and REBUILD.
278
+ adapterLog(`pickDocuments FAILED — ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
279
+ return [];
280
+ }
281
+ // Modern API: { canceled: true } on cancel; legacy <=48: { type: 'cancel' }.
282
+ if ((result === null || result === void 0 ? void 0 : result.canceled) || (result === null || result === void 0 ? void 0 : result.type) === "cancel")
283
+ return [];
284
+ // Modern: assets[]; legacy success returned a flat { uri, name, size, mimeType }.
285
+ const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets)
286
+ ? result.assets
287
+ : (result === null || result === void 0 ? void 0 : result.uri)
288
+ ? [result]
289
+ : [];
290
+ return assets.map(normalizeDocumentAsset);
291
+ };
292
+ }
293
+ return adapter;
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // readBase64 (expo-file-system) -> (uri) => Promise<string> (RAW base64)
297
+ // ---------------------------------------------------------------------------
298
+ /**
299
+ * Default file->base64 reader backed by `expo-file-system`. Returns RAW base64
300
+ * (no `data:` prefix) — `toDataUrl` adds the prefix downstream. Only invoked for
301
+ * assets WITHOUT eager base64 (documents, recorded audio); image-picker assets
302
+ * carry `base64` and short-circuit it.
303
+ *
304
+ * SDK drift: SDK 54 promoted the OO `File`/`Directory` API to the package root and
305
+ * DEPRECATED the classic functions. On SDK 56 the root `readAsStringAsync` is a STUB
306
+ * that THROWS at runtime ("This method will throw in runtime"), so we MUST prefer the
307
+ * OO `new File(uri).base64()` and only fall back to `readAsStringAsync` on OLDER SDKs
308
+ * (<=53) where the `File` class doesn't exist. Returns `undefined` when the lib is
309
+ * absent (provider's throwing reader still only fires if an attach without eager
310
+ * base64 is actually attempted).
311
+ */
312
+ function createExpoReadBase64() {
313
+ var _a, _b;
314
+ // Static require of the ROOT package only. `expo-file-system/legacy` is
315
+ // intentionally NOT required: a static require of a possibly-absent subpath would
316
+ // FAIL the Metro bundle. The SDK-56 root exposes the new File API (preferred) and
317
+ // a throwing `readAsStringAsync` stub (legacy fallback only) — both handled below.
318
+ const root = safeRequire(() => require("expo-file-system"), "expo-file-system");
319
+ // SDK 54+ OO API FIRST: `new File(uri).base64()` -> Promise<string> (RAW base64).
320
+ // CRITICAL: this MUST come before readAsStringAsync. On SDK 56 the root
321
+ // `readAsStringAsync` is a DEPRECATED STUB that throws at runtime, so preferring it
322
+ // (the previous order) made every NON-image attach fail at encode — recorded audio,
323
+ // picked video, and documents have no eager base64, so they hit this reader and
324
+ // surfaced as WebChatError("send", "media encode failed") -> "[webchat] error send".
325
+ // The File API is the only working root path on current SDKs.
326
+ const FileCtor = root === null || root === void 0 ? void 0 : root.File;
327
+ if (typeof FileCtor === "function") {
328
+ return async (uri) => {
329
+ const out = new FileCtor(uri).base64();
330
+ return typeof (out === null || out === void 0 ? void 0 : out.then) === "function" ? await out : out;
331
+ };
332
+ }
333
+ // Legacy SDKs (<=53): classic readAsStringAsync at the root (works there; on SDK 56
334
+ // it would throw, but on <=53 there is no File class so this is the only path).
335
+ if (typeof (root === null || root === void 0 ? void 0 : root.readAsStringAsync) === "function") {
336
+ const encoding = (_b = (_a = root.EncodingType) === null || _a === void 0 ? void 0 : _a.Base64) !== null && _b !== void 0 ? _b : "base64";
337
+ return (uri) => root.readAsStringAsync(uri, { encoding });
338
+ }
339
+ return undefined;
340
+ }
341
+ /**
342
+ * Stable RN family name for one tenant font entry. Web uses a single CSS family
343
+ * with per-weight `@font-face`; RN cannot share one family across weights via
344
+ * `loadAsync`, so each weight is registered as its OWN family. The name is
345
+ * derived from the entry's `fontName` (preferred — already weight-specific like
346
+ * "Tajawal-Bold") else its `fontWeight`, sanitized to `[A-Za-z0-9_-]`, e.g.
347
+ * `webchat-Tajawal-Bold` or `webchat-700`. Returns `undefined` when neither
348
+ * `fontName` nor `fontWeight` is present (entry is unkeyable -> skipped).
349
+ */
350
+ function brandFontFamilyName(font) {
351
+ var _a;
352
+ const raw = (_a = font === null || font === void 0 ? void 0 : font.fontName) !== null && _a !== void 0 ? _a : font === null || font === void 0 ? void 0 : font.fontWeight;
353
+ if (raw == null || String(raw).trim() === "")
354
+ return undefined;
355
+ const safe = String(raw).trim().replace(/[^A-Za-z0-9_-]/g, "-");
356
+ return `webchat-${safe}`;
357
+ }
358
+ /**
359
+ * Default brand-font loader backed by `expo-font`. Returns `undefined` (no-op)
360
+ * when expo-font is absent — so Metro never pulls the native module into the
361
+ * import graph and the jest suite (where it is not installed) keeps working,
362
+ * exactly like the other expo defaults.
363
+ *
364
+ * When present it returns `loadBrandFonts(fontFamily)`, an async loader that:
365
+ * - maps each provided font entry to a stable RN family name (`webchat-{…}`),
366
+ * - calls `Font.loadAsync({ [familyName]: url })` for each (in parallel via
367
+ * Promise.all), TOLERATING individual failures (a bad url drops just that
368
+ * weight, never rejects the whole batch),
369
+ * - RESOLVES to a `weight -> familyName` map for the apply phase. The map is
370
+ * keyed by each entry's `fontWeight` (so `resolveFontFamily(map, weight)`
371
+ * can look a weight up); entries without a `fontWeight` are still loaded but
372
+ * are only reachable via the family-level `name` key (added when present).
373
+ *
374
+ * An empty/undefined `fonts` array resolves to `{}` (no fonts loaded).
375
+ */
376
+ function createExpoFontLoader() {
377
+ const Font = safeRequire(() => require("expo-font"), "expo-font");
378
+ if (!Font || typeof Font.loadAsync !== "function")
379
+ return undefined;
380
+ return async (fontFamily) => {
381
+ const fonts = Array.isArray(fontFamily === null || fontFamily === void 0 ? void 0 : fontFamily.fonts) ? fontFamily.fonts : [];
382
+ const map = {};
383
+ if (fonts.length === 0)
384
+ return map;
385
+ await Promise.all(fonts.map(async (f) => {
386
+ const url = f === null || f === void 0 ? void 0 : f.url;
387
+ const familyName = brandFontFamilyName(f);
388
+ // Skip unkeyable (no fontName/fontWeight) or sourceless entries.
389
+ if (!url || !familyName)
390
+ return;
391
+ try {
392
+ await Font.loadAsync({ [familyName]: url });
393
+ }
394
+ catch {
395
+ // Tolerate a single weight failing to load (bad url, network, decode):
396
+ // the rest of the batch still resolves and that weight just falls back
397
+ // to the system font via resolveFontFamily returning undefined.
398
+ return;
399
+ }
400
+ // Only expose successfully-loaded families in the returned map.
401
+ if ((f === null || f === void 0 ? void 0 : f.fontWeight) != null && String(f.fontWeight).trim() !== "") {
402
+ map[String(f.fontWeight)] = familyName;
403
+ }
404
+ // Family-level alias (web `name`) maps to the first loaded family so the
405
+ // apply phase can resolve "the brand font" without a specific weight.
406
+ if ((fontFamily === null || fontFamily === void 0 ? void 0 : fontFamily.name) && !map[fontFamily.name]) {
407
+ map[fontFamily.name] = familyName;
408
+ }
409
+ }));
410
+ return map;
411
+ };
412
+ }
413
+ // ---------------------------------------------------------------------------
414
+ // Audio playback (expo-audio) -> AudioAdapter (PLAYBACK-CAPABLE default)
415
+ // ---------------------------------------------------------------------------
416
+ // B6 CODEC / ENGINE FLAG — READ BEFORE CHANGING.
417
+ // ------------------------------------------------------------------------
418
+ // The audio engine is `expo-audio` (the dead `expo-av` was removed: 16.x does
419
+ // not build on Expo SDK 56). expo-audio splits its surface in two:
420
+ //
421
+ // • PLAYBACK is IMPERATIVE -> createAudioPlayer(uri) | player.play() |
422
+ // player.pause() | player.remove(). This fits the AudioAdapter object shape,
423
+ // so the DEFAULT adapter implements play()/stop() here.
424
+ // • RECORDING is HOOK-ONLY -> useAudioRecorder(RecordingPresets.HIGH_QUALITY)
425
+ // can ONLY be called from inside a React component (Rules of Hooks). It does
426
+ // NOT fit a plain object method. So the default adapter does NOT record: it
427
+ // delegates recording to the <AudioRecorder> component path (Phase 2), which
428
+ // drives the hook and produces a RecordingResult through the same contract.
429
+ // Here startRecording()/stopRecording() are inert no-op stubs (they keep the
430
+ // AudioAdapter type satisfied) and MUST NOT be used to capture a note.
431
+ //
432
+ // CODEC: expo-audio's HIGH_QUALITY preset records MPEG-4 / AAC (.m4a) on BOTH iOS
433
+ // and Android — it CANNOT produce Ogg/Opus (the SDK's web-parity CANONICAL_AUDIO_MIME).
434
+ // The recorder component (Phase 2) HONESTLY returns the real container mime; we do
435
+ // NOT mislabel m4a bytes as audio/ogg. For the voice note to pass validateSelection,
436
+ // "audio/mp4" / "audio/aac" / "audio/m4a" are in src/core/media.ts ALLOW_LIST.
437
+ //
438
+ // ⚠️ OPEN DEPENDENCY (B6): the WEB widget only ever sent audio/ogg;codecs=opus.
439
+ // Backend/infra acceptance of m4a/aac voice notes MUST BE CONFIRMED. If the backend
440
+ // rejects them, substitute an Opus-capable recorder behind this same AudioAdapter
441
+ // (expo-audio alone cannot do Opus) so mimeType genuinely equals CANONICAL_AUDIO_MIME.
442
+ // Do NOT "solve" this by lying about the mime.
443
+ const RECORDED_AUDIO_MIME = "audio/mp4"; // .m4a / AAC container (iOS + Android)
444
+ /**
445
+ * Default PLAYBACK-CAPABLE {@link AudioAdapter} backed by `expo-audio`. Returns
446
+ * `undefined` when expo-audio is absent (received-voice falls back to a download
447
+ * tile — parity with the absent-prop case).
448
+ *
449
+ * PLAYBACK (imperative, fully implemented here):
450
+ * play(uri) -> createAudioPlayer(uri); player.play(); retain the player.
451
+ * stop() -> retained player.pause() + player.remove() (MUST remove() to free
452
+ * native resources); null-safe; a single active player is retained,
453
+ * so a new play() removes any prior one first.
454
+ *
455
+ * RECORDING (delegated to Phase 2 component): startRecording()/stopRecording() are
456
+ * inert stubs that keep the AudioAdapter type satisfied. expo-audio's recorder is
457
+ * HOOK-ONLY (useAudioRecorder), so the real capture lives in the <AudioRecorder>
458
+ * component (Phase 2) which drives the hook and yields a RecordingResult tagged with
459
+ * the m4a container mime above. The default object adapter cannot call a hook.
460
+ */
461
+ function createExpoAudioAdapter() {
462
+ const audio = safeRequire(() => require("expo-audio"), "expo-audio");
463
+ adapterLog("audio — expo-audio:", audio ? (typeof audio.createAudioPlayer === "function" ? "READY" : "loaded but no createAudioPlayer") : "NOT AVAILABLE (native missing → rebuild)");
464
+ if (!audio || typeof audio.createAudioPlayer !== "function")
465
+ return undefined;
466
+ // Single retained active player (imperative). play() replaces any prior one;
467
+ // stop() pauses + removes it to free native resources, then clears the ref.
468
+ let player = null;
469
+ // Subscription to expo-audio's "playbackStatusUpdate" (position/duration/finish);
470
+ // removed alongside the player so no stale listener fires after stop/replace.
471
+ let statusSub = null;
472
+ function releasePlayer() {
473
+ var _a, _b, _c;
474
+ try {
475
+ (_a = statusSub === null || statusSub === void 0 ? void 0 : statusSub.remove) === null || _a === void 0 ? void 0 : _a.call(statusSub);
476
+ }
477
+ catch {
478
+ /* best-effort */
479
+ }
480
+ statusSub = null;
481
+ if (!player)
482
+ return;
483
+ try {
484
+ (_b = player.pause) === null || _b === void 0 ? void 0 : _b.call(player);
485
+ }
486
+ catch {
487
+ /* best-effort: never throw out of release */
488
+ }
489
+ try {
490
+ (_c = player.remove) === null || _c === void 0 ? void 0 : _c.call(player); // REQUIRED to free expo-audio native resources
491
+ }
492
+ catch {
493
+ /* best-effort */
494
+ }
495
+ player = null;
496
+ }
497
+ return {
498
+ // RECORDING is provided by the Phase 2 <AudioRecorder> component (expo-audio
499
+ // useAudioRecorder hook), NOT by this object adapter — these stubs only keep
500
+ // the AudioAdapter type satisfied for the playback-capable default. The flag
501
+ // below tells <AudioRecorder> to drive the hook directly instead of calling
502
+ // these inert stubs (a host-injected imperative recorder leaves it unset and
503
+ // its real startRecording/stopRecording are used as-is).
504
+ recordingDelegated: true,
505
+ async startRecording() {
506
+ /* no-op: default recording is delegated to the AudioRecorder component */
507
+ },
508
+ async stopRecording() {
509
+ // Inert result, tagged with the container mime the Phase 2 recorder emits.
510
+ return { uri: "", mimeType: RECORDED_AUDIO_MIME, durationMs: 0 };
511
+ },
512
+ async play(uri, opts) {
513
+ var _a;
514
+ // Replace any currently-retained player so only one note plays at a time.
515
+ releasePlayer();
516
+ const p = audio.createAudioPlayer(uri);
517
+ player = p;
518
+ // Live progress: subscribe to expo-audio's status events so the player UI can
519
+ // show "current / total" and reset on finish. currentTime/duration are SECONDS
520
+ // in expo-audio → convert to ms. Guarded so engines without addListener degrade
521
+ // gracefully (no progress; the UI keeps showing the recorded total).
522
+ const onStatus = opts === null || opts === void 0 ? void 0 : opts.onStatus;
523
+ if (onStatus && typeof (p === null || p === void 0 ? void 0 : p.addListener) === "function") {
524
+ statusSub = p.addListener("playbackStatusUpdate", (st) => {
525
+ var _a, _b, _c, _d, _e, _f, _g;
526
+ const durationMs = Math.max(0, Math.round(((_b = (_a = st === null || st === void 0 ? void 0 : st.duration) !== null && _a !== void 0 ? _a : p === null || p === void 0 ? void 0 : p.duration) !== null && _b !== void 0 ? _b : 0) * 1000));
527
+ const positionMs = Math.max(0, Math.round(((_d = (_c = st === null || st === void 0 ? void 0 : st.currentTime) !== null && _c !== void 0 ? _c : p === null || p === void 0 ? void 0 : p.currentTime) !== null && _d !== void 0 ? _d : 0) * 1000));
528
+ const isPlaying = (_f = (_e = st === null || st === void 0 ? void 0 : st.playing) !== null && _e !== void 0 ? _e : p === null || p === void 0 ? void 0 : p.playing) !== null && _f !== void 0 ? _f : true;
529
+ const didJustFinish = (_g = st === null || st === void 0 ? void 0 : st.didJustFinish) !== null && _g !== void 0 ? _g : (durationMs > 0 && positionMs >= durationMs);
530
+ onStatus({ positionMs, durationMs, isPlaying, didJustFinish });
531
+ });
532
+ }
533
+ (_a = p === null || p === void 0 ? void 0 : p.play) === null || _a === void 0 ? void 0 : _a.call(p);
534
+ },
535
+ async stop() {
536
+ releasePlayer();
537
+ },
538
+ };
539
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * A normalized file selected from the device, independent of the underlying
3
+ * picker library. `base64` is optional: some pickers return it eagerly, others
4
+ * require a separate read (see {@link toMediaEntry}).
5
+ */
6
+ export interface Asset {
7
+ /** local file URI (file://… or content://…) */
8
+ uri: string;
9
+ /** display name including extension, e.g. "photo.jpg" */
10
+ name: string;
11
+ /** MIME type, e.g. "image/jpeg", "application/pdf" */
12
+ mimeType: string;
13
+ /** raw byte size before base64 inflation */
14
+ size: number;
15
+ /** eagerly-provided base64 payload (no data: prefix), when available */
16
+ base64?: string;
17
+ }
18
+ export interface PickImagesOptions {
19
+ /** allow selecting more than one image */
20
+ allowsMultiple?: boolean;
21
+ /** 0..1 compression quality hint passed to the native picker */
22
+ quality?: number;
23
+ /** longest-edge resize hint passed to the native picker */
24
+ maxDimension?: number;
25
+ }
26
+ export interface PickDocumentsOptions {
27
+ allowsMultiple?: boolean;
28
+ /** allow-listed MIME types the picker should restrict to */
29
+ mimeTypes?: string[];
30
+ }
31
+ /**
32
+ * The injectable picker surface. An empty array means the user cancelled.
33
+ * Both capabilities are OPTIONAL so a host can ship a partial picker (e.g.
34
+ * images only); the Surface feature-detects with `picker?.pickImages` and only
35
+ * offers the corresponding selector row when a method is present.
36
+ */
37
+ export interface PickerAdapter {
38
+ pickImages?(opts?: PickImagesOptions): Promise<Asset[]>;
39
+ pickVideos?(opts?: PickImagesOptions): Promise<Asset[]>;
40
+ pickDocuments?(opts?: PickDocumentsOptions): Promise<Asset[]>;
41
+ }
42
+ /**
43
+ * The shape of a single `user_uttered.media[]` entry. The payload is a
44
+ * self-describing data URL so the existing base64-in-socket transport carries
45
+ * it unchanged (web parity).
46
+ */
47
+ export interface MediaEntry {
48
+ /** "data:<mimeType>;base64,<payload>" */
49
+ data: string;
50
+ name: string;
51
+ mimeType: string;
52
+ size: number;
53
+ }
54
+ /**
55
+ * Build a `data:` URL from a MIME type and raw base64 payload.
56
+ * Pure — no I/O.
57
+ */
58
+ export declare function toDataUrl(mimeType: string, base64: string): string;
59
+ /**
60
+ * Pure transform: turn an {@link Asset} into a {@link MediaEntry} carrying a
61
+ * base64 data URL. Base64 acquisition is injected via `readBase64` so this
62
+ * stays I/O-free and testable with a fake reader.
63
+ *
64
+ * If the asset already carries `base64` it is used directly and `readBase64`
65
+ * is not called; otherwise `readBase64(asset.uri)` supplies the payload.
66
+ */
67
+ export declare function toMediaEntry(asset: Asset, readBase64: (uri: string) => Promise<string>): Promise<MediaEntry>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ // src/adapters/picker.ts
3
+ //
4
+ // Picker capability behind an injectable interface so the framework-agnostic
5
+ // core never calls a native module directly. Real impls (Expo
6
+ // `expo-image-picker`/`expo-document-picker`, bare `react-native-image-picker`/
7
+ // `react-native-document-picker`) are constructed in the example apps and
8
+ // injected; unit tests use a fake that returns canned `Asset[]`.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.toDataUrl = toDataUrl;
11
+ exports.toMediaEntry = toMediaEntry;
12
+ /**
13
+ * Build a `data:` URL from a MIME type and raw base64 payload.
14
+ * Pure — no I/O.
15
+ */
16
+ function toDataUrl(mimeType, base64) {
17
+ return `data:${mimeType};base64,${base64}`;
18
+ }
19
+ /**
20
+ * Pure transform: turn an {@link Asset} into a {@link MediaEntry} carrying a
21
+ * base64 data URL. Base64 acquisition is injected via `readBase64` so this
22
+ * stays I/O-free and testable with a fake reader.
23
+ *
24
+ * If the asset already carries `base64` it is used directly and `readBase64`
25
+ * is not called; otherwise `readBase64(asset.uri)` supplies the payload.
26
+ */
27
+ async function toMediaEntry(asset, readBase64) {
28
+ const base64 = asset.base64 != null && asset.base64.length > 0
29
+ ? asset.base64
30
+ : await readBase64(asset.uri);
31
+ return {
32
+ data: toDataUrl(asset.mimeType, base64),
33
+ name: asset.name,
34
+ mimeType: asset.mimeType,
35
+ size: asset.size,
36
+ };
37
+ }