vdb-ai-chat 1.0.0 → 1.0.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 (85) hide show
  1. package/dist/248.chat-widget.js +1 -0
  2. package/dist/538.chat-widget.js +1 -0
  3. package/dist/751.chat-widget.js +1 -0
  4. package/dist/chat-widget.js +1 -1
  5. package/dist/chat-widget.js.LICENSE.txt +2 -0
  6. package/lib/commonjs/api.js +2 -5
  7. package/lib/commonjs/api.js.map +1 -1
  8. package/lib/commonjs/components/ChatHeader.js +15 -2
  9. package/lib/commonjs/components/ChatHeader.js.map +1 -1
  10. package/lib/commonjs/components/ChatWidget.js +49 -24
  11. package/lib/commonjs/components/ChatWidget.js.map +1 -1
  12. package/lib/commonjs/components/MessageBubble.js +33 -9
  13. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  14. package/lib/commonjs/components/ProductsList.js +2 -2
  15. package/lib/commonjs/components/ProductsList.js.map +1 -1
  16. package/lib/commonjs/components/utils.js +46 -1
  17. package/lib/commonjs/components/utils.js.map +1 -1
  18. package/lib/commonjs/contexts/AnalyticsClientContext.js +19 -0
  19. package/lib/commonjs/contexts/AnalyticsClientContext.js.map +1 -0
  20. package/lib/commonjs/contexts/SegmentClientContext.js +19 -0
  21. package/lib/commonjs/contexts/SegmentClientContext.js.map +1 -0
  22. package/lib/commonjs/hooks/useAnalytics.js +158 -0
  23. package/lib/commonjs/hooks/useAnalytics.js.map +1 -0
  24. package/lib/commonjs/index.web.js +29 -3
  25. package/lib/commonjs/index.web.js.map +1 -1
  26. package/lib/commonjs/storage.js +5 -5
  27. package/lib/commonjs/storage.js.map +1 -1
  28. package/lib/module/api.js +2 -5
  29. package/lib/module/api.js.map +1 -1
  30. package/lib/module/components/ChatHeader.js +15 -2
  31. package/lib/module/components/ChatHeader.js.map +1 -1
  32. package/lib/module/components/ChatWidget.js +50 -25
  33. package/lib/module/components/ChatWidget.js.map +1 -1
  34. package/lib/module/components/MessageBubble.js +34 -10
  35. package/lib/module/components/MessageBubble.js.map +1 -1
  36. package/lib/module/components/ProductsList.js +2 -2
  37. package/lib/module/components/ProductsList.js.map +1 -1
  38. package/lib/module/components/utils.js +42 -0
  39. package/lib/module/components/utils.js.map +1 -1
  40. package/lib/module/contexts/AnalyticsClientContext.js +10 -0
  41. package/lib/module/contexts/AnalyticsClientContext.js.map +1 -0
  42. package/lib/module/contexts/SegmentClientContext.js +10 -0
  43. package/lib/module/contexts/SegmentClientContext.js.map +1 -0
  44. package/lib/module/hooks/useAnalytics.js +146 -0
  45. package/lib/module/hooks/useAnalytics.js.map +1 -0
  46. package/lib/module/index.native.js +5 -5
  47. package/lib/module/index.web.js +30 -4
  48. package/lib/module/index.web.js.map +1 -1
  49. package/lib/module/storage.js +6 -6
  50. package/lib/module/storage.js.map +1 -1
  51. package/lib/typescript/api.d.ts +1 -1
  52. package/lib/typescript/api.d.ts.map +1 -1
  53. package/lib/typescript/components/ChatHeader.d.ts.map +1 -1
  54. package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
  55. package/lib/typescript/components/MessageBubble.d.ts +2 -0
  56. package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
  57. package/lib/typescript/components/ProductsList.d.ts.map +1 -1
  58. package/lib/typescript/components/utils.d.ts +23 -0
  59. package/lib/typescript/components/utils.d.ts.map +1 -1
  60. package/lib/typescript/contexts/AnalyticsClientContext.d.ts +9 -0
  61. package/lib/typescript/contexts/AnalyticsClientContext.d.ts.map +1 -0
  62. package/lib/typescript/contexts/SegmentClientContext.d.ts +9 -0
  63. package/lib/typescript/contexts/SegmentClientContext.d.ts.map +1 -0
  64. package/lib/typescript/hooks/useAnalytics.d.ts +36 -0
  65. package/lib/typescript/hooks/useAnalytics.d.ts.map +1 -0
  66. package/lib/typescript/index.native.d.ts +5 -5
  67. package/lib/typescript/index.web.d.ts +1 -1
  68. package/lib/typescript/index.web.d.ts.map +1 -1
  69. package/lib/typescript/storage.d.ts.map +1 -1
  70. package/lib/typescript/types.d.ts +1 -1
  71. package/lib/typescript/types.d.ts.map +1 -1
  72. package/package.json +13 -3
  73. package/src/api.ts +22 -11
  74. package/src/components/ChatHeader.tsx +13 -2
  75. package/src/components/ChatWidget.tsx +53 -28
  76. package/src/components/MessageBubble.tsx +52 -10
  77. package/src/components/ProductsList.tsx +8 -6
  78. package/src/components/utils.ts +60 -1
  79. package/src/contexts/AnalyticsClientContext.tsx +20 -0
  80. package/src/contexts/SegmentClientContext.tsx +20 -0
  81. package/src/hooks/useAnalytics.tsx +176 -0
  82. package/src/index.native.tsx +5 -5
  83. package/src/index.web.tsx +36 -3
  84. package/src/storage.ts +16 -13
  85. package/src/types.ts +1 -1
