react-native-inapp-inspector 2.3.8 → 2.3.10

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 (82) hide show
  1. package/dist/commonjs/components/Inspector/AnalyticsTab.js +27 -2
  2. package/dist/commonjs/components/Inspector/BundleTab.js +3 -0
  3. package/dist/commonjs/components/Inspector/ConsoleTab.js +24 -1
  4. package/dist/commonjs/components/Inspector/CrashTab.js +30 -1
  5. package/dist/commonjs/components/Inspector/FeatureUnderDevNotice.d.ts +7 -0
  6. package/dist/commonjs/components/Inspector/FeatureUnderDevNotice.js +111 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +225 -108
  8. package/dist/commonjs/components/Inspector/NetworkDetail.js +2 -1
  9. package/dist/commonjs/components/Inspector/NetworkFilterModal.d.ts +19 -0
  10. package/dist/commonjs/components/Inspector/NetworkFilterModal.js +648 -0
  11. package/dist/commonjs/components/Inspector/NetworkTab.js +89 -147
  12. package/dist/commonjs/components/Inspector/NpmUpdateToast.js +1 -1
  13. package/dist/commonjs/components/Inspector/PerformanceTab.js +3 -0
  14. package/dist/commonjs/components/Inspector/ReduxTab.js +24 -1
  15. package/dist/commonjs/components/Inspector/SettingsPanel.js +73 -88
  16. package/dist/commonjs/components/Inspector/StorageTab.js +37 -11
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +2 -0
  18. package/dist/commonjs/components/LogCard.js +35 -26
  19. package/dist/commonjs/components/NetworkIcons.d.ts +1 -0
  20. package/dist/commonjs/components/NetworkIcons.js +6 -1
  21. package/dist/commonjs/constants/index.js +3 -0
  22. package/dist/commonjs/constants/version.d.ts +1 -1
  23. package/dist/commonjs/constants/version.js +1 -1
  24. package/dist/commonjs/helpers/telemetry.js +34 -0
  25. package/dist/commonjs/index.js +3 -0
  26. package/dist/commonjs/styles/index.d.ts +11 -0
  27. package/dist/commonjs/styles/index.js +20 -5
  28. package/dist/commonjs/types/enums.d.ts +3 -0
  29. package/dist/commonjs/types/enums.js +3 -0
  30. package/dist/esm/components/Inspector/AnalyticsTab.js +29 -4
  31. package/dist/esm/components/Inspector/BundleTab.js +3 -0
  32. package/dist/esm/components/Inspector/ConsoleTab.js +26 -3
  33. package/dist/esm/components/Inspector/CrashTab.js +32 -3
  34. package/dist/esm/components/Inspector/FeatureUnderDevNotice.d.ts +7 -0
  35. package/dist/esm/components/Inspector/FeatureUnderDevNotice.js +74 -0
  36. package/dist/esm/components/Inspector/InspectorHeader.js +226 -109
  37. package/dist/esm/components/Inspector/NetworkDetail.js +2 -1
  38. package/dist/esm/components/Inspector/NetworkFilterModal.d.ts +19 -0
  39. package/dist/esm/components/Inspector/NetworkFilterModal.js +607 -0
  40. package/dist/esm/components/Inspector/NetworkTab.js +91 -149
  41. package/dist/esm/components/Inspector/NpmUpdateToast.js +1 -1
  42. package/dist/esm/components/Inspector/PerformanceTab.js +3 -0
  43. package/dist/esm/components/Inspector/ReduxTab.js +26 -3
  44. package/dist/esm/components/Inspector/SettingsPanel.js +73 -88
  45. package/dist/esm/components/Inspector/StorageTab.js +39 -13
  46. package/dist/esm/components/Inspector/UpdateAvailableModal.js +2 -0
  47. package/dist/esm/components/LogCard.js +36 -27
  48. package/dist/esm/components/NetworkIcons.d.ts +1 -0
  49. package/dist/esm/components/NetworkIcons.js +4 -0
  50. package/dist/esm/constants/index.js +3 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/helpers/telemetry.js +34 -0
  54. package/dist/esm/index.js +3 -0
  55. package/dist/esm/styles/index.d.ts +11 -0
  56. package/dist/esm/styles/index.js +20 -5
  57. package/dist/esm/types/enums.d.ts +3 -0
  58. package/dist/esm/types/enums.js +3 -0
  59. package/package.json +2 -2
  60. package/src/components/Inspector/AnalyticsTab.tsx +71 -43
  61. package/src/components/Inspector/BundleTab.tsx +3 -0
  62. package/src/components/Inspector/ConsoleTab.tsx +28 -1
  63. package/src/components/Inspector/CrashTab.tsx +45 -14
  64. package/src/components/Inspector/FeatureUnderDevNotice.tsx +94 -0
  65. package/src/components/Inspector/InspectorHeader.tsx +257 -115
  66. package/src/components/Inspector/NetworkDetail.tsx +2 -1
  67. package/src/components/Inspector/NetworkFilterModal.tsx +710 -0
  68. package/src/components/Inspector/NetworkTab.tsx +127 -204
  69. package/src/components/Inspector/NpmUpdateToast.tsx +1 -1
  70. package/src/components/Inspector/PerformanceTab.tsx +3 -0
  71. package/src/components/Inspector/ReduxTab.tsx +28 -1
  72. package/src/components/Inspector/SettingsPanel.tsx +93 -132
  73. package/src/components/Inspector/StorageTab.tsx +56 -27
  74. package/src/components/Inspector/UpdateAvailableModal.tsx +2 -0
  75. package/src/components/LogCard.tsx +43 -31
  76. package/src/components/NetworkIcons.tsx +27 -0
  77. package/src/constants/index.ts +3 -0
  78. package/src/constants/version.ts +1 -1
  79. package/src/helpers/telemetry.ts +36 -0
  80. package/src/index.tsx +5 -0
  81. package/src/styles/index.ts +20 -5
  82. package/src/types/enums.ts +3 -0
