react-native-inapp-inspector 2.2.9 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) 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/MainScreen.js +7 -11
  12. package/dist/commonjs/components/Inspector/NetworkDetail.js +3 -2
  13. package/dist/commonjs/components/Inspector/NetworkTab.js +103 -4
  14. package/dist/commonjs/components/Inspector/ReduxTab.js +19 -3
  15. package/dist/commonjs/components/Inspector/SettingsPanel.js +4 -4
  16. package/dist/commonjs/components/Inspector/StorageTab.js +123 -92
  17. package/dist/commonjs/components/JsonViewer.js +2 -0
  18. package/dist/commonjs/components/MetaAccordion.js +15 -3
  19. package/dist/commonjs/components/SkeletonPlaceholder.d.ts +6 -0
  20. package/dist/commonjs/components/SkeletonPlaceholder.js +208 -0
  21. package/dist/commonjs/constants/version.d.ts +1 -1
  22. package/dist/commonjs/constants/version.js +1 -1
  23. package/dist/commonjs/customHooks/networkLogger.d.ts +3 -0
  24. package/dist/commonjs/customHooks/networkLogger.js +10 -1
  25. package/dist/commonjs/customHooks/storageInspector.d.ts +2 -2
  26. package/dist/commonjs/customHooks/storageInspector.js +17 -13
  27. package/dist/commonjs/helpers/index.d.ts +6 -0
  28. package/dist/commonjs/helpers/index.js +196 -6
  29. package/dist/commonjs/helpers/searchQueryParser.js +62 -20
  30. package/dist/commonjs/i18n/index.d.ts +13 -0
  31. package/dist/commonjs/i18n/index.js +35 -2
  32. package/dist/commonjs/index.d.ts +3 -0
  33. package/dist/commonjs/index.js +108 -35
  34. package/dist/commonjs/styles/AppColors.d.ts +4 -0
  35. package/dist/commonjs/styles/AppColors.js +11 -1
  36. package/dist/commonjs/styles/AppFonts.d.ts +11 -0
  37. package/dist/commonjs/styles/AppFonts.js +11 -1
  38. package/dist/commonjs/types/interfaces.d.ts +2 -2
  39. package/dist/esm/components/AppHeaderLogo.js +45 -12
  40. package/dist/esm/components/BrandSquareIcon.js +92 -49
  41. package/dist/esm/components/EmptyState.d.ts +7 -2
  42. package/dist/esm/components/EmptyState.js +11 -5
  43. package/dist/esm/components/Inspector/AnalyticsTab.js +30 -19
  44. package/dist/esm/components/Inspector/ConsoleTab.js +6 -1
  45. package/dist/esm/components/Inspector/CrashDetail.js +43 -9
  46. package/dist/esm/components/Inspector/CrashTab.js +34 -3
  47. package/dist/esm/components/Inspector/DeviceInfoTab.js +80 -16
  48. package/dist/esm/components/Inspector/InspectorHeader.js +17 -4
  49. package/dist/esm/components/Inspector/MainScreen.js +8 -12
  50. package/dist/esm/components/Inspector/NetworkDetail.js +3 -2
  51. package/dist/esm/components/Inspector/NetworkTab.js +103 -4
  52. package/dist/esm/components/Inspector/ReduxTab.js +19 -3
  53. package/dist/esm/components/Inspector/SettingsPanel.js +4 -4
  54. package/dist/esm/components/Inspector/StorageTab.js +124 -93
  55. package/dist/esm/components/JsonViewer.js +2 -0
  56. package/dist/esm/components/MetaAccordion.js +15 -3
  57. package/dist/esm/components/SkeletonPlaceholder.d.ts +6 -0
  58. package/dist/esm/components/SkeletonPlaceholder.js +173 -0
  59. package/dist/esm/constants/version.d.ts +1 -1
  60. package/dist/esm/constants/version.js +1 -1
  61. package/dist/esm/customHooks/networkLogger.d.ts +3 -0
  62. package/dist/esm/customHooks/networkLogger.js +8 -0
  63. package/dist/esm/customHooks/storageInspector.d.ts +2 -2
  64. package/dist/esm/customHooks/storageInspector.js +17 -13
  65. package/dist/esm/helpers/index.d.ts +6 -0
  66. package/dist/esm/helpers/index.js +193 -5
  67. package/dist/esm/helpers/searchQueryParser.js +62 -20
  68. package/dist/esm/i18n/index.d.ts +13 -0
  69. package/dist/esm/i18n/index.js +33 -3
  70. package/dist/esm/index.d.ts +3 -0
  71. package/dist/esm/index.js +97 -37
  72. package/dist/esm/styles/AppColors.d.ts +4 -0
  73. package/dist/esm/styles/AppColors.js +9 -0
  74. package/dist/esm/styles/AppFonts.d.ts +11 -0
  75. package/dist/esm/styles/AppFonts.js +9 -0
  76. package/dist/esm/types/interfaces.d.ts +2 -2
  77. package/package.json +1 -1
  78. package/src/components/AppHeaderLogo.tsx +46 -12
  79. package/src/components/BrandSquareIcon.tsx +127 -78
  80. package/src/components/EmptyState.tsx +27 -5
  81. package/src/components/Inspector/AnalyticsTab.tsx +48 -27
  82. package/src/components/Inspector/ConsoleTab.tsx +10 -0
  83. package/src/components/Inspector/CrashDetail.tsx +65 -9
  84. package/src/components/Inspector/CrashTab.tsx +41 -3
  85. package/src/components/Inspector/DeviceInfoTab.tsx +87 -16
  86. package/src/components/Inspector/InspectorHeader.tsx +24 -4
  87. package/src/components/Inspector/MainScreen.tsx +7 -11
  88. package/src/components/Inspector/NetworkDetail.tsx +4 -4
  89. package/src/components/Inspector/NetworkTab.tsx +129 -4
  90. package/src/components/Inspector/ReduxTab.tsx +26 -8
  91. package/src/components/Inspector/SettingsPanel.tsx +4 -4
  92. package/src/components/Inspector/StorageTab.tsx +191 -117
  93. package/src/components/JsonViewer.tsx +2 -0
  94. package/src/components/MetaAccordion.tsx +18 -3
  95. package/src/components/SkeletonPlaceholder.tsx +206 -0
  96. package/src/constants/version.ts +1 -1
  97. package/src/customHooks/networkLogger.ts +15 -0
  98. package/src/customHooks/storageInspector.ts +18 -14
  99. package/src/helpers/index.ts +213 -5
  100. package/src/helpers/searchQueryParser.ts +63 -20
  101. package/src/i18n/index.ts +38 -3
  102. package/src/index.tsx +124 -37
  103. package/src/styles/AppColors.ts +9 -0
  104. package/src/styles/AppFonts.ts +16 -0
  105. package/src/types/interfaces.ts +2 -2
