react-native-inapp-inspector 1.1.1 → 1.1.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.
Files changed (45) hide show
  1. package/README.md +14 -0
  2. package/dist/commonjs/components/ConsoleLogCard.js +18 -0
  3. package/dist/commonjs/components/JsonViewer.d.ts +2 -1
  4. package/dist/commonjs/components/JsonViewer.js +6 -4
  5. package/dist/commonjs/components/LogCard.js +17 -0
  6. package/dist/commonjs/components/MetaAccordion.js +26 -6
  7. package/dist/commonjs/components/NetworkIcons.d.ts +9 -2
  8. package/dist/commonjs/components/NetworkIcons.js +59 -3
  9. package/dist/commonjs/components/ReduxTreeView.js +80 -5
  10. package/dist/commonjs/constants/version.d.ts +1 -1
  11. package/dist/commonjs/constants/version.js +1 -1
  12. package/dist/commonjs/customHooks/reduxLogger.d.ts +21 -7
  13. package/dist/commonjs/customHooks/reduxLogger.js +147 -48
  14. package/dist/commonjs/customHooks/webViewLogger.js +13 -8
  15. package/dist/commonjs/helpers/settingsStore.d.ts +24 -0
  16. package/dist/commonjs/helpers/settingsStore.js +74 -0
  17. package/dist/commonjs/index.d.ts +1 -1
  18. package/dist/commonjs/index.js +897 -170
  19. package/dist/commonjs/styles/index.d.ts +40 -0
  20. package/dist/commonjs/styles/index.js +45 -2
  21. package/dist/commonjs/types/index.d.ts +4 -0
  22. package/dist/esm/components/ConsoleLogCard.js +18 -0
  23. package/dist/esm/components/JsonViewer.d.ts +2 -1
  24. package/dist/esm/components/JsonViewer.js +6 -4
  25. package/dist/esm/components/LogCard.js +17 -0
  26. package/dist/esm/components/MetaAccordion.js +27 -7
  27. package/dist/esm/components/NetworkIcons.d.ts +9 -2
  28. package/dist/esm/components/NetworkIcons.js +51 -2
  29. package/dist/esm/components/ReduxTreeView.js +81 -6
  30. package/dist/esm/constants/version.d.ts +1 -1
  31. package/dist/esm/constants/version.js +1 -1
  32. package/dist/esm/customHooks/reduxLogger.d.ts +21 -7
  33. package/dist/esm/customHooks/reduxLogger.js +145 -47
  34. package/dist/esm/customHooks/webViewLogger.js +13 -8
  35. package/dist/esm/helpers/settingsStore.d.ts +24 -0
  36. package/dist/esm/helpers/settingsStore.js +67 -0
  37. package/dist/esm/index.d.ts +1 -1
  38. package/dist/esm/index.js +896 -172
  39. package/dist/esm/styles/index.d.ts +40 -0
  40. package/dist/esm/styles/index.js +45 -2
  41. package/dist/esm/types/index.d.ts +4 -0
  42. package/example/App.tsx +199 -61
  43. package/example/ios/example.xcodeproj/project.pbxproj +0 -8
  44. package/example/package-lock.json +4 -3
  45. package/package.json +1 -1
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
2
- import { Alert, Animated, StyleSheet, FlatList, LayoutAnimation, Modal, Platform, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, TouchableOpacity, UIManager, LogBox, } from 'react-native';
2
+ import { Alert, Animated, StyleSheet, FlatList, LayoutAnimation, Modal, PanResponder, Platform, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, TouchableOpacity, UIManager, LogBox, } from 'react-native';
3
3
  import Svg, { Circle, Path } from 'react-native-svg';
4
4
  import LinearGradient from 'react-native-linear-gradient';
5
5
  import { useNavigationState, NavigationContext } from '@react-navigation/native';
@@ -23,8 +23,10 @@ import CodeSnippet from './components/CodeSnippet';
23
23
  import AnimatedEntrance from './components/AnimatedEntrance';
24
24
  // Helpers
25
25
  import { formatDateTime, getStatusColor, getNavigationInfo, deduplicateLogs, getDomainColor, formatDisplayUrl, getFetchCommand, getCurlCommand, getSize, } from './helpers';
26
+ // #5 — settings persistence
27
+ import { loadSettings, saveSettings } from './helpers/settingsStore';
26
28
  // Assets
27
- import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, InsightsIcon, GlobeIcon, DownloadIcon, CloseWhite, TrashIcon, WhiteBackNavigation, TerminalIcon, SignalIcon, AnalyticsIcon, SunIcon, MoonIcon, BrandCircleIcon, BrandSquareIcon, HtmlIcon, CssIcon, JsIcon, ClockIcon, EyeIcon, CheckIcon, SettingsIcon, RequestIcon, ResponseIcon, HeadersIcon, StatusIcon, } from './components/NetworkIcons';
29
+ import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, InsightsIcon, GlobeIcon, DownloadIcon, CloseWhite, TrashIcon, WhiteBackNavigation, TerminalIcon, SignalIcon, AnalyticsIcon, SunIcon, MoonIcon, BrandCircleIcon, BrandSquareIcon, HtmlIcon, CssIcon, JsIcon, ClockIcon, EyeIcon, CheckIcon, SettingsIcon, RequestIcon, ResponseIcon, HeadersIcon, StatusIcon, ChevronIcon, WipeIcon, LayersIcon, UserIcon, InfoCircleIcon, WarningTriangleIcon, ErrorCircleIcon, TrendingUpIcon, } from './components/NetworkIcons';
28
30
  import ErrorBoundary from './components/ErrorBoundary';
29
31
  // Stylesheet
30
32
  import { AppColors } from './styles/AppColors';
