vdb-ai-chat 1.0.0 → 1.0.2

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 (132) hide show
  1. package/dist/10.chat-widget.js +2 -0
  2. package/dist/10.chat-widget.js.LICENSE.txt +1 -0
  3. package/dist/104.chat-widget.js +1 -0
  4. package/dist/248.chat-widget.js +1 -0
  5. package/dist/50.chat-widget.js +1 -0
  6. package/dist/521.chat-widget.js +1 -0
  7. package/dist/538.chat-widget.js +1 -0
  8. package/dist/572.chat-widget.js +1 -0
  9. package/dist/694.chat-widget.js +1 -0
  10. package/dist/chat-widget.js +1 -1
  11. package/dist/chat-widget.js.LICENSE.txt +2 -0
  12. package/lib/commonjs/api.js +6 -8
  13. package/lib/commonjs/api.js.map +1 -1
  14. package/lib/commonjs/components/BetaNotice.js +38 -0
  15. package/lib/commonjs/components/BetaNotice.js.map +1 -0
  16. package/lib/commonjs/components/ChatHeader.js +41 -21
  17. package/lib/commonjs/components/ChatHeader.js.map +1 -1
  18. package/lib/commonjs/components/ChatInput.js +21 -21
  19. package/lib/commonjs/components/ChatInput.js.map +1 -1
  20. package/lib/commonjs/components/ChatWidget.js +162 -90
  21. package/lib/commonjs/components/ChatWidget.js.map +1 -1
  22. package/lib/commonjs/components/MessageBubble.js +28 -68
  23. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  24. package/lib/commonjs/components/MessageMetaRow.js +135 -0
  25. package/lib/commonjs/components/MessageMetaRow.js.map +1 -0
  26. package/lib/commonjs/components/ProductsGrid.js +139 -0
  27. package/lib/commonjs/components/ProductsGrid.js.map +1 -0
  28. package/lib/commonjs/components/ProductsList.js +22 -126
  29. package/lib/commonjs/components/ProductsList.js.map +1 -1
  30. package/lib/commonjs/components/ProductsListView.js +139 -0
  31. package/lib/commonjs/components/ProductsListView.js.map +1 -0
  32. package/lib/commonjs/components/SuggestionsRow.js +41 -23
  33. package/lib/commonjs/components/SuggestionsRow.js.map +1 -1
  34. package/lib/commonjs/components/utils.js +50 -4
  35. package/lib/commonjs/components/utils.js.map +1 -1
  36. package/lib/commonjs/contexts/AnalyticsClientContext.js +19 -0
  37. package/lib/commonjs/contexts/AnalyticsClientContext.js.map +1 -0
  38. package/lib/commonjs/hooks/useAnalytics.js +158 -0
  39. package/lib/commonjs/hooks/useAnalytics.js.map +1 -0
  40. package/lib/commonjs/index.web.js +91 -8
  41. package/lib/commonjs/index.web.js.map +1 -1
  42. package/lib/commonjs/storage.js +5 -5
  43. package/lib/commonjs/storage.js.map +1 -1
  44. package/lib/commonjs/theme.js +4 -4
  45. package/lib/commonjs/theme.js.map +1 -1
  46. package/lib/module/api.js +6 -8
  47. package/lib/module/api.js.map +1 -1
  48. package/lib/module/components/BetaNotice.js +30 -0
  49. package/lib/module/components/BetaNotice.js.map +1 -0
  50. package/lib/module/components/ChatHeader.js +41 -21
  51. package/lib/module/components/ChatHeader.js.map +1 -1
  52. package/lib/module/components/ChatInput.js +21 -21
  53. package/lib/module/components/ChatInput.js.map +1 -1
  54. package/lib/module/components/ChatWidget.js +164 -92
  55. package/lib/module/components/ChatWidget.js.map +1 -1
  56. package/lib/module/components/MessageBubble.js +28 -70
  57. package/lib/module/components/MessageBubble.js.map +1 -1
  58. package/lib/module/components/MessageMetaRow.js +127 -0
  59. package/lib/module/components/MessageMetaRow.js.map +1 -0
  60. package/lib/module/components/ProductsGrid.js +133 -0
  61. package/lib/module/components/ProductsGrid.js.map +1 -0
  62. package/lib/module/components/ProductsList.js +21 -126
  63. package/lib/module/components/ProductsList.js.map +1 -1
  64. package/lib/module/components/ProductsListView.js +132 -0
  65. package/lib/module/components/ProductsListView.js.map +1 -0
  66. package/lib/module/components/SuggestionsRow.js +41 -23
  67. package/lib/module/components/SuggestionsRow.js.map +1 -1
  68. package/lib/module/components/utils.js +46 -3
  69. package/lib/module/components/utils.js.map +1 -1
  70. package/lib/module/contexts/AnalyticsClientContext.js +10 -0
  71. package/lib/module/contexts/AnalyticsClientContext.js.map +1 -0
  72. package/lib/module/hooks/useAnalytics.js +146 -0
  73. package/lib/module/hooks/useAnalytics.js.map +1 -0
  74. package/lib/module/index.native.js +5 -5
  75. package/lib/module/index.web.js +89 -6
  76. package/lib/module/index.web.js.map +1 -1
  77. package/lib/module/storage.js +6 -6
  78. package/lib/module/storage.js.map +1 -1
  79. package/lib/module/theme.js +4 -4
  80. package/lib/module/theme.js.map +1 -1
  81. package/lib/typescript/api.d.ts +1 -1
  82. package/lib/typescript/api.d.ts.map +1 -1
  83. package/lib/typescript/components/BetaNotice.d.ts +5 -0
  84. package/lib/typescript/components/BetaNotice.d.ts.map +1 -0
  85. package/lib/typescript/components/ChatHeader.d.ts +5 -2
  86. package/lib/typescript/components/ChatHeader.d.ts.map +1 -1
  87. package/lib/typescript/components/ChatInput.d.ts.map +1 -1
  88. package/lib/typescript/components/ChatWidget.d.ts.map +1 -1
  89. package/lib/typescript/components/MessageBubble.d.ts +9 -3
  90. package/lib/typescript/components/MessageBubble.d.ts.map +1 -1
  91. package/lib/typescript/components/MessageMetaRow.d.ts +14 -0
  92. package/lib/typescript/components/MessageMetaRow.d.ts.map +1 -0
  93. package/lib/typescript/components/ProductsGrid.d.ts +10 -0
  94. package/lib/typescript/components/ProductsGrid.d.ts.map +1 -0
  95. package/lib/typescript/components/ProductsList.d.ts +4 -2
  96. package/lib/typescript/components/ProductsList.d.ts.map +1 -1
  97. package/lib/typescript/components/ProductsListView.d.ts +10 -0
  98. package/lib/typescript/components/ProductsListView.d.ts.map +1 -0
  99. package/lib/typescript/components/SuggestionsRow.d.ts +2 -0
  100. package/lib/typescript/components/SuggestionsRow.d.ts.map +1 -1
  101. package/lib/typescript/components/utils.d.ts +24 -0
  102. package/lib/typescript/components/utils.d.ts.map +1 -1
  103. package/lib/typescript/contexts/AnalyticsClientContext.d.ts +9 -0
  104. package/lib/typescript/contexts/AnalyticsClientContext.d.ts.map +1 -0
  105. package/lib/typescript/hooks/useAnalytics.d.ts +36 -0
  106. package/lib/typescript/hooks/useAnalytics.d.ts.map +1 -0
  107. package/lib/typescript/index.native.d.ts +5 -5
  108. package/lib/typescript/index.web.d.ts +1 -1
  109. package/lib/typescript/index.web.d.ts.map +1 -1
  110. package/lib/typescript/storage.d.ts.map +1 -1
  111. package/lib/typescript/types.d.ts +4 -2
  112. package/lib/typescript/types.d.ts.map +1 -1
  113. package/package.json +13 -3
  114. package/src/api.ts +26 -14
  115. package/src/components/BetaNotice.tsx +32 -0
  116. package/src/components/ChatHeader.tsx +43 -18
  117. package/src/components/ChatInput.tsx +20 -21
  118. package/src/components/ChatWidget.tsx +282 -219
  119. package/src/components/MessageBubble.tsx +48 -111
  120. package/src/components/MessageMetaRow.tsx +199 -0
  121. package/src/components/ProductsGrid.tsx +163 -0
  122. package/src/components/ProductsList.tsx +20 -144
  123. package/src/components/ProductsListView.tsx +149 -0
  124. package/src/components/SuggestionsRow.tsx +45 -21
  125. package/src/components/utils.ts +66 -5
  126. package/src/contexts/AnalyticsClientContext.tsx +20 -0
  127. package/src/hooks/useAnalytics.tsx +176 -0
  128. package/src/index.native.tsx +5 -5
  129. package/src/index.web.tsx +92 -4
  130. package/src/storage.ts +16 -13
  131. package/src/theme.ts +4 -4
  132. package/src/types.ts +4 -3
