react-native-inapp-inspector 1.1.25 → 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 (70) 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/ErrorBoundary.d.ts +1 -1
  6. package/dist/commonjs/components/ErrorBoundary.js +9 -145
  7. package/dist/commonjs/components/Inspector/CrashDetail.d.ts +3 -0
  8. package/dist/commonjs/components/Inspector/CrashDetail.js +796 -0
  9. package/dist/commonjs/components/Inspector/CrashFilterModal.d.ts +20 -0
  10. package/dist/commonjs/components/Inspector/CrashFilterModal.js +597 -0
  11. package/dist/commonjs/components/Inspector/CrashTab.d.ts +3 -0
  12. package/dist/commonjs/components/Inspector/CrashTab.js +727 -0
  13. package/dist/commonjs/components/Inspector/InspectorHeader.js +52 -23
  14. package/dist/commonjs/components/Inspector/LogDetail.js +4 -3
  15. package/dist/commonjs/components/Inspector/MainScreen.js +12 -3
  16. package/dist/commonjs/components/Inspector/SettingsPanel.js +90 -1
  17. package/dist/commonjs/components/Inspector/TabBar.js +15 -3
  18. package/dist/commonjs/components/NetworkIcons.d.ts +11 -0
  19. package/dist/commonjs/components/NetworkIcons.js +96 -1
  20. package/dist/commonjs/components/SegmentedTabs.js +2 -1
  21. package/dist/commonjs/constants/version.d.ts +1 -1
  22. package/dist/commonjs/constants/version.js +1 -1
  23. package/dist/commonjs/customHooks/analyticsLogger.js +70 -44
  24. package/dist/commonjs/customHooks/consoleLogger.d.ts +1 -0
  25. package/dist/commonjs/customHooks/consoleLogger.js +70 -6
  26. package/dist/commonjs/customHooks/crashHandler.d.ts +64 -0
  27. package/dist/commonjs/customHooks/crashHandler.js +681 -0
  28. package/dist/commonjs/customHooks/networkLogger.js +64 -48
  29. package/dist/commonjs/helpers/gaAnalyticsRegistry.js +1 -1
  30. package/dist/commonjs/i18n/locales/en.json +131 -0
  31. package/dist/commonjs/index.d.ts +3 -1
  32. package/dist/commonjs/index.js +95 -1
  33. package/dist/commonjs/types/enums.d.ts +41 -0
  34. package/dist/commonjs/types/enums.js +37 -1
  35. package/dist/commonjs/types/interfaces.d.ts +51 -1
  36. package/dist/esm/components/ErrorBoundary.d.ts +1 -1
  37. package/dist/esm/components/ErrorBoundary.js +10 -113
  38. package/dist/esm/components/Inspector/CrashDetail.d.ts +3 -0
  39. package/dist/esm/components/Inspector/CrashDetail.js +758 -0
  40. package/dist/esm/components/Inspector/CrashFilterModal.d.ts +20 -0
  41. package/dist/esm/components/Inspector/CrashFilterModal.js +557 -0
  42. package/dist/esm/components/Inspector/CrashTab.d.ts +3 -0
  43. package/dist/esm/components/Inspector/CrashTab.js +689 -0
  44. package/dist/esm/components/Inspector/InspectorHeader.js +52 -23
  45. package/dist/esm/components/Inspector/LogDetail.js +4 -3
  46. package/dist/esm/components/Inspector/MainScreen.js +12 -3
  47. package/dist/esm/components/Inspector/SettingsPanel.js +91 -2
  48. package/dist/esm/components/Inspector/TabBar.js +16 -4
  49. package/dist/esm/components/NetworkIcons.d.ts +11 -0
  50. package/dist/esm/components/NetworkIcons.js +83 -0
  51. package/dist/esm/components/SegmentedTabs.js +2 -1
  52. package/dist/esm/constants/version.d.ts +1 -1
  53. package/dist/esm/constants/version.js +1 -1
  54. package/dist/esm/customHooks/analyticsLogger.js +70 -44
  55. package/dist/esm/customHooks/consoleLogger.d.ts +1 -0
  56. package/dist/esm/customHooks/consoleLogger.js +68 -5
  57. package/dist/esm/customHooks/crashHandler.d.ts +64 -0
  58. package/dist/esm/customHooks/crashHandler.js +659 -0
  59. package/dist/esm/customHooks/networkLogger.js +64 -48
  60. package/dist/esm/helpers/gaAnalyticsRegistry.js +1 -1
  61. package/dist/esm/i18n/locales/en.json +131 -0
  62. package/dist/esm/index.d.ts +3 -1
  63. package/dist/esm/index.js +73 -1
  64. package/dist/esm/types/enums.d.ts +41 -0
  65. package/dist/esm/types/enums.js +36 -0
  66. package/dist/esm/types/interfaces.d.ts +51 -1
  67. package/ios/NetworkInspectorModule.h +6 -0
  68. package/ios/NetworkInspectorModule.m +125 -0
  69. package/package.json +5 -2
  70. 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
+ });