@@ -150,6 +152,8 @@ const NetworkInspector = ({ enabled = true, }) => {
150
152
  const [showNetworkMenu, setShowNetworkMenu] = useState(false);
151
153
  const [showUiMenu, setShowUiMenu] = useState(false);
152
154
  const [sortOrder, setSortOrder] = useState('newest');
155
+ // #7 — sort order for the Logs (console) tab
156
+ const [logSortOrder, setLogSortOrder] = useState('newest');
153
157
  const [reqExpanded, setReqExpanded] = useState(undefined);
154
158
  const [resExpanded, setResExpanded] = useState(undefined);
155
159
  const [showReqDiff, setShowReqDiff] = useState(false);
@@ -235,6 +239,141 @@ const NetworkInspector = ({ enabled = true, }) => {
235
239
  const [reduxExpandDepth, setReduxExpandDepth] = useState(1);
236
240
  const [slowRequestThreshold, setSlowRequestThreshold] = useState(1000);
237
241
  const [insightsShowConsoleAlerts, setInsightsShowConsoleAlerts] = useState(true);
242
+ // #6 — tab the inspector opens on. Shown with a DEFAULT badge in Settings.
243
+ const [defaultTab, setDefaultTab] = useState('apis');
244
+ // #9 — when false (default), consecutive identical entries in the API and
245
+ // Console lists are collapsed into one row with a ×N counter.
246
+ const [showDuplicateLogs, setShowDuplicateLogs] = useState(false);
247
+ // #5 — hydrate persisted settings once, then auto-save on any change.
248
+ const settingsHydratedRef = useRef(false);
249
+ useEffect(() => {
250
+ let cancelled = false;
251
+ loadSettings().then(saved => {
252
+ if (cancelled)
253
+ return;
254
+ if (saved.isDark != null) {
255
+ setIsDark(saved.isDark);
256
+ toggleGlobalTheme(saved.isDark);
257
+ }
258
+ if (saved.modalHeightPercent != null)
259
+ setModalHeightPercent(saved.modalHeightPercent);
260
+ if (saved.tabVisibility)
261
+ setTabVisibility(prev => ({
262
+ ...prev,
263
+ ...saved.tabVisibility,
264
+ apis: true, // APIs is always required
265
+ }));
266
+ if (saved.defaultTab)
267
+ setDefaultTab(saved.defaultTab);
268
+ if (saved.maxNetworkLogs != null)
269
+ setMaxNetworkLogs(saved.maxNetworkLogs);
270
+ if (saved.maxConsoleLogs != null)
271
+ setMaxConsoleLogs(saved.maxConsoleLogs);
272
+ if (saved.showConsoleLevels)
273
+ setShowConsoleLevels(saved.showConsoleLevels);
274
+ if (saved.webViewCaptureCssJs != null)
275
+ setWebViewCaptureCssJs(saved.webViewCaptureCssJs);
276
+ if (saved.reduxAutoRefresh != null)
277
+ setReduxAutoRefreshState(saved.reduxAutoRefresh);
278
+ if (saved.reduxExpandDepth != null)
279
+ setReduxExpandDepth(saved.reduxExpandDepth);
280
+ if (saved.slowRequestThreshold != null)
281
+ setSlowRequestThreshold(saved.slowRequestThreshold);
282
+ if (saved.insightsShowConsoleAlerts != null)
283
+ setInsightsShowConsoleAlerts(saved.insightsShowConsoleAlerts);
284
+ if (saved.showDuplicateLogs != null)
285
+ setShowDuplicateLogs(saved.showDuplicateLogs);
286
+ if (saved.defaultTab) {
287
+ const dt = saved.defaultTab;
288
+ const vis = {
289
+ ...{
290
+ insights: true,
291
+ apis: true,
292
+ logs: true,
293
+ analytics: true,
294
+ webview: true,
295
+ redux: true,
296
+ },
297
+ ...(saved.tabVisibility || {}),
298
+ apis: true,
299
+ };
300
+ setActiveTab(vis[dt] ? dt : 'apis');
301
+ }
302
+ settingsHydratedRef.current = true;
303
+ });
304
+ return () => {
305
+ cancelled = true;
306
+ };
307
+ }, []);
308
+ useEffect(() => {
309
+ if (!settingsHydratedRef.current)
310
+ return;
311
+ saveSettings({
312
+ isDark,
313
+ modalHeightPercent,
314
+ tabVisibility,
315
+ defaultTab,
316
+ maxNetworkLogs,
317
+ maxConsoleLogs,
318
+ showConsoleLevels,
319
+ webViewCaptureCssJs,
320
+ reduxAutoRefresh,
321
+ reduxExpandDepth,
322
+ slowRequestThreshold,
323
+ insightsShowConsoleAlerts,
324
+ showDuplicateLogs,
325
+ });
326
+ }, [
327
+ isDark,
328
+ modalHeightPercent,
329
+ tabVisibility,
330
+ defaultTab,
331
+ maxNetworkLogs,
332
+ maxConsoleLogs,
333
+ showConsoleLevels,
334
+ webViewCaptureCssJs,
335
+ reduxAutoRefresh,
336
+ reduxExpandDepth,
337
+ slowRequestThreshold,
338
+ insightsShowConsoleAlerts,
339
+ showDuplicateLogs,
340
+ ]);
341
+ // #1 — check NPM for a newer published version; surfaces an animated dot
342
+ // in the header next to the npm chip when an update is available.
343
+ const [latestNpmVersion, setLatestNpmVersion] = useState(null);
344
+ const updateAvailable = useMemo(() => {
345
+ if (!latestNpmVersion)
346
+ return false;
347
+ const parse = (v) => v
348
+ .replace(/^v/, '')
349
+ .split('.')
350
+ .map(n => parseInt(n, 10) || 0);
351
+ const cur = parse(LIB_VERSION);
352
+ const latest = parse(latestNpmVersion);
353
+ for (let i = 0; i < 3; i++) {
354
+ if ((latest[i] || 0) > (cur[i] || 0))
355
+ return true;
356
+ if ((latest[i] || 0) < (cur[i] || 0))
357
+ return false;
358
+ }
359
+ return false;
360
+ }, [latestNpmVersion]);
361
+ useEffect(() => {
362
+ let cancelled = false;
363
+ fetch('https://registry.npmjs.org/react-native-inapp-inspector/latest')
364
+ .then(res => (res.ok ? res.json() : null))
365
+ .then(data => {
366
+ if (!cancelled && data && typeof data.version === 'string') {
367
+ setLatestNpmVersion(data.version);
368
+ }
369
+ })
370
+ .catch(() => {
371
+ // Offline / blocked — silently skip the update check.
372
+ });
373
+ return () => {
374
+ cancelled = true;
375
+ };
376
+ }, []);
238
377
  useEffect(() => {
239
378
  setReduxAutoRefresh(reduxAutoRefresh);
240
379
  }, [reduxAutoRefresh]);
@@ -248,6 +387,10 @@ const NetworkInspector = ({ enabled = true, }) => {
248
387
  animateNextLayout();
249
388
  setActiveTab('apis');
250
389
  }
390
+ // #6 — a hidden module can't be the default landing tab.
391
+ if (!nextVal && defaultTab === key) {
392
+ setDefaultTab('apis');
393
+ }
251
394
  return newVisibility;
252
395
  });
253
396
  };
