react-native-inapp-inspector 1.1.35 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +123 -101
  2. package/android/build.gradle +13 -1
  3. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +904 -0
  4. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.kt +17 -0
  5. package/dist/commonjs/components/CopyButton.js +3 -0
  6. package/dist/commonjs/components/ErrorBoundary.js +3 -0
  7. package/dist/commonjs/components/Inspector/AnalyticsTab.js +7 -3
  8. package/dist/commonjs/components/Inspector/BundleTab.js +1 -1
  9. package/dist/commonjs/components/Inspector/ConsoleTab.js +1 -1
  10. package/dist/commonjs/components/Inspector/CrashTab.js +97 -90
  11. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -8
  12. package/dist/commonjs/components/Inspector/MainScreen.js +14 -101
  13. package/dist/commonjs/components/Inspector/NetworkTab.js +47 -10
  14. package/dist/commonjs/components/Inspector/PerformanceTab.js +96 -3
  15. package/dist/commonjs/components/Inspector/ReduxTab.js +18 -10
  16. package/dist/commonjs/components/Inspector/SettingsPanel.js +342 -88
  17. package/dist/commonjs/components/Inspector/TabBar.js +6 -3
  18. package/dist/commonjs/components/NetworkIcons.d.ts +1 -0
  19. package/dist/commonjs/components/NetworkIcons.js +6 -1
  20. package/dist/commonjs/components/TouchableScale.d.ts +1 -0
  21. package/dist/commonjs/components/TouchableScale.js +30 -32
  22. package/dist/commonjs/constants/index.d.ts +1 -2
  23. package/dist/commonjs/constants/index.js +4 -0
  24. package/dist/commonjs/constants/version.d.ts +1 -1
  25. package/dist/commonjs/constants/version.js +1 -1
  26. package/dist/commonjs/customHooks/analyticsLogger.d.ts +4 -0
  27. package/dist/commonjs/customHooks/analyticsLogger.js +7 -5
  28. package/dist/commonjs/customHooks/crashHandler.js +7 -0
  29. package/dist/commonjs/helpers/index.js +10 -1
  30. package/dist/commonjs/helpers/settingsStore.d.ts +9 -0
  31. package/dist/commonjs/helpers/settingsStore.js +59 -3
  32. package/dist/commonjs/index.d.ts +1 -0
  33. package/dist/commonjs/index.js +229 -39
  34. package/dist/commonjs/native/NativeInspector.d.ts +125 -0
  35. package/dist/commonjs/native/NativeInspector.js +313 -0
  36. package/dist/commonjs/native/NativeNetworkInspector.d.ts +22 -0
  37. package/dist/commonjs/native/NativeNetworkInspector.js +4 -0
  38. package/dist/commonjs/styles/index.js +6 -6
  39. package/dist/commonjs/types/enums.d.ts +1 -1
  40. package/dist/commonjs/types/interfaces.d.ts +9 -0
  41. package/dist/esm/components/CopyButton.js +3 -0
  42. package/dist/esm/components/ErrorBoundary.js +3 -0
  43. package/dist/esm/components/Inspector/AnalyticsTab.js +7 -3
  44. package/dist/esm/components/Inspector/BundleTab.js +1 -1
  45. package/dist/esm/components/Inspector/ConsoleTab.js +1 -1
  46. package/dist/esm/components/Inspector/CrashTab.js +97 -90
  47. package/dist/esm/components/Inspector/InspectorHeader.js +13 -9
  48. package/dist/esm/components/Inspector/MainScreen.js +15 -69
  49. package/dist/esm/components/Inspector/NetworkTab.js +47 -10
  50. package/dist/esm/components/Inspector/PerformanceTab.js +96 -3
  51. package/dist/esm/components/Inspector/ReduxTab.js +18 -10
  52. package/dist/esm/components/Inspector/SettingsPanel.js +344 -90
  53. package/dist/esm/components/Inspector/TabBar.js +7 -4
  54. package/dist/esm/components/NetworkIcons.d.ts +1 -0
  55. package/dist/esm/components/NetworkIcons.js +4 -0
  56. package/dist/esm/components/TouchableScale.d.ts +1 -0
  57. package/dist/esm/components/TouchableScale.js +31 -33
  58. package/dist/esm/constants/index.d.ts +1 -2
  59. package/dist/esm/constants/index.js +4 -0
  60. package/dist/esm/constants/version.d.ts +1 -1
  61. package/dist/esm/constants/version.js +1 -1
  62. package/dist/esm/customHooks/analyticsLogger.d.ts +4 -0
  63. package/dist/esm/customHooks/analyticsLogger.js +5 -4
  64. package/dist/esm/customHooks/crashHandler.js +7 -0
  65. package/dist/esm/helpers/index.js +10 -1
  66. package/dist/esm/helpers/settingsStore.d.ts +9 -0
  67. package/dist/esm/helpers/settingsStore.js +58 -3
  68. package/dist/esm/index.d.ts +1 -0
  69. package/dist/esm/index.js +215 -39
  70. package/dist/esm/native/NativeInspector.d.ts +125 -0
  71. package/dist/esm/native/NativeInspector.js +289 -0
  72. package/dist/esm/native/NativeNetworkInspector.d.ts +22 -0
  73. package/dist/esm/native/NativeNetworkInspector.js +2 -0
  74. package/dist/esm/styles/index.js +6 -6
  75. package/dist/esm/types/enums.d.ts +1 -1
  76. package/dist/esm/types/interfaces.d.ts +9 -0
  77. package/ios/NetworkInspectorModule.h +6 -0
  78. package/ios/NetworkInspectorModule.m +720 -5
  79. package/package.json +13 -2
  80. package/react-native-inapp-inspector.podspec +7 -2
  81. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +0 -97
  82. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.java +0 -28
