react-native-inapp-inspector 1.1.24 → 1.1.26

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 (114) hide show
  1. package/android/build.gradle +36 -0
  2. package/android/src/main/AndroidManifest.xml +3 -0
  3. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +114 -0
  4. package/android/src/main/java/com/inappinspector/NetworkInspectorPackage.java +28 -0
  5. package/dist/commonjs/components/AnalyticsEventCard.js +94 -88
  6. package/dist/commonjs/components/ConsoleLogCard.js +49 -25
  7. package/dist/commonjs/components/ErrorBoundary.d.ts +1 -1
  8. package/dist/commonjs/components/ErrorBoundary.js +9 -145
  9. package/dist/commonjs/components/Inspector/AnalyticsFilterModal.d.ts +7 -0
  10. package/dist/commonjs/components/Inspector/AnalyticsFilterModal.js +1002 -0
  11. package/dist/commonjs/components/Inspector/AnalyticsTab.js +637 -10
  12. package/dist/commonjs/components/Inspector/BundleTab.d.ts +1 -1
  13. package/dist/commonjs/components/Inspector/BundleTab.js +826 -406
  14. package/dist/commonjs/components/Inspector/CrashDetail.d.ts +3 -0
  15. package/dist/commonjs/components/Inspector/CrashDetail.js +796 -0
  16. package/dist/commonjs/components/Inspector/CrashFilterModal.d.ts +20 -0
  17. package/dist/commonjs/components/Inspector/CrashFilterModal.js +597 -0
  18. package/dist/commonjs/components/Inspector/CrashTab.d.ts +3 -0
  19. package/dist/commonjs/components/Inspector/CrashTab.js +727 -0
  20. package/dist/commonjs/components/Inspector/InspectorContext.js +6 -1
  21. package/dist/commonjs/components/Inspector/InspectorHeader.js +65 -28
  22. package/dist/commonjs/components/Inspector/LogDetail.js +227 -114
  23. package/dist/commonjs/components/Inspector/MainScreen.js +13 -4
  24. package/dist/commonjs/components/Inspector/NetworkDetail.js +131 -81
  25. package/dist/commonjs/components/Inspector/PerformanceTab.js +20 -10
  26. package/dist/commonjs/components/Inspector/SettingsPanel.js +90 -1
  27. package/dist/commonjs/components/Inspector/TabBar.js +15 -3
  28. package/dist/commonjs/components/NetworkIcons.d.ts +25 -0
  29. package/dist/commonjs/components/NetworkIcons.js +188 -1
  30. package/dist/commonjs/components/SegmentedTabs.js +33 -23
  31. package/dist/commonjs/components/TreeNode.js +13 -12
  32. package/dist/commonjs/constants/version.d.ts +1 -1
  33. package/dist/commonjs/constants/version.js +1 -1
  34. package/dist/commonjs/customHooks/analyticsLogger.js +70 -44
  35. package/dist/commonjs/customHooks/bundleAnalyzer.d.ts +5 -1
  36. package/dist/commonjs/customHooks/bundleAnalyzer.js +497 -126
  37. package/dist/commonjs/customHooks/consoleLogger.d.ts +1 -0
  38. package/dist/commonjs/customHooks/consoleLogger.js +153 -51
  39. package/dist/commonjs/customHooks/crashHandler.d.ts +64 -0
  40. package/dist/commonjs/customHooks/crashHandler.js +681 -0
  41. package/dist/commonjs/customHooks/networkLogger.js +74 -49
  42. package/dist/commonjs/customHooks/performanceTracker.d.ts +8 -0
  43. package/dist/commonjs/customHooks/performanceTracker.js +44 -3
  44. package/dist/commonjs/decorators/index.d.ts +49 -0
  45. package/dist/commonjs/decorators/index.js +142 -0
  46. package/dist/commonjs/helpers/gaAnalyticsRegistry.d.ts +18 -0
  47. package/dist/commonjs/helpers/gaAnalyticsRegistry.js +174 -0
  48. package/dist/commonjs/helpers/index.d.ts +7 -4
  49. package/dist/commonjs/helpers/index.js +102 -64
  50. package/dist/commonjs/i18n/locales/en.json +182 -4
  51. package/dist/commonjs/index.d.ts +6 -0
  52. package/dist/commonjs/index.js +310 -8
  53. package/dist/commonjs/styles/index.d.ts +4 -1
  54. package/dist/commonjs/styles/index.js +22 -19
  55. package/dist/commonjs/types/enums.d.ts +156 -9
  56. package/dist/commonjs/types/enums.js +139 -2
  57. package/dist/commonjs/types/interfaces.d.ts +70 -1
  58. package/dist/esm/components/AnalyticsEventCard.js +95 -89
  59. package/dist/esm/components/ConsoleLogCard.js +49 -25
  60. package/dist/esm/components/ErrorBoundary.d.ts +1 -1
  61. package/dist/esm/components/ErrorBoundary.js +10 -113
  62. package/dist/esm/components/Inspector/AnalyticsFilterModal.d.ts +7 -0
  63. package/dist/esm/components/Inspector/AnalyticsFilterModal.js +964 -0
  64. package/dist/esm/components/Inspector/AnalyticsTab.js +639 -12
  65. package/dist/esm/components/Inspector/BundleTab.d.ts +1 -1
  66. package/dist/esm/components/Inspector/BundleTab.js +827 -407
  67. package/dist/esm/components/Inspector/CrashDetail.d.ts +3 -0
  68. package/dist/esm/components/Inspector/CrashDetail.js +758 -0
  69. package/dist/esm/components/Inspector/CrashFilterModal.d.ts +20 -0
  70. package/dist/esm/components/Inspector/CrashFilterModal.js +557 -0
  71. package/dist/esm/components/Inspector/CrashTab.d.ts +3 -0
  72. package/dist/esm/components/Inspector/CrashTab.js +689 -0
  73. package/dist/esm/components/Inspector/InspectorContext.js +1 -0
  74. package/dist/esm/components/Inspector/InspectorHeader.js +66 -29
  75. package/dist/esm/components/Inspector/LogDetail.js +227 -114
  76. package/dist/esm/components/Inspector/MainScreen.js +14 -5
  77. package/dist/esm/components/Inspector/NetworkDetail.js +132 -49
  78. package/dist/esm/components/Inspector/PerformanceTab.js +20 -10
  79. package/dist/esm/components/Inspector/SettingsPanel.js +91 -2
  80. package/dist/esm/components/Inspector/TabBar.js +16 -4
  81. package/dist/esm/components/NetworkIcons.d.ts +25 -0
  82. package/dist/esm/components/NetworkIcons.js +161 -0
  83. package/dist/esm/components/SegmentedTabs.js +33 -23
  84. package/dist/esm/components/TreeNode.js +13 -12
  85. package/dist/esm/constants/version.d.ts +1 -1
  86. package/dist/esm/constants/version.js +1 -1
  87. package/dist/esm/customHooks/analyticsLogger.js +70 -44
  88. package/dist/esm/customHooks/bundleAnalyzer.d.ts +5 -1
  89. package/dist/esm/customHooks/bundleAnalyzer.js +493 -125
  90. package/dist/esm/customHooks/consoleLogger.d.ts +1 -0
  91. package/dist/esm/customHooks/consoleLogger.js +151 -50
  92. package/dist/esm/customHooks/crashHandler.d.ts +64 -0
  93. package/dist/esm/customHooks/crashHandler.js +659 -0
  94. package/dist/esm/customHooks/networkLogger.js +74 -49
  95. package/dist/esm/customHooks/performanceTracker.d.ts +8 -0
  96. package/dist/esm/customHooks/performanceTracker.js +38 -1
  97. package/dist/esm/decorators/index.d.ts +49 -0
  98. package/dist/esm/decorators/index.js +137 -0
  99. package/dist/esm/helpers/gaAnalyticsRegistry.d.ts +18 -0
  100. package/dist/esm/helpers/gaAnalyticsRegistry.js +169 -0
  101. package/dist/esm/helpers/index.d.ts +7 -4
  102. package/dist/esm/helpers/index.js +100 -63
  103. package/dist/esm/i18n/locales/en.json +182 -4
  104. package/dist/esm/index.d.ts +6 -0
  105. package/dist/esm/index.js +262 -8
  106. package/dist/esm/styles/index.d.ts +4 -1
  107. package/dist/esm/styles/index.js +22 -19
  108. package/dist/esm/types/enums.d.ts +156 -9
  109. package/dist/esm/types/enums.js +138 -3
  110. package/dist/esm/types/interfaces.d.ts +70 -1
  111. package/ios/NetworkInspectorModule.h +6 -0
  112. package/ios/NetworkInspectorModule.m +125 -0
  113. package/package.json +5 -2
  114. package/react-native-inapp-inspector.podspec +18 -0
