@robylon/react-native-sdk 2.2.0 → 2.2.1-staging.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/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/robylonreactnativesdk/RobylonDownloadModule.java +83 -0
- package/android/src/main/java/com/robylonreactnativesdk/RobylonReactNativeSdkPackage.java +24 -0
- package/lib/commonjs/Chatbotsdk.js +3 -3
- package/lib/commonjs/Chatbotsdk.js.map +1 -1
- package/lib/commonjs/config.js +1 -1
- package/lib/commonjs/config.js.map +1 -1
- package/lib/commonjs/constants.js +1 -1
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/versions/version.staging.js +1 -1
- package/lib/module/Chatbotsdk.js +3 -3
- package/lib/module/Chatbotsdk.js.map +1 -1
- package/lib/module/config.js +1 -1
- package/lib/module/config.js.map +1 -1
- package/lib/module/constants.js +1 -1
- package/lib/module/constants.js.map +1 -1
- package/lib/module/openChatbot.js +1 -2
- package/lib/module/openChatbot.js.map +1 -1
- package/lib/module/versions/version.staging.js +1 -1
- package/lib/typescript/Chatbotsdk.d.ts +40 -0
- package/lib/typescript/Chatbotsdk.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/versions/version.staging.d.ts +1 -1
- package/package.json +1 -1
- package/robylon-react-native-sdk-2.2.1-staging.0.tgz +0 -0
- package/src/Chatbotsdk.tsx +1191 -0
- package/src/FloatingButton/launchers/ImageLauncher.tsx +75 -0
- package/src/FloatingButton/launchers/TextLauncher.tsx +84 -0
- package/src/FloatingButton/launchers/TextualImageLauncher.tsx +133 -0
- package/src/FloatingButton.tsx +97 -0
- package/src/LoadingIndicator.tsx +11 -0
- package/src/Toast.tsx +65 -0
- package/src/components/DebugButton.tsx +46 -0
- package/src/components/ErrorBoundary.tsx +38 -0
- package/src/config.ts +4 -0
- package/src/constants/errorConstants.ts +21 -0
- package/src/constants/fontStyles.ts +3 -0
- package/src/constants.ts +5 -0
- package/src/global.d.ts +11 -0
- package/src/hooks/useChatbotEvents.ts +117 -0
- package/src/index.tsx +14 -0
- package/src/openChatbot.ts +11 -0
- package/src/openChatbot.tsx +0 -0
- package/src/services/ErrorTrackingService.ts +217 -0
- package/src/types/events.ts +28 -0
- package/src/types/react-native-flipper-performance-plugin.d.ts +3 -0
- package/src/types/react-native-globals.d.ts +10 -0
- package/src/utils/animations.ts +120 -0
- package/src/utils/colorUtils.ts +35 -0
- package/src/utils/cookieUtils.ts +7 -0
- package/src/utils/debugConfig.ts +56 -0
- package/src/utils/debugMenu.ts +14 -0
- package/src/utils/errorHandler.ts +58 -0
- package/src/utils/fileDownload.ts +198 -0
- package/src/utils/logger.ts +14 -0
- package/src/utils/session.ts +26 -0
- package/src/utils/systemInfo.ts +110 -0
- package/src/utils/webViewStorage.ts +62 -0
- package/src/version.ts +2 -0
- package/src/versions/version.dev.ts +2 -0
- package/src/versions/version.production.ts +2 -0
- package/src/versions/version.staging.ts +2 -0
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useMemo,
|
|
6
|
+
useCallback,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
forwardRef,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
View,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Animated,
|
|
14
|
+
Dimensions,
|
|
15
|
+
Platform,
|
|
16
|
+
StatusBar,
|
|
17
|
+
PixelRatio,
|
|
18
|
+
FlexAlignType,
|
|
19
|
+
PermissionsAndroid,
|
|
20
|
+
Alert,
|
|
21
|
+
} from "react-native";
|
|
22
|
+
import { WebView, WebViewMessageEvent } from "react-native-webview";
|
|
23
|
+
import FloatingButton from "./FloatingButton";
|
|
24
|
+
import { API_URL, BASE_CHATBOT_URL, DEFAULT_LAUNCHER_IMAGE } from "./constants";
|
|
25
|
+
import { getSystemInfo, getBrowserAndOSInfoScript } from "./utils/systemInfo";
|
|
26
|
+
import { useChatbotEvents } from "./hooks/useChatbotEvents";
|
|
27
|
+
import {
|
|
28
|
+
ChatbotEventType,
|
|
29
|
+
ChatbotEventHandler,
|
|
30
|
+
InternalEventType,
|
|
31
|
+
} from "./types/events";
|
|
32
|
+
import { logger } from "./utils/logger";
|
|
33
|
+
import { enableNetworkDebug, enableWebViewDebug } from "./utils/debugConfig";
|
|
34
|
+
import DebugButton from "./components/DebugButton";
|
|
35
|
+
import { ErrorBoundary } from "./components/ErrorBoundary";
|
|
36
|
+
import {
|
|
37
|
+
ErrorCodes,
|
|
38
|
+
ErrorTypes,
|
|
39
|
+
ErrorMessages,
|
|
40
|
+
} from "./constants/errorConstants";
|
|
41
|
+
import { setupGlobalErrorHandlers } from "./utils/errorHandler";
|
|
42
|
+
import { errorTracker } from "./services/ErrorTrackingService";
|
|
43
|
+
import { generateUUID } from "./utils/cookieUtils";
|
|
44
|
+
import {
|
|
45
|
+
StorageMessageType,
|
|
46
|
+
StorageMessage,
|
|
47
|
+
getStorageScript,
|
|
48
|
+
createStorageMessage,
|
|
49
|
+
} from "./utils/webViewStorage";
|
|
50
|
+
import { animateWebViewOpen, animateWebViewClose } from "./utils/animations";
|
|
51
|
+
import { handleDownloadRequest, DownloadRequest } from "./utils/fileDownload";
|
|
52
|
+
import {
|
|
53
|
+
SESSION_UPDATED_MESSAGE_TYPE,
|
|
54
|
+
RESUME_SESSION_KEY,
|
|
55
|
+
extractSessionId,
|
|
56
|
+
} from "./utils/session";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Host-app overrides for the branding the chat UI paints on its very first
|
|
60
|
+
* frame, before its own config call lands. Every field is optional and wins
|
|
61
|
+
* over the value the SDK fetched from /chat/chatbot/get/; anything omitted
|
|
62
|
+
* falls back to that fetched config.
|
|
63
|
+
*/
|
|
64
|
+
export interface ChatbotLoadingScreen {
|
|
65
|
+
/** Header avatar. */
|
|
66
|
+
image?: string;
|
|
67
|
+
/** Display name in the header. */
|
|
68
|
+
bot_name?: string;
|
|
69
|
+
/** Colour the header is tinted with. */
|
|
70
|
+
brand_color?: string;
|
|
71
|
+
/** Whether the "powered by" footer shows. */
|
|
72
|
+
powered_by?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Sub-text shown under the header. Unlike the fields above, no brand_config
|
|
75
|
+
* field backs this one, so it ships only when the host app supplies it.
|
|
76
|
+
*/
|
|
77
|
+
sub_text?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ChatbotProps {
|
|
81
|
+
api_key: string;
|
|
82
|
+
user_id?: string | null | number;
|
|
83
|
+
user_token?: string;
|
|
84
|
+
user_profile?: {
|
|
85
|
+
email?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
mobile?: string | number;
|
|
88
|
+
is_test_user?: boolean;
|
|
89
|
+
[key: string | number]: any;
|
|
90
|
+
};
|
|
91
|
+
session_id?: string;
|
|
92
|
+
session_context?: Record<string, any>;
|
|
93
|
+
discrete?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Overrides for the branding the chat UI paints before its own config call
|
|
96
|
+
* lands. Purely a first-paint concern: the chat UI reconciles to its own
|
|
97
|
+
* config afterwards, so these do not restyle the running chat.
|
|
98
|
+
*/
|
|
99
|
+
loading_screen?: ChatbotLoadingScreen;
|
|
100
|
+
onEvent?: ChatbotEventHandler;
|
|
101
|
+
onOpen?: () => void;
|
|
102
|
+
onClose?: () => void;
|
|
103
|
+
onReady?: () => void;
|
|
104
|
+
onSession?: (
|
|
105
|
+
session_id: string,
|
|
106
|
+
session_context?: Record<string, any>,
|
|
107
|
+
) => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ChatbotRef {
|
|
111
|
+
open: () => void;
|
|
112
|
+
close: () => void;
|
|
113
|
+
toggle: () => void;
|
|
114
|
+
isReady: () => boolean;
|
|
115
|
+
getSessionId: () => string | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface InternalChatbotProps extends ChatbotProps {
|
|
119
|
+
show_floating_button?: boolean;
|
|
120
|
+
onMessage?: (message: any) => void;
|
|
121
|
+
isFullScreen?: boolean;
|
|
122
|
+
enableAnimation?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ChatbotMessage {
|
|
126
|
+
type: string;
|
|
127
|
+
data?: any;
|
|
128
|
+
level?: string;
|
|
129
|
+
url?: string;
|
|
130
|
+
filename?: string;
|
|
131
|
+
inPlace?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export enum ChatbotInterfaceType {
|
|
135
|
+
WIDGET = "WIDGET",
|
|
136
|
+
POPOVER = "POPOVER",
|
|
137
|
+
EMBED = "EMBED",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export enum WidgetPositionEnums {
|
|
141
|
+
RIGHT = "Right",
|
|
142
|
+
LEFT = "Left",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface WidgetInterfaceProperties {
|
|
146
|
+
position: WidgetPositionEnums;
|
|
147
|
+
side_spacing?: number;
|
|
148
|
+
bottom_spacing?: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export enum LauncherType {
|
|
152
|
+
TEXT = "TEXT",
|
|
153
|
+
IMAGE = "IMAGE",
|
|
154
|
+
TEXTUAL_IMAGE = "TEXTUAL_IMAGE",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ChatbotConfig {
|
|
158
|
+
brand_colour: string;
|
|
159
|
+
/**
|
|
160
|
+
* Bot display name, from brand_config.name. Not to be confused with
|
|
161
|
+
* chat_interface_config.display_name below, which reads a
|
|
162
|
+
* brand_config.display_name that the API does not return and which nothing
|
|
163
|
+
* in the SDK consumes.
|
|
164
|
+
*/
|
|
165
|
+
name?: string;
|
|
166
|
+
image_url?: string;
|
|
167
|
+
chat_interface_config?: {
|
|
168
|
+
chat_bubble_prompts?: string[];
|
|
169
|
+
display_name?: string;
|
|
170
|
+
welcome_message?: string;
|
|
171
|
+
redirect_url?: string;
|
|
172
|
+
};
|
|
173
|
+
interface_type?: ChatbotInterfaceType;
|
|
174
|
+
interface_properties?: WidgetInterfaceProperties;
|
|
175
|
+
launcher_type: LauncherType;
|
|
176
|
+
launcher_properties: {
|
|
177
|
+
text?: string;
|
|
178
|
+
};
|
|
179
|
+
images: {
|
|
180
|
+
launcher_image_url?: {
|
|
181
|
+
url: string;
|
|
182
|
+
};
|
|
183
|
+
header_image_url?: {
|
|
184
|
+
url: string;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
footer?: {
|
|
188
|
+
show_powered_by?: boolean;
|
|
189
|
+
};
|
|
190
|
+
chat_iframe_url: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
|
|
194
|
+
(
|
|
195
|
+
{
|
|
196
|
+
api_key,
|
|
197
|
+
user_id,
|
|
198
|
+
user_token,
|
|
199
|
+
show_floating_button = true,
|
|
200
|
+
onEvent,
|
|
201
|
+
onMessage,
|
|
202
|
+
isFullScreen = true,
|
|
203
|
+
enableAnimation = true,
|
|
204
|
+
onOpen: externalOnOpen,
|
|
205
|
+
onClose: externalOnClose,
|
|
206
|
+
onReady,
|
|
207
|
+
user_profile,
|
|
208
|
+
session_id,
|
|
209
|
+
session_context,
|
|
210
|
+
discrete = false,
|
|
211
|
+
onSession,
|
|
212
|
+
loading_screen,
|
|
213
|
+
},
|
|
214
|
+
ref,
|
|
215
|
+
) => {
|
|
216
|
+
const [isWebViewVisible, setIsWebViewVisible] = useState(false);
|
|
217
|
+
const [chatbotConfig, setChatbotConfig] = useState<ChatbotConfig | null>(
|
|
218
|
+
null,
|
|
219
|
+
);
|
|
220
|
+
const [loading, setLoading] = useState(true);
|
|
221
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
222
|
+
const webViewRef = useRef<WebView>(null);
|
|
223
|
+
const isFirstLoadRef = useRef(true);
|
|
224
|
+
const [toastVisible, setToastVisible] = useState(false);
|
|
225
|
+
const [toastMessage, setToastMessage] = useState("");
|
|
226
|
+
const [effectiveUserId, setEffectiveUserId] = useState<string>();
|
|
227
|
+
const [isStorageReady, setIsStorageReady] = useState(false);
|
|
228
|
+
const pendingStorageOps = useRef<Array<() => void>>([]);
|
|
229
|
+
const memoryCache = useRef<Record<string, string>>({});
|
|
230
|
+
const [systemInfo, setSystemInfo] = useState({ os: "", browser: "" });
|
|
231
|
+
const systemInfoRef = useRef({ os: "", browser: "" });
|
|
232
|
+
|
|
233
|
+
// Session id pass-through. All four are refs so that client-supplied values
|
|
234
|
+
// never enter handleMessage's dependency array: adding them would rebuild
|
|
235
|
+
// the handler on every client re-render, omitting them without a ref would
|
|
236
|
+
// capture a stale closure.
|
|
237
|
+
// The imperative handle gates on these rather than on the state values it
|
|
238
|
+
// captured when it was built. A handle is only rebuilt on the render after
|
|
239
|
+
// a state change, so onReady — which fires in the same pass that sets
|
|
240
|
+
// isInitialized — used to hand out a handle whose open() was still gated
|
|
241
|
+
// shut. These mirror the state writes so the handle is never stale.
|
|
242
|
+
const isInitializedRef = useRef(false);
|
|
243
|
+
const isWebViewVisibleRef = useRef(false);
|
|
244
|
+
|
|
245
|
+
const sessionIdRef = useRef<string | undefined>(undefined);
|
|
246
|
+
const onSessionRef = useRef(onSession);
|
|
247
|
+
const sessionContextRef = useRef(session_context);
|
|
248
|
+
const resumeSessionRef = useRef(session_id);
|
|
249
|
+
const discreteRef = useRef(discrete);
|
|
250
|
+
const loadingScreenRef = useRef(loading_screen);
|
|
251
|
+
onSessionRef.current = onSession;
|
|
252
|
+
sessionContextRef.current = session_context;
|
|
253
|
+
resumeSessionRef.current = session_id;
|
|
254
|
+
discreteRef.current = discrete;
|
|
255
|
+
loadingScreenRef.current = loading_screen;
|
|
256
|
+
|
|
257
|
+
// Same reasoning as above: the branding hint is read inside handleMessage,
|
|
258
|
+
// which does not list chatbotConfig among its dependencies.
|
|
259
|
+
const chatbotConfigRef = useRef<ChatbotConfig | null>(null);
|
|
260
|
+
chatbotConfigRef.current = chatbotConfig;
|
|
261
|
+
|
|
262
|
+
// The init_load_* hint rides the first registerUserId after the chat
|
|
263
|
+
// surface loads and no later one. A reopen re-triggers APP_READY, but by
|
|
264
|
+
// then the chat UI has its own config and the hint would be noise.
|
|
265
|
+
const hasSentInitLoadRef = useRef(false);
|
|
266
|
+
|
|
267
|
+
// Initialize animated values
|
|
268
|
+
const animatedValues = useRef({
|
|
269
|
+
scale: new Animated.Value(1),
|
|
270
|
+
translateY: new Animated.Value(0),
|
|
271
|
+
opacity: new Animated.Value(1),
|
|
272
|
+
}).current;
|
|
273
|
+
|
|
274
|
+
// Create chatbotConfig object for events
|
|
275
|
+
const chatbotConfigForEvents = useMemo(
|
|
276
|
+
() => ({
|
|
277
|
+
userId: effectiveUserId,
|
|
278
|
+
isAnonymous: !user_id && user_id !== 0,
|
|
279
|
+
// Add other config properties as needed
|
|
280
|
+
}),
|
|
281
|
+
[effectiveUserId, user_id],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Initialize event handling
|
|
285
|
+
const { emitEvent, onInternalEvent } = useChatbotEvents({
|
|
286
|
+
api_key,
|
|
287
|
+
chatbotConfig: chatbotConfigForEvents ?? {},
|
|
288
|
+
user_profile: user_profile,
|
|
289
|
+
onEvent: onEvent,
|
|
290
|
+
systemInfo: systemInfo ?? { os: "", browser: "" },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Expose ref methods
|
|
294
|
+
useImperativeHandle(
|
|
295
|
+
ref,
|
|
296
|
+
() => ({
|
|
297
|
+
open: () => {
|
|
298
|
+
if (isInitializedRef.current) {
|
|
299
|
+
openWebView();
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
close: () => {
|
|
303
|
+
if (isInitializedRef.current) {
|
|
304
|
+
closeWebView();
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
toggle: () => {
|
|
308
|
+
if (isInitializedRef.current) {
|
|
309
|
+
if (isWebViewVisibleRef.current) {
|
|
310
|
+
closeWebView();
|
|
311
|
+
} else {
|
|
312
|
+
openWebView();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
isReady: () => isInitializedRef.current,
|
|
317
|
+
getSessionId: () => sessionIdRef.current,
|
|
318
|
+
}),
|
|
319
|
+
[isInitialized, isWebViewVisible],
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Hands the session id to the client. Deduped so that a re-emission on
|
|
323
|
+
// reopen does not fire client side effects twice, and wrapped so that a
|
|
324
|
+
// throwing client handler cannot break message handling.
|
|
325
|
+
const emitSession = useCallback((data: any) => {
|
|
326
|
+
const sessionId = extractSessionId(data);
|
|
327
|
+
if (!sessionId || sessionId === sessionIdRef.current) return;
|
|
328
|
+
sessionIdRef.current = sessionId;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
onSessionRef.current?.(sessionId, sessionContextRef.current ?? {});
|
|
332
|
+
} catch (error) {
|
|
333
|
+
errorTracker.trackError(
|
|
334
|
+
error instanceof Error
|
|
335
|
+
? error
|
|
336
|
+
: new Error("onSession handler threw an error"),
|
|
337
|
+
"ChatbotSDK",
|
|
338
|
+
{
|
|
339
|
+
// The session id itself is deliberately left out of the report so
|
|
340
|
+
// that it stays confined to the two opt-in client channels.
|
|
341
|
+
type: ErrorTypes.RUNTIME_ERROR,
|
|
342
|
+
context: { source: SESSION_UPDATED_MESSAGE_TYPE },
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
const triggerAppInit = () => {
|
|
349
|
+
if (webViewRef.current) {
|
|
350
|
+
webViewRef.current?.injectJavaScript(getBrowserAndOSInfoScript());
|
|
351
|
+
webViewRef.current.injectJavaScript(`
|
|
352
|
+
// Trigger APP_READY equivalent
|
|
353
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'APP_READY' }));
|
|
354
|
+
true;
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const executeStorageOp = useCallback(
|
|
360
|
+
(operation: () => void) => {
|
|
361
|
+
if (isStorageReady) {
|
|
362
|
+
operation();
|
|
363
|
+
} else {
|
|
364
|
+
pendingStorageOps.current.push(operation);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
[isStorageReady],
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Handle WebView messages
|
|
371
|
+
const handleMessage = useCallback(
|
|
372
|
+
async (event: WebViewMessageEvent) => {
|
|
373
|
+
const { data } = event.nativeEvent;
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const parsedData: ChatbotMessage = JSON.parse(data);
|
|
377
|
+
// Handle storage messages
|
|
378
|
+
if (
|
|
379
|
+
Object.values(StorageMessageType).includes(
|
|
380
|
+
parsedData.type as StorageMessageType,
|
|
381
|
+
)
|
|
382
|
+
) {
|
|
383
|
+
const storageMessage = parsedData as StorageMessage;
|
|
384
|
+
switch (storageMessage.type) {
|
|
385
|
+
case StorageMessageType.STORAGE_READY:
|
|
386
|
+
setIsStorageReady(true);
|
|
387
|
+
pendingStorageOps.current.forEach((op) => op());
|
|
388
|
+
pendingStorageOps.current = [];
|
|
389
|
+
// Try to get stored ID immediately after storage is ready
|
|
390
|
+
webViewRef.current?.injectJavaScript(
|
|
391
|
+
createStorageMessage(
|
|
392
|
+
StorageMessageType.GET_STORAGE,
|
|
393
|
+
"rblyn_anon",
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
break;
|
|
397
|
+
case StorageMessageType.STORAGE_RESPONSE:
|
|
398
|
+
if (storageMessage.key && storageMessage.value) {
|
|
399
|
+
memoryCache.current[storageMessage.key] =
|
|
400
|
+
storageMessage.value;
|
|
401
|
+
if (storageMessage.key === "rblyn_anon") {
|
|
402
|
+
setEffectiveUserId(storageMessage.value);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
case StorageMessageType.STORAGE_ERROR:
|
|
407
|
+
logger.error("WebView storage error:", storageMessage.error);
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Add debug logging
|
|
414
|
+
if (__DEV__) {
|
|
415
|
+
if (parsedData?.type === "CONSOLE") {
|
|
416
|
+
console.log(`WebView ${parsedData?.level}:`, parsedData?.data);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (parsedData?.type === "ERROR") {
|
|
420
|
+
console.error("WebView Error:", parsedData?.data);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (parsedData?.type === "SYSTEM_INFO") {
|
|
426
|
+
setSystemInfo(parsedData?.data ?? { os: "", browser: "" });
|
|
427
|
+
systemInfoRef.current = parsedData?.data;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Returns before the switch below so the session id never reaches
|
|
432
|
+
// emitEvent (and through it onEvent and /users/sdk/record-logs/) or
|
|
433
|
+
// the default branch's onMessage. Clients who registered neither
|
|
434
|
+
// onSession nor getSessionId cannot observe it.
|
|
435
|
+
if (parsedData?.type === SESSION_UPDATED_MESSAGE_TYPE) {
|
|
436
|
+
emitSession(parsedData?.data);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (parsedData?.type === "APP_READY") {
|
|
441
|
+
emitEvent(ChatbotEventType.CHATBOT_APP_READY);
|
|
442
|
+
if (webViewRef.current && isWebViewVisible) {
|
|
443
|
+
webViewRef.current?.injectJavaScript(getBrowserAndOSInfoScript());
|
|
444
|
+
|
|
445
|
+
// First-paint branding hint. The chat UI makes its own config
|
|
446
|
+
// call on boot and renders a skeleton until it lands; the SDK
|
|
447
|
+
// already holds the same branding from /chat/chatbot/get/, so
|
|
448
|
+
// passing it here lets the header paint immediately. A hint, not
|
|
449
|
+
// a source of truth: the chat UI keeps whatever its own call
|
|
450
|
+
// returns, and must stay correct when these are absent.
|
|
451
|
+
const brandConfig = chatbotConfigRef.current;
|
|
452
|
+
const loadingScreen = loadingScreenRef.current;
|
|
453
|
+
|
|
454
|
+
// Every key is dropped unless something actually backs it, so
|
|
455
|
+
// the chat UI never has to tell "the SDK knows it is blank" from
|
|
456
|
+
// "the SDK does not know" — a missing key and an empty one mean
|
|
457
|
+
// the same thing to it, and both fall back to its own config.
|
|
458
|
+
// ?? rather than || throughout, so a host app that deliberately
|
|
459
|
+
// passes powered_by: false still overrides a truthy config.
|
|
460
|
+
const hint: Record<string, string | boolean> = {};
|
|
461
|
+
const addHint = (
|
|
462
|
+
key: string,
|
|
463
|
+
value: string | boolean | undefined,
|
|
464
|
+
) => {
|
|
465
|
+
if (value !== undefined && value !== "") {
|
|
466
|
+
hint[key] = value;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
addHint(
|
|
471
|
+
"init_load_image",
|
|
472
|
+
// Header image first, brand logo second, so orgs that set only
|
|
473
|
+
// a launcher logo still get a branded header.
|
|
474
|
+
loadingScreen?.image ??
|
|
475
|
+
(brandConfig?.images?.header_image_url?.url ||
|
|
476
|
+
brandConfig?.image_url),
|
|
477
|
+
);
|
|
478
|
+
addHint(
|
|
479
|
+
"init_load_bot_name",
|
|
480
|
+
loadingScreen?.bot_name ?? brandConfig?.name,
|
|
481
|
+
);
|
|
482
|
+
addHint(
|
|
483
|
+
"init_load_brand_color",
|
|
484
|
+
loadingScreen?.brand_color ?? brandConfig?.brand_colour,
|
|
485
|
+
);
|
|
486
|
+
addHint(
|
|
487
|
+
"init_load_powered_by",
|
|
488
|
+
loadingScreen?.powered_by ?? brandConfig?.footer?.show_powered_by,
|
|
489
|
+
);
|
|
490
|
+
// Prop-only: no brand_config field backs the sub-text.
|
|
491
|
+
addHint("init_load_sub_text", loadingScreen?.sub_text);
|
|
492
|
+
|
|
493
|
+
let initLoadHint: Record<string, string | boolean> | undefined;
|
|
494
|
+
// Left unlatched when there was nothing to send, so a later
|
|
495
|
+
// registration can still carry the hint once config arrives.
|
|
496
|
+
if (!hasSentInitLoadRef.current && Object.keys(hint).length > 0) {
|
|
497
|
+
initLoadHint = hint;
|
|
498
|
+
hasSentInitLoadRef.current = true;
|
|
499
|
+
}
|
|
500
|
+
const messageData = {
|
|
501
|
+
name: "registerUserId",
|
|
502
|
+
action: "registerUserId",
|
|
503
|
+
data: {
|
|
504
|
+
userId: effectiveUserId,
|
|
505
|
+
...initLoadHint,
|
|
506
|
+
[RESUME_SESSION_KEY]: resumeSessionRef.current || undefined,
|
|
507
|
+
// Sent only when opted in. false is the default and is
|
|
508
|
+
// conveyed by the key's absence, so integrations that do not
|
|
509
|
+
// use it keep sending an unchanged payload.
|
|
510
|
+
discrete: discreteRef.current ? true : undefined,
|
|
511
|
+
token: user_token ? `${user_token}` : undefined,
|
|
512
|
+
userProfile: {
|
|
513
|
+
...user_profile,
|
|
514
|
+
...getSystemInfo(systemInfoRef?.current),
|
|
515
|
+
__INT_SYS_INFO__: {
|
|
516
|
+
...getSystemInfo(systemInfoRef?.current),
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
// logger.debug is an unconditional console.log, so the session id
|
|
522
|
+
// is redacted here rather than printed into every client's
|
|
523
|
+
// production console.
|
|
524
|
+
logger.debug(
|
|
525
|
+
"messageData====>",
|
|
526
|
+
JSON.stringify({
|
|
527
|
+
...messageData,
|
|
528
|
+
data: {
|
|
529
|
+
...messageData.data,
|
|
530
|
+
[RESUME_SESSION_KEY]: resumeSessionRef.current
|
|
531
|
+
? "[redacted]"
|
|
532
|
+
: undefined,
|
|
533
|
+
},
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
webViewRef.current?.injectJavaScript(`
|
|
537
|
+
window.postMessage(${JSON.stringify({
|
|
538
|
+
name: "openFrame",
|
|
539
|
+
domain: "app-domain.com",
|
|
540
|
+
})}, '*');
|
|
541
|
+
window.postMessage(${JSON.stringify(messageData)}, '*');
|
|
542
|
+
`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Handle other message types
|
|
547
|
+
switch (parsedData?.type) {
|
|
548
|
+
case "close_chatbot":
|
|
549
|
+
closeWebView();
|
|
550
|
+
break;
|
|
551
|
+
// Download file is not being used currently. This event would come from the iframe chatbot if download file problem need to be solved
|
|
552
|
+
case "download_file":
|
|
553
|
+
if (
|
|
554
|
+
parsedData?.data ||
|
|
555
|
+
(parsedData?.url && parsedData?.filename)
|
|
556
|
+
) {
|
|
557
|
+
// Handle both formats: { data: { url, filename } } or { url, filename }
|
|
558
|
+
const downloadData: DownloadRequest = parsedData?.data || {
|
|
559
|
+
url: parsedData?.url,
|
|
560
|
+
filename: parsedData?.filename,
|
|
561
|
+
inPlace: parsedData?.inPlace,
|
|
562
|
+
};
|
|
563
|
+
handleDownloadRequest(downloadData);
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case "CHATBOT_LOADED":
|
|
567
|
+
emitEvent(ChatbotEventType.CHATBOT_LOADED, parsedData?.data);
|
|
568
|
+
break;
|
|
569
|
+
case "CHAT_INITIALIZED":
|
|
570
|
+
emitEvent(ChatbotEventType.CHAT_INITIALIZED, parsedData?.data);
|
|
571
|
+
break;
|
|
572
|
+
case "SESSION_REFRESHED":
|
|
573
|
+
emitEvent(ChatbotEventType.SESSION_REFRESHED, parsedData?.data);
|
|
574
|
+
break;
|
|
575
|
+
case "CHAT_INITIALIZATION_FAILED":
|
|
576
|
+
emitEvent(
|
|
577
|
+
ChatbotEventType.CHAT_INITIALIZATION_FAILED,
|
|
578
|
+
parsedData?.data,
|
|
579
|
+
);
|
|
580
|
+
break;
|
|
581
|
+
case "REQUEST_MIC_PERMISSION":
|
|
582
|
+
const granted = await requestAndroidMicPermission();
|
|
583
|
+
if (webViewRef.current && isWebViewVisible) {
|
|
584
|
+
const messageData = {
|
|
585
|
+
name: "requestMicPermission",
|
|
586
|
+
action: "requestMicPermission",
|
|
587
|
+
type: granted
|
|
588
|
+
? "MIC_PERMISSION_GRANTED"
|
|
589
|
+
: "MIC_PERMISSION_DENIED",
|
|
590
|
+
};
|
|
591
|
+
webViewRef.current?.injectJavaScript(`
|
|
592
|
+
window.postMessage(${JSON.stringify(messageData)}, '*');`);
|
|
593
|
+
}
|
|
594
|
+
// if (granted) {
|
|
595
|
+
// emitEvent(ChatbotEventType.MIC_PERMISSION_GRANTED);
|
|
596
|
+
// } else {
|
|
597
|
+
// emitEvent(ChatbotEventType.MIC_PERMISSION_DENIED);
|
|
598
|
+
// }
|
|
599
|
+
break;
|
|
600
|
+
default:
|
|
601
|
+
if (onMessage) onMessage(parsedData ?? {});
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
errorTracker.trackError(
|
|
605
|
+
error instanceof Error
|
|
606
|
+
? error
|
|
607
|
+
: new Error("Failed to parse WebView message"),
|
|
608
|
+
"ChatbotSDK",
|
|
609
|
+
{
|
|
610
|
+
type: ErrorTypes.RUNTIME_ERROR,
|
|
611
|
+
context: { messageData: data },
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
[
|
|
617
|
+
emitEvent,
|
|
618
|
+
emitSession,
|
|
619
|
+
isWebViewVisible,
|
|
620
|
+
onMessage,
|
|
621
|
+
systemInfo,
|
|
622
|
+
effectiveUserId,
|
|
623
|
+
user_token,
|
|
624
|
+
user_profile,
|
|
625
|
+
],
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Initialize user ID
|
|
629
|
+
useEffect(() => {
|
|
630
|
+
const initializeUserId = async () => {
|
|
631
|
+
let userId = user_id ? String(user_id) : undefined;
|
|
632
|
+
|
|
633
|
+
if (!userId) {
|
|
634
|
+
// Try memory cache first
|
|
635
|
+
userId = memoryCache.current["rblyn_anon"];
|
|
636
|
+
|
|
637
|
+
if (!userId) {
|
|
638
|
+
userId = generateUUID();
|
|
639
|
+
executeStorageOp(() => {
|
|
640
|
+
webViewRef.current?.injectJavaScript(
|
|
641
|
+
createStorageMessage(
|
|
642
|
+
StorageMessageType.SET_STORAGE,
|
|
643
|
+
"rblyn_anon",
|
|
644
|
+
userId!,
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
setEffectiveUserId(userId);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
initializeUserId();
|
|
655
|
+
}, [user_id, executeStorageOp]);
|
|
656
|
+
|
|
657
|
+
// Removed controlled isOpen syncing to avoid desync issues. Using ref API instead.
|
|
658
|
+
|
|
659
|
+
// Initialize WebView storage
|
|
660
|
+
useEffect(() => {
|
|
661
|
+
if (webViewRef.current) {
|
|
662
|
+
webViewRef.current.injectJavaScript(getStorageScript());
|
|
663
|
+
// Try to get stored ID once storage is ready
|
|
664
|
+
executeStorageOp(() => {
|
|
665
|
+
webViewRef.current?.injectJavaScript(
|
|
666
|
+
createStorageMessage(StorageMessageType.GET_STORAGE, "rblyn_anon"),
|
|
667
|
+
);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}, []);
|
|
671
|
+
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
const fetchChatbotConfig = async () => {
|
|
674
|
+
try {
|
|
675
|
+
const endpointUrl = `${API_URL}/chat/chatbot/get/`;
|
|
676
|
+
const payload = {
|
|
677
|
+
client_user_id: effectiveUserId,
|
|
678
|
+
// Read from the ref rather than the prop so that a later change
|
|
679
|
+
// does not re-trigger the config fetch. Consistent with session_id
|
|
680
|
+
// being an initialization-time input. JSON.stringify drops the key
|
|
681
|
+
// when there is nothing to resume.
|
|
682
|
+
resumable_session_id: resumeSessionRef.current || undefined,
|
|
683
|
+
// See the registerUserId payload — omitted when false.
|
|
684
|
+
discrete: discreteRef.current ? true : undefined,
|
|
685
|
+
org_id: api_key,
|
|
686
|
+
token: user_token,
|
|
687
|
+
extra_info: {},
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// setToastMessage(endpointUrl + " : " + BASE_CHATBOT_URL);
|
|
691
|
+
// setToastVisible(true);
|
|
692
|
+
|
|
693
|
+
const response = await fetch(endpointUrl, {
|
|
694
|
+
method: "POST",
|
|
695
|
+
headers: {
|
|
696
|
+
"Content-Type": "application/json",
|
|
697
|
+
},
|
|
698
|
+
body: JSON.stringify(payload),
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
if (!response.ok) {
|
|
702
|
+
throw new Error("Failed to fetch chatbot configuration");
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const data = await response.json();
|
|
706
|
+
const orgInfo = data?.user?.org_info;
|
|
707
|
+
|
|
708
|
+
if (orgInfo) {
|
|
709
|
+
const config: ChatbotConfig = {
|
|
710
|
+
brand_colour: orgInfo.brand_config?.colors?.brand_color || "",
|
|
711
|
+
name: orgInfo.brand_config?.name || "",
|
|
712
|
+
image_url: orgInfo.brand_config?.launcher_logo_url || "",
|
|
713
|
+
chat_interface_config: {
|
|
714
|
+
chat_bubble_prompts: [],
|
|
715
|
+
display_name: orgInfo.brand_config?.display_name || "",
|
|
716
|
+
welcome_message:
|
|
717
|
+
orgInfo.brand_config?.welcome_message ||
|
|
718
|
+
"Hey! What can we help you with today?",
|
|
719
|
+
redirect_url: orgInfo.brand_config?.redirect_url || "",
|
|
720
|
+
},
|
|
721
|
+
interface_properties: {
|
|
722
|
+
position:
|
|
723
|
+
orgInfo.brand_config?.interface_properties?.position ||
|
|
724
|
+
WidgetPositionEnums.RIGHT,
|
|
725
|
+
side_spacing:
|
|
726
|
+
orgInfo.brand_config?.interface_properties?.side_spacing ??
|
|
727
|
+
20,
|
|
728
|
+
bottom_spacing:
|
|
729
|
+
orgInfo.brand_config?.interface_properties?.bottom_spacing ??
|
|
730
|
+
20,
|
|
731
|
+
},
|
|
732
|
+
interface_type:
|
|
733
|
+
orgInfo.brand_config?.interface_type ||
|
|
734
|
+
ChatbotInterfaceType.WIDGET,
|
|
735
|
+
launcher_type:
|
|
736
|
+
orgInfo.brand_config?.launcher_type || LauncherType.IMAGE,
|
|
737
|
+
launcher_properties: {
|
|
738
|
+
text: orgInfo.brand_config?.launcher_properties?.text || "",
|
|
739
|
+
},
|
|
740
|
+
images: {
|
|
741
|
+
launcher_image_url: {
|
|
742
|
+
url:
|
|
743
|
+
orgInfo.brand_config?.images?.launcher_image_url?.url ||
|
|
744
|
+
DEFAULT_LAUNCHER_IMAGE,
|
|
745
|
+
},
|
|
746
|
+
header_image_url: {
|
|
747
|
+
url: orgInfo.brand_config?.images?.header_image_url?.url || "",
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
footer: {
|
|
751
|
+
// Left uncoerced so an org that never set it stays undefined
|
|
752
|
+
// and the init_load_powered_by key is dropped rather than
|
|
753
|
+
// asserting a false the backend never stated.
|
|
754
|
+
show_powered_by: orgInfo.brand_config?.footer?.show_powered_by,
|
|
755
|
+
},
|
|
756
|
+
chat_iframe_url: orgInfo.brand_config?.chat_iframe_url || "",
|
|
757
|
+
};
|
|
758
|
+
setChatbotConfig(config);
|
|
759
|
+
}
|
|
760
|
+
} catch (error) {
|
|
761
|
+
errorTracker.trackError(
|
|
762
|
+
error instanceof Error
|
|
763
|
+
? error
|
|
764
|
+
: new Error("Failed to fetch chatbot configuration"),
|
|
765
|
+
"ChatbotSDK",
|
|
766
|
+
{
|
|
767
|
+
type: ErrorTypes.NETWORK_ERROR,
|
|
768
|
+
context: { api_key, user_id },
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
} finally {
|
|
772
|
+
setLoading(false);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
if (api_key && effectiveUserId) {
|
|
777
|
+
fetchChatbotConfig();
|
|
778
|
+
}
|
|
779
|
+
}, [api_key, effectiveUserId, user_token]);
|
|
780
|
+
|
|
781
|
+
function constructUrl(): string {
|
|
782
|
+
if (!effectiveUserId) return "";
|
|
783
|
+
const params = new URLSearchParams({
|
|
784
|
+
id: api_key,
|
|
785
|
+
});
|
|
786
|
+
// const iFrameUrlFromBackend = chatbotConfig?.chat_iframe_url;
|
|
787
|
+
// if (iFrameUrlFromBackend) {
|
|
788
|
+
// return `${iFrameUrlFromBackend}`;
|
|
789
|
+
// }
|
|
790
|
+
return `${BASE_CHATBOT_URL}?${params.toString()}`;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Handle opening the chatbot
|
|
794
|
+
const openWebView = useCallback(() => {
|
|
795
|
+
// Idempotent. Integrations that worked around open()-inside-onReady not
|
|
796
|
+
// firing may now have both that call and their workaround run; without
|
|
797
|
+
// this guard the second one would replay the animation and emit a
|
|
798
|
+
// duplicate CHATBOT_OPENED.
|
|
799
|
+
if (isWebViewVisibleRef.current) return;
|
|
800
|
+
|
|
801
|
+
isWebViewVisibleRef.current = true;
|
|
802
|
+
setIsWebViewVisible(true);
|
|
803
|
+
if (enableAnimation) {
|
|
804
|
+
animateWebViewOpen(animatedValues, () => {
|
|
805
|
+
externalOnOpen?.();
|
|
806
|
+
emitEvent(ChatbotEventType.CHATBOT_OPENED);
|
|
807
|
+
if (!isFirstLoadRef.current) {
|
|
808
|
+
setTimeout(triggerAppInit, 100);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
} else {
|
|
812
|
+
requestAnimationFrame(() => {
|
|
813
|
+
externalOnOpen?.();
|
|
814
|
+
emitEvent(ChatbotEventType.CHATBOT_OPENED);
|
|
815
|
+
if (!isFirstLoadRef.current) {
|
|
816
|
+
setTimeout(triggerAppInit, 100);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}, [enableAnimation, animatedValues, externalOnOpen, emitEvent]);
|
|
821
|
+
|
|
822
|
+
// Handle closing the chatbot
|
|
823
|
+
const closeWebView = useCallback(() => {
|
|
824
|
+
if (webViewRef.current) {
|
|
825
|
+
webViewRef.current.postMessage(
|
|
826
|
+
JSON.stringify({
|
|
827
|
+
name: "closeFrame",
|
|
828
|
+
domain: "app-domain.com",
|
|
829
|
+
}),
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// The ref mirrors each state write at the point it happens, so it stays
|
|
834
|
+
// true for the duration of the close animation exactly as the state does.
|
|
835
|
+
if (enableAnimation) {
|
|
836
|
+
animateWebViewClose(animatedValues, () => {
|
|
837
|
+
isWebViewVisibleRef.current = false;
|
|
838
|
+
setIsWebViewVisible(false);
|
|
839
|
+
externalOnClose?.();
|
|
840
|
+
emitEvent(ChatbotEventType.CHATBOT_CLOSED);
|
|
841
|
+
});
|
|
842
|
+
} else {
|
|
843
|
+
isWebViewVisibleRef.current = false;
|
|
844
|
+
setIsWebViewVisible(false);
|
|
845
|
+
externalOnClose?.();
|
|
846
|
+
emitEvent(ChatbotEventType.CHATBOT_CLOSED);
|
|
847
|
+
}
|
|
848
|
+
}, [enableAnimation, animatedValues, externalOnClose, emitEvent]);
|
|
849
|
+
|
|
850
|
+
// Emit button loaded event when component mounts
|
|
851
|
+
useEffect(() => {
|
|
852
|
+
if (show_floating_button && chatbotConfig) {
|
|
853
|
+
emitEvent(ChatbotEventType.CHATBOT_BUTTON_LOADED);
|
|
854
|
+
}
|
|
855
|
+
}, [show_floating_button, chatbotConfig, emitEvent]);
|
|
856
|
+
|
|
857
|
+
// Set initialization state when chatbot is ready
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
if (!loading && chatbotConfig && effectiveUserId && !isInitialized) {
|
|
860
|
+
// Set before onReady fires, so a client calling open() from inside it
|
|
861
|
+
// finds the handle already usable.
|
|
862
|
+
isInitializedRef.current = true;
|
|
863
|
+
setIsInitialized(true);
|
|
864
|
+
onReady?.();
|
|
865
|
+
}
|
|
866
|
+
}, [loading, chatbotConfig, effectiveUserId, isInitialized, onReady]);
|
|
867
|
+
|
|
868
|
+
const getScreenDimensions = () => {
|
|
869
|
+
const windowHeight = Dimensions.get("window").height;
|
|
870
|
+
const windowWidth = Dimensions.get("window").width;
|
|
871
|
+
const statusBarHeight =
|
|
872
|
+
Platform.OS === "ios" ? 0 : StatusBar.currentHeight || 0;
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
height: windowHeight - statusBarHeight,
|
|
876
|
+
width: windowWidth,
|
|
877
|
+
};
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
const requestAndroidMicPermission = useCallback(async () => {
|
|
881
|
+
try {
|
|
882
|
+
if (Platform.OS === "ios") {
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
// Check if already granted
|
|
886
|
+
const alreadyGranted = await PermissionsAndroid.check(
|
|
887
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
if (alreadyGranted) {
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const result = await PermissionsAndroid.request(
|
|
895
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
896
|
+
{
|
|
897
|
+
title: "Microphone permission",
|
|
898
|
+
message: "This app uses your microphone for voice features.",
|
|
899
|
+
buttonPositive: "Allow",
|
|
900
|
+
buttonNegative: "Deny",
|
|
901
|
+
},
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
const granted = result === PermissionsAndroid.RESULTS.GRANTED;
|
|
905
|
+
|
|
906
|
+
if (!granted) {
|
|
907
|
+
// Optional UX
|
|
908
|
+
Alert.alert(
|
|
909
|
+
"Microphone required",
|
|
910
|
+
"Voice features will not work unless you allow microphone access.",
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return granted;
|
|
915
|
+
} catch (err) {
|
|
916
|
+
console.warn("Error requesting mic permission", err);
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
}, []);
|
|
920
|
+
|
|
921
|
+
const screenDimensions = getScreenDimensions();
|
|
922
|
+
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
if (__DEV__) {
|
|
925
|
+
enableNetworkDebug();
|
|
926
|
+
}
|
|
927
|
+
}, []);
|
|
928
|
+
|
|
929
|
+
// Deliberate error for testing error boundary
|
|
930
|
+
// useEffect(() => {
|
|
931
|
+
// if (__DEV__) {
|
|
932
|
+
// // Set up global error handlers
|
|
933
|
+
// setupGlobalErrorHandlers();
|
|
934
|
+
|
|
935
|
+
// // Test async error
|
|
936
|
+
// setTimeout(() => {
|
|
937
|
+
// Promise.reject(new Error("Test async error"));
|
|
938
|
+
// }, 1000);
|
|
939
|
+
|
|
940
|
+
// // Test sync error
|
|
941
|
+
// setTimeout(() => {
|
|
942
|
+
// throw new Error("Test sync error");
|
|
943
|
+
// }, 2000);
|
|
944
|
+
// }
|
|
945
|
+
// }, []);
|
|
946
|
+
|
|
947
|
+
const position = chatbotConfig?.interface_properties?.position || "Right";
|
|
948
|
+
const sideSpacing = chatbotConfig?.interface_properties?.side_spacing ?? 20;
|
|
949
|
+
const bottomSpacing =
|
|
950
|
+
chatbotConfig?.interface_properties?.bottom_spacing ?? 20;
|
|
951
|
+
|
|
952
|
+
// const mainContainerStyle = useMemo(() => {
|
|
953
|
+
// return {
|
|
954
|
+
// ...styles.mainContainer,
|
|
955
|
+
// [position.toLowerCase()]: `${sideSpacing ?? 20}px`,
|
|
956
|
+
// alignItems:
|
|
957
|
+
// position === "Right" ? "flex-end" : ("flex-start" as FlexAlignType),
|
|
958
|
+
// // Explicitly clear mobile-specific styles
|
|
959
|
+
// top: undefined,
|
|
960
|
+
// [position === "Right" ? "left" : "right"]: undefined,
|
|
961
|
+
// backgroundColor: "green",
|
|
962
|
+
// };
|
|
963
|
+
// }, [position, sideSpacing, styles.mainContainer]);
|
|
964
|
+
|
|
965
|
+
return (
|
|
966
|
+
<ErrorBoundary componentName="ChatbotSDK">
|
|
967
|
+
{/*
|
|
968
|
+
This root view is flex: 1 and stays mounted while the chat is closed.
|
|
969
|
+
Left hit-testable, it sits invisibly over whatever the host renders
|
|
970
|
+
behind it and swallows every tap. "box-none" exempts the container
|
|
971
|
+
itself while leaving its children interactive, which is what the
|
|
972
|
+
floating button and the dev-only debug button need. The WebView
|
|
973
|
+
wrapper below manages its own pointerEvents.
|
|
974
|
+
*/}
|
|
975
|
+
<View
|
|
976
|
+
style={styles?.mainContainer}
|
|
977
|
+
pointerEvents={isWebViewVisible ? "auto" : "box-none"}>
|
|
978
|
+
{!loading && (
|
|
979
|
+
<>
|
|
980
|
+
<ErrorBoundary componentName="FloatingButton">
|
|
981
|
+
{show_floating_button && api_key && chatbotConfig && (
|
|
982
|
+
<FloatingButton
|
|
983
|
+
chatbotConfig={chatbotConfig}
|
|
984
|
+
isWebViewVisible={isWebViewVisible}
|
|
985
|
+
onPress={() => {
|
|
986
|
+
emitEvent(ChatbotEventType.CHATBOT_BUTTON_CLICKED);
|
|
987
|
+
openWebView();
|
|
988
|
+
}}
|
|
989
|
+
brandColor={chatbotConfig.brand_colour}
|
|
990
|
+
imageUrl={chatbotConfig.image_url}
|
|
991
|
+
/>
|
|
992
|
+
)}
|
|
993
|
+
</ErrorBoundary>
|
|
994
|
+
<View
|
|
995
|
+
style={[
|
|
996
|
+
styles.webViewWrapper,
|
|
997
|
+
{ height: isWebViewVisible ? "100%" : 0 },
|
|
998
|
+
]}
|
|
999
|
+
pointerEvents={isWebViewVisible ? "auto" : "none"}
|
|
1000
|
+
>
|
|
1001
|
+
<Animated.View
|
|
1002
|
+
style={[
|
|
1003
|
+
styles.fullScreenContainer,
|
|
1004
|
+
{
|
|
1005
|
+
opacity: animatedValues.opacity,
|
|
1006
|
+
transform: [
|
|
1007
|
+
{ scale: animatedValues.scale },
|
|
1008
|
+
{ translateY: animatedValues.translateY },
|
|
1009
|
+
],
|
|
1010
|
+
},
|
|
1011
|
+
]}
|
|
1012
|
+
>
|
|
1013
|
+
<ErrorBoundary componentName="ChatbotWebView">
|
|
1014
|
+
<WebView
|
|
1015
|
+
mediaCapturePermissionGrantType={"grant"}
|
|
1016
|
+
webviewDebuggingEnabled={true}
|
|
1017
|
+
ref={webViewRef}
|
|
1018
|
+
source={{
|
|
1019
|
+
uri: constructUrl(),
|
|
1020
|
+
}}
|
|
1021
|
+
onMessage={handleMessage}
|
|
1022
|
+
style={styles.webview}
|
|
1023
|
+
containerStyle={
|
|
1024
|
+
isFullScreen
|
|
1025
|
+
? {
|
|
1026
|
+
width: screenDimensions.width,
|
|
1027
|
+
height: screenDimensions.height - 40,
|
|
1028
|
+
}
|
|
1029
|
+
: undefined
|
|
1030
|
+
}
|
|
1031
|
+
allowsInlineMediaPlayback={true}
|
|
1032
|
+
mediaPlaybackRequiresUserAction={false}
|
|
1033
|
+
allowFileAccess={false}
|
|
1034
|
+
geolocationEnabled={false}
|
|
1035
|
+
javaScriptEnabled={true}
|
|
1036
|
+
domStorageEnabled={true}
|
|
1037
|
+
cacheEnabled={true}
|
|
1038
|
+
scrollEnabled={true}
|
|
1039
|
+
bounces={false}
|
|
1040
|
+
onShouldStartLoadWithRequest={(request) => {
|
|
1041
|
+
return (
|
|
1042
|
+
request.url.startsWith(BASE_CHATBOT_URL) ||
|
|
1043
|
+
request.url.startsWith(
|
|
1044
|
+
chatbotConfig?.chat_iframe_url || "",
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
}}
|
|
1048
|
+
startInLoadingState={isFirstLoadRef.current}
|
|
1049
|
+
onLoadEnd={() => {
|
|
1050
|
+
if (__DEV__) {
|
|
1051
|
+
enableWebViewDebug(webViewRef);
|
|
1052
|
+
}
|
|
1053
|
+
webViewRef.current?.injectJavaScript(`
|
|
1054
|
+
if (!document.querySelector('meta[name="viewport"]')) {
|
|
1055
|
+
var meta = document.createElement('meta');
|
|
1056
|
+
meta.name = 'viewport';
|
|
1057
|
+
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
|
1058
|
+
document.getElementsByTagName('head')[0].appendChild(meta);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (!document.querySelector('#injectedStyle')) {
|
|
1062
|
+
const style = document.createElement('style');
|
|
1063
|
+
style.id = 'injectedStyle';
|
|
1064
|
+
style.textContent = \`
|
|
1065
|
+
html, body, #root, #__next {
|
|
1066
|
+
height: 100% !important;
|
|
1067
|
+
min-height: 100% !important;
|
|
1068
|
+
overflow: hidden !important;
|
|
1069
|
+
}
|
|
1070
|
+
\`;
|
|
1071
|
+
document.head.appendChild(style);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
window.sendToChatbot = function(message) {
|
|
1075
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// Only create observer if it doesn't exist
|
|
1079
|
+
if (!window.closeButtonObserver) {
|
|
1080
|
+
// Setup mutation observer to watch for close button
|
|
1081
|
+
window.closeButtonObserver = new MutationObserver(function(mutations) {
|
|
1082
|
+
const closeButton = document.querySelector('.chatbot-close-button');
|
|
1083
|
+
if (closeButton && !closeButton.hasAttribute('listener-attached')) {
|
|
1084
|
+
closeButton.setAttribute('listener-attached', 'true');
|
|
1085
|
+
closeButton.addEventListener('click', () => {
|
|
1086
|
+
try {
|
|
1087
|
+
const message = { type: 'close_chatbot' };
|
|
1088
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
console.error('Error sending close message:', error);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
console.log('Close button listener attached');
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// Start observing
|
|
1098
|
+
window.closeButtonObserver.observe(document.body, {
|
|
1099
|
+
childList: true,
|
|
1100
|
+
subtree: true
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Initial check for existing button
|
|
1104
|
+
const existingButton = document.querySelector('.chatbot-close-button');
|
|
1105
|
+
if (existingButton && !existingButton.hasAttribute('listener-attached')) {
|
|
1106
|
+
existingButton.setAttribute('listener-attached', 'true');
|
|
1107
|
+
existingButton.addEventListener('click', () => {
|
|
1108
|
+
try {
|
|
1109
|
+
const message = { type: 'close_chatbot' };
|
|
1110
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
console.error('Error sending close message:', error);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
console.log('Close button listener attached to existing button');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
true;
|
|
1120
|
+
`);
|
|
1121
|
+
isFirstLoadRef.current = false;
|
|
1122
|
+
}}
|
|
1123
|
+
onError={(syntheticEvent) => {
|
|
1124
|
+
const { nativeEvent } = syntheticEvent;
|
|
1125
|
+
errorTracker.trackError(
|
|
1126
|
+
new Error(nativeEvent.description),
|
|
1127
|
+
"ChatbotWebView",
|
|
1128
|
+
{
|
|
1129
|
+
type: ErrorTypes.RUNTIME_ERROR,
|
|
1130
|
+
context: {
|
|
1131
|
+
url: nativeEvent.url,
|
|
1132
|
+
code: nativeEvent.code,
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
);
|
|
1136
|
+
}}
|
|
1137
|
+
/>
|
|
1138
|
+
</ErrorBoundary>
|
|
1139
|
+
</Animated.View>
|
|
1140
|
+
</View>
|
|
1141
|
+
</>
|
|
1142
|
+
)}
|
|
1143
|
+
{/* <Toast message={toastMessage} visible={toastVisible} duration={10000} /> */}
|
|
1144
|
+
{__DEV__ && <DebugButton />}
|
|
1145
|
+
</View>
|
|
1146
|
+
</ErrorBoundary>
|
|
1147
|
+
);
|
|
1148
|
+
},
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1151
|
+
Chatbot.displayName = "Chatbot";
|
|
1152
|
+
|
|
1153
|
+
const styles = StyleSheet.create({
|
|
1154
|
+
mainContainer: {
|
|
1155
|
+
flex: 1,
|
|
1156
|
+
position: "relative",
|
|
1157
|
+
overflow: "visible",
|
|
1158
|
+
},
|
|
1159
|
+
webViewWrapper: {
|
|
1160
|
+
position: "absolute",
|
|
1161
|
+
top: 0,
|
|
1162
|
+
left: 0,
|
|
1163
|
+
right: 0,
|
|
1164
|
+
bottom: 0,
|
|
1165
|
+
overflow: "visible",
|
|
1166
|
+
zIndex: 10000,
|
|
1167
|
+
backgroundColor: "#ffffff",
|
|
1168
|
+
},
|
|
1169
|
+
fullScreenContainer: {
|
|
1170
|
+
position: "absolute",
|
|
1171
|
+
top: 0,
|
|
1172
|
+
left: 0,
|
|
1173
|
+
right: 0,
|
|
1174
|
+
bottom: 0,
|
|
1175
|
+
backgroundColor: "white",
|
|
1176
|
+
zIndex: 10000,
|
|
1177
|
+
overflow: "visible",
|
|
1178
|
+
// height: Platform.OS === "android" ? "100%" : "100%",
|
|
1179
|
+
},
|
|
1180
|
+
inlineContainer: {
|
|
1181
|
+
flex: 1,
|
|
1182
|
+
backgroundColor: "white",
|
|
1183
|
+
},
|
|
1184
|
+
webview: {
|
|
1185
|
+
flex: 1,
|
|
1186
|
+
backgroundColor: "white",
|
|
1187
|
+
zIndex: 10000,
|
|
1188
|
+
},
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
export default Chatbot;
|