@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
package/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # @experiaapp/webchat-react-native
2
+
3
+ A drop-in **React Native webchat SDK** — real-time chat, media attachments (images, documents, voice notes), and 1:1 video calls — that you embed in your app with a single component. Theming, language (English/Arabic), and behavior are driven by your channel configuration.
4
+
5
+ - 💬 Real-time messaging (text, quick replies, message status, unread tracking)
6
+ - 📎 Attachments — photos, PDF documents, and recorded voice notes
7
+ - 🎥 In-app video calls (optional)
8
+ - 🎨 Server-driven theming + a customizable launcher
9
+ - 🌍 English / Arabic with full RTL
10
+ - ⚙️ Works in **Expo** and **bare React Native**
11
+
12
+ ---
13
+
14
+ ## Requirements
15
+
16
+ - React Native **>= 0.74**, React **>= 18**
17
+ - An Expo-managed / dev-client app, **or** a bare RN app with Expo Modules (see [Bare React Native](#bare-react-native))
18
+ - A **channel** and connection details from your Experia account
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # the SDK
26
+ npm install @experiaapp/webchat-react-native
27
+
28
+ # required peers
29
+ npx expo install \
30
+ react-native-safe-area-context \
31
+ react-native-localize \
32
+ @react-native-async-storage/async-storage
33
+ ```
34
+
35
+ ### Optional peers (each enables a feature when installed)
36
+
37
+ Install only what you need — the SDK auto-detects them and degrades gracefully when absent:
38
+
39
+ | Feature | Install |
40
+ | --- | --- |
41
+ | Photo / document attachments | `npx expo install expo-image-picker expo-document-picker expo-file-system` |
42
+ | Voice-note recording & playback | `npx expo install expo-audio` |
43
+ | Crisp vector icons | `npx expo install react-native-svg` |
44
+ | Connectivity-aware reconnect | `npx expo install @react-native-community/netinfo` |
45
+ | Video calls | `npx expo install react-native-webrtc react-native-incall-manager` |
46
+
47
+ ### Expo config plugin
48
+
49
+ Add the plugin to your `app.json` / `app.config.js` so the required iOS/Android permissions (camera, microphone, photo library) are injected at build time:
50
+
51
+ ```jsonc
52
+ {
53
+ "expo": {
54
+ "plugins": [
55
+ "@experiaapp/webchat-react-native",
56
+ "@config-plugins/react-native-webrtc" // only if you use video calls
57
+ ]
58
+ }
59
+ }
60
+ ```
61
+
62
+ Then create a development build (`eas build`, or `expo prebuild && expo run:ios|run:android`) — the chat, media, and video features rely on native modules and **won't work in Expo Go**.
63
+
64
+ ### Bare React Native
65
+
66
+ The optional Expo peers above need the Expo Modules runtime. In a bare app, run once:
67
+
68
+ ```bash
69
+ npx install-expo-modules@latest
70
+ ```
71
+
72
+ Then `cd ios && pod install` and rebuild.
73
+
74
+ ---
75
+
76
+ ## Quick start
77
+
78
+ ```tsx
79
+ import React from "react";
80
+ import { SafeAreaProvider } from "react-native-safe-area-context";
81
+ import { WebChat } from "@experiaapp/webchat-react-native";
82
+
83
+ const CONFIG = {
84
+ channelId: "YOUR_CHANNEL_ID",
85
+ connectionUrl: "YOUR_SOCKET_HOST",
86
+ configUrl: "YOUR_CONFIG_HOST",
87
+ language: "en", // "en" | "ar"
88
+ };
89
+
90
+ export default function App() {
91
+ return (
92
+ <SafeAreaProvider>
93
+ <WebChat config={CONFIG} />
94
+ </SafeAreaProvider>
95
+ );
96
+ }
97
+ ```
98
+
99
+ That's the whole integration: a floating launcher button appears, and tapping it opens the chat. **Branding, title, avatar, and theme come from your channel configuration** (fetched via `configUrl`), so you usually only provide the connection details above.
100
+
101
+ > `channelId`, `connectionUrl`, and `configUrl` identify and connect your channel — you receive these with your Experia account.
102
+
103
+ ---
104
+
105
+ ## Configuration
106
+
107
+ The `config` prop carries your **connection details**. Everything else — video calls, attachments, the pre-chat form, branding, title, avatar, and colors — is configured in your **Experia channel** and fetched at runtime via `configUrl`. In most apps you only pass the connection details (plus, optionally, the language):
108
+
109
+ ```tsx
110
+ <WebChat
111
+ config={{
112
+ channelId: "YOUR_CHANNEL_ID",
113
+ connectionUrl: "YOUR_SOCKET_HOST",
114
+ configUrl: "YOUR_CONFIG_HOST",
115
+ language: "en", // "en" | "ar" — drives RTL (optional)
116
+ }}
117
+ />
118
+ ```
119
+
120
+ ### Managed in your Experia channel (no app changes)
121
+
122
+ Controlled from your channel configuration and applied automatically — you don't set these in code:
123
+
124
+ - **Features:** video calls, image / document / voice attachments, emoji picker, typing indicator, "powered by" badge
125
+ - **Launcher & chrome:** default launcher visibility, close button, online/offline status, connecting text
126
+ - **Branding:** theme & colors, header title/subtitle, agent avatar, launcher image
127
+ - **Pre-chat form:** enabled, fields (text / email / phone / dropdown), labels, headline, submit label
128
+ - **Behavior:** history-persistence window, welcome message
129
+
130
+ ### Advanced — overriding the channel config
131
+
132
+ Precedence is **prop → channel → default**, so you *can* override any setting per build by passing it on `config`. This is an escape hatch, not the usual path — e.g. force a pre-chat form or turn video off regardless of the channel:
133
+
134
+ ```tsx
135
+ <WebChat
136
+ config={{
137
+ channelId: "YOUR_CHANNEL_ID",
138
+ connectionUrl: "YOUR_SOCKET_HOST",
139
+ configUrl: "YOUR_CONFIG_HOST",
140
+ makeVideoCall: false, // override the channel
141
+ prechatForm: {
142
+ enabled: true,
143
+ fields: [{ key: "name", type: "text", label: { en: "Name", ar: "الاسم" }, required: true }],
144
+ },
145
+ }}
146
+ />
147
+ ```
148
+
149
+ ### Host-side props
150
+
151
+ These live on the component, not the channel config:
152
+
153
+ | Prop | Type | Description |
154
+ | --- | --- | --- |
155
+ | `config` | object | Connection details (+ optional overrides). **Required.** |
156
+ | `open` | `boolean` | Open the surface on mount. |
157
+ | `title` | `string` | Override the channel's header title. |
158
+ | `renderLauncher` | `({ open }) => ReactNode` | Render your own launcher (see below). |
159
+ | `prechatValues` | `Record<string, string>` | Pre-fill the greeting with known user data. |
160
+ | `disabledInput` | `boolean` | Lock the composer. |
161
+ | `secureScreen` | `boolean` | Block screen capture while the chat is open (best-effort). |
162
+
163
+ Callbacks: `onOpen`, `onClose`, `onMessageReceived(message)`, `onConnected`, `onDisconnected`, `onError(error)`, `onUnreadChange(count)`.
164
+
165
+ ---
166
+
167
+ ## Controlling the chat (imperative API)
168
+
169
+ Attach a `ref` to open/close the chat or manage the session from anywhere in your app:
170
+
171
+ ```tsx
172
+ import { useRef } from "react";
173
+ import { WebChat, type WebChatHandle } from "@experiaapp/webchat-react-native";
174
+
175
+ const chat = useRef<WebChatHandle>(null);
176
+
177
+ <WebChat ref={chat} config={CONFIG} />;
178
+
179
+ // elsewhere:
180
+ chat.current?.open();
181
+ chat.current?.close();
182
+ chat.current?.sendMessage("Hello");
183
+ chat.current?.getUnreadCount(); // number
184
+ chat.current?.clearHistory(); // drop messages, keep the session
185
+ chat.current?.resetSession(); // start a new session + clear history
186
+ ```
187
+
188
+ > `clearHistory()` / `resetSession()` erase the locally stored conversation — call one on user logout.
189
+
190
+ ---
191
+
192
+ ## Customizing the launcher
193
+
194
+ The default is a round floating button. You can change it four ways:
195
+
196
+ **1. Your own launcher (any shape):**
197
+
198
+ ```tsx
199
+ <WebChat
200
+ config={CONFIG}
201
+ renderLauncher={({ open }) => (
202
+ <Pressable onPress={open} style={{ /* a pill, bar, avatar, anything */ }}>
203
+ <Text>Chat with us</Text>
204
+ </Pressable>
205
+ )}
206
+ />
207
+ ```
208
+
209
+ **2. Trigger from your own UI** — hide the built-in button and open via the ref:
210
+
211
+ ```tsx
212
+ <WebChat ref={chat} config={{ ...CONFIG, showButtonChat: false }} />
213
+ // <YourButton onPress={() => chat.current?.open()} />
214
+ ```
215
+
216
+ **3. Reshape the default** via channel theme tokens (size, color, corner radius).
217
+
218
+ **4. Use a branded image** via the `openLauncherImage` channel config.
219
+
220
+ ---
221
+
222
+ ## Media & video
223
+
224
+ - **Attachments:** images (PNG/JPEG), PDF documents, and voice notes. Picking is gated by the `uploadMedia` / `upload*` flags; selected files are validated against the channel's allow-list before sending.
225
+ - **Video calls:** set `makeVideoCall: true` to show the call button (a call can also start automatically when your channel triggers it). Video requires the `react-native-webrtc` + `react-native-incall-manager` peers, the `@config-plugins/react-native-webrtc` Expo plugin (managed apps), and a development build.
226
+
227
+ Notifications: the SDK does **not** integrate push. Use the `onMessageReceived` callback to fire your own local notification.
228
+
229
+ ---
230
+
231
+ ## TypeScript
232
+
233
+ The package ships full type declarations. Import types alongside values:
234
+
235
+ ```tsx
236
+ import { WebChat, type WebChatHandle } from "@experiaapp/webchat-react-native";
237
+ ```
238
+
239
+ Lower-level building blocks (the theme helpers, media utilities, individual UI pieces, and the video-call primitives) are also exported if you need to compose a custom surface.
240
+
241
+ ---
242
+
243
+ ## Troubleshooting
244
+
245
+ - **Native feature does nothing / picker won't open in Expo Go** — Expo Go can't load the native modules; use a development build (`eas build` / `expo prebuild`).
246
+ - **Icons render as plain glyphs** — install `react-native-svg`.
247
+ - **Video-call button missing** — set `makeVideoCall: true`, install the WebRTC peers + plugin, and ensure your channel enables it.
248
+ - **The same conversation keeps resuming** — that's `storage: "localStorage"` (the default). Use `"sessionStorage"` to start fresh each launch, or call `resetSession()`.
249
+
250
+ ---
251
+
252
+ ## License
253
+
254
+ Proprietary. © Experia. All rights reserved. Use is governed by your agreement with Experia.
package/app.plugin.js ADDED
@@ -0,0 +1,6 @@
1
+ // Expo loads this file (by convention) when the package name appears in `expo.plugins`.
2
+ // It re-exports the compiled config plugin from `lib/` (built via `tsc -p tsconfig.build.json`).
3
+ // CommonJS interop: the plugin is the default export, so unwrap `.default`.
4
+ const mod = require('./lib/plugin/withWebchat');
5
+
6
+ module.exports = mod.default || mod;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Canonical voice-note codec, RESOLVED for web parity (plan B6 / Task 6).
3
+ *
4
+ * The backend expects exactly what the web widget sends: Ogg/Opus. A real
5
+ * Opus-capable recorder MUST be injected — note that iOS cannot natively
6
+ * record Opus, so the bare/Expo impls have to wrap an Opus-capable library
7
+ * (validated in the Task 6 pre-step). Tests inject {@link NoopAudioAdapter}
8
+ * and assert against this constant rather than calling a native module.
9
+ */
10
+ export declare const CANONICAL_AUDIO_MIME: "audio/ogg;codecs=opus";
11
+ /**
12
+ * Result of a completed recording. `mimeType` is expected to equal
13
+ * {@link CANONICAL_AUDIO_MIME}; `durationMs` is the recorded length.
14
+ */
15
+ export interface RecordingResult {
16
+ uri: string;
17
+ mimeType: string;
18
+ durationMs: number;
19
+ }
20
+ /**
21
+ * Live playback progress (Task 6 UI). Reported by an engine that emits position
22
+ * updates so a player can show "current / total" and reset when the note ends.
23
+ * Times are in ms; `durationMs` is 0 until the engine knows the media length.
24
+ */
25
+ export interface PlaybackStatus {
26
+ positionMs: number;
27
+ durationMs: number;
28
+ isPlaying: boolean;
29
+ didJustFinish: boolean;
30
+ }
31
+ /**
32
+ * The injectable audio surface. `startRecording`/`stopRecording` capture a
33
+ * note; `play`/`stop` drive inline playback. Real impls request mic permission
34
+ * and coordinate mic mutual-exclusion with video calls at the call site.
35
+ */
36
+ export interface AudioAdapter {
37
+ startRecording(): Promise<void>;
38
+ stopRecording(): Promise<RecordingResult>;
39
+ /**
40
+ * Start inline playback of `uri`. When `opts.onStatus` is provided AND the
41
+ * engine reports progress, it is called with position/duration so the player UI
42
+ * can show "current / total" and reset on finish. Engines without progress
43
+ * events simply never call it (the UI keeps showing the recorded total).
44
+ */
45
+ play(uri: string, opts?: {
46
+ onStatus?: (status: PlaybackStatus) => void;
47
+ }): Promise<void>;
48
+ stop(): Promise<void>;
49
+ /**
50
+ * Marks an adapter whose `startRecording`/`stopRecording` are INERT stubs (e.g.
51
+ * the default {@link createExpoAudioAdapter}, where recording is hook-only and
52
+ * cannot live on an object method). When `true`, the {@link AudioRecorder}
53
+ * component MUST NOT call this adapter to capture a note — it drives the
54
+ * expo-audio `useAudioRecorder` hook directly instead (and falls back to a
55
+ * hidden/disabled recorder when expo-audio is absent). When absent/false the
56
+ * adapter's imperative `startRecording`/`stopRecording` are the real capture
57
+ * surface (a host-injected recorder, or the test fakes) and are used as-is.
58
+ */
59
+ recordingDelegated?: boolean;
60
+ }
61
+ /**
62
+ * Inert adapter for unit tests and SSR/no-mic environments. It records no audio
63
+ * and returns a zero-length result tagged with the canonical codec, so the
64
+ * surrounding state machine and attach() path can be exercised without any
65
+ * native module.
66
+ */
67
+ export declare class NoopAudioAdapter implements AudioAdapter {
68
+ startRecording(): Promise<void>;
69
+ stopRecording(): Promise<RecordingResult>;
70
+ play(_uri: string, _opts?: {
71
+ onStatus?: (status: PlaybackStatus) => void;
72
+ }): Promise<void>;
73
+ stop(): Promise<void>;
74
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ // src/adapters/audio.ts
3
+ //
4
+ // Voice record/playback behind an injectable interface so the core never
5
+ // touches a native audio module directly. The recorder MUST emit the canonical
6
+ // codec below (web parity); the player accepts a local/remote URI.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.NoopAudioAdapter = exports.CANONICAL_AUDIO_MIME = void 0;
9
+ /**
10
+ * Canonical voice-note codec, RESOLVED for web parity (plan B6 / Task 6).
11
+ *
12
+ * The backend expects exactly what the web widget sends: Ogg/Opus. A real
13
+ * Opus-capable recorder MUST be injected — note that iOS cannot natively
14
+ * record Opus, so the bare/Expo impls have to wrap an Opus-capable library
15
+ * (validated in the Task 6 pre-step). Tests inject {@link NoopAudioAdapter}
16
+ * and assert against this constant rather than calling a native module.
17
+ */
18
+ exports.CANONICAL_AUDIO_MIME = "audio/ogg;codecs=opus";
19
+ /**
20
+ * Inert adapter for unit tests and SSR/no-mic environments. It records no audio
21
+ * and returns a zero-length result tagged with the canonical codec, so the
22
+ * surrounding state machine and attach() path can be exercised without any
23
+ * native module.
24
+ */
25
+ class NoopAudioAdapter {
26
+ async startRecording() {
27
+ /* no-op */
28
+ }
29
+ async stopRecording() {
30
+ return { uri: "", mimeType: exports.CANONICAL_AUDIO_MIME, durationMs: 0 };
31
+ }
32
+ async play(_uri, _opts) {
33
+ /* no-op */
34
+ }
35
+ async stop() {
36
+ /* no-op */
37
+ }
38
+ }
39
+ exports.NoopAudioAdapter = NoopAudioAdapter;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The subset of `react-native-incall-manager` this wrapper drives. The library
3
+ * exports a singleton instance as its default export; only these four methods
4
+ * are used here. Structural so a `{ start: jest.fn(), ... }` spy satisfies it.
5
+ */
6
+ export interface InCallManagerLike {
7
+ start(setup?: {
8
+ auto?: boolean;
9
+ media?: "video" | "audio";
10
+ ringback?: string;
11
+ }): void;
12
+ stop(setup?: {
13
+ busytone?: string;
14
+ }): void;
15
+ setForceSpeakerphoneOn(flag: boolean): void;
16
+ setSpeakerphoneOn?(enable: boolean): void;
17
+ }
18
+ export interface AudioRouteStartOptions {
19
+ /** `"video"` (default) configures the session for a video call. */
20
+ media?: "video" | "audio";
21
+ }
22
+ /**
23
+ * Owns the audio route for the lifetime of a call. Construct once per call with
24
+ * the injected manager, `start()` on connect, toggle the speaker mid-call, and
25
+ * `stop()` on teardown to hand the audio session back to the OS.
26
+ */
27
+ export declare class AudioRoute {
28
+ private readonly manager;
29
+ private active;
30
+ constructor(manager: InCallManagerLike);
31
+ /**
32
+ * Begin the in-call audio session and route to the loudspeaker by default
33
+ * (B14 — a video call should be audible without holding the phone to the ear).
34
+ * Idempotent: calling `start` twice is a no-op until `stop` runs.
35
+ */
36
+ start(opts?: AudioRouteStartOptions): void;
37
+ /**
38
+ * Toggle the output route mid-call. `true` forces speakerphone; `false`
39
+ * releases the force so audio returns to the earpiece (or a connected
40
+ * Bluetooth/wired route the OS prefers).
41
+ */
42
+ setSpeaker(on: boolean): void;
43
+ /** Whether an in-call audio session is currently active. */
44
+ isActive(): boolean;
45
+ /**
46
+ * End the in-call audio session and restore the previous audio mode. Releases
47
+ * the speakerphone force first so the OS route is clean for the next call /
48
+ * for Phase-2 voice playback (shared-mic mutual exclusion, §4.4). Idempotent.
49
+ */
50
+ stop(): void;
51
+ }
52
+ /**
53
+ * Real {@link AudioRoute} backed by the `react-native-incall-manager` singleton.
54
+ * The module is lazy-required so importing this file for the type alone does not
55
+ * load native bindings (chat-only consumers / the unit-test core never touch it).
56
+ */
57
+ export declare function reactNativeAudioRoute(): AudioRoute;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // src/adapters/audioRoute.ts
3
+ //
4
+ // Audio-route control for video calls, a thin wrapper over
5
+ // `react-native-incall-manager`. The signaling core / call lifecycle calls
6
+ // {@link AudioRoute.start} when a call begins (default speakerphone, B14),
7
+ // {@link AudioRoute.setSpeaker} for the in-call route toggle, and
8
+ // {@link AudioRoute.stop} on teardown to restore the audio session (Plan Task 4).
9
+ //
10
+ // The native manager is injected so this is unit-testable with a spy and never
11
+ // pulls native bindings into the test/core import graph. Real impls construct
12
+ // {@link reactNativeAudioRoute}, which lazy-requires the singleton default
13
+ // export of `react-native-incall-manager`.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AudioRoute = void 0;
16
+ exports.reactNativeAudioRoute = reactNativeAudioRoute;
17
+ /**
18
+ * Owns the audio route for the lifetime of a call. Construct once per call with
19
+ * the injected manager, `start()` on connect, toggle the speaker mid-call, and
20
+ * `stop()` on teardown to hand the audio session back to the OS.
21
+ */
22
+ class AudioRoute {
23
+ constructor(manager) {
24
+ this.active = false;
25
+ this.manager = manager;
26
+ }
27
+ /**
28
+ * Begin the in-call audio session and route to the loudspeaker by default
29
+ * (B14 — a video call should be audible without holding the phone to the ear).
30
+ * Idempotent: calling `start` twice is a no-op until `stop` runs.
31
+ */
32
+ start(opts = {}) {
33
+ var _a;
34
+ if (this.active)
35
+ return;
36
+ this.manager.start({ media: (_a = opts.media) !== null && _a !== void 0 ? _a : "video", auto: false });
37
+ // Force-route to speakerphone so the remote party is audible by default.
38
+ this.manager.setForceSpeakerphoneOn(true);
39
+ this.active = true;
40
+ }
41
+ /**
42
+ * Toggle the output route mid-call. `true` forces speakerphone; `false`
43
+ * releases the force so audio returns to the earpiece (or a connected
44
+ * Bluetooth/wired route the OS prefers).
45
+ */
46
+ setSpeaker(on) {
47
+ this.manager.setForceSpeakerphoneOn(on);
48
+ }
49
+ /** Whether an in-call audio session is currently active. */
50
+ isActive() {
51
+ return this.active;
52
+ }
53
+ /**
54
+ * End the in-call audio session and restore the previous audio mode. Releases
55
+ * the speakerphone force first so the OS route is clean for the next call /
56
+ * for Phase-2 voice playback (shared-mic mutual exclusion, §4.4). Idempotent.
57
+ */
58
+ stop() {
59
+ if (!this.active)
60
+ return;
61
+ this.manager.setForceSpeakerphoneOn(false);
62
+ this.manager.stop();
63
+ this.active = false;
64
+ }
65
+ }
66
+ exports.AudioRoute = AudioRoute;
67
+ /**
68
+ * Real {@link AudioRoute} backed by the `react-native-incall-manager` singleton.
69
+ * The module is lazy-required so importing this file for the type alone does not
70
+ * load native bindings (chat-only consumers / the unit-test core never touch it).
71
+ */
72
+ function reactNativeAudioRoute() {
73
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
74
+ const InCallManager = require("react-native-incall-manager")
75
+ .default;
76
+ return new AudioRoute(InCallManager);
77
+ }
@@ -0,0 +1,77 @@
1
+ import type { PickerAdapter } from "./picker";
2
+ import type { AudioAdapter } from "./audio";
3
+ /**
4
+ * Default {@link PickerAdapter} backed by Expo. Returns `undefined` when NEITHER
5
+ * expo-image-picker nor expo-document-picker is installed — so the attach button
6
+ * stays hidden (feature-detect parity with the absent-prop case). When only one
7
+ * lib is present the adapter exposes only that method (a partial picker is valid;
8
+ * the Surface omits the missing row).
9
+ */
10
+ export declare function createExpoPicker(): PickerAdapter | undefined;
11
+ /**
12
+ * Default file->base64 reader backed by `expo-file-system`. Returns RAW base64
13
+ * (no `data:` prefix) — `toDataUrl` adds the prefix downstream. Only invoked for
14
+ * assets WITHOUT eager base64 (documents, recorded audio); image-picker assets
15
+ * carry `base64` and short-circuit it.
16
+ *
17
+ * SDK drift: SDK 54 promoted the OO `File`/`Directory` API to the package root and
18
+ * DEPRECATED the classic functions. On SDK 56 the root `readAsStringAsync` is a STUB
19
+ * that THROWS at runtime ("This method will throw in runtime"), so we MUST prefer the
20
+ * OO `new File(uri).base64()` and only fall back to `readAsStringAsync` on OLDER SDKs
21
+ * (<=53) where the `File` class doesn't exist. Returns `undefined` when the lib is
22
+ * absent (provider's throwing reader still only fires if an attach without eager
23
+ * base64 is actually attempted).
24
+ */
25
+ export declare function createExpoReadBase64(): ((uri: string) => Promise<string>) | undefined;
26
+ /**
27
+ * Shape of `publicStyle.defaultFontFamily` (web parity). One remote TTF per
28
+ * weight (ExtraLight..Black); `name` is the family-level label. All optional —
29
+ * an absent/empty `fonts` array means "no custom font" and the loader resolves
30
+ * to an empty map. Kept local (not imported from core) so this adapter file has
31
+ * no cross-layer dependency, mirroring the picker/audio adapters.
32
+ */
33
+ export interface BrandFontFamily {
34
+ name?: string;
35
+ fonts?: Array<{
36
+ url: string;
37
+ fontWeight?: string;
38
+ fontName?: string;
39
+ }>;
40
+ }
41
+ /**
42
+ * Default brand-font loader backed by `expo-font`. Returns `undefined` (no-op)
43
+ * when expo-font is absent — so Metro never pulls the native module into the
44
+ * import graph and the jest suite (where it is not installed) keeps working,
45
+ * exactly like the other expo defaults.
46
+ *
47
+ * When present it returns `loadBrandFonts(fontFamily)`, an async loader that:
48
+ * - maps each provided font entry to a stable RN family name (`webchat-{…}`),
49
+ * - calls `Font.loadAsync({ [familyName]: url })` for each (in parallel via
50
+ * Promise.all), TOLERATING individual failures (a bad url drops just that
51
+ * weight, never rejects the whole batch),
52
+ * - RESOLVES to a `weight -> familyName` map for the apply phase. The map is
53
+ * keyed by each entry's `fontWeight` (so `resolveFontFamily(map, weight)`
54
+ * can look a weight up); entries without a `fontWeight` are still loaded but
55
+ * are only reachable via the family-level `name` key (added when present).
56
+ *
57
+ * An empty/undefined `fonts` array resolves to `{}` (no fonts loaded).
58
+ */
59
+ export declare function createExpoFontLoader(): ((fontFamily?: BrandFontFamily) => Promise<Record<string, string>>) | undefined;
60
+ /**
61
+ * Default PLAYBACK-CAPABLE {@link AudioAdapter} backed by `expo-audio`. Returns
62
+ * `undefined` when expo-audio is absent (received-voice falls back to a download
63
+ * tile — parity with the absent-prop case).
64
+ *
65
+ * PLAYBACK (imperative, fully implemented here):
66
+ * play(uri) -> createAudioPlayer(uri); player.play(); retain the player.
67
+ * stop() -> retained player.pause() + player.remove() (MUST remove() to free
68
+ * native resources); null-safe; a single active player is retained,
69
+ * so a new play() removes any prior one first.
70
+ *
71
+ * RECORDING (delegated to Phase 2 component): startRecording()/stopRecording() are
72
+ * inert stubs that keep the AudioAdapter type satisfied. expo-audio's recorder is
73
+ * HOOK-ONLY (useAudioRecorder), so the real capture lives in the <AudioRecorder>
74
+ * component (Phase 2) which drives the hook and yields a RecordingResult tagged with
75
+ * the m4a container mime above. The default object adapter cannot call a hook.
76
+ */
77
+ export declare function createExpoAudioAdapter(): AudioAdapter | undefined;