react-native-inapp-inspector 2.2.9 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/commonjs/components/AppHeaderLogo.js +45 -12
  2. package/dist/commonjs/components/BrandSquareIcon.js +92 -49
  3. package/dist/commonjs/components/EmptyState.d.ts +7 -2
  4. package/dist/commonjs/components/EmptyState.js +11 -5
  5. package/dist/commonjs/components/Inspector/AnalyticsTab.js +29 -18
  6. package/dist/commonjs/components/Inspector/ConsoleTab.js +6 -1
  7. package/dist/commonjs/components/Inspector/CrashDetail.js +43 -9
  8. package/dist/commonjs/components/Inspector/CrashTab.js +34 -3
  9. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +80 -16
  10. package/dist/commonjs/components/Inspector/InspectorHeader.js +16 -3
  11. package/dist/commonjs/components/Inspector/NetworkDetail.js +3 -2
  12. package/dist/commonjs/components/Inspector/NetworkTab.js +103 -4
  13. package/dist/commonjs/components/Inspector/ReduxTab.js +19 -3
  14. package/dist/commonjs/components/Inspector/SettingsPanel.js +4 -4
  15. package/dist/commonjs/components/Inspector/StorageTab.js +19 -3
  16. package/dist/commonjs/components/JsonViewer.js +2 -0
  17. package/dist/commonjs/components/MetaAccordion.js +15 -3
  18. package/dist/commonjs/constants/version.d.ts +1 -1
  19. package/dist/commonjs/constants/version.js +1 -1
  20. package/dist/commonjs/customHooks/networkLogger.d.ts +3 -0
  21. package/dist/commonjs/customHooks/networkLogger.js +10 -1
  22. package/dist/commonjs/helpers/index.d.ts +6 -0
  23. package/dist/commonjs/helpers/index.js +196 -6
  24. package/dist/commonjs/helpers/searchQueryParser.js +62 -20
  25. package/dist/commonjs/i18n/index.d.ts +13 -0
  26. package/dist/commonjs/i18n/index.js +35 -2
  27. package/dist/commonjs/index.d.ts +3 -0
  28. package/dist/commonjs/index.js +80 -11
  29. package/dist/commonjs/styles/AppColors.d.ts +4 -0
  30. package/dist/commonjs/styles/AppColors.js +11 -1
  31. package/dist/commonjs/styles/AppFonts.d.ts +11 -0
  32. package/dist/commonjs/styles/AppFonts.js +11 -1
  33. package/dist/commonjs/types/interfaces.d.ts +1 -0
  34. package/dist/esm/components/AppHeaderLogo.js +45 -12
  35. package/dist/esm/components/BrandSquareIcon.js +92 -49
  36. package/dist/esm/components/EmptyState.d.ts +7 -2
  37. package/dist/esm/components/EmptyState.js +11 -5
  38. package/dist/esm/components/Inspector/AnalyticsTab.js +30 -19
  39. package/dist/esm/components/Inspector/ConsoleTab.js +6 -1
  40. package/dist/esm/components/Inspector/CrashDetail.js +43 -9
  41. package/dist/esm/components/Inspector/CrashTab.js +34 -3
  42. package/dist/esm/components/Inspector/DeviceInfoTab.js +80 -16
  43. package/dist/esm/components/Inspector/InspectorHeader.js +17 -4
  44. package/dist/esm/components/Inspector/NetworkDetail.js +3 -2
  45. package/dist/esm/components/Inspector/NetworkTab.js +103 -4
  46. package/dist/esm/components/Inspector/ReduxTab.js +19 -3
  47. package/dist/esm/components/Inspector/SettingsPanel.js +4 -4
  48. package/dist/esm/components/Inspector/StorageTab.js +19 -3
  49. package/dist/esm/components/JsonViewer.js +2 -0
  50. package/dist/esm/components/MetaAccordion.js +15 -3
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/networkLogger.d.ts +3 -0
  54. package/dist/esm/customHooks/networkLogger.js +8 -0
  55. package/dist/esm/helpers/index.d.ts +6 -0
  56. package/dist/esm/helpers/index.js +193 -5
  57. package/dist/esm/helpers/searchQueryParser.js +62 -20
  58. package/dist/esm/i18n/index.d.ts +13 -0
  59. package/dist/esm/i18n/index.js +33 -3
  60. package/dist/esm/index.d.ts +3 -0
  61. package/dist/esm/index.js +68 -12
  62. package/dist/esm/styles/AppColors.d.ts +4 -0
  63. package/dist/esm/styles/AppColors.js +9 -0
  64. package/dist/esm/styles/AppFonts.d.ts +11 -0
  65. package/dist/esm/styles/AppFonts.js +9 -0
  66. package/dist/esm/types/interfaces.d.ts +1 -0
  67. package/package.json +1 -1
  68. package/src/components/AppHeaderLogo.tsx +46 -12
  69. package/src/components/BrandSquareIcon.tsx +127 -78
  70. package/src/components/EmptyState.tsx +27 -5
  71. package/src/components/Inspector/AnalyticsTab.tsx +48 -27
  72. package/src/components/Inspector/ConsoleTab.tsx +10 -0
  73. package/src/components/Inspector/CrashDetail.tsx +65 -9
  74. package/src/components/Inspector/CrashTab.tsx +41 -3
  75. package/src/components/Inspector/DeviceInfoTab.tsx +87 -16
  76. package/src/components/Inspector/InspectorHeader.tsx +24 -4
  77. package/src/components/Inspector/NetworkDetail.tsx +4 -4
  78. package/src/components/Inspector/NetworkTab.tsx +129 -4
  79. package/src/components/Inspector/ReduxTab.tsx +26 -8
  80. package/src/components/Inspector/SettingsPanel.tsx +4 -4
  81. package/src/components/Inspector/StorageTab.tsx +21 -3
  82. package/src/components/JsonViewer.tsx +2 -0
  83. package/src/components/MetaAccordion.tsx +18 -3
  84. package/src/constants/version.ts +1 -1
  85. package/src/customHooks/networkLogger.ts +15 -0
  86. package/src/helpers/index.ts +213 -5
  87. package/src/helpers/searchQueryParser.ts +63 -20
  88. package/src/i18n/index.ts +38 -3
  89. package/src/index.tsx +89 -11
  90. package/src/styles/AppColors.ts +9 -0
  91. package/src/styles/AppFonts.ts +16 -0
  92. package/src/types/interfaces.ts +1 -0