@@ -0,0 +1,689 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { Alert, FlatList, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
3
+ import { useTranslation } from '../../i18n';
4
+ import { useInspector } from './InspectorContext';
5
+ import TouchableScale from '../TouchableScale';
6
+ import AnimatedEntrance from '../AnimatedEntrance';
7
+ import EndOfListFooter from '../EndOfListFooter';
8
+ import HighlightText from '../HighlightText';
9
+ import { AppColors } from '../../styles/AppColors';
10
+ import { AppFonts } from '../../styles/AppFonts';
11
+ import { clearCrashRecords, } from '../../customHooks/crashHandler';
12
+ import CrashFilterModal, { DEFAULT_CRASH_FILTERS, isCrashFiltersDefault, } from './CrashFilterModal';
13
+ import { SearchIcon, ClearIcon, TrashIcon, TerminalIcon, LayersIcon, ClockIcon, ShieldAlertIcon, FlameIcon, JsIcon, HourglassIcon, SkullIcon, LayoutIcon, ChipIcon, AppleIcon, AndroidIcon, RepeatIcon, CodeBracketsIcon, FilterIcon, } from '../NetworkIcons';
14
+ const getRelativeTime = (timestamp) => {
15
+ const now = Date.now();
16
+ const diff = Math.max(0, now - timestamp);
17
+ const seconds = Math.floor(diff / 1000);
18
+ const minutes = Math.floor(seconds / 60);
19
+ const hours = Math.floor(minutes / 60);
20
+ const days = Math.floor(hours / 24);
21
+ if (seconds < 60)
22
+ return `${seconds}s ago`;
23
+ if (minutes < 60)
24
+ return `${minutes}m ago`;
25
+ if (hours < 24)
26
+ return `${hours}h ago`;
27
+ return `${days}d ago`;
28
+ };
29
+ const CrashTab = React.memo(() => {
30
+ const { t } = useTranslation();
31
+ const { crashRecords, selectedCrash, setSelectedCrash } = useInspector();
32
+ const [searchQuery, setSearchQuery] = useState('');
33
+ const [filterType, setFilterType] = useState('all');
34
+ const [crashFilters, setCrashFilters] = useState(DEFAULT_CRASH_FILTERS);
35
+ const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
36
+ const [detailSubTab, setDetailSubTab] = useState('stack');
37
+ // Auto-open the latest crash detail on first visit if crashes exist
38
+ const hasAutoOpened = React.useRef(false);
39
+ React.useEffect(() => {
40
+ if (!hasAutoOpened.current && !selectedCrash && crashRecords.length > 0) {
41
+ setSelectedCrash(crashRecords[0]);
42
+ hasAutoOpened.current = true;
43
+ }
44
+ }, [crashRecords, selectedCrash, setSelectedCrash]);
45
+ // Calculate statistics
46
+ const stats = useMemo(() => {
47
+ const total = crashRecords.length;
48
+ let fatalCount = 0;
49
+ let jsCount = 0;
50
+ let promiseCount = 0;
51
+ let renderCount = 0;
52
+ let nativeCount = 0;
53
+ crashRecords.forEach(c => {
54
+ if (c.isFatal)
55
+ fatalCount++;
56
+ if (c.type === 'js')
57
+ jsCount++;
58
+ else if (c.type === 'promise')
59
+ promiseCount++;
60
+ else if (c.type === 'render')
61
+ renderCount++;
62
+ else if (c.type === 'native')
63
+ nativeCount++;
64
+ });
65
+ return { total, fatalCount, jsCount, promiseCount, renderCount, nativeCount };
66
+ }, [crashRecords]);
67
+ // Filtered crash list
68
+ const filteredList = useMemo(() => {
69
+ let list = crashRecords.filter(item => {
70
+ // Type filter
71
+ if (filterType === 'fatal' && !item.isFatal)
72
+ return false;
73
+ if (filterType === 'js' && item.type !== 'js')
74
+ return false;
75
+ if (filterType === 'promise' && item.type !== 'promise')
76
+ return false;
77
+ if (filterType === 'render' && item.type !== 'render')
78
+ return false;
79
+ if (filterType === 'native' && item.type !== 'native')
80
+ return false;
81
+ // Modal filters: crash types
82
+ if (crashFilters.types.size > 0 && !crashFilters.types.has('all')) {
83
+ const matches = (crashFilters.types.has('fatal') && item.isFatal) ||
84
+ (crashFilters.types.has('js') && item.type === 'js') ||
85
+ (crashFilters.types.has('promise') && item.type === 'promise') ||
86
+ (crashFilters.types.has('render') && item.type === 'render') ||
87
+ (crashFilters.types.has('native') && item.type === 'native');
88
+ if (!matches)
89
+ return false;
90
+ }
91
+ // Modal filters: time window
92
+ if (crashFilters.timeWindow !== 'all') {
93
+ const now = Date.now();
94
+ const cutoff = crashFilters.timeWindow === '15m'
95
+ ? now - 15 * 60 * 1000
96
+ : crashFilters.timeWindow === '1h'
97
+ ? now - 60 * 60 * 1000
98
+ : crashFilters.timeWindow === '24h'
99
+ ? now - 24 * 60 * 60 * 1000
100
+ : now - 7 * 24 * 60 * 60 * 1000;
101
+ if (item.timestamp < cutoff)
102
+ return false;
103
+ }
104
+ // Modal filters: platform
105
+ if (crashFilters.platform !== 'all') {
106
+ const platform = (item.deviceInfo?.platform || '').toLowerCase();
107
+ if (platform !== crashFilters.platform)
108
+ return false;
109
+ }
110
+ // Modal filters: engine
111
+ if (crashFilters.engine !== 'all') {
112
+ const isHermes = item.deviceInfo?.isHermes;
113
+ if (crashFilters.engine === 'hermes' && isHermes !== true)
114
+ return false;
115
+ if (crashFilters.engine === 'jsc' && (isHermes === true))
116
+ return false;
117
+ }
118
+ // Search filter
119
+ if (searchQuery.trim().length > 0) {
120
+ const q = searchQuery.toLowerCase();
121
+ const msg = (item.message || '').toLowerCase();
122
+ const name = (item.name || '').toLowerCase();
123
+ const stack = (item.stack || '').toLowerCase();
124
+ return msg.includes(q) || name.includes(q) || stack.includes(q);
125
+ }
126
+ return true;
127
+ });
128
+ // Modal filters: sort order
129
+ if (crashFilters.sortBy === 'time_asc') {
130
+ list = [...list].sort((a, b) => a.timestamp - b.timestamp);
131
+ }
132
+ return list;
133
+ }, [crashRecords, filterType, searchQuery, crashFilters]);
134
+ const handleClearAll = () => {
135
+ Alert.alert(t('crash.clearTitle'), t('crash.clearMessage'), [
136
+ { text: t('crash.clearCancel'), style: 'cancel' },
137
+ {
138
+ text: t('crash.clearConfirm'),
139
+ style: 'destructive',
140
+ onPress: () => clearCrashRecords(),
141
+ },
142
+ ]);
143
+ };
144
+ const renderCard = useCallback(({ item, index }) => {
145
+ const isFatal = item.isFatal;
146
+ const typeLabel = item.type.toUpperCase();
147
+ const topFrame = item.parsedStack && item.parsedStack.length > 0
148
+ ? item.parsedStack.find(f => f.isAppCode) || item.parsedStack[0]
149
+ : null;
150
+ const typeBadgeBg = item.type === 'native'
151
+ ? `${AppColors.errorColor}22`
152
+ : item.type === 'render'
153
+ ? `${AppColors.purple}22`
154
+ : item.type === 'promise'
155
+ ? `${AppColors.darkOrange}22`
156
+ : `${AppColors.offerPurple}22`;
157
+ const typeBadgeColor = item.type === 'native'
158
+ ? AppColors.errorColor
159
+ : item.type === 'render'
160
+ ? AppColors.purple
161
+ : item.type === 'promise'
162
+ ? AppColors.darkOrange
163
+ : AppColors.offerPurple;
164
+ return (<AnimatedEntrance index={index} distance={8}>
165
+ <TouchableScale onPress={() => setSelectedCrash(item)} style={[
166
+ localStyles.cardContainer,
167
+ isFatal && localStyles.fatalCardBorder,
168
+ ]}>
169
+ {/* Header: #S.No, Badges & Timestamp */}
170
+ <View style={localStyles.cardHeader}>
171
+ <View style={localStyles.badgeRow}>
172
+ <View style={localStyles.sNoBadge}>
173
+ <Text style={localStyles.sNoBadgeText}>#{index + 1}</Text>
174
+ </View>
175
+
176
+ {isFatal ? (<View style={localStyles.fatalBadge}>
177
+ <FlameIcon size={10} color={AppColors.white}/>
178
+ <Text style={localStyles.fatalBadgeText}>{t('crash.fatalBadge')}</Text>
179
+ </View>) : (<View style={localStyles.caughtBadge}>
180
+ <Text style={localStyles.caughtBadgeText}>{t('crash.handledBadge')}</Text>
181
+ </View>)}
182
+
183
+ <View style={[
184
+ localStyles.typeBadge,
185
+ { backgroundColor: typeBadgeBg },
186
+ ]}>
187
+ <Text style={[
188
+ localStyles.typeBadgeText,
189
+ { color: typeBadgeColor },
190
+ ]}>
191
+ {typeLabel}
192
+ </Text>
193
+ </View>
194
+ </View>
195
+
196
+ <View style={localStyles.timeRow}>
197
+ <ClockIcon size={11} color={AppColors.grayTextWeak}/>
198
+ <Text style={localStyles.timestampText}>
199
+ {item.timeStr || new Date(item.timestamp).toLocaleTimeString()}
200
+ </Text>
201
+ <Text style={localStyles.relativeTimeText}>
202
+ {getRelativeTime(item.timestamp)}
203
+ </Text>
204
+ </View>
205
+ </View>
206
+
207
+ {/* Error Message */}
208
+ <View style={{ marginVertical: 6 }}>
209
+ <HighlightText text={item.message || t('crash.unknownException')} highlight={searchQuery} numberOfLines={3} style={localStyles.errorMessageText}/>
210
+ </View>
211
+
212
+ {/* Top stack frame location if available */}
213
+ {topFrame && (<View style={localStyles.locationRow}>
214
+ <TerminalIcon size={12} color={AppColors.grayTextWeak}/>
215
+ <Text numberOfLines={1} ellipsizeMode="middle" style={localStyles.locationText}>
216
+ {topFrame.method !== '<anonymous>' ? `${topFrame.method} ` : ''}
217
+ <Text style={{ color: AppColors.purple, fontFamily: AppFonts.interBold }}>
218
+ {topFrame.file}:{topFrame.lineNumber}
219
+ </Text>
220
+ </Text>
221
+ </View>)}
222
+
223
+ {/* Footer row: Actions */}
224
+ <View style={localStyles.cardFooter}>
225
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
226
+ {item.deviceInfo && (<View style={localStyles.metaPill}>
227
+ {item.deviceInfo.platform === 'ios' ? (<AppleIcon size={10} color={AppColors.grayText}/>) : item.deviceInfo.platform === 'android' ? (<AndroidIcon size={10} color={AppColors.grayText}/>) : null}
228
+ <Text style={localStyles.metaPillText}>
229
+ {item.deviceInfo.platform?.toUpperCase()}{' '}
230
+ {item.deviceInfo.osVersion || ''}
231
+ </Text>
232
+ </View>)}
233
+ {item.deviceInfo && (<View style={localStyles.metaPill}>
234
+ <Text style={localStyles.metaPillText}>
235
+ {item.deviceInfo.isHermes ? 'Hermes' : 'JSC'}
236
+ </Text>
237
+ </View>)}
238
+ {item.parsedStack && item.parsedStack.length > 0 && (<View style={localStyles.metaPill}>
239
+ <CodeBracketsIcon size={10} color={AppColors.grayText}/>
240
+ <Text style={localStyles.metaPillText}>
241
+ {item.parsedStack.length}
242
+ </Text>
243
+ </View>)}
244
+ {item.breadcrumbs && item.breadcrumbs.length > 0 && (<View style={localStyles.metaPill}>
245
+ <RepeatIcon size={10} color={AppColors.grayText}/>
246
+ <Text style={localStyles.metaPillText}>
247
+ {item.breadcrumbs.length} trail
248
+ </Text>
249
+ </View>)}
250
+ {item.memoryInfo && (<View style={localStyles.metaPill}>
251
+ <Text style={localStyles.metaPillText}>
252
+ {item.memoryInfo.usedJSHeapSize} MB
253
+ </Text>
254
+ </View>)}
255
+ </View>
256
+ </View>
257
+ </TouchableScale>
258
+ </AnimatedEntrance>);
259
+ }, [searchQuery, setSelectedCrash]);
260
+ return (<View style={localStyles.container}>
261
+ {/* ─── Top Stats Bar ─── */}
262
+ <View style={localStyles.statsBar}>
263
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.statsScrollContent}>
264
+ <View style={[localStyles.statChip, localStyles.statChipActive]}>
265
+ <Text style={localStyles.statChipValue}>{stats.total}</Text>
266
+ <Text style={localStyles.statChipLabel}>{t('crash.statCrashes')}</Text>
267
+ </View>
268
+
269
+ <View style={localStyles.statChip}>
270
+ <Text style={[localStyles.statChipValue, { color: AppColors.errorColor }]}>
271
+ {stats.fatalCount}
272
+ </Text>
273
+ <Text style={localStyles.statChipLabel}>{t('crash.statFatal')}</Text>
274
+ </View>
275
+
276
+ <View style={localStyles.statChip}>
277
+ <Text style={[localStyles.statChipValue, { color: AppColors.offerPurple }]}>
278
+ {stats.jsCount}
279
+ </Text>
280
+ <Text style={localStyles.statChipLabel}>{t('crash.statJsErrors')}</Text>
281
+ </View>
282
+
283
+ <View style={localStyles.statChip}>
284
+ <Text style={[localStyles.statChipValue, { color: AppColors.darkOrange }]}>
285
+ {stats.promiseCount}
286
+ </Text>
287
+ <Text style={localStyles.statChipLabel}>{t('crash.statPromises')}</Text>
288
+ </View>
289
+
290
+ <View style={localStyles.statChip}>
291
+ <Text style={[localStyles.statChipValue, { color: AppColors.purple }]}>
292
+ {stats.renderCount}
293
+ </Text>
294
+ <Text style={localStyles.statChipLabel}>{t('crash.statRender')}</Text>
295
+ </View>
296
+
297
+ <View style={localStyles.statChip}>
298
+ <Text style={[localStyles.statChipValue, { color: AppColors.skyBlue }]}>
299
+ {stats.nativeCount}
300
+ </Text>
301
+ <Text style={localStyles.statChipLabel}>{t('crash.statNative')}</Text>
302
+ </View>
303
+ </ScrollView>
304
+ </View>
305
+
306
+ {/* ─── Search & Controls ─── */}
307
+ <View style={localStyles.controlsContainer}>
308
+ <View style={localStyles.searchRow}>
309
+ <View style={localStyles.searchInputWrapper}>
310
+ <SearchIcon color={AppColors.grayTextWeak} size={14}/>
311
+ <TextInput value={searchQuery} onChangeText={setSearchQuery} placeholder={t('crash.searchPlaceholder')} placeholderTextColor={AppColors.grayTextWeak} style={localStyles.searchInput} autoCapitalize="none" autoCorrect={false}/>
312
+ {searchQuery.length > 0 && (<TouchableOpacity onPress={() => setSearchQuery('')} hitSlop={8}>
313
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
314
+ </TouchableOpacity>)}
315
+ </View>
316
+
317
+ {/* Crash Filters */}
318
+ {crashRecords.length > 0 && (<TouchableScale onPress={() => setIsFilterModalOpen(true)} style={[
319
+ localStyles.filterButton,
320
+ !isCrashFiltersDefault(crashFilters) && {
321
+ borderColor: `${AppColors.brandPurple}60`,
322
+ backgroundColor: `${AppColors.brandPurple}15`,
323
+ },
324
+ ]}>
325
+ <FilterIcon size={14} color={isCrashFiltersDefault(crashFilters)
326
+ ? AppColors.grayTextStrong
327
+ : AppColors.brandPurple}/>
328
+ {!isCrashFiltersDefault(crashFilters) && (<View style={{
329
+ position: 'absolute',
330
+ top: 2,
331
+ right: 2,
332
+ width: 7,
333
+ height: 7,
334
+ borderRadius: 3.5,
335
+ backgroundColor: AppColors.darkOrange,
336
+ borderWidth: 1,
337
+ borderColor: AppColors.white,
338
+ }}/>)}
339
+ </TouchableScale>)}
340
+
341
+ {/* Clear Crashes */}
342
+ {crashRecords.length > 0 && (<TouchableScale onPress={handleClearAll} style={localStyles.clearButton}>
343
+ <TrashIcon size={14} color={AppColors.errorColor}/>
344
+ </TouchableScale>)}
345
+ </View>
346
+
347
+ {/* ─── Filter Chips ─── */}
348
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={localStyles.filterScrollContent}>
349
+ {[
350
+ { key: 'all', label: t('crash.filterAll'), Icon: LayersIcon },
351
+ { key: 'fatal', label: t('crash.filterFatal'), Icon: SkullIcon },
352
+ { key: 'js', label: t('crash.filterJsError'), Icon: JsIcon },
353
+ { key: 'promise', label: t('crash.filterPromise'), Icon: HourglassIcon },
354
+ { key: 'render', label: t('crash.filterRender'), Icon: LayoutIcon },
355
+ { key: 'native', label: t('crash.filterNative'), Icon: ChipIcon },
356
+ ].map(chip => {
357
+ const isActive = filterType === chip.key;
358
+ return (<TouchableOpacity key={chip.key} onPress={() => setFilterType(chip.key)} style={[
359
+ localStyles.filterChip,
360
+ isActive && localStyles.filterChipActive,
361
+ ]}>
362
+ <chip.Icon size={12} color={isActive ? AppColors.white : AppColors.grayTextStrong}/>
363
+ <Text style={[
364
+ localStyles.filterChipText,
365
+ isActive && localStyles.filterChipTextActive,
366
+ ]}>
367
+ {chip.label}
368
+ </Text>
369
+ </TouchableOpacity>);
370
+ })}
371
+ </ScrollView>
372
+ </View>
373
+
374
+ {/* ─── Crash List or Empty State ─── */}
375
+ {filteredList.length === 0 ? (<ScrollView contentContainerStyle={localStyles.emptyContainer} showsVerticalScrollIndicator={false}>
376
+ <View style={localStyles.emptyIconCircle}>
377
+ <ShieldAlertIcon size={38} color={AppColors.greenColor}/>
378
+ </View>
379
+ <Text style={localStyles.emptyTitle}>{t('crash.emptyTitle')}</Text>
380
+ <Text style={localStyles.emptySub}>
381
+ {searchQuery
382
+ ? t('crash.emptySearchSubtitle')
383
+ : t('crash.emptySubtitle')}
384
+ </Text>
385
+ </ScrollView>) : (<FlatList data={filteredList} keyExtractor={item => item.id} renderItem={renderCard} contentContainerStyle={localStyles.listContent} showsVerticalScrollIndicator={false} ListFooterComponent={<EndOfListFooter />}/>)}
386
+
387
+ {/* Filter Modal */}
388
+ <CrashFilterModal visible={isFilterModalOpen} onClose={() => setIsFilterModalOpen(false)} filters={crashFilters} onApply={setCrashFilters} searchQuery={searchQuery}/>
389
+ </View>);
390
+ });
391
+ export default CrashTab;
392
+ const localStyles = StyleSheet.create({
393
+ container: {
394
+ flex: 1,
395
+ backgroundColor: AppColors.grayBackground,
396
+ },
397
+ statsBar: {
398
+ backgroundColor: AppColors.primaryLight,
399
+ borderBottomWidth: 1,
400
+ borderBottomColor: AppColors.grayBorderSecondary,
401
+ paddingVertical: 10,
402
+ },
403
+ statsScrollContent: {
404
+ paddingHorizontal: 16,
405
+ gap: 8,
406
+ },
407
+ statChip: {
408
+ flexDirection: 'row',
409
+ alignItems: 'center',
410
+ gap: 6,
411
+ paddingHorizontal: 10,
412
+ paddingVertical: 5,
413
+ borderRadius: 8,
414
+ backgroundColor: AppColors.purpleShade50,
415
+ borderWidth: 1,
416
+ borderColor: `${AppColors.purple}14`,
417
+ },
418
+ statChipActive: {
419
+ backgroundColor: `${AppColors.purple}14`,
420
+ borderColor: `${AppColors.purple}33`,
421
+ },
422
+ statChipValue: {
423
+ fontFamily: AppFonts.interBold,
424
+ fontSize: 13,
425
+ color: AppColors.primaryBlack,
426
+ },
427
+ statChipLabel: {
428
+ fontFamily: AppFonts.interMedium,
429
+ fontSize: 11,
430
+ color: AppColors.grayText,
431
+ },
432
+ controlsContainer: {
433
+ paddingHorizontal: 16,
434
+ paddingTop: 12,
435
+ paddingBottom: 6,
436
+ backgroundColor: AppColors.grayBackground,
437
+ },
438
+ searchRow: {
439
+ flexDirection: 'row',
440
+ alignItems: 'center',
441
+ gap: 8,
442
+ },
443
+ searchInputWrapper: {
444
+ flex: 1,
445
+ flexDirection: 'row',
446
+ alignItems: 'center',
447
+ backgroundColor: AppColors.primaryLight,
448
+ borderWidth: 1,
449
+ borderColor: AppColors.grayBorderSecondary,
450
+ borderRadius: 10,
451
+ paddingHorizontal: 10,
452
+ height: 38,
453
+ gap: 8,
454
+ },
455
+ searchInput: {
456
+ flex: 1,
457
+ fontFamily: AppFonts.interRegular,
458
+ fontSize: 13,
459
+ color: AppColors.primaryBlack,
460
+ paddingVertical: 0,
461
+ },
462
+ clearButton: {
463
+ width: 38,
464
+ height: 38,
465
+ borderRadius: 10,
466
+ backgroundColor: `${AppColors.errorColor}14`,
467
+ borderWidth: 1,
468
+ borderColor: `${AppColors.errorColor}22`,
469
+ alignItems: 'center',
470
+ justifyContent: 'center',
471
+ },
472
+ filterButton: {
473
+ width: 38,
474
+ height: 38,
475
+ borderRadius: 10,
476
+ backgroundColor: `${AppColors.brandPurple}0F`,
477
+ borderWidth: 1,
478
+ borderColor: `${AppColors.brandPurple}30`,
479
+ alignItems: 'center',
480
+ justifyContent: 'center',
481
+ },
482
+ filterScrollContent: {
483
+ paddingVertical: 10,
484
+ gap: 6,
485
+ },
486
+ filterChip: {
487
+ flexDirection: 'row',
488
+ alignItems: 'center',
489
+ gap: 5,
490
+ paddingHorizontal: 12,
491
+ paddingVertical: 6,
492
+ borderRadius: 8,
493
+ backgroundColor: AppColors.primaryLight,
494
+ borderWidth: 1,
495
+ borderColor: AppColors.grayBorderSecondary,
496
+ },
497
+ filterChipActive: {
498
+ backgroundColor: AppColors.purple,
499
+ borderColor: AppColors.purple,
500
+ },
501
+ filterChipText: {
502
+ fontFamily: AppFonts.interMedium,
503
+ fontSize: 12,
504
+ color: AppColors.grayText,
505
+ },
506
+ filterChipTextActive: {
507
+ fontFamily: AppFonts.interBold,
508
+ color: AppColors.white,
509
+ },
510
+ listContent: {
511
+ paddingHorizontal: 16,
512
+ paddingTop: 4,
513
+ paddingBottom: 24,
514
+ gap: 10,
515
+ },
516
+ cardContainer: {
517
+ backgroundColor: AppColors.primaryLight,
518
+ borderRadius: 12,
519
+ borderWidth: 1,
520
+ borderColor: AppColors.grayBorderSecondary,
521
+ padding: 14,
522
+ shadowColor: AppColors.shadowColorString,
523
+ shadowOffset: { width: 0, height: 1 },
524
+ shadowOpacity: 0.05,
525
+ shadowRadius: 3,
526
+ elevation: 2,
527
+ },
528
+ fatalCardBorder: {
529
+ borderColor: `${AppColors.errorColor}44`,
530
+ },
531
+ cardHeader: {
532
+ flexDirection: 'row',
533
+ alignItems: 'center',
534
+ justifyContent: 'space-between',
535
+ },
536
+ badgeRow: {
537
+ flexDirection: 'row',
538
+ alignItems: 'center',
539
+ gap: 6,
540
+ flexShrink: 1,
541
+ },
542
+ sNoBadge: {
543
+ backgroundColor: AppColors.grayBackground,
544
+ borderWidth: 1,
545
+ borderColor: AppColors.grayBorderSecondary,
546
+ paddingHorizontal: 6,
547
+ paddingVertical: 3,
548
+ borderRadius: 5,
549
+ },
550
+ sNoBadgeText: {
551
+ fontFamily: AppFonts.interBold,
552
+ fontSize: 9.5,
553
+ color: AppColors.grayText,
554
+ letterSpacing: 0.3,
555
+ },
556
+ fatalBadge: {
557
+ flexDirection: 'row',
558
+ alignItems: 'center',
559
+ gap: 4,
560
+ backgroundColor: AppColors.errorColor,
561
+ paddingHorizontal: 6,
562
+ paddingVertical: 3,
563
+ borderRadius: 5,
564
+ },
565
+ fatalBadgeText: {
566
+ fontFamily: AppFonts.interBold,
567
+ fontSize: 9.5,
568
+ color: AppColors.white,
569
+ letterSpacing: 0.5,
570
+ },
571
+ caughtBadge: {
572
+ backgroundColor: `${AppColors.greenColor}1F`,
573
+ paddingHorizontal: 6,
574
+ paddingVertical: 3,
575
+ borderRadius: 5,
576
+ },
577
+ caughtBadgeText: {
578
+ fontFamily: AppFonts.interBold,
579
+ fontSize: 9.5,
580
+ color: AppColors.greenColor,
581
+ letterSpacing: 0.5,
582
+ },
583
+ typeBadge: {
584
+ paddingHorizontal: 6,
585
+ paddingVertical: 3,
586
+ borderRadius: 5,
587
+ },
588
+ typeBadgeText: {
589
+ fontFamily: AppFonts.interBold,
590
+ fontSize: 9.5,
591
+ letterSpacing: 0.5,
592
+ },
593
+ timeRow: {
594
+ flexDirection: 'row',
595
+ alignItems: 'center',
596
+ gap: 4,
597
+ flexShrink: 1,
598
+ },
599
+ timestampText: {
600
+ fontFamily: AppFonts.interRegular,
601
+ fontSize: 11,
602
+ color: AppColors.grayTextWeak,
603
+ },
604
+ relativeTimeText: {
605
+ fontFamily: AppFonts.interBold,
606
+ fontSize: 10,
607
+ color: AppColors.grayTextWeak,
608
+ backgroundColor: AppColors.grayBackground,
609
+ paddingHorizontal: 5,
610
+ paddingVertical: 1,
611
+ borderRadius: 4,
612
+ overflow: 'hidden',
613
+ },
614
+ errorMessageText: {
615
+ fontFamily: AppFonts.interBold,
616
+ fontSize: 13.5,
617
+ color: AppColors.primaryBlack,
618
+ lineHeight: 18,
619
+ },
620
+ locationRow: {
621
+ flexDirection: 'row',
622
+ alignItems: 'center',
623
+ gap: 6,
624
+ marginTop: 2,
625
+ marginBottom: 6,
626
+ backgroundColor: AppColors.purpleShade50,
627
+ paddingHorizontal: 8,
628
+ paddingVertical: 4,
629
+ borderRadius: 6,
630
+ },
631
+ locationText: {
632
+ fontFamily: AppFonts.interRegular,
633
+ fontSize: 11,
634
+ color: AppColors.grayText,
635
+ flex: 1,
636
+ },
637
+ cardFooter: {
638
+ flexDirection: 'row',
639
+ alignItems: 'center',
640
+ justifyContent: 'space-between',
641
+ marginTop: 6,
642
+ paddingTop: 8,
643
+ borderTopWidth: 1,
644
+ borderTopColor: AppColors.dividerColor,
645
+ },
646
+ metaPill: {
647
+ flexDirection: 'row',
648
+ alignItems: 'center',
649
+ gap: 4,
650
+ backgroundColor: AppColors.purpleShade50,
651
+ paddingHorizontal: 6,
652
+ paddingVertical: 2,
653
+ borderRadius: 4,
654
+ },
655
+ metaPillText: {
656
+ fontFamily: AppFonts.interMedium,
657
+ fontSize: 10,
658
+ color: AppColors.grayTextWeak,
659
+ },
660
+ emptyContainer: {
661
+ alignItems: 'center',
662
+ justifyContent: 'center',
663
+ padding: 32,
664
+ marginTop: 40,
665
+ },
666
+ emptyIconCircle: {
667
+ width: 72,
668
+ height: 72,
669
+ borderRadius: 36,
670
+ backgroundColor: `${AppColors.greenColor}1F`,
671
+ alignItems: 'center',
672
+ justifyContent: 'center',
673
+ marginBottom: 16,
674
+ },
675
+ emptyTitle: {
676
+ fontFamily: AppFonts.interBold,
677
+ fontSize: 17,
678
+ color: AppColors.primaryBlack,
679
+ marginBottom: 6,
680
+ },
681
+ emptySub: {
682
+ fontFamily: AppFonts.interRegular,
683
+ fontSize: 13,
684
+ color: AppColors.grayText,
685
+ textAlign: 'center',
686
+ lineHeight: 18,
687
+ maxWidth: 280,
688
+ },
689
+ });
@@ -1,4 +1,5 @@
1
1
  import { createContext, useContext } from 'react';
2
+ export { ModalAnimationType, SettingsPage, SettingsSubTab, LogFilter, } from '../../types';
2
3
  export const InspectorContext = createContext(null);
3
4
  export const useInspector = () => {
4
5
  const ctx = useContext(InspectorContext);