@robylon/react-native-sdk 2.0.30 → 2.0.32-staging.4

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 (81) hide show
  1. package/lib/commonjs/Chatbotsdk.js +3 -3
  2. package/lib/commonjs/Chatbotsdk.js.map +1 -1
  3. package/lib/commonjs/FloatingButton/launchers/ImageLauncher.js +1 -1
  4. package/lib/commonjs/FloatingButton/launchers/ImageLauncher.js.map +1 -1
  5. package/lib/commonjs/FloatingButton/launchers/TextLauncher.js +1 -1
  6. package/lib/commonjs/FloatingButton/launchers/TextLauncher.js.map +1 -1
  7. package/lib/commonjs/FloatingButton/launchers/TextualImageLauncher.js +1 -1
  8. package/lib/commonjs/FloatingButton/launchers/TextualImageLauncher.js.map +1 -1
  9. package/lib/commonjs/FloatingButton.js +1 -1
  10. package/lib/commonjs/FloatingButton.js.map +1 -1
  11. package/lib/commonjs/LoadingIndicator.js +1 -1
  12. package/lib/commonjs/LoadingIndicator.js.map +1 -1
  13. package/lib/commonjs/Toast.js +1 -1
  14. package/lib/commonjs/Toast.js.map +1 -1
  15. package/lib/commonjs/components/DebugButton.js +1 -1
  16. package/lib/commonjs/components/DebugButton.js.map +1 -1
  17. package/lib/commonjs/config.js +1 -1
  18. package/lib/commonjs/config.js.map +1 -1
  19. package/lib/commonjs/constants.js +1 -1
  20. package/lib/commonjs/constants.js.map +1 -1
  21. package/lib/commonjs/versions/version.staging.js +1 -1
  22. package/lib/module/Chatbotsdk.js +3 -3
  23. package/lib/module/Chatbotsdk.js.map +1 -1
  24. package/lib/module/FloatingButton/launchers/ImageLauncher.js +1 -1
  25. package/lib/module/FloatingButton/launchers/ImageLauncher.js.map +1 -1
  26. package/lib/module/FloatingButton/launchers/TextLauncher.js +1 -1
  27. package/lib/module/FloatingButton/launchers/TextLauncher.js.map +1 -1
  28. package/lib/module/FloatingButton/launchers/TextualImageLauncher.js +1 -1
  29. package/lib/module/FloatingButton/launchers/TextualImageLauncher.js.map +1 -1
  30. package/lib/module/FloatingButton.js +1 -1
  31. package/lib/module/FloatingButton.js.map +1 -1
  32. package/lib/module/LoadingIndicator.js +1 -1
  33. package/lib/module/LoadingIndicator.js.map +1 -1
  34. package/lib/module/Toast.js +1 -1
  35. package/lib/module/Toast.js.map +1 -1
  36. package/lib/module/components/DebugButton.js +1 -1
  37. package/lib/module/components/DebugButton.js.map +1 -1
  38. package/lib/module/config.js +1 -1
  39. package/lib/module/config.js.map +1 -1
  40. package/lib/module/constants.js +1 -1
  41. package/lib/module/constants.js.map +1 -1
  42. package/lib/module/versions/version.staging.js +1 -1
  43. package/lib/typescript/Chatbotsdk.d.ts.map +1 -1
  44. package/lib/typescript/versions/version.staging.d.ts +1 -1
  45. package/package.json +1 -1
  46. package/src/Chatbotsdk.tsx +979 -0
  47. package/src/FloatingButton/launchers/ImageLauncher.tsx +75 -0
  48. package/src/FloatingButton/launchers/TextLauncher.tsx +84 -0
  49. package/src/FloatingButton/launchers/TextualImageLauncher.tsx +133 -0
  50. package/src/FloatingButton.tsx +97 -0
  51. package/src/LoadingIndicator.tsx +11 -0
  52. package/src/Toast.tsx +65 -0
  53. package/src/components/DebugButton.tsx +46 -0
  54. package/src/components/ErrorBoundary.tsx +38 -0
  55. package/src/config.ts +4 -0
  56. package/src/constants/errorConstants.ts +21 -0
  57. package/src/constants/fontStyles.ts +3 -0
  58. package/src/constants.ts +5 -0
  59. package/src/global.d.ts +11 -0
  60. package/src/hooks/useChatbotEvents.ts +93 -0
  61. package/src/index.tsx +10 -0
  62. package/src/openChatbot.ts +11 -0
  63. package/src/openChatbot.tsx +0 -0
  64. package/src/services/ErrorTrackingService.ts +217 -0
  65. package/src/types/events.ts +28 -0
  66. package/src/types/react-native-flipper-performance-plugin.d.ts +3 -0
  67. package/src/types/react-native-globals.d.ts +10 -0
  68. package/src/utils/animations.ts +120 -0
  69. package/src/utils/colorUtils.ts +35 -0
  70. package/src/utils/cookieUtils.ts +7 -0
  71. package/src/utils/debugConfig.ts +56 -0
  72. package/src/utils/debugMenu.ts +14 -0
  73. package/src/utils/errorHandler.ts +58 -0
  74. package/src/utils/fileDownload.ts +70 -0
  75. package/src/utils/logger.ts +14 -0
  76. package/src/utils/systemInfo.ts +110 -0
  77. package/src/utils/webViewStorage.ts +62 -0
  78. package/src/version.ts +2 -0
  79. package/src/versions/version.dev.ts +2 -0
  80. package/src/versions/version.production.ts +2 -0
  81. package/src/versions/version.staging.ts +2 -0
