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

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.
@@ -87,30 +87,9 @@ interface ChatbotMessage {
87
87
  level?: string;
88
88
  url?: string;
89
89
  filename?: string;
90
+ inPlace?: boolean;
90
91
  }
91
92
 
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
93
  export enum ChatbotInterfaceType {
115
94
  WIDGET = "WIDGET",
116
95
  POPOVER = "POPOVER",
@@ -173,11 +152,11 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
173
152
  onReady,
174
153
  user_profile,
175
154
  },
176
- ref
155
+ ref,
177
156
  ) => {
178
157
  const [isWebViewVisible, setIsWebViewVisible] = useState(false);
179
158
  const [chatbotConfig, setChatbotConfig] = useState<ChatbotConfig | null>(
180
- null
159
+ null,
181
160
  );
182
161
  const [loading, setLoading] = useState(true);
183
162
  const [isInitialized, setIsInitialized] = useState(false);
@@ -206,7 +185,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
206
185
  isAnonymous: !user_id && user_id !== 0,
207
186
  // Add other config properties as needed
208
187
  }),
209
- [effectiveUserId, user_id]
188
+ [effectiveUserId, user_id],
210
189
  );
211
190
 
212
191
  // Initialize event handling
@@ -243,7 +222,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
243
222
  },
244
223
  isReady: () => isInitialized,
245
224
  }),
246
- [isInitialized, isWebViewVisible]
225
+ [isInitialized, isWebViewVisible],
247
226
  );
248
227
 
249
228
  const triggerAppInit = () => {
@@ -265,7 +244,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
265
244
  pendingStorageOps.current.push(operation);
266
245
  }
267
246
  },
268
- [isStorageReady]
247
+ [isStorageReady],
269
248
  );
270
249
 
271
250
  // Handle WebView messages
@@ -278,7 +257,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
278
257
  // Handle storage messages
279
258
  if (
280
259
  Object.values(StorageMessageType).includes(
281
- parsedData.type as StorageMessageType
260
+ parsedData.type as StorageMessageType,
282
261
  )
283
262
  ) {
284
263
  const storageMessage = parsedData as StorageMessage;
@@ -291,8 +270,8 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
291
270
  webViewRef.current?.injectJavaScript(
292
271
  createStorageMessage(
293
272
  StorageMessageType.GET_STORAGE,
294
- "rblyn_anon"
295
- )
273
+ "rblyn_anon",
274
+ ),
296
275
  );
297
276
  break;
298
277
  case StorageMessageType.STORAGE_RESPONSE:
@@ -375,6 +354,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
375
354
  const downloadData: DownloadRequest = parsedData?.data || {
376
355
  url: parsedData?.url,
377
356
  filename: parsedData?.filename,
357
+ inPlace: parsedData?.inPlace,
378
358
  };
379
359
  handleDownloadRequest(downloadData);
380
360
  }
@@ -391,7 +371,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
391
371
  case "CHAT_INITIALIZATION_FAILED":
392
372
  emitEvent(
393
373
  ChatbotEventType.CHAT_INITIALIZATION_FAILED,
394
- parsedData?.data
374
+ parsedData?.data,
395
375
  );
396
376
  break;
397
377
  case "REQUEST_MIC_PERMISSION":
@@ -404,7 +384,6 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
404
384
  ? "MIC_PERMISSION_GRANTED"
405
385
  : "MIC_PERMISSION_DENIED",
406
386
  };
407
- logger.debug("messageData====>", JSON.stringify(messageData));
408
387
  webViewRef.current?.injectJavaScript(`
409
388
  window.postMessage(${JSON.stringify(messageData)}, '*');`);
410
389
  }
@@ -426,7 +405,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
426
405
  {
427
406
  type: ErrorTypes.RUNTIME_ERROR,
428
407
  context: { messageData: data },
429
- }
408
+ },
430
409
  );
431
410
  }
432
411
  },
@@ -438,7 +417,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
438
417
  effectiveUserId,
439
418
  user_token,
440
419
  user_profile,
441
- ]
420
+ ],
442
421
  );
443
422
 
444
423
  // Initialize user ID
@@ -457,8 +436,8 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
457
436
  createStorageMessage(
458
437
  StorageMessageType.SET_STORAGE,
459
438
  "rblyn_anon",
460
- userId!
461
- )
439
+ userId!,
440
+ ),
462
441
  );
463
442
  });
464
443
  }
@@ -479,7 +458,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
479
458
  // Try to get stored ID once storage is ready
480
459
  executeStorageOp(() => {
481
460
  webViewRef.current?.injectJavaScript(
482
- createStorageMessage(StorageMessageType.GET_STORAGE, "rblyn_anon")
461
+ createStorageMessage(StorageMessageType.GET_STORAGE, "rblyn_anon"),
483
462
  );
484
463
  });
485
464
  }
@@ -565,7 +544,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
565
544
  {
566
545
  type: ErrorTypes.NETWORK_ERROR,
567
546
  context: { api_key, user_id },
568
- }
547
+ },
569
548
  );