@@ -9,12 +9,27 @@ import { AppColors } from '../../styles/AppColors';
9
9
  import { AppFonts } from '../../styles/AppFonts';
10
10
  import { METHOD_COLORS } from '../../constants';
11
11
  import { LIB_VERSION } from '../../constants';
12
- import { getStatusColor, getAppName, formatTime, getSize } from '../../helpers';
12
+ import { getStatusColor, getAppName, formatTime, getSize, getAppVersionAndBuild, } from '../../helpers';
13
+ import { getNativeDeviceMetrics } from '../../native/NativeInspector';
13
14
  import { UpdateAvailableModal } from './UpdateAvailableModal';
14
15
  import { WhiteBackNavigation, TrashIcon, SettingsIcon, CloseWhite, ClockIcon, SizeIcon, AppleIcon, AndroidIcon, NpmIcon, ResetIcon, BoltIcon, } from '../NetworkIcons';
15
16
  const InspectorHeader = React.memo(() => {
16
17
  const { modalHeightPercent, appIcon, selected, setSelected, selectedEvent, setSelectedEvent, selectedLog, setSelectedLog, selectedReduxSlice, setSelectedReduxSlice, selectedReduxAction, setSelectedReduxAction, reduxState, reduxLastActionMap, showHeaderInfo, setShowHeaderInfo, updateAvailable, latestNpmVersion, clearAnim, activePulseAnim, unreadPulseAnim, runClearAllWithAnimation, settingsPage, setSettingsPage, resetToDefaults, closeModal, detailTitle, activeTab, environment, visible, selectedCrash, setSelectedCrash, } = useInspector();
17
18
  const [showUpdateModal, setShowUpdateModal] = React.useState(false);
19
+ const [appVersionString, setAppVersionString] = React.useState(() => {
20
+ return getAppVersionAndBuild().formatted;
21
+ });
22
+ React.useEffect(() => {
23
+ getNativeDeviceMetrics()
24
+ .then(metrics => {
25
+ if (metrics?.appVersion) {
26
+ const v = metrics.appVersion;
27
+ const b = metrics.appBuild || '1';
28
+ setAppVersionString(`${v} (${b})`);
29
+ }
30
+ })
31
+ .catch(() => { });
32
+ }, []);
18
33
  const envConfig = useMemo(() => {
19
34
  const rawEnv = (environment || (__DEV__ ? 'DEV' : 'PROD')).trim();
20
35
  const clean = rawEnv.toUpperCase();
@@ -265,9 +280,7 @@ const InspectorHeader = React.memo(() => {
265
280
  color: `${AppColors.white}EB`,
266
281
  letterSpacing: 0.1,
267
282
  }} numberOfLines={1}>
268
- {Platform.OS === 'ios'
269
- ? `iOS ${Platform.Version}`
270
- : `Android ${Platform.Version}`}
283
+ {appVersionString}
271
284
  </Text>
272
285
  </View>
273
286
 
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { ActivityIndicator, Animated, Modal, Platform, Pressable, StatusBar, StyleSheet, Text, View, } from 'react-native';
2
+ import { Animated, Modal, Platform, Pressable, StatusBar, StyleSheet, View, } from 'react-native';
3
3
  import { useInspector } from './InspectorContext';
4
4
  import ErrorBoundary from '../ErrorBoundary';
5
5
  import FabLauncher from './FabLauncher';
@@ -11,6 +11,7 @@ import LogDetail from './LogDetail';
11
11
  import ConsoleTab from './ConsoleTab';
12
12
  import AnalyticsTab from './AnalyticsTab';
13
13
  import AnalyticsDetail from '../AnalyticsDetail';
14
+ import SkeletonPlaceholder from '../SkeletonPlaceholder';
14
15
  import ReduxTab from './ReduxTab';
15
16
  import ReduxDetail from './ReduxDetail';
16
17
  import BundleTab from './BundleTab';
@@ -26,7 +27,7 @@ import styles from '../../styles';
26
27
  import { AppColors } from '../../styles/AppColors';
27
28
  import NavigationTracker from './NavigationTracker';
28
29
  const MainScreen = () => {
29
- const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, isEnabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
30
+ const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, enabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
30
31
  const isDetailActive = (activeTab === 'apis' && selected != null) ||
31
32
  (activeTab === 'analytics' && selectedEvent != null) ||
32
33
  (activeTab === 'logs' && selectedLog != null) ||
@@ -68,7 +69,7 @@ const MainScreen = () => {
68
69
  }, [activeTab]);
69
70
  return (<>
70
71
  {(Platform.OS === 'ios' || Platform.OS === 'android') &&
71
- isEnabled &&
72
+ enabled &&
72
73
  !visible &&
73
74
  !useNativeFab && <FabLauncher />}
74
75
  <Modal visible={visible} animationType={modalAnimationType} transparent statusBarTranslucent={true}>
@@ -88,10 +89,10 @@ const MainScreen = () => {
88
89
  <InspectorHeader />
89
90
 
90
91
  <View style={{ flex: 1 }}>
91
- {isReady ? (<View style={{ flex: 1 }}>
92
- {/* ─── Horizontal Scrollable Tab Bar inside Content ─── */}
93
- {!isDetailActive && <TabBar />}
92
+ {/* ─── Horizontal Scrollable Tab Bar inside Content (Always visible) ─── */}
93
+ {!isDetailActive && <TabBar />}
94
94
 
95
+ {isReady ? (<View style={{ flex: 1 }}>
95
96
  {/* Persistent List Layer - Never unmounted, preserves 100% native scroll with smooth tab transition */}
96
97
  <Animated.View style={[
97
98
  {
@@ -143,12 +144,7 @@ const MainScreen = () => {
143
144
  {activeTab === 'redux' && <ReduxDetail />}
144
145
  {activeTab === 'crash' && selectedCrash != null && (<CrashDetail />)}
145
146
  </Animated.View>)}
146
- </View>) : (<View style={styles.empty}>
147
- <ActivityIndicator size="large" color={AppColors.purple}/>
148
- <Text style={[styles.emptySub, { marginTop: 12 }]}>
149
- Loading logs...
150
- </Text>
151
- </View>)}
147
+ </View>) : (<SkeletonPlaceholder />)}
152
148
 
153
149
  {/* Settings Panel Layer - Rendered on top with smooth slide & spring transition */}
154
150
  {settingsPage !== null && (<Animated.View style={[
@@ -396,8 +396,9 @@ const NetworkDetail = React.memo(() => {
396
396
  selected.responseHeaders?.['Content-Type']} url={selected.url}/>
397
397
 
398
398
  {(() => {
399
- const routeInfo = logRouteMapRef.current.get(selected.id);
400
- if (!routeInfo || routeInfo.path === 'Navigators')
399
+ const routeInfo = logRouteMapRef.current.get(selected.id) ||
400
+ selected?.routeInfo;
401
+ if (!routeInfo || !routeInfo.path || routeInfo.path === 'Navigators')
401
402
  return null;
402
403
  return <SourcePageCard routeInfo={routeInfo}/>;
403
404
  })()}
@@ -469,7 +469,7 @@ const NetworkTab = React.memo(() => {
469
469
  <View style={{
470
470
  backgroundColor: `${AppColors.purple}20`,
471
471
  borderRadius: 10,
472
- paddingHorizontal: 5,
472
+ paddingHorizontal: 6,
473
473
  paddingVertical: 1.5,
474
474
  }}>
475
475
  <Text style={{
@@ -522,14 +522,104 @@ const NetworkTab = React.memo(() => {
522
522
  styles.toolbarBtn,
523
523
  filtersAccordion.isOpen &&
524
524
  styles.toolbarBtnActive,
525
- ]} onPress={filtersAccordion.toggleOpen} hitSlop={10}>
525
+ ]} onPress={filtersAccordion.toggleOpen} hitSlop={6}>
526
526
  <FilterIcon color={filtersAccordion.isOpen
527
527
  ? AppColors.purple
528
- : AppColors.grayTextStrong} size={18}/>
528
+ : AppColors.grayTextStrong} size={16}/>
529
+ {(statusFilters.size > 0 ||
530
+ methodFilters.size > 0) && (<View style={styles.activeFilterDot}/>)}
529
531
  </TouchableScale>
530
532
  </View>
531
533
  </View>
532
534
 
535
+ {/* Quick Search Scope & Filter Tags Row */}
536
+ <View style={{ paddingBottom: 4 }}>
537
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
538
+ paddingHorizontal: 12,
539
+ flexDirection: 'row',
540
+ alignItems: 'center',
541
+ gap: 5,
542
+ paddingVertical: 2,
543
+ }}>
544
+ {/* Search Scopes */}
545
+ {SEARCH_SCOPES.map(s => {
546
+ const isActive = searchScope === s.id;
547
+ return (<TouchableScale key={`scope_${s.id}`} onPress={() => setSearchScope(s.id)} hitSlop={6} style={{
548
+ flexDirection: 'row',
549
+ alignItems: 'center',
550
+ gap: 3.5,
551
+ paddingHorizontal: 7.5,
552
+ paddingVertical: 3.5,
553
+ borderRadius: 6,
554
+ backgroundColor: isActive
555
+ ? AppColors.purple
556
+ : AppColors.grayBackground,
557
+ borderWidth: 1,
558
+ borderColor: isActive
559
+ ? AppColors.purple
560
+ : AppColors.grayBorderSecondary,
561
+ }}>
562
+ {s.icon(isActive ? AppColors.white : AppColors.grayTextWeak)}
563
+ <Text style={{
564
+ fontFamily: AppFonts.interBold,
565
+ fontSize: 10,
566
+ color: isActive ? AppColors.white : AppColors.grayText,
567
+ }}>
568
+ {s.label}
569
+ </Text>
570
+ </TouchableScale>);
571
+ })}
572
+
573
+ <View style={{
574
+ width: 1,
575
+ height: 14,
576
+ backgroundColor: AppColors.dividerColor,
577
+ marginHorizontal: 3,
578
+ }}/>
579
+
580
+ {/* Quick Query Suggestions */}
581
+ {QUICK_API_QUERY_TAGS.map(q => {
582
+ const isActive = search.includes(q.query);
583
+ return (<TouchableScale key={`quick_${q.query}`} onPress={() => {
584
+ setSearch(prev => {
585
+ if (prev.includes(q.query)) {
586
+ return prev
587
+ .replace(q.query, '')
588
+ .replace(/\s+/g, ' ')
589
+ .trim();
590
+ }
591
+ return `${prev} ${q.query}`.trim();
592
+ });
593
+ }} hitSlop={6} style={{
594
+ flexDirection: 'row',
595
+ alignItems: 'center',
596
+ gap: 3.5,
597
+ paddingHorizontal: 7.5,
598
+ paddingVertical: 3.5,
599
+ borderRadius: 6,
600
+ backgroundColor: isActive
601
+ ? `${AppColors.purple}20`
602
+ : AppColors.grayBackground,
603
+ borderWidth: 1,
604
+ borderColor: isActive
605
+ ? AppColors.purple
606
+ : AppColors.grayBorderSecondary,
607
+ }}>
608
+ {q.icon(isActive ? AppColors.purple : AppColors.grayTextWeak)}
609
+ <Text style={{
610
+ fontFamily: AppFonts.interMedium,
611
+ fontSize: 10,
612
+ color: isActive
613
+ ? AppColors.purple
614
+ : AppColors.grayText,
615
+ }}>
616
+ {q.label}
617
+ </Text>
618
+ </TouchableScale>);
619
+ })}
620
+ </ScrollView>
621
+ </View>
622
+
533
623
  {/* Quick Filter Horizontal Chips Bar with Live Counts */}
534
624
  <View style={{ marginBottom: 8 }}>
535
625
  <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
@@ -815,7 +905,16 @@ const NetworkTab = React.memo(() => {
815
905
  ? `${logs.length} requests`
816
906
  : `${filteredLogs.length} of ${logs.length} filtered requests`}
817
907
  </Text>)}
818
- </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} ListFooterComponent={groupedData.length > 0 ? (<EndOfListFooter count={filteredLogs.length} label="requests"/>) : null} contentContainerStyle={[
908
+ </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 ||
909
+ statusFilters.size > 0 ||
910
+ methodFilters.size > 0} searchQuery={search} customTitle={search.length > 0
911
+ ? 'No matching API requests'
912
+ : 'No network activity'} onClearSearch={() => {
913
+ setSearch('');
914
+ setStatusFilters(new Set());
915
+ setMethodFilters(new Set());
916
+ setSearchScope('all');
917
+ }}/>} ListFooterComponent={groupedData.length > 0 ? (<EndOfListFooter count={filteredLogs.length} label="requests"/>) : null} contentContainerStyle={[
819
918
  styles.listContent,
820
919
  filteredLogs.length === 0 && { flexGrow: 1 },
821
920
  ]} keyboardShouldPersistTaps="handled"/>
@@ -396,9 +396,25 @@ const ReduxTab = React.memo(() => {
396
396
  <View style={styles.searchContainer}>
397
397
  <SearchIcon color={AppColors.grayTextWeak} size={15}/>
398
398
  <TextInput accessible={true} accessibilityRole="search" accessibilityLabel="Search Redux slices or actions" accessibilityHint="Type to filter state slices in real time" placeholder="Search Redux slices or actions..." placeholderTextColor={AppColors.grayTextWeak} value={reduxSearch} onChangeText={setReduxSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
399
- {reduxSearch.length > 0 && (<Pressable accessible={true} accessibilityRole="button" accessibilityLabel="Clear search input" onPress={() => setReduxSearch('')} hitSlop={10}>
400
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
401
- </Pressable>)}
399
+ {reduxSearch.length > 0 && (<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
400
+ <View style={{
401
+ backgroundColor: `${AppColors.purple}20`,
402
+ borderRadius: 10,
403
+ paddingHorizontal: 6,
404
+ paddingVertical: 1.5,
405
+ }}>
406
+ <Text style={{
407
+ color: AppColors.purple,
408
+ fontSize: 9.5,
409
+ fontFamily: AppFonts.interBold,
410
+ }}>
411
+ {filteredSlices.length}
412
+ </Text>
413
+ </View>
414
+ <Pressable accessible={true} accessibilityRole="button" accessibilityLabel="Clear search input" onPress={() => setReduxSearch('')} hitSlop={10}>
415
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
416
+ </Pressable>
417
+ </View>)}
402
418
  </View>
403
419
 
404
420
  {/* Pause / Resume Live Updates Button */}
@@ -108,8 +108,8 @@ const SettingsPanel = () => {
108
108
  bundle: Boolean(tabVisibility?.bundle),
109
109
  performance: Boolean(tabVisibility?.performance),
110
110
  crash: Boolean(tabVisibility?.crash),
111
- device: Boolean(tabVisibility?.device ?? true),
112
- storage: Boolean(tabVisibility?.storage ?? true),
111
+ device: Boolean(tabVisibility?.device),
112
+ storage: Boolean(tabVisibility?.storage),
113
113
  }));
114
114
  // Synchronize staged state with tabVisibility when tabVisibility updates
115
115
  useEffect(() => {
@@ -121,8 +121,8 @@ const SettingsPanel = () => {
121
121
  bundle: Boolean(tabVisibility?.bundle),
122
122
  performance: Boolean(tabVisibility?.performance),
123
123
  crash: Boolean(tabVisibility?.crash),
124
- device: Boolean(tabVisibility?.device ?? true),
125
- storage: Boolean(tabVisibility?.storage ?? true),
124
+ device: Boolean(tabVisibility?.device),
125
+ storage: Boolean(tabVisibility?.storage),
126
126
  });
127
127
  }, [tabVisibility]);
128
128
  const hasUnsavedChanges = useMemo(() => {
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect, useMemo, useCallback } from 'react';
2
- import { View, Text, ScrollView, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
2
+ import { View, Text, ScrollView, FlatList, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
3
3
  import TouchableScale from '../TouchableScale';
4
4
  import { AppColors } from '../../styles/AppColors';
5
5
  import { AppFonts } from '../../styles/AppFonts';
@@ -8,6 +8,92 @@ import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, C
8
8
  import { fetchStorageEntries, setStorageEntry, removeStorageEntry, clearStorageDriver, isAsyncStorageConnected, isMMKVConnected, getRegisteredMMKVInstanceIds, subscribeToStorageChanges, } from '../../customHooks/storageInspector';
9
9
  import { copyToClipboard } from '../../helpers';
10
10
  import { showToast } from '../../helpers/toast';
11
+ const getTypeBadge = (type) => {
12
+ switch (type) {
13
+ case 'json':
14
+ return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
15
+ case 'number':
16
+ return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
17
+ case 'boolean':
18
+ return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
19
+ case 'null':
20
+ return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
21
+ default:
22
+ return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
23
+ }
24
+ };
25
+ const StorageEntryCard = React.memo(function StorageEntryCard({ entry, isExpanded, onToggleExpand, onCopy, onEdit, onDelete, badge, }) {
26
+ const formattedBytes = useMemo(() => {
27
+ return entry.byteSize < 1024
28
+ ? `${entry.byteSize} B`
29
+ : `${(entry.byteSize / 1024).toFixed(1)} KB`;
30
+ }, [entry.byteSize]);
31
+ // Lazy compute displayed value to avoid layout thrashing on large JSON strings
32
+ const displayValue = useMemo(() => {
33
+ if (!isExpanded) {
34
+ if (entry.value.length > 250) {
35
+ return entry.value.slice(0, 250) + '...';
36
+ }
37
+ return entry.value;
38
+ }
39
+ if (entry.type === 'json') {
40
+ try {
41
+ const parsed = entry.parsedValue ?? JSON.parse(entry.value);
42
+ return JSON.stringify(parsed, null, 2);
43
+ }
44
+ catch {
45
+ return entry.value;
46
+ }
47
+ }
48
+ return entry.value;
49
+ }, [isExpanded, entry.value, entry.type, entry.parsedValue]);
50
+ return (<View style={styles.entryCard}>
51
+ {/* Entry Header: Key name, Type Badge, Size, Actions */}
52
+ <View style={styles.entryHeader}>
53
+ <View style={{ flex: 1, marginRight: 8 }}>
54
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
55
+ <Text style={styles.entryKey} numberOfLines={1} selectable>
56
+ {entry.key}
57
+ </Text>
58
+ <View style={[
59
+ styles.typeBadge,
60
+ { backgroundColor: badge.bg, borderColor: `${badge.color}33` },
61
+ ]}>
62
+ <Text style={[styles.typeBadgeText, { color: badge.color }]}>
63
+ {badge.label}
64
+ </Text>
65
+ </View>
66
+ <Text style={styles.sizeText}>{formattedBytes}</Text>
67
+ </View>
68
+ </View>
69
+
70
+ {/* Actions: Copy, Edit, Delete */}
71
+ <View style={styles.entryActions}>
72
+ <TouchableScale hitSlop={6} onPress={() => onCopy(entry)} style={styles.entryActionBtn}>
73
+ <CopyIcon size={12} color={AppColors.grayText}/>
74
+ </TouchableScale>
75
+
76
+ <TouchableScale hitSlop={6} onPress={() => onEdit(entry)} style={styles.entryActionBtn}>
77
+ <PencilIcon size={12} color={AppColors.purple}/>
78
+ </TouchableScale>
79
+
80
+ <TouchableScale hitSlop={6} onPress={() => onDelete(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
81
+ <TrashIcon size={12} color={AppColors.errorColor}/>
82
+ </TouchableScale>
83
+ </View>
84
+ </View>
85
+
86
+ {/* Entry Value Preview / Viewer */}
87
+ <TouchableScale onPress={() => onToggleExpand(entry.key)} style={styles.valuePreviewBox}>
88
+ <Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable={isExpanded}>
89
+ {displayValue}
90
+ </Text>
91
+ {entry.value.length > 80 && (<Text style={styles.expandHint}>
92
+ {isExpanded ? '▲ Collapse' : '▼ Expand'}
93
+ </Text>)}
94
+ </TouchableScale>
95
+ </View>);
96
+ });
11
97
  export const StorageTab = React.memo(() => {
12
98
  const { t } = useTranslation();
13
99
  const [activeDriver, setActiveDriver] = useState('asyncStorage');
@@ -166,20 +252,11 @@ export const StorageTab = React.memo(() => {
166
252
  return `${(totalBytes / 1024).toFixed(1)} KB`;
167
253
  return `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`;
168
254
  }, [totalBytes]);
169
- const getTypeBadge = (type) => {
170
- switch (type) {
171
- case 'json':
172
- return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
173
- case 'number':
174
- return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
175
- case 'boolean':
176
- return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
177
- case 'null':
178
- return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
179
- default:
180
- return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
181
- }
182
- };
255
+ const handleCopyEntry = useCallback((entry) => {
256
+ copyToClipboard(entry.value, entry.key);
257
+ showToast(`Copied value of "${entry.key}"`);
258
+ }, []);
259
+ const renderItem = useCallback(({ item }) => (<StorageEntryCard entry={item} isExpanded={Boolean(expandedKeys[item.key])} onToggleExpand={toggleExpand} onCopy={handleCopyEntry} onEdit={handleOpenEdit} onDelete={handleDeleteKey} badge={getTypeBadge(item.type)}/>), [expandedKeys, handleCopyEntry]);
183
260
  const isConnected = activeDriver === 'asyncStorage'
184
261
  ? isAsyncStorageConnected()
185
262
  : isMMKVConnected();
@@ -245,9 +322,25 @@ export const StorageTab = React.memo(() => {
245
322
  <View style={styles.searchBar}>
246
323
  <SearchIcon size={14} color={AppColors.grayTextWeak}/>
247
324
  <TextInput value={search} onChangeText={setSearch} placeholder={`Search ${entries.length} keys & values...`} placeholderTextColor={AppColors.grayTextWeak} style={styles.searchInput} clearButtonMode="while-editing"/>
248
- {search.length > 0 && (<TouchableScale onPress={() => setSearch('')} hitSlop={8}>
249
- <ClearIcon size={13} color={AppColors.grayTextWeak}/>
250
- </TouchableScale>)}
325
+ {search.length > 0 && (<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
326
+ <View style={{
327
+ backgroundColor: `${AppColors.purple}20`,
328
+ borderRadius: 10,
329
+ paddingHorizontal: 6,
330
+ paddingVertical: 1.5,
331
+ }}>
332
+ <Text style={{
333
+ color: AppColors.purple,
334
+ fontSize: 9.5,
335
+ fontFamily: AppFonts.interBold,
336
+ }}>
337
+ {filteredEntries.length}
338
+ </Text>
339
+ </View>
340
+ <TouchableScale onPress={() => setSearch('')} hitSlop={8}>
341
+ <ClearIcon size={13} color={AppColors.grayTextWeak}/>
342
+ </TouchableScale>
343
+ </View>)}
251
344
  </View>
252
345
 
253
346
  {/* Action Buttons: Add Key & Refresh */}
@@ -279,81 +372,19 @@ export const StorageTab = React.memo(() => {
279
372
  {isLoading ? (<View style={styles.loadingContainer}>
280
373
  <ActivityIndicator size="small" color={AppColors.purple}/>
281
374
  <Text style={styles.loadingText}>Loading storage entries...</Text>
282
- </View>) : filteredEntries.length === 0 ? (<ScrollView contentContainerStyle={styles.emptyContainer} showsVerticalScrollIndicator={false}>
283
- <View style={styles.emptyIconWrap}>
284
- <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
285
- </View>
286
- <Text style={styles.emptyTitle}>
287
- {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
288
- </Text>
289
- <Text style={styles.emptySubtitle}>
290
- {search
291
- ? 'Try modifying your search query'
292
- : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
293
- </Text>
294
- </ScrollView>) : (<ScrollView style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
295
- {filteredEntries.map(entry => {
296
- const isExpanded = Boolean(expandedKeys[entry.key]);
297
- const badge = getTypeBadge(entry.type);
298
- const formattedBytes = entry.byteSize < 1024
299
- ? `${entry.byteSize} B`
300
- : `${(entry.byteSize / 1024).toFixed(1)} KB`;
301
- return (<View key={entry.key} style={styles.entryCard}>
302
- {/* Entry Header: Key name, Type Badge, Size, Actions */}
303
- <View style={styles.entryHeader}>
304
- <View style={{ flex: 1, marginRight: 8 }}>
305
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
306
- <Text style={styles.entryKey} numberOfLines={1} selectable>
307
- {entry.key}
308
- </Text>
309
- <View style={[
310
- styles.typeBadge,
311
- { backgroundColor: badge.bg, borderColor: `${badge.color}33` },
312
- ]}>
313
- <Text style={[styles.typeBadgeText, { color: badge.color }]}>
314
- {badge.label}
315
- </Text>
316
- </View>
317
- <Text style={styles.sizeText}>{formattedBytes}</Text>
318
- </View>
319
- </View>
320
-
321
- {/* Actions: Copy, Edit, Delete */}
322
- <View style={styles.entryActions}>
323
- <TouchableScale hitSlop={6} onPress={() => {
324
- copyToClipboard(entry.value, entry.key);
325
- showToast(`Copied value of "${entry.key}"`);
326
- }} style={styles.entryActionBtn}>
327
- <CopyIcon size={12} color={AppColors.grayText}/>
328
- </TouchableScale>
329
-
330
- <TouchableScale hitSlop={6} onPress={() => handleOpenEdit(entry)} style={styles.entryActionBtn}>
331
- <PencilIcon size={12} color={AppColors.purple}/>
332
- </TouchableScale>
333
-
334
- <TouchableScale hitSlop={6} onPress={() => handleDeleteKey(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
335
- <TrashIcon size={12} color={AppColors.errorColor}/>
336
- </TouchableScale>
337
- </View>
338
- </View>
339
-
340
- {/* Entry Value Preview / Viewer */}
341
- <TouchableScale onPress={() => toggleExpand(entry.key)} style={styles.valuePreviewBox}>
342
- <Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable>
343
- {entry.type === 'json' && entry.parsedValue
344
- ? isExpanded
345
- ? JSON.stringify(entry.parsedValue, null, 2)
346
- : entry.value
347
- : entry.value}
348
- </Text>
349
- {entry.value.length > 80 && (<Text style={styles.expandHint}>
350
- {isExpanded ? '▲ Collapse' : '▼ Expand'}
351
- </Text>)}
352
- </TouchableScale>
353
- </View>);
354
- })}
355
- <View style={{ height: 60 }}/>
356
- </ScrollView>)}
375
+ </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}>
376
+ <View style={styles.emptyIconWrap}>
377
+ <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
378
+ </View>
379
+ <Text style={styles.emptyTitle}>
380
+ {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
381
+ </Text>
382
+ <Text style={styles.emptySubtitle}>
383
+ {search
384
+ ? 'Try modifying your search query'
385
+ : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
386
+ </Text>
387
+ </View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>)}
357
388
 
358
389
  {/* ── Create / Edit Key Modal ── */}
359
390
  <Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
@@ -5,6 +5,7 @@ import { useTranslation } from '../i18n';
5
5
  import TreeNode from './TreeNode';
6
6
  import SegmentedTabs from './SegmentedTabs';
7
7
  import HighlightText from './HighlightText';
8
+ import CopyButton from './CopyButton';
8
9
  import { PrettyIcon, RawIcon, TableIcon } from './NetworkIcons';
9
10
  // Styles
10
11
  import { AppColors } from '../styles/AppColors';
@@ -151,6 +152,7 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth = 1
151
152
  icon: (isActive) => (<TableIcon color={isActive ? AppColors.white : AppColors.slate400} size={12}/>),
152
153
  },
153
154
  ]} activeKey={mode} onChange={key => setMode(key)}/>
155
+ <CopyButton value={data} label="JSON"/>
154
156
  </View>)}
155
157
 
156
158
  {/* ── Main View Content Area (Royal Monospace Theme) ── */}
@@ -11,6 +11,7 @@ import { getDurationColor, handleOpenExternalLink } from '../helpers';
11
11
  import { ChevronIcon, CalendarIcon, StatusIcon, ClockIcon, SizeIcon, TerminalIcon, GlobeIcon, } from './NetworkIcons';
12
12
  // Components
13
13
  import TouchableScale from './TouchableScale';
14
+ import CopyButton from './CopyButton';
14
15
  // Stylesheet
15
16
  import { AppColors } from '../styles/AppColors';
16
17
  import { AppFonts } from '../styles/AppFonts';
@@ -60,9 +61,20 @@ const MetaAccordion = ({ status, statusColor, duration, size, triggeredAt, metho
60
61
  <Pressable onPress={toggleOpen} hitSlop={12}>
61
62
  <View style={styles.metaHeader}>
62
63
  <Text style={styles.metaTitle}>Metadata</Text>
63
- <Animated.View style={chevronStyle}>
64
- <ChevronIcon color={AppColors.grayTextWeak} size={14}/>
65
- </Animated.View>
64
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
65
+ <CopyButton value={{
66
+ status: status ?? 0,
67
+ duration: duration != null ? `${duration} ms` : '—',
68
+ size: size || '—',
69
+ triggeredAt: triggeredAt || '',
70
+ method: method || 'GET',
71
+ contentType: contentType || 'application/json',
72
+ url: url || '',
73
+ }} label="Metadata"/>
74
+ <Animated.View style={chevronStyle}>
75
+ <ChevronIcon color={AppColors.grayTextWeak} size={14}/>
76
+ </Animated.View>
77
+ </View>
66
78
  </View>
67
79
  </Pressable>
68
80
 
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface SkeletonPlaceholderProps {
3
+ cardCount?: number;
4
+ }
5
+ declare const SkeletonPlaceholder: React.NamedExoticComponent<SkeletonPlaceholderProps>;
6
+ export default SkeletonPlaceholder;