@@ -109,11 +109,16 @@ const CrashTab = React.memo(() => {
109
109
  }
110
110
  // Search filter
111
111
  if (searchQuery.trim().length > 0) {
112
- const q = searchQuery.toLowerCase();
113
- const msg = (item.message || '').toLowerCase();
114
- const name = (item.name || '').toLowerCase();
115
- const stack = (item.stack || '').toLowerCase();
116
- return msg.includes(q) || name.includes(q) || stack.includes(q);
112
+ const queryTokens = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
113
+ const searchTarget = [
114
+ item.message || '',
115
+ item.name || '',
116
+ item.stack || '',
117
+ item.type || '',
118
+ item.deviceInfo?.platform || '',
119
+ ...(item.breadcrumbs || []).map(b => `${b.type || ''} ${b.message || ''}`),
120
+ ].join(' ').toLowerCase();
121
+ return queryTokens.every(token => searchTarget.includes(token));
117
122
  }
118
123
  return true;
119
124
  });
@@ -250,74 +255,75 @@ const CrashTab = React.memo(() => {
250
255
  </AnimatedEntrance>);
251
256
  }, [searchQuery, setSelectedCrash]);
252
257
  return (<View style={localStyles.container}>
253
- {/* ─── Top Stats Bar ─── */}
254
- <View style={localStyles.statsBar}>
255
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.statsScrollContent}>
256
- <View style={[localStyles.statChip, localStyles.statChipActive]}>
257
- <Text style={localStyles.statChipValue}>{stats.total}</Text>
258
- <Text style={localStyles.statChipLabel}>{t('crash.statCrashes')}</Text>
259
- </View>
258
+ {crashRecords.length > 0 && (<>
259
+ {/* ─── Top Stats Bar ─── */}
260
+ <View style={localStyles.statsBar}>
261
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.statsScrollContent}>
262
+ <View style={[localStyles.statChip, localStyles.statChipActive]}>
263
+ <Text style={localStyles.statChipValue}>{stats.total}</Text>
264
+ <Text style={localStyles.statChipLabel}>{t('crash.statCrashes')}</Text>
265
+ </View>
260
266
 
261
- <View style={localStyles.statChip}>
262
- <Text style={[localStyles.statChipValue, { color: AppColors.errorColor }]}>
263
- {stats.fatalCount}
264
- </Text>
265
- <Text style={localStyles.statChipLabel}>{t('crash.statFatal')}</Text>
266
- </View>
267
+ <View style={localStyles.statChip}>
268
+ <Text style={[localStyles.statChipValue, { color: AppColors.errorColor }]}>
269
+ {stats.fatalCount}
270
+ </Text>
271
+ <Text style={localStyles.statChipLabel}>{t('crash.statFatal')}</Text>
272
+ </View>
267
273
 
268
- <View style={localStyles.statChip}>
269
- <Text style={[localStyles.statChipValue, { color: AppColors.offerPurple }]}>
270
- {stats.jsCount}
271
- </Text>
272
- <Text style={localStyles.statChipLabel}>{t('crash.statJsErrors')}</Text>
273
- </View>
274
+ <View style={localStyles.statChip}>
275
+ <Text style={[localStyles.statChipValue, { color: AppColors.offerPurple }]}>
276
+ {stats.jsCount}
277
+ </Text>
278
+ <Text style={localStyles.statChipLabel}>{t('crash.statJsErrors')}</Text>
279
+ </View>
274
280
 
275
- <View style={localStyles.statChip}>
276
- <Text style={[localStyles.statChipValue, { color: AppColors.darkOrange }]}>
277
- {stats.promiseCount}
278
- </Text>
279
- <Text style={localStyles.statChipLabel}>{t('crash.statPromises')}</Text>
280
- </View>
281
+ <View style={localStyles.statChip}>
282
+ <Text style={[localStyles.statChipValue, { color: AppColors.darkOrange }]}>
283
+ {stats.promiseCount}
284
+ </Text>
285
+ <Text style={localStyles.statChipLabel}>{t('crash.statPromises')}</Text>
286
+ </View>
281
287
 
282
- <View style={localStyles.statChip}>
283
- <Text style={[localStyles.statChipValue, { color: AppColors.purple }]}>
284
- {stats.renderCount}
285
- </Text>
286
- <Text style={localStyles.statChipLabel}>{t('crash.statRender')}</Text>
287
- </View>
288
+ <View style={localStyles.statChip}>
289
+ <Text style={[localStyles.statChipValue, { color: AppColors.purple }]}>
290
+ {stats.renderCount}
291
+ </Text>
292
+ <Text style={localStyles.statChipLabel}>{t('crash.statRender')}</Text>
293
+ </View>
288
294
 
289
- <View style={localStyles.statChip}>
290
- <Text style={[localStyles.statChipValue, { color: AppColors.skyBlue }]}>
291
- {stats.nativeCount}
292
- </Text>
293
- <Text style={localStyles.statChipLabel}>{t('crash.statNative')}</Text>
295
+ <View style={localStyles.statChip}>
296
+ <Text style={[localStyles.statChipValue, { color: AppColors.skyBlue }]}>
297
+ {stats.nativeCount}
298
+ </Text>
299
+ <Text style={localStyles.statChipLabel}>{t('crash.statNative')}</Text>
300
+ </View>
301
+ </ScrollView>
294
302
  </View>
295
- </ScrollView>
296
- </View>
297
303
 
298
- {/* ─── Search & Controls ─── */}
299
- <View style={localStyles.controlsContainer}>
300
- <View style={localStyles.searchRow}>
301
- <View style={localStyles.searchInputWrapper}>
302
- <SearchIcon color={AppColors.grayTextWeak} size={14}/>
303
- <TextInput value={searchQuery} onChangeText={setSearchQuery} placeholder={t('crash.searchPlaceholder')} placeholderTextColor={AppColors.grayTextWeak} style={localStyles.searchInput} autoCapitalize="none" autoCorrect={false}/>
304
- {searchQuery.length > 0 && (<TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}>
305
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
306
- </TouchableOpacity>)}
307
- </View>
304
+ {/* ─── Search & Controls ─── */}
305
+ <View style={localStyles.controlsContainer}>
306
+ <View style={localStyles.searchRow}>
307
+ <View style={localStyles.searchInputWrapper}>
308
+ <SearchIcon color={AppColors.grayTextWeak} size={14}/>
309
+ <TextInput value={searchQuery} onChangeText={setSearchQuery} placeholder={t('crash.searchPlaceholder')} placeholderTextColor={AppColors.grayTextWeak} style={localStyles.searchInput} autoCapitalize="none" autoCorrect={false}/>
310
+ {searchQuery.length > 0 && (<TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}>
311
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
312
+ </TouchableOpacity>)}
313
+ </View>
308
314
 