570
549
  } finally {
571
550
  setLoading(false);
@@ -577,20 +556,6 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
577
556
  }
578
557
  }, [api_key, effectiveUserId, user_token]);
579
558
 
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
559
  function constructUrl(): string {
595
560
  if (!effectiveUserId) return "";
596
561
  const params = new URLSearchParams({
@@ -631,7 +596,8 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
631
596
  webViewRef.current.postMessage(
632
597
  JSON.stringify({
633
598
  name: "closeFrame",
634
- })
599
+ domain: "app-domain.com",
600
+ }),
635
601
  );
636
602
  }
637
603
 
@@ -682,7 +648,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
682
648
  }
683
649
  // Check if already granted
684
650
  const alreadyGranted = await PermissionsAndroid.check(
685
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
651
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
686
652
  );
687
653
 
688
654
  if (alreadyGranted) {
@@ -696,7 +662,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
696
662
  message: "This app uses your microphone for voice features.",
697
663
  buttonPositive: "Allow",
698
664
  buttonNegative: "Deny",
699
- }
665
+ },
700
666
  );
701
667
 
702
668
  const granted = result === PermissionsAndroid.RESULTS.GRANTED;
@@ -705,7 +671,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
705
671
  // Optional UX
706
672
  Alert.alert(
707
673
  "Microphone required",
708
- "Voice features will not work unless you allow microphone access."
674
+ "Voice features will not work unless you allow microphone access.",
709
675
  );
710
676
  }
711
677
 
@@ -829,7 +795,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
829
795
  return (
830
796
  request.url.startsWith(BASE_CHATBOT_URL) ||
831
797
  request.url.startsWith(
832
- chatbotConfig?.chat_iframe_url || ""
798
+ chatbotConfig?.chat_iframe_url || "",
833
799
  )
834
800
  );
835
801
  }}
@@ -919,7 +885,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
919
885
  url: nativeEvent.url,
920
886
  code: nativeEvent.code,
921
887
  },
922
- }
888
+ },
923
889
  );
924
890
  }}
925
891
  />
@@ -933,7 +899,7 @@ const Chatbot = forwardRef<ChatbotRef, InternalChatbotProps>(
933
899
  </View>
934
900
  </ErrorBoundary>
935
901
  );
936
- }
902
+ },
937
903
  );
938
904
 
939
905
  Chatbot.displayName = "Chatbot";
@@ -3,43 +3,167 @@
3
3
  * Utility functions for handling file downloads
4
4
  */
5
5
 
6
- import { Platform, Linking } from "react-native";
6
+ import { Linking, NativeModules, Platform } from "react-native";
7
7
  import { logger } from "./logger";
8
8
 
9
9
  export interface DownloadRequest {
10
10
  url: string;
11
11
  filename?: string;
12
+ inPlace?: boolean;
12
13
  }
13
14
 
15
+ const normalizeUrl = (url: string): string => {
16
+ return url?.trim();
17
+ };
18
+
19
+ const getUrlPathTail = (url?: string): string => {
20
+ const cleanedUrl = url?.split("?")?.[0] || "";
21
+ return cleanedUrl?.split("/")?.pop()?.trim() || "";
22
+ };
23
+
24
+ const getExtensionFromName = (name?: string): string => {
25
+ if (!name?.includes(".")) return "";
26
+ return name.substring(name.lastIndexOf("."));
27
+ };
28
+
29
+ const resolveDownloadFileName = (filename?: string, url?: string): string => {
30
+ const trimmedName = filename?.trim();
31
+ if (trimmedName) {
32
+ const sanitizedName = trimmedName.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_");
33
+ const urlExtension = getExtensionFromName(getUrlPathTail(url));
34
+ if (getExtensionFromName(sanitizedName) || !urlExtension)
35
+ return sanitizedName;
36
+ return `${sanitizedName}${urlExtension}`;
37
+ }
38
+ const tailName = getUrlPathTail(url);
39
+ if (tailName) return tailName.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_");
40
+ return `download-robylon-${Date.now()}`;
41
+ };
42
+
43
+ type RobylonDownloadNativeModule = {
44
+ downloadFile: (url: string, filename: string) => Promise<string>;
45
+ };
46
+
47
+ const isValidHttpUrl = (url: string): boolean => {
48
+ return /^https?:\/\//i.test(url);
49
+ };
50
+
51
+ const getNativeDownloadModule = (): RobylonDownloadNativeModule | null => {
52
+ return (
53
+ (NativeModules?.RobylonDownloadModule as RobylonDownloadNativeModule) ||
54
+ null
55
+ );
56
+ };
57
+
58
+ const openUrlWithFallback = async (url: string): Promise<void> => {
59
+ try {
60
+ await Linking.openURL(url);
61
+ return;
62
+ } catch (primaryError) {
63
+ logger.warn("Primary URL open failed, trying browser fallback", {
64
+ url,
65
+ primaryError:
66
+ primaryError instanceof Error
67
+ ? primaryError?.message
68
+ : String(primaryError),
69
+ });
70
+ }
71
+
72
+ await Linking.openURL(
73
+ `https://docs.google.com/viewer?url=${encodeURIComponent(url)}`,
74
+ );
75
+ };
76
+
77
+ const downloadOnAndroid = async (
78
+ url: string,
79
+ filename: string,
80
+ ): Promise<void> => {
81
+ const module = getNativeDownloadModule();
82
+ if (!module?.downloadFile) {
83
+ throw new Error("Robylon native download module is not linked on Android");
84
+ }
85
+ await module.downloadFile(url, filename);
86
+ };
87
+
88
+ const downloadOniOS = async (url: string, filename: string): Promise<void> => {
89
+ const module = getNativeDownloadModule();
90
+ if (!module?.downloadFile) {
91
+ throw new Error("Robylon native download module is not linked on iOS");
92
+ }
93
+ await module.downloadFile(url, filename);
94
+ };
95
+
96
+ const downloadWithNativeStorage = async (
97
+ url: string,
98
+ filename?: string,
99
+ ): Promise<void> => {
100
+ const safeName = resolveDownloadFileName(filename, url);
101
+ if (Platform.OS === "android") {
102
+ await downloadOnAndroid(url, safeName);
103
+ return;
104
+ }
105
+ if (Platform.OS === "ios") {
106
+ await downloadOniOS(url, safeName);
107
+ return;
108
+ }
109
+ await openUrlWithFallback(url);
110
+ };
111
+
112
+ const shouldUseInPlaceDownload = (
113
+ downloadRequest?: DownloadRequest,
114
+ ): boolean => {
115
+ return downloadRequest?.inPlace === true;
116
+ };
117
+
14
118
  /**
15
- * Downloads a file using the platform's default mechanism
119
+ * Downloads a file using native storage when possible
16
120
  * @param url - The URL of the file to download
17
121
  * @param filename - Optional filename for the download
18
122
  */