@@ -1,99 +1,53 @@
1
1
  import React, { useCallback, useMemo, useRef } from 'react';
2
- import { Animated, FlatList, Pressable, ScrollView, Text, TextInput, View, } from 'react-native';
2
+ import { FlatList, Pressable, ScrollView, Text, TextInput, View, } from 'react-native';
3
3
  import { animateNextLayout, useInspector } from './InspectorContext';
4
- import useAccordion from '../../customHooks/useAccordion';
5
4
  import TouchableScale from '../TouchableScale';
6
5
  import AnimatedEntrance from '../AnimatedEntrance';
7
6
  import DomainHeader from '../DomainHeader';
8
7
  import LogCard from '../LogCard';
9
8
  import EmptyState from '../EmptyState';
10
9
  import EndOfListFooter from '../EndOfListFooter';
10
+ import NetworkFilterModal from './NetworkFilterModal';
11
11
  import styles from '../../styles';
12
12
  import { AppColors } from '../../styles/AppColors';
13
13
  import { AppFonts } from '../../styles/AppFonts';
14
- import { STATUS_FILTERS, METHOD_COLORS } from '../../constants';
14
+ import { METHOD_COLORS } from '../../constants';
15
15
  import { useTranslation } from '../../i18n';
16
16
  import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIcon, CircleCheckIcon, CircleXIcon, CircleAlertIcon, LayersIcon, HeaderPauseIcon, ClockIcon, GlobeIcon, RequestIcon, ResponseIcon, HeadersIcon, AtomIcon, BoltIcon, ShieldAlertIcon, } from '../NetworkIcons';