@@ -329,6 +472,68 @@ const NetworkInspector = ({ enabled = true, }) => {
329
472
  const badgeAnim = useRef(new Animated.Value(1)).current;
330
473
  const activePulseAnim = useRef(new Animated.Value(0.4)).current;
331
474
  const unreadPulseAnim = useRef(new Animated.Value(1)).current;
475
+ // #4 — diagonal light streak sweeping across the floating launcher
476
+ const fabShineAnim = useRef(new Animated.Value(0)).current;
477
+ // #11 — header "clear all" icon spin/scale animation
478
+ const clearAnim = useRef(new Animated.Value(0)).current;
479
+ // #4 — draggable floating launcher (drag anywhere on screen)
480
+ const fabPan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
481
+ const fabPanRef = useRef({ x: 0, y: 0 });
482
+ useEffect(() => {
483
+ const idX = fabPan.x.addListener(v => (fabPanRef.current.x = v.value));
484
+ const idY = fabPan.y.addListener(v => (fabPanRef.current.y = v.value));
485
+ return () => {
486
+ fabPan.x.removeListener(idX);
487
+ fabPan.y.removeListener(idY);
488
+ };
489
+ }, [fabPan]);
490
+ const fabDraggedRef = useRef(false);
491
+ const fabPanResponder = useRef(PanResponder.create({
492
+ // Let taps fall through to the launcher; only hijack once the
493
+ // finger actually moves, so onPress still fires on a tap.
494
+ onStartShouldSetPanResponder: () => false,
495
+ onMoveShouldSetPanResponder: (_e, g) => Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4,
496
+ onPanResponderGrant: () => {
497
+ fabDraggedRef.current = true;
498
+ fabPan.setOffset({
499
+ x: fabPanRef.current.x,
500
+ y: fabPanRef.current.y,
501
+ });
502
+ fabPan.setValue({ x: 0, y: 0 });
503
+ },
504
+ onPanResponderMove: Animated.event([null, { dx: fabPan.x, dy: fabPan.y }], {
505
+ useNativeDriver: false,
506
+ }),
507
+ onPanResponderRelease: () => {
508
+ fabPan.flattenOffset();
509
+ // small delay so the trailing tap (if any) is ignored
510
+ setTimeout(() => {
511
+ fabDraggedRef.current = false;
512
+ }, 50);
513
+ },
514
+ onPanResponderTerminate: () => {
515
+ fabPan.flattenOffset();
516
+ fabDraggedRef.current = false;
517
+ },
518
+ })).current;
519
+ // #2 — scroll-to-top button for the main APIs list, always visible at the
520
+ // bottom right of the list.
521
+ const apisListRef = useRef(null);
522
+ const runClearAllWithAnimation = useCallback(() => {
523
+ Animated.sequence([
524
+ Animated.timing(clearAnim, {
525
+ toValue: 1,
526
+ duration: 320,
527
+ useNativeDriver: true,
528
+ }),
529
+ Animated.timing(clearAnim, {
530
+ toValue: 0,
531
+ duration: 0,
532
+ useNativeDriver: true,
533
+ }),
534
+ ]).start();
535
+ handleClearAll();
536
+ }, [clearAnim]);
332
537
  useEffect(() => {
333
538
  if (Platform.OS === 'android') {
334
539
  UIManager.setLayoutAnimationEnabledExperimental?.(true);
@@ -350,6 +555,24 @@ const NetworkInspector = ({ enabled = true, }) => {
350
555
  loop.start();
351
556
  return () => loop.stop();
352
557
  }, [pulseAnim]);
558
+ // #4 — sweep the shine streak across the launcher, pause, repeat.
559
+ useEffect(() => {
560
+ const loop = Animated.loop(Animated.sequence([
561
+ Animated.timing(fabShineAnim, {
562
+ toValue: 1,
563
+ duration: 1100,
564
+ useNativeDriver: true,
565
+ }),
566
+ Animated.delay(1600),
567
+ Animated.timing(fabShineAnim, {
568
+ toValue: 0,
569
+ duration: 0,
570
+ useNativeDriver: true,
571
+ }),
572
+ ]));
573
+ loop.start();
574
+ return () => loop.stop();
575
+ }, [fabShineAnim]);
353
576
  useEffect(() => {
354
577
  const loop = Animated.loop(Animated.sequence([
355
578
  Animated.timing(activePulseAnim, {
@@ -393,6 +616,16 @@ const NetworkInspector = ({ enabled = true, }) => {
393
616
  }).start();
394
617
  }
395
618
  }, [newLogIds]);
619
+ // #6 — every time the inspector is opened, land on the chosen default tab.
620
+ useEffect(() => {
621
+ if (visible) {
622
+ const target = defaultTab === 'apis' || tabVisibility[defaultTab]
623
+ ? defaultTab
624
+ : 'apis';
625
+ setActiveTab(target);
626
+ }
627
+ // eslint-disable-next-line react-hooks/exhaustive-deps
628
+ }, [visible]);
396
629
  useEffect(() => {
397
630
  if (visible) {
398
631
  const task = InteractionManager.runAfterInteractions(() => {
@@ -546,8 +779,38 @@ const NetworkInspector = ({ enabled = true, }) => {
546
779
  if (sortOrder === 'oldest') {
547
780
  result = [...result].reverse();
548
781
  }
782
+ // #9 — collapse consecutive identical requests (same method + url +
783
+ // status) into one row carrying a ×N counter, unless the user opted in
784
+ // to seeing every duplicate via Settings → "Show Duplicate Logs".
785
+ if (!showDuplicateLogs) {
786
+ const collapsed = [];
787
+ for (const log of result) {
788
+ const last = collapsed[collapsed.length - 1];
789
+ if (last &&
790
+ last.method === log.method &&
791
+ last.url === log.url &&
792
+ last.status === log.status) {
793
+ collapsed[collapsed.length - 1] = {
794
+ ...last,
795
+ duplicateCount: (last.duplicateCount || 1) + 1,
796
+ };
797
+ }
798
+ else {
799
+ collapsed.push({ ...log, duplicateCount: 1 });
800
+ }
801
+ }
802
+ result = collapsed;
803
+ }
549
804
  return result.slice(0, maxNetworkLogs);
550
- }, [logs, search, statusFilters, methodFilters, sortOrder, maxNetworkLogs]);
805
+ }, [
806
+ logs,
807
+ search,
808
+ statusFilters,
809
+ methodFilters,
810
+ sortOrder,
811
+ maxNetworkLogs,
812
+ showDuplicateLogs,
813
+ ]);
551
814
  const availableMethods = useMemo(() => {
552
815
  const methods = new Set();
553
816
  logs.forEach(log => {
@@ -753,8 +1016,39 @@ const NetworkInspector = ({ enabled = true, }) => {
753
1016
  result = result.filter(log => log.message.toLowerCase().includes(s) ||
754
1017
  (log.caller ?? '').toLowerCase().includes(s));
755
1018
  }
1019
+ // #7 — apply sort order (newest/oldest first)
1020
+ result = [...result].sort((a, b) => logSortOrder === 'newest'
1021
+ ? b.timestamp - a.timestamp
1022
+ : a.timestamp - b.timestamp);
1023
+ // #9 — collapse consecutive identical messages into one row with a ×N
1024
+ // counter unless duplicates are explicitly enabled in Settings.
1025
+ if (!showDuplicateLogs) {
1026
+ const collapsed = [];
1027
+ for (const log of result) {
1028
+ const last = collapsed[collapsed.length - 1];
1029
+ if (last &&
1030
+ last.type === log.type &&
1031
+ last.sourceMethod === log.sourceMethod &&
1032
+ last.message === log.message) {
1033
+ collapsed[collapsed.length - 1] = {
1034
+ ...last,
1035
+ duplicateCount: (last.duplicateCount || 1) + 1,
1036
+ };
1037
+ }
1038
+ else {
1039
+ collapsed.push({ ...log, duplicateCount: 1 });
1040
+ }
1041
+ }
1042
+ result = collapsed;
1043
+ }
756
1044
  return result;
757
- }, [visibleConsoleLogs, logFilters, logSearch]);
1045
+ }, [
1046
+ visibleConsoleLogs,
1047
+ logFilters,
1048
+ logSearch,
1049
+ logSortOrder,
1050
+ showDuplicateLogs,
1051
+ ]);
758
1052
  const filteredWebViewLogs = useMemo(() => {
759
1053
  let result = webViewLogs;
760
1054
  if (webViewSearch) {
@@ -880,6 +1174,22 @@ const NetworkInspector = ({ enabled = true, }) => {
880
1174
  ]);
881
1175
  return;
882
1176
  }
1177
+ if (activeTab === 'redux') {
1178
+ Alert.alert('Clear Redux Timeline', 'Are you sure you want to clear the dispatched action history?', [
1179
+ { text: 'Cancel', style: 'cancel' },
1180
+ {
1181
+ text: 'Clear All',
1182
+ onPress: () => {
1183
+ clearActionHistory();
1184
+ setReduxActionHistory([]);
1185
+ setReduxLastActionMap({});
1186
+ },
1187
+ style: 'destructive',
1188
+ },
1189
+ ]);
1190
+ return;
1191
+ }
1192
+ // Default: APIs tab. Only clears NETWORK logs — never touches the other tabs.
883
1193
  if (selectedLogs.size > 0) {
884
1194
  setLogs(prev => prev.filter(l => !selectedLogs.has(l.id)));
885
1195
  setSelectedLogs(new Set());
@@ -887,10 +1197,26 @@ const NetworkInspector = ({ enabled = true, }) => {
887
1197
  else {
888
1198
  Alert.alert('Clear Logs', 'Are you sure you want to clear all network logs?', [
889
1199
  { text: 'Cancel', style: 'cancel' },
890
- { text: 'Clear All', onPress: handleClearAll, style: 'destructive' },
1200
+ {
1201
+ text: 'Clear All',
1202
+ onPress: clearNetworkOnly,
1203
+ style: 'destructive',
1204
+ },
891
1205
  ]);
892
1206
  }
893
1207
  }
1208
+ // Clears ONLY network logs + their derived selection/filter state.
1209
+ function clearNetworkOnly() {
1210
+ clearNetworkLogs();
1211
+ setLogs([]);
1212
+ setSelectedLogs(new Set());
1213
+ setSectionFilters({});
1214
+ setCollapsedSections(new Set());
1215
+ setStatusFilters(new Set());
1216
+ setMethodFilters(new Set());
1217
+ prevLogIdsRef.current = new Set();
1218
+ logRouteMapRef.current = new Map();
1219
+ }
894
1220
  const detailTitle = useMemo(() => {
895
1221
  if (!selected)
896
1222
  return '';
@@ -1119,7 +1445,8 @@ const NetworkInspector = ({ enabled = true, }) => {
1119
1445
  }}>
1120
1446
  {tab.label}
1121
1447
  </Text>
1122
- {isLocked && (<View style={{
1448
+ {/* #6 badge marks the configured default tab */}
1449
+ {tab.key === defaultTab && (<View style={{
1123
1450
  flexDirection: 'row',
1124
1451
  alignItems: 'center',
1125
1452
  backgroundColor: 'rgba(104,75,155,0.08)',
@@ -1224,7 +1551,7 @@ const NetworkInspector = ({ enabled = true, }) => {
1224
1551
  })}
1225
1552
  </View>
1226
1553
 
1227
- {/* Preferences Section */}
1554
+ {/* UI Preferences Section */}
1228
1555
  <View style={{ marginTop: 8 }}>
1229
1556
  <Text style={{
1230
1557
  fontFamily: AppFonts.interBold,
@@ -1233,7 +1560,7 @@ const NetworkInspector = ({ enabled = true, }) => {
1233
1560
  letterSpacing: 0.6,
1234
1561
  marginBottom: 8,
1235
1562
  }}>
1236
- PREFERENCES
1563
+ UI PREFERENCES
1237
1564
  </Text>
1238
1565
  <View style={{
1239
1566
  backgroundColor: AppColors.primaryLight,
@@ -1387,6 +1714,169 @@ const NetworkInspector = ({ enabled = true, }) => {
1387
1714
  })}
1388
1715
  </View>
1389
1716
  </View>
1717
+
1718
+ {/* Divider */}
1719
+ <View style={{
1720
+ height: 1,
1721
+ backgroundColor: AppColors.dividerColor,
1722
+ }}/>
1723
+
1724
+ {/* #6 — Default Tab */}
1725
+ <View style={{
1726
+ paddingVertical: 12,
1727
+ paddingHorizontal: 14,
1728
+ }}>
1729
+ <View style={{
1730
+ flexDirection: 'row',
1731
+ alignItems: 'center',
1732
+ gap: 8,
1733
+ }}>
1734
+ <View style={{
1735
+ width: 20,
1736
+ height: 20,
1737
+ borderRadius: 6,
1738
+ backgroundColor: AppColors.purpleShade50,
1739
+ borderWidth: 1,
1740
+ borderColor: 'rgba(104,75,155,0.2)',
1741
+ alignItems: 'center',
1742
+ justifyContent: 'center',
1743
+ }}>
1744
+ <LayersIcon color={AppColors.purple} size={11}/>
1745
+ </View>
1746
+ <View style={{ flex: 1 }}>
1747
+ <Text style={{
1748
+ fontFamily: AppFonts.interBold,
1749
+ fontSize: 13,
1750
+ color: AppColors.primaryBlack,
1751
+ }}>
1752
+ Default Tab
1753
+ </Text>
1754
+ <Text style={{
1755
+ fontFamily: AppFonts.interRegular,
1756
+ fontSize: 11,
1757
+ color: AppColors.grayText,
1758
+ marginTop: 1,
1759
+ }}>
1760
+ Tab shown when the inspector opens
1761
+ </Text>
1762
+ </View>
1763
+ </View>
1764
+
1765
+ {/* Segmented picker — only visible tabs are offered */}
1766
+ <View style={{
1767
+ flexDirection: 'row',
1768
+ flexWrap: 'wrap',
1769
+ backgroundColor: AppColors.grayBackground,
1770
+ borderRadius: 8,
1771
+ padding: 2.5,
1772
+ marginTop: 10,
1773
+ borderWidth: 1,
1774
+ borderColor: AppColors.dividerColor,
1775
+ gap: 2,
1776
+ }}>
1777
+ {settingsTabs
1778
+ .filter(tab => tab.key === 'apis' || tabVisibility[tab.key])
1779
+ .map(tab => {
1780
+ const isActive = defaultTab === tab.key;
1781
+ return (<TouchableScale key={tab.key} onPress={() => setDefaultTab(tab.key)} style={{
1782
+ paddingVertical: 6,
1783
+ paddingHorizontal: 10,
1784
+ alignItems: 'center',
1785
+ borderRadius: 6,
1786
+ backgroundColor: isActive
1787
+ ? AppColors.purple
1788
+ : 'transparent',
1789
+ }}>
1790
+ <Text style={{
1791
+ fontFamily: AppFonts.interBold,
1792
+ fontSize: 11,
1793
+ color: isActive
1794
+ ? '#FFFFFF'
1795
+ : AppColors.grayText,
1796
+ }}>
1797
+ {tab.label}
1798
+ </Text>
1799
+ </TouchableScale>);
1800
+ })}
1801
+ </View>
1802
+ </View>
1803
+
1804
+ {/* Divider */}
1805
+ <View style={{
1806
+ height: 1,
1807
+ backgroundColor: AppColors.dividerColor,
1808
+ }}/>
1809
+
1810
+ {/* #9 — Show Duplicate Logs */}
1811
+ <View style={{
1812
+ flexDirection: 'row',
1813
+ alignItems: 'center',
1814
+ paddingVertical: 12,
1815
+ paddingHorizontal: 14,
1816
+ gap: 12,
1817
+ }}>
1818
+ <View style={{
1819
+ flex: 1,
1820
+ flexDirection: 'row',
1821
+ alignItems: 'center',
1822
+ gap: 8,
1823
+ }}>
1824
+ <View style={{
1825
+ width: 20,
1826
+ height: 20,
1827
+ borderRadius: 6,
1828
+ backgroundColor: AppColors.purpleShade50,
1829
+ borderWidth: 1,
1830
+ borderColor: 'rgba(104,75,155,0.2)',
1831
+ alignItems: 'center',
1832
+ justifyContent: 'center',
1833
+ }}>
1834
+ <EyeIcon color={AppColors.purple} size={11}/>
1835
+ </View>
1836
+ <View style={{ flex: 1 }}>
1837
+ <Text style={{
1838
+ fontFamily: AppFonts.interBold,
1839
+ fontSize: 13,
1840
+ color: AppColors.primaryBlack,
1841
+ }}>
1842
+ Show Duplicate Logs
1843
+ </Text>
1844
+ <Text style={{
1845
+ fontFamily: AppFonts.interRegular,
1846
+ fontSize: 11,
1847
+ color: AppColors.grayText,
1848
+ marginTop: 1,
1849
+ }}>
1850
+ Off: repeated identical entries collapse into one row
1851
+ with a ×N count
1852
+ </Text>
1853
+ </View>
1854
+ </View>
1855
+
1856
+ {/* Toggle Switch */}
1857
+ <TouchableScale onPress={() => setShowDuplicateLogs(prev => !prev)} style={{
1858
+ width: 38,
1859
+ height: 22,
1860
+ borderRadius: 11,
1861
+ backgroundColor: showDuplicateLogs
1862
+ ? AppColors.purple
1863
+ : AppColors.grayBorderSecondary,
1864
+ padding: 2,
1865
+ justifyContent: 'center',
1866
+ alignItems: showDuplicateLogs ? 'flex-end' : 'flex-start',
1867
+ }}>
1868
+ <View style={{
1869
+ width: 18,
1870
+ height: 18,
1871
+ borderRadius: 9,
1872
+ backgroundColor: '#FFFFFF',
1873
+ shadowColor: '#000',
1874
+ shadowOpacity: 0.15,
1875
+ shadowRadius: 1.5,
1876
+ shadowOffset: { width: 0, height: 1 },
1877
+ }}/>
1878
+ </TouchableScale>
1879
+ </View>
1390
1880
  </View>
1391
1881
  </View>
1392
1882
  </ScrollView>
@@ -2489,50 +2979,6 @@ const NetworkInspector = ({ enabled = true, }) => {
2489
2979
  const lastActionMap = reduxLastActionMap;
2490
2980
  const actionHistory = reduxActionHistory;
2491
2981
  return (<ScrollView style={styles.detailScroll} contentContainerStyle={{ paddingBottom: 24 }}>
2492
- {/* Top Summary Card */}
2493
- <View style={{
2494
- backgroundColor: AppColors.primaryLight,
2495
- borderRadius: 12,
2496
- borderWidth: 1,
2497
- borderColor: AppColors.grayBorderSecondary,
2498
- padding: 14,
2499
- marginHorizontal: 16,
2500
- marginTop: 12,
2501
- marginBottom: 12,
2502
- flexDirection: 'row',
2503
- alignItems: 'center',
2504
- gap: 12,
2505
- }}>
2506
- <View style={{
2507
- width: 44,
2508
- height: 44,
2509
- borderRadius: 10,
2510
- backgroundColor: AppColors.purpleShade50,
2511
- alignItems: 'center',
2512
- justifyContent: 'center',
2513
- }}>
2514
- <TerminalIcon color={AppColors.purple} size={20}/>
2515
- </View>
2516
- <View style={{ flex: 1 }}>
2517
- <Text style={{
2518
- fontFamily: AppFonts.interBold,
2519
- fontSize: 13,
2520
- color: AppColors.primaryBlack,
2521
- }}>
2522
- Redux Store Snapshot
2523
- </Text>
2524
- <Text style={{
2525
- fontFamily: AppFonts.interRegular,
2526
- fontSize: 11,
2527
- color: AppColors.grayText,
2528
- marginTop: 2,
2529
- }}>
2530
- Total size: {getSize(reduxState)} • {reducerKeys.length} Reducers
2531
- </Text>
2532
- </View>
2533
- <CopyButton value={() => reduxState} label="Overall Store"/>
2534
- </View>
2535
-
2536
2982
  {/* Tab View Selection Segments */}
2537
2983
  <View style={{
2538
2984
  flexDirection: 'row',
@@ -2540,6 +2986,7 @@ const NetworkInspector = ({ enabled = true, }) => {
2540
2986
  borderRadius: 10,
2541
2987
  padding: 3,
2542
2988
  marginHorizontal: 16,
2989
+ marginTop: 12,
2543
2990
  marginBottom: 12,
2544
2991
  borderWidth: 1,
2545
2992
  borderColor: AppColors.dividerColor,
@@ -2575,6 +3022,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2575
3022
  }}>
2576
3023
  Action Timeline
2577
3024
  </Text>
3025
+ <View style={{
3026
+ minWidth: 18,
3027
+ paddingHorizontal: 5,
3028
+ height: 16,
3029
+ borderRadius: 8,
3030
+ alignItems: 'center',
3031
+ justifyContent: 'center',
3032
+ backgroundColor: reduxActiveSubTab === 'timeline'
3033
+ ? 'rgba(255,255,255,0.28)'
3034
+ : AppColors.dividerColor,
3035
+ }}>
3036
+ <Text style={{
3037
+ fontFamily: AppFonts.interBold,
3038
+ fontSize: 9,
3039
+ color: reduxActiveSubTab === 'timeline'
3040
+ ? '#FFFFFF'
3041
+ : AppColors.grayText,
3042
+ }}>
3043
+ {actionHistory.length}
3044
+ </Text>
3045
+ </View>
2578
3046
  </View>
2579
3047
  </TouchableOpacity>
2580
3048
  <TouchableOpacity onPress={() => {
@@ -2604,6 +3072,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2604
3072
  }}>
2605
3073
  Store Tree
2606
3074
  </Text>
3075
+ <View style={{
3076
+ minWidth: 18,
3077
+ paddingHorizontal: 5,
3078
+ height: 16,
3079
+ borderRadius: 8,
3080
+ alignItems: 'center',
3081
+ justifyContent: 'center',
3082
+ backgroundColor: reduxActiveSubTab === 'tree'
3083
+ ? 'rgba(255,255,255,0.28)'
3084
+ : AppColors.dividerColor,
3085
+ }}>
3086
+ <Text style={{
3087
+ fontFamily: AppFonts.interBold,
3088
+ fontSize: 9,
3089
+ color: reduxActiveSubTab === 'tree'
3090
+ ? '#FFFFFF'
3091
+ : AppColors.grayText,
3092
+ }}>
3093
+ {reducerKeys.length}
3094
+ </Text>
3095
+ </View>
2607
3096
  </View>
2608
3097
  </TouchableOpacity>
2609
3098
  </View>
@@ -2650,21 +3139,50 @@ const NetworkInspector = ({ enabled = true, }) => {
2650
3139
  };
2651
3140
  return (<>
2652
3141
  {hasNavigationContext && (<NavigationTracker onStateChange={setNavState}/>)}
2653
- <TouchableScale style={styles.fabWrapper} onPress={() => setVisible(true)} hitSlop={10}>
2654
- <Animated.View style={[styles.fabPulseRing, { transform: [{ scale: pulseAnim }] }]}/>
2655
- <BrandCircleIcon size={62}/>
2656
- {(logs.length > 0 || analyticsEvents.length > 0) && (<Animated.View style={[
3142
+ <Animated.View style={[styles.fabWrapper, { transform: fabPan.getTranslateTransform() }]} {...fabPanResponder.panHandlers}>
3143
+ <TouchableScale style={{ alignItems: 'center', justifyContent: 'center' }} onPress={() => {
3144
+ if (fabDraggedRef.current)
3145
+ return;
3146
+ setVisible(true);
3147
+ }} hitSlop={10}>
3148
+ <Animated.View style={[styles.fabPulseRing, { transform: [{ scale: pulseAnim }] }]}/>
3149
+ <BrandCircleIcon size={62}/>
3150
+ {/* #4 — shining sweep, clipped inside the circular launcher */}
3151
+ <View pointerEvents="none" style={styles.fabShineClip}>
3152
+ <Animated.View style={[
3153
+ styles.fabShineStreak,
3154
+ {
3155
+ transform: [
3156
+ {
3157
+ translateX: fabShineAnim.interpolate({
3158
+ inputRange: [0, 1],
3159
+ outputRange: [-48, 96],
3160
+ }),
3161
+ },
3162
+ { rotate: '25deg' },
3163
+ ],
3164
+ },
3165
+ ]}>
3166
+ <LinearGradient colors={[
3167
+ 'rgba(255,255,255,0)',
3168
+ 'rgba(255,255,255,0.55)',
3169
+ 'rgba(255,255,255,0)',
3170
+ ]} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} style={{ flex: 1 }}/>
3171
+ </Animated.View>
3172
+ </View>
3173
+ {(logs.length > 0 || analyticsEvents.length > 0) && (<Animated.View style={[
2657
3174
  styles.fabBadge,
2658
3175
  hasErrors ? styles.fabBadgeError : styles.fabBadgeNormal,
2659
3176
  { transform: [{ scale: badgeAnim }] },
2660
3177
  ]}>
2661
- <Text style={styles.fabBadgeText}>
2662
- {logs.length + analyticsEvents.length > 99
3178
+ <Text style={styles.fabBadgeText}>
3179
+ {logs.length + analyticsEvents.length > 99
2663
3180
  ? '99+'
2664
3181
  : logs.length + analyticsEvents.length}
2665
- </Text>
2666
- </Animated.View>)}
2667
- </TouchableScale>
3182
+ </Text>
3183
+ </Animated.View>)}
3184
+ </TouchableScale>
3185
+ </Animated.View>
2668
3186
 
2669
3187
  <Modal visible={visible} animationType="slide" transparent>
2670
3188
  {visible && (<ErrorBoundary onClose={closeModal}>
@@ -2694,10 +3212,27 @@ const NetworkInspector = ({ enabled = true, }) => {
2694
3212
  setSelectedEvent(null);
2695
3213
  });
2696
3214
  }} hitSlop={15} style={[
2697
- styles.iconBtnMinimal,
3215
+ {
3216
+ width: 38,
3217
+ height: 38,
3218
+ borderRadius: 19,
3219
+ alignItems: 'center',
3220
+ justifyContent: 'center',
3221
+ backgroundColor: 'rgba(255,255,255,0.18)',
3222
+ borderWidth: 1,
3223
+ borderColor: 'rgba(255,255,255,0.30)',
3224
+ },
2698
3225
  selected == null &&
2699
3226
  selectedEvent == null && { display: 'none' },
2700
3227
  ]}>
3228
+ {/* Soft outer glow to fake a blurred circle */}
3229
+ <View style={{
3230
+ position: 'absolute',
3231
+ width: 48,
3232
+ height: 48,
3233
+ borderRadius: 24,
3234
+ backgroundColor: 'rgba(255,255,255,0.10)',
3235
+ }}/>
2701
3236
  <WhiteBackNavigation />
2702
3237
  </TouchableScale>
2703
3238
 
@@ -2708,8 +3243,8 @@ const NetworkInspector = ({ enabled = true, }) => {
2708
3243
  flex: 1,
2709
3244
  }}>
2710
3245
  <View style={{
2711
- width: 42,
2712
- height: 42,
3246
+ width: 50,
3247
+ height: 50,
2713
3248
  borderRadius: 10,
2714
3249
  backgroundColor: 'rgba(255,255,255,0.13)',
2715
3250
  borderWidth: 1.5,
@@ -2721,13 +3256,10 @@ const NetworkInspector = ({ enabled = true, }) => {
2721
3256
  shadowRadius: 4,
2722
3257
  shadowOffset: { width: 0, height: 2 },
2723
3258
  }}>
2724
- <BrandSquareIcon size={36}/>
3259
+ <BrandSquareIcon size={45}/>
2725
3260
  </View>
2726
3261
  <View style={{ gap: 3 }}>
2727
- <Text style={[
2728
- styles.headerTitle,
2729
- { fontSize: 17, letterSpacing: 0.2 },
2730
- ]}>
3262
+ <Text style={[styles.headerTitle]}>
2731
3263
  RN InApp Inspector
2732
3264
  </Text>
2733
3265
  <View style={{
@@ -2810,6 +3342,29 @@ const NetworkInspector = ({ enabled = true, }) => {
2810
3342
  </Text>
2811
3343
  </View>
2812
3344
  </View>
3345
+
3346
+ {/* #1 — pulsing dot when a newer version is on NPM */}
3347
+ {updateAvailable && (<Pressable hitSlop={10} onPress={() => Alert.alert('Update Available', `react-native-inapp-inspector v${latestNpmVersion} is available on NPM (installed: v${LIB_VERSION}).`, [
3348
+ { text: 'Later', style: 'cancel' },
3349
+ {
3350
+ text: 'View on NPM',
3351
+ onPress: () => Linking.openURL('https://www.npmjs.com/package/react-native-inapp-inspector').catch(() => { }),
3352
+ },
3353
+ ])} style={{
3354
+ alignItems: 'center',
3355
+ justifyContent: 'center',
3356
+ }}>
3357
+ <Animated.View style={{
3358
+ width: 8,
3359
+ height: 8,
3360
+ borderRadius: 4,
3361
+ backgroundColor: '#4ADE80',
3362
+ borderWidth: 1,
3363
+ borderColor: 'rgba(255,255,255,0.9)',
3364
+ opacity: activePulseAnim,
3365
+ transform: [{ scale: unreadPulseAnim }],
3366
+ }}/>
3367
+ </Pressable>)}
2813
3368
  </View>
2814
3369
  </View>
2815
3370
  </View>) : null}
@@ -2834,21 +3389,48 @@ const NetworkInspector = ({ enabled = true, }) => {
2834
3389
  </Text>
2835
3390
  </View>
2836
3391
  <View style={styles.headerDetailSubRow}>
2837
- <View style={[
3392
+ <View style={{
3393
+ flexDirection: 'row',
3394
+ alignItems: 'center',
3395
+ gap: 5,
3396
+ paddingHorizontal: 8,
3397
+ paddingVertical: 3,
3398
+ borderRadius: 20,
3399
+ backgroundColor: `${getStatusColor(selected.status)}26`,
3400
+ borderWidth: 1,
3401
+ borderColor: `${getStatusColor(selected.status)}55`,
3402
+ }}>
3403
+ <View style={[
2838
3404
  styles.headerStatusDot,
2839
3405
  {
2840
3406
  backgroundColor: getStatusColor(selected.status),
2841
3407
  },
2842
3408
  ]}/>
2843
- <Text style={styles.headerSubTitle}>
2844
- {selected.status === 0
3409
+ <Text style={[
3410
+ styles.headerSubTitle,
3411
+ { fontFamily: AppFonts.interBold },
3412
+ ]}>
3413
+ {selected.status === 0
2845
3414
  ? 'Failed'
2846
- : selected.status ?? 'Pending'}{' '}
2847
- •{' '}
2848
- {selected.duration != null
3415
+ : selected.status ?? 'Pending'}
3416
+ </Text>
3417
+ </View>
3418
+ <View style={{
3419
+ flexDirection: 'row',
3420
+ alignItems: 'center',
3421
+ gap: 4,
3422
+ paddingHorizontal: 8,
3423
+ paddingVertical: 3,
3424
+ borderRadius: 20,
3425
+ backgroundColor: 'rgba(255,255,255,0.16)',
3426
+ }}>
3427
+ <ClockIcon color="#FFFFFF" size={11}/>
3428
+ <Text style={styles.headerSubTitle}>
3429
+ {selected.duration != null
2849
3430
  ? `${selected.duration}ms`
2850
- : '-'}
2851
- </Text>
3431
+ : ''}
3432
+ </Text>
3433
+ </View>
2852
3434
  </View>
2853
3435
  </View>) : selectedEvent != null ? (<View style={styles.headerDetailCenter}>
2854
3436
  <View style={styles.headerDetailRow}>
@@ -2891,7 +3473,50 @@ const NetworkInspector = ({ enabled = true, }) => {
2891
3473
  </View>) : null}
2892
3474
  </View>
2893
3475
 
2894
- <View style={styles.headerRight}>
3476
+ <View style={[
3477
+ styles.headerRight,
3478
+ selected == null &&
3479
+ selectedEvent == null && {
3480
+ flexShrink: 0,
3481
+ minWidth: 116,
3482
+ },
3483
+ ]}>
3484
+ {selected == null && selectedEvent == null && (<TouchableScale onPress={() => {
3485
+ Alert.alert('Clear Everything', 'This clears all tabs — APIs, Logs, Analytics, WebView and Redux timeline. Continue?', [
3486
+ { text: 'Cancel', style: 'cancel' },
3487
+ {
3488
+ text: 'Clear All',
3489
+ onPress: runClearAllWithAnimation,
3490
+ style: 'destructive',
3491
+ },
3492
+ ]);
3493
+ }} hitSlop={15} style={[
3494
+ styles.closeButtonSquare,
3495
+ {
3496
+ marginRight: 8,
3497
+ backgroundColor: 'rgba(255,255,255,0.15)',
3498
+ },
3499
+ ]}>
3500
+ <Animated.View style={{
3501
+ transform: [
3502
+ {
3503
+ rotate: clearAnim.interpolate({
3504
+ inputRange: [0, 1],
3505
+ outputRange: ['0deg', '-25deg'],
3506
+ }),
3507
+ },
3508
+ {
3509
+ scale: clearAnim.interpolate({
3510
+ inputRange: [0, 0.5, 1],
3511
+ outputRange: [1, 1.25, 1],
3512
+ }),
3513
+ },
3514
+ ],
3515
+ }}>
3516
+ <WipeIcon color="#FFFFFF" size={16}/>
3517
+ </Animated.View>
3518
+ </TouchableScale>)}
3519
+
2895
3520
  {selected == null && selectedEvent == null && (<TouchableScale onPress={() => setSettingsPage('main')} hitSlop={15} style={[
2896
3521
  styles.closeButtonSquare,
2897
3522
  {
@@ -3068,7 +3693,16 @@ const NetworkInspector = ({ enabled = true, }) => {
3068
3693
  animateNextLayout();
3069
3694
  setAnalyticsSubTab('ga_events');
3070
3695
  }}>
3071
- <Text style={[
3696
+ <View style={{
3697
+ flexDirection: 'row',
3698
+ alignItems: 'center',
3699
+ gap: 6,
3700
+ }}>
3701
+ {/* #7 */}
3702
+ <AnalyticsIcon size={13} color={analyticsSubTab === 'ga_events'
3703
+ ? AppColors.purple
3704
+ : AppColors.grayTextStrong}/>
3705
+ <Text style={[
3072
3706
  {
3073
3707
  fontFamily: AppFonts.interMedium,
3074
3708
  fontSize: 13,
@@ -3079,12 +3713,13 @@ const NetworkInspector = ({ enabled = true, }) => {
3079
3713
  color: AppColors.purple,
3080
3714
  },
3081
3715
  ]}>
3082
- GA Events (
3083
- {analyticsSearch
3716
+ GA Events (
3717
+ {analyticsSearch
3084
3718
  ? filteredAnalyticsEvents.length
3085
3719
  : analyticsEvents.length}
3086
- )
3087
- </Text>
3720
+ )
3721
+ </Text>
3722
+ </View>
3088
3723
  </Pressable>
3089
3724
  <Pressable style={[
3090
3725
  {
@@ -3105,7 +3740,16 @@ const NetworkInspector = ({ enabled = true, }) => {
3105
3740
  animateNextLayout();
3106
3741
  setAnalyticsSubTab('top_events');
3107
3742
  }}>
3108
- <Text style={[
3743
+ <View style={{
3744
+ flexDirection: 'row',
3745
+ alignItems: 'center',
3746
+ gap: 6,
3747
+ }}>
3748
+ {/* #7 */}
3749
+ <TrendingUpIcon size={13} color={analyticsSubTab === 'top_events'
3750
+ ? AppColors.purple
3751
+ : AppColors.grayTextStrong}/>
3752
+ <Text style={[
3109
3753
  {
3110
3754
  fontFamily: AppFonts.interMedium,
3111
3755
  fontSize: 13,
@@ -3116,8 +3760,9 @@ const NetworkInspector = ({ enabled = true, }) => {
3116
3760
  color: AppColors.purple,
3117
3761
  },
3118
3762
  ]}>
3119
- Top Events ({topEventsArray.length})
3120
- </Text>
3763
+ Top Events ({topEventsArray.length})
3764
+ </Text>
3765
+ </View>
3121
3766
  </Pressable>
3122
3767
  </View>
3123
3768
  </View>)}
@@ -3226,50 +3871,53 @@ const NetworkInspector = ({ enabled = true, }) => {
3226
3871
  filteredAnalyticsEvents.length === 0 && {
3227
3872
  flexGrow: 1,
3228
3873
  },
3229
- ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (<FlatList data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
3230
- <View style={styles.toolbarRow}>
3231
- <View style={styles.searchContainer}>
3232
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
3233
- <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
3234
- {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
3235
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
3236
- </Pressable>)}
3237
- </View>
3874
+ ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (<View style={{ flex: 1 }}>
3875
+ <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
3876
+ <View style={styles.toolbarRow}>
3877
+ <View style={styles.searchContainer}>
3878
+ <SearchIcon color={AppColors.grayTextWeak} size={16}/>
3879
+ <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
3880
+ {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
3881
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
3882
+ </Pressable>)}
3883
+ </View>
3238
3884
 
3239
- <View style={styles.toolbarRight}>
3240
- <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3241
- <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3242
- {selectedLogs.size > 0 && (<View style={styles.trashBadge}>
3243
- <Text style={styles.trashBadgeText}>
3244
- {selectedLogs.size}
3245
- </Text>
3246
- </View>)}
3247
- </TouchableScale>
3885
+ <View style={styles.toolbarRight}>
3886
+ <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3887
+ <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3888
+ {selectedLogs.size > 0 && (<View style={styles.trashBadge}>
3889
+ <Text style={styles.trashBadgeText}>
3890
+ {selectedLogs.size}
3891
+ </Text>
3892
+ </View>)}
3893
+ </TouchableScale>
3248
3894
 
3249
- <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
3250
- <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
3251
- </TouchableScale>
3895
+ <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
3896
+ <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
3897
+ </TouchableScale>
3252
3898
 
3253
- <TouchableScale style={[
3899
+ <TouchableScale style={[
3254
3900
  styles.toolbarBtn,
3255
3901
  filtersAccordion.isOpen &&
3256
3902
  styles.toolbarBtnActive,
3257
3903
  ]} onPress={filtersAccordion.toggleOpen} hitSlop={10}>
3258
- <FilterIcon color={filtersAccordion.isOpen
3904
+ <FilterIcon color={filtersAccordion.isOpen
3259
3905
  ? AppColors.purple
3260
3906
  : AppColors.grayTextStrong} size={18}/>
3261
- </TouchableScale>
3907
+ </TouchableScale>
3908
+ </View>
3262
3909
  </View>
3263
- </View>
3264
3910
 
3265
- <Animated.View style={[
3911
+ <Animated.View style={[
3266
3912
  filtersAccordion.bodyStyle,
3267
3913
  { overflow: 'hidden' },
3268
3914
  ]}>
3269
- <View style={styles.filtersContainer}>
3270
- <Text style={styles.filtersHeading}>STATUS</Text>
3271
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3272
- {STATUS_FILTERS.map(filter => {
3915
+ <View style={styles.filtersContainer}>
3916
+ <Text style={styles.filtersHeading}>
3917
+ STATUS
3918
+ </Text>
3919
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3920
+ {STATUS_FILTERS.map(filter => {
3273
3921
  const isAll = filter === 'ALL';
3274
3922
  const active = isAll
3275
3923
  ? statusFilters.size === 0
@@ -3288,38 +3936,38 @@ const NetworkInspector = ({ enabled = true, }) => {
3288
3936
  });
3289
3937
  }
3290
3938
  }} hitSlop={10}>
3291
- {active ? (<View style={[
3939
+ {active ? (<View style={[
3292
3940
  styles.statusFilterChip,
3293
3941
  styles.statusFilterActive,
3294
3942
  { overflow: 'hidden' },
3295
3943
  ]}>
3296
- <LinearGradient colors={[
3944
+ <LinearGradient colors={[
3297
3945
  AppColors.purpleShade50,
3298
3946
  '#EAE5FF',
3299
3947
  ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
3300
- <Text style={[
3948
+ <Text style={[
3301
3949
  styles.statusFilterText,
3302
3950
  { color: AppColors.purple },
3303
3951
  ]}>
3304
- {filter}
3305
- </Text>
3306
- </View>) : (<View style={styles.statusFilterChip}>
3307
- <Text style={styles.statusFilterText}>
3308
- {filter}
3309
- </Text>
3310
- </View>)}
3311
- </TouchableScale>);
3952
+ {filter}
3953
+ </Text>
3954
+ </View>) : (<View style={styles.statusFilterChip}>
3955
+ <Text style={styles.statusFilterText}>
3956
+ {filter}
3957
+ </Text>
3958
+ </View>)}
3959
+ </TouchableScale>);
3312
3960
  })}
3313
- </ScrollView>
3961
+ </ScrollView>
3314
3962
 
3315
- <Text style={[
3963
+ <Text style={[
3316
3964
  styles.filtersHeading,
3317
3965
  { marginTop: 16 },
3318
3966
  ]}>
3319
- METHOD
3320
- </Text>
3321
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3322
- {availableMethods.map(filter => {
3967
+ METHOD
3968
+ </Text>
3969
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
3970
+ {availableMethods.map(filter => {
3323
3971
  const isAll = filter === 'ALL';
3324
3972
  const active = isAll
3325
3973
  ? methodFilters.size === 0
@@ -3338,43 +3986,55 @@ const NetworkInspector = ({ enabled = true, }) => {
3338
3986
  });
3339
3987
  }
3340
3988
  }} hitSlop={10}>
3341
- {active ? (<View style={[
3989
+ {active ? (<View style={[
3342
3990
  styles.statusFilterChip,
3343
3991
  styles.statusFilterActive,
3344
3992
  { overflow: 'hidden' },
3345
3993
  ]}>
3346
- <LinearGradient colors={[
3994
+ <LinearGradient colors={[
3347
3995
  AppColors.purpleShade50,
3348
3996
  '#EAE5FF',
3349
3997
  ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
3350
- <Text style={[
3998
+ <Text style={[
3351
3999
  styles.statusFilterText,
3352
4000
  { color: AppColors.purple },
3353
4001
  ]}>
3354
- {filter}
3355
- </Text>
3356
- </View>) : (<View style={styles.statusFilterChip}>
3357
- <Text style={styles.statusFilterText}>
3358
- {filter}
3359
- </Text>
3360
- </View>)}
3361
- </TouchableScale>);
4002
+ {filter}
4003
+ </Text>
4004
+ </View>) : (<View style={styles.statusFilterChip}>
4005
+ <Text style={styles.statusFilterText}>
4006
+ {filter}
4007
+ </Text>
4008
+ </View>)}
4009
+ </TouchableScale>);
3362
4010
  })}
3363
- </ScrollView>
3364
- </View>
3365
- </Animated.View>
4011
+ </ScrollView>
4012
+ </View>
4013
+ </Animated.View>
3366
4014
 
3367
- {(search ||
4015
+ {(search ||
3368
4016
  statusFilters.size > 0 ||
3369
4017
  methodFilters.size > 0) && (<Text style={styles.resultCount}>
3370
- {filteredLogs.length === logs.length
4018
+ {filteredLogs.length === logs.length
3371
4019
  ? `${logs.length} requests`
3372
4020
  : `${filteredLogs.length} of ${logs.length} filtered requests`}
3373
- </Text>)}
3374
- </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
4021
+ </Text>)}
4022
+ </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
3375
4023
  styles.listContent,
3376
4024
  filteredLogs.length === 0 && { flexGrow: 1 },
3377
- ]} keyboardShouldPersistTaps="handled"/>) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
4025
+ ]} keyboardShouldPersistTaps="handled"/>
4026
+ {/* #2 — always-visible scroll-to-top, bottom right */}
4027
+ <TouchableScale onPress={() => {
4028
+ apisListRef.current?.scrollToOffset({
4029
+ offset: 0,
4030
+ animated: true,
4031
+ });
4032
+ }} hitSlop={10} style={styles.scrollTopBtn}>
4033
+ <View style={{ transform: [{ rotate: '180deg' }] }}>
4034
+ <ChevronIcon color="#FFFFFF" size={18}/>
4035
+ </View>
4036
+ </TouchableScale>
4037
+ </View>) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
3378
4038
  <View style={{
3379
4039
  backgroundColor: '#FFFFFF',
3380
4040
  borderBottomWidth: 1,
@@ -3394,6 +4054,9 @@ const NetworkInspector = ({ enabled = true, }) => {
3394
4054
  </View>
3395
4055
 
3396
4056
  <View style={styles.toolbarRight}>
4057
+ <TouchableScale style={styles.toolbarBtn} onPress={() => setLogSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
4058
+ <SortArrowIcon color={AppColors.grayTextStrong} size={18} direction={logSortOrder === 'newest' ? 'down' : 'up'}/>
4059
+ </TouchableScale>
3397
4060
  <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
3398
4061
  <TrashIcon color={AppColors.grayTextStrong} size={18}/>
3399
4062
  </TouchableScale>
@@ -3419,15 +4082,25 @@ const NetworkInspector = ({ enabled = true, }) => {
3419
4082
  backgroundColor: '#F4EBFF',
3420
4083
  },
3421
4084
  ]}>
3422
- <Text numberOfLines={1} style={[
4085
+ {/* #7 */}
4086
+ <View style={{
4087
+ flexDirection: 'row',
4088
+ alignItems: 'center',
4089
+ gap: 5,
4090
+ }}>
4091
+ <LayersIcon size={12} color={active
4092
+ ? AppColors.purpleShade700
4093
+ : AppColors.grayTextStrong}/>
4094
+ <Text numberOfLines={1} style={[
3423
4095
  styles.statusFilterText,
3424
4096
  active && {
3425
4097
  color: AppColors.purpleShade700,
3426
4098
  fontFamily: AppFonts.interBold,
3427
4099
  },
3428
4100
  ]}>
3429
- All ({logCounts.all})
3430
- </Text>
4101
+ All ({logCounts.all})
4102
+ </Text>
4103
+ </View>
3431
4104
  </View>
3432
4105
  </TouchableScale>);
3433
4106
  })()}
@@ -3453,15 +4126,25 @@ const NetworkInspector = ({ enabled = true, }) => {
3453
4126
  backgroundColor: '#F1F5F9',
3454
4127
  },
3455
4128
  ]}>
3456
- <Text numberOfLines={1} style={[
4129
+ {/* #7 */}
4130
+ <View style={{
4131
+ flexDirection: 'row',
4132
+ alignItems: 'center',
4133
+ gap: 5,
4134
+ }}>
4135
+ <UserIcon size={12} color={active
4136
+ ? '#334155'
4137
+ : AppColors.grayTextStrong}/>
4138
+ <Text numberOfLines={1} style={[
3457
4139
  styles.statusFilterText,
3458
4140
  active && {
3459
4141
  color: '#334155',
3460
4142
  fontFamily: AppFonts.interBold,
3461
4143
  },
3462
4144
  ]}>
3463
- User Log ({logCounts['user-log']})
3464
- </Text>
4145
+ User Log ({logCounts['user-log']})
4146
+ </Text>
4147
+ </View>
3465
4148
  </View>
3466
4149
  </TouchableScale>);
3467
4150
  })()}
@@ -3487,15 +4170,25 @@ const NetworkInspector = ({ enabled = true, }) => {
3487
4170
  backgroundColor: AppColors.purpleShade50,
3488
4171
  },
3489
4172
  ]}>
3490
- <Text numberOfLines={1} style={[
4173
+ {/* #7 */}
4174
+ <View style={{
4175
+ flexDirection: 'row',
4176
+ alignItems: 'center',
4177
+ gap: 5,
4178
+ }}>
4179
+ <InfoCircleIcon size={12} color={active
4180
+ ? AppColors.purple
4181
+ : AppColors.grayTextStrong}/>
4182
+ <Text numberOfLines={1} style={[
3491
4183
  styles.statusFilterText,
3492
4184
  active && {
3493
4185
  color: AppColors.purple,
3494
4186
  fontFamily: AppFonts.interBold,
3495
4187
  },
3496
4188
  ]}>
3497
- Info ({logCounts.info})
3498
- </Text>
4189
+ Info ({logCounts.info})
4190
+ </Text>
4191
+ </View>
3499
4192
  </View>
3500
4193
  </TouchableScale>);
3501
4194
  })()}
@@ -3521,7 +4214,17 @@ const NetworkInspector = ({ enabled = true, }) => {
3521
4214
  backgroundColor: '#FFFDF6',
3522
4215
  },
3523
4216
  ]}>
3524
- <Text numberOfLines={1} style={[
4217
+ {/* #7 */}
4218
+ <View style={{
4219
+ flexDirection: 'row',
4220
+ alignItems: 'center',
4221
+ gap: 5,
4222
+ }}>
4223
+ <WarningTriangleIcon size={12} color={active
4224
+ ? AppColors.darkOrange ||
4225
+ AppColors.lightOrange
4226
+ : AppColors.grayTextStrong}/>
4227
+ <Text numberOfLines={1} style={[
3525
4228
  styles.statusFilterText,
3526
4229
  active && {
3527
4230
  color: AppColors.darkOrange ||
@@ -3529,8 +4232,9 @@ const NetworkInspector = ({ enabled = true, }) => {
3529
4232
  fontFamily: AppFonts.interBold,
3530
4233
  },
3531
4234
  ]}>
3532
- Warning ({logCounts.warn})
3533
- </Text>
4235
+ Warning ({logCounts.warn})
4236
+ </Text>
4237
+ </View>
3534
4238
  </View>
3535
4239
  </TouchableScale>);
3536
4240
  })()}
@@ -3556,15 +4260,25 @@ const NetworkInspector = ({ enabled = true, }) => {
3556
4260
  backgroundColor: '#FFF5F6',
3557
4261
  },
3558
4262
  ]}>
3559
- <Text numberOfLines={1} style={[
4263
+ {/* #7 */}
4264
+ <View style={{
4265
+ flexDirection: 'row',
4266
+ alignItems: 'center',
4267
+ gap: 5,
4268
+ }}>
4269
+ <ErrorCircleIcon size={12} color={active
4270
+ ? AppColors.errorColor
4271
+ : AppColors.grayTextStrong}/>
4272
+ <Text numberOfLines={1} style={[
3560
4273
  styles.statusFilterText,
3561
4274
  active && {
3562
4275
  color: AppColors.errorColor,
3563
4276
  fontFamily: AppFonts.interBold,
3564
4277
  },
3565
4278
  ]}>
3566
- Error ({logCounts.error})
3567
- </Text>
4279
+ Error ({logCounts.error})
4280
+ </Text>
4281
+ </View>
3568
4282
  </View>
3569
4283
  </TouchableScale>);
3570
4284
  })()}
@@ -3590,15 +4304,25 @@ const NetworkInspector = ({ enabled = true, }) => {
3590
4304
  backgroundColor: `${AppColors.skyBlue}15`,
3591
4305
  },
3592
4306
  ]}>
3593
- <Text numberOfLines={1} style={[
4307
+ {/* #7 */}
4308
+ <View style={{
4309
+ flexDirection: 'row',
4310
+ alignItems: 'center',
4311
+ gap: 5,
4312
+ }}>
4313
+ <AnalyticsIcon size={12} color={active
4314
+ ? AppColors.skyBlue
4315
+ : AppColors.grayTextStrong}/>
4316
+ <Text numberOfLines={1} style={[
3594
4317
  styles.statusFilterText,
3595
4318
  active && {
3596
4319
  color: AppColors.skyBlue,
3597
4320
  fontFamily: AppFonts.interBold,
3598
4321
  },
3599
4322
  ]}>
3600
- Analytics ({logCounts.analytics})
3601
- </Text>
4323
+ Analytics ({logCounts.analytics})
4324
+ </Text>
4325
+ </View>
3602
4326
  </View>
3603
4327
  </TouchableScale>);
3604
4328
  })()}
@@ -4633,7 +5357,7 @@ const NetworkInspector = ({ enabled = true, }) => {
4633
5357
  if (!resExpanded && !showResDiff)
4634
5358
  setResExpanded(true);
4635
5359
  }}/>
4636
- {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded}/>)}
5360
+ {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded} wrap/>)}
4637
5361
  </View>
4638
5362
  </>)}
4639
5363
  </ScrollView>
@@ -4671,4 +5395,4 @@ export { setupConsoleLogger, clearConsoleLogs, subscribeConsoleLogs, } from './c
4671
5395
  export { setupAnalyticsLogger, logAnalyticsEvent, subscribeAnalyticsEvents, clearAnalyticsEvents, } from './customHooks/analyticsLogger';
4672
5396
  export { WebView, getWebViewLogs, getWebViewNavHistory, getWebViewHtml, getWebViewCss, getWebViewJs, getWebViewHtmlUrl, clearWebViewData, subscribeWebView, } from './customHooks/webViewLogger';
4673
5397
  export { default as ErrorBoundary } from './components/ErrorBoundary';
4674
- export { connectReduxStore, getReduxState, subscribeReduxState, } from './customHooks/reduxLogger';
5398
+ export { connectReduxStore, inspectorReduxMiddleware, getReduxState, subscribeReduxState, getActionHistory, clearActionHistory, } from './customHooks/reduxLogger';