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

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