react-native-inapp-inspector 2.2.2 → 2.2.3

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 (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,721 @@
1
+ import React, {useEffect, useMemo, useState} from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import {useInspector} from './InspectorContext';
12
+ import {CrashFilterType} from '../../types/enums';
13
+ import {AppColors} from '../../styles/AppColors';
14
+ import {AppFonts} from '../../styles/AppFonts';
15
+ import {useTranslation} from '../../i18n';
16
+ import {
17
+ FilterIcon,
18
+ CloseWhite,
19
+ CheckIcon,
20
+ ClockIcon,
21
+ HourglassIcon,
22
+ SkullIcon,
23
+ JsIcon,
24
+ LayoutIcon,
25
+ ChipIcon,
26
+ AppleIcon,
27
+ AndroidIcon,
28
+ BoltIcon,
29
+ } from '../NetworkIcons';
30
+ import TouchableScale from '../TouchableScale';
31
+
32
+ export interface CrashFilters {
33
+ types: Set<CrashFilterType>;
34
+ timeWindow: 'all' | '15m' | '1h' | '24h' | '7d';
35
+ platform: 'all' | 'ios' | 'android';
36
+ engine: 'all' | 'hermes' | 'jsc';
37
+ sortBy: 'time_desc' | 'time_asc';
38
+ }
39
+
40
+ export const DEFAULT_CRASH_FILTERS: CrashFilters = {
41
+ types: new Set(['all']),
42
+ timeWindow: 'all',
43
+ platform: 'all',
44
+ engine: 'all',
45
+ sortBy: 'time_desc',
46
+ };
47
+
48
+ export const isCrashFiltersDefault = (f: CrashFilters): boolean =>
49
+ (f.types.has('all') || f.types.size === 0) &&
50
+ f.timeWindow === 'all' &&
51
+ f.platform === 'all' &&
52
+ f.engine === 'all' &&
53
+ f.sortBy === 'time_desc';
54
+
55
+ interface CrashFilterModalProps {
56
+ visible: boolean;
57
+ onClose: () => void;
58
+ filters: CrashFilters;
59
+ onApply: (filters: CrashFilters) => void;
60
+ searchQuery?: string;
61
+ }
62
+
63
+ const CrashFilterModal: React.FC<CrashFilterModalProps> = ({
64
+ visible,
65
+ onClose,
66
+ filters,
67
+ onApply,
68
+ searchQuery = '',
69
+ }) => {
70
+ const {t} = useTranslation();
71
+ const {crashRecords} = useInspector();
72
+
73
+ const [draft, setDraft] = useState<CrashFilters>(() => ({
74
+ ...filters,
75
+ types: new Set(filters.types),
76
+ }));
77
+
78
+ useEffect(() => {
79
+ if (visible) {
80
+ setDraft({
81
+ ...filters,
82
+ types: new Set(filters.types),
83
+ });
84
+ }
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ }, [visible]);
87
+
88
+ const toggleType = (key: CrashFilterType) => {
89
+ setDraft(prev => {
90
+ const next = new Set(prev.types);
91
+ if (key === 'all') {
92
+ return {...prev, types: new Set(['all'])};
93
+ }
94
+ next.delete('all');
95
+ if (next.has(key)) {
96
+ next.delete(key);
97
+ if (next.size === 0) next.add('all');
98
+ } else {
99
+ next.add(key);
100
+ }
101
+ return {...prev, types: next};
102
+ });
103
+ };
104
+
105
+ const setTimeWindow = (tw: CrashFilters['timeWindow']) => {
106
+ setDraft(prev => ({...prev, timeWindow: tw}));
107
+ };
108
+
109
+ const setPlatform = (p: CrashFilters['platform']) => {
110
+ setDraft(prev => ({...prev, platform: p}));
111
+ };
112
+
113
+ const setEngine = (e: CrashFilters['engine']) => {
114
+ setDraft(prev => ({...prev, engine: e}));
115
+ };
116
+
117
+ const setSort = (sortBy: CrashFilters['sortBy']) => {
118
+ setDraft(prev => ({...prev, sortBy}));
119
+ };
120
+
121
+ const handleResetDraft = () => {
122
+ setDraft({
123
+ ...DEFAULT_CRASH_FILTERS,
124
+ types: new Set(DEFAULT_CRASH_FILTERS.types),
125
+ });
126
+ };
127
+
128
+ const handleApply = () => {
129
+ onApply({
130
+ ...draft,
131
+ types: new Set(draft.types),
132
+ });
133
+ onClose();
134
+ };
135
+
136
+ const handleDiscard = () => {
137
+ setDraft({
138
+ ...filters,
139
+ types: new Set(filters.types),
140
+ });
141
+ onClose();
142
+ };
143
+
144
+ const draftMatchCount = useMemo(() => {
145
+ let crashes = crashRecords;
146
+
147
+ if (searchQuery.trim().length > 0) {
148
+ const q = searchQuery.toLowerCase();
149
+ crashes = crashes.filter(
150
+ c =>
151
+ (c.message || '').toLowerCase().includes(q) ||
152
+ (c.name || '').toLowerCase().includes(q) ||
153
+ (c.stack || '').toLowerCase().includes(q),
154
+ );
155
+ }
156
+
157
+ if (draft.types.size > 0 && !draft.types.has('all')) {
158
+ crashes = crashes.filter(c => {
159
+ if (draft.types.has('fatal') && c.isFatal) return true;
160
+ if (draft.types.has('js') && c.type === 'js') return true;
161
+ if (draft.types.has('promise') && c.type === 'promise') return true;
162
+ if (draft.types.has('render') && c.type === 'render') return true;
163
+ if (draft.types.has('native') && c.type === 'native') return true;
164
+ return false;
165
+ });
166
+ }
167
+
168
+ if (draft.timeWindow !== 'all') {
169
+ const now = Date.now();
170
+ const cutoff =
171
+ draft.timeWindow === '15m'
172
+ ? now - 15 * 60 * 1000
173
+ : draft.timeWindow === '1h'
174
+ ? now - 60 * 60 * 1000
175
+ : draft.timeWindow === '24h'
176
+ ? now - 24 * 60 * 60 * 1000
177
+ : now - 7 * 24 * 60 * 60 * 1000;
178
+ crashes = crashes.filter(c => c.timestamp >= cutoff);
179
+ }
180
+
181
+ if (draft.platform !== 'all') {
182
+ crashes = crashes.filter(
183
+ c =>
184
+ (c.deviceInfo?.platform || '').toLowerCase() === draft.platform,
185
+ );
186
+ }
187
+
188
+ if (draft.engine !== 'all') {
189
+ crashes = crashes.filter(c => {
190
+ const isHermes = c.deviceInfo?.isHermes;
191
+ if (draft.engine === 'hermes') return isHermes === true;
192
+ return isHermes === false || isHermes === undefined;
193
+ });
194
+ }
195
+
196
+ return crashes.length;
197
+ }, [crashRecords, searchQuery, draft]);
198
+
199
+ const isAllTypes = draft.types.has('all') || draft.types.size === 0;
200
+
201
+ const typeChips: Array<{
202
+ key: CrashFilterType;
203
+ label: string;
204
+ Icon: React.ComponentType<{size?: number; color?: string}>;
205
+ color: string;
206
+ }> = [
207
+ {key: 'all', label: t('crash.filterTypeAll'), Icon: FilterIcon, color: AppColors.grayTextStrong},
208
+ {key: 'fatal', label: t('crash.filterFatal'), Icon: SkullIcon, color: AppColors.errorColor},
209
+ {key: 'js', label: t('crash.filterJsError'), Icon: JsIcon, color: AppColors.sky600},
210
+ {key: 'promise', label: t('crash.filterPromise'), Icon: HourglassIcon, color: AppColors.darkOrange},
211
+ {key: 'render', label: t('crash.filterRender'), Icon: LayoutIcon, color: AppColors.purple},
212
+ {key: 'native', label: t('crash.filterNative'), Icon: ChipIcon, color: AppColors.amber700},
213
+ ];
214
+
215
+ return (
216
+ <Modal
217
+ visible={visible}
218
+ transparent
219
+ animationType="fade"
220
+ onRequestClose={handleDiscard}>
221
+ <View style={styles.backdrop}>
222
+ <Pressable style={StyleSheet.absoluteFill} onPress={handleDiscard} />
223
+
224
+ <View style={styles.modalCard}>
225
+ <View style={styles.header}>
226
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
227
+ <View style={styles.headerIconBox}>
228
+ <FilterIcon color={AppColors.brandPurple} size={15} />
229
+ </View>
230
+ <View>
231
+ <Text style={styles.headerTitle}>{t('crash.filterTitle')}</Text>
232
+ <Text style={styles.headerSubtitle}>
233
+ {t('crash.filterSubtitle')}
234
+ </Text>
235
+ </View>
236
+ </View>
237
+
238
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
239
+ <TouchableOpacity
240
+ onPress={handleResetDraft}
241
+ hitSlop={8}
242
+ style={styles.resetBtn}>
243
+ <Text style={styles.resetBtnText}>{t('crash.filterReset')}</Text>
244
+ </TouchableOpacity>
245
+
246
+ <TouchableOpacity
247
+ onPress={handleDiscard}
248
+ hitSlop={10}
249
+ style={styles.closeBtn}>
250
+ <CloseWhite />
251
+ </TouchableOpacity>
252
+ </View>
253
+ </View>
254
+
255
+ <ScrollView
256
+ style={{maxHeight: 460}}
257
+ contentContainerStyle={styles.scrollContent}
258
+ showsVerticalScrollIndicator={true}>
259
+ <View style={styles.section}>
260
+ <View style={styles.sectionHeaderRow}>
261
+ <SkullIcon color={AppColors.grayTextWeak} size={12} />
262
+ <Text style={styles.sectionTitle}>
263
+ {t('crash.filterTypeSection')}
264
+ </Text>
265
+ </View>
266
+ <View style={styles.chipsWrap}>
267
+ {typeChips.map(chip => {
268
+ const isSelected =
269
+ chip.key === 'all' ? isAllTypes : draft.types.has(chip.key);
270
+ return (
271
+ <TouchableScale
272
+ key={chip.key}
273
+ onPress={() => toggleType(chip.key)}
274
+ style={[
275
+ styles.filterPill,
276
+ isSelected && styles.filterPillActive,
277
+ ]}>
278
+ {chip.key === 'all' && isSelected && (
279
+ <CheckIcon color={AppColors.white} size={12} />
280
+ )}
281
+ <chip.Icon
282
+ size={12}
283
+ color={
284
+ isSelected ? AppColors.white : chip.color
285
+ }
286
+ />
287
+ <Text
288
+ style={[
289
+ styles.filterPillText,
290
+ isSelected && styles.filterPillTextActive,
291
+ ]}>
292
+ {chip.label}
293
+ </Text>
294
+ </TouchableScale>
295
+ );
296
+ })}
297
+ </View>
298
+ </View>
299
+
300
+ <View style={styles.section}>
301
+ <View style={styles.sectionHeaderRow}>
302
+ <ClockIcon color={AppColors.grayTextWeak} size={12} />
303
+ <Text style={styles.sectionTitle}>
304
+ {t('crash.filterTimeSection')}
305
+ </Text>
306
+ </View>
307
+ <View style={styles.chipsWrap}>
308
+ {[
309
+ {key: 'all' as const, label: t('crash.filterTimeAll')},
310
+ {key: '15m' as const, label: t('crash.filterTime15m')},
311
+ {key: '1h' as const, label: t('crash.filterTime1h')},
312
+ {key: '24h' as const, label: t('crash.filterTime24h')},
313
+ {key: '7d' as const, label: t('crash.filterTime7d')},
314
+ ].map(item => (
315
+ <TouchableScale
316
+ key={item.key}
317
+ onPress={() => setTimeWindow(item.key)}
318
+ style={[
319
+ styles.filterPill,
320
+ draft.timeWindow === item.key && styles.filterPillActive,
321
+ ]}>
322
+ <Text
323
+ style={[
324
+ styles.filterPillText,
325
+ draft.timeWindow === item.key &&
326
+ styles.filterPillTextActive,
327
+ ]}>
328
+ {item.label}
329
+ </Text>
330
+ </TouchableScale>
331
+ ))}
332
+ </View>
333
+ </View>
334
+
335
+ <View style={styles.section}>
336
+ <View style={styles.sectionHeaderRow}>
337
+ <AppleIcon color={AppColors.grayTextWeak} size={12} />
338
+ <Text style={styles.sectionTitle}>
339
+ {t('crash.filterPlatformSection')}
340
+ </Text>
341
+ </View>
342
+ <View style={styles.chipsWrap}>
343
+ <TouchableScale
344
+ onPress={() => setPlatform('all')}
345
+ style={[
346
+ styles.filterPill,
347
+ draft.platform === 'all' && styles.filterPillActive,
348
+ ]}>
349
+ <Text
350
+ style={[
351
+ styles.filterPillText,
352
+ draft.platform === 'all' && styles.filterPillTextActive,
353
+ ]}>
354
+ {t('crash.filterPlatformAll')}
355
+ </Text>
356
+ </TouchableScale>
357
+
358
+ <TouchableScale
359
+ onPress={() => setPlatform('ios')}
360
+ style={[
361
+ styles.filterPill,
362
+ draft.platform === 'ios' && styles.filterPillActive,
363
+ ]}>
364
+ <AppleIcon
365
+ size={12}
366
+ color={
367
+ draft.platform === 'ios'
368
+ ? AppColors.white
369
+ : AppColors.grayTextStrong
370
+ }
371
+ />
372
+ <Text
373
+ style={[
374
+ styles.filterPillText,
375
+ draft.platform === 'ios' && styles.filterPillTextActive,
376
+ ]}>
377
+ {t('crash.filterPlatformIos')}
378
+ </Text>
379
+ </TouchableScale>
380
+
381
+ <TouchableScale
382
+ onPress={() => setPlatform('android')}
383
+ style={[
384
+ styles.filterPill,
385
+ draft.platform === 'android' && styles.filterPillActive,
386
+ ]}>
387
+ <AndroidIcon
388
+ size={12}
389
+ color={
390
+ draft.platform === 'android'
391
+ ? AppColors.white
392
+ : AppColors.greenColor
393
+ }
394
+ />
395
+ <Text
396
+ style={[
397
+ styles.filterPillText,
398
+ draft.platform === 'android' && styles.filterPillTextActive,
399
+ ]}>
400
+ {t('crash.filterPlatformAndroid')}
401
+ </Text>
402
+ </TouchableScale>
403
+ </View>
404
+ </View>
405
+
406
+ <View style={styles.section}>
407
+ <View style={styles.sectionHeaderRow}>
408
+ <BoltIcon color={AppColors.grayTextWeak} size={12} />
409
+ <Text style={styles.sectionTitle}>
410
+ {t('crash.filterEngineSection')}
411
+ </Text>
412
+ </View>
413
+ <View style={styles.chipsWrap}>
414
+ <TouchableScale
415
+ onPress={() => setEngine('all')}
416
+ style={[
417
+ styles.filterPill,
418
+ draft.engine === 'all' && styles.filterPillActive,
419
+ ]}>
420
+ <Text
421
+ style={[
422
+ styles.filterPillText,
423
+ draft.engine === 'all' && styles.filterPillTextActive,
424
+ ]}>
425
+ {t('crash.filterEngineAll')}
426
+ </Text>
427
+ </TouchableScale>
428
+
429
+ <TouchableScale
430
+ onPress={() => setEngine('hermes')}
431
+ style={[
432
+ styles.filterPill,
433
+ draft.engine === 'hermes' && styles.filterPillActive,
434
+ ]}>
435
+ <BoltIcon
436
+ size={12}
437
+ color={
438
+ draft.engine === 'hermes'
439
+ ? AppColors.white
440
+ : AppColors.brandPurple
441
+ }
442
+ />
443
+ <Text
444
+ style={[
445
+ styles.filterPillText,
446
+ draft.engine === 'hermes' && styles.filterPillTextActive,
447
+ ]}>
448
+ {t('crash.filterEngineHermes')}
449
+ </Text>
450
+ </TouchableScale>
451
+
452
+ <TouchableScale
453
+ onPress={() => setEngine('jsc')}
454
+ style={[
455
+ styles.filterPill,
456
+ draft.engine === 'jsc' && styles.filterPillActive,
457
+ ]}>
458
+ <Text
459
+ style={[
460
+ styles.filterPillText,
461
+ draft.engine === 'jsc' && styles.filterPillTextActive,
462
+ ]}>
463
+ {t('crash.jsc')}
464
+ </Text>
465
+ </TouchableScale>
466
+ </View>
467
+ </View>
468
+
469
+ <View style={styles.section}>
470
+ <View style={styles.sectionHeaderRow}>
471
+ <ClockIcon color={AppColors.grayTextWeak} size={12} />
472
+ <Text style={styles.sectionTitle}>
473
+ {t('crash.filterSortSection')}
474
+ </Text>
475
+ </View>
476
+ <View style={styles.chipsWrap}>
477
+ <TouchableScale
478
+ onPress={() => setSort('time_desc')}
479
+ style={[
480
+ styles.filterPill,
481
+ draft.sortBy === 'time_desc' && styles.filterPillActive,
482
+ ]}>
483
+ <ClockIcon
484
+ size={12}
485
+ color={
486
+ draft.sortBy === 'time_desc'
487
+ ? AppColors.white
488
+ : AppColors.grayText
489
+ }
490
+ />
491
+ <Text
492
+ style={[
493
+ styles.filterPillText,
494
+ draft.sortBy === 'time_desc' &&
495
+ styles.filterPillTextActive,
496
+ ]}>
497
+ {t('crash.filterSortNewest')}
498
+ </Text>
499
+ </TouchableScale>
500
+
501
+ <TouchableScale
502
+ onPress={() => setSort('time_asc')}
503
+ style={[
504
+ styles.filterPill,
505
+ draft.sortBy === 'time_asc' && styles.filterPillActive,
506
+ ]}>
507
+ <HourglassIcon
508
+ size={12}
509
+ color={
510
+ draft.sortBy === 'time_asc'
511
+ ? AppColors.white
512
+ : AppColors.grayText
513
+ }
514
+ />
515
+ <Text
516
+ style={[
517
+ styles.filterPillText,
518
+ draft.sortBy === 'time_asc' &&
519
+ styles.filterPillTextActive,
520
+ ]}>
521
+ {t('crash.filterSortOldest')}
522
+ </Text>
523
+ </TouchableScale>
524
+ </View>
525
+ </View>
526
+ </ScrollView>
527
+
528
+ <View style={styles.footer}>
529
+ <TouchableOpacity
530
+ onPress={handleDiscard}
531
+ style={styles.discardBtn}
532
+ activeOpacity={0.8}>
533
+ <Text style={styles.discardBtnText}>
534
+ {t('crash.filterDiscard')}
535
+ </Text>
536
+ </TouchableOpacity>
537
+
538
+ <TouchableOpacity
539
+ onPress={handleApply}
540
+ style={styles.applyBtn}
541
+ activeOpacity={0.85}>
542
+ <Text style={styles.applyBtnText}>
543
+ {t('crash.filterApply', {count: draftMatchCount})}
544
+ </Text>
545
+ </TouchableOpacity>
546
+ </View>
547
+ </View>
548
+ </View>
549
+ </Modal>
550
+ );
551
+ };
552
+
553
+ const styles = StyleSheet.create({
554
+ backdrop: {
555
+ flex: 1,
556
+ backgroundColor: `${AppColors.primaryBlack}85`,
557
+ justifyContent: 'center',
558
+ alignItems: 'center',
559
+ padding: 16,
560
+ },
561
+ modalCard: {
562
+ width: '100%',
563
+ maxWidth: 440,
564
+ backgroundColor: AppColors.white,
565
+ borderRadius: 14,
566
+ borderWidth: 1,
567
+ borderColor: AppColors.dividerColor,
568
+ shadowColor: AppColors.black,
569
+ shadowOpacity: 0.18,
570
+ shadowRadius: 12,
571
+ shadowOffset: {width: 0, height: 4},
572
+ elevation: 8,
573
+ overflow: 'hidden',
574
+ },
575
+ header: {
576
+ flexDirection: 'row',
577
+ alignItems: 'center',
578
+ justifyContent: 'space-between',
579
+ paddingHorizontal: 14,
580
+ paddingVertical: 12,
581
+ borderBottomWidth: 1,
582
+ borderBottomColor: AppColors.dividerColor,
583
+ backgroundColor: `${AppColors.brandPurple}08`,
584
+ },
585
+ headerIconBox: {
586
+ width: 28,
587
+ height: 28,
588
+ borderRadius: 7,
589
+ backgroundColor: `${AppColors.brandPurple}18`,
590
+ alignItems: 'center',
591
+ justifyContent: 'center',
592
+ },
593
+ headerTitle: {
594
+ fontFamily: AppFonts.interBold,
595
+ fontSize: 13.5,
596
+ color: AppColors.primaryBlack,
597
+ },
598
+ headerSubtitle: {
599
+ fontFamily: AppFonts.interMedium,
600
+ fontSize: 10,
601
+ color: AppColors.grayTextWeak,
602
+ },
603
+ resetBtn: {
604
+ paddingHorizontal: 8,
605
+ paddingVertical: 4,
606
+ borderRadius: 5,
607
+ backgroundColor: `${AppColors.darkOrange}14`,
608
+ borderWidth: 1,
609
+ borderColor: `${AppColors.darkOrange}30`,
610
+ },
611
+ resetBtnText: {
612
+ fontFamily: AppFonts.interBold,
613
+ fontSize: 10,
614
+ color: AppColors.darkOrange,
615
+ },
616
+ closeBtn: {
617
+ width: 24,
618
+ height: 24,
619
+ borderRadius: 12,
620
+ backgroundColor: `${AppColors.primaryBlack}18`,
621
+ alignItems: 'center',
622
+ justifyContent: 'center',
623
+ },
624
+ scrollContent: {
625
+ padding: 14,
626
+ gap: 14,
627
+ },
628
+ section: {
629
+ gap: 7,
630
+ },
631
+ sectionHeaderRow: {
632
+ flexDirection: 'row',
633
+ alignItems: 'center',
634
+ gap: 5,
635
+ },
636
+ sectionTitle: {
637
+ fontFamily: AppFonts.interBold,
638
+ fontSize: 9.5,
639
+ color: AppColors.grayTextWeak,
640
+ letterSpacing: 0.5,
641
+ },
642
+ chipsWrap: {
643
+ flexDirection: 'row',
644
+ flexWrap: 'wrap',
645
+ gap: 6.5,
646
+ },
647
+ filterPill: {
648
+ flexDirection: 'row',
649
+ alignItems: 'center',
650
+ gap: 5,
651
+ paddingHorizontal: 9,
652
+ paddingVertical: 5.5,
653
+ borderRadius: 7,
654
+ backgroundColor: AppColors.grayBackground,
655
+ borderWidth: 1,
656
+ borderColor: AppColors.dividerColor,
657
+ },
658
+ filterPillActive: {
659
+ backgroundColor: AppColors.brandPurple,
660
+ borderColor: AppColors.brandPurple,
661
+ shadowColor: AppColors.brandPurple,
662
+ shadowOffset: {width: 0, height: 1},
663
+ shadowOpacity: 0.25,
664
+ shadowRadius: 3,
665
+ elevation: 2,
666
+ },
667
+ filterPillText: {
668
+ fontFamily: AppFonts.interMedium,
669
+ fontSize: 11,
670
+ color: AppColors.primaryBlack,
671
+ },
672
+ filterPillTextActive: {
673
+ color: AppColors.white,
674
+ fontFamily: AppFonts.interBold,
675
+ },
676
+ footer: {
677
+ flexDirection: 'row',
678
+ alignItems: 'center',
679
+ gap: 10,
680
+ paddingHorizontal: 14,
681
+ paddingVertical: 12,
682
+ borderTopWidth: 1,
683
+ borderTopColor: AppColors.dividerColor,
684
+ backgroundColor: AppColors.grayBackground,
685
+ },
686
+ discardBtn: {
687
+ flex: 1,
688
+ paddingVertical: 9.5,
689
+ borderRadius: 8,
690
+ borderWidth: 1,
691
+ borderColor: AppColors.dividerColor,
692
+ backgroundColor: AppColors.white,
693
+ alignItems: 'center',
694
+ justifyContent: 'center',
695
+ },
696
+ discardBtnText: {
697
+ fontFamily: AppFonts.interBold,
698
+ fontSize: 12.5,
699
+ color: AppColors.grayTextStrong,
700
+ },
701
+ applyBtn: {
702
+ flex: 2,
703
+ backgroundColor: AppColors.brandPurple,
704
+ paddingVertical: 9.5,
705
+ borderRadius: 8,
706
+ alignItems: 'center',
707
+ justifyContent: 'center',
708
+ shadowColor: AppColors.brandPurple,
709
+ shadowOffset: {width: 0, height: 1},
710
+ shadowOpacity: 0.25,
711
+ shadowRadius: 3,
712
+ elevation: 2,
713
+ },
714
+ applyBtnText: {
715
+ fontFamily: AppFonts.interBold,
716
+ fontSize: 12.5,
717
+ color: AppColors.white,
718
+ },
719
+ });
720
+
721
+ export default CrashFilterModal;