@@ -7,6 +7,8 @@ import {
7
7
  Image,
8
8
  } from "react-native";
9
9
  import React from "react";
10
+ import { useUserAnalytics } from "../hooks/useAnalytics";
11
+ import { AnalyticsEventNames } from "./utils";
10
12
 
11
13
  // Use expo-image on native if available, fallback to RN Image
12
14
  let ImageComponent: typeof Image = Image;
@@ -38,6 +40,7 @@ const ChatHeader = ({
38
40
  onClose?: () => void;
39
41
  onClearChat?: () => void;
40
42
  }) => {
43
+ const { trackEvent } = useUserAnalytics();
41
44
  return (
42
45
  <View style={styles.container}>
43
46
  <View style={styles.logoContainer}>
@@ -49,12 +52,20 @@ const ChatHeader = ({
49
52
  <Text style={styles.title}>AI Search</Text>
50
53
  </View>
51
54
  <View style={styles.buttonContainer}>
52
- <TouchableOpacity onPress={() => onClearChat?.()}>
55
+ <TouchableOpacity onPress={() => {
56
+ trackEvent?.(AnalyticsEventNames.CHAT_CLEARED, {});
57
+ console.log("Segment: Clear Chat tracked");
58
+ onClearChat?.();
59
+ }}>
53
60
  <Text style={styles.clearChatText}>Clear Chat</Text>
54
61
  </TouchableOpacity>
55
62
  <TouchableOpacity
56
63
  style={styles.closeButton}
57
- onPress={() => onClose?.()}
64
+ onPress={() => {
65
+ trackEvent?.(AnalyticsEventNames.WIDGET_CLOSED, {});
66
+ console.log("Segment: Widget Close tracked");
67
+ onClose?.();
68
+ }}
58
69
  >
59
70
  <CloseIcon />
60
71
  </TouchableOpacity>
@@ -31,7 +31,8 @@ import {
31
31
  import ChatHeader from "./ChatHeader";
32
32
  import SuggestionsRow from "./SuggestionsRow";
33
33
  import ProductsList from "./ProductsList";
34
- import { FeedbackAction, formatToTime } from "./utils";
34
+ import { FeedbackAction, formatToTime, getUserDetails } from "./utils";
35
+ import { useUserAnalytics } from "../hooks/useAnalytics";
35
36
  import { Storage } from "../storage";
36
37
 
37
38
  export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
@@ -63,10 +64,10 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
63
64
  const [assistantResponse, setAssistantResponse] = useState<
64
65
  ChatMessage | undefined
65
66
  >(undefined);
66
- const [products, setProducts] = useState<{
67
- messageId: string;
68
- data: any;
69
- } | null>(null);
67
+ const [productsByMsg, setProductsByMsg] = useState<Record<string, any>>({});
68
+ const [reloadLoadingIds, setReloadLoadingIds] = useState<Set<string>>(
69
+ new Set()
70
+ );
70
71
  const [priceMode, setPriceMode] = useState<string | null>(
71
72
  priceModeProp || null
72
73
  );
@@ -76,7 +77,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
76
77
  const scrollRef = useRef<ScrollView | null>(null);
77
78
  const inputRef = useRef<TextInput | null>(null);
78
79
  const theme = useMemo(() => mergeTheme(themeOverrides), [themeOverrides]);
79
-
80
+ const { _identify } = useUserAnalytics();
80
81
  // Load user auth data from storage on mount
81
82
  useEffect(() => {
82
83
  const loadAuthData = async () => {
@@ -104,10 +105,8 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
104
105
  }, [userIdProp, userTokenProp]);
105
106
 
106
107
  const onViewAll = useCallback(() => {
107
- const searchPayload = JSON.stringify(
108
- assistantResponse?.agent_response?.payload
109
- );
110
- const payload = assistantResponse?.agent_response?.payload;
108
+ const searchPayload = JSON.stringify(assistantResponse?.search_payload);
109
+ const payload = assistantResponse?.search_payload;
111
110
  if (!payload) return;
112
111
 
113
112
  const domain = apiUrl.split("v3");
@@ -231,9 +230,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
231
230
  async (rawText: string) => {
232
231
  const trimmed = rawText.trim();
233
232
  if (!trimmed || loading) return;
234
- if (products) {
235
- setProducts(null);
236
- }
233
+
237
234
  const userMessage: ChatMessage = {
238
235
  id: `user-${Date.now()}`,
239
236
  role: "user",
@@ -289,12 +286,12 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
289
286
  if (latestAssistant?.text) {
290
287
  // If there is an agent_response, try to fetch products first
291
288
  if (
292
- latestAssistant?.agent_response &&
293
- typeof latestAssistant.agent_response === "object" &&
294
- Object.keys(latestAssistant.agent_response).length > 0
289
+ latestAssistant?.search_payload &&
290
+ typeof latestAssistant.search_payload === "object" &&
291
+ Object.keys(latestAssistant.search_payload).length > 0
295
292
  ) {
296
293
  const productsResult = await getProducts(
297
- latestAssistant.agent_response.payload
294
+ latestAssistant.search_payload
298
295
  );
299
296
 
300
297
  const hasDiamonds =
@@ -311,7 +308,6 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
311
308
  setLoadingMessageId(null);
312
309
  setTypingMessageId(null);
313
310
  setTypingFullText("");
314
- setProducts(null);
315
311
 
316
312
  setMessages((prev) =>
317
313
  prev.map((msg) =>
@@ -329,7 +325,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
329
325
  inputRef.current?.focus();
330
326
  return;
331
327
  }
332
- setProducts({
328
+ setProductsByMsg({
333
329
  messageId: latestAssistant.id,
334
330
  data: productsResult,
335
331
  });
@@ -386,9 +382,19 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
386
382
  inputRef.current?.focus();
387
383
  }
388
384
  },
389
- [apiUrl, loading, messages, apiParams, hasAuth, priceMode, products]
385
+ [apiUrl, loading, messages, apiParams, hasAuth, priceMode, productsByMsg]
390
386
  );
391
387
 
388
+ const identifySegmentUser = async () => {
389
+ const user = await getUserDetails();
390
+ const userId = user?.id;
391
+ const email = JSON.stringify(user?.email) ?? undefined;
392
+ if (userId) {
393
+ _identify(`${userId}`, {
394
+ email: email,
395
+ }).then();
396
+ }
397
+ };
392
398
  const handleSend = useCallback(async () => {
393
399
  const trimmed = input.trim();
394
400
  if (!trimmed) return;
@@ -404,13 +410,21 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
404
410
  );
405
411
 
406
412
  const handleReloadResults = useCallback(async (msg: ChatMessage) => {
413
+ const id = msg.id;
407
414
  try {
408
- const payload = msg?.agent_response?.payload;
409
- if (!payload) return;
415
+ setReloadLoadingIds((prev) => new Set(prev).add(id));
416
+ const payload = msg?.search_payload;
417
+ if (!payload || Object.keys(payload).length === 0) return;
410
418
  const productsResult = await getProducts(payload);
411
- setProducts({ messageId: msg.id, data: productsResult });
419
+ setProductsByMsg((prev) => ({ ...prev, [id]: productsResult }));
412
420
  } catch (e) {
413
421
  console.error("Reload results failed", e);
422
+ } finally {
423
+ setReloadLoadingIds((prev) => {
424
+ const next = new Set(prev);
425
+ next.delete(id);
426
+ return next;
427
+ });
414
428
  }
415
429
  }, []);
416
430
 
@@ -421,6 +435,7 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
421
435
  (data: { priceMode: string }) => changePriceMode(data.priceMode)
422
436
  );
423
437
  changePriceMode(undefined);
438
+ identifySegmentUser();
424
439
 
425
440
  return () => {
426
441
  DeviceEventEmitter.removeAllListeners("clearChat");
@@ -475,7 +490,8 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
475
490
  }
476
491
  setMessages([]);
477
492
  setAssistantResponse(undefined);
478
- setProducts(null);
493
+ setProductsByMsg({});
494
+ setReloadLoadingIds(new Set());
479
495
  setLoading(false);
480
496
  setLoadingMessageId(null);
481
497
  setTypingMessageId(null);
@@ -581,7 +597,11 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
581
597
  )}
582
598
  <ScrollView
583
599
  ref={scrollRef}
584
- style={modalHeight ? { height: modalHeight } : undefined}
600
+ style={
601
+ modalHeight
602
+ ? { height: modalHeight, backgroundColor: "#f5f5f5" }
603
+ : { backgroundColor: "#f5f5f5" }
604
+ }
585
605
  contentContainerStyle={{
586
606
  backgroundColor: theme?.listContentBackgroundColor || "#f5f5f5",
587
607
  ...styles.listContent,
@@ -611,12 +631,17 @@ export const ChatWidget = forwardRef<ChatWidgetRef, ChatWidgetProps>(
611
631
  conversationId={conversationId}
612
632
  handleFeedbackAction={handleFeedbackAction}
613
633
  onReloadResults={handleReloadResults}
634
+ reloading={reloadLoadingIds.has(item.id)}
635
+ hasResults={Boolean(
636
+ productsByMsg[item.id]?.response?.body?.diamonds &&
637
+ productsByMsg[item.id].response.body.diamonds.length > 0
638
+ )}
614
639
  />
615
640
  {item.role === "assistant" &&
616
- products &&
617
- products.messageId === item.id && (
641
+ productsByMsg &&
642
+ productsByMsg.messageId === item.id && (
618
643
  <ProductsList
619
- data={products?.data?.response?.body?.diamonds}
644
+ data={productsByMsg?.data?.response?.body?.diamonds}
620
645
  onViewAll={onViewAll}
621
646
  onItemPress={onItemPress}
622
647
  />
@@ -1,5 +1,13 @@
1
1
  import React, { memo } from "react";
2
- import { View, Text, StyleSheet, Image, TouchableOpacity, Platform } from "react-native";
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ Image,
7
+ TouchableOpacity,
8
+ Platform,
9
+ ActivityIndicator,
10
+ } from "react-native";
3
11
  import type { ChatMessage, ChatTheme } from "../types";
4
12
  import { FeedbackAction, formatToTime } from "./utils";
5
13
 
@@ -24,6 +32,8 @@ interface Props {
24
32
  message_id: string
25
33
  ) => void;
26
34
  onReloadResults?: (message: ChatMessage) => void;
35
+ reloading?: boolean;
36
+ hasResults?: boolean;
27
37
  }
28
38
 
29
39
  const MessageBubbleComponent: React.FC<Props> = ({
@@ -32,6 +42,8 @@ const MessageBubbleComponent: React.FC<Props> = ({
32
42
  conversationId,
33
43
  handleFeedbackAction,
34
44
  onReloadResults,
45
+ reloading,
46
+ hasResults,
35
47
  }) => {
36
48
  const isUser = message.role === "user";
37
49
  const isValidMessageId =
@@ -75,18 +87,48 @@ const MessageBubbleComponent: React.FC<Props> = ({
75
87
  </Text>
76
88
  </View>
77
89
  {(canFeedback ||
78
- (message.agent_response &&
79
- typeof message.agent_response === "object")) && (
90
+ (message.search_payload &&
91
+ typeof message.search_payload === "object" &&
92
+ Object.keys(message.search_payload).length > 0)) && (
80
93
  <View style={styles.rowContainer}>
81
94
  <View style={styles.time}>
82
95
  <Text>{formatToTime(message.createdAt)}</Text>
83
96
  </View>
84
- {message.agent_response &&
85
- typeof message.agent_response === "object" && (
86
- <TouchableOpacity onPress={() => onReloadResults?.(message)}>
87
- <Text>Reload Results</Text>
97
+ {message.search_payload &&
98
+ typeof message.search_payload === "object" &&
99
+ Object.keys(message.search_payload).length > 0 &&
100
+ !hasResults &&
101
+ (reloading ? (
102
+ <View
103
+ style={{ flexDirection: "row", alignItems: "center", gap: 6 }}
104
+ >
105
+ <ActivityIndicator size="small" color="#1a73e8" />
106
+ <Text
107
+ style={{
108
+ fontSize: 12,
109
+ color: "#1a73e8",
110
+ textDecorationLine: "underline",
111
+ }}
112
+ >
113
+ Loading…
114
+ </Text>
115
+ </View>
116
+ ) : (
117
+ <TouchableOpacity
118
+ onPress={() => onReloadResults?.(message)}
119
+ disabled={reloading}
120
+ >
121
+ <Text
122
+ style={{
123
+ fontSize: 12,
124
+ color: "#1a73e8",
125
+ textDecorationLine: "underline",
126
+ }}
127
+ >
128
+ Reload Results
129
+ </Text>
88
130
  </TouchableOpacity>
89
- )}
131
+ ))}
90
132
  <View style={styles.likeDislikeContainer}>
91
133
  <TouchableOpacity
92
134
  onPress={() =>
@@ -151,12 +193,12 @@ const styles = StyleSheet.create({
151
193
  justifyContent: "space-between",
152
194
  alignItems: "center",
153
195
  gap: 12,
196
+ marginTop: 8,
197
+ marginBottom: 8,
154
198
  },
155
199
  likeDislikeContainer: {
156
200
  flexDirection: "row",
157
201
  gap: 12,
158
- marginTop: 4,
159
- marginBottom: 4,
160
202
  },
161
203
  alignRight: {
162
204
  alignItems: "flex-end",
@@ -48,17 +48,19 @@ const ProductsListComponent: React.FC<ProductsListProps> = ({
48
48
  }}
49
49
  >
50
50
  <View key={item.id} style={styles.card}>
51
- <ImageComponent
52
- style={styles.image}
53
- source={{ uri: item.image_thumb_url }}
54
- />
51
+ {item.image_thumb_url ? (
52
+ <ImageComponent
53
+ style={styles.image}
54
+ source={{ uri: item.image_thumb_url }}
55
+ />
56
+ ) : null}
55
57
 
56
58
  <View style={styles.content}>
57
59
  <Text numberOfLines={2} style={styles.title}>
58
60
  {item.short_title}
59
61
  </Text>
60
62
  <Text style={styles.price}>${item.total_sales_price}</Text>
61
- </View>
63
+ </View>
62
64
  </View>
63
65
  </TouchableOpacity>
64
66
  ))}
@@ -72,7 +74,7 @@ const ProductsListComponent: React.FC<ProductsListProps> = ({
72
74
  >
73
75
  <Text style={styles.buttonText}>View All {">>"}</Text>
74
76
  </TouchableOpacity>
75
- </View>
77
+ </View>
76
78
  );
77
79
  };
78
80
 
@@ -38,6 +38,65 @@ export const fetchConversationId = async (
38
38
  );
39
39
  priceMode = userData?.price_mode || "";
40
40
  }
41
-
42
41
  return conversations[priceMode]?.conversation_id || null;
43
42
  };
43
+
44
+ export interface UserDetails {
45
+ id?: string | number;
46
+ email?: string;
47
+ first_name?: string;
48
+ last_name?: string;
49
+ company?: string;
50
+ company_id?: string | number;
51
+ country?: string;
52
+ price_mode?: string | number;
53
+ plan_details?: any;
54
+ }
55
+
56
+ export const getUserDetails = async (): Promise<UserDetails | null> => {
57
+ try {
58
+ const stored = await Storage.getJSON<{ user?: string | UserDetails }>(
59
+ "persist:userInfo",
60
+ {}
61
+ );
62
+ if (!stored?.user) return null;
63
+
64
+ // If user is stored as a JSON string, parse it
65
+ if (typeof stored.user === "string") {
66
+ try {
67
+ return JSON.parse(stored.user) as UserDetails;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ return stored.user as UserDetails;
74
+ } catch {
75
+ return null;
76
+ }
77
+ };
78
+
79
+ export enum DeviceType {
80
+ WEB = "web",
81
+ IOS = "ios",
82
+ ANDROID = "android",
83
+ }
84
+
85
+ export const getDeviceType = (platform?: string) => {
86
+ switch (platform) {
87
+ case DeviceType.WEB:
88
+ return 0;
89
+ case DeviceType.IOS:
90
+ return 1;
91
+ case DeviceType.ANDROID:
92
+ return 2;
93
+ default:
94
+ return 2;
95
+ }
96
+ };
97
+
98
+ export enum AnalyticsEventNames {
99
+ WIDGET_OPENED = "ai_chat_widget_opened",
100
+ WIDGET_CLOSED = "ai_chat_widget_closed",
101
+ CHAT_CLEARED = "ai_chat_cleared",
102
+ }
@@ -0,0 +1,20 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import type { Analytics } from "@segment/analytics-next";
3
+
4
+ export const AnalyticsClientContext = createContext<Analytics | undefined>(
5
+ undefined
6
+ );
7
+
8
+ export const useAnalyticsClient = () => useContext(AnalyticsClientContext);
9
+
10
+ export const AnalyticsClientProvider = ({
11
+ client,
12
+ children,
13
+ }: {
14
+ client: Analytics;
15
+ children: React.ReactNode;
16
+ }) => (
17
+ <AnalyticsClientContext.Provider value={client}>
18
+ {children}
19
+ </AnalyticsClientContext.Provider>
20
+ );
@@ -0,0 +1,20 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { SegmentClient } from "@segment/analytics-react-native";
3
+
4
+ export const SegmentClientContext = createContext<SegmentClient | undefined>(
5
+ undefined
6
+ );
7
+
8
+ export const useSegmentClient = () => useContext(SegmentClientContext);
9
+
10
+ export const SegmentClientProvider = ({
11
+ client,
12
+ children,
13
+ }: {
14
+ client: SegmentClient;
15
+ children: React.ReactNode;
16
+ }) => (
17
+ <SegmentClientContext.Provider value={client}>
18
+ {children}
19
+ </SegmentClientContext.Provider>
20
+ );
@@ -0,0 +1,176 @@
1
+ type JsonMap = Record<string, unknown>;
2
+ import { Platform } from "react-native";
3
+ import { DeviceType, getDeviceType, getUserDetails } from "../components/utils";
4
+ import { useAnalyticsClient } from "../contexts/AnalyticsClientContext";
5
+
6
+ const getSystemTypeEnum = (deviceType: any) => {
7
+ let type: string = "web";
8
+ if (deviceType) {
9
+ switch (deviceType) {
10
+ case "Linux":
11
+ type = "android";
12
+ break;
13
+ case "iPhone":
14
+ type = "ios";
15
+ break;
16
+ default:
17
+ type = "web";
18
+ break;
19
+ }
20
+ } else {
21
+ type = String(Platform.OS);
22
+ }
23
+ return getDeviceType(type).toString();
24
+ };
25
+
26
+ export const formatOSName = (rawOsName: string) => {
27
+ if (!rawOsName) return "";
28
+
29
+ const osName = rawOsName.toLowerCase();
30
+
31
+ const osMap = {
32
+ android: "Android",
33
+ ios: "iOS",
34
+ ipad: "iPadOS",
35
+ ipados: "iPadOS",
36
+ windows: "Windows",
37
+ macos: "macOS",
38
+ mac: "macOS",
39
+ linux: "Linux",
40
+ web: "Web",
41
+ tizen: "Tizen",
42
+ harmonyos: "HarmonyOS",
43
+ fireos: "Fire OS",
44
+ };
45
+
46
+ for (const key of Object.keys(osMap) as Array<keyof typeof osMap>) {
47
+ if (osName.includes(key)) {
48
+ return osMap[key];
49
+ }
50
+ }
51
+
52
+ return rawOsName.charAt(0).toUpperCase() + rawOsName.slice(1);
53
+ };
54
+
55
+ export const getUserAnalyticsPayload = async () => {
56
+ const user = await getUserDetails();
57
+ const deviceDetails =
58
+ Platform.OS === "web"
59
+ ? {
60
+ system_type_enum: getSystemTypeEnum(null),
61
+ platform_action_enum: "0",
62
+ }
63
+ : {};
64
+ return {
65
+ user_company_id: user?.company_id?.toString(),
66
+ user_company_name: user?.company,
67
+ user_email: user?.email,
68
+ user_id: user?.id?.toString(),
69
+ user_name: user?.first_name
70
+ ? `${user?.first_name} ${user?.last_name}`
71
+ : undefined,
72
+ user_price_mode: user?.price_mode?.toString(),
73
+ price_mode: user?.price_mode?.toString(),
74
+ has_plan_details: user?.plan_details ? true : false,
75
+ userAgent: Platform.OS === "web" ? navigator.userAgent : "",
76
+ ...deviceDetails,
77
+ };
78
+ };
79
+
80
+ export const isSegmentUserAgent = (userAgent: string) => {
81
+ if (!userAgent) return false;
82
+
83
+ // Regex to match Segment's specific Safari-like pattern
84
+ const segmentPattern =
85
+ /^Mozilla\/5\.0 \([^;]+; CPU [^)]+ like Mac OS X\) AppleWebKit\/600\.1\.4 \(KHTML, like Gecko\) Version\/\d\.0 Mobile\/10B329 Safari\/8536\.25$/;
86
+
87
+ return segmentPattern.test(userAgent);
88
+ };
89
+
90
+ export function isBot(userAgent = "") {
91
+ if (!userAgent && Platform.OS === "web") return true;
92
+
93
+ const botPatterns = [
94
+ /bot/i, // "Googlebot", "Bingbot"
95
+ /crawl/i, // "crawler"
96
+ /spider/i, // "spider"
97
+ /HeadlessChrome/i, // Puppeteer/Headless
98
+ /PhantomJS/i, // Old automation
99
+ /slurp/i, // Yahoo Slurp bot
100
+ /curl/i, // CLI traffic
101
+ /wget/i, // CLI downloads
102
+ /httpclient/i, // Generic clients
103
+ ];
104
+
105
+ return botPatterns.some((pattern) => pattern.test(userAgent));
106
+ }
107
+
108
+ export const useUserAnalytics = () => {
109
+ // This will return undefined if not wrapped in provider, which is fine
110
+ const analyticsClient = useAnalyticsClient();
111
+
112
+ const _track = async (eventName: string, options: any) => {
113
+ const userAnalyticsPayload = await getUserAnalyticsPayload();
114
+ if (isSegmentUserAgent(userAnalyticsPayload?.userAgent)) {
115
+ console.warn(
116
+ "Skipping tracking for Segment user agent:",
117
+ userAnalyticsPayload.userAgent
118
+ );
119
+ return;
120
+ }
121
+ if (isBot(userAnalyticsPayload?.userAgent)) {
122
+ console.warn(
123
+ "Skipping tracking for bot user agent:",
124
+ userAnalyticsPayload.userAgent
125
+ );
126
+ return;
127
+ }
128
+ const client = analyticsClient || (window as any).analytics;
129
+ if (!client) return;
130
+
131
+ const user = await getUserDetails();
132
+ if (!user?.id) {
133
+ console.error(
134
+ "Track Event tracked without user:",
135
+ eventName,
136
+ JSON.stringify(options)
137
+ );
138
+ console.error(
139
+ "Can't identify as user is not logged in",
140
+ JSON.stringify(user)
141
+ );
142
+ } else {
143
+ await client.identify(`${user.id}`, {
144
+ name: `${user.first_name} ${user.last_name}`,
145
+ email: user.email,
146
+ country: user.country,
147
+ });
148
+ }
149
+ await client.track(eventName, {
150
+ ...userAnalyticsPayload,
151
+ ...options,
152
+ });
153
+ };
154
+
155
+ const _identify = async (userId?: string, userTraits?: JsonMap) => {
156
+ const client = analyticsClient || (window as any).analytics;
157
+ if (!client) return;
158
+ await client.identify(userId, userTraits);
159
+ };
160
+
161
+ const trackEvent = async (eventName: string, options: any) => {
162
+ const userAnalyticsPayload = await getUserAnalyticsPayload();
163
+ const client = analyticsClient || (window as any).analytics;
164
+ if (!client) return;
165
+ await client.track(eventName, {
166
+ ...userAnalyticsPayload,
167
+ ...options,
168
+ });
169
+ };
170
+
171
+ return {
172
+ trackEvent,
173
+ _identify,
174
+ _track,
175
+ };
176
+ };
@@ -1,6 +1,6 @@
1
1
  // React Native entry point - re-export everything from main index
2
- export { ChatWidget } from './components/ChatWidget';
3
- export * from './types';
4
- export * from './theme';
5
- export { normaliseMessages } from './api';
6
- export { initStorage, isStorageInitialized, Storage } from './storage';
2
+ export { ChatWidget } from "./components/ChatWidget";
3
+ export * from "./types";
4
+ export * from "./theme";
5
+ export { normaliseMessages } from "./api";
6
+ export { initStorage, isStorageInitialized, Storage } from "./storage";