19
123
  export const downloadFile = async (
20
124
  url: string,
21
- filename?: string
125
+ filename?: string,
126
+ inPlace?: boolean,
22
127
  ): Promise<void> => {
23
128
  try {
24
- logger.debug("Initiating file download", { url, filename });
129
+ const normalizedUrl = normalizeUrl(url);
130
+ logger.debug("Initiating file download", { url: normalizedUrl, filename });
25
131
 
26
132
  // Validate URL
27
- if (!url || typeof url !== "string") {
133
+ if (!normalizedUrl || typeof normalizedUrl !== "string") {
28
134
  throw new Error("Invalid URL provided for download");
29
135
  }
30
136
 
31
- // For React Native, we'll use Linking to open the URL
32
- // This will use the device's default browser/download manager
33
- const supported = await Linking.canOpenURL(url);
137
+ if (!isValidHttpUrl(normalizedUrl)) {
138
+ throw new Error("Download URL must be an http/https URL");
139
+ }
34
140
 
35
- if (!supported) {
36
- throw new Error("Cannot open the provided URL");
141
+ if (inPlace) {
142
+ try {
143
+ await downloadWithNativeStorage(normalizedUrl, filename);
144
+ logger.debug("File downloaded with native storage flow");
145
+ } catch (nativeDownloadError) {
146
+ logger.warn("Native download failed, falling back to URL open", {
147
+ url: normalizedUrl,
148
+ nativeDownloadError:
149
+ nativeDownloadError instanceof Error
150
+ ? nativeDownloadError?.message
151
+ : String(nativeDownloadError),
152
+ });
153
+ await openUrlWithFallback(normalizedUrl);
154
+ logger.debug("File download opened with fallback browser flow");
155
+ }
156
+ return;
37
157
  }
38
158
 
39
- await Linking.openURL(url);
40
- logger.debug("File download initiated successfully");
159
+ await openUrlWithFallback(normalizedUrl);
160
+ logger.debug("File download opened with legacy URL flow");
41
161
  } catch (error) {
42
- logger.error("File download failed", { url, filename, error });
162
+ logger.error("File download failed", {
163
+ url,
164
+ filename,
165
+ error: error instanceof Error ? error?.message : String(error),
166
+ });
43
167
  throw error;
44
168
  }
45
169
  };
@@ -49,7 +173,7 @@ export const downloadFile = async (
49
173
  * @param downloadRequest - The download request object
50
174
  */
51
175
  export const handleDownloadRequest = async (
52
- downloadRequest: DownloadRequest
176
+ downloadRequest: DownloadRequest,
53
177
  ): Promise<void> => {
54
178
  const { url, filename } = downloadRequest;
55
179
 
@@ -59,7 +183,11 @@ export const handleDownloadRequest = async (
59
183
  }
60
184
 
61
185
  try {
62
- await downloadFile(url, filename);
186
+ await downloadFile(
187
+ url,
188
+ filename,
189
+ shouldUseInPlaceDownload(downloadRequest),
190
+ );
63
191
  } catch (error) {
64
192
  logger.error("Failed to handle download request", {
65
193
  downloadRequest,
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not modify it manually.
2
- export const SDK_VERSION = '2.0.32-staging.4';
2
+ export const SDK_VERSION = '2.1.1-staging.3';