@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.
- package/README.md +254 -0
- package/app.plugin.js +6 -0
- package/lib/adapters/audio.d.ts +74 -0
- package/lib/adapters/audio.js +39 -0
- package/lib/adapters/audioRoute.d.ts +57 -0
- package/lib/adapters/audioRoute.js +77 -0
- package/lib/adapters/expoDefaults.d.ts +77 -0
- package/lib/adapters/expoDefaults.js +539 -0
- package/lib/adapters/picker.d.ts +67 -0
- package/lib/adapters/picker.js +37 -0
- package/lib/adapters/webrtc.d.ts +131 -0
- package/lib/adapters/webrtc.js +70 -0
- package/lib/core/VideoCallClient.d.ts +106 -0
- package/lib/core/VideoCallClient.js +302 -0
- package/lib/core/WebchatClient.d.ts +34 -0
- package/lib/core/WebchatClient.js +132 -0
- package/lib/core/configClient.d.ts +42 -0
- package/lib/core/configClient.js +302 -0
- package/lib/core/greet.d.ts +11 -0
- package/lib/core/greet.js +17 -0
- package/lib/core/ice.d.ts +31 -0
- package/lib/core/ice.js +48 -0
- package/lib/core/linkify.d.ts +11 -0
- package/lib/core/linkify.js +25 -0
- package/lib/core/logger.d.ts +17 -0
- package/lib/core/logger.js +53 -0
- package/lib/core/media.d.ts +52 -0
- package/lib/core/media.js +115 -0
- package/lib/core/mediaType.d.ts +21 -0
- package/lib/core/mediaType.js +66 -0
- package/lib/core/messagesReducer.d.ts +36 -0
- package/lib/core/messagesReducer.js +58 -0
- package/lib/core/persistence.d.ts +45 -0
- package/lib/core/persistence.js +63 -0
- package/lib/core/socketFactory.d.ts +16 -0
- package/lib/core/socketFactory.js +82 -0
- package/lib/core/types.d.ts +320 -0
- package/lib/core/types.js +30 -0
- package/lib/core/unread.d.ts +2 -0
- package/lib/core/unread.js +5 -0
- package/lib/i18n/ar.json +1 -0
- package/lib/i18n/en.json +1 -0
- package/lib/i18n/index.d.ts +7 -0
- package/lib/i18n/index.js +43 -0
- package/lib/index.d.ts +59 -0
- package/lib/index.js +142 -0
- package/lib/plugin/withWebchat.d.ts +53 -0
- package/lib/plugin/withWebchat.js +164 -0
- package/lib/state/WebchatProvider.d.ts +132 -0
- package/lib/state/WebchatProvider.js +906 -0
- package/lib/state/useWebchat.d.ts +1 -0
- package/lib/state/useWebchat.js +12 -0
- package/lib/theme/dir.d.ts +14 -0
- package/lib/theme/dir.js +20 -0
- package/lib/theme/themeFactory.d.ts +219 -0
- package/lib/theme/themeFactory.js +182 -0
- package/lib/ui/AttachButton.d.ts +35 -0
- package/lib/ui/AttachButton.js +26 -0
- package/lib/ui/AudioRecorder.d.ts +25 -0
- package/lib/ui/AudioRecorder.js +228 -0
- package/lib/ui/Bubble.d.ts +1 -0
- package/lib/ui/Bubble.js +265 -0
- package/lib/ui/CallControls.d.ts +27 -0
- package/lib/ui/CallControls.js +92 -0
- package/lib/ui/CallPlaceholder.d.ts +16 -0
- package/lib/ui/CallPlaceholder.js +73 -0
- package/lib/ui/Composer.d.ts +5 -0
- package/lib/ui/Composer.js +272 -0
- package/lib/ui/FileTile.d.ts +9 -0
- package/lib/ui/FileTile.js +31 -0
- package/lib/ui/Header.d.ts +52 -0
- package/lib/ui/Header.js +236 -0
- package/lib/ui/Icon.d.ts +21 -0
- package/lib/ui/Icon.js +110 -0
- package/lib/ui/ImageBubble.d.ts +11 -0
- package/lib/ui/ImageBubble.js +16 -0
- package/lib/ui/MediaUploadMenu.d.ts +23 -0
- package/lib/ui/MediaUploadMenu.js +68 -0
- package/lib/ui/MessageList.d.ts +1 -0
- package/lib/ui/MessageList.js +46 -0
- package/lib/ui/PoweredBy.d.ts +8 -0
- package/lib/ui/PoweredBy.js +14 -0
- package/lib/ui/PrechatForm.d.ts +1 -0
- package/lib/ui/PrechatForm.js +230 -0
- package/lib/ui/QuickReplies.d.ts +1 -0
- package/lib/ui/QuickReplies.js +24 -0
- package/lib/ui/TypingIndicator.d.ts +9 -0
- package/lib/ui/TypingIndicator.js +88 -0
- package/lib/ui/VideoBubble.d.ts +10 -0
- package/lib/ui/VideoBubble.js +130 -0
- package/lib/ui/VideoCall.d.ts +34 -0
- package/lib/ui/VideoCall.js +191 -0
- package/lib/ui/VideoTile.d.ts +25 -0
- package/lib/ui/VideoTile.js +13 -0
- package/lib/ui/VoiceMessage.d.ts +19 -0
- package/lib/ui/VoiceMessage.js +127 -0
- package/lib/ui/WebChat.d.ts +10 -0
- package/lib/ui/WebChat.js +386 -0
- package/lib/ui/openLink.d.ts +1 -0
- package/lib/ui/openLink.js +16 -0
- package/package.json +94 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebChat = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
// src/ui/WebChat.tsx
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
8
|
+
const react_native_safe_area_context_1 = require("react-native-safe-area-context");
|
|
9
|
+
const WebchatProvider_1 = require("../state/WebchatProvider");
|
|
10
|
+
const useWebchat_1 = require("../state/useWebchat");
|
|
11
|
+
const socketFactory_1 = require("../core/socketFactory");
|
|
12
|
+
const MessageList_1 = require("./MessageList");
|
|
13
|
+
const Composer_1 = require("./Composer");
|
|
14
|
+
const Header_1 = require("./Header");
|
|
15
|
+
const Icon_1 = require("./Icon");
|
|
16
|
+
const PoweredBy_1 = require("./PoweredBy");
|
|
17
|
+
const PrechatForm_1 = require("./PrechatForm");
|
|
18
|
+
const dir_1 = require("../theme/dir");
|
|
19
|
+
const i18n_1 = require("../i18n");
|
|
20
|
+
const expoDefaults_1 = require("../adapters/expoDefaults");
|
|
21
|
+
const webrtc_1 = require("../adapters/webrtc");
|
|
22
|
+
// <VideoCall> is lazy-required (NOT statically imported) so the native binding it
|
|
23
|
+
// transitively pulls in (react-native-webrtc's RTCView, which runs a top-level
|
|
24
|
+
// NativeEventEmitter at module load) is only loaded when a call is actually rendered.
|
|
25
|
+
// This keeps chat-only consumers — and every non-video test that renders <WebChat>
|
|
26
|
+
// without mocking react-native-webrtc — free of the native module (C15/C16).
|
|
27
|
+
function loadVideoCall() {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
29
|
+
return require("./VideoCall").VideoCall;
|
|
30
|
+
}
|
|
31
|
+
// secureScreen (S10): when the host opts in, lazily flag the surface as
|
|
32
|
+
// screen-capture-protected. expo-screen-capture (preventScreenCaptureAsync) is an
|
|
33
|
+
// OPTIONAL peer; the require is guarded so a chat-only consumer without it — and every
|
|
34
|
+
// unit test — degrades to a no-op rather than crashing. Default off (tenant opts in).
|
|
35
|
+
function applySecureScreen() {
|
|
36
|
+
var _a;
|
|
37
|
+
try {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
|
+
const cap = require("expo-screen-capture");
|
|
40
|
+
(_a = cap.preventScreenCaptureAsync) === null || _a === void 0 ? void 0 : _a.call(cap);
|
|
41
|
+
return () => {
|
|
42
|
+
var _a;
|
|
43
|
+
try {
|
|
44
|
+
(_a = cap.allowScreenCaptureAsync) === null || _a === void 0 ? void 0 : _a.call(cap);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* best-effort release */
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// expo-screen-capture absent — secure-screen is best-effort, so no-op.
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function Surface({ config, title, prechatFields, prechatHeadline: prechatHeadlineProp, prechatSubmitLabel: prechatSubmitLabelProp, onPrechatSubmit, prechatDone, picker, audioAdapter: audioAdapterProp, secureScreen, disabledInput, onClose, }) {
|
|
57
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
58
|
+
// Context contract: theme / dir / language / resolvedConfig are the single source of
|
|
59
|
+
// truth (provider derives them from prop>server>default). The Surface NEVER hardcodes
|
|
60
|
+
// a palette/direction — it reads them here and threads them to Header/MessageList/Composer.
|
|
61
|
+
// audioAdapter (received-voice playback, #23) and setSpeaker (in-call route, #25) are read
|
|
62
|
+
// from context so the live path reaches them; an explicit prop still wins (testability).
|
|
63
|
+
const { messages, send, attach, isStorageReady, language, theme, dir, resolvedConfig, videoCallStarted, startCall, endCall, _video, status, awaitingReply, wasConnected, audioAdapter: audioAdapterCtx, setSpeaker, resumedSession, } = (0, useWebchat_1.useWebchat)();
|
|
64
|
+
const audioAdapter = audioAdapterProp !== null && audioAdapterProp !== void 0 ? audioAdapterProp : audioAdapterCtx;
|
|
65
|
+
const variant = theme === null || theme === void 0 ? void 0 : theme.variant; // experia | digitalGovernment (resolved from interfaceTheme)
|
|
66
|
+
// Connection status for the header's online/offline indicator (web parity).
|
|
67
|
+
const connected = status === "connected" || status === "session-confirmed";
|
|
68
|
+
// typeWidget (Tier C #30): the SDK has a SINGLE Surface (+ optional FAB Launcher)
|
|
69
|
+
// and no DrawerWidget/embedded-shell analog beyond the `embedded` flag below, so
|
|
70
|
+
// any non-"default" value is treated as "default" — accepted, never errors. This
|
|
71
|
+
// key is resolved by DOCUMENTATION, not new behaviour (see CONFIG-PARITY-AUDIT.md).
|
|
72
|
+
// (Intentionally not branched on: there is only one shell to render.)
|
|
73
|
+
// MOBILE LAYOUT: the chat surface ALWAYS fills its parent (flex:1) — it is full
|
|
74
|
+
// screen on a phone, full stop. The web `fullScreenHeight` flag (Tier C #32) is a
|
|
75
|
+
// DESKTOP floating-panel notion (a bounded corner widget capped to part of the
|
|
76
|
+
// viewport) that does NOT translate to mobile, where the chat takes the whole screen
|
|
77
|
+
// (or fills whatever container a host deliberately mounts WebChat into). So we still
|
|
78
|
+
// ACCEPT `fullScreenHeight` in config (web parity, never errors) but intentionally
|
|
79
|
+
// DO NOT apply it here — it must never shrink the mobile view. (The bug: a server
|
|
80
|
+
// config with `fullScreenHeight:false` left the surface capped at maxHeight:"80%".)
|
|
81
|
+
// `embedded` (#31) stays a host opt-in for inline layouts: it still flex-fills, so
|
|
82
|
+
// the bounding is governed by the container the host provides, not a web flag.
|
|
83
|
+
const embedded = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.embedded) === true;
|
|
84
|
+
const containerStyle = embedded
|
|
85
|
+
? { alignSelf: "stretch", flex: 1 }
|
|
86
|
+
: { flex: 1 };
|
|
87
|
+
// hideWhenNotConnected (Tier C #34): hide the surface ONLY after a real
|
|
88
|
+
// established-then-lost drop — i.e. we reached a CONFIRMED session at least once
|
|
89
|
+
// (wasConnected, latched in the provider on "session-confirmed") and are now no
|
|
90
|
+
// longer connected. NEVER on the initial pre-connect / handshake (wasConnected
|
|
91
|
+
// false) — otherwise an open surface mounted before the first connection would
|
|
92
|
+
// vanish and the user could never connect. Default off (opt-in). When the gate is
|
|
93
|
+
// active we render the empty surface shell (keeps the testID stable) — no chrome.
|
|
94
|
+
const hideWhenNotConnected = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.hideWhenNotConnected) === true;
|
|
95
|
+
const hiddenForDisconnect = hideWhenNotConnected && wasConnected && !connected;
|
|
96
|
+
// subTitle / statusIndicator are server-driven config keys the header reads
|
|
97
|
+
// (web parity). They are not in the core Config type yet, so read them off a
|
|
98
|
+
// loosely-typed view of the resolved config rather than widening the type here.
|
|
99
|
+
const headerConfig = (resolvedConfig !== null && resolvedConfig !== void 0 ? resolvedConfig : {});
|
|
100
|
+
// Header title: the host prop wins; otherwise fall back to server config.title
|
|
101
|
+
// (web parity — config.title drives the header when the host omits it).
|
|
102
|
+
const headerTitle = title !== null && title !== void 0 ? title : headerConfig.title;
|
|
103
|
+
// Per-message timestamp toggle (web showMessageDate; default true). Threaded
|
|
104
|
+
// resolvedConfig -> MessageList -> Bubble so the server flag actually reaches it.
|
|
105
|
+
const showMessageDate = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.showMessageDate) !== false;
|
|
106
|
+
// Typing indicator (item 23, web Body/index.tsx:134): shown only when the server
|
|
107
|
+
// flag is on AND we're awaiting a bot reply (the SDK-faithful trigger from the
|
|
108
|
+
// provider). Threaded resolvedConfig + awaitingReply -> MessageList -> TypingIndicator.
|
|
109
|
+
const showTyping = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.displayTypingIndication) === true && awaitingReply;
|
|
110
|
+
// Powered-by badge (item 24, web Body/index.tsx:92): rendered below the message
|
|
111
|
+
// list, gated by showPoweredBy (default ON — only an explicit false hides it).
|
|
112
|
+
const showPoweredBy = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.showPoweredBy) !== false;
|
|
113
|
+
// Agent avatar (item 25, web showAvatar/avatar; default showAvatar true). Threaded
|
|
114
|
+
// to the Header (header avatar + status dot) and each received Bubble (per-message
|
|
115
|
+
// avatar). A graceful fallback applies when no avatar.url is configured.
|
|
116
|
+
const showAvatar = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.showAvatar) !== false;
|
|
117
|
+
const avatar = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.avatar;
|
|
118
|
+
// Custom header close image + launcher images (item 29, web close/launcher images).
|
|
119
|
+
const closeImage = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.closeImage;
|
|
120
|
+
// B21 focus-on-open: move the accessibility focus to the header when the surface
|
|
121
|
+
// mounts so a screen-reader user lands on the chat title, not wherever it was.
|
|
122
|
+
const headerRef = (0, react_1.useRef)(null);
|
|
123
|
+
(0, react_1.useEffect)(() => {
|
|
124
|
+
const node = (0, react_native_1.findNodeHandle)(headerRef.current);
|
|
125
|
+
if (node != null)
|
|
126
|
+
react_native_1.AccessibilityInfo.setAccessibilityFocus(node);
|
|
127
|
+
}, []);
|
|
128
|
+
// secureScreen (S10): apply on mount when the host opted in; release on unmount.
|
|
129
|
+
(0, react_1.useEffect)(() => {
|
|
130
|
+
if (!secureScreen)
|
|
131
|
+
return;
|
|
132
|
+
const release = applySecureScreen();
|
|
133
|
+
return release;
|
|
134
|
+
}, [secureScreen]);
|
|
135
|
+
// B9: don't render/evaluate prechat until hydrated.
|
|
136
|
+
if (!isStorageReady)
|
|
137
|
+
return (0, jsx_runtime_1.jsx)(react_native_safe_area_context_1.SafeAreaView, { testID: "webchat-surface" });
|
|
138
|
+
// hideWhenNotConnected (item 34): after a connect-then-disconnect, collapse the
|
|
139
|
+
// surface to an empty shell (keeps the testID for the harness; renders no chrome).
|
|
140
|
+
if (hiddenForDisconnect)
|
|
141
|
+
return (0, jsx_runtime_1.jsx)(react_native_safe_area_context_1.SafeAreaView, { testID: "webchat-surface" });
|
|
142
|
+
// Attach flow (P2): the composer's attach button opens a media selector with
|
|
143
|
+
// up to three rows (image / document / record-audio). Each picker row runs the
|
|
144
|
+
// injected PickerAdapter method, then routes the picked assets through the
|
|
145
|
+
// provider's attach(). Rows are feature-detected: a method absent on the
|
|
146
|
+
// adapter simply omits its row. The audio row is driven by audioAdapter inside
|
|
147
|
+
// the Composer. Documents are restricted to the allow-listed mime set.
|
|
148
|
+
// Server-config gating (web parity: FooterIcons reads `config.uploadMedia` /
|
|
149
|
+
// `config.uploadAudio`, default true). A flag set to false hides that control
|
|
150
|
+
// even when an adapter is wired. Received-voice PLAYBACK is NOT gated (below).
|
|
151
|
+
//
|
|
152
|
+
// Upload sub-types (web parity, audit #17): the web widget gates image /
|
|
153
|
+
// document / video INDEPENDENTLY. `uploadMedia` is the master gate; each
|
|
154
|
+
// sub-type then has its own flag (uploadImage / uploadDocuments / uploadVideo),
|
|
155
|
+
// all default true. A row shows only when BOTH the master and its sub-flag are
|
|
156
|
+
// not false.
|
|
157
|
+
const uploadMedia = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.uploadMedia) !== false;
|
|
158
|
+
const uploadAudio = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.uploadAudio) !== false;
|
|
159
|
+
const uploadImage = uploadMedia && (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.uploadImage) !== false;
|
|
160
|
+
const uploadDocuments = uploadMedia && (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.uploadDocuments) !== false;
|
|
161
|
+
// uploadVideo (web parity, audit #17): the master uploadMedia gate AND the
|
|
162
|
+
// per-type uploadVideo flag must both be enabled. The video row shows only when
|
|
163
|
+
// the picker exposes a `pickVideos` capability (feature-detected below) so a
|
|
164
|
+
// picker without it stays hidden rather than faking a non-functional row.
|
|
165
|
+
const uploadVideo = uploadMedia && (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.uploadVideo) !== false;
|
|
166
|
+
// Web parity (handleSetFileValue): picking only STAGES the assets — the Composer
|
|
167
|
+
// previews them and SENDS on the Send tap (via onAttach), NOT immediately on pick.
|
|
168
|
+
// So these just RETURN the picked Asset[] for the Composer to stage; the actual
|
|
169
|
+
// attach()/emit happens when the user taps Send.
|
|
170
|
+
const onPickVideos = uploadVideo && (picker === null || picker === void 0 ? void 0 : picker.pickVideos)
|
|
171
|
+
? () => picker.pickVideos({ allowsMultiple: true })
|
|
172
|
+
: undefined;
|
|
173
|
+
const onPickImages = uploadImage && (picker === null || picker === void 0 ? void 0 : picker.pickImages)
|
|
174
|
+
? () => picker.pickImages({ allowsMultiple: true })
|
|
175
|
+
: undefined;
|
|
176
|
+
const onPickDocuments = uploadDocuments && (picker === null || picker === void 0 ? void 0 : picker.pickDocuments)
|
|
177
|
+
? // No `type` filter: the allow-list contains PARAMETRIZED mimes (e.g.
|
|
178
|
+
// "audio/ogg;codecs=opus") that iOS can't map to a UTI, which wedges
|
|
179
|
+
// UIDocumentPickerViewController and HANGS getDocumentAsync (it never
|
|
180
|
+
// presents or resolves). Present a universal picker; the picked file is
|
|
181
|
+
// still enforced against the allow-list post-pick by validateSelection.
|
|
182
|
+
() => picker.pickDocuments()
|
|
183
|
+
: undefined;
|
|
184
|
+
// Voice RECORDING is gated by config.uploadAudio; received-voice PLAYBACK
|
|
185
|
+
// (MessageList) keeps the full adapter so the agent's notes still play.
|
|
186
|
+
const recordAudioAdapter = uploadAudio ? audioAdapter : undefined;
|
|
187
|
+
// When prechat is enabled and there is no session yet, render <PrechatForm/>;
|
|
188
|
+
// its submit feeds prechatValues into the greet flow. Otherwise render the thread.
|
|
189
|
+
//
|
|
190
|
+
// Gate (audit #20): the form shows when it is NOT done AND either (a) the server
|
|
191
|
+
// config explicitly enables it (resolvedConfig.prechatForm.enabled) OR (b) the
|
|
192
|
+
// existing host-prop path supplied a non-empty prechatFields array. (a) lets a
|
|
193
|
+
// server-driven prechat config turn the form on without the host hand-passing
|
|
194
|
+
// fields; (b) preserves the current host-only behaviour.
|
|
195
|
+
const prechatConfigEnabled = ((_a = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.prechatForm) === null || _a === void 0 ? void 0 : _a.enabled) === true;
|
|
196
|
+
const hasHostFields = Array.isArray(prechatFields) && prechatFields.length > 0;
|
|
197
|
+
// Prefer host-supplied fields; otherwise use the server-config fields (so a
|
|
198
|
+
// server-enabled form has something to render).
|
|
199
|
+
const effectivePrechatFields = hasHostFields
|
|
200
|
+
? prechatFields
|
|
201
|
+
: (_b = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.prechatForm) === null || _b === void 0 ? void 0 : _b.fields;
|
|
202
|
+
// Web parity (ChatContainer `!sessionId` gate): show the form only for a NEW session.
|
|
203
|
+
// A RESUMED session (returning user, restored session id) continues the conversation
|
|
204
|
+
// without re-asking. `prechatDone` is the one-shot per-mount submit latch.
|
|
205
|
+
const showPrechat = !prechatDone &&
|
|
206
|
+
!resumedSession &&
|
|
207
|
+
(prechatConfigEnabled || hasHostFields) &&
|
|
208
|
+
Array.isArray(effectivePrechatFields) &&
|
|
209
|
+
effectivePrechatFields.length > 0;
|
|
210
|
+
// Localized headline / submit label come from the server prechat config (web
|
|
211
|
+
// parity: PrechatForm.tsx headline, PrechatSubmitFooter.tsx submitLabel). Host
|
|
212
|
+
// props still win when supplied (mirrors the fields precedence above).
|
|
213
|
+
const prechatHeadline = prechatHeadlineProp !== null && prechatHeadlineProp !== void 0 ? prechatHeadlineProp : (_c = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.prechatForm) === null || _c === void 0 ? void 0 : _c.headline;
|
|
214
|
+
const prechatSubmitLabel = prechatSubmitLabelProp !== null && prechatSubmitLabelProp !== void 0 ? prechatSubmitLabelProp : (_d = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.prechatForm) === null || _d === void 0 ? void 0 : _d.submitLabel;
|
|
215
|
+
// P3: video is opt-out (web parity: defualtConfig.makeVideoCall === true). Gate on the
|
|
216
|
+
// RESOLVED config (prop > server > default true) so BOTH a host prop AND a server
|
|
217
|
+
// config can disable it with makeVideoCall:false — matching web, which hides the
|
|
218
|
+
// button on a server-driven flag. Default-on flows from DEFAULT_CONFIG.makeVideoCall.
|
|
219
|
+
const videoEnabled = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.makeVideoCall) !== false;
|
|
220
|
+
// #disabledInput: the composer is hard-disabled when the host prop OR the resolved
|
|
221
|
+
// config asks for it (server can lock input per tenant/conversation state).
|
|
222
|
+
const inputDisabled = disabledInput !== null && disabledInput !== void 0 ? disabledInput : (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.disabledInput) === true;
|
|
223
|
+
// Close button (BUG 4): shown unless the config explicitly disables it. Closing the
|
|
224
|
+
// surface also tears down any active call so we never leave a dangling call behind.
|
|
225
|
+
const showClose = (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.showCloseButton) !== false;
|
|
226
|
+
const onHeaderClose = () => {
|
|
227
|
+
try {
|
|
228
|
+
endCall === null || endCall === void 0 ? void 0 : endCall();
|
|
229
|
+
}
|
|
230
|
+
catch { /* no active call */ }
|
|
231
|
+
onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
232
|
+
};
|
|
233
|
+
// P3: once a call is ACTIVE, render <VideoCall> ABOVE the thread — gated on
|
|
234
|
+
// videoCallStarted only, NOT videoEnabled, so an auto-joined call (autoJoinVideoCall)
|
|
235
|
+
// always shows even under a contradictory config (makeVideoCall:false + auto-join).
|
|
236
|
+
// It renders as a view over the PROVIDER's single VideoCallClient (createClient returns
|
|
237
|
+
// it; the provider owns the audio session). start()/stop() stay no-ops here because the
|
|
238
|
+
// provider already brackets the call's audio, but setSpeaker DELEGATES to the provider's
|
|
239
|
+
// real AudioRoute (audit #25) so the in-call speaker toggle reaches the live audio route
|
|
240
|
+
// rather than a stub. onEnd routes back through endCall so leave/inbound-hangup flips the flag.
|
|
241
|
+
if (videoCallStarted && (_video === null || _video === void 0 ? void 0 : _video.socket)) {
|
|
242
|
+
const VideoCall = loadVideoCall();
|
|
243
|
+
return ((0, jsx_runtime_1.jsx)(react_native_safe_area_context_1.SafeAreaView, { testID: "webchat-surface", style: containerStyle, children: (0, jsx_runtime_1.jsx)(VideoCall, { socket: _video.socket, getSessionId: _video.getSessionId, onEnd: endCall, createClient: () => _video.client, audioRoute: { start() { }, stop() { }, setSpeaker: (on) => setSpeaker(on) }, dir: dir, theme: theme, language: language,
|
|
244
|
+
// Agent identity for the "connecting" placeholder (before the remote joins):
|
|
245
|
+
// the header title (agent/tenant name) + the configured avatar, gated by showAvatar.
|
|
246
|
+
agentName: headerTitle !== null && headerTitle !== void 0 ? headerTitle : avatar === null || avatar === void 0 ? void 0 : avatar.name, agentAvatarUrl: showAvatar ? avatar === null || avatar === void 0 ? void 0 : avatar.url : undefined }) }));
|
|
247
|
+
}
|
|
248
|
+
// Surface sits on the chat background (web bgChatColor / theme.chat.bg) so the
|
|
249
|
+
// thread area matches the message list; falls back to the surface colour.
|
|
250
|
+
const surfaceBg = (_f = (_e = theme === null || theme === void 0 ? void 0 : theme.chat) === null || _e === void 0 ? void 0 : _e.bg) !== null && _f !== void 0 ? _f : (_g = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _g === void 0 ? void 0 : _g.surface;
|
|
251
|
+
return (
|
|
252
|
+
// edges EXCLUDE "top": the Header owns the top safe-area inset itself
|
|
253
|
+
// (paddingTop: insets.top) so its brand color fills the status-bar strip. If
|
|
254
|
+
// this SafeAreaView also padded the top, the inset would be applied TWICE
|
|
255
|
+
// (surface + header) AND the strip would show the chat bg, not the header color.
|
|
256
|
+
(0, jsx_runtime_1.jsxs)(react_native_safe_area_context_1.SafeAreaView, { testID: "webchat-surface", edges: ["left", "right", "bottom"], style: { ...containerStyle, backgroundColor: surfaceBg }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { ref: headerRef, accessible: true, accessibilityRole: "header", children: (0, jsx_runtime_1.jsx)(Header_1.Header, { variant: variant, dir: dir, theme: theme, title: headerTitle, subtitle: headerConfig.subTitle, language: language, onClose: onHeaderClose, showClose: showClose, connected: connected, statusIndicator: headerConfig.statusIndicator !== false,
|
|
257
|
+
// Connecting banner (web parity): show the configured connectingText
|
|
258
|
+
// while not yet connected (the Header hides it once connected).
|
|
259
|
+
connectingText: headerConfig.connectingText,
|
|
260
|
+
// Agent avatar + status dot (item 25) and custom close image (item 29).
|
|
261
|
+
avatar: avatar, showAvatar: showAvatar, closeImage: closeImage,
|
|
262
|
+
// Video-call button lives INSIDE the header now (web parity). Wired only
|
|
263
|
+
// when video is enabled and the thread (not prechat) is showing; pressing
|
|
264
|
+
// it starts the call exactly like the old floating button did.
|
|
265
|
+
onStartCall: videoEnabled && !showPrechat ? startCall : undefined }) }), showPrechat ? ((0, jsx_runtime_1.jsx)(PrechatForm_1.PrechatForm, { fields: effectivePrechatFields, onSubmit: onPrechatSubmit, dir: dir, theme: theme, language: language, headline: prechatHeadline, submitLabel: prechatSubmitLabel })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(MessageList_1.MessageList, { messages: messages, dir: dir, theme: theme, audioAdapter: audioAdapter, showMessageDate: showMessageDate, showTyping: showTyping, showAvatar: showAvatar, avatar: avatar }), showPoweredBy ? (0, jsx_runtime_1.jsx)(PoweredBy_1.PoweredBy, { theme: theme }) : null, (0, jsx_runtime_1.jsx)(Composer_1.Composer, { onSend: send, onPickImages: onPickImages, onPickDocuments: onPickDocuments, onPickVideos: onPickVideos, onAttach: attach, audioAdapter: recordAudioAdapter, disabledInput: inputDisabled, dir: dir, theme: theme, language: language,
|
|
266
|
+
// web config.emoji gates the emoji affordance (default false per web).
|
|
267
|
+
emojiEnabled: (resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.emoji) === true,
|
|
268
|
+
// web config.inputTextFieldHint overrides the composer placeholder.
|
|
269
|
+
placeholder: headerConfig.inputTextFieldHint })] }))] }));
|
|
270
|
+
}
|
|
271
|
+
// Bridges the imperative handle into provider state (clearHistory/resetSession/unread).
|
|
272
|
+
function ImperativeBridge({ handleRef, setOpen }) {
|
|
273
|
+
const { send, clearHistory, resetSession, unread } = (0, useWebchat_1.useWebchat)();
|
|
274
|
+
(0, react_1.useImperativeHandle)(handleRef, () => ({
|
|
275
|
+
open: () => setOpen(true),
|
|
276
|
+
close: () => setOpen(false),
|
|
277
|
+
sendMessage: (t) => send(t),
|
|
278
|
+
getUnreadCount: () => unread, // real value (B19/B20)
|
|
279
|
+
clearHistory, // real purge (B19)
|
|
280
|
+
resetSession, // real reset (B19)
|
|
281
|
+
}), [send, clearHistory, resetSession, unread]);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
// Default FAB launcher. Reads theme/dir from context (NOT props) so the brand color and
|
|
285
|
+
// logical-end placement follow the resolved config: the FAB anchors to the logical end
|
|
286
|
+
// (left under RTL) instead of a hardcoded 'right'.
|
|
287
|
+
function Launcher({ renderLauncher, setOpen }) {
|
|
288
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
289
|
+
const { theme, dir, language, resolvedConfig, status, wasConnected } = (0, useWebchat_1.useWebchat)();
|
|
290
|
+
const d = (0, dir_1.dirStyles)(dir);
|
|
291
|
+
const t = (0, i18n_1.makeT)(language);
|
|
292
|
+
// showButtonChat (item 27, web parity): hide the launcher FAB entirely when the
|
|
293
|
+
// server config explicitly disables it. Only an explicit `false` hides it (default on).
|
|
294
|
+
if ((resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.showButtonChat) === false)
|
|
295
|
+
return null;
|
|
296
|
+
// hideWhenNotConnected (item 34): hide the launcher ONLY after a real
|
|
297
|
+
// connect-then-disconnect (reached "up" once, then dropped). NEVER on the initial
|
|
298
|
+
// pre-connect (wasConnected false) — the user must still be able to tap to connect.
|
|
299
|
+
// Default off (opt-in). Mirrors the Surface gate so launcher + surface hide together.
|
|
300
|
+
const connected = status === "connected" || status === "session-confirmed";
|
|
301
|
+
if ((resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.hideWhenNotConnected) === true && wasConnected && !connected)
|
|
302
|
+
return null;
|
|
303
|
+
if (renderLauncher)
|
|
304
|
+
return renderLauncher({ open: () => setOpen(true) });
|
|
305
|
+
// Launcher dimensions/colors come from the launcher token group (web
|
|
306
|
+
// wedgitSectionStyle). Defaults match the historical hardcoded look (56×56,
|
|
307
|
+
// r28, primary bg, white icon) so an unset config keeps today's launcher.
|
|
308
|
+
const launcher = (_a = theme === null || theme === void 0 ? void 0 : theme.launcher) !== null && _a !== void 0 ? _a : {};
|
|
309
|
+
const buttonBg = (_b = launcher.buttonBg) !== null && _b !== void 0 ? _b : theme.colors.primary;
|
|
310
|
+
const buttonColor = (_c = launcher.buttonColor) !== null && _c !== void 0 ? _c : "#fff";
|
|
311
|
+
// Dimensions are typed number|string on the token (web allows e.g. "56px");
|
|
312
|
+
// RN's DimensionValue is stricter, so cast at the style boundary.
|
|
313
|
+
const buttonWidth = ((_d = launcher.buttonWidth) !== null && _d !== void 0 ? _d : 56);
|
|
314
|
+
const buttonHeight = ((_e = launcher.buttonHeight) !== null && _e !== void 0 ? _e : 56);
|
|
315
|
+
const buttonBorderRadius = ((_f = launcher.buttonBorderRadius) !== null && _f !== void 0 ? _f : 28);
|
|
316
|
+
// Custom launcher image (item 29, web `openLauncherImage`/`closeLauncherImage`):
|
|
317
|
+
// when an open-launcher image url is configured, render it as an <Image> instead
|
|
318
|
+
// of the built-in chat Icon (the closed/minimized variant maps to closeLauncherImage).
|
|
319
|
+
// Closed surface => the launcher is the "open" affordance, so prefer openLauncherImage;
|
|
320
|
+
// fall back to closeLauncherImage, then the Icon.
|
|
321
|
+
const launcherImageUrl = (_h = (_g = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.openLauncherImage) === null || _g === void 0 ? void 0 : _g.url) !== null && _h !== void 0 ? _h : (_j = resolvedConfig === null || resolvedConfig === void 0 ? void 0 : resolvedConfig.closeLauncherImage) === null || _j === void 0 ? void 0 : _j.url;
|
|
322
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "webchat-launcher", accessibilityRole: "button", accessibilityLabel: t("open_chat"), onPress: () => setOpen(true), style: {
|
|
323
|
+
position: "absolute",
|
|
324
|
+
bottom: 24,
|
|
325
|
+
right: d.isRtl ? undefined : 24,
|
|
326
|
+
left: d.isRtl ? 24 : undefined,
|
|
327
|
+
backgroundColor: launcherImageUrl ? undefined : buttonBg,
|
|
328
|
+
borderRadius: buttonBorderRadius,
|
|
329
|
+
width: buttonWidth,
|
|
330
|
+
height: buttonHeight,
|
|
331
|
+
alignItems: "center",
|
|
332
|
+
justifyContent: "center",
|
|
333
|
+
overflow: "hidden",
|
|
334
|
+
}, children: launcherImageUrl ? ((0, jsx_runtime_1.jsx)(react_native_1.Image, { testID: "webchat-launcher-image", accessibilityRole: "image", source: { uri: launcherImageUrl }, resizeMode: "cover", style: { width: buttonWidth, height: buttonHeight, borderRadius: buttonBorderRadius } })) : ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chat", color: buttonColor, size: 26 })) }));
|
|
335
|
+
}
|
|
336
|
+
exports.WebChat = (0, react_1.forwardRef)(function WebChat(props, ref) {
|
|
337
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
338
|
+
const { config, renderLauncher, onOpen, onClose, title, prechatFields, prechatHeadline, prechatSubmitLabel, } = props;
|
|
339
|
+
const [open, setOpenState] = (0, react_1.useState)((_a = props.open) !== null && _a !== void 0 ? _a : false);
|
|
340
|
+
const [prechatDone, setPrechatDone] = (0, react_1.useState)(false);
|
|
341
|
+
const [prechatValues, setPrechatValues] = (0, react_1.useState)(props.prechatValues);
|
|
342
|
+
const setOpen = (v) => {
|
|
343
|
+
setOpenState(v);
|
|
344
|
+
v ? onOpen === null || onOpen === void 0 ? void 0 : onOpen() : onClose === null || onClose === void 0 ? void 0 : onClose();
|
|
345
|
+
};
|
|
346
|
+
const onPrechatSubmit = (values) => {
|
|
347
|
+
setPrechatValues(values);
|
|
348
|
+
setPrechatDone(true);
|
|
349
|
+
};
|
|
350
|
+
// BATTERIES-INCLUDED defaults (zero-adapter path): build each default adapter
|
|
351
|
+
// ONCE per mount and let an injected prop (or the `_`-prefixed test seam) ALWAYS
|
|
352
|
+
// win (`prop ?? default`). Each create*() lazily requires its Expo peer in
|
|
353
|
+
// try/catch and returns `undefined` when absent — so a host without the peer
|
|
354
|
+
// degrades exactly like the absent-prop case (attach row hidden, recorder
|
|
355
|
+
// suppressed, throwing-reader fallback). Memoized so the factories aren't rebuilt
|
|
356
|
+
// every render (and so the peer require runs at most once per mount).
|
|
357
|
+
const defaultPicker = (0, react_1.useMemo)(() => (0, expoDefaults_1.createExpoPicker)(), []);
|
|
358
|
+
const defaultAudioAdapter = (0, react_1.useMemo)(() => (0, expoDefaults_1.createExpoAudioAdapter)(), []);
|
|
359
|
+
const defaultReadBase64 = (0, react_1.useMemo)(() => (0, expoDefaults_1.createExpoReadBase64)(), []);
|
|
360
|
+
// webrtcFactory already defaults to reactNativeWebRTCFactory() lazily inside the
|
|
361
|
+
// provider's startCall; we thread the memoized real factory here too so the
|
|
362
|
+
// default is explicit at the public seam (a host/test prop still wins).
|
|
363
|
+
const defaultWebrtcFactory = (0, react_1.useMemo)(() => (0, webrtc_1.reactNativeWebRTCFactory)(), []);
|
|
364
|
+
const picker = (_c = (_b = props._picker) !== null && _b !== void 0 ? _b : props.picker) !== null && _c !== void 0 ? _c : defaultPicker;
|
|
365
|
+
const audioAdapter = (_e = (_d = props._audioAdapter) !== null && _d !== void 0 ? _d : props.audioAdapter) !== null && _e !== void 0 ? _e : defaultAudioAdapter;
|
|
366
|
+
const readBase64 = (_g = (_f = props._readBase64) !== null && _f !== void 0 ? _f : props.readBase64) !== null && _g !== void 0 ? _g : defaultReadBase64;
|
|
367
|
+
const webrtcFactory = (_j = (_h = props._webrtcFactory) !== null && _h !== void 0 ? _h : props.webrtcFactory) !== null && _j !== void 0 ? _j : defaultWebrtcFactory;
|
|
368
|
+
// Provider always mounts so the socket can stay connected while closed (S7) and the
|
|
369
|
+
// imperative handle/callbacks remain live. Surface toggles on open. theme/dir/language
|
|
370
|
+
// are owned by the provider (context contract) — WebChat no longer carries hardcoded
|
|
371
|
+
// theme/dir defaults; consumers read them from useWebchat().
|
|
372
|
+
return ((0, jsx_runtime_1.jsxs)(WebchatProvider_1.WebchatProvider, { config: config, socketFactory: (_k = props._socketFactory) !== null && _k !== void 0 ? _k : ((cfg) => (0, socketFactory_1.createSocket)(cfg.connectionUrl, cfg.channelId, cfg.debug)), store: props._store,
|
|
373
|
+
// config-fetch seam: `_fetch` (tests) or a host-provided `fetchImpl`; the provider
|
|
374
|
+
// falls back to the ambient global fetch when neither is set.
|
|
375
|
+
fetchImpl: (_l = props._fetch) !== null && _l !== void 0 ? _l : props.fetchImpl, onMessageReceived: props.onMessageReceived, onConnected: props.onConnected, onDisconnected: props.onDisconnected, onError: props.onError, onUnreadChange: props.onUnreadChange, prechatValues: prechatValues,
|
|
376
|
+
// Host-prop prechat fields present — lets the provider know a form will collect
|
|
377
|
+
// data (so it defers the greet until submit), alongside server prechatForm.enabled.
|
|
378
|
+
hasPrechatFields: Array.isArray(prechatFields) && prechatFields.length > 0,
|
|
379
|
+
// Web parity (!sessionId gate): a session reset re-presents the prechat form.
|
|
380
|
+
// resetSession() fires this so the one-time prechatDone latch is cleared and the
|
|
381
|
+
// next (fresh) session shows the form again instead of dropping into an empty thread.
|
|
382
|
+
onSessionReset: () => {
|
|
383
|
+
setPrechatDone(false);
|
|
384
|
+
setPrechatValues(undefined);
|
|
385
|
+
}, readBase64: readBase64, mediaLimits: props.mediaLimits, webrtcFactory: webrtcFactory, audioRoute: (_m = props._audioRoute) !== null && _m !== void 0 ? _m : props.audioRoute, createVideoClient: props._createVideoClient, audioAdapter: audioAdapter, children: [(0, jsx_runtime_1.jsx)(ImperativeBridge, { handleRef: ref, setOpen: setOpen }), open ? ((0, jsx_runtime_1.jsx)(Surface, { config: config, title: title, prechatFields: prechatFields, prechatHeadline: prechatHeadline, prechatSubmitLabel: prechatSubmitLabel, onPrechatSubmit: onPrechatSubmit, prechatDone: prechatDone, picker: picker, audioAdapter: audioAdapter, secureScreen: props.secureScreen, disabledInput: props.disabledInput, onClose: () => setOpen(false) })) : ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 1 }, pointerEvents: "box-none", children: (0, jsx_runtime_1.jsx)(Launcher, { renderLauncher: renderLauncher, setOpen: setOpen }) }))] }));
|
|
386
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function openLink(href: string): Promise<any>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openLink = openLink;
|
|
4
|
+
// src/ui/openLink.ts — the ONE shared scheme gate (reused by Phase 2 attachment-open too)
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
async function openLink(href) {
|
|
7
|
+
if (/^https:\/\//.test(href))
|
|
8
|
+
return react_native_1.Linking.openURL(href); // safe: open
|
|
9
|
+
if (/^(mailto:|tel:)/.test(href)) { // confirm first
|
|
10
|
+
return new Promise((resolve) => react_native_1.Alert.alert("Open link?", href, [
|
|
11
|
+
{ text: "Cancel", style: "cancel", onPress: () => resolve() },
|
|
12
|
+
{ text: "Open", onPress: () => { react_native_1.Linking.openURL(href); resolve(); } },
|
|
13
|
+
]));
|
|
14
|
+
}
|
|
15
|
+
return; // reject ftp:/file:/intent:/custom — bot text is untrusted (B13)
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@experiaapp/webchat-react-native",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Experia App live-chat SDK for React Native — drop a real-time support channel (chat, media & video calls) into your app so your customers can reach you with ease.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"react-native": "lib/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib",
|
|
11
|
+
"app.plugin.js"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copy-assets.js",
|
|
22
|
+
"prepack": "npm run build",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"test:core": "jest src/core src/theme",
|
|
25
|
+
"setup:example": "npm run build && npm pack && node -e \"const v=require('./package.json').version;require('fs').copyFileSync('experiaapp-webchat-react-native-'+v+'.tgz','example/webchat-sdk.tgz')\" && cd example && npm install ./webchat-sdk.tgz",
|
|
26
|
+
"smoke:live": "npm run build && node scripts/live-smoke.js",
|
|
27
|
+
"sync:example": "npm run build && node scripts/sync-example.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"expo-audio": "~56.0.12",
|
|
31
|
+
"expo-font": "~56.0.5",
|
|
32
|
+
"expo-video": "~56.1.4",
|
|
33
|
+
"i18next": "^26.3.1",
|
|
34
|
+
"socket.io-client": "2.5.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@react-native-async-storage/async-storage": ">=1.21",
|
|
38
|
+
"react": ">=18",
|
|
39
|
+
"react-native": ">=0.74",
|
|
40
|
+
"react-native-incall-manager": ">=4",
|
|
41
|
+
"react-native-localize": ">=3",
|
|
42
|
+
"react-native-safe-area-context": ">=4",
|
|
43
|
+
"react-native-webrtc": ">=124",
|
|
44
|
+
"react-native-svg": ">=13",
|
|
45
|
+
"@react-native-community/netinfo": ">=11",
|
|
46
|
+
"expo-image-picker": ">=14",
|
|
47
|
+
"expo-document-picker": ">=11",
|
|
48
|
+
"expo-file-system": ">=15"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react-native-webrtc": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"react-native-incall-manager": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"react-native-svg": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"@react-native-community/netinfo": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"expo-image-picker": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"expo-document-picker": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"expo-file-system": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"//peerNote": "The expo-* peers above are OPTIONAL and auto-wired by <WebChat> when installed. In a bare React Native app (no Expo prebuild) they require the expo-modules-core runtime: run `npx install-expo-modules` once so the autolinked native side exists. Expo-managed / dev-client apps already have it. Pin SDK-correct versions with `npx expo install`.",
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@react-native-async-storage/async-storage": "^3.1.1",
|
|
76
|
+
"@react-native-community/netinfo": "^12.0.1",
|
|
77
|
+
"@react-native/babel-preset": "^0.85.3",
|
|
78
|
+
"@react-native/jest-preset": "^0.85.3",
|
|
79
|
+
"@testing-library/react-native": "^13.3.3",
|
|
80
|
+
"@types/jest": "^29.5.12",
|
|
81
|
+
"@types/node": "^20.11.30",
|
|
82
|
+
"@types/react": "~19.0.0",
|
|
83
|
+
"jest": "^29.7.0",
|
|
84
|
+
"react": "^19.2.3",
|
|
85
|
+
"react-native": "^0.85.3",
|
|
86
|
+
"react-native-incall-manager": "^4.2.1",
|
|
87
|
+
"react-native-localize": "^3.7.0",
|
|
88
|
+
"react-native-safe-area-context": "^5.8.0",
|
|
89
|
+
"react-native-webrtc": "^124.0.7",
|
|
90
|
+
"react-test-renderer": "^19.2.3",
|
|
91
|
+
"ts-jest": "^29.1.2",
|
|
92
|
+
"typescript": "^5.4.5"
|
|
93
|
+
}
|
|
94
|
+
}
|