@@ -665,7 +665,7 @@ const NetworkTab = React.memo(() => {
665
665
  style={{
666
666
  backgroundColor: `${AppColors.purple}20`,
667
667
  borderRadius: 10,
668
- paddingHorizontal: 5,
668
+ paddingHorizontal: 6,
669
669
  paddingVertical: 1.5,
670
670
  }}>
671
671
  <Text
@@ -759,19 +759,130 @@ const NetworkTab = React.memo(() => {
759
759
  styles.toolbarBtnActive,
760
760
  ]}
761
761
  onPress={filtersAccordion.toggleOpen}
762
- hitSlop={10}>
762
+ hitSlop={6}>
763
763
  <FilterIcon
764
764
  color={
765
765
  filtersAccordion.isOpen
766
766
  ? AppColors.purple
767
767
  : AppColors.grayTextStrong
768
768
  }
769
- size={18}
769
+ size={16}
770
770
  />
771
+ {(statusFilters.size > 0 ||
772
+ methodFilters.size > 0) && (
773
+ <View style={styles.activeFilterDot} />
774
+ )}
771
775
  </TouchableScale>
772
776
  </View>
773
777
  </View>
774
778
 
779
+ {/* Quick Search Scope & Filter Tags Row */}
780
+ <View style={{paddingBottom: 4}}>
781
+ <ScrollView
782
+ horizontal
783
+ showsHorizontalScrollIndicator={false}
784
+ contentContainerStyle={{
785
+ paddingHorizontal: 12,
786
+ flexDirection: 'row',
787
+ alignItems: 'center',
788
+ gap: 5,
789
+ paddingVertical: 2,
790
+ }}>
791
+ {/* Search Scopes */}
792
+ {SEARCH_SCOPES.map(s => {
793
+ const isActive = searchScope === s.id;
794
+ return (
795
+ <TouchableScale
796
+ key={`scope_${s.id}`}
797
+ onPress={() => setSearchScope(s.id)}
798
+ hitSlop={6}
799
+ style={{
800
+ flexDirection: 'row',
801
+ alignItems: 'center',
802
+ gap: 3.5,
803
+ paddingHorizontal: 7.5,
804
+ paddingVertical: 3.5,
805
+ borderRadius: 6,
806
+ backgroundColor: isActive
807
+ ? AppColors.purple
808
+ : AppColors.grayBackground,
809
+ borderWidth: 1,
810
+ borderColor: isActive
811
+ ? AppColors.purple
812
+ : AppColors.grayBorderSecondary,
813
+ }}>
814
+ {s.icon(isActive ? AppColors.white : AppColors.grayTextWeak)}
815
+ <Text
816
+ style={{
817
+ fontFamily: AppFonts.interBold,
818
+ fontSize: 10,
819
+ color: isActive ? AppColors.white : AppColors.grayText,
820
+ }}>
821
+ {s.label}
822
+ </Text>
823
+ </TouchableScale>
824
+ );
825
+ })}
826
+
827
+ <View
828
+ style={{
829
+ width: 1,
830
+ height: 14,
831
+ backgroundColor: AppColors.dividerColor,
832
+ marginHorizontal: 3,
833
+ }}
834
+ />
835
+
836
+ {/* Quick Query Suggestions */}
837
+ {QUICK_API_QUERY_TAGS.map(q => {
838
+ const isActive = search.includes(q.query);
839
+ return (
840
+ <TouchableScale
841
+ key={`quick_${q.query}`}
842
+ onPress={() => {
843
+ setSearch(prev => {
844
+ if (prev.includes(q.query)) {
845
+ return prev
846
+ .replace(q.query, '')
847
+ .replace(/\s+/g, ' ')
848
+ .trim();
849
+ }
850
+ return `${prev} ${q.query}`.trim();
851
+ });
852
+ }}
853
+ hitSlop={6}
854
+ style={{
855
+ flexDirection: 'row',
856
+ alignItems: 'center',
857
+ gap: 3.5,
858
+ paddingHorizontal: 7.5,
859
+ paddingVertical: 3.5,
860
+ borderRadius: 6,
861
+ backgroundColor: isActive
862
+ ? `${AppColors.purple}20`
863
+ : AppColors.grayBackground,
864
+ borderWidth: 1,
865
+ borderColor: isActive
866
+ ? AppColors.purple
867
+ : AppColors.grayBorderSecondary,
868
+ }}>
869
+ {q.icon(isActive ? AppColors.purple : AppColors.grayTextWeak)}
870
+ <Text
871
+ style={{
872
+ fontFamily: AppFonts.interMedium,
873
+ fontSize: 10,
874
+ color: isActive
875
+ ? AppColors.purple
876
+ : AppColors.grayText,
877
+ }}>
878
+ {q.label}
879
+ </Text>
880
+ </TouchableScale>
881
+ );
882
+ })}
883
+ </ScrollView>
884
+ </View>
885
+
775
886
  {/* Quick Filter Horizontal Chips Bar with Live Counts */}
776
887
  <View style={{marginBottom: 8}}>
777
888
  <ScrollView
@@ -1138,8 +1249,22 @@ const NetworkTab = React.memo(() => {
1138
1249
  ListEmptyComponent={
1139
1250
  <EmptyState
1140
1251
  isSearch={
1141
- search.length > 0 || statusFilters.size > 0
1252
+ search.length > 0 ||
1253
+ statusFilters.size > 0 ||
1254
+ methodFilters.size > 0
1255
+ }
1256
+ searchQuery={search}
1257
+ customTitle={
1258
+ search.length > 0
1259
+ ? 'No matching API requests'
1260
+ : 'No network activity'
1142
1261
  }
1262
+ onClearSearch={() => {
1263
+ setSearch('');
1264
+ setStatusFilters(new Set());
1265
+ setMethodFilters(new Set());
1266
+ setSearchScope('all');
1267
+ }}
1143
1268
  />
1144
1269
  }
1145
1270
  ListFooterComponent={
@@ -548,14 +548,32 @@ const ReduxTab = React.memo(() => {
548
548
  autoCapitalize="none"
549
549
  />
550
550
  {reduxSearch.length > 0 && (
551
- <Pressable
552
- accessible={true}
553
- accessibilityRole="button"
554
- accessibilityLabel="Clear search input"
555
- onPress={() => setReduxSearch('')}
556
- hitSlop={10}>
557
- <ClearIcon color={AppColors.grayTextWeak} size={14} />
558
- </Pressable>
551
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
552
+ <View
553
+ style={{
554
+ backgroundColor: `${AppColors.purple}20`,
555
+ borderRadius: 10,
556
+ paddingHorizontal: 6,
557
+ paddingVertical: 1.5,
558
+ }}>
559
+ <Text
560
+ style={{
561
+ color: AppColors.purple,
562
+ fontSize: 9.5,
563
+ fontFamily: AppFonts.interBold,
564
+ }}>
565
+ {filteredSlices.length}
566
+ </Text>
567
+ </View>
568
+ <Pressable
569
+ accessible={true}
570
+ accessibilityRole="button"
571
+ accessibilityLabel="Clear search input"
572
+ onPress={() => setReduxSearch('')}
573
+ hitSlop={10}>
574
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
575
+ </Pressable>
576
+ </View>
559
577
  )}
560
578
  </View>
561
579
 
@@ -207,8 +207,8 @@ const SettingsPanel = () => {
207
207
  bundle: Boolean(tabVisibility?.bundle),
208
208
  performance: Boolean(tabVisibility?.performance),
209
209
  crash: Boolean(tabVisibility?.crash),
210
- device: Boolean(tabVisibility?.device ?? true),
211
- storage: Boolean(tabVisibility?.storage ?? true),
210
+ device: Boolean(tabVisibility?.device),
211
+ storage: Boolean(tabVisibility?.storage),
212
212
  }));
213
213
 
214
214
  // Synchronize staged state with tabVisibility when tabVisibility updates
@@ -221,8 +221,8 @@ const SettingsPanel = () => {
221
221
  bundle: Boolean(tabVisibility?.bundle),
222
222
  performance: Boolean(tabVisibility?.performance),
223
223
  crash: Boolean(tabVisibility?.crash),
224
- device: Boolean(tabVisibility?.device ?? true),
225
- storage: Boolean(tabVisibility?.storage ?? true),
224
+ device: Boolean(tabVisibility?.device),
225
+ storage: Boolean(tabVisibility?.storage),
226
226
  });
227
227
  }, [tabVisibility]);
228
228
 
@@ -357,9 +357,27 @@ export const StorageTab = React.memo(() => {
357
357
  clearButtonMode="while-editing"
358
358
  />
359
359
  {search.length > 0 && (
360
- <TouchableScale onPress={() => setSearch('')} hitSlop={8}>
361
- <ClearIcon size={13} color={AppColors.grayTextWeak} />
362
- </TouchableScale>
360
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
361
+ <View
362
+ style={{
363
+ backgroundColor: `${AppColors.purple}20`,
364
+ borderRadius: 10,
365
+ paddingHorizontal: 6,
366
+ paddingVertical: 1.5,
367
+ }}>
368
+ <Text
369
+ style={{
370
+ color: AppColors.purple,
371
+ fontSize: 9.5,
372
+ fontFamily: AppFonts.interBold,
373
+ }}>
374
+ {filteredEntries.length}
375
+ </Text>
376
+ </View>
377
+ <TouchableScale onPress={() => setSearch('')} hitSlop={8}>
378
+ <ClearIcon size={13} color={AppColors.grayTextWeak} />
379
+ </TouchableScale>
380
+ </View>
363
381
  )}
364
382
  </View>
365
383
 
@@ -6,6 +6,7 @@ import {useTranslation} from '../i18n';
6
6
  import TreeNode from './TreeNode';
7
7
  import SegmentedTabs from './SegmentedTabs';
8
8
  import HighlightText from './HighlightText';
9
+ import CopyButton from './CopyButton';
9
10
  import {PrettyIcon, RawIcon, TableIcon} from './NetworkIcons';
10
11
 
11
12
  // Styles
@@ -260,6 +261,7 @@ const JsonViewer = React.memo(({
260
261
  setMode(key as 'pretty' | 'raw' | 'table')
261
262
  }
262
263
  />
264
+ <CopyButton value={data} label="JSON" />
263
265
  </View>
264
266
  )}
265
267
 
@@ -27,6 +27,7 @@ import {
27
27
 
28
28
  // Components
29
29
  import TouchableScale from './TouchableScale';
30
+ import CopyButton from './CopyButton';
30
31
 
31
32
  // Stylesheet
32
33
  import {AppColors} from '../styles/AppColors';
@@ -97,9 +98,23 @@ const MetaAccordion = ({
97
98
  <Pressable onPress={toggleOpen} hitSlop={12}>
98
99
  <View style={styles.metaHeader}>
99
100
  <Text style={styles.metaTitle}>Metadata</Text>
100
- <Animated.View style={chevronStyle}>
101
- <ChevronIcon color={AppColors.grayTextWeak} size={14} />
102
- </Animated.View>
101
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
102
+ <CopyButton
103
+ value={{
104
+ status: status ?? 0,
105
+ duration: duration != null ? `${duration} ms` : '—',
106
+ size: size || '—',
107
+ triggeredAt: triggeredAt || '',
108
+ method: method || 'GET',
109
+ contentType: contentType || 'application/json',
110
+ url: url || '',
111
+ }}
112
+ label="Metadata"
113
+ />
114
+ <Animated.View style={chevronStyle}>
115
+ <ChevronIcon color={AppColors.grayTextWeak} size={14} />
116
+ </Animated.View>
117
+ </View>
103
118
  </View>
104
119
  </Pressable>
105
120
 
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED FILE — do not edit by hand.
2
2
  // Regenerated from package.json on every build by scripts/gen-version.js.
3
- export const LIB_VERSION = '2.2.9';
3
+ export const LIB_VERSION = '2.3.0';
@@ -1,5 +1,6 @@
1
1
  import axios from "axios";
2
2
  import {setupGlobalCrashHandler} from "./crashHandler";
3
+ import {RouteInfo} from "../types";
3
4
 
4
5
  type NetworkLog = {
5
6
  id: number;
@@ -14,12 +15,20 @@ type NetworkLog = {
14
15
  client?: string; // ✅ Captures request client: axios, fetch, xhr, apollo, etc.
15
16
  requestHeaders?: Record<string, string>;
16
17
  responseHeaders?: Record<string, string>;
18
+ routeInfo?: RouteInfo;
17
19
  };
18
20
 
19
21
  let logs: NetworkLog[] = [];
20
22
  let listeners: ((logs: NetworkLog[]) => void)[] = [];
21
23
  let counter = 0;
22
24
  let isNetworkModuleEnabled = true;
25
+ let currentRouteProvider: (() => RouteInfo | null) | null = null;
26
+
27
+ export const setRouteInfoProvider = (
28
+ provider: (() => RouteInfo | null) | null,
29
+ ) => {
30
+ currentRouteProvider = provider;
31
+ };
23
32
 
24
33
  export const setNetworkModuleEnabled = (enabled: boolean) => {
25
34
  isNetworkModuleEnabled = enabled;
@@ -232,6 +241,8 @@ export const setupNetworkLogger = () => {
232
241
  client = 'axios';
233
242
  }
234
243
 
244
+ const currentRoute = currentRouteProvider ? currentRouteProvider() : null;
245
+
235
246
  addOrUpdateLog({
236
247
  id,
237
248
  url: finalUrl,
@@ -239,6 +250,7 @@ export const setupNetworkLogger = () => {
239
250
  startTime: start,
240
251
  caller,
241
252
  client,
253
+ routeInfo: currentRoute || undefined,
242
254
  request: method === "GET" ? undefined : parseRequestData(options?.body),
243
255
  requestHeaders,
244
256
  });
@@ -347,6 +359,8 @@ export const addAxiosInterceptors = (axiosInstance: any) => {
347
359
  config.__logStart = start;
348
360
  config.__logCaller = caller;
349
361
 
362
+ const currentRoute = currentRouteProvider ? currentRouteProvider() : null;
363
+
350
364
  addOrUpdateLog({
351
365
  id,
352
366
  url,
@@ -354,6 +368,7 @@ export const addAxiosInterceptors = (axiosInstance: any) => {
354
368
  startTime: start,
355
369
  caller,
356
370
  client: 'axios',
371
+ routeInfo: currentRoute || undefined,
357
372
  request: method === "GET" ? undefined : parseRequestData(config.data),
358
373
  requestHeaders: normaliseHeaders(config.headers),
359
374
  });
@@ -198,16 +198,37 @@ export const getNavigationInfo = (
198
198
  state: any,
199
199
  path: string[] = [],
200
200
  ): RouteInfo => {
201
- if (!state?.routes) {
201
+ if (!state) {
202
202
  return {
203
- path: path.length > 0 ? path.join(' ➔ ') : 'Navigators',
203
+ path: path.length > 0 ? path.join(' ➔ ') : '',
204
204
  params: null,
205
205
  };
206
206
  }
207
- const route = state.routes[state.index ?? 0];
207
+ if (state.name && !state.routes) {
208
+ const fullPath =
209
+ path.length > 0 ? [...path, state.name].join(' ➔ ') : state.name;
210
+ return {path: fullPath, params: state.params || null};
211
+ }
212
+ if (
213
+ !state.routes ||
214
+ !Array.isArray(state.routes) ||
215
+ state.routes.length === 0
216
+ ) {
217
+ return {
218
+ path: path.length > 0 ? path.join(' ➔ ') : '',
219
+ params: null,
220
+ };
221
+ }
222
+ const index =
223
+ typeof state.index === 'number' &&
224
+ state.index >= 0 &&
225
+ state.index < state.routes.length
226
+ ? state.index
227
+ : 0;
228
+ const route = state.routes[index];
208
229
  if (!route) {
209
230
  return {
210
- path: path.length > 0 ? path.join(' ➔ ') : 'Navigators',
231
+ path: path.length > 0 ? path.join(' ➔ ') : '',
211
232
  params: null,
212
233
  };
213
234
  }
@@ -217,10 +238,96 @@ export const getNavigationInfo = (
217
238
  return getNavigationInfo(route.state, currentPath);
218
239
  }
219
240
  const resolved =
220
- currentPath.length > 0 ? currentPath.join(' ➔ ') : 'Navigators';
241
+ currentPath.length > 0 ? currentPath.join(' ➔ ') : route.name || '';
221
242
  return {path: resolved, params: route.params || null};
222
243
  };
223
244
 
245
+ export const getLogPageName = (
246
+ log: NetworkLog,
247
+ routeInfo?: RouteInfo | null,
248
+ ): string => {
249
+ // 1. If routeInfo has a valid path, resolve the leaf screen name
250
+ const effectiveRoute = routeInfo || (log as any)?.routeInfo;
251
+ if (
252
+ effectiveRoute &&
253
+ effectiveRoute.path &&
254
+ effectiveRoute.path !== 'Navigators'
255
+ ) {
256
+ const parts = effectiveRoute.path
257
+ .split(' ➔ ')
258
+ .map((s: string) => s.trim())
259
+ .filter(Boolean);
260
+ if (parts.length > 0) {
261
+ return parts[parts.length - 1]; // Leaf screen name, e.g. "HomeScreen", "ProfileScreen"
262
+ }
263
+ return effectiveRoute.path;
264
+ }
265
+
266
+ // 2. Extract screen / component name from log.caller stack trace
267
+ if (log.caller && log.caller !== 'Unknown') {
268
+ try {
269
+ const match = log.caller.match(/([^/\\]+)\.(tsx|jsx|ts|js)/i);
270
+ if (match && match[1]) {
271
+ const file = match[1];
272
+ const ignored = [
273
+ 'networklogger',
274
+ 'fetch',
275
+ 'axios',
276
+ 'apiclient',
277
+ 'request',
278
+ 'http',
279
+ 'index',
280
+ 'nativeinspector',
281
+ 'inappinspector',
282
+ 'bundleanalyzer',
283
+ ];
284
+ if (!ignored.includes(file.toLowerCase())) {
285
+ return file;
286
+ }
287
+ }
288
+ } catch (e) {}
289
+ }
290
+
291
+ // 3. Fallback to API resource / endpoint category from URL
292
+ if (log.url) {
293
+ try {
294
+ const cleanUrl = log.url.split('?')[0].split('#')[0];
295
+ const withoutProto = cleanUrl.replace(/^https?:\/\//i, '');
296
+ const slashIdx = withoutProto.indexOf('/');
297
+ if (slashIdx !== -1) {
298
+ const pathSegments = withoutProto
299
+ .substring(slashIdx + 1)
300
+ .split('/')
301
+ .filter(Boolean);
302
+ const ignoredSegments = [
303
+ 'api',
304
+ 'v1',
305
+ 'v2',
306
+ 'v3',
307
+ 'v4',
308
+ 'rest',
309
+ 'graphql',
310
+ 'json',
311
+ 'data',
312
+ 'service',
313
+ 'app',
314
+ ];
315
+ const meaningful = pathSegments.filter(
316
+ s => !ignoredSegments.includes(s.toLowerCase()),
317
+ );
318
+ if (meaningful.length > 0) {
319
+ const seg = meaningful[0];
320
+ return seg.charAt(0).toUpperCase() + seg.slice(1);
321
+ }
322
+ }
323
+ const host = withoutProto.split('/')[0];
324
+ if (host) return host;
325
+ } catch (e) {}
326
+ }
327
+
328
+ return 'General';
329
+ };
330
+
224
331
  export const flattenObject = (obj: any, prefix = ''): Record<string, any> => {
225
332
  let result: Record<string, any> = {};
226
333
  if (typeof obj === 'object' && obj !== null) {
@@ -409,6 +516,107 @@ export const getAppName = (): string => {
409
516
  return 'App';
410
517
  };
411
518
 
519
+ export const getAppVersionAndBuild = (): {
520
+ version: string;
521
+ build: string;
522
+ formatted: string;
523
+ } => {
524
+ let version = '';
525
+ let build = '';
526
+
527
+ // 1. Try ExpoApplication / ExponentConstants
528
+ const ExpoApplication = NativeModules.ExpoApplication;
529
+ if (ExpoApplication) {
530
+ if (typeof ExpoApplication.nativeAppVersion === 'string') {
531
+ version = ExpoApplication.nativeAppVersion;
532
+ }
533
+ if (typeof ExpoApplication.nativeBuildVersion === 'string') {
534
+ build = ExpoApplication.nativeBuildVersion;
535
+ }
536
+ }
537
+
538
+ const ExponentConstants = NativeModules.ExponentConstants;
539
+ if (ExponentConstants) {
540
+ const manifest = ExponentConstants.manifest || ExponentConstants.expoConfig;
541
+ if (manifest) {
542
+ if (!version && manifest.version) {
543
+ version = String(manifest.version);
544
+ }
545
+ if (!build) {
546
+ if (manifest.ios?.buildNumber) {
547
+ build = String(manifest.ios.buildNumber);
548
+ } else if (manifest.android?.versionCode) {
549
+ build = String(manifest.android.versionCode);
550
+ }
551
+ }
552
+ }
553
+ }
554
+
555
+ // 2. Try react-native-device-info
556
+ const RNDeviceInfo = NativeModules.RNDeviceInfo;
557
+ if (RNDeviceInfo) {
558
+ if (!version) {
559
+ if (typeof RNDeviceInfo.appVersion === 'string') {
560
+ version = RNDeviceInfo.appVersion;
561
+ } else if (typeof RNDeviceInfo.getVersion === 'function') {
562
+ try {
563
+ version = RNDeviceInfo.getVersion();
564
+ } catch (e) {}
565
+ }
566
+ }
567
+ if (!build) {
568
+ if (typeof RNDeviceInfo.buildNumber === 'string') {
569
+ build = RNDeviceInfo.buildNumber;
570
+ } else if (typeof RNDeviceInfo.getBuildNumber === 'function') {
571
+ try {
572
+ build = RNDeviceInfo.getBuildNumber();
573
+ } catch (e) {}
574
+ }
575
+ }
576
+ }
577
+
578
+ // 3. Try PlatformConstants / Platform.constants
579
+ const constants =
580
+ (Platform.constants as any) || NativeModules.PlatformConstants;
581
+ if (constants) {
582
+ if (
583
+ !version &&
584
+ constants.Version &&
585
+ typeof constants.Version === 'string' &&
586
+ constants.Version.includes('.')
587
+ ) {
588
+ version = constants.Version;
589
+ }
590
+ if (!version && constants.reactNativeVersion) {
591
+ const rnv = constants.reactNativeVersion;
592
+ if (rnv.major !== undefined) {
593
+ version = `${rnv.major}.${rnv.minor}.${rnv.patch}`;
594
+ }
595
+ }
596
+ }
597
+
598
+ // 4. Try Android AppInfo
599
+ const AppInfo = NativeModules.AppInfo;
600
+ if (AppInfo) {
601
+ if (!version && typeof AppInfo.versionName === 'string') {
602
+ version = AppInfo.versionName;
603
+ }
604
+ if (!build && typeof AppInfo.versionCode === 'string') {
605
+ build = AppInfo.versionCode;
606
+ }
607
+ }
608
+
609
+ // Fallbacks
610
+ if (!version) version = '1.0.0';
611
+ if (!build) build = '1';
612
+
613
+ return {
614
+ version,
615
+ build,
616
+ formatted: `${version} (${build})`,
617
+ };
618
+ };
619
+
412
620
  export const handleOpenExternalLink = (url: string): void => {
413
621
  if (!url) return;
414
622
  const openUrl = formatDisplayUrl(url);