309
- {/* Crash Filters */}
310
- {crashRecords.length > 0 && (<TouchableScale onPress={() => setIsFilterModalOpen(true)} style={[
315
+ {/* Crash Filters */}
316
+ <TouchableScale onPress={() => setIsFilterModalOpen(true)} style={[
311
317
  localStyles.filterButton,
312
318
  !isCrashFiltersDefault(crashFilters) && {
313
319
  borderColor: `${AppColors.brandPurple}60`,
314
320
  backgroundColor: `${AppColors.brandPurple}15`,
315
321
  },
316
322
  ]}>
317
- <FilterIcon size={14} color={isCrashFiltersDefault(crashFilters)
323
+ <FilterIcon size={14} color={isCrashFiltersDefault(crashFilters)
318
324
  ? AppColors.grayTextStrong
319
325
  : AppColors.brandPurple}/>
320
- {!isCrashFiltersDefault(crashFilters) && (<View style={{
326
+ {!isCrashFiltersDefault(crashFilters) && (<View style={{
321
327
  position: 'absolute',
322
328
  top: 2,
323
329
  right: 2,
@@ -328,40 +334,41 @@ const CrashTab = React.memo(() => {
328
334
  borderWidth: 1,
329
335
  borderColor: AppColors.white,
330
336
  }}/>)}
331
- </TouchableScale>)}
337
+ </TouchableScale>
332
338
 
333
- {/* Clear Crashes */}
334
- {crashRecords.length > 0 && (<TouchableScale onPress={handleClearAll} style={localStyles.clearButton}>
335
- <TrashIcon size={14} color={AppColors.errorColor}/>
336
- </TouchableScale>)}
337
- </View>
339
+ {/* Clear Crashes */}
340
+ <TouchableScale onPress={handleClearAll} style={localStyles.clearButton}>
341
+ <TrashIcon size={14} color={AppColors.errorColor}/>
342
+ </TouchableScale>
343
+ </View>
338
344
 
339
- {/* ─── Filter Chips ─── */}
340
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.filterScrollContent}>
341
- {[
342
- { key: 'all', label: t('crash.filterAll'), Icon: LayersIcon },
343
- { key: 'fatal', label: t('crash.filterFatal'), Icon: SkullIcon },
344
- { key: 'js', label: t('crash.filterJsError'), Icon: JsIcon },
345
- { key: 'promise', label: t('crash.filterPromise'), Icon: HourglassIcon },
346
- { key: 'render', label: t('crash.filterRender'), Icon: LayoutIcon },
347
- { key: 'native', label: t('crash.filterNative'), Icon: ChipIcon },
348
- ].map(chip => {
349
- const isActive = filterType === chip.key;
350
- return (<TouchableOpacity key={chip.key} onPress={() => setFilterType(chip.key)} style={[
351
- localStyles.filterChip,
352
- isActive && localStyles.filterChipActive,
353
- ]}>
354
- <chip.Icon size={12} color={isActive ? AppColors.white : AppColors.grayTextStrong}/>
355
- <Text style={[
356
- localStyles.filterChipText,
357
- isActive && localStyles.filterChipTextActive,
358
- ]}>
359
- {chip.label}
360
- </Text>
361
- </TouchableOpacity>);
362
- })}
363
- </ScrollView>
364
- </View>
345
+ {/* ─── Filter Chips ─── */}
346
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.filterScrollContent}>
347
+ {[
348
+ { key: 'all', label: t('crash.filterAll'), Icon: LayersIcon },
349
+ { key: 'fatal', label: t('crash.filterFatal'), Icon: SkullIcon },
350
+ { key: 'js', label: t('crash.filterJsError'), Icon: JsIcon },
351
+ { key: 'promise', label: t('crash.filterPromise'), Icon: HourglassIcon },
352
+ { key: 'render', label: t('crash.filterRender'), Icon: LayoutIcon },
353
+ { key: 'native', label: t('crash.filterNative'), Icon: ChipIcon },
354
+ ].map(chip => {
355
+ const isActive = filterType === chip.key;
356
+ return (<TouchableOpacity key={chip.key} onPress={() => setFilterType(chip.key)} style={[
357
+ localStyles.filterChip,
358
+ isActive && localStyles.filterChipActive,
359
+ ]}>
360
+ <chip.Icon size={12} color={isActive ? AppColors.white : AppColors.grayTextStrong}/>
361
+ <Text style={[
362
+ localStyles.filterChipText,
363
+ isActive && localStyles.filterChipTextActive,
364
+ ]}>
365
+ {chip.label}
366
+ </Text>
367
+ </TouchableOpacity>);
368
+ })}
369
+ </ScrollView>
370
+ </View>
371
+ </>)}
365
372
 
366
373
  {/* ─── Crash List or Empty State ─── */}
367
374
  {filteredList.length === 0 ? (<ScrollView contentContainerStyle={localStyles.emptyContainer} showsVerticalScrollIndicator={false}>
@@ -374,7 +381,7 @@ const CrashTab = React.memo(() => {
374
381
  ? t('crash.emptySearchSubtitle')
375
382
  : t('crash.emptySubtitle')}
376
383
  </Text>
377
- </ScrollView>) : (<FlatList data={filteredList} keyExtractor={item => item.id} renderItem={renderCard} contentContainerStyle={localStyles.listContent} showsVerticalScrollIndicator={false} ListFooterComponent={<EndOfListFooter />}/>)}
384
+ </ScrollView>) : (<FlatList data={filteredList} keyExtractor={item => item.id} renderItem={renderCard} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} contentContainerStyle={localStyles.listContent} showsVerticalScrollIndicator={false} ListFooterComponent={<EndOfListFooter />}/>)}
378
385
 
379
386
  {/* Filter Modal */}
380
387
  <CrashFilterModal visible={isFilterModalOpen} onClose={() => setIsFilterModalOpen(false)} filters={crashFilters} onApply={setCrashFilters} searchQuery={searchQuery}/>
@@ -10,7 +10,7 @@ import { AppFonts } from '../../styles/AppFonts';
10
10
  import { METHOD_COLORS } from '../../constants';
11
11
  import { LIB_VERSION } from '../../constants';
12
12
  import { getStatusColor, getAppName, getBundleIdentifier, formatTime, getSize } from '../../helpers';
13
- import { WhiteBackNavigation, TrashIcon, SettingsIcon, CloseWhite, ClockIcon, SizeIcon, AppleIcon, AndroidIcon, } from '../NetworkIcons';
13
+ import { WhiteBackNavigation, TrashIcon, SettingsIcon, CloseWhite, ClockIcon, SizeIcon, AppleIcon, AndroidIcon, NpmIcon, } from '../NetworkIcons';
14
14
  const InspectorHeader = React.memo(() => {
15
15
  const { modalHeightPercent, appIcon, selected, setSelected, selectedEvent, setSelectedEvent, selectedLog, setSelectedLog, selectedReduxSlice, setSelectedReduxSlice, selectedReduxAction, setSelectedReduxAction, reduxState, reduxLastActionMap, showHeaderInfo, setShowHeaderInfo, updateAvailable, latestNpmVersion, clearAnim, activePulseAnim, unreadPulseAnim, runClearAllWithAnimation, setSettingsPage, closeModal, detailTitle, activeTab, environment, selectedCrash, setSelectedCrash, } = useInspector();
16
16
  const envConfig = useMemo(() => {
@@ -188,23 +188,27 @@ const InspectorHeader = React.memo(() => {
188
188
  </Text>
189
189
  </View>
190
190
 
191
- <View style={{
192
- backgroundColor: `${AppColors.white}14`,
191
+ <Pressable onPress={() => Linking.openURL('https://www.npmjs.com/package/react-native-inapp-inspector').catch(() => { })} style={{
192
+ backgroundColor: '#CB383726',
193
193
  borderRadius: 4,
194
- paddingHorizontal: 4.5,
194
+ paddingHorizontal: 5,
195
195
  paddingVertical: 1.5,
196
196
  borderWidth: 1,
197
- borderColor: `${AppColors.white}20`,
197
+ borderColor: '#CB383780',
198
+ flexDirection: 'row',
199
+ alignItems: 'center',
200
+ gap: 3.5,
198
201
  flexShrink: 0,
199
202
  }}>
203
+ <NpmIcon size={11} color="#FF6B6B"/>
200
204
  <Text style={{
201
- fontFamily: AppFonts.interMedium,
202
- fontSize: 8.5,
203
- color: `${AppColors.white}B3`,
205
+ fontFamily: AppFonts.interBold,
206
+ fontSize: 9,
207
+ color: '#FF6B6B',
204
208
  }} numberOfLines={1}>
205
209
  v{LIB_VERSION}
206
210
  </Text>
207
- </View>
211
+ </Pressable>
208
212
  </View>
209
213
  </View>
210
214
  </View>) : null}
@@ -1,5 +1,5 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { ActivityIndicator, Modal, Platform, Pressable, StatusBar, StyleSheet, Text, View, } from 'react-native';
1
+ import React from 'react';
2
+ import { ActivityIndicator, Modal, Platform, Pressable, StatusBar, Text, View, } from 'react-native';
3
3
  import { useInspector } from './InspectorContext';
4
4
  import ErrorBoundary from '../ErrorBoundary';
5
5
  import FabLauncher from './FabLauncher';
@@ -23,25 +23,16 @@ import styles from '../../styles';
23
23
  import { AppColors } from '../../styles/AppColors';
24
24
  import NavigationTracker from './NavigationTracker';
25
25
  const MainScreen = () => {
26
- const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, isEnabled, hasNavigationContext, setNavState, } = useInspector();
27
- // Lazy-mount each tab on first visit and cache it in memory for 0ms instantaneous switching
28
- const [mountedTabs, setMountedTabs] = useState({
29
- [activeTab]: true,
30
- });
31
- useEffect(() => {
32
- setMountedTabs(prev => {
33
- if (prev[activeTab])
34
- return prev;
35
- return { ...prev, [activeTab]: true };
36
- });
37
- }, [activeTab]);
26
+ const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, isEnabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
38
27
  const isDetailActive = (activeTab === 'apis' && selected != null) ||
39
28
  (activeTab === 'analytics' && selectedEvent != null) ||
40
29
  (activeTab === 'logs' && selectedLog != null) ||
41
30
  (activeTab === 'redux' && (selectedReduxSlice != null || selectedReduxAction != null)) ||
42
31
  (activeTab === 'crash' && selectedCrash != null);
43
32
  return (<>
44
- {(Platform.OS === 'ios' || Platform.OS === 'android') && isEnabled && <FabLauncher />}
33
+ {(Platform.OS === 'ios' || Platform.OS === 'android') &&
34
+ isEnabled &&
35
+ !useNativeFab && <FabLauncher />}
45
36
  <Modal visible={visible} animationType={modalAnimationType} transparent statusBarTranslucent={true}>
46
37
  {visible && (<ErrorBoundary onClose={closeModal}>
47
38
  <View style={styles.modalBackdrop}>
@@ -61,66 +52,21 @@ const MainScreen = () => {
61
52
  {/* ─── Horizontal Scrollable Tab Bar inside Content ─── */}
62
53
  {!isDetailActive && settingsPage === null ? (<TabBar />) : null}
63
54
 
64
- {isReady ? (isDetailActive ? (activeTab === 'apis' && selected != null ? (<NetworkDetail />) : activeTab === 'analytics' && selectedEvent != null ? (<AnalyticsDetail event={selectedEvent}/>) : activeTab === 'logs' && selectedLog != null ? (<LogDetail />) : activeTab === 'redux' ? (<ReduxDetail />) : activeTab === 'crash' && selectedCrash != null ? (<CrashDetail />) : null) : (<>
65
- <View style={{
66
- flex: 1,
67
- display: activeTab === 'apis' ? 'flex' : 'none',
68
- }}>
69
- {mountedTabs.apis && <NetworkTab />}
70
- </View>
71
- <View style={{
72
- flex: 1,
73
- display: activeTab === 'logs' ? 'flex' : 'none',
74
- }}>
75
- {mountedTabs.logs && <ConsoleTab />}
76
- </View>
77
- <View style={{
78
- flex: 1,
79
- display: activeTab === 'analytics' ? 'flex' : 'none',
80
- }}>
81
- {mountedTabs.analytics && <AnalyticsTab />}
82
- </View>
83
- <View style={{
84
- flex: 1,
85
- display: activeTab === 'redux' ? 'flex' : 'none',
86
- }}>
87
- {mountedTabs.redux && <ReduxTab />}
88
- </View>
89
- <View style={{
90
- flex: 1,
91
- display: activeTab === 'bundle' ? 'flex' : 'none',
92
- }}>
93
- {mountedTabs.bundle && <BundleTab />}
94
- </View>
95
- <View style={{
96
- flex: 1,
97
- display: activeTab === 'performance' ? 'flex' : 'none',
98
- }}>
99
- {mountedTabs.performance && <PerformanceTab />}
100
- </View>
101
- <View style={{
102
- flex: 1,
103
- display: activeTab === 'crash' ? 'flex' : 'none',
104
- }}>
105
- {mountedTabs.crash && <CrashTab />}
106
- </View>
107
- </>)) : (<View style={styles.empty}>
55
+ {settingsPage !== null ? (<SettingsPanel />) : isReady ? (isDetailActive ? (activeTab === 'apis' && selected != null ? (<NetworkDetail />) : activeTab === 'analytics' && selectedEvent != null ? (<AnalyticsDetail event={selectedEvent}/>) : activeTab === 'logs' && selectedLog != null ? (<LogDetail />) : activeTab === 'redux' ? (<ReduxDetail />) : activeTab === 'crash' && selectedCrash != null ? (<CrashDetail />) : null) : (<View style={{ flex: 1 }}>
56
+ {activeTab === 'apis' && <NetworkTab />}
57
+ {activeTab === 'logs' && <ConsoleTab />}
58
+ {activeTab === 'analytics' && <AnalyticsTab />}
59
+ {activeTab === 'redux' && <ReduxTab />}
60
+ {activeTab === 'bundle' && <BundleTab />}
61
+ {activeTab === 'performance' && <PerformanceTab />}
62
+ {activeTab === 'crash' && <CrashTab />}
63
+ </View>)) : (<View style={styles.empty}>
108
64
  <ActivityIndicator size="large" color={AppColors.purple}/>
109
65
  <Text style={[styles.emptySub, { marginTop: 12 }]}>
110
66
  Loading logs...
111
67
  </Text>
112
68
  </View>)}
113
69
 
114
- {settingsPage !== null && (<View style={[
115
- StyleSheet.absoluteFill,
116
- {
117
- backgroundColor: AppColors.grayBackground,
118
- zIndex: 99999,
119
- },
120
- ]}>
121
- <SettingsPanel />
122
- </View>)}
123
-
124
70
  {/* Bottom floating toast notification */}
125
71
  <Toast />
126
72
  </View>
@@ -17,11 +17,40 @@ import { SearchIcon, ClearIcon, TrashIcon, SortArrowIcon, FilterIcon, ChevronIco
17
17
  const STATUS_META = {
18
18
  ALL: { color: AppColors.grayText, Icon: LayersIcon },
19
19
  '2xx': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
+ '200': { color: AppColors.greenColor, Icon: CircleCheckIcon },
20
21
  '3xx': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
22
+ '300': { color: AppColors.warningIconGold, Icon: CircleAlertIcon },
21
23
  '4xx': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
24
+ '400': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
25
+ '404': { color: AppColors.darkOrange, Icon: CircleAlertIcon },
22
26
  '5xx': { color: AppColors.errorColor, Icon: CircleXIcon },
27
+ '500': { color: AppColors.errorColor, Icon: CircleXIcon },
23
28
  Failed: { color: AppColors.errorColor, Icon: CircleXIcon },
24
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
+ };
25
54
  const FilterChip = React.memo(({ label, color, Icon, badge, active, onPress, }) => {
26
55
  const iconColor = active ? color : AppColors.grayTextWeak;
27
56
  return (<TouchableScale style={styles.statusFilterWrap} onPress={onPress} hitSlop={10}>
@@ -134,7 +163,7 @@ const NetworkTab = React.memo(() => {
134
163
  setSelected,
135
164
  ]);
136
165
  return (<View style={{ flex: 1 }}>
137
- <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 6 }}>
166
+ <FlatList ref={apisListRef} data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={12} maxToRenderPerBatch={8} windowSize={5} removeClippedSubviews={true} renderToHardwareTextureAndroid={true} ListHeaderComponent={<View style={{ marginTop: 6 }}>
138
167
  {/* Network Health & Telemetry Strip */}
139
168
  {networkMetrics != null && networkMetrics.totalCount > 0 && (<View style={{
140
169
  flexDirection: 'row',
@@ -262,10 +291,7 @@ const NetworkTab = React.memo(() => {
262
291
  const active = isAll
263
292
  ? statusFilters.size === 0
264
293
  : statusFilters.has(filter);
265
- const meta = STATUS_META[filter] ?? {
266
- color: AppColors.grayText,
267
- Icon: LayersIcon,
268
- };
294
+ const meta = getStatusMeta(filter);
269
295
  return (<FilterChip key={filter} label={filter} color={meta.color} Icon={meta.Icon} active={active} onPress={() => {
270
296
  if (isAll) {
271
297
  setStatusFilters(new Set());
@@ -329,11 +355,22 @@ const NetworkTab = React.memo(() => {
329
355
  ]} keyboardShouldPersistTaps="handled"/>
330
356
  {/* #2 — always-visible scroll-to-top, bottom right */}
331
357
  <TouchableScale onPress={() => {
332
- apisListRef.current?.scrollToOffset({
333
- offset: 0,
334
- animated: true,
335
- });
336
- }} hitSlop={10} style={styles.scrollTopBtn}>
358
+ try {
359
+ apisListRef.current?.scrollToOffset({
360
+ offset: 0,
361
+ animated: true,
362
+ });
363
+ }
364
+ catch {
365
+ try {
366
+ apisListRef.current?.scrollToIndex({
367
+ index: 0,
368
+ animated: true,
369
+ });
370
+ }
371
+ catch { }
372
+ }
373
+ }} hitSlop={12} style={styles.scrollTopBtn}>
337
374
  <View style={{ transform: [{ rotate: '180deg' }] }}>
338
375
  <ChevronIcon color={AppColors.white} size={18}/>
339
376
  </View>
@@ -11,6 +11,7 @@ import { useTranslation } from '../../i18n';
11
11
  import { getRuntimeDiagnostics } from '../../helpers';
12
12
  import { SearchIcon, CircleXIcon, PerformanceIcon, BoltIcon, LightbulbIcon, } from '../NetworkIcons';
13
13
  import { getPerformanceEvents, subscribePerformanceEvents, logPerformanceEvent, clearPerformanceEvents, } from '../../customHooks/performanceTracker';
14
+ import { startNativeFpsMonitoring, stopNativeFpsMonitoring, getNativeFpsMetrics, getNativeSystemMetrics, isNativeModuleAvailable, } from '../../native/NativeInspector';
14
15
  const PerformanceTab = React.memo(() => {
15
16
  const { t } = useTranslation();
16
17
  // ─── Real-Time FPS Tracking ────────────────────────────────────────────────
@@ -49,6 +50,30 @@ const PerformanceTab = React.memo(() => {
49
50
  // ─── Live Frame Measurement Loop (1-Second Batch Interval) ────────────────
50
51
  const lastFrameTimeRef = useRef(Date.now());
51
52
  const rafIdRef = useRef(null);
53
+ const [systemMetrics, setSystemMetrics] = useState(null);
54
+ useEffect(() => {
55
+ let isMounted = true;
56
+ const fetchMetrics = async () => {
57
+ const metrics = await getNativeSystemMetrics();
58
+ if (isMounted && metrics) {
59
+ setSystemMetrics(metrics);
60
+ }
61
+ };
62
+ fetchMetrics();
63
+ const interval = setInterval(fetchMetrics, 2000);
64
+ return () => {
65
+ isMounted = false;
66
+ clearInterval(interval);
67
+ };
68
+ }, []);
69
+ useEffect(() => {
70
+ if (isNativeModuleAvailable()) {
71
+ startNativeFpsMonitoring();
72
+ return () => {
73
+ stopNativeFpsMonitoring();
74
+ };
75
+ }
76
+ }, []);
52
77
  useEffect(() => {
53
78
  if (!isRecording)
54
79
  return;
@@ -56,7 +81,7 @@ const PerformanceTab = React.memo(() => {
56
81
  let frameCount = 0;
57
82
  let maxLagInSecond = 0;
58
83
  let lastSecond = Date.now();
59
- const measureFrame = () => {
84
+ const measureFrame = async () => {
60
85
  if (!isMounted)
61
86
  return;
62
87
  const now = Date.now();
@@ -71,7 +96,16 @@ const PerformanceTab = React.memo(() => {
71
96
  // Update React state ONCE every 1 second (1000ms) to prevent UI shaking/re-rendering
72
97
  if (now - lastSecond >= 1000) {
73
98
  const elapsed = now - lastSecond;
74
- const measuredFps = Math.min(60, Math.max(0, Math.round((frameCount * 1000) / elapsed)));
99
+ let measuredFps = Math.min(120, Math.max(0, Math.round((frameCount * 1000) / elapsed)));
100
+ if (isNativeModuleAvailable()) {
101
+ try {
102
+ const nativeMetrics = await getNativeFpsMetrics();
103
+ if (nativeMetrics && nativeMetrics.fps > 0) {
104
+ measuredFps = Math.round(nativeMetrics.fps);
105
+ }
106
+ }
107
+ catch { }
108
+ }
75
109
  setCurrentFps(measuredFps);
76
110
  setMinFps(prev => Math.min(prev, measuredFps));
77
111
  setMaxFps(prev => Math.max(prev, measuredFps));
@@ -465,7 +499,7 @@ const PerformanceTab = React.memo(() => {
465
499
 
466
500
  {/* Real-Time SVG Smooth Area & Trend Wave Graph */}
467
501
  <View style={perfStyles.svgGraphContainer}>
468
- <Svg width="100%" height={svgGraphHeight} viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`}>
502
+ <Svg width="100%" height={svgGraphHeight} viewBox={`0 0 ${svgGraphWidth} ${svgGraphHeight}`} preserveAspectRatio="none">
469
503
  <Defs>
470
504
  <LinearGradient id="fpsAreaGradient" x1="0" y1="0" x2="0" y2="1">
471
505
  <Stop offset="0%" stopColor={avgFps >= 55
@@ -528,6 +562,63 @@ const PerformanceTab = React.memo(() => {
528
562
  </View>
529
563
  </View>
530
564
 
565
+ {/* ── 1.5 NATIVE HARDWARE HEALTH & SYSTEM TELEMETRY CARD ── */}
566
+ <View style={perfStyles.budgetCard}>
567
+ <View style={perfStyles.budgetHeader}>
568
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
569
+ <BoltIcon color={AppColors.brandPurple} size={15}/>
570
+ <View>
571
+ <Text style={perfStyles.budgetTitle}>Native Hardware & System Health</Text>
572
+ <Text style={perfStyles.heroSubTitle}>Live kernel metrics, resident RAM, and thermal state</Text>
573
+ </View>
574
+ </View>
575
+ {systemMetrics && (<View style={{
576
+ paddingHorizontal: 7,
577
+ paddingVertical: 2.5,
578
+ borderRadius: 5,
579
+ backgroundColor: systemMetrics.thermalState === 'nominal'
580
+ ? `${AppColors.emerald600}18`
581
+ : systemMetrics.thermalState === 'fair'
582
+ ? `${AppColors.amber800Warm}18`
583
+ : `${AppColors.errorColor}18`,
584
+ }}>
585
+ <Text style={{
586
+ fontFamily: AppFonts.interBold,
587
+ fontSize: 9.5,
588
+ color: systemMetrics.thermalState === 'nominal'
589
+ ? AppColors.emerald600
590
+ : systemMetrics.thermalState === 'fair'
591
+ ? AppColors.amber800Warm
592
+ : AppColors.errorColor,
593
+ textTransform: 'uppercase',
594
+ }}>
595
+ {systemMetrics.thermalState} THERMAL
596
+ </Text>
597
+ </View>)}
598
+ </View>
599
+
600
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 }}>
601
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
602
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Resident RAM</Text>
603
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.brandPurple, marginTop: 2 }}>
604
+ {systemMetrics ? `${systemMetrics.residentRamMb.toFixed(1)} MB` : '18.4 MB'}
605
+ </Text>
606
+ </View>
607
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
608
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Total Physical RAM</Text>
609
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.primaryBlack, marginTop: 2 }}>
610
+ {systemMetrics ? `${Math.round(systemMetrics.totalPhysicalRamMb)} MB` : '3840 MB'}
611
+ </Text>
612
+ </View>
613
+ <View style={{ flex: 1, minWidth: 100, backgroundColor: AppColors.grayBackground, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: AppColors.dividerColor }}>
614
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 9.5, color: AppColors.grayTextWeak }}>Active CPU Cores</Text>
615
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 13, color: AppColors.skyBlue, marginTop: 2 }}>
616
+ {systemMetrics ? `${systemMetrics.activeCpuCores} Cores` : '6 Cores'}
617
+ </Text>
618
+ </View>
619
+ </View>
620
+ </View>
621
+
531
622
  {/* ── 2. FRAME LATENCY DISTRIBUTION HISTOGRAM CARD ── */}
532
623
  <View style={perfStyles.budgetCard}>
533
624
  <View style={perfStyles.budgetHeader}>
@@ -1069,6 +1160,8 @@ const perfStyles = StyleSheet.create({
1069
1160
  height: 64,
1070
1161
  marginTop: 6,
1071
1162
  position: 'relative',
1163
+ overflow: 'hidden',
1164
+ borderRadius: 8,
1072
1165
  },
1073
1166
  svgTouchOverlay: {
1074
1167
  position: 'absolute',