@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,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWebchatClient = createWebchatClient;
4
+ const types_1 = require("./types");
5
+ const logger_1 = require("./logger");
6
+ function createWebchatClient(opts) {
7
+ var _a, _b;
8
+ const listeners = {};
9
+ let socket = null;
10
+ let sessionTimer = null;
11
+ const timeoutMs = (_a = opts.sessionTimeoutMs) !== null && _a !== void 0 ? _a : 10000;
12
+ const maxRetries = (_b = opts.maxSessionRetries) !== null && _b !== void 0 ? _b : 3;
13
+ // B7 retry bookkeeping (per connect lifecycle).
14
+ let retryCount = 0;
15
+ // C4/de-dupe: 'connected' fires once per connected period — not on every
16
+ // socket 'connect' event (socket.io can re-fire 'connect' within a single
17
+ // connection). Reset on 'disconnect' so a genuine reconnect re-emits it.
18
+ let connectedEmitted = false;
19
+ const emit = (event, data) => { var _a; return ((_a = listeners[event]) !== null && _a !== void 0 ? _a : []).forEach((l) => l(data)); };
20
+ function clearSessionTimer() {
21
+ if (sessionTimer) {
22
+ clearTimeout(sessionTimer);
23
+ sessionTimer = null;
24
+ }
25
+ }
26
+ // C2: registered ONCE when the socket is created (not per reconnect)
27
+ function wireSessionConfirm(s) {
28
+ s.on("session_confirm", (status) => {
29
+ var _a, _b;
30
+ clearSessionTimer();
31
+ retryCount = 0; // recovered — reset the backoff ladder
32
+ // The LIVE backend sends `session_confirm` as a BARE STRING that IS the session
33
+ // id (web parity: redux/auth/actions.ts passes the payload straight through as
34
+ // `newSessionId`). Reading `status.session_id` off a string yields undefined, so
35
+ // the id was lost — user_uttered then went out with session_id:"" and every
36
+ // reconnect re-requested a brand-new session. Extract robustly: a string is the
37
+ // id; otherwise fall back to the object forms ({session_id}/{sessionId}) older
38
+ // servers/tests use.
39
+ const id = typeof status === "string"
40
+ ? status
41
+ : (_b = (_a = status === null || status === void 0 ? void 0 : status.session_id) !== null && _a !== void 0 ? _a : status === null || status === void 0 ? void 0 : status.sessionId) !== null && _b !== void 0 ? _b : "";
42
+ emit("status", "session-confirmed");
43
+ emit("session", id);
44
+ });
45
+ }
46
+ // B7: exponential backoff (capped). delay = timeoutMs * 2^retryCount.
47
+ function scheduleSessionTimeout() {
48
+ clearSessionTimer();
49
+ const delay = timeoutMs * Math.pow(2, retryCount);
50
+ sessionTimer = setTimeout(() => {
51
+ sessionTimer = null;
52
+ if (retryCount < maxRetries) {
53
+ retryCount += 1;
54
+ logger_1.logger.warn(`session_request retry ${retryCount}/${maxRetries}`);
55
+ // re-emit session_request (recovers when a later session_confirm lands)
56
+ requestSession();
57
+ }
58
+ else {
59
+ logger_1.logger.warn("session_request gave up after max retries");
60
+ emit("status", "session-failed");
61
+ emit("error", new types_1.WebChatError("session-timeout", "no session_confirm", true));
62
+ }
63
+ }, delay);
64
+ }
65
+ // B8: resume by stored id (provided by the host/persistence layer)
66
+ function requestSession() {
67
+ var _a, _b;
68
+ if (!socket)
69
+ return;
70
+ socket.emit("session_request", { session_id: (_b = (_a = opts.getStoredSessionId) === null || _a === void 0 ? void 0 : _a.call(opts)) !== null && _b !== void 0 ? _b : "" });
71
+ emit("status", "session-pending");
72
+ scheduleSessionTimeout();
73
+ }
74
+ return {
75
+ on(event, cb) { var _a; ((_a = listeners[event]) !== null && _a !== void 0 ? _a : (listeners[event] = [])).push(cb); },
76
+ connect() {
77
+ var _a;
78
+ retryCount = 0;
79
+ connectedEmitted = false;
80
+ socket = opts.socketFactory();
81
+ wireSessionConfirm(socket); // once per socket instance (C2)
82
+ socket.on("bot_uttered", (data) => emit("message", data));
83
+ // Inbound feedback confirmation (web parity): the server reflects a like/dislike
84
+ // toggle back on "activityAck" with { messageKey, type }. Re-emit it so the
85
+ // provider can map type -> userReaction and dispatch a REACT.
86
+ socket.on("activityAck", (data) => emit("activityAck", data));
87
+ socket.on("connect", () => {
88
+ // de-dupe: one 'connected' per session lifecycle, not per reconnect
89
+ // 'connect' (socket.io auto-reconnect re-fires this repeatedly).
90
+ if (!connectedEmitted) {
91
+ connectedEmitted = true;
92
+ emit("status", "connected");
93
+ }
94
+ requestSession();
95
+ });
96
+ socket.on("disconnect", () => {
97
+ clearSessionTimer();
98
+ // Reset the de-dupe latch so the NEXT genuine (re)connect re-emits
99
+ // 'connected'. Consumers (provider B10 queue flush) rely on a fresh
100
+ // 'connected' per connected period; the latch only suppresses socket.io
101
+ // firing 'connect' repeatedly within a single uninterrupted connection.
102
+ connectedEmitted = false;
103
+ emit("status", "reconnecting");
104
+ });
105
+ // C4/#14/#15: surface transport-level failures as a typed connection error
106
+ // plus a 'disconnected' status (distinct from 'reconnecting').
107
+ const onConnError = (err) => {
108
+ emit("status", "disconnected");
109
+ emit("error", new types_1.WebChatError("connection", "socket connection error", true, err));
110
+ };
111
+ socket.on("connect_error", onConnError);
112
+ socket.on("error", onConnError);
113
+ (_a = socket.connect) === null || _a === void 0 ? void 0 : _a.call(socket);
114
+ },
115
+ sendMessage(payload) {
116
+ socket === null || socket === void 0 ? void 0 : socket.emit("user_uttered", payload);
117
+ },
118
+ /**
119
+ * Emit a feedback activity (web parity): `socket.emit("activity", { messageKey, type })`.
120
+ * `type` is the backend verb the provider computes from the toggle —
121
+ * "like" | "unlike" | "dislike" | "undislike". The matching inbound reflection
122
+ * arrives on "activityAck" (re-emitted as the client's "activityAck" event).
123
+ */
124
+ emitActivity(payload) {
125
+ socket === null || socket === void 0 ? void 0 : socket.emit("activity", payload);
126
+ },
127
+ disconnect() {
128
+ clearSessionTimer();
129
+ socket === null || socket === void 0 ? void 0 : socket.disconnect();
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,42 @@
1
+ import { Config } from "./types";
2
+ /** Minimal fetch surface — matches the global `fetch` (and node/undici). */
3
+ export type FetchLike = (input: string, init?: {
4
+ method?: string;
5
+ headers?: Record<string, string>;
6
+ }) => Promise<{
7
+ ok: boolean;
8
+ status: number;
9
+ json(): Promise<unknown>;
10
+ }>;
11
+ /** Server config never carries the connection trio — that comes from the host. */
12
+ export type ServerConfig = Partial<Omit<Config, "channelId" | "configUrl" | "connectionUrl">>;
13
+ /**
14
+ * SDK defaults — the lowest-precedence layer. Connection fields are intentionally
15
+ * absent (the host must supply them); behaviour flags default to the web widget's
16
+ * baseline.
17
+ */
18
+ export declare const DEFAULT_CONFIG: ServerConfig;
19
+ /**
20
+ * Fetch the server-side config for a channel (web parity).
21
+ *
22
+ * Hits `https://${configUrl}/api/webchat/options?channelID=${channelId}` and
23
+ * defensively double-parses the body (the web ships a stringified JSON body, but
24
+ * a plain object is tolerated too). The payload is then mapped onto the flat
25
+ * {@link ServerConfig} shape via {@link mapPayload}.
26
+ *
27
+ * @param configUrl config host (scheme optional — `https://` is prepended).
28
+ * @param channelId tenant channel id (sent as the `channelID` query param).
29
+ * @param fetchImpl injected fetch (defaults to the ambient global).
30
+ * @throws WebChatError('config-fetch', …, recoverable=true) on network/non-2xx/parse
31
+ * failure — recoverable so the host can retry.
32
+ */
33
+ export declare function fetchConfig(configUrl: string, channelId: string, fetchImpl?: FetchLike): Promise<ServerConfig>;
34
+ /**
35
+ * Merge config layers with precedence prop > server > default (S5/S8).
36
+ *
37
+ * `publicStyle` is merged shallowly across layers (so a server can set a brand
38
+ * colour and the host can override just the font without dropping the colour).
39
+ * The connection trio (channelId/configUrl/connectionUrl) comes only from the
40
+ * host prop — it is required there and not overridable by the server.
41
+ */
42
+ export declare function mergeConfig(propConfig: Config, serverConfig?: ServerConfig | null): Config;
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ // src/core/configClient.ts
3
+ //
4
+ // Config fetch + merge (audit #2 / S5 / S8). Framework-agnostic: `fetch` is
5
+ // injectable so the core never depends on a browser/RN global and tests run
6
+ // offline. Two pure-ish functions:
7
+ //
8
+ // fetchConfig(configUrl, channelId, fetchImpl?) -> Partial<Config> (server view)
9
+ // mergeConfig(propConfig, serverConfig) -> Config (prop > server > default)
10
+ //
11
+ // Precedence (S5/S8): a value provided by the host prop wins over the server,
12
+ // which wins over the SDK default. `undefined` prop/server fields fall through
13
+ // to the next layer (so a host can omit a field and inherit the server value).
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEFAULT_CONFIG = void 0;
16
+ exports.fetchConfig = fetchConfig;
17
+ exports.mergeConfig = mergeConfig;
18
+ const types_1 = require("./types");
19
+ const logger_1 = require("./logger");
20
+ /**
21
+ * SDK defaults — the lowest-precedence layer. Connection fields are intentionally
22
+ * absent (the host must supply them); behaviour flags default to the web widget's
23
+ * baseline.
24
+ */
25
+ exports.DEFAULT_CONFIG = {
26
+ interfaceTheme: "experia",
27
+ language: "en",
28
+ autoConnection: false,
29
+ enableConnection: true,
30
+ openOnLoad: false,
31
+ sendWelcomeMessage: true,
32
+ disabledInput: false,
33
+ // Default-ON (web parity: defualtConfig.makeVideoCall === true). A host prop or
34
+ // server config can opt out with makeVideoCall:false — both are honored because the
35
+ // button gates on the RESOLVED config (prop > server > default), see WebChat.tsx.
36
+ makeVideoCall: true,
37
+ uploadMedia: true,
38
+ uploadAudio: true,
39
+ // Tier A web-parity defaults (web defualtConfig.ts):
40
+ uploadImage: true,
41
+ uploadDocuments: true,
42
+ uploadVideo: true,
43
+ emoji: false, // web defaultConfig.emoji === false
44
+ showButtonChat: true,
45
+ showMessageDate: true,
46
+ // Tier B-3a chrome-feature defaults (web defualtConfig.ts):
47
+ displayTypingIndication: false, // web defaultConfig.displayTypingIndication === false
48
+ showPoweredBy: true,
49
+ showAvatar: true,
50
+ autoJoinVideoCall: false,
51
+ statusIndicatorPosition: "top-right",
52
+ // Tier C web-shell keys (CONFIG-PARITY-AUDIT.md Tier C). storage selects the
53
+ // persistence backend (web parity default "localStorage" -> persistent store);
54
+ // hideWhenNotConnected defaults FALSE in the SDK (opt-in) so the open-to-connect
55
+ // flow is never broken even though web defaults it true; embedded defaults false
56
+ // (floating); fullScreenHeight defaults true (full-height, today's flex:1);
57
+ // typeWidget defaults "default" (the only shell the SDK models).
58
+ storage: "localStorage",
59
+ hideWhenNotConnected: false,
60
+ embedded: false,
61
+ fullScreenHeight: true,
62
+ typeWidget: "default",
63
+ ttlMs: 24 * 60 * 60 * 1000, // 24h sliding TTL
64
+ };
65
+ function resolveFetch(injected) {
66
+ if (injected)
67
+ return injected;
68
+ const g = globalThis.fetch;
69
+ if (g)
70
+ return g;
71
+ throw new types_1.WebChatError("config-fetch", "no fetch implementation available", false);
72
+ }
73
+ /**
74
+ * Build the web-parity options endpoint. The web fetches
75
+ * `https://${configUrl}/api/webchat/options?channelID=${channelId}` — so we
76
+ * prepend `https://` when `configUrl` carries no scheme, strip a trailing slash,
77
+ * and append the documented path + query.
78
+ */
79
+ function buildOptionsUrl(configUrl, channelId) {
80
+ const withScheme = /^[a-z]+:\/\//i.test(configUrl) ? configUrl : `https://${configUrl}`;
81
+ const base = withScheme.replace(/\/$/, "");
82
+ return `${base}/api/webchat/options?channelID=${encodeURIComponent(channelId)}`;
83
+ }
84
+ /**
85
+ * Map the web options payload onto the SDK's flat {@link ServerConfig} shape.
86
+ *
87
+ * Behaviour flags live under `payload.config.*` and styles under `payload.theme.*`
88
+ * (web parity). We read those nested locations first, then fall back to the same
89
+ * key at the top level — so a server that single-encodes a flat object (or a test
90
+ * fixture) still maps. Missing fields are simply omitted.
91
+ */
92
+ function mapPayload(payload) {
93
+ var _a, _b;
94
+ const p = payload !== null && payload !== void 0 ? payload : {};
95
+ const cfg = (_a = p.config) !== null && _a !== void 0 ? _a : {};
96
+ const theme = (_b = p.theme) !== null && _b !== void 0 ? _b : {};
97
+ const out = {};
98
+ // flag <- payload.config.flag ?? payload.flag (nested preferred, flat tolerated).
99
+ const flag = (k) => (cfg[k] !== undefined ? cfg[k] : p[k]);
100
+ const style = (k) => (theme[k] !== undefined ? theme[k] : p[k]);
101
+ if (p.interfaceTheme !== undefined)
102
+ out.interfaceTheme = p.interfaceTheme;
103
+ if (flag("language") !== undefined)
104
+ out.language = flag("language");
105
+ if (flag("autoConnection") !== undefined)
106
+ out.autoConnection = flag("autoConnection");
107
+ if (flag("openOnLoad") !== undefined)
108
+ out.openOnLoad = flag("openOnLoad");
109
+ if (flag("sendWelcomeMessage") !== undefined)
110
+ out.sendWelcomeMessage = flag("sendWelcomeMessage");
111
+ if (flag("disabledInput") !== undefined)
112
+ out.disabledInput = flag("disabledInput");
113
+ if (flag("makeVideoCall") !== undefined)
114
+ out.makeVideoCall = flag("makeVideoCall");
115
+ if (flag("autoJoinVideoCall") !== undefined)
116
+ out.autoJoinVideoCall = flag("autoJoinVideoCall");
117
+ if (flag("uploadMedia") !== undefined)
118
+ out.uploadMedia = flag("uploadMedia");
119
+ if (flag("uploadAudio") !== undefined)
120
+ out.uploadAudio = flag("uploadAudio");
121
+ if (flag("uploadImage") !== undefined)
122
+ out.uploadImage = flag("uploadImage");
123
+ if (flag("uploadDocuments") !== undefined)
124
+ out.uploadDocuments = flag("uploadDocuments");
125
+ if (flag("uploadVideo") !== undefined)
126
+ out.uploadVideo = flag("uploadVideo");
127
+ if (flag("emoji") !== undefined)
128
+ out.emoji = flag("emoji");
129
+ if (flag("showCloseButton") !== undefined)
130
+ out.showCloseButton = flag("showCloseButton");
131
+ if (flag("showButtonChat") !== undefined)
132
+ out.showButtonChat = flag("showButtonChat");
133
+ if (flag("showMessageDate") !== undefined)
134
+ out.showMessageDate = flag("showMessageDate");
135
+ if (flag("displayTypingIndication") !== undefined)
136
+ out.displayTypingIndication = flag("displayTypingIndication");
137
+ if (flag("showPoweredBy") !== undefined)
138
+ out.showPoweredBy = flag("showPoweredBy");
139
+ if (flag("showAvatar") !== undefined)
140
+ out.showAvatar = flag("showAvatar");
141
+ if (flag("avatar") !== undefined)
142
+ out.avatar = flag("avatar");
143
+ if (flag("closeImage") !== undefined)
144
+ out.closeImage = flag("closeImage");
145
+ if (flag("openLauncherImage") !== undefined)
146
+ out.openLauncherImage = flag("openLauncherImage");
147
+ if (flag("closeLauncherImage") !== undefined)
148
+ out.closeLauncherImage = flag("closeLauncherImage");
149
+ if (flag("title") !== undefined)
150
+ out.title = flag("title");
151
+ if (flag("subTitle") !== undefined)
152
+ out.subTitle = flag("subTitle");
153
+ if (flag("connectingText") !== undefined)
154
+ out.connectingText = flag("connectingText");
155
+ if (flag("inputTextFieldHint") !== undefined)
156
+ out.inputTextFieldHint = flag("inputTextFieldHint");
157
+ if (flag("statusIndicatorPosition") !== undefined)
158
+ out.statusIndicatorPosition = flag("statusIndicatorPosition");
159
+ if (flag("prechatForm") !== undefined)
160
+ out.prechatForm = flag("prechatForm");
161
+ // Tier C web-shell keys — mapped so the server value reaches the SDK. `storage`
162
+ // selects the persistence backend; `hideWhenNotConnected`/`embedded`/`fullScreenHeight`
163
+ // adjust surface/launcher layout + visibility; `typeWidget` is accepted but only
164
+ // "default" is modeled (non-default falls back to default at the consumer).
165
+ if (flag("storage") !== undefined)
166
+ out.storage = flag("storage");
167
+ if (flag("hideWhenNotConnected") !== undefined)
168
+ out.hideWhenNotConnected = flag("hideWhenNotConnected");
169
+ if (flag("embedded") !== undefined)
170
+ out.embedded = flag("embedded");
171
+ if (flag("fullScreenHeight") !== undefined)
172
+ out.fullScreenHeight = flag("fullScreenHeight");
173
+ if (flag("typeWidget") !== undefined)
174
+ out.typeWidget = flag("typeWidget");
175
+ if (style("publicStyle") !== undefined)
176
+ out.publicStyle = style("publicStyle");
177
+ if (style("chatSectionStyle") !== undefined)
178
+ out.chatSectionStyle = style("chatSectionStyle");
179
+ if (style("headerSectionStyle") !== undefined)
180
+ out.headerSectionStyle = style("headerSectionStyle");
181
+ if (style("sendSectionStyle") !== undefined)
182
+ out.sendSectionStyle = style("sendSectionStyle");
183
+ if (style("wedgitSectionStyle") !== undefined)
184
+ out.wedgitSectionStyle = style("wedgitSectionStyle");
185
+ return out;
186
+ }
187
+ /**
188
+ * Fetch the server-side config for a channel (web parity).
189
+ *
190
+ * Hits `https://${configUrl}/api/webchat/options?channelID=${channelId}` and
191
+ * defensively double-parses the body (the web ships a stringified JSON body, but
192
+ * a plain object is tolerated too). The payload is then mapped onto the flat
193
+ * {@link ServerConfig} shape via {@link mapPayload}.
194
+ *
195
+ * @param configUrl config host (scheme optional — `https://` is prepended).
196
+ * @param channelId tenant channel id (sent as the `channelID` query param).
197
+ * @param fetchImpl injected fetch (defaults to the ambient global).
198
+ * @throws WebChatError('config-fetch', …, recoverable=true) on network/non-2xx/parse
199
+ * failure — recoverable so the host can retry.
200
+ */
201
+ async function fetchConfig(configUrl, channelId, fetchImpl) {
202
+ const doFetch = resolveFetch(fetchImpl);
203
+ const url = buildOptionsUrl(configUrl, channelId);
204
+ let res;
205
+ try {
206
+ res = await doFetch(url, {
207
+ method: "GET",
208
+ headers: { "Content-Type": "application/json" },
209
+ });
210
+ }
211
+ catch (cause) {
212
+ logger_1.logger.warn("config fetch failed (network)");
213
+ throw new types_1.WebChatError("config-fetch", "config fetch failed", true, cause);
214
+ }
215
+ if (!res.ok) {
216
+ logger_1.logger.warn("config fetch failed (status)", res.status);
217
+ throw new types_1.WebChatError("config-fetch", `config fetch failed: ${res.status}`, true);
218
+ }
219
+ try {
220
+ // Defensive double-parse: the web body is a JSON-encoded string, but a server
221
+ // returning a plain object is tolerated (parse only when we got a string).
222
+ const raw = await res.json();
223
+ const payload = (typeof raw === "string" ? JSON.parse(raw) : raw);
224
+ return mapPayload(payload);
225
+ }
226
+ catch (cause) {
227
+ logger_1.logger.warn("config parse failed");
228
+ throw new types_1.WebChatError("config-fetch", "config parse failed", true, cause);
229
+ }
230
+ }
231
+ /** Pick only the keys whose value is not `undefined` (so they actually override). */
232
+ function defined(o) {
233
+ if (!o)
234
+ return {};
235
+ const out = {};
236
+ for (const k of Object.keys(o)) {
237
+ if (o[k] !== undefined)
238
+ out[k] = o[k];
239
+ }
240
+ return out;
241
+ }
242
+ /**
243
+ * Merge config layers with precedence prop > server > default (S5/S8).
244
+ *
245
+ * `publicStyle` is merged shallowly across layers (so a server can set a brand
246
+ * colour and the host can override just the font without dropping the colour).
247
+ * The connection trio (channelId/configUrl/connectionUrl) comes only from the
248
+ * host prop — it is required there and not overridable by the server.
249
+ */
250
+ function mergeConfig(propConfig, serverConfig) {
251
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
252
+ const server = defined(serverConfig !== null && serverConfig !== void 0 ? serverConfig : {});
253
+ const prop = defined(propConfig);
254
+ const publicStyle = {
255
+ ...exports.DEFAULT_CONFIG.publicStyle,
256
+ ...((_a = server.publicStyle) !== null && _a !== void 0 ? _a : {}),
257
+ ...((_b = prop.publicStyle) !== null && _b !== void 0 ? _b : {}),
258
+ };
259
+ // chatSectionStyle is merged shallowly across layers too (so a server can set
260
+ // bubble colours and the host can override just one without dropping the rest).
261
+ const chatSectionStyle = {
262
+ ...exports.DEFAULT_CONFIG.chatSectionStyle,
263
+ ...((_c = server.chatSectionStyle) !== null && _c !== void 0 ? _c : {}),
264
+ ...((_d = prop.chatSectionStyle) !== null && _d !== void 0 ? _d : {}),
265
+ };
266
+ // header/send section styles merge shallowly across layers, like publicStyle.
267
+ const headerSectionStyle = {
268
+ ...exports.DEFAULT_CONFIG.headerSectionStyle,
269
+ ...((_e = server.headerSectionStyle) !== null && _e !== void 0 ? _e : {}),
270
+ ...((_f = prop.headerSectionStyle) !== null && _f !== void 0 ? _f : {}),
271
+ };
272
+ const sendSectionStyle = {
273
+ ...exports.DEFAULT_CONFIG.sendSectionStyle,
274
+ ...((_g = server.sendSectionStyle) !== null && _g !== void 0 ? _g : {}),
275
+ ...((_h = prop.sendSectionStyle) !== null && _h !== void 0 ? _h : {}),
276
+ };
277
+ const wedgitSectionStyle = {
278
+ ...exports.DEFAULT_CONFIG.wedgitSectionStyle,
279
+ ...((_j = server.wedgitSectionStyle) !== null && _j !== void 0 ? _j : {}),
280
+ ...((_k = prop.wedgitSectionStyle) !== null && _k !== void 0 ? _k : {}),
281
+ };
282
+ const merged = {
283
+ ...exports.DEFAULT_CONFIG,
284
+ ...server,
285
+ ...prop,
286
+ // connection trio is authoritative from the prop
287
+ channelId: propConfig.channelId,
288
+ configUrl: propConfig.configUrl,
289
+ connectionUrl: propConfig.connectionUrl,
290
+ };
291
+ if (Object.keys(publicStyle).length > 0)
292
+ merged.publicStyle = publicStyle;
293
+ if (Object.keys(chatSectionStyle).length > 0)
294
+ merged.chatSectionStyle = chatSectionStyle;
295
+ if (Object.keys(headerSectionStyle).length > 0)
296
+ merged.headerSectionStyle = headerSectionStyle;
297
+ if (Object.keys(sendSectionStyle).length > 0)
298
+ merged.sendSectionStyle = sendSectionStyle;
299
+ if (Object.keys(wedgitSectionStyle).length > 0)
300
+ merged.wedgitSectionStyle = wedgitSectionStyle;
301
+ return merged;
302
+ }
@@ -0,0 +1,11 @@
1
+ export declare function buildGreet(prechat: Record<string, string>): {
2
+ message: "/chitchat.greet";
3
+ prechatFormSubmission?: undefined;
4
+ } | {
5
+ message: string;
6
+ prechatFormSubmission: Record<string, string>;
7
+ };
8
+ export declare const shouldGreet: (o: {
9
+ sessionId: string;
10
+ greetedSessions: Set<string>;
11
+ }) => boolean;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldGreet = void 0;
4
+ exports.buildGreet = buildGreet;
5
+ // src/core/greet.ts
6
+ function buildGreet(prechat) {
7
+ const keys = Object.keys(prechat);
8
+ if (keys.length === 0)
9
+ return { message: "/chitchat.greet" };
10
+ const lines = keys.map((k) => `${k}: ${prechat[k]}`).join("\n");
11
+ return {
12
+ message: `/chitchat.greet\nForm Response :\n${lines}`,
13
+ prechatFormSubmission: prechat,
14
+ };
15
+ }
16
+ const shouldGreet = (o) => !!o.sessionId && !o.greetedSessions.has(o.sessionId);
17
+ exports.shouldGreet = shouldGreet;
@@ -0,0 +1,31 @@
1
+ /** Shape of a single ICE server entry (RTCIceServer-compatible). */
2
+ export interface IceServer {
3
+ urls: string;
4
+ username?: string;
5
+ credential?: string;
6
+ }
7
+ /** RTCConfiguration-shaped object consumed by `createPeerConnection`. */
8
+ export interface PcConfig {
9
+ iceServers: IceServer[];
10
+ }
11
+ /**
12
+ * Web-parity TURN credentials. Exported so the adapter/tests can assert parity
13
+ * with the web widget without duplicating the literals.
14
+ */
15
+ export declare const TURN_URL = "turn:141.147.138.24:3478";
16
+ export declare const TURN_USERNAME = "root";
17
+ export declare const TURN_CREDENTIAL = "1234567";
18
+ export declare const STUN_URL = "stun:stun.l.google.com:19302";
19
+ /**
20
+ * The web-parity `pc_config`. Order matches the web widget: STUN first, TURN second.
21
+ * Returns a fresh object/array each call so callers can't mutate the shared config.
22
+ */
23
+ export declare function iceServers(): PcConfig;
24
+ /** ICE candidate types the web widget forwards over the signaling channel. */
25
+ export type CandidateType = "host" | "prflx" | "relay" | "srflx" | string;
26
+ /**
27
+ * Host-candidate policy (C7) — mirror the web widget: forward only
28
+ * `prflx`/`relay`/`srflx` candidates; drop `host` candidates. There is no
29
+ * same-LAN host fallback today (revisit only if a tenant requires it).
30
+ */
31
+ export declare function isForwardableCandidate(type: CandidateType | null | undefined): boolean;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ // src/core/ice.ts
3
+ // Static ICE / pc_config — WEB PARITY (B12 resolved to parity, no ephemeral creds).
4
+ //
5
+ // Mirrors the chatbot web UI `pc_config` exactly (see web `VideoCall.tsx`):
6
+ // - STUN stun:stun.l.google.com:19302
7
+ // - TURN turn:141.147.138.24:3478 (username "root", credential "1234567")
8
+ //
9
+ // Distribution is a private registry, so exposing these static creds mirrors the
10
+ // web widget's exposure. There is NO fetch endpoint and NO ephemeral-credential
11
+ // flow — the call uses the same hard-coded servers the web widget ships with.
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.STUN_URL = exports.TURN_CREDENTIAL = exports.TURN_USERNAME = exports.TURN_URL = void 0;
14
+ exports.iceServers = iceServers;
15
+ exports.isForwardableCandidate = isForwardableCandidate;
16
+ /**
17
+ * Web-parity TURN credentials. Exported so the adapter/tests can assert parity
18
+ * with the web widget without duplicating the literals.
19
+ */
20
+ exports.TURN_URL = "turn:141.147.138.24:3478";
21
+ exports.TURN_USERNAME = "root";
22
+ exports.TURN_CREDENTIAL = "1234567";
23
+ exports.STUN_URL = "stun:stun.l.google.com:19302";
24
+ /**
25
+ * The web-parity `pc_config`. Order matches the web widget: STUN first, TURN second.
26
+ * Returns a fresh object/array each call so callers can't mutate the shared config.
27
+ */
28
+ function iceServers() {
29
+ return {
30
+ iceServers: [
31
+ { urls: exports.STUN_URL },
32
+ {
33
+ urls: exports.TURN_URL,
34
+ username: exports.TURN_USERNAME,
35
+ credential: exports.TURN_CREDENTIAL,
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ const FORWARDABLE = new Set(["prflx", "relay", "srflx"]);
41
+ /**
42
+ * Host-candidate policy (C7) — mirror the web widget: forward only
43
+ * `prflx`/`relay`/`srflx` candidates; drop `host` candidates. There is no
44
+ * same-LAN host fallback today (revisit only if a tenant requires it).
45
+ */
46
+ function isForwardableCandidate(type) {
47
+ return type != null && FORWARDABLE.has(type);
48
+ }
@@ -0,0 +1,11 @@
1
+ export type Token = {
2
+ type: "text";
3
+ value: string;
4
+ } | {
5
+ type: "link";
6
+ value: string;
7
+ href: string;
8
+ };
9
+ export declare function linkify(text: string): Token[];
10
+ /** schemes safe to hand to Linking.openURL without confirmation */
11
+ export declare const isSafeHref: (href: string) => boolean;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSafeHref = void 0;
4
+ exports.linkify = linkify;
5
+ const URL_RE = /(https:\/\/[^\s]+)|([\w.+-]+@[\w-]+\.[\w.-]+)|(tel:\+?[\d-]+)/g;
6
+ function linkify(text) {
7
+ var _a;
8
+ const tokens = [];
9
+ let last = 0;
10
+ for (const m of text.matchAll(URL_RE)) {
11
+ const idx = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
12
+ if (idx > last)
13
+ tokens.push({ type: "text", value: text.slice(last, idx) });
14
+ const raw = m[0];
15
+ const href = raw.startsWith("https://") ? raw : raw.startsWith("tel:") ? raw : `mailto:${raw}`;
16
+ tokens.push({ type: "link", value: raw, href });
17
+ last = idx + raw.length;
18
+ }
19
+ if (last < text.length)
20
+ tokens.push({ type: "text", value: text.slice(last) });
21
+ return tokens.length ? tokens : [{ type: "text", value: text }];
22
+ }
23
+ /** schemes safe to hand to Linking.openURL without confirmation */
24
+ const isSafeHref = (href) => /^https:\/\//.test(href);
25
+ exports.isSafeHref = isSafeHref;
@@ -0,0 +1,17 @@
1
+ export interface Logger {
2
+ debug(...args: unknown[]): void;
3
+ info(...args: unknown[]): void;
4
+ warn(...args: unknown[]): void;
5
+ error(...args: unknown[]): void;
6
+ }
7
+ /**
8
+ * Build a logger. In release (`__DEV__` !== true) every method is a no-op so no
9
+ * core diagnostics reach production logs. In dev they forward to the matching
10
+ * `console` method behind a stable prefix.
11
+ *
12
+ * @param sink injectable console-like sink (tests assert calls without spying on
13
+ * the global console).
14
+ */
15
+ export declare function createLogger(sink?: Partial<Console>): Logger;
16
+ /** Process-wide default logger (dev-gated). */
17
+ export declare const logger: Logger;