@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,320 @@
1
+ export type SessionStatus = "idle" | "connecting" | "connected" | "session-pending" | "session-confirmed" | "reconnecting" | "disconnected" | "session-failed";
2
+ export type WebChatErrorCode = "connection" | "session-timeout" | "send" | "config-fetch" | "permission-denied" | "media-webrtc";
3
+ export interface QuickReply {
4
+ title: string;
5
+ payload: string;
6
+ }
7
+ /**
8
+ * A configurable image reference (web parity — agent `avatar`, `closeImage`,
9
+ * `openLauncherImage`, `closeLauncherImage`). Web stores `{ name, size, url }`;
10
+ * the SDK only needs `url` to render and `name` for a11y/labels, so `size` is
11
+ * intentionally omitted. Both fields are optional — an empty object means "no
12
+ * custom image" and the consumer falls back to the built-in glyph.
13
+ */
14
+ export interface ConfigImage {
15
+ url?: string;
16
+ name?: string;
17
+ }
18
+ /**
19
+ * The two feedback reactions a user can apply to a received (bot/agent) message
20
+ * (web parity). The stored `Message.userReaction` widens this to allow `null`
21
+ * (no/cleared reaction); the toggle verbs (`unlike`/`undislike`) are transport
22
+ * strings on the wire, not reaction states.
23
+ */
24
+ export type MessageReaction = "like" | "dislike";
25
+ export interface Message {
26
+ /** client-generated stable key for reconciliation (the SDK's own id). */
27
+ key: string;
28
+ /**
29
+ * Server-assigned stable message id (web `messageKey`). Distinct from `key`
30
+ * (the SDK's client-side reconciliation id). Carried through verbatim from the
31
+ * `bot_uttered` payload; it is the id echoed on the `activity`/`activityAck`
32
+ * reaction events. Present only on server-originated messages.
33
+ */
34
+ messageKey?: string;
35
+ sender: "user" | "response";
36
+ text?: string;
37
+ quick_replies?: QuickReply[];
38
+ media?: string[];
39
+ files?: unknown[];
40
+ mediaUrl?: string;
41
+ /**
42
+ * Message-level MIME (web `message.type`). Backstops attachment classification
43
+ * when a hosted media URL has no file extension and the file object carries no
44
+ * own mime — so a received image/video still renders inline instead of as a
45
+ * document tile / browser link.
46
+ */
47
+ type?: string;
48
+ timestamp: number;
49
+ status?: "pending" | "sent" | "failed";
50
+ /**
51
+ * The user's current feedback reaction on this message (web parity). `null`
52
+ * (or absent) = no reaction. Mutually exclusive — set by the `REACT` reducer
53
+ * action / inbound `activityAck`.
54
+ */
55
+ userReaction?: MessageReaction | null;
56
+ /**
57
+ * Per-message server options carried verbatim from `bot_uttered`. `enableReaction`
58
+ * gates the thumbs-up/down feedback row (web `options.enableReaction`); the index
59
+ * signature preserves any other server-sent option keys untouched.
60
+ */
61
+ options?: {
62
+ enableReaction?: boolean;
63
+ } & Record<string, any>;
64
+ }
65
+ export interface OutgoingPayload {
66
+ message: string;
67
+ text?: string;
68
+ session_id: string;
69
+ customData: {
70
+ language: string;
71
+ };
72
+ /**
73
+ * Base64 data-URL STRINGS (web parity: `media: filesBase64`). NOT objects — the
74
+ * backend reads each entry as a string and rejects objects with
75
+ * "MsgToExp: invalid media string".
76
+ */
77
+ media?: string[];
78
+ /** Web parity: the first attachment's MIME type (web `typeMedia: files[0].type`). */
79
+ typeMedia?: string;
80
+ messageKey?: string;
81
+ type?: string;
82
+ /** S9: free-form host metadata passed through to the backend untouched. */
83
+ metadata?: Record<string, unknown>;
84
+ /**
85
+ * Pre-chat form submission (web parity: ChatContainer sends the collected fields as
86
+ * `prechatFormSubmission` on the /chitchat.greet message). Only present on the greet.
87
+ */
88
+ prechatFormSubmission?: Record<string, string>;
89
+ }
90
+ /**
91
+ * Full webchat configuration (S8). Fields split conceptually into:
92
+ * - connection (channelId/configUrl/connectionUrl) — required to talk to a tenant
93
+ * - server-driven theming/i18n (interfaceTheme, publicStyle, language)
94
+ * - behaviour flags the host or server can toggle.
95
+ *
96
+ * `mergeConfig` (configClient) applies precedence prop > server > default, so every
97
+ * non-connection field is optional here; the connection trio is required.
98
+ */
99
+ export interface Config {
100
+ /** Tenant channel id — selects the socket path and storage namespace. */
101
+ channelId: string;
102
+ /** Remote endpoint that returns the server-side config JSON. */
103
+ configUrl: string;
104
+ /** WebSocket host (no scheme — the socket factory prepends `wss://`). */
105
+ connectionUrl: string;
106
+ /** Theme variant key (interfaceThemeEnum: '' | 'experia' | 'digitalGovernment'). */
107
+ interfaceTheme?: string;
108
+ /** Tenant colour/font overrides (themeColor, defaultFontColor, …). */
109
+ publicStyle?: PublicStyle;
110
+ /** Tenant chat-bubble style overrides (sentMsgBg, receivedMsgBg, …) — server-driven. */
111
+ chatSectionStyle?: Record<string, any>;
112
+ /** Tenant header style overrides (bgColor, titleColor, videoCallBtn*, …) — server-driven. */
113
+ headerSectionStyle?: Record<string, any>;
114
+ /** Tenant composer/send style overrides (textFieldBg, sendBtnBg, …) — server-driven. */
115
+ sendSectionStyle?: Record<string, any>;
116
+ /** Tenant launcher (FAB) style overrides (buttonColor, buttonBg, buttonWidth, …) — server-driven (web `wedgitSectionStyle`). */
117
+ wedgitSectionStyle?: Record<string, any>;
118
+ /** Initial language ('en' | 'ar' | …); drives dir = 'ar' ? 'rtl' : 'ltr'. */
119
+ language?: string;
120
+ /** Connect the socket automatically (vs. waiting for an explicit open). */
121
+ autoConnection?: boolean;
122
+ /** Master switch: when false the socket never connects regardless of open state. */
123
+ enableConnection?: boolean;
124
+ /** Open the chat surface on first load. */
125
+ openOnLoad?: boolean;
126
+ /** Send the welcome/greet flow on a new session. */
127
+ sendWelcomeMessage?: boolean;
128
+ /** Disable the composer input (read-only / bot-driven conversations). */
129
+ disabledInput?: boolean;
130
+ /** Expose the video-call entry point. */
131
+ makeVideoCall?: boolean;
132
+ /** Auto-join an incoming video call without an explicit tap (web `autoJoinVideoCall`; default false). */
133
+ autoJoinVideoCall?: boolean;
134
+ /** Show the file/image upload control (web `uploadMedia`; default true). */
135
+ uploadMedia?: boolean;
136
+ /** Show the voice-note recorder (web `uploadAudio`; default true). */
137
+ uploadAudio?: boolean;
138
+ /** Show the image option in the attach menu (web `uploadImage`; default true). */
139
+ uploadImage?: boolean;
140
+ /** Show the document option in the attach menu (web `uploadDocuments`; default true). */
141
+ uploadDocuments?: boolean;
142
+ /** Show the video option in the attach menu (web `uploadVideo`; default true). */
143
+ uploadVideo?: boolean;
144
+ /** Show the emoji-picker button (web `emoji`; default false per web defaultConfig). */
145
+ emoji?: boolean;
146
+ /** Show the header close/back button (default shown; only `false` hides it). */
147
+ showCloseButton?: boolean;
148
+ /** Show the floating launcher button (web `showButtonChat`; default true). */
149
+ showButtonChat?: boolean;
150
+ /** Render the per-message timestamp row (web `showMessageDate`; default true). */
151
+ showMessageDate?: boolean;
152
+ /** Render the typing indicator while the agent is typing (web `displayTypingIndication`; default false). */
153
+ displayTypingIndication?: boolean;
154
+ /** Render the "powered by" badge below the message list (web `showPoweredBy`; default true). */
155
+ showPoweredBy?: boolean;
156
+ /** Show the agent avatar on header + received messages (web `showAvatar`; default true). */
157
+ showAvatar?: boolean;
158
+ /** Agent avatar image (web `config.avatar`; `{ url, name }`). */
159
+ avatar?: ConfigImage;
160
+ /** Custom header close-button image (web `config.closeImage`; `{ url, name }`). */
161
+ closeImage?: ConfigImage;
162
+ /** Custom launcher (open) image (web `config.openLauncherImage`; `{ url, name }`). */
163
+ openLauncherImage?: ConfigImage;
164
+ /** Custom launcher (close/minimized) image (web `config.closeLauncherImage`; `{ url, name }`). */
165
+ closeLauncherImage?: ConfigImage;
166
+ /** Header title text (web `config.title`; falls back to host prop when absent). */
167
+ title?: string;
168
+ /** Header subtitle text under the title (web `config.subTitle`). */
169
+ subTitle?: string;
170
+ /** Text shown in the header "connecting" banner (web `config.connectingText`). */
171
+ connectingText?: string;
172
+ /** Composer placeholder override (web `config.inputTextFieldHint`). */
173
+ inputTextFieldHint?: string;
174
+ /** Where to place the online/offline status indicator (web `config.statusIndicatorPosition`; default 'top-right'). */
175
+ statusIndicatorPosition?: string;
176
+ /** Pre-chat form descriptor (web `config.prechatForm`); `.enabled` gates the form. */
177
+ prechatForm?: PrechatForm;
178
+ /**
179
+ * Persisted-history storage backend (web `config.storage`). On web this selects
180
+ * `localStorage` vs `sessionStorage` vs in-memory. The RN analog (Tier C #33):
181
+ * - "localStorage" (default) -> the persistent AsyncStorage-backed store (history
182
+ * survives across app launches; the 24h sliding TTL still applies).
183
+ * - "sessionStorage" | "memory" -> an EPHEMERAL in-memory store (history does NOT
184
+ * survive an app relaunch; closest RN analog to a per-session/non-durable store).
185
+ * Only honored when the host injects NO store of its own — a host-supplied store
186
+ * always wins (see `selectStore` in persistence.ts). Default "localStorage".
187
+ */
188
+ storage?: "localStorage" | "sessionStorage" | "memory";
189
+ /**
190
+ * Hide the launcher/surface while not connected (web `config.hideWhenNotConnected`,
191
+ * default true on web — though effectively dead there). RN semantics (Tier C #34):
192
+ * the launcher/surface is hidden ONLY after a connection was established and then
193
+ * lost (status left "connected"/"session-confirmed" having previously reached it),
194
+ * NEVER on the initial pre-connect — otherwise the user could never open to connect.
195
+ * Default false (opt-in) so the SDK's open-to-connect flow is unchanged unless set.
196
+ */
197
+ hideWhenNotConnected?: boolean;
198
+ /**
199
+ * Inline-embedded vs floating-widget layout (web `config.embedded`). RN analog
200
+ * (Tier C #31): when true the OPEN surface lays out inline within its parent
201
+ * (no absolute/floating positioning, no launcher-overlay padding) so a host can
202
+ * embed it in a screen; when false (default) the surface fills its mount as today.
203
+ * Host mounting ultimately controls placement in RN — this flag only adjusts the
204
+ * Surface's own container style. Default false.
205
+ */
206
+ embedded?: boolean;
207
+ /**
208
+ * Full-height vs bounded open-surface sizing (web `config.fullScreenHeight`). RN
209
+ * analog (Tier C #32): when true (or unset) the open surface fills available height
210
+ * (`flex: 1`, today's behaviour); when explicitly false it is capped to a bounded
211
+ * height (`maxHeight`) so it does not consume the whole mount. Host mounting also
212
+ * influences final height in RN. Default true (omit/true => full height).
213
+ */
214
+ fullScreenHeight?: boolean;
215
+ /**
216
+ * Widget shell type (web `config.typeWidget`: "default" | "drawer" | embedded
217
+ * shell). Tier C #30: the SDK has a SINGLE Surface + optional FAB Launcher and no
218
+ * DrawerWidget/embedded-shell analog beyond the `embedded` flag, so only "default"
219
+ * maps; any non-default value is treated as "default" (no error). Resolved by
220
+ * DOCUMENTATION, not new behaviour. Default "default".
221
+ */
222
+ typeWidget?: string;
223
+ /** Sliding session TTL in ms (24h default applied by consumers). */
224
+ ttlMs?: number;
225
+ /**
226
+ * DEV-only: trace every socket message (outbound emits + inbound events +
227
+ * lifecycle) to the console/Metro for debugging. Never enable in production —
228
+ * full payloads (incl. session ids / content) are logged.
229
+ */
230
+ debug?: boolean;
231
+ }
232
+ /**
233
+ * A `{ en, ar }` localized string pair (web parity — `interfaces/index.ts`
234
+ * `LocalizedText`). The renderer reads `text[language] || text.en` so an absent
235
+ * translation falls back to English rather than blanking.
236
+ */
237
+ export type LocalizedText = {
238
+ en: string;
239
+ ar: string;
240
+ };
241
+ /** Supported pre-chat field input kinds (web parity). */
242
+ export type PrechatFieldType = "text" | "email" | "phone" | "dropdown";
243
+ /** A single selectable option for a `dropdown` field (web parity). */
244
+ export interface PrechatFieldOption {
245
+ value: string;
246
+ label: LocalizedText;
247
+ }
248
+ /**
249
+ * One pre-chat form field (web `interfaces/index.ts` `PrechatField`). `label`,
250
+ * `placeholder`, and `errorMessage` are localized `{en,ar}` pairs; `required`
251
+ * defaults to TRUE (web semantics — only an explicit `false` makes it optional).
252
+ * `options` is required only for `type === "dropdown"`.
253
+ */
254
+ export interface PrechatField {
255
+ key: string;
256
+ type: PrechatFieldType;
257
+ label: LocalizedText;
258
+ placeholder?: LocalizedText;
259
+ required?: boolean;
260
+ errorMessage?: LocalizedText;
261
+ options?: PrechatFieldOption[];
262
+ }
263
+ /**
264
+ * Pre-chat form descriptor (web `config.prechatForm`). Now fully typed at parity
265
+ * with the web schema: `.enabled` gates the form, `.fields[]` drives per-field
266
+ * rendering/validation, and `.headline`/`.submitLabel` are optional localized
267
+ * `{en,ar}` strings. The trailing index signature is retained so an unknown
268
+ * server key on the object never breaks the structural type.
269
+ */
270
+ export interface PrechatForm {
271
+ /** Master gate — only show the form when explicitly enabled. */
272
+ enabled?: boolean;
273
+ /** Form fields rendered in order (text / email / phone / dropdown). */
274
+ fields?: PrechatField[];
275
+ /** Optional localized headline shown above the fields. */
276
+ headline?: LocalizedText;
277
+ /** Optional localized submit-button label (falls back to the i18n default). */
278
+ submitLabel?: LocalizedText;
279
+ [k: string]: any;
280
+ }
281
+ /**
282
+ * Tenant style overrides carried on `Config.publicStyle`. Kept structurally
283
+ * identical to the theme layer's `PublicStyle` so `buildTheme` can consume it
284
+ * directly without a cross-import (core stays framework-agnostic).
285
+ */
286
+ export interface PublicStyle {
287
+ themeColor?: string;
288
+ defaultFontColor?: string;
289
+ defaultFontSize?: number;
290
+ defaultFontWeight?: string;
291
+ /**
292
+ * Tenant custom font (web `publicStyle.defaultFontFamily`). Web injects an
293
+ * `@font-face` per weight at runtime; RN registers each weight as its own font
294
+ * family via `expo-font` loadAsync (see {@link createExpoFontLoader}). `fonts`
295
+ * holds one remote TTF per weight (ExtraLight..Black). All fields optional.
296
+ */
297
+ defaultFontFamily?: {
298
+ name?: string;
299
+ fonts?: Array<{
300
+ url: string;
301
+ fontWeight?: string;
302
+ fontName?: string;
303
+ }>;
304
+ };
305
+ }
306
+ /**
307
+ * Maps the backend's string error keys (web parity — emitted on the socket) to
308
+ * the SDK's typed {@link WebChatErrorCode}. Unknown keys fall back to
309
+ * 'connection' at the call site.
310
+ */
311
+ export declare const WEB_ERROR_CODE_MAP: Record<string, WebChatErrorCode>;
312
+ /** Translate a backend error key to a typed code, defaulting to 'connection'. */
313
+ export declare const mapWebErrorCode: (key: string | undefined) => WebChatErrorCode;
314
+ export declare class WebChatError extends Error {
315
+ code: WebChatErrorCode | "session-timeout";
316
+ recoverable: boolean;
317
+ cause?: unknown | undefined;
318
+ constructor(code: WebChatErrorCode | "session-timeout", message: string, recoverable: boolean, cause?: unknown | undefined);
319
+ }
320
+ export declare const isWebChatError: (e: unknown) => e is WebChatError;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWebChatError = exports.WebChatError = exports.mapWebErrorCode = exports.WEB_ERROR_CODE_MAP = void 0;
4
+ /**
5
+ * Maps the backend's string error keys (web parity — emitted on the socket) to
6
+ * the SDK's typed {@link WebChatErrorCode}. Unknown keys fall back to
7
+ * 'connection' at the call site.
8
+ */
9
+ exports.WEB_ERROR_CODE_MAP = {
10
+ send_message_fail: "send",
11
+ request_socket_session_fail: "session-timeout",
12
+ get_config_fail: "config-fetch",
13
+ socket_connection_fail: "connection",
14
+ media_upload_fail: "send",
15
+ };
16
+ /** Translate a backend error key to a typed code, defaulting to 'connection'. */
17
+ const mapWebErrorCode = (key) => (key && exports.WEB_ERROR_CODE_MAP[key]) || "connection";
18
+ exports.mapWebErrorCode = mapWebErrorCode;
19
+ class WebChatError extends Error {
20
+ constructor(code, message, recoverable, cause) {
21
+ super(message);
22
+ this.code = code;
23
+ this.recoverable = recoverable;
24
+ this.cause = cause;
25
+ this.name = "WebChatError";
26
+ }
27
+ }
28
+ exports.WebChatError = WebChatError;
29
+ const isWebChatError = (e) => e instanceof WebChatError;
30
+ exports.isWebChatError = isWebChatError;
@@ -0,0 +1,2 @@
1
+ import type { Message } from "./types";
2
+ export declare const unreadCount: (messages: Message[], lastReadAt: number) => number;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unreadCount = void 0;
4
+ const unreadCount = (messages, lastReadAt) => messages.filter((m) => m.sender === "response" && m.timestamp > lastReadAt).length;
5
+ exports.unreadCount = unreadCount;
@@ -0,0 +1 @@
1
+ { "send": "إرسال", "open_chat": "افتح المحادثة", "start_chat": "ابدأ المحادثة", "type_message": "اكتب رسالة", "start_video_call": "بدء مكالمة فيديو", "back": "رجوع", "close": "إغلاق", "mute": "كتم الميكروفون", "unmute": "إلغاء كتم الميكروفون", "camera_on": "تشغيل الكاميرا", "camera_off": "إيقاف الكاميرا", "leave_call": "إنهاء المكالمة", "flip_camera": "تبديل الكاميرا", "speaker": "مكبر الصوت", "call_error": "الوصول إلى الكاميرا والميكروفون مطلوب لإجراء مكالمات الفيديو.", "open_settings": "فتح الإعدادات", "cancel": "إلغاء", "upload_image": "صورة", "upload_video": "فيديو", "upload_document": "مستند", "record_audio": "تسجيل صوتي", "connecting": "جارٍ الاتصال…", "camera_off_status": "الكاميرا متوقفة", "you": "أنت", "agent": "الوكيل" }
@@ -0,0 +1 @@
1
+ { "send": "Send", "open_chat": "Open chat", "start_chat": "Start chat", "type_message": "Type a message", "start_video_call": "Start video call", "back": "Back", "close": "Close", "mute": "Mute microphone", "unmute": "Unmute microphone", "camera_on": "Turn camera on", "camera_off": "Turn camera off", "leave_call": "Leave call", "flip_camera": "Flip camera", "speaker": "Speaker", "call_error": "Camera and microphone access is required for video calls.", "open_settings": "Open Settings", "cancel": "Cancel", "upload_image": "Photo", "upload_video": "Video", "upload_document": "Document", "record_audio": "Record audio", "connecting": "Connecting…", "camera_off_status": "Camera off", "you": "You", "agent": "Agent" }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Language-keyed string lookup with an EN fallback (audit #8 — no hardcoded
3
+ * English). Returns the key itself when neither the requested language nor EN
4
+ * has it, so a missing key is visible rather than silently blank.
5
+ */
6
+ export declare function makeT(language?: string): (key: string) => string;
7
+ export declare function initI18n(language?: string): any;
@@ -0,0 +1,43 @@
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.makeT = makeT;
7
+ exports.initI18n = initI18n;
8
+ // src/i18n/index.ts
9
+ //
10
+ // Two i18n surfaces share the SAME bundled JSON strings:
11
+ //
12
+ // - makeT(language): a tiny, synchronous, language-keyed lookup with an EN
13
+ // fallback. It imports en/ar JSON as plain objects so NO native peer
14
+ // (react-native-localize) is pulled in, and it works before initI18n has run.
15
+ // This is the helper every prop-driven UI component (Composer/Header/
16
+ // CallControls/VideoCall/WebChat launcher) uses to translate (audit #8).
17
+ //
18
+ // - initI18n(language): the full i18next runtime, lazily wired by the provider.
19
+ // It is the only consumer of react-native-localize, which is lazy-required
20
+ // INSIDE the function so a static `import { makeT } from "../i18n"` never
21
+ // loads the native peer.
22
+ const en_json_1 = __importDefault(require("./en.json"));
23
+ const ar_json_1 = __importDefault(require("./ar.json"));
24
+ const STRINGS = { en: en_json_1.default, ar: ar_json_1.default };
25
+ /**
26
+ * Language-keyed string lookup with an EN fallback (audit #8 — no hardcoded
27
+ * English). Returns the key itself when neither the requested language nor EN
28
+ * has it, so a missing key is visible rather than silently blank.
29
+ */
30
+ function makeT(language) {
31
+ const lang = language === "ar" ? "ar" : "en";
32
+ return (key) => { var _a, _b, _c; return (_c = (_b = (_a = STRINGS[lang]) === null || _a === void 0 ? void 0 : _a[key]) !== null && _b !== void 0 ? _b : STRINGS.en[key]) !== null && _c !== void 0 ? _c : key; };
33
+ }
34
+ function initI18n(language) {
35
+ var _a, _b;
36
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
37
+ const i18n = (_a = require("i18next").default) !== null && _a !== void 0 ? _a : require("i18next");
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
+ const { getLocales } = require("react-native-localize");
40
+ const lng = language !== null && language !== void 0 ? language : (((_b = getLocales()[0]) === null || _b === void 0 ? void 0 : _b.languageCode) === "ar" ? "ar" : "en");
41
+ i18n.init({ resources: { en: { translation: en_json_1.default }, ar: { translation: ar_json_1.default } }, lng, fallbackLng: "en", interpolation: { escapeValue: false } });
42
+ return i18n;
43
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ export { createWebchatClient } from "./core/WebchatClient";
2
+ export type { MinimalSocket, WebchatClientOptions } from "./core/WebchatClient";
3
+ export { createSocket } from "./core/socketFactory";
4
+ export { messagesReducer, initialState } from "./core/messagesReducer";
5
+ export type { MessagesState, MessagesAction } from "./core/messagesReducer";
6
+ export { unreadCount } from "./core/unread";
7
+ export { buildGreet, shouldGreet } from "./core/greet";
8
+ export { MemoryStore, saveSession, loadSession, clearSession, } from "./core/persistence";
9
+ export type { KeyValueStore, PersistedSession } from "./core/persistence";
10
+ export { linkify, isSafeHref } from "./core/linkify";
11
+ export type { Token } from "./core/linkify";
12
+ export { dirStyles } from "./theme/dir";
13
+ export type { Dir } from "./theme/dir";
14
+ export { resolveVariant, buildTheme } from "./theme/themeFactory";
15
+ export type { Variant, PublicStyle, WebChatTheme } from "./theme/themeFactory";
16
+ export * from "./core/types";
17
+ export { fetchConfig, mergeConfig, DEFAULT_CONFIG } from "./core/configClient";
18
+ export type { FetchLike, ServerConfig } from "./core/configClient";
19
+ export { createLogger, logger } from "./core/logger";
20
+ export type { Logger } from "./core/logger";
21
+ export { validateSelection, acceptedMimeTypes, stripBase64OnAck, DEFAULT_LIMITS, } from "./core/media";
22
+ export type { MediaFile, MediaLimits, RejectReason, RejectedFile, ValidationResult, } from "./core/media";
23
+ export { toDataUrl, toMediaEntry } from "./adapters/picker";
24
+ export type { Asset, PickerAdapter, PickImagesOptions, PickDocumentsOptions, MediaEntry, } from "./adapters/picker";
25
+ export { CANONICAL_AUDIO_MIME, NoopAudioAdapter } from "./adapters/audio";
26
+ export type { AudioAdapter, RecordingResult } from "./adapters/audio";
27
+ export { createExpoPicker, createExpoAudioAdapter, createExpoReadBase64, } from "./adapters/expoDefaults";
28
+ export { WebChat } from "./ui/WebChat";
29
+ export type { WebChatHandle } from "./ui/WebChat";
30
+ export { ImageBubble } from "./ui/ImageBubble";
31
+ export type { ImageBubbleProps } from "./ui/ImageBubble";
32
+ export { FileTile } from "./ui/FileTile";
33
+ export type { FileTileProps } from "./ui/FileTile";
34
+ export { AttachButton } from "./ui/AttachButton";
35
+ export type { AttachButtonProps, PendingAttachment } from "./ui/AttachButton";
36
+ export { MediaUploadMenu } from "./ui/MediaUploadMenu";
37
+ export type { MediaUploadMenuProps, MediaUploadOption } from "./ui/MediaUploadMenu";
38
+ export { Icon } from "./ui/Icon";
39
+ export type { IconName, IconProps } from "./ui/Icon";
40
+ export { AudioRecorder } from "./ui/AudioRecorder";
41
+ export type { AudioRecorderProps } from "./ui/AudioRecorder";
42
+ export { VoiceMessage } from "./ui/VoiceMessage";
43
+ export type { VoiceMessageProps } from "./ui/VoiceMessage";
44
+ export { TypingIndicator } from "./ui/TypingIndicator";
45
+ export { PoweredBy } from "./ui/PoweredBy";
46
+ export { createVideoCallClient } from "./core/VideoCallClient";
47
+ export type { VideoCallClient, VideoCallClientOptions, VideoCallEvent, WebRTCFactory, FakeableStream, FakeableTrack, FakeablePeerConnection, MinimalSocket as VideoCallSocket, } from "./core/VideoCallClient";
48
+ export { iceServers, isForwardableCandidate, TURN_URL, TURN_USERNAME, TURN_CREDENTIAL, STUN_URL, } from "./core/ice";
49
+ export type { PcConfig, IceServer as VideoIceServer, CandidateType } from "./core/ice";
50
+ export { reactNativeWebRTCFactory, teardownPeerConnection } from "./adapters/webrtc";
51
+ export type { RTCPeerConnectionLike, MediaStreamLike, MediaStreamTrackLike, PeerConnectionConfig, MediaStreamConstraints, } from "./adapters/webrtc";
52
+ export { AudioRoute, reactNativeAudioRoute } from "./adapters/audioRoute";
53
+ export type { InCallManagerLike, AudioRouteStartOptions } from "./adapters/audioRoute";
54
+ export { VideoCall } from "./ui/VideoCall";
55
+ export type { VideoCallProps, AudioRouteLike } from "./ui/VideoCall";
56
+ export { CallControls } from "./ui/CallControls";
57
+ export type { CallControlsProps } from "./ui/CallControls";
58
+ export { VideoTile } from "./ui/VideoTile";
59
+ export type { VideoTileProps, VideoTileStream } from "./ui/VideoTile";
package/lib/index.js ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // Public entrypoint — framework-agnostic core + theme layer (Phase 0 + Phase 1 logic).
3
+ // The React `state/` provider and `ui/` components are added with the RN toolchain layer.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.reactNativeWebRTCFactory = exports.STUN_URL = exports.TURN_CREDENTIAL = exports.TURN_USERNAME = exports.TURN_URL = exports.isForwardableCandidate = exports.iceServers = exports.createVideoCallClient = exports.PoweredBy = exports.TypingIndicator = exports.VoiceMessage = exports.AudioRecorder = exports.Icon = exports.MediaUploadMenu = exports.AttachButton = exports.FileTile = exports.ImageBubble = exports.WebChat = exports.createExpoReadBase64 = exports.createExpoAudioAdapter = exports.createExpoPicker = exports.NoopAudioAdapter = exports.CANONICAL_AUDIO_MIME = exports.toMediaEntry = exports.toDataUrl = exports.DEFAULT_LIMITS = exports.stripBase64OnAck = exports.acceptedMimeTypes = exports.validateSelection = exports.logger = exports.createLogger = exports.DEFAULT_CONFIG = exports.mergeConfig = exports.fetchConfig = exports.buildTheme = exports.resolveVariant = exports.dirStyles = exports.isSafeHref = exports.linkify = exports.clearSession = exports.loadSession = exports.saveSession = exports.MemoryStore = exports.shouldGreet = exports.buildGreet = exports.unreadCount = exports.initialState = exports.messagesReducer = exports.createSocket = exports.createWebchatClient = void 0;
20
+ exports.VideoTile = exports.CallControls = exports.VideoCall = exports.reactNativeAudioRoute = exports.AudioRoute = exports.teardownPeerConnection = void 0;
21
+ // Transport + protocol
22
+ var WebchatClient_1 = require("./core/WebchatClient");
23
+ Object.defineProperty(exports, "createWebchatClient", { enumerable: true, get: function () { return WebchatClient_1.createWebchatClient; } });
24
+ var socketFactory_1 = require("./core/socketFactory");
25
+ Object.defineProperty(exports, "createSocket", { enumerable: true, get: function () { return socketFactory_1.createSocket; } });
26
+ // State logic
27
+ var messagesReducer_1 = require("./core/messagesReducer");
28
+ Object.defineProperty(exports, "messagesReducer", { enumerable: true, get: function () { return messagesReducer_1.messagesReducer; } });
29
+ Object.defineProperty(exports, "initialState", { enumerable: true, get: function () { return messagesReducer_1.initialState; } });
30
+ var unread_1 = require("./core/unread");
31
+ Object.defineProperty(exports, "unreadCount", { enumerable: true, get: function () { return unread_1.unreadCount; } });
32
+ var greet_1 = require("./core/greet");
33
+ Object.defineProperty(exports, "buildGreet", { enumerable: true, get: function () { return greet_1.buildGreet; } });
34
+ Object.defineProperty(exports, "shouldGreet", { enumerable: true, get: function () { return greet_1.shouldGreet; } });
35
+ // Persistence
36
+ var persistence_1 = require("./core/persistence");
37
+ Object.defineProperty(exports, "MemoryStore", { enumerable: true, get: function () { return persistence_1.MemoryStore; } });
38
+ Object.defineProperty(exports, "saveSession", { enumerable: true, get: function () { return persistence_1.saveSession; } });
39
+ Object.defineProperty(exports, "loadSession", { enumerable: true, get: function () { return persistence_1.loadSession; } });
40
+ Object.defineProperty(exports, "clearSession", { enumerable: true, get: function () { return persistence_1.clearSession; } });
41
+ // Text rendering
42
+ var linkify_1 = require("./core/linkify");
43
+ Object.defineProperty(exports, "linkify", { enumerable: true, get: function () { return linkify_1.linkify; } });
44
+ Object.defineProperty(exports, "isSafeHref", { enumerable: true, get: function () { return linkify_1.isSafeHref; } });
45
+ // Theme + RTL
46
+ var dir_1 = require("./theme/dir");
47
+ Object.defineProperty(exports, "dirStyles", { enumerable: true, get: function () { return dir_1.dirStyles; } });
48
+ var themeFactory_1 = require("./theme/themeFactory");
49
+ Object.defineProperty(exports, "resolveVariant", { enumerable: true, get: function () { return themeFactory_1.resolveVariant; } });
50
+ Object.defineProperty(exports, "buildTheme", { enumerable: true, get: function () { return themeFactory_1.buildTheme; } });
51
+ // Shared types
52
+ __exportStar(require("./core/types"), exports);
53
+ // Config fetch + merge (S5/S8): server view + prop>server>default precedence
54
+ var configClient_1 = require("./core/configClient");
55
+ Object.defineProperty(exports, "fetchConfig", { enumerable: true, get: function () { return configClient_1.fetchConfig; } });
56
+ Object.defineProperty(exports, "mergeConfig", { enumerable: true, get: function () { return configClient_1.mergeConfig; } });
57
+ Object.defineProperty(exports, "DEFAULT_CONFIG", { enumerable: true, get: function () { return configClient_1.DEFAULT_CONFIG; } });
58
+ // Dev-gated logger (C18): no-ops in release, never logs session_id/content
59
+ var logger_1 = require("./core/logger");
60
+ Object.defineProperty(exports, "createLogger", { enumerable: true, get: function () { return logger_1.createLogger; } });
61
+ Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
62
+ // Media (Phase 2): policy/limit helpers — pure, no I/O
63
+ var media_1 = require("./core/media");
64
+ Object.defineProperty(exports, "validateSelection", { enumerable: true, get: function () { return media_1.validateSelection; } });
65
+ Object.defineProperty(exports, "acceptedMimeTypes", { enumerable: true, get: function () { return media_1.acceptedMimeTypes; } });
66
+ Object.defineProperty(exports, "stripBase64OnAck", { enumerable: true, get: function () { return media_1.stripBase64OnAck; } });
67
+ Object.defineProperty(exports, "DEFAULT_LIMITS", { enumerable: true, get: function () { return media_1.DEFAULT_LIMITS; } });
68
+ // Adapters (Phase 2): injectable picker + audio capabilities (real impls live in the example
69
+ // apps; tests inject fakes — the core never calls a native module directly)
70
+ var picker_1 = require("./adapters/picker");
71
+ Object.defineProperty(exports, "toDataUrl", { enumerable: true, get: function () { return picker_1.toDataUrl; } });
72
+ Object.defineProperty(exports, "toMediaEntry", { enumerable: true, get: function () { return picker_1.toMediaEntry; } });
73
+ var audio_1 = require("./adapters/audio");
74
+ Object.defineProperty(exports, "CANONICAL_AUDIO_MIME", { enumerable: true, get: function () { return audio_1.CANONICAL_AUDIO_MIME; } });
75
+ Object.defineProperty(exports, "NoopAudioAdapter", { enumerable: true, get: function () { return audio_1.NoopAudioAdapter; } });
76
+ // Batteries-included default adapters: auto-wired inside <WebChat> from installed
77
+ // Expo peers (expo-image-picker / expo-document-picker / expo-file-system / expo-audio),
78
+ // each returning undefined when its OPTIONAL peer is absent (graceful degradation).
79
+ // Exported so a host can compose its own surface or override selectively.
80
+ var expoDefaults_1 = require("./adapters/expoDefaults");
81
+ Object.defineProperty(exports, "createExpoPicker", { enumerable: true, get: function () { return expoDefaults_1.createExpoPicker; } });
82
+ Object.defineProperty(exports, "createExpoAudioAdapter", { enumerable: true, get: function () { return expoDefaults_1.createExpoAudioAdapter; } });
83
+ Object.defineProperty(exports, "createExpoReadBase64", { enumerable: true, get: function () { return expoDefaults_1.createExpoReadBase64; } });
84
+ // Public component
85
+ var WebChat_1 = require("./ui/WebChat");
86
+ Object.defineProperty(exports, "WebChat", { enumerable: true, get: function () { return WebChat_1.WebChat; } });
87
+ // Media UI (Phase 2): exported for hosts that compose their own surface
88
+ var ImageBubble_1 = require("./ui/ImageBubble");
89
+ Object.defineProperty(exports, "ImageBubble", { enumerable: true, get: function () { return ImageBubble_1.ImageBubble; } });
90
+ var FileTile_1 = require("./ui/FileTile");
91
+ Object.defineProperty(exports, "FileTile", { enumerable: true, get: function () { return FileTile_1.FileTile; } });
92
+ var AttachButton_1 = require("./ui/AttachButton");
93
+ Object.defineProperty(exports, "AttachButton", { enumerable: true, get: function () { return AttachButton_1.AttachButton; } });
94
+ // Media-upload selector: the controlled bottom-sheet menu the composer opens
95
+ // from the attach button (image / document / record-audio rows).
96
+ var MediaUploadMenu_1 = require("./ui/MediaUploadMenu");
97
+ Object.defineProperty(exports, "MediaUploadMenu", { enumerable: true, get: function () { return MediaUploadMenu_1.MediaUploadMenu; } });
98
+ // Themeable Material-style icon set (SVG with a Text-glyph fallback when the
99
+ // optional react-native-svg peer is absent).
100
+ var Icon_1 = require("./ui/Icon");
101
+ Object.defineProperty(exports, "Icon", { enumerable: true, get: function () { return Icon_1.Icon; } });
102
+ var AudioRecorder_1 = require("./ui/AudioRecorder");
103
+ Object.defineProperty(exports, "AudioRecorder", { enumerable: true, get: function () { return AudioRecorder_1.AudioRecorder; } });
104
+ var VoiceMessage_1 = require("./ui/VoiceMessage");
105
+ Object.defineProperty(exports, "VoiceMessage", { enumerable: true, get: function () { return VoiceMessage_1.VoiceMessage; } });
106
+ // Chrome features (Tier B-3a): typing indicator + powered-by badge.
107
+ var TypingIndicator_1 = require("./ui/TypingIndicator");
108
+ Object.defineProperty(exports, "TypingIndicator", { enumerable: true, get: function () { return TypingIndicator_1.TypingIndicator; } });
109
+ var PoweredBy_1 = require("./ui/PoweredBy");
110
+ Object.defineProperty(exports, "PoweredBy", { enumerable: true, get: function () { return PoweredBy_1.PoweredBy; } });
111
+ // --- Phase 3: video calls (WebRTC) -----------------------------------------
112
+ // Signaling core (answerer-only state machine) + its injectable seams. The WebRTC
113
+ // factory is the only native seam; tests/chat-only consumers never load it.
114
+ var VideoCallClient_1 = require("./core/VideoCallClient");
115
+ Object.defineProperty(exports, "createVideoCallClient", { enumerable: true, get: function () { return VideoCallClient_1.createVideoCallClient; } });
116
+ // Static ICE / pc_config helpers (web parity, B12) + the host-candidate filter (C7).
117
+ var ice_1 = require("./core/ice");
118
+ Object.defineProperty(exports, "iceServers", { enumerable: true, get: function () { return ice_1.iceServers; } });
119
+ Object.defineProperty(exports, "isForwardableCandidate", { enumerable: true, get: function () { return ice_1.isForwardableCandidate; } });
120
+ Object.defineProperty(exports, "TURN_URL", { enumerable: true, get: function () { return ice_1.TURN_URL; } });
121
+ Object.defineProperty(exports, "TURN_USERNAME", { enumerable: true, get: function () { return ice_1.TURN_USERNAME; } });
122
+ Object.defineProperty(exports, "TURN_CREDENTIAL", { enumerable: true, get: function () { return ice_1.TURN_CREDENTIAL; } });
123
+ Object.defineProperty(exports, "STUN_URL", { enumerable: true, get: function () { return ice_1.STUN_URL; } });
124
+ // Real native adapters (lazy-require native bindings inside; importing the type is free)
125
+ // + the teardown helper that fully releases the camera/mic (C10).
126
+ var webrtc_1 = require("./adapters/webrtc");
127
+ Object.defineProperty(exports, "reactNativeWebRTCFactory", { enumerable: true, get: function () { return webrtc_1.reactNativeWebRTCFactory; } });
128
+ Object.defineProperty(exports, "teardownPeerConnection", { enumerable: true, get: function () { return webrtc_1.teardownPeerConnection; } });
129
+ var audioRoute_1 = require("./adapters/audioRoute");
130
+ Object.defineProperty(exports, "AudioRoute", { enumerable: true, get: function () { return audioRoute_1.AudioRoute; } });
131
+ Object.defineProperty(exports, "reactNativeAudioRoute", { enumerable: true, get: function () { return audioRoute_1.reactNativeAudioRoute; } });
132
+ // Call UI: full in-call screen + the control bar + a single video tile (compose your own).
133
+ var VideoCall_1 = require("./ui/VideoCall");
134
+ Object.defineProperty(exports, "VideoCall", { enumerable: true, get: function () { return VideoCall_1.VideoCall; } });
135
+ var CallControls_1 = require("./ui/CallControls");
136
+ Object.defineProperty(exports, "CallControls", { enumerable: true, get: function () { return CallControls_1.CallControls; } });
137
+ var VideoTile_1 = require("./ui/VideoTile");
138
+ Object.defineProperty(exports, "VideoTile", { enumerable: true, get: function () { return VideoTile_1.VideoTile; } });
139
+ // Expo config-plugin (Phase 2 / Task 7): permission injection for media + mic.
140
+ // Hosts add it to app.json/app.config: `"plugins": ["@experiaapp/webchat-react-native"]`
141
+ // (resolves to `app.plugin.js` at the package root). Not re-exported here because the
142
+ // plugin module is consumed by the Expo CLI, not imported into app code.