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