@@ -1,160 +1,36 @@
1
- import {
2
- View,
3
- Image,
4
- StyleSheet,
5
- Text,
6
- ScrollView,
7
- TouchableOpacity,
8
- Platform,
9
- } from "react-native";
10
- import React, { memo } from "react";
11
-
12
- // Use expo-image on native if available, fallback to RN Image
13
- let ImageComponent: typeof Image = Image;
14
- if (Platform.OS !== "web") {
15
- try {
16
- const ExpoImage = require("expo-image").Image;
17
- if (ExpoImage) ImageComponent = ExpoImage;
18
- } catch {
19
- // expo-image not installed, use React Native Image
20
- }
21
- }
1
+ import React from "react";
2
+ import ProductsGrid from "./ProductsGrid";
3
+ import ProductsListView from "./ProductsListView";
22
4
 
23
5
  interface ProductsListProps {
24
- data: any;
6
+ data: any[];
25
7
  onViewAll?: () => void;
26
8
  onItemPress?: (item: any) => void;
9
+ variant?: "grid" | "list";
10
+ totalResults: number
27
11
  }
28
12
 
29
- const ProductsListComponent: React.FC<ProductsListProps> = ({
13
+ const ProductsList: React.FC<ProductsListProps> = ({
30
14
  data,
31
15
  onViewAll,
32
16
  onItemPress,
17
+ variant = "list",
18
+ totalResults,
33
19
  }) => {
34
20
  if (!data || !data.length) return null;
35
-
21
+ if (variant === "list") {
22
+ return (
23
+ <ProductsListView
24
+ data={data}
25
+ onViewAll={onViewAll}
26
+ onItemPress={onItemPress}
27
+ totalResults={totalResults}
28
+ />
29
+ );
30
+ }
36
31
  return (
37
- <View style={styles.wrapper}>
38
- <ScrollView
39
- horizontal
40
- showsHorizontalScrollIndicator={false}
41
- contentContainerStyle={styles.listContent}
42
- >
43
- {data.map((item: any) => (
44
- <TouchableOpacity
45
- key={item.id}
46
- onPress={() => {
47
- onItemPress?.(item);
48
- }}
49
- >
50
- <View key={item.id} style={styles.card}>
51
- <ImageComponent
52
- style={styles.image}
53
- source={{ uri: item.image_thumb_url }}
54
- />
55
-
56
- <View style={styles.content}>
57
- <Text numberOfLines={2} style={styles.title}>
58
- {item.short_title}
59
- </Text>
60
- <Text style={styles.price}>${item.total_sales_price}</Text>
61
- </View>
62
- </View>
63
- </TouchableOpacity>
64
- ))}
65
- </ScrollView>
66
-
67
- {/* View All Button */}
68
- <TouchableOpacity
69
- style={styles.button}
70
- activeOpacity={0.8}
71
- onPress={onViewAll}
72
- >
73
- <Text style={styles.buttonText}>View All {">>"}</Text>
74
- </TouchableOpacity>
75
- </View>
32
+ <ProductsGrid data={data} onViewAll={onViewAll} onItemPress={onItemPress} totalResults={totalResults} />
76
33
  );
77
34
  };
78
35
 
79
- const styles = StyleSheet.create({
80
- wrapper: {
81
- paddingHorizontal: 12,
82
- marginHorizontal: 12,
83
- marginBottom: 12,
84
- paddingTop: 6,
85
- paddingBottom: 14,
86
- backgroundColor: "#fff",
87
- borderRadius: 8,
88
- borderWidth: 1,
89
- borderColor: "#e8e8e8",
90
- elevation: 3,
91
- shadowColor: "#000",
92
- shadowOpacity: 0.08,
93
- shadowOffset: { width: 0, height: 2 },
94
- shadowRadius: 6,
95
- },
96
-
97
- listContent: {
98
- gap: 12,
99
- paddingVertical: 6,
100
- },
101
-
102
- card: {
103
- width: 150,
104
- backgroundColor: "#fff",
105
- borderRadius: 14,
106
- overflow: "hidden",
107
- borderColor: "#e8e8e8",
108
- borderWidth: 1,
109
- elevation: 3,
110
- shadowColor: "#000",
111
- shadowOpacity: 0.08,
112
- shadowOffset: { width: 0, height: 2 },
113
- shadowRadius: 6,
114
- },
115
-
116
- image: {
117
- width: "100%",
118
- height: 120,
119
- borderBottomWidth: 1,
120
- borderBottomColor: "#e8e8e8",
121
- },
122
-
123
- content: {
124
- padding: 10,
125
- gap: 4,
126
- },
127
-
128
- title: {
129
- fontSize: 13,
130
- color: "#222",
131
- fontWeight: "500",
132
- },
133
-
134
- price: {
135
- marginTop: 4,
136
- fontSize: 14,
137
- fontWeight: "700",
138
- color: "#000",
139
- },
140
-
141
- button: {
142
- marginTop: 12,
143
- alignSelf: "center",
144
- paddingHorizontal: 20,
145
- paddingVertical: 8,
146
- backgroundColor: "#804195",
147
- borderRadius: 20,
148
- width: 300,
149
- alignItems: "center",
150
- },
151
-
152
- buttonText: {
153
- color: "#fff",
154
- fontSize: 14,
155
- fontWeight: "600",
156
- },
157
- });
158
-
159
- const ProductsList = memo(ProductsListComponent);
160
36
  export default ProductsList;
@@ -0,0 +1,149 @@
1
+ import React, { memo, useState } from "react";
2
+ import { View, StyleSheet, Text, TouchableOpacity, ScrollView, Platform, Image } from "react-native";
3
+
4
+ let ImageComponent: typeof Image = Image;
5
+ if (Platform.OS !== "web") {
6
+ try {
7
+ const ExpoImage = require("expo-image").Image;
8
+ if (ExpoImage) ImageComponent = ExpoImage;
9
+ } catch {
10
+ // expo-image not installed, use React Native Image
11
+ }
12
+ }
13
+
14
+ interface ProductsListViewProps {
15
+ data: any[];
16
+ totalResults?: number;
17
+ onViewAll?: () => void;
18
+ onItemPress?: (item: any) => void;
19
+ }
20
+
21
+ const ProductsListViewComponent: React.FC<ProductsListViewProps> = ({ data, totalResults, onViewAll, onItemPress }) => {
22
+ if (!data || !data.length) return null;
23
+
24
+ const [priceWidth, setPriceWidth] = useState<number>(0);
25
+
26
+ return (
27
+ <View style={styles.wrapper}>
28
+ <ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.listContent}>
29
+ {data.map((item: any, index: number) => (
30
+ <TouchableOpacity key={item.id} onPress={() => onItemPress?.(item)} activeOpacity={0.8}>
31
+ <View style={styles.row}>
32
+ <View style={styles.left}>
33
+ <Text style={styles.serial}>{index + 1}.</Text>
34
+ <Text
35
+ numberOfLines={1}
36
+ ellipsizeMode="tail"
37
+ style={styles.title}
38
+ >
39
+ {item.short_title}
40
+ </Text>
41
+ </View>
42
+ {item.total_sales_price ? (
43
+ <View style={[styles.priceContainer, priceWidth ? { width: priceWidth } : null]}>
44
+ <Text
45
+ style={styles.price}
46
+ onLayout={(e) => {
47
+ const w = e.nativeEvent.layout.width;
48
+ if (w > priceWidth) setPriceWidth(w);
49
+ }}
50
+ >
51
+ ${item.total_sales_price}
52
+ </Text>
53
+ </View>
54
+ ) : (
55
+ <></>
56
+ )}
57
+ </View>
58
+ </TouchableOpacity>
59
+ ))}
60
+ </ScrollView>
61
+
62
+ <TouchableOpacity style={styles.button} activeOpacity={0.8} onPress={onViewAll}>
63
+ <Text style={styles.buttonText}>{`View All ${totalResults} Results`}</Text>
64
+ <Image
65
+ source={{
66
+ uri: "https://cdn.vdbapp.com/ai/chat-widget/assets/img/right.svg",
67
+ }}
68
+ resizeMode="contain"
69
+ style={{ width: 20, height: 20, tintColor: "#fff" }}
70
+ />
71
+ </TouchableOpacity>
72
+ </View>
73
+ );
74
+ };
75
+
76
+ const styles = StyleSheet.create({
77
+ wrapper: {
78
+ paddingHorizontal: 8,
79
+ },
80
+
81
+ listContent: {
82
+ // No extra padding/margins
83
+ },
84
+
85
+ row: {
86
+ flexDirection: "row",
87
+ justifyContent: "space-between",
88
+ alignItems: "center",
89
+ paddingVertical: 6,
90
+ backgroundColor: "#fff",
91
+ },
92
+
93
+ left: {
94
+ flexDirection: "row",
95
+ alignItems: "center",
96
+ gap: 4,
97
+ flex: 1,
98
+ minWidth: 0,
99
+ },
100
+
101
+ serial: {
102
+ fontSize: 13,
103
+ color: "#020001",
104
+ fontWeight: "500",
105
+ },
106
+
107
+ title: {
108
+ flexShrink: 1,
109
+ fontSize: 13,
110
+ color: "#3378F6",
111
+ fontWeight: "500",
112
+ },
113
+
114
+ price: {
115
+ fontSize: 13,
116
+ fontWeight: "500",
117
+ color: "#020001",
118
+ textAlign: "left",
119
+ },
120
+ priceContainer: {
121
+ alignItems: "flex-start",
122
+ justifyContent: "center",
123
+ },
124
+
125
+ button: {
126
+ marginTop: 12,
127
+ marginHorizontal: 12,
128
+ alignSelf: "center",
129
+ paddingHorizontal: 16,
130
+ paddingVertical: 6,
131
+ backgroundColor: "#292735",
132
+ borderRadius: 8,
133
+ alignItems: "center",
134
+ gap: 16,
135
+ width: "100%",
136
+ flexDirection: "row",
137
+ justifyContent: "center",
138
+ alignContent: "center",
139
+ },
140
+
141
+ buttonText: {
142
+ color: "#fff",
143
+ fontSize: 14,
144
+ fontWeight: "600",
145
+ },
146
+ });
147
+
148
+ const ProductsListView = memo(ProductsListViewComponent);
149
+ export default ProductsListView;
@@ -10,24 +10,30 @@ import {
10
10
  interface SuggestionsRowProps {
11
11
  suggestions: string[];
12
12
  onSelect: (value: string) => void;
13
+ /** Inline variant renders inside a message bubble, without outer padding/background */
14
+ variant?: "inline" | "default";
13
15
  }
14
16
 
15
17
  const SuggestionsRowComponent: React.FC<SuggestionsRowProps> = ({
16
18
  suggestions,
17
19
  onSelect,
20
+ variant = "default",
18
21
  }) => {
19
22
  if (!suggestions.length) return null;
20
23
 
21
24
  return (
22
25
  <ScrollView
23
- horizontal
24
- showsHorizontalScrollIndicator={false}
25
- contentContainerStyle={styles.container}
26
+ showsVerticalScrollIndicator={false}
27
+ contentContainerStyle={[
28
+ styles.container,
29
+ variant === "inline" && styles.containerInline,
30
+ ]}
31
+ style={[styles.scroll, variant === "inline" && styles.scrollInline]}
26
32
  >
27
33
  {suggestions.map((s) => (
28
34
  <TouchableOpacity
29
35
  key={s}
30
- style={styles.chip}
36
+ style={[styles.chip, variant === "inline" && styles.chipInline]}
31
37
  onPress={() => onSelect(s)}
32
38
  activeOpacity={0.75}
33
39
  >
@@ -39,33 +45,51 @@ const SuggestionsRowComponent: React.FC<SuggestionsRowProps> = ({
39
45
  };
40
46
 
41
47
  const styles = StyleSheet.create({
48
+ scroll: {
49
+ maxWidth: "80%",
50
+ alignSelf: "flex-start",
51
+ paddingHorizontal: 8,
52
+ },
53
+ scrollInline: {
54
+ maxWidth: "100%",
55
+ alignSelf: "stretch",
56
+ paddingHorizontal: 0,
57
+ },
42
58
  container: {
43
- paddingHorizontal: 12,
44
- paddingBottom: 8,
45
- flexDirection: "row",
46
- gap: 10,
59
+ paddingVertical: 0,
60
+ flexDirection: "column",
61
+ justifyContent: "center",
62
+ alignItems: "center",
63
+ gap: 4,
64
+ alignSelf: "stretch",
65
+ // backgroundColor: "#EDEDF2",
66
+ },
67
+ containerInline: {
68
+ // inside bubble, rely on bubble background
69
+ gap: 6,
47
70
  },
48
71
 
49
72
  chip: {
50
- backgroundColor: "#ffffff",
51
- paddingHorizontal: 16,
52
- paddingVertical: 8,
53
- borderRadius: 18,
54
-
55
- // Modern soft shadow (Material 3 / iOS style)
56
- shadowColor: "#000",
57
- shadowOpacity: 0.07,
58
- shadowRadius: 4,
59
- shadowOffset: { width: 0, height: 2 },
60
-
61
- elevation: 2,
73
+ backgroundColor: "#EDEDF2",
74
+ borderRadius: 4,
75
+ paddingHorizontal: 8,
76
+ paddingVertical: 6,
77
+ width: "100%",
78
+ alignSelf: "stretch",
79
+ justifyContent: "center",
80
+ alignItems: "flex-start",
81
+ borderWidth: 1,
82
+ borderColor: "#E0E0E0",
83
+ },
84
+ chipInline: {
85
+ backgroundColor: "#FFFFFF",
62
86
  },
63
87
 
64
88
  chipText: {
65
89
  fontSize: 14,
66
90
  color: "#1A1A1A",
67
91
  fontWeight: "500",
68
- letterSpacing: 0.3,
92
+ textAlign: "left",
69
93
  },
70
94
  });
71
95
 
@@ -31,13 +31,74 @@ export const fetchConversationId = async (
31
31
  if (!conversations) return null;
32
32
 
33
33
  let priceMode = _priceMode;
34
- if (!priceMode) {
35
- const userData = await Storage.getJSON<{ price_mode?: string }>(
36
- "userData",
34
+ if (priceMode === undefined) {
35
+ const userInfo = await Storage.getJSON<UserDetails>(
36
+ "persist:userInfo",
37
37
  {}
38
38
  );
39
- priceMode = userData?.price_mode || "";
39
+ const userData = userInfo?.user as UserDetails;
40
+ priceMode = String(userData?.price_mode) || "";
40
41
  }
41
-
42
42
  return conversations[priceMode]?.conversation_id || null;
43
43
  };
44
+
45
+ export interface UserDetails {
46
+ id?: string | number;
47
+ email?: string;
48
+ first_name?: string;
49
+ last_name?: string;
50
+ company?: string;
51
+ company_id?: string | number;
52
+ country?: string;
53
+ price_mode?: string | number;
54
+ plan_details?: any;
55
+ user?: any;
56
+ }
57
+
58
+ export const getUserDetails = async (): Promise<UserDetails | null> => {
59
+ try {
60
+ const stored = await Storage.getJSON<{ user?: string | UserDetails }>(
61
+ "persist:userInfo",
62
+ {}
63
+ );
64
+ if (!stored?.user) return null;
65
+
66
+ // If user is stored as a JSON string, parse it
67
+ if (typeof stored.user === "string") {
68
+ try {
69
+ return JSON.parse(stored.user) as UserDetails;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ return stored.user as UserDetails;
76
+ } catch {
77
+ return null;
78
+ }
79
+ };
80
+
81
+ export enum DeviceType {
82
+ WEB = "web",
83
+ IOS = "ios",
84
+ ANDROID = "android",
85
+ }
86
+
87
+ export const getDeviceType = (platform?: string) => {
88
+ switch (platform) {
89
+ case DeviceType.WEB:
90
+ return 0;
91
+ case DeviceType.IOS:
92
+ return 1;
93
+ case DeviceType.ANDROID:
94
+ return 2;
95
+ default:
96
+ return 2;
97
+ }
98
+ };
99
+
100
+ export enum AnalyticsEventNames {
101
+ WIDGET_OPENED = "ai_chat_widget_opened",
102
+ WIDGET_CLOSED = "ai_chat_widget_closed",
103
+ CHAT_CLEARED = "ai_chat_cleared",
104
+ }
@@ -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,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
+ };