17
- const STATUS_META = {
18
- ALL: { color: AppColors.grayText, Icon: LayersIcon },
19
- '2xx': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
- '200': { color: AppColors.greenColor, Icon: CircleCheckIcon },
21
- '3xx': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
22
- '300': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
23
- '4xx': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
24
- '400': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
25
- '404': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
26
- '5xx': { color: AppColors.errorColor, Icon: CircleXIcon },
27
- '500': { color: AppColors.errorColor, Icon: CircleXIcon },
28
- Failed: { color: AppColors.errorColor, Icon: CircleXIcon },
29
- };
30
- const getStatusMeta = (filter) => {
31
- if (STATUS_META[filter])
32
- return STATUS_META[filter];
33
- const num = parseInt(filter, 10);
34
- if (!isNaN(num)) {
35
- if (num >= 200 && num < 300)
36
- return { color: AppColors.greenColor, Icon: CircleCheckIcon };
37
- if (num >= 300 && num < 400)
38
- return { color: AppColors.warningIconGold, Icon: CircleAlertIcon };
39
- if (num >= 400 && num < 500)
40
- return { color: AppColors.darkOrange, Icon: CircleAlertIcon };
41
- if (num >= 500)
42
- return { color: AppColors.errorColor, Icon: CircleXIcon };
43
- }
44
- if (filter.startsWith('2'))
45
- return { color: AppColors.greenColor, Icon: CircleCheckIcon };
46
- if (filter.startsWith('3'))
47
- return { color: AppColors.warningIconGold, Icon: CircleAlertIcon };
48
- if (filter.startsWith('4'))
49
- return { color: AppColors.darkOrange, Icon: CircleAlertIcon };
50
- if (filter.startsWith('5'))
51
- return { color: AppColors.errorColor, Icon: CircleXIcon };
52
- return { color: AppColors.grayText, Icon: LayersIcon };
53
- };
54
- const FilterChip = React.memo(({ label, color, Icon, badge, active, onPress, }) => {
55
- const iconColor = active ? color : AppColors.grayTextWeak;
56
- return (<TouchableScale style={styles.statusFilterWrap} onPress={onPress} hitSlop={10}>
57
- <View style={[
58
- styles.statusFilterChip,
59
- {
60
- paddingHorizontal: 9,
61
- paddingVertical: 5,
62
- borderRadius: 7,
63
- borderWidth: 1,
64
- borderColor: active ? color : `${AppColors.grayBorderSecondary}`,
65
- backgroundColor: active ? `${color}14` : AppColors.grayBackground,
66
- },
67
- active && {
68
- shadowColor: color,
69
- shadowOffset: { width: 0, height: 1 },
70
- shadowOpacity: 0.2,
71
- shadowRadius: 2,
72
- elevation: 1.5,
73
- },
74
- ]}>
75
- {badge != null ? (<View style={[
76
- styles.methodBadgeMini,
77
- { backgroundColor: badge },
78
- ]}/>) : (Icon && (<Icon color={iconColor} size={13}/>))}
79
- <Text numberOfLines={1} style={[
80
- styles.statusFilterText,
81
- {
82
- color: active ? color : AppColors.grayText,
83
- fontFamily: active ? AppFonts.interBold : AppFonts.interMedium,
84
- },
85
- ]}>
86
- {label}
87
- </Text>
88
- </View>
89
- </TouchableScale>);
90
- });
91
17
  const NetworkTab = React.memo(() => {
92
18
  const { groupedData, search, setSearch, searchScope, setSearchScope, isRegexSearch, setIsRegexSearch, isCaseSensitive, setIsCaseSensitive, quickFilter, setQuickFilter, handleDelete, selectedLogs, sortOrder, setSortOrder, statusFilters, setStatusFilters, methodFilters, setMethodFilters, availableMethods, filteredLogs, logs, toggleSectionFilter, toggleSectionCollapse, minStart, totalRange, newLogIds, toggleSelect, setSelected, isNetworkPaused, setIsNetworkPaused, } = useInspector();
93
19
  const { t } = useTranslation();
94
- const filtersAccordion = useAccordion(false, 300, 260);
95
20
  const apisListRef = useRef(null);
21
+ const [isFilterModalOpen, setIsFilterModalOpen] = React.useState(false);
96
22
  const [isSearchFocused, setIsSearchFocused] = React.useState(false);
23
+ const modalFilterState = useMemo(() => ({
24
+ statusCodes: statusFilters.size > 0 ? new Set(statusFilters) : new Set(['all']),
25
+ methods: methodFilters.size > 0 ? new Set(methodFilters) : new Set(['all']),
26
+ latency: 'all',
27
+ protocol: 'all',
28
+ sortBy: sortOrder === 'newest' ? 'time_desc' : 'time_asc',
29
+ }), [statusFilters, methodFilters, sortOrder]);
30
+ const handleApplyNetworkFilters = useCallback((newFilters) => {
31
+ if (newFilters.statusCodes.has('all') ||
32
+ newFilters.statusCodes.size === 0) {
33
+ setStatusFilters(new Set());
34
+ }
35
+ else {
36
+ setStatusFilters(new Set(newFilters.statusCodes));
37
+ }
38
+ if (newFilters.methods.has('all') || newFilters.methods.size === 0) {
39
+ setMethodFilters(new Set());
40
+ }
41
+ else {
42
+ setMethodFilters(new Set([...newFilters.methods].map(m => m)));
43
+ }
44
+ if (newFilters.sortBy === 'time_asc') {
45
+ setSortOrder('oldest');
46
+ }
47
+ else {
48
+ setSortOrder('newest');
49
+ }
50
+ }, [setStatusFilters, setMethodFilters, setSortOrder]);
97
51
  const quickCounts = useMemo(() => {
98
52
  let errorCount = 0;
99
53
  let successCount = 0;
@@ -520,10 +474,12 @@ const NetworkTab = React.memo(() => {
520
474
 
521
475
  <TouchableScale style={[
522
476
  styles.toolbarBtn,
523
- filtersAccordion.isOpen &&
524
- styles.toolbarBtnActive,
525
- ]} onPress={filtersAccordion.toggleOpen} hitSlop={6}>
526
- <FilterIcon color={filtersAccordion.isOpen
477
+ (statusFilters.size > 0 || methodFilters.size > 0) && {
478
+ borderColor: AppColors.purple,
479
+ backgroundColor: `${AppColors.purple}15`,
480
+ },
481
+ ]} onPress={() => setIsFilterModalOpen(true)} hitSlop={6}>
482
+ <FilterIcon color={statusFilters.size > 0 || methodFilters.size > 0
527
483
  ? AppColors.purple
528
484
  : AppColors.grayTextStrong} size={16}/>
529
485
  {(statusFilters.size > 0 ||
@@ -685,6 +641,51 @@ const NetworkTab = React.memo(() => {
685
641
  </View>
686
642
  </TouchableScale>);
687
643
  })}
644
+
645
+ <TouchableScale onPress={() => setIsFilterModalOpen(true)}>
646
+ <View style={{
647
+ flexDirection: 'row',
648
+ alignItems: 'center',
649
+ paddingHorizontal: 10,
650
+ paddingVertical: 4.5,
651
+ borderRadius: 8,
652
+ backgroundColor: statusFilters.size > 0 || methodFilters.size > 0
653
+ ? `${AppColors.purple}20`
654
+ : AppColors.grayBackground,
655
+ borderWidth: 1,
656
+ borderColor: statusFilters.size > 0 || methodFilters.size > 0
657
+ ? AppColors.purple
658
+ : AppColors.grayBorderSecondary,
659
+ gap: 5,
660
+ }}>
661
+ <FilterIcon size={11} color={statusFilters.size > 0 || methodFilters.size > 0
662
+ ? AppColors.purple
663
+ : AppColors.grayText}/>
664
+ <Text style={{
665
+ fontFamily: AppFonts.interBold,
666
+ fontSize: 10.5,
667
+ color: statusFilters.size > 0 || methodFilters.size > 0
668
+ ? AppColors.purple
669
+ : AppColors.grayText,
670
+ }}>
671
+ More Filters
672
+ </Text>
673
+ {(statusFilters.size > 0 || methodFilters.size > 0) && (<View style={{
674
+ backgroundColor: AppColors.purple,
675
+ paddingHorizontal: 4.5,
676
+ paddingVertical: 1,
677
+ borderRadius: 6,
678
+ }}>
679
+ <Text style={{
680
+ fontFamily: AppFonts.interBold,
681
+ fontSize: 8.5,
682
+ color: AppColors.white,
683
+ }}>
684
+ {statusFilters.size + methodFilters.size}
685
+ </Text>
686
+ </View>)}
687
+ </View>
688
+ </TouchableScale>
688
689
  </ScrollView>
689
690
  </View>
690
691
 
@@ -833,86 +834,24 @@ const NetworkTab = React.memo(() => {
833
834
  </TouchableScale>
834
835
  </View>)}
835
836
 
836
- <Animated.View style={[
837
- filtersAccordion.bodyStyle,
838
- { overflow: 'hidden' },
839
- ]}>
840
- <View style={styles.filtersContainer}>
841
- <Text style={styles.filtersHeading}>
842
- STATUS
843
- </Text>
844
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
845
- {STATUS_FILTERS.map(filter => {
846
- const isAll = filter === 'ALL';
847
- const active = isAll
848
- ? statusFilters.size === 0
849
- : statusFilters.has(filter);
850
- const meta = getStatusMeta(filter);
851
- return (<FilterChip key={filter} label={filter} color={meta.color} Icon={meta.Icon} active={active} onPress={() => {
852
- if (isAll) {
853
- setStatusFilters(new Set());
854
- }
855
- else {
856
- setStatusFilters(prev => {
857
- const next = new Set(prev);
858
- next.has(filter)
859
- ? next.delete(filter)
860
- : next.add(filter);
861
- return next;
862
- });
863
- }
864
- }}/>);
865
- })}
866
- </ScrollView>
867
-
868
- <Text style={[
869
- styles.filtersHeading,
870
- { marginTop: 16 },
871
- ]}>
872
- METHOD
873
- </Text>
874
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
875
- {availableMethods.map(filter => {
876
- const isAll = filter === 'ALL';
877
- const active = isAll
878
- ? methodFilters.size === 0
879
- : methodFilters.has(filter);
880
- const methodColor = METHOD_COLORS[filter] ??
881
- METHOD_COLORS.ALL;
882
- return (<FilterChip key={filter} label={filter} color={methodColor} badge={methodColor} active={active} onPress={() => {
883
- if (isAll) {
884
- setMethodFilters(new Set());
885
- }
886
- else {
887
- setMethodFilters(prev => {
888
- const next = new Set(prev);
889
- next.has(filter)
890
- ? next.delete(filter)
891
- : next.add(filter);
892
- return next;
893
- });
894
- }
895
- }}/>);
896
- })}
897
- </ScrollView>
898
- </View>
899
- </Animated.View>
900
-
901
837
  {(search ||
902
838
  statusFilters.size > 0 ||
903
- methodFilters.size > 0) && (<Text style={styles.resultCount}>
839
+ methodFilters.size > 0 ||
840
+ quickFilter !== 'all') && (<Text style={styles.resultCount}>
904
841
  {filteredLogs.length === logs.length
905
842
  ? `${logs.length} requests`
906
843
  : `${filteredLogs.length} of ${logs.length} filtered requests`}
907
844
  </Text>)}
908
845
  </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 ||
909
846
  statusFilters.size > 0 ||
910
- methodFilters.size > 0} searchQuery={search} customTitle={search.length > 0
847
+ methodFilters.size > 0 ||
848
+ quickFilter !== 'all'} searchQuery={search} customTitle={search.length > 0
911
849
  ? 'No matching API requests'
912
850
  : 'No network activity'} onClearSearch={() => {
913
851
  setSearch('');
914
852
  setStatusFilters(new Set());
915
853
  setMethodFilters(new Set());
854
+ setQuickFilter('all');
916
855
  setSearchScope('all');
917
856
  }}/>} ListFooterComponent={groupedData.length > 0 ? (<EndOfListFooter count={filteredLogs.length} label="requests"/>) : null} contentContainerStyle={[
918
857
  styles.listContent,
@@ -940,6 +879,9 @@ const NetworkTab = React.memo(() => {
940
879
  <ChevronIcon color={AppColors.white} size={18}/>
941
880
  </View>
942
881
  </TouchableScale>
882
+
883
+
884
+ <NetworkFilterModal visible={isFilterModalOpen} onClose={() => setIsFilterModalOpen(false)} filters={modalFilterState} onApply={handleApplyNetworkFilters} searchQuery={search}/>
943
885
  </View>);
944
886
  });
945
887
  export default NetworkTab;
@@ -92,7 +92,7 @@ export const NpmUpdateToast = () => {
92
92
  Linking.openURL('https://www.npmjs.com/package/react-native-inapp-inspector').catch(() => { });
93
93
  handleDismiss();
94
94
  };
95
- if (!showUpdateToast || !visible || !updateAvailable || !latestNpmVersion)
95
+ if (typeof __DEV__ === 'undefined' || !__DEV__ || !showUpdateToast || !visible || !updateAvailable || !latestNpmVersion)
96
96
  return null;
97
97
  const progressWidth = progressAnim.interpolate({
98
98
  inputRange: [0, 1],
@@ -5,6 +5,7 @@ import TouchableScale from '../TouchableScale';
5
5
  import CopyButton from '../CopyButton';
6
6
  import HighlightText from '../HighlightText';
7
7
  import EndOfListFooter from '../EndOfListFooter';
8
+ import FeatureUnderDevNotice from './FeatureUnderDevNotice';
8
9
  import { AppColors } from '../../styles/AppColors';
9
10
  import { AppFonts } from '../../styles/AppFonts';
10
11
  import { useTranslation } from '../../i18n';
@@ -356,6 +357,8 @@ const PerformanceTab = React.memo(() => {
356
357
  </TouchableScale>
357
358
  </View>
358
359
 
360
+ <FeatureUnderDevNotice featureName="Performance & FPS Profiler"/>
361
+
359
362
 
360
363
  <View style={perfStyles.categoryScrollWrapper}>
361
364
  <ScrollView horizontal showsHorizontalScrollIndicator={false} style={perfStyles.categoryScrollView} contentContainerStyle={perfStyles.categoryScroll}>
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import { FlatList, Pressable, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
3
3
  import { useTranslation } from '../../i18n';
4
4
  import { useInspector } from './InspectorContext';
@@ -12,7 +12,7 @@ import styles from '../../styles';
12
12
  import { AppColors } from '../../styles/AppColors';
13
13
  import { AppFonts } from '../../styles/AppFonts';
14
14
  import { getActionHistory, getReduxAutoRefresh, setReduxAutoRefresh, } from '../../customHooks/reduxLogger';
15
- import { TerminalIcon, SearchIcon, ClearIcon, LayersIcon, ClockIcon, ForwardChevronIcon, SortArrowIcon, BoltIcon, AtomIcon, ScreenIcon, ListenerIcon, DocIcon, LoadingSpinnerIcon, CircleAlertIcon, TextAaIcon, HeaderPauseIcon, } from '../NetworkIcons';
15
+ import { TerminalIcon, SearchIcon, ClearIcon, LayersIcon, ClockIcon, ForwardChevronIcon, SortArrowIcon, BoltIcon, AtomIcon, ScreenIcon, ListenerIcon, DocIcon, LoadingSpinnerIcon, CircleAlertIcon, TextAaIcon, HeaderPauseIcon, ChevronIcon, } from '../NetworkIcons';
16
16
  const getOriginBadge = (originType) => {
17
17
  switch (originType) {
18
18
  case 'saga':
@@ -60,6 +60,7 @@ const getOriginBadge = (originType) => {
60
60
  const ReduxTab = React.memo(() => {
61
61
  const { t } = useTranslation();
62
62
  const { reduxState, reduxLastActionMap, reduxSearch, setReduxSearch, setSelectedReduxSlice, } = useInspector();
63
+ const listRef = useRef(null);
63
64
  const [sortMode, setSortMode] = useState('latest');
64
65
  const [isReduxPaused, setIsReduxPaused] = useState(!getReduxAutoRefresh());
65
66
  const handleTogglePause = useCallback(() => {
@@ -453,10 +454,32 @@ const ReduxTab = React.memo(() => {
453
454
  </View>
454
455
 
455
456
 
456
- <FlatList data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.length > 0}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
457
+ <FlatList ref={listRef} data={filteredSlices} keyExtractor={item => item.id} renderItem={renderSliceItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListEmptyComponent={<EmptyState isSearch={reduxSearch.length > 0}/>} ListFooterComponent={filteredSlices.length > 0 ? (<EndOfListFooter count={filteredSlices.length} label="slices"/>) : null} contentContainerStyle={[
457
458
  reduxTabStyles.listContent,
458
459
  filteredSlices.length === 0 && { flexGrow: 1 },
459
460
  ]} keyboardShouldPersistTaps="handled"/>
461
+
462
+ <TouchableScale onPress={() => {
463
+ try {
464
+ listRef.current?.scrollToOffset({
465
+ offset: 0,
466
+ animated: true,
467
+ });
468
+ }
469
+ catch {
470
+ try {
471
+ listRef.current?.scrollToIndex({
472
+ index: 0,
473
+ animated: true,
474
+ });
475
+ }
476
+ catch { }
477
+ }
478
+ }} hitSlop={12} style={styles.scrollTopBtn}>
479
+ <View style={{ transform: [{ rotate: '180deg' }] }}>
480
+ <ChevronIcon color={AppColors.white} size={18}/>
481
+ </View>
482
+ </TouchableScale>
460
483
  </View>);
461
484
  });
462
485
  const reduxTabStyles = StyleSheet.create({
@@ -275,11 +275,12 @@ const SettingsPanel = () => {
275
275
  }}>
276
276
  <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, paddingBottom: 100, gap: 14 }} showsVerticalScrollIndicator={false}>
277
277
 
278
+
278
279
  <View style={{
279
- flexDirection: 'row',
280
280
  backgroundColor: AppColors.primaryLight,
281
- borderRadius: 12,
282
- padding: 4,
281
+ borderRadius: 13,
282
+ paddingVertical: 4,
283
+ paddingHorizontal: 4,
283
284
  borderWidth: 1,
284
285
  borderColor: AppColors.grayBorderSecondary,
285
286
  shadowColor: AppColors.black,
@@ -287,95 +288,79 @@ const SettingsPanel = () => {
287
288
  shadowRadius: 4,
288
289
  shadowOffset: { width: 0, height: 2 },
289
290
  }}>
290
- <TouchableScale accessible={true} accessibilityRole="tab" accessibilityLabel={t('settings.modulesAndTools')} accessibilityState={{ selected: settingsActiveSubTab === 'module' }} onPress={() => {
291
- animateNextLayout();
292
- setSettingsActiveSubTab('module');
293
- }} style={{
294
- flex: 1,
295
- paddingVertical: 9,
291
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
296
292
  flexDirection: 'row',
297
293
  alignItems: 'center',
298
- justifyContent: 'center',
299
294
  gap: 6,
300
- borderRadius: 9,
301
- backgroundColor: settingsActiveSubTab === 'module'
302
- ? AppColors.purple
303
- : 'transparent',
304
- }}>
305
- <LayersIcon color={settingsActiveSubTab === 'module'
306
- ? AppColors.white
307
- : AppColors.grayText} size={13}/>
308
- <Text style={{
309
- fontFamily: AppFonts.interBold,
310
- fontSize: 12.5,
311
- lineHeight: 16,
312
- color: settingsActiveSubTab === 'module'
313
- ? AppColors.white
314
- : AppColors.grayText,
315
- }}>
316
- {t('settings.modulesAndTools')}
317
- </Text>
318
- </TouchableScale>
319
-
320
- <TouchableScale accessible={true} accessibilityRole="tab" accessibilityLabel={t('settings.uiPreferences')} accessibilityState={{ selected: settingsActiveSubTab === 'ui' }} onPress={() => {
321
- animateNextLayout();
322
- setSettingsActiveSubTab('ui');
323
- }} style={{
324
- flex: 1,
325
- paddingVertical: 9,
326
- flexDirection: 'row',
327
- alignItems: 'center',
328
- justifyContent: 'center',
329
- gap: 5,
330
- borderRadius: 9,
331
- backgroundColor: settingsActiveSubTab === 'ui'
332
- ? AppColors.purple
333
- : 'transparent',
334
- }}>
335
- <ScreenIcon color={settingsActiveSubTab === 'ui'
336
- ? AppColors.white
337
- : AppColors.grayText} size={13}/>
338
- <Text numberOfLines={1} style={{
339
- fontFamily: AppFonts.interBold,
340
- fontSize: 12,
341
- lineHeight: 16,
342
- color: settingsActiveSubTab === 'ui'
343
- ? AppColors.white
344
- : AppColors.grayText,
345
- }}>
346
- {t('settings.uiPreferences')}
347
- </Text>
348
- </TouchableScale>
349
-
350
- <TouchableScale accessible={true} accessibilityRole="tab" accessibilityLabel={t('settings.ramLimits')} accessibilityState={{ selected: settingsActiveSubTab === 'limits' }} onPress={() => {
351
- animateNextLayout();
352
- setSettingsActiveSubTab('limits');
353
- }} style={{
354
- flex: 1,
355
- paddingVertical: 9,
356
- flexDirection: 'row',
357
- alignItems: 'center',
358
- justifyContent: 'center',
359
- gap: 5,
360
- borderRadius: 9,
361
- backgroundColor: settingsActiveSubTab === 'limits'
362
- ? AppColors.purple
363
- : 'transparent',
364
- }}>
365
- <BrainIcon color={settingsActiveSubTab === 'limits'
366
- ? AppColors.white
367
- : AppColors.grayText} size={13}/>
368
- <Text numberOfLines={1} style={{
369
- fontFamily: AppFonts.interBold,
370
- fontSize: 12,
371
- lineHeight: 16,
372
- color: settingsActiveSubTab === 'limits'
373
- ? AppColors.white
374
- : AppColors.grayText,
295
+ paddingHorizontal: 2,
375
296
  }}>
376
- {t('settings.ramLimits')}
377
- </Text>
378
- </TouchableScale>
297
+ {[
298
+ {
299
+ key: 'module',
300
+ label: t('settings.modulesAndTools', 'Modules & Tools'),
301
+ Icon: LayersIcon,
302
+ badge: `${allModules.filter(m => stagedTabVisibility?.[m.key] || m.key === 'apis').length}/${allModules.length}`,
303
+ },
304
+ {
305
+ key: 'ui',
306
+ label: t('settings.uiPreferences', 'UI Preferences'),
307
+ Icon: ScreenIcon,
308
+ },
309
+ {
310
+ key: 'limits',
311
+ label: t('settings.ramLimits', 'RAM & Limits'),
312
+ Icon: BrainIcon,
313
+ },
314
+ ].map(tab => {
315
+ const isActive = settingsActiveSubTab === tab.key;
316
+ const IconComp = tab.Icon;
317
+ return (<TouchableScale key={tab.key} accessible={true} accessibilityRole="tab" accessibilityLabel={tab.label} accessibilityState={{ selected: isActive }} onPress={() => {
318
+ animateNextLayout();
319
+ setSettingsActiveSubTab(tab.key);
320
+ }} style={{
321
+ paddingVertical: 8,
322
+ paddingHorizontal: 14,
323
+ flexDirection: 'row',
324
+ alignItems: 'center',
325
+ justifyContent: 'center',
326
+ gap: 6,
327
+ borderRadius: 9,
328
+ backgroundColor: isActive
329
+ ? AppColors.purple
330
+ : 'transparent',
331
+ }}>
332
+ <IconComp color={isActive ? AppColors.white : AppColors.grayText} size={13}/>
333
+ <Text style={{
334
+ fontFamily: AppFonts.interBold,
335
+ fontSize: 12.5,
336
+ lineHeight: 16,
337
+ color: isActive
338
+ ? AppColors.white
339
+ : AppColors.grayText,
340
+ }}>
341
+ {tab.label}
342
+ </Text>
343
+ {tab.badge && (<View style={{
344
+ backgroundColor: isActive
345
+ ? 'rgba(255,255,255,0.25)'
346
+ : `${AppColors.purple}18`,
347
+ paddingHorizontal: 5.5,
348
+ paddingVertical: 1,
349
+ borderRadius: 10,
350
+ }}>
351
+ <Text style={{
352
+ fontFamily: AppFonts.interBold,
353
+ fontSize: 9.5,
354
+ color: isActive
355
+ ? AppColors.white
356
+ : AppColors.purple,
357
+ }}>
358
+ {tab.badge}
359
+ </Text>
360
+ </View>)}
361
+ </TouchableScale>);
362
+ })}
363
+ </ScrollView>
379
364
  </View>
380
365
 
381
366
  {settingsActiveSubTab === 'module' && (<View style={{ gap: 12 }}>
@@ -1,10 +1,11 @@
1
- import React, { useState, useEffect, useMemo, useCallback } from 'react';
1
+ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
2
2
  import { View, Text, ScrollView, FlatList, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
3
3
  import TouchableScale from '../TouchableScale';
4
+ import globalStyles from '../../styles';
4
5
  import { AppColors } from '../../styles/AppColors';
5
6
  import { AppFonts } from '../../styles/AppFonts';
6
7
  import { useTranslation } from '../../i18n';
7
- import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, CopyIcon, CheckIcon, LayersIcon, ResetIcon, } from '../NetworkIcons';
8
+ import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, CopyIcon, CheckIcon, LayersIcon, ResetIcon, ChevronIcon, } from '../NetworkIcons';
8
9
  import { fetchStorageEntries, setStorageEntry, removeStorageEntry, clearStorageDriver, isAsyncStorageConnected, isMMKVConnected, getRegisteredMMKVInstanceIds, subscribeToStorageChanges, } from '../../customHooks/storageInspector';
9
10
  import { copyToClipboard } from '../../helpers';
10
11
  import { showToast } from '../../helpers/toast';
@@ -95,6 +96,7 @@ const StorageEntryCard = React.memo(function StorageEntryCard({ entry, isExpande
95
96
  });
96
97
  export const StorageTab = React.memo(() => {
97
98
  const { t } = useTranslation();
99
+ const listRef = useRef(null);
98
100
  const [activeDriver, setActiveDriver] = useState('asyncStorage');
99
101
  const [activeMMKVId, setActiveMMKVId] = useState('default');
100
102
  const [entries, setEntries] = useState([]);
@@ -360,19 +362,43 @@ export const StorageTab = React.memo(() => {
360
362
  {isLoading ? (<View style={styles.loadingContainer}>
361
363
  <ActivityIndicator size="small" color={AppColors.purple}/>
362
364
  <Text style={styles.loadingText}>Loading storage entries...</Text>
363
- </View>) : (<FlatList data={filteredEntries} keyExtractor={item => item.key} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={Platform.OS === 'android'} style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} ListEmptyComponent={<View style={styles.emptyContainer}>
364
- <View style={styles.emptyIconWrap}>
365
- <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
366
- </View>
367
- <Text style={styles.emptyTitle}>
368
- {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
369
- </Text>
370
- <Text style={styles.emptySubtitle}>
371
- {search
365
+ </View>) : (<>
366
+ <FlatList ref={listRef} data={filteredEntries} keyExtractor={item => item.key} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={Platform.OS === 'android'} style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} ListEmptyComponent={<View style={styles.emptyContainer}>
367
+ <View style={styles.emptyIconWrap}>
368
+ <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
369
+ </View>
370
+ <Text style={styles.emptyTitle}>
371
+ {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
372
+ </Text>
373
+ <Text style={styles.emptySubtitle}>
374
+ {search
372
375
  ? 'Try modifying your search query'
373
376
  : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
374
- </Text>
375
- </View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>)}
377
+ </Text>
378
+ </View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>
379
+
380
+ <TouchableScale onPress={() => {
381
+ try {
382
+ listRef.current?.scrollToOffset({
383
+ offset: 0,
384
+ animated: true,
385
+ });
386
+ }
387
+ catch {
388
+ try {
389
+ listRef.current?.scrollToIndex({
390
+ index: 0,
391
+ animated: true,
392
+ });
393
+ }
394
+ catch { }
395
+ }
396
+ }} hitSlop={12} style={globalStyles.scrollTopBtn}>
397
+ <View style={{ transform: [{ rotate: '180deg' }] }}>
398
+ <ChevronIcon color={AppColors.white} size={18}/>
399
+ </View>
400
+ </TouchableScale>
401
+ </>)}
376
402
 
377
403
 
378
404
  <Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
@@ -32,6 +32,8 @@ export const UpdateAvailableModal = ({ visible, latestVersion, onClose, }) => {
32
32
  const fadeAnim = useState(new Animated.Value(0))[0];
33
33
  const scaleAnim = useState(new Animated.Value(0.92))[0];
34
34
  const installCommand = `npm install react-native-inapp-inspector@latest`;
35
+ if (typeof __DEV__ === 'undefined' || !__DEV__)
36
+ return null;
35
37
  useEffect(() => {
36
38
  if (visible) {
37
39
  setCopied(false);