@robylon/react-native-sdk 2.2.1 → 2.2.3-staging.0

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