@@ -0,0 +1,979 @@
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
+
53
+ export interface ChatbotProps {
54
+ api_key: string;
55
+ user_id?: string | null | number;
56
+ user_token?: string;
57
+ user_profile?: {
58
+ email?: string;
59
+ name?: string;
60
+ mobile?: string | number;
61
+ is_test_user?: boolean;
62
+ [key: string | number]: any;
63
+ };
64
+ onEvent?: ChatbotEventHandler;
65
+ onOpen?: () => void;
66
+ onClose?: () => void;
67
+ onReady?: () => void;
68
+ }
69
+
70
+ export interface ChatbotRef {
71
+ open: () => void;
72
+ close: () => void;
73
+ toggle: () => void;
74
+ isReady: () => boolean;
75
+ }
76
+
77
+ interface InternalChatbotProps extends ChatbotProps {
78
+ show_floating_button?: boolean;
79
+ onMessage?: (message: any) => void;
80
+ isFullScreen?: boolean;
81
+ enableAnimation?: boolean;
82
+ }
83
+
84
+ interface ChatbotMessage {
85
+ type: string;
86
+ data?: any;
87
+ level?: string;
88
+ url?: string;
89
+ filename?: string;
90
+ }
91
+
92
+ const getOriginFromUrl = (url?: string): string | null => {
93
+ if (!url) return null;
94
+ try {
95
+ return new URL(url)?.origin ?? null;
96
+ } catch {
97
+ return null;
98
+ }
99
+ };
100
+
101
+ const warmupHttpOrigin = async (origin?: string | null) => {
102
+ if (!origin) return;
103
+ try {
104
+ await fetch(origin, { method: "HEAD" });
105
+ } catch {
106
+ try {
107
+ await fetch(origin, { method: "GET" });
108
+ } catch {
109
+ // Network warmup should be best-effort and non-blocking.
110
+ }
111
+ }
112
+ };
113
+
114
+ export enum ChatbotInterfaceType {
115
+ WIDGET = "WIDGET",
116
+ POPOVER = "POPOVER",
117
+ EMBED = "EMBED",
118
+ }
119
+
120
+ export enum WidgetPositionEnums {
121
+ RIGHT = "Right",
122
+ LEFT = "Left",
123
+ }
124
+
125
+ export interface WidgetInterfaceProperties {
126
+ position: WidgetPositionEnums;
127
+ side_spacing?: number;
128
+ bottom_spacing?: number;
129
+ }
130
+
131
+ export enum LauncherType {
132
+ TEXT = "TEXT",
133
+ IMAGE = "IMAGE",
134
+ TEXTUAL_IMAGE = "TEXTUAL_IMAGE",
135
+ }
136
+
137
+ export interface ChatbotConfig {
138
+ brand_colour: string;
139
+ image_url?: string;
140
+ chat_interface_config?: {
141
+ chat_bubble_prompts?: string[];
142
+ display_name?: string;
143
+ welcome_message?: string;
144
+ redirect_url?: string;
145
+ };
146
+ interface_type?: ChatbotInterfaceType;
147
+ interface_properties?: WidgetInterfaceProperties;
148
+ launcher_type: LauncherType;
149
+ launcher_properties: {
150
+ text?: string;
151
+ };
152
+ images: {
153
+ launcher_image_url?: {
154
+ url: string;
155
+ };
156
+ };
157
+ chat_iframe_url: string;
158
+ }
159
+
160
+ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
161
+ (
162
+ {
163
+ api_key,
164
+ user_id,
165
+ user_token,
166
+ show_floating_button = true,
167
+ onEvent,
168
+ onMessage,
169
+ isFullScreen = true,
170
+ enableAnimation = true,
171
+ onOpen: externalOnOpen,
172
+ onClose: externalOnClose,
173
+ onReady,
174
+ user_profile,
175
+ },
176
+ ref
177
+ ) => {
178
+ const [isWebViewVisible, setIsWebViewVisible] = useState(false);
179
+ const [chatbotConfig, setChatbotConfig] = useState<ChatbotConfig | null>(
180
+ null
181
+ );
182
+ const [loading, setLoading] = useState(true);
183
+ const [isInitialized, setIsInitialized] = useState(false);
184
+ const webViewRef = useRef<WebView>(null);
185
+ const isFirstLoadRef = useRef(true);
186
+ const [toastVisible, setToastVisible] = useState(false);
187
+ const [toastMessage, setToastMessage] = useState("");
188
+ const [effectiveUserId, setEffectiveUserId] = useState<string>();
189
+ const [isStorageReady, setIsStorageReady] = useState(false);
190
+ const pendingStorageOps = useRef<Array<() => void>>([]);
191
+ const memoryCache = useRef<Record<string, string>>({});
192
+ const [systemInfo, setSystemInfo] = useState({ os: "", browser: "" });
193
+ const systemInfoRef = useRef({ os: "", browser: "" });
194
+
195
+ // Initialize animated values
196
+ const animatedValues = useRef({
197
+ scale: new Animated.Value(1),
198
+ translateY: new Animated.Value(0),
199
+ opacity: new Animated.Value(1),
200
+ }).current;
201
+
202
+ // Create chatbotConfig object for events
203
+ const chatbotConfigForEvents = useMemo(
204
+ () => ({
205
+ userId: effectiveUserId,
206
+ isAnonymous: !user_id && user_id !== 0,
207
+ // Add other config properties as needed
208
+ }),
209
+ [effectiveUserId, user_id]
210
+ );
211
+
212
+ // Initialize event handling
213
+ const { emitEvent, onInternalEvent } = useChatbotEvents({
214
+ api_key,
215
+ chatbotConfig: chatbotConfigForEvents ?? {},
216
+ user_profile: user_profile,
217
+ onEvent: onEvent,
218
+ systemInfo: systemInfo ?? { os: "", browser: "" },
219
+ });
220
+
221
+ // Expose ref methods
222
+ useImperativeHandle(
223
+ ref,
224
+ () => ({
225
+ open: () => {
226
+ if (isInitialized) {
227
+ openWebView();
228
+ }
229
+ },
230
+ close: () => {
231
+ if (isInitialized) {
232
+ closeWebView();
233
+ }
234
+ },
235
+ toggle: () => {
236
+ if (isInitialized) {
237
+ if (isWebViewVisible) {
238
+ closeWebView();
239
+ } else {
240
+ openWebView();
241
+ }
242
+ }
243
+ },
244
+ isReady: () => isInitialized,
245
+ }),
246
+ [isInitialized, isWebViewVisible]
247
+ );
248
+
249
+ const triggerAppInit = () => {
250
+ if (webViewRef.current) {
251
+ webViewRef.current?.injectJavaScript(getBrowserAndOSInfoScript());
252
+ webViewRef.current.injectJavaScript(`
253
+ // Trigger APP_READY equivalent
254
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'APP_READY' }));
255
+ true;
256
+ `);
257
+ }
258
+ };
259
+
260
+ const executeStorageOp = useCallback(
261
+ (operation: () => void) => {
262
+ if (isStorageReady) {
263
+ operation();
264
+ } else {
265
+ pendingStorageOps.current.push(operation);
266
+ }
267
+ },
268
+ [isStorageReady]
269
+ );
270
+
271
+ // Handle WebView messages
272
+ const handleMessage = useCallback(
273
+ async (event: WebViewMessageEvent) => {
274
+ const { data } = event.nativeEvent;
275
+
276
+ try {
277
+ const parsedData: ChatbotMessage = JSON.parse(data);
278
+ // Handle storage messages
279
+ if (
280
+ Object.values(StorageMessageType).includes(
281
+ parsedData.type as StorageMessageType
282
+ )
283
+ ) {
284
+ const storageMessage = parsedData as StorageMessage;
285
+ switch (storageMessage.type) {
286
+ case StorageMessageType.STORAGE_READY:
287
+ setIsStorageReady(true);
288
+ pendingStorageOps.current.forEach((op) => op());
289
+ pendingStorageOps.current = [];
290
+ // Try to get stored ID immediately after storage is ready
291
+ webViewRef.current?.injectJavaScript(
292
+ createStorageMessage(
293
+ StorageMessageType.GET_STORAGE,
294
+ "rblyn_anon"
295
+ )
296
+ );
297
+ break;
298
+ case StorageMessageType.STORAGE_RESPONSE:
299
+ if (storageMessage.key && storageMessage.value) {
300
+ memoryCache.current[storageMessage.key] =
301
+ storageMessage.value;
302
+ if (storageMessage.key === "rblyn_anon") {
303
+ setEffectiveUserId(storageMessage.value);
304
+ }
305
+ }
306
+ break;
307
+ case StorageMessageType.STORAGE_ERROR:
308
+ logger.error("WebView storage error:", storageMessage.error);
309
+ break;
310
+ }
311
+ return;
312
+ }
313
+
314
+ // Add debug logging
315
+ if (__DEV__) {
316
+ if (parsedData?.type === "CONSOLE") {
317
+ console.log(`WebView ${parsedData?.level}:`, parsedData?.data);
318
+ return;
319
+ }
320
+ if (parsedData?.type === "ERROR") {
321
+ console.error("WebView Error:", parsedData?.data);
322
+ return;
323
+ }
324
+ }
325
+
326
+ if (parsedData?.type === "SYSTEM_INFO") {
327
+ setSystemInfo(parsedData?.data ?? { os: "", browser: "" });
328
+ systemInfoRef.current = parsedData?.data;
329
+ return;
330
+ }
331
+
332
+ if (parsedData?.type === "APP_READY") {
333
+ emitEvent(ChatbotEventType.CHATBOT_APP_READY);
334
+ if (webViewRef.current && isWebViewVisible) {
335
+ webViewRef.current?.injectJavaScript(getBrowserAndOSInfoScript());
336
+
337
+ const messageData = {
338
+ name: "registerUserId",
339
+ action: "registerUserId",
340
+ data: {
341
+ userId: effectiveUserId,
342
+ token: user_token ? `${user_token}` : undefined,
343
+ userProfile: {
344
+ ...user_profile,
345
+ ...getSystemInfo(systemInfoRef?.current),
346
+ __INT_SYS_INFO__: {
347
+ ...getSystemInfo(systemInfoRef?.current),
348
+ },
349
+ },
350
+ },
351
+ };
352
+ logger.debug("messageData====>", JSON.stringify(messageData));
353
+ webViewRef.current?.injectJavaScript(`
354
+ window.postMessage(${JSON.stringify({
355
+ name: "openFrame",
356
+ domain: "app-domain.com",
357
+ })}, '*');
358
+ window.postMessage(${JSON.stringify(messageData)}, '*');
359
+ `);
360
+ }
361
+ }
362
+
363
+ // Handle other message types
364
+ switch (parsedData?.type) {
365
+ case "close_chatbot":
366
+ closeWebView();
367
+ break;
368
+ // Download file is not being used currently. This event would come from the iframe chatbot if download file problem need to be solved
369
+ case "download_file":
370
+ if (
371
+ parsedData?.data ||
372
+ (parsedData?.url && parsedData?.filename)
373
+ ) {
374
+ // Handle both formats: { data: { url, filename } } or { url, filename }
375
+ const downloadData: DownloadRequest = parsedData?.data || {
376
+ url: parsedData?.url,
377
+ filename: parsedData?.filename,
378
+ };
379
+ handleDownloadRequest(downloadData);
380
+ }
381
+ break;
382
+ case "CHATBOT_LOADED":
383
+ emitEvent(ChatbotEventType.CHATBOT_LOADED, parsedData?.data);
384
+ break;
385
+ case "CHAT_INITIALIZED":
386
+ emitEvent(ChatbotEventType.CHAT_INITIALIZED, parsedData?.data);
387
+ break;
388
+ case "SESSION_REFRESHED":
389
+ emitEvent(ChatbotEventType.SESSION_REFRESHED, parsedData?.data);
390
+ break;
391
+ case "CHAT_INITIALIZATION_FAILED":
392
+ emitEvent(
393
+ ChatbotEventType.CHAT_INITIALIZATION_FAILED,
394
+ parsedData?.data
395
+ );
396
+ break;
397
+ case "REQUEST_MIC_PERMISSION":
398
+ const granted = await requestAndroidMicPermission();
399
+ if (webViewRef.current && isWebViewVisible) {
400
+ const messageData = {
401
+ name: "requestMicPermission",
402
+ action: "requestMicPermission",
403
+ type: granted
404
+ ? "MIC_PERMISSION_GRANTED"
405
+ : "MIC_PERMISSION_DENIED",
406
+ };
407
+ logger.debug("messageData====>", JSON.stringify(messageData));
408
+ webViewRef.current?.injectJavaScript(`
409
+ window.postMessage(${JSON.stringify(messageData)}, '*');`);
410
+ }
411
+ // if (granted) {
412
+ // emitEvent(ChatbotEventType.MIC_PERMISSION_GRANTED);
413
+ // } else {
414
+ // emitEvent(ChatbotEventType.MIC_PERMISSION_DENIED);
415
+ // }
416
+ break;
417
+ default:
418
+ if (onMessage) onMessage(parsedData ?? {});
419
+ }
420
+ } catch (error) {
421
+ errorTracker.trackError(
422
+ error instanceof Error
423
+ ? error
424
+ : new Error("Failed to parse WebView message"),
425
+ "ChatbotSDK",
426
+ {
427
+ type: ErrorTypes.RUNTIME_ERROR,
428
+ context: { messageData: data },
429
+ }
430
+ );
431
+ }
432
+ },
433
+ [
434
+ emitEvent,
435
+ isWebViewVisible,
436
+ onMessage,
437
+ systemInfo,
438
+ effectiveUserId,
439
+ user_token,
440
+ user_profile,
441
+ ]
442
+ );
443
+
444
+ // Initialize user ID
445
+ useEffect(() => {
446
+ const initializeUserId = async () => {
447
+ let userId = user_id ? String(user_id) : undefined;
448
+
449
+ if (!userId) {
450
+ // Try memory cache first
451
+ userId = memoryCache.current["rblyn_anon"];
452
+
453
+ if (!userId) {
454
+ userId = generateUUID();
455
+ executeStorageOp(() => {
456
+ webViewRef.current?.injectJavaScript(
457
+ createStorageMessage(
458
+ StorageMessageType.SET_STORAGE,
459
+ "rblyn_anon",
460
+ userId!
461
+ )
462
+ );
463
+ });
464
+ }
465
+ }
466
+
467
+ setEffectiveUserId(userId);
468
+ };
469
+
470
+ initializeUserId();
471
+ }, [user_id, executeStorageOp]);
472
+
473
+ // Removed controlled isOpen syncing to avoid desync issues. Using ref API instead.
474
+
475
+ // Initialize WebView storage
476
+ useEffect(() => {
477
+ if (webViewRef.current) {
478
+ webViewRef.current.injectJavaScript(getStorageScript());
479
+ // Try to get stored ID once storage is ready
480
+ executeStorageOp(() => {
481
+ webViewRef.current?.injectJavaScript(
482
+ createStorageMessage(StorageMessageType.GET_STORAGE, "rblyn_anon")
483
+ );
484
+ });
485
+ }
486
+ }, []);
487
+
488
+ useEffect(() => {
489
+ const fetchChatbotConfig = async () => {
490
+ try {
491
+ const endpointUrl = `${API_URL}/chat/chatbot/get/`;
492
+ const payload = {
493
+ client_user_id: effectiveUserId,
494
+ org_id: api_key,
495
+ token: user_token,
496
+ extra_info: {},
497
+ };
498
+
499
+ // setToastMessage(endpointUrl + " : " + BASE_CHATBOT_URL);
500
+ // setToastVisible(true);
501
+
502
+ const response = await fetch(endpointUrl, {
503
+ method: "POST",
504
+ headers: {
505
+ "Content-Type": "application/json",
506
+ },
507
+ body: JSON.stringify(payload),
508
+ });
509
+
510
+ if (!response.ok) {
511
+ throw new Error("Failed to fetch chatbot configuration");
512
+ }
513
+
514
+ const data = await response.json();
515
+ const orgInfo = data?.user?.org_info;
516
+
517
+ if (orgInfo) {
518
+ const config: ChatbotConfig = {
519
+ brand_colour: orgInfo.brand_config?.colors?.brand_color || "",
520
+ image_url: orgInfo.brand_config?.launcher_logo_url || "",
521
+ chat_interface_config: {
522
+ chat_bubble_prompts: [],
523
+ display_name: orgInfo.brand_config?.display_name || "",
524
+ welcome_message:
525
+ orgInfo.brand_config?.welcome_message ||
526
+ "Hey! What can we help you with today?",
527
+ redirect_url: orgInfo.brand_config?.redirect_url || "",
528
+ },
529
+ interface_properties: {
530
+ position:
531
+ orgInfo.brand_config?.interface_properties?.position ||
532
+ WidgetPositionEnums.RIGHT,
533
+ side_spacing:
534
+ orgInfo.brand_config?.interface_properties?.side_spacing ??
535
+ 20,
536
+ bottom_spacing:
537
+ orgInfo.brand_config?.interface_properties?.bottom_spacing ??
538
+ 20,
539
+ },
540
+ interface_type:
541
+ orgInfo.brand_config?.interface_type ||
542
+ ChatbotInterfaceType.WIDGET,
543
+ launcher_type:
544
+ orgInfo.brand_config?.launcher_type || LauncherType.IMAGE,
545
+ launcher_properties: {
546
+ text: orgInfo.brand_config?.launcher_properties?.text || "",
547
+ },
548
+ images: {
549
+ launcher_image_url: {
550
+ url:
551
+ orgInfo.brand_config?.images?.launcher_image_url?.url ||
552
+ DEFAULT_LAUNCHER_IMAGE,
553
+ },
554
+ },
555
+ chat_iframe_url: orgInfo.brand_config?.chat_iframe_url || "",
556
+ };
557
+ setChatbotConfig(config);
558
+ }
559
+ } catch (error) {
560
+ errorTracker.trackError(
561
+ error instanceof Error
562
+ ? error
563
+ : new Error("Failed to fetch chatbot configuration"),
564
+ "ChatbotSDK",
565
+ {
566
+ type: ErrorTypes.NETWORK_ERROR,
567
+ context: { api_key, user_id },
568
+ }
569
+ );
570
+ } finally {
571
+ setLoading(false);
572
+ }
573
+ };
574
+
575
+ if (api_key && effectiveUserId) {
576
+ fetchChatbotConfig();
577
+ }
578
+ }, [api_key, effectiveUserId, user_token]);
579
+
580
+ useEffect(() => {
581
+ const warmupOrigins = async () => {
582
+ const apiOrigin = getOriginFromUrl(API_URL);
583
+ const chatbotOrigin = getOriginFromUrl(BASE_CHATBOT_URL);
584
+
585
+ await Promise.allSettled([
586
+ warmupHttpOrigin(apiOrigin),
587
+ warmupHttpOrigin(chatbotOrigin),
588
+ ]);
589
+ };
590
+
591
+ warmupOrigins();
592
+ }, []);
593
+
594
+ function constructUrl(): string {
595
+ if (!effectiveUserId) return "";
596
+ const params = new URLSearchParams({
597
+ id: api_key,
598
+ });
599
+ // const iFrameUrlFromBackend = chatbotConfig?.chat_iframe_url;
600
+ // if (iFrameUrlFromBackend) {
601
+ // return `${iFrameUrlFromBackend}`;
602
+ // }
603
+ return `${BASE_CHATBOT_URL}?${params.toString()}`;
604
+ }
605
+
606
+ // Handle opening the chatbot
607
+ const openWebView = useCallback(() => {
608
+ setIsWebViewVisible(true);
609
+ if (enableAnimation) {
610
+ animateWebViewOpen(animatedValues, () => {
611
+ externalOnOpen?.();
612
+ emitEvent(ChatbotEventType.CHATBOT_OPENED);
613
+ if (!isFirstLoadRef.current) {
614
+ setTimeout(triggerAppInit, 100);
615
+ }
616
+ });
617
+ } else {
618
+ requestAnimationFrame(() => {
619
+ externalOnOpen?.();
620
+ emitEvent(ChatbotEventType.CHATBOT_OPENED);
621
+ if (!isFirstLoadRef.current) {
622
+ setTimeout(triggerAppInit, 100);
623
+ }
624
+ });
625
+ }
626
+ }, [enableAnimation, animatedValues, externalOnOpen, emitEvent]);
627
+
628
+ // Handle closing the chatbot
629
+ const closeWebView = useCallback(() => {
630
+ if (webViewRef.current) {
631
+ webViewRef.current.postMessage(
632
+ JSON.stringify({
633
+ name: "closeFrame",
634
+ })
635
+ );
636
+ }
637
+
638
+ if (enableAnimation) {
639
+ animateWebViewClose(animatedValues, () => {
640
+ setIsWebViewVisible(false);
641
+ externalOnClose?.();
642
+ emitEvent(ChatbotEventType.CHATBOT_CLOSED);
643
+ });
644
+ } else {
645
+ setIsWebViewVisible(false);
646
+ externalOnClose?.();
647
+ emitEvent(ChatbotEventType.CHATBOT_CLOSED);
648
+ }
649
+ }, [enableAnimation, animatedValues, externalOnClose, emitEvent]);
650
+
651
+ // Emit button loaded event when component mounts
652
+ useEffect(() => {
653
+ if (show_floating_button && chatbotConfig) {
654
+ emitEvent(ChatbotEventType.CHATBOT_BUTTON_LOADED);
655
+ }
656
+ }, [show_floating_button, chatbotConfig, emitEvent]);
657
+
658
+ // Set initialization state when chatbot is ready
659
+ useEffect(() => {
660
+ if (!loading && chatbotConfig && effectiveUserId && !isInitialized) {
661
+ setIsInitialized(true);
662
+ onReady?.();
663
+ }
664
+ }, [loading, chatbotConfig, effectiveUserId, isInitialized, onReady]);
665
+
666
+ const getScreenDimensions = () => {
667
+ const windowHeight = Dimensions.get("window").height;
668
+ const windowWidth = Dimensions.get("window").width;
669
+ const statusBarHeight =
670
+ Platform.OS === "ios" ? 0 : StatusBar.currentHeight || 0;
671
+
672
+ return {
673
+ height: windowHeight - statusBarHeight,
674
+ width: windowWidth,
675
+ };
676
+ };
677
+
678
+ const requestAndroidMicPermission = useCallback(async () => {
679
+ try {
680
+ if (Platform.OS === "ios") {
681
+ return true;
682
+ }
683
+ // Check if already granted
684
+ const alreadyGranted = await PermissionsAndroid.check(
685
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
686
+ );
687
+
688
+ if (alreadyGranted) {
689
+ return true;
690
+ }
691
+
692
+ const result = await PermissionsAndroid.request(
693
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
694
+ {
695
+ title: "Microphone permission",
696
+ message: "This app uses your microphone for voice features.",
697
+ buttonPositive: "Allow",
698
+ buttonNegative: "Deny",
699
+ }
700
+ );
701
+
702
+ const granted = result === PermissionsAndroid.RESULTS.GRANTED;
703
+
704
+ if (!granted) {
705
+ // Optional UX
706
+ Alert.alert(
707
+ "Microphone required",
708
+ "Voice features will not work unless you allow microphone access."
709
+ );
710
+ }
711
+
712
+ return granted;
713
+ } catch (err) {
714
+ console.warn("Error requesting mic permission", err);
715
+ return false;
716
+ }
717
+ }, []);
718
+
719
+ const screenDimensions = getScreenDimensions();
720
+
721
+ useEffect(() => {
722
+ if (__DEV__) {
723
+ enableNetworkDebug();
724
+ }
725
+ }, []);
726
+
727
+ // Deliberate error for testing error boundary
728
+ // useEffect(() => {
729
+ // if (__DEV__) {
730
+ // // Set up global error handlers
731
+ // setupGlobalErrorHandlers();
732
+
733
+ // // Test async error
734
+ // setTimeout(() => {
735
+ // Promise.reject(new Error("Test async error"));
736
+ // }, 1000);
737
+
738
+ // // Test sync error
739
+ // setTimeout(() => {
740
+ // throw new Error("Test sync error");
741
+ // }, 2000);
742
+ // }
743
+ // }, []);
744
+
745
+ const position = chatbotConfig?.interface_properties?.position || "Right";
746
+ const sideSpacing = chatbotConfig?.interface_properties?.side_spacing ?? 20;
747
+ const bottomSpacing =
748
+ chatbotConfig?.interface_properties?.bottom_spacing ?? 20;
749
+
750
+ // const mainContainerStyle = useMemo(() => {
751
+ // return {
752
+ // ...styles.mainContainer,
753
+ // [position.toLowerCase()]: `${sideSpacing ?? 20}px`,
754
+ // alignItems:
755
+ // position === "Right" ? "flex-end" : ("flex-start" as FlexAlignType),
756
+ // // Explicitly clear mobile-specific styles
757
+ // top: undefined,
758
+ // [position === "Right" ? "left" : "right"]: undefined,
759
+ // backgroundColor: "green",
760
+ // };
761
+ // }, [position, sideSpacing, styles.mainContainer]);
762
+
763
+ return (
764
+ <ErrorBoundary componentName="ChatbotSDK">
765
+ <View style={styles?.mainContainer}>
766
+ {!loading && (
767
+ <>
768
+ <ErrorBoundary componentName="FloatingButton">
769
+ {show_floating_button && api_key && chatbotConfig && (
770
+ <FloatingButton
771
+ chatbotConfig={chatbotConfig}
772
+ isWebViewVisible={isWebViewVisible}
773
+ onPress={() => {
774
+ emitEvent(ChatbotEventType.CHATBOT_BUTTON_CLICKED);
775
+ openWebView();
776
+ }}
777
+ brandColor={chatbotConfig.brand_colour}
778
+ imageUrl={chatbotConfig.image_url}
779
+ />
780
+ )}
781
+ </ErrorBoundary>
782
+ <View
783
+ style={[
784
+ styles.webViewWrapper,
785
+ { height: isWebViewVisible ? "100%" : 0 },
786
+ ]}
787
+ pointerEvents={isWebViewVisible ? "auto" : "none"}
788
+ >
789
+ <Animated.View
790
+ style={[
791
+ styles.fullScreenContainer,
792
+ {
793
+ opacity: animatedValues.opacity,
794
+ transform: [
795
+ { scale: animatedValues.scale },
796
+ { translateY: animatedValues.translateY },
797
+ ],
798
+ },
799
+ ]}
800
+ >
801
+ <ErrorBoundary componentName="ChatbotWebView">
802
+ <WebView
803
+ mediaCapturePermissionGrantType={"grant"}
804
+ webviewDebuggingEnabled={true}
805
+ ref={webViewRef}
806
+ source={{
807
+ uri: constructUrl(),
808
+ }}
809
+ onMessage={handleMessage}
810
+ style={styles.webview}
811
+ containerStyle={
812
+ isFullScreen
813
+ ? {
814
+ width: screenDimensions.width,
815
+ height: screenDimensions.height - 40,
816
+ }
817
+ : undefined
818
+ }
819
+ allowsInlineMediaPlayback={true}
820
+ mediaPlaybackRequiresUserAction={false}
821
+ allowFileAccess={false}
822
+ geolocationEnabled={false}
823
+ javaScriptEnabled={true}
824
+ domStorageEnabled={true}
825
+ cacheEnabled={true}
826
+ scrollEnabled={true}
827
+ bounces={false}
828
+ onShouldStartLoadWithRequest={(request) => {
829
+ return (
830
+ request.url.startsWith(BASE_CHATBOT_URL) ||
831
+ request.url.startsWith(
832
+ chatbotConfig?.chat_iframe_url || ""
833
+ )
834
+ );
835
+ }}
836
+ startInLoadingState={isFirstLoadRef.current}
837
+ onLoadEnd={() => {
838
+ if (__DEV__) {
839
+ enableWebViewDebug(webViewRef);
840
+ }
841
+ webViewRef.current?.injectJavaScript(`
842
+ if (!document.querySelector('meta[name="viewport"]')) {
843
+ var meta = document.createElement('meta');
844
+ meta.name = 'viewport';
845
+ meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
846
+ document.getElementsByTagName('head')[0].appendChild(meta);
847
+ }
848
+
849
+ if (!document.querySelector('#injectedStyle')) {
850
+ const style = document.createElement('style');
851
+ style.id = 'injectedStyle';
852
+ style.textContent = \`
853
+ html, body, #root, #__next {
854
+ height: 100% !important;
855
+ min-height: 100% !important;
856
+ overflow: hidden !important;
857
+ }
858
+ \`;
859
+ document.head.appendChild(style);
860
+ }
861
+
862
+ window.sendToChatbot = function(message) {
863
+ window.ReactNativeWebView.postMessage(JSON.stringify(message));
864
+ };
865
+
866
+ // Only create observer if it doesn't exist
867
+ if (!window.closeButtonObserver) {
868
+ // Setup mutation observer to watch for close button
869
+ window.closeButtonObserver = new MutationObserver(function(mutations) {
870
+ const closeButton = document.querySelector('.chatbot-close-button');
871
+ if (closeButton && !closeButton.hasAttribute('listener-attached')) {
872
+ closeButton.setAttribute('listener-attached', 'true');
873
+ closeButton.addEventListener('click', () => {
874
+ try {
875
+ const message = { type: 'close_chatbot' };
876
+ window.ReactNativeWebView.postMessage(JSON.stringify(message));
877
+ } catch (error) {
878
+ console.error('Error sending close message:', error);
879
+ }
880
+ });
881
+ console.log('Close button listener attached');
882
+ }
883
+ });
884
+
885
+ // Start observing
886
+ window.closeButtonObserver.observe(document.body, {
887
+ childList: true,
888
+ subtree: true
889
+ });
890
+
891
+ // Initial check for existing button
892
+ const existingButton = document.querySelector('.chatbot-close-button');
893
+ if (existingButton && !existingButton.hasAttribute('listener-attached')) {
894
+ existingButton.setAttribute('listener-attached', 'true');
895
+ existingButton.addEventListener('click', () => {
896
+ try {
897
+ const message = { type: 'close_chatbot' };
898
+ window.ReactNativeWebView.postMessage(JSON.stringify(message));
899
+ } catch (error) {
900
+ console.error('Error sending close message:', error);
901
+ }
902
+ });
903
+ console.log('Close button listener attached to existing button');
904
+ }
905
+ }
906
+
907
+ true;
908
+ `);
909
+ isFirstLoadRef.current = false;
910
+ }}
911
+ onError={(syntheticEvent) => {
912
+ const { nativeEvent } = syntheticEvent;
913
+ errorTracker.trackError(
914
+ new Error(nativeEvent.description),
915
+ "ChatbotWebView",
916
+ {
917
+ type: ErrorTypes.RUNTIME_ERROR,
918
+ context: {
919
+ url: nativeEvent.url,
920
+ code: nativeEvent.code,
921
+ },
922
+ }
923
+ );
924
+ }}
925
+ />
926
+ </ErrorBoundary>
927
+ </Animated.View>
928
+ </View>
929
+ </>
930
+ )}
931
+ {/* <Toast message={toastMessage} visible={toastVisible} duration={10000} /> */}
932
+ {__DEV__ && <DebugButton />}
933
+ </View>
934
+ </ErrorBoundary>
935
+ );
936
+ }
937
+ );
938
+
939
+ Chatbot.displayName = "Chatbot";
940
+
941
+ const styles = StyleSheet.create({
942
+ mainContainer: {
943
+ flex: 1,
944
+ position: "relative",
945
+ overflow: "visible",
946
+ },
947
+ webViewWrapper: {
948
+ position: "absolute",
949
+ top: 0,
950
+ left: 0,
951
+ right: 0,
952
+ bottom: 0,
953
+ overflow: "visible",
954
+ zIndex: 10000,
955
+ backgroundColor: "#ffffff",
956
+ },
957
+ fullScreenContainer: {
958
+ position: "absolute",
959
+ top: 0,
960
+ left: 0,
961
+ right: 0,
962
+ bottom: 0,
963
+ backgroundColor: "white",
964
+ zIndex: 10000,
965
+ overflow: "visible",
966
+ // height: Platform.OS === "android" ? "100%" : "100%",
967
+ },
968
+ inlineContainer: {
969
+ flex: 1,
970
+ backgroundColor: "white",
971
+ },
972
+ webview: {
973
+ flex: 1,
974
+ backgroundColor: "white",
975
+ zIndex: 10000,
976
+ },
977
+ });
978
+
979
+ export default Chatbot;