@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,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;
|
package/lib/core/ice.js
ADDED
|
@@ -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;
|