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,1095 @@
1
+ import React, {useCallback, useMemo, useRef} from 'react';
2
+ import {
3
+ Animated,
4
+ FlatList,
5
+ Pressable,
6
+ ScrollView,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native';
11
+ import {animateNextLayout, useInspector} from './InspectorContext';
12
+ import useAccordion from '../../customHooks/useAccordion';
13
+ import TouchableScale from '../TouchableScale';
14
+ import AnimatedEntrance from '../AnimatedEntrance';
15
+ import DomainHeader from '../DomainHeader';
16
+ import LogCard from '../LogCard';
17
+ import EmptyState from '../EmptyState';
18
+ import EndOfListFooter from '../EndOfListFooter';
19
+ import styles from '../../styles';
20
+ import {AppColors} from '../../styles/AppColors';
21
+ import {AppFonts} from '../../styles/AppFonts';
22
+ import {STATUS_FILTERS, METHOD_COLORS} from '../../constants';
23
+ import {GroupedListItem, Method, SearchScope} from '../../types';
24
+ import {useTranslation} from '../../i18n';
25
+ import {
26
+ SearchIcon,
27
+ ClearIcon,
28
+ TrashIcon,
29
+ SortArrowIcon,
30
+ FilterIcon,
31
+ ChevronIcon,
32
+ CheckIcon,
33
+ CircleCheckIcon,
34
+ CircleXIcon,
35
+ CircleAlertIcon,
36
+ LayersIcon,
37
+ HeaderPauseIcon,
38
+ ClockIcon,
39
+ } from '../NetworkIcons';
40
+
41
+ const STATUS_META: Record<
42
+ string,
43
+ {color: string; Icon: (props: {color?: string; size?: number}) => React.JSX.Element}
44
+ > = {
45
+ ALL: {color: AppColors.grayText, Icon: LayersIcon},
46
+ '2xx': {color: AppColors.greenColor, Icon: CircleCheckIcon},
47
+ '200': {color: AppColors.greenColor, Icon: CircleCheckIcon},
48
+ '3xx': {color: AppColors.warningIconGold, Icon: CircleAlertIcon},
49
+ '300': {color: AppColors.warningIconGold, Icon: CircleAlertIcon},
50
+ '4xx': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
51
+ '400': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
52
+ '404': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
53
+ '5xx': {color: AppColors.errorColor, Icon: CircleXIcon},
54
+ '500': {color: AppColors.errorColor, Icon: CircleXIcon},
55
+ Failed: {color: AppColors.errorColor, Icon: CircleXIcon},
56
+ };
57
+
58
+ const getStatusMeta = (filter: string) => {
59
+ if (STATUS_META[filter]) return STATUS_META[filter];
60
+ const num = parseInt(filter, 10);
61
+ if (!isNaN(num)) {
62
+ if (num >= 200 && num < 300) return {color: AppColors.greenColor, Icon: CircleCheckIcon};
63
+ if (num >= 300 && num < 400) return {color: AppColors.warningIconGold, Icon: CircleAlertIcon};
64
+ if (num >= 400 && num < 500) return {color: AppColors.darkOrange, Icon: CircleAlertIcon};
65
+ if (num >= 500) return {color: AppColors.errorColor, Icon: CircleXIcon};
66
+ }
67
+ if (filter.startsWith('2')) return {color: AppColors.greenColor, Icon: CircleCheckIcon};
68
+ if (filter.startsWith('3')) return {color: AppColors.warningIconGold, Icon: CircleAlertIcon};
69
+ if (filter.startsWith('4')) return {color: AppColors.darkOrange, Icon: CircleAlertIcon};
70
+ if (filter.startsWith('5')) return {color: AppColors.errorColor, Icon: CircleXIcon};
71
+ return {color: AppColors.grayText, Icon: LayersIcon};
72
+ };
73
+
74
+ const FilterChip = React.memo(({
75
+ label,
76
+ color,
77
+ Icon,
78
+ badge,
79
+ active,
80
+ onPress,
81
+ }: {
82
+ label: string;
83
+ color: string;
84
+ Icon?: (props: {color?: string; size?: number}) => React.JSX.Element;
85
+ badge?: string;
86
+ active: boolean;
87
+ onPress: () => void;
88
+ }) => {
89
+ const iconColor = active ? color : AppColors.grayTextWeak;
90
+ return (
91
+ <TouchableScale
92
+ style={styles.statusFilterWrap}
93
+ onPress={onPress}
94
+ hitSlop={10}>
95
+ <View
96
+ style={[
97
+ styles.statusFilterChip,
98
+ {
99
+ paddingHorizontal: 9,
100
+ paddingVertical: 5,
101
+ borderRadius: 7,
102
+ borderWidth: 1,
103
+ borderColor: active ? color : `${AppColors.grayBorderSecondary}`,
104
+ backgroundColor: active ? `${color}14` : AppColors.grayBackground,
105
+ },
106
+ active && {
107
+ shadowColor: color,
108
+ shadowOffset: {width: 0, height: 1},
109
+ shadowOpacity: 0.2,
110
+ shadowRadius: 2,
111
+ elevation: 1.5,
112
+ },
113
+ ]}>
114
+ {badge != null ? (
115
+ <View
116
+ style={[
117
+ styles.methodBadgeMini,
118
+ {backgroundColor: badge},
119
+ ]}
120
+ />
121
+ ) : (
122
+ Icon && (
123
+ <Icon
124
+ color={iconColor}
125
+ size={13}
126
+ />
127
+ )
128
+ )}
129
+ <Text
130
+ numberOfLines={1}
131
+ style={[
132
+ styles.statusFilterText,
133
+ {
134
+ color: active ? color : AppColors.grayText,
135
+ fontFamily: active ? AppFonts.interBold : AppFonts.interMedium,
136
+ },
137
+ ]}>
138
+ {label}
139
+ </Text>
140
+ </View>
141
+ </TouchableScale>
142
+ );
143
+ });
144
+
145
+ const NetworkTab = React.memo(() => {
146
+ const {
147
+ groupedData,
148
+ search,
149
+ setSearch,
150
+ searchScope,
151
+ setSearchScope,
152
+ isRegexSearch,
153
+ setIsRegexSearch,
154
+ isCaseSensitive,
155
+ setIsCaseSensitive,
156
+ quickFilter,
157
+ setQuickFilter,
158
+ handleDelete,
159
+ selectedLogs,
160
+ sortOrder,
161
+ setSortOrder,
162
+ statusFilters,
163
+ setStatusFilters,
164
+ methodFilters,
165
+ setMethodFilters,
166
+ availableMethods,
167
+ filteredLogs,
168
+ logs,
169
+ toggleSectionFilter,
170
+ toggleSectionCollapse,
171
+ minStart,
172
+ totalRange,
173
+ newLogIds,
174
+ toggleSelect,
175
+ setSelected,
176
+ isNetworkPaused,
177
+ setIsNetworkPaused,
178
+ } = useInspector();
179
+
180
+ const {t} = useTranslation();
181
+ const filtersAccordion = useAccordion(false, 300, 260);
182
+ const apisListRef = useRef<FlatList<any>>(null);
183
+
184
+ const [isSearchFocused, setIsSearchFocused] = React.useState(false);
185
+
186
+ const quickCounts = useMemo(() => {
187
+ let errorCount = 0;
188
+ let successCount = 0;
189
+ let slowCount = 0;
190
+ let postCount = 0;
191
+ let getCount = 0;
192
+ let gqlCount = 0;
193
+
194
+ logs.forEach(l => {
195
+ const s =
196
+ typeof l.status === 'number'
197
+ ? l.status
198
+ : parseInt(String(l.status), 10);
199
+ if (l.status === 0 || l.status == null || (!isNaN(s) && s >= 400)) {
200
+ errorCount++;
201
+ } else if (!isNaN(s) && s >= 200 && s < 400) {
202
+ successCount++;
203
+ }
204
+ if ((l.duration || 0) >= 500) {
205
+ slowCount++;
206
+ }
207
+ const m = (l.method || '').toUpperCase();
208
+ if (m === 'POST') postCount++;
209
+ if (m === 'GET') getCount++;
210
+ const u = (l.url || '').toLowerCase();
211
+ const c = (l.client || '').toLowerCase();
212
+ if (
213
+ u.includes('graphql') ||
214
+ c.includes('graphql') ||
215
+ c.includes('apollo')
216
+ ) {
217
+ gqlCount++;
218
+ }
219
+ });
220
+
221
+ return {
222
+ all: logs.length,
223
+ errors: errorCount,
224
+ success: successCount,
225
+ slow: slowCount,
226
+ post: postCount,
227
+ get: getCount,
228
+ graphql: gqlCount,
229
+ };
230
+ }, [logs]);
231
+
232
+ const QUICK_CHIPS = useMemo(
233
+ () => [
234
+ {
235
+ id: 'all',
236
+ label: 'All',
237
+ count: quickCounts.all,
238
+ color: AppColors.purple,
239
+ },
240
+ {
241
+ id: 'errors',
242
+ label: 'Errors',
243
+ count: quickCounts.errors,
244
+ color: AppColors.errorColor,
245
+ },
246
+ {
247
+ id: 'success',
248
+ label: '2xx OK',
249
+ count: quickCounts.success,
250
+ color: AppColors.greenColor,
251
+ },
252
+ {
253
+ id: 'slow',
254
+ label: 'Slow >500ms',
255
+ count: quickCounts.slow,
256
+ color: AppColors.warningIconGold,
257
+ },
258
+ {
259
+ id: 'POST',
260
+ label: 'POST',
261
+ count: quickCounts.post,
262
+ color: METHOD_COLORS.POST || '#3B82F6',
263
+ },
264
+ {
265
+ id: 'GET',
266
+ label: 'GET',
267
+ count: quickCounts.get,
268
+ color: METHOD_COLORS.GET || '#10B981',
269
+ },
270
+ {
271
+ id: 'graphql',
272
+ label: 'GraphQL',
273
+ count: quickCounts.graphql,
274
+ color: '#E10098',
275
+ },
276
+ ],
277
+ [quickCounts],
278
+ );
279
+
280
+ const SEARCH_SCOPES: Array<{id: SearchScope; label: string}> = [
281
+ {id: 'all', label: 'All'},
282
+ {id: 'url', label: 'URL / Path'},
283
+ {id: 'reqBody', label: 'Payload'},
284
+ {id: 'resBody', label: 'Response'},
285
+ {id: 'headers', label: 'Headers'},
286
+ ];
287
+
288
+ const QUICK_API_QUERY_TAGS = useMemo(
289
+ () => [
290
+ {label: '⚠️ Failed', query: 'is:error'},
291
+ {label: '🐢 Slow >1s', query: 'slow:>1s'},
292
+ {label: 'POST', query: 'm:POST'},
293
+ {label: 'GET', query: 'm:GET'},
294
+ {label: 'GraphQL', query: 'is:graphql'},
295
+ {label: 'Axios', query: 'client:axios'},
296
+ {label: '200 OK', query: 'status:200'},
297
+ {label: '404', query: 'status:404'},
298
+ {label: '500', query: 'status:500'},
299
+ {label: '🔒 HTTPS', query: 'is:https'},
300
+ ],
301
+ [],
302
+ );
303
+
304
+ const networkMetrics = useMemo(() => {
305
+ if (!logs || logs.length === 0) return null;
306
+ let successCount = 0;
307
+ let completedCount = 0;
308
+ let totalDuration = 0;
309
+ let totalBytes = 0;
310
+ const durations: number[] = [];
311
+
312
+ logs.forEach(log => {
313
+ if (log.status != null) {
314
+ completedCount++;
315
+ const s = typeof log.status === 'number' ? log.status : parseInt(String(log.status), 10);
316
+ if (!isNaN(s)) {
317
+ if (s >= 200 && s < 400) {
318
+ successCount++;
319
+ }
320
+ }
321
+ }
322
+ if (typeof log.duration === 'number' && log.duration > 0) {
323
+ durations.push(log.duration);
324
+ totalDuration += log.duration;
325
+ }
326
+ });
327
+
328
+ durations.sort((a, b) => a - b);
329
+ const avgDuration = durations.length > 0 ? Math.round(totalDuration / durations.length) : 0;
330
+ const p95 = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0;
331
+ const successRate = completedCount > 0 ? Math.round((successCount / completedCount) * 100) : 100;
332
+
333
+ return {
334
+ successRate,
335
+ avgDuration,
336
+ p95,
337
+ totalCount: logs.length,
338
+ };
339
+ }, [logs]);
340
+
341
+ const renderItem = useCallback(
342
+ ({item, index}: {item: GroupedListItem; index: number}) => {
343
+ if (item.type === 'header') {
344
+ return (
345
+ <AnimatedEntrance index={index} distance={8}>
346
+ <DomainHeader
347
+ pageName={item.pageName}
348
+ color={item.color}
349
+ stats={item.stats}
350
+ activeFilters={item.activeFilters}
351
+ onToggleFilter={toggleSectionFilter}
352
+ isCollapsed={item.isCollapsed}
353
+ onToggleCollapse={toggleSectionCollapse}
354
+ isFirst={item.isFirst}
355
+ timestamp={item.timestamp}
356
+ />
357
+ </AnimatedEntrance>
358
+ );
359
+ }
360
+
361
+ const {log, isLast, color} = item;
362
+ return (
363
+ <AnimatedEntrance
364
+ index={index}
365
+ distance={8}
366
+ style={[
367
+ styles.treeNodeRow,
368
+ isLast && styles.treeNodeRowLast,
369
+ ]}>
370
+ <View style={styles.treeLines}>
371
+ <View
372
+ style={[
373
+ styles.modernTreeLine,
374
+ {borderColor: color},
375
+ isLast && styles.modernTreeLineLast,
376
+ ]}
377
+ />
378
+ {!isLast && (
379
+ <View style={[styles.modernTreeBranch, {borderColor: color}]} />
380
+ )}
381
+ </View>
382
+ <View style={styles.treeCardWrapper}>
383
+ <LogCard
384
+ item={log}
385
+ isSelected={selectedLogs.has(log.id)}
386
+ onToggleSelect={toggleSelect}
387
+ onPress={() => {
388
+ animateNextLayout();
389
+ setSelected(log);
390
+ }}
391
+ timelineMinStart={minStart}
392
+ timelineTotalRange={totalRange}
393
+ isNew={newLogIds.has(log.id)}
394
+ searchStr={search}
395
+ />
396
+ </View>
397
+ </AnimatedEntrance>
398
+ );
399
+ },
400
+ [
401
+ minStart,
402
+ totalRange,
403
+ newLogIds,
404
+ selectedLogs,
405
+ toggleSelect,
406
+ search,
407
+ toggleSectionFilter,
408
+ toggleSectionCollapse,
409
+ setSelected,
410
+ ],
411
+ );
412
+
413
+ return (
414
+ <View style={{flex: 1}}>
415
+ <FlatList
416
+ ref={apisListRef}
417
+ data={groupedData}
418
+ keyExtractor={item => item?.id?.toString()}
419
+ renderItem={renderItem}
420
+ initialNumToRender={12}
421
+ maxToRenderPerBatch={8}
422
+ windowSize={5}
423
+ removeClippedSubviews={true}
424
+ renderToHardwareTextureAndroid={true}
425
+ ListHeaderComponent={
426
+ <View style={{marginTop: 6}}>
427
+ {/* Network Health & Telemetry Strip */}
428
+ {networkMetrics != null && networkMetrics.totalCount > 0 && (
429
+ <View
430
+ style={{
431
+ flexDirection: 'row',
432
+ alignItems: 'center',
433
+ justifyContent: 'space-between',
434
+ backgroundColor: AppColors.grayBackground,
435
+ borderRadius: 8,
436
+ paddingHorizontal: 10,
437
+ paddingVertical: 5.5,
438
+ marginHorizontal: 12,
439
+ marginBottom: 6,
440
+ borderWidth: 1,
441
+ borderColor: AppColors.dividerColor,
442
+ }}>
443
+ {/* Success Rate */}
444
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4.5}}>
445
+ <View
446
+ style={{
447
+ width: 6,
448
+ height: 6,
449
+ borderRadius: 3,
450
+ backgroundColor:
451
+ networkMetrics.successRate >= 95
452
+ ? AppColors.greenColor
453
+ : networkMetrics.successRate >= 80
454
+ ? AppColors.amber500
455
+ : AppColors.errorColor,
456
+ }}
457
+ />
458
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack}}>
459
+ {networkMetrics.successRate}%
460
+ </Text>
461
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
462
+ {t('network.successRate')}
463
+ </Text>
464
+ </View>
465
+
466
+ <View style={{width: 1, height: 11, backgroundColor: AppColors.dividerColor}} />
467
+
468
+ {/* Avg Latency */}
469
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
470
+ <ClockIcon color={AppColors.purple} size={10} />
471
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.purple}}>
472
+ {networkMetrics.avgDuration}ms
473
+ </Text>
474
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
475
+ {t('network.avgLatency')}
476
+ </Text>
477
+ </View>
478
+
479
+ <View style={{width: 1, height: 11, backgroundColor: AppColors.dividerColor}} />
480
+
481
+ {/* P95 Latency */}
482
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
483
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack}}>
484
+ {networkMetrics.p95}ms
485
+ </Text>
486
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
487
+ P95
488
+ </Text>
489
+ </View>
490
+ </View>
491
+ )}
492
+
493
+ {/* Toolbar Row with Search & Actions */}
494
+ <View style={styles.toolbarRow}>
495
+ <View
496
+ style={[
497
+ styles.searchContainer,
498
+ isSearchFocused && {
499
+ borderColor: AppColors.purple,
500
+ borderWidth: 1.5,
501
+ },
502
+ ]}>
503
+ <SearchIcon
504
+ color={
505
+ isSearchFocused
506
+ ? AppColors.purple
507
+ : AppColors.grayTextWeak
508
+ }
509
+ size={15}
510
+ />
511
+ <TextInput
512
+ placeholder="Search url, body, is:error, slow:>1s..."
513
+ placeholderTextColor={AppColors.grayTextWeak}
514
+ value={search}
515
+ onChangeText={setSearch}
516
+ onFocus={() => setIsSearchFocused(true)}
517
+ onBlur={() => setIsSearchFocused(false)}
518
+ style={styles.searchInput}
519
+ autoCorrect={false}
520
+ autoCapitalize="none"
521
+ />
522
+
523
+ {/* Match Case (Aa) & Regex (.*) Quick Toggles */}
524
+ <View
525
+ style={{
526
+ flexDirection: 'row',
527
+ alignItems: 'center',
528
+ gap: 3,
529
+ marginRight: 2,
530
+ }}>
531
+ <Pressable
532
+ hitSlop={8}
533
+ onPress={() => setIsCaseSensitive(prev => !prev)}
534
+ style={{
535
+ paddingHorizontal: 4.5,
536
+ paddingVertical: 2,
537
+ borderRadius: 4,
538
+ backgroundColor: isCaseSensitive
539
+ ? AppColors.purple
540
+ : 'transparent',
541
+ borderWidth: 1,
542
+ borderColor: isCaseSensitive
543
+ ? AppColors.purple
544
+ : `${AppColors.grayBorderSecondary}`,
545
+ }}>
546
+ <Text
547
+ style={{
548
+ fontFamily: AppFonts.interBold,
549
+ fontSize: 9.5,
550
+ color: isCaseSensitive
551
+ ? AppColors.white
552
+ : AppColors.grayTextWeak,
553
+ }}>
554
+ Aa
555
+ </Text>
556
+ </Pressable>
557
+
558
+ <Pressable
559
+ hitSlop={8}
560
+ onPress={() => setIsRegexSearch(prev => !prev)}
561
+ style={{
562
+ paddingHorizontal: 4.5,
563
+ paddingVertical: 2,
564
+ borderRadius: 4,
565
+ backgroundColor: isRegexSearch
566
+ ? AppColors.purple
567
+ : 'transparent',
568
+ borderWidth: 1,
569
+ borderColor: isRegexSearch
570
+ ? AppColors.purple
571
+ : `${AppColors.grayBorderSecondary}`,
572
+ }}>
573
+ <Text
574
+ style={{
575
+ fontFamily: AppFonts.interBold,
576
+ fontSize: 9.5,
577
+ color: isRegexSearch
578
+ ? AppColors.white
579
+ : AppColors.grayTextWeak,
580
+ }}>
581
+ .*
582
+ </Text>
583
+ </Pressable>
584
+ </View>
585
+
586
+ {search.length > 0 && (
587
+ <View
588
+ style={{
589
+ flexDirection: 'row',
590
+ alignItems: 'center',
591
+ gap: 4,
592
+ }}>
593
+ <View
594
+ style={{
595
+ backgroundColor: `${AppColors.purple}20`,
596
+ borderRadius: 10,
597
+ paddingHorizontal: 5,
598
+ paddingVertical: 1.5,
599
+ }}>
600
+ <Text
601
+ style={{
602
+ color: AppColors.purple,
603
+ fontSize: 9.5,
604
+ fontFamily: AppFonts.interBold,
605
+ }}>
606
+ {filteredLogs.length}
607
+ </Text>
608
+ </View>
609
+ <Pressable
610
+ onPress={() => setSearch('')}
611
+ hitSlop={10}
612
+ style={styles.clearBtn}>
613
+ <ClearIcon
614
+ color={AppColors.grayTextWeak}
615
+ size={13}
616
+ />
617
+ </Pressable>
618
+ </View>
619
+ )}
620
+ </View>
621
+
622
+ <View style={styles.toolbarRight}>
623
+ <TouchableScale
624
+ style={[
625
+ styles.toolbarBtn,
626
+ isNetworkPaused && {
627
+ borderColor: `${AppColors.darkOrange}50`,
628
+ backgroundColor: `${AppColors.darkOrange}18`,
629
+ },
630
+ ]}
631
+ onPress={() => setIsNetworkPaused(p => !p)}
632
+ hitSlop={6}>
633
+ <HeaderPauseIcon
634
+ isPaused={isNetworkPaused}
635
+ color={
636
+ isNetworkPaused
637
+ ? AppColors.darkOrange
638
+ : AppColors.grayTextStrong
639
+ }
640
+ size={16}
641
+ />
642
+ </TouchableScale>
643
+
644
+ <TouchableScale
645
+ style={[
646
+ styles.toolbarBtn,
647
+ {
648
+ borderColor: `${AppColors.errorColor}38`,
649
+ backgroundColor: `${AppColors.errorColor}0F`,
650
+ },
651
+ ]}
652
+ onPress={handleDelete}
653
+ hitSlop={6}>
654
+ <TrashIcon
655
+ color={AppColors.errorColor}
656
+ size={15}
657
+ />
658
+ {selectedLogs.size > 0 && (
659
+ <View style={styles.trashBadge}>
660
+ <Text style={styles.trashBadgeText}>
661
+ {selectedLogs.size}
662
+ </Text>
663
+ </View>
664
+ )}
665
+ </TouchableScale>
666
+
667
+ <TouchableScale
668
+ style={styles.toolbarBtn}
669
+ onPress={() =>
670
+ setSortOrder(o =>
671
+ o === 'newest' ? 'oldest' : 'newest',
672
+ )
673
+ }
674
+ hitSlop={10}>
675
+ <SortArrowIcon
676
+ direction={
677
+ sortOrder === 'newest' ? 'down' : 'up'
678
+ }
679
+ color={AppColors.grayTextStrong}
680
+ size={18}
681
+ />
682
+ </TouchableScale>
683
+
684
+ <TouchableScale
685
+ style={[
686
+ styles.toolbarBtn,
687
+ filtersAccordion.isOpen &&
688
+ styles.toolbarBtnActive,
689
+ ]}
690
+ onPress={filtersAccordion.toggleOpen}
691
+ hitSlop={10}>
692
+ <FilterIcon
693
+ color={
694
+ filtersAccordion.isOpen
695
+ ? AppColors.purple
696
+ : AppColors.grayTextStrong
697
+ }
698
+ size={18}
699
+ />
700
+ </TouchableScale>
701
+ </View>
702
+ </View>
703
+
704
+ {/* Quick Filter Horizontal Chips Bar with Live Counts */}
705
+ <View style={{marginBottom: 8}}>
706
+ <ScrollView
707
+ horizontal
708
+ showsHorizontalScrollIndicator={false}
709
+ contentContainerStyle={{
710
+ paddingHorizontal: 12,
711
+ flexDirection: 'row',
712
+ alignItems: 'center',
713
+ gap: 6,
714
+ }}>
715
+ {QUICK_CHIPS.map(chip => {
716
+ const isActive = quickFilter === chip.id;
717
+ const chipColor = chip.color || AppColors.purple;
718
+ return (
719
+ <TouchableScale
720
+ key={chip.id}
721
+ onPress={() => {
722
+ setQuickFilter(isActive ? 'all' : chip.id);
723
+ }}>
724
+ <View
725
+ style={{
726
+ flexDirection: 'row',
727
+ alignItems: 'center',
728
+ paddingHorizontal: 9,
729
+ paddingVertical: 4.5,
730
+ borderRadius: 8,
731
+ backgroundColor: isActive
732
+ ? chipColor
733
+ : `${chipColor}12`,
734
+ borderWidth: 1,
735
+ borderColor: isActive
736
+ ? chipColor
737
+ : `${chipColor}30`,
738
+ gap: 5,
739
+ }}>
740
+ <Text
741
+ style={{
742
+ fontFamily: AppFonts.interBold,
743
+ fontSize: 10.5,
744
+ color: isActive
745
+ ? AppColors.white
746
+ : AppColors.primaryBlack,
747
+ }}>
748
+ {chip.label}
749
+ </Text>
750
+ <View
751
+ style={{
752
+ backgroundColor: isActive
753
+ ? 'rgba(255,255,255,0.25)'
754
+ : `${chipColor}20`,
755
+ paddingHorizontal: 5,
756
+ paddingVertical: 1,
757
+ borderRadius: 8,
758
+ }}>
759
+ <Text
760
+ style={{
761
+ fontFamily: AppFonts.interBold,
762
+ fontSize: 9,
763
+ color: isActive ? AppColors.white : chipColor,
764
+ }}>
765
+ {chip.count}
766
+ </Text>
767
+ </View>
768
+ </View>
769
+ </TouchableScale>
770
+ );
771
+ })}
772
+ </ScrollView>
773
+ </View>
774
+
775
+ {/* Search Scope Selector Bar (Visible on search or focus) */}
776
+ {(isSearchFocused || search.length > 0) && (
777
+ <View
778
+ style={{
779
+ flexDirection: 'row',
780
+ alignItems: 'center',
781
+ paddingHorizontal: 12,
782
+ marginBottom: 6,
783
+ gap: 6,
784
+ }}>
785
+ <Text
786
+ style={{
787
+ fontFamily: AppFonts.interBold,
788
+ fontSize: 9.5,
789
+ color: AppColors.grayTextWeak,
790
+ textTransform: 'uppercase',
791
+ letterSpacing: 0.5,
792
+ }}>
793
+ Scope:
794
+ </Text>
795
+ <ScrollView
796
+ horizontal
797
+ showsHorizontalScrollIndicator={false}
798
+ contentContainerStyle={{
799
+ flexDirection: 'row',
800
+ alignItems: 'center',
801
+ gap: 5,
802
+ }}>
803
+ {SEARCH_SCOPES.map(s => {
804
+ const isSelected = searchScope === s.id;
805
+ return (
806
+ <TouchableScale
807
+ key={s.id}
808
+ onPress={() => setSearchScope(s.id)}>
809
+ <View
810
+ style={{
811
+ paddingHorizontal: 7,
812
+ paddingVertical: 2.5,
813
+ borderRadius: 5,
814
+ backgroundColor: isSelected
815
+ ? AppColors.purple
816
+ : `${AppColors.grayBorderSecondary}40`,
817
+ borderWidth: 1,
818
+ borderColor: isSelected
819
+ ? AppColors.purple
820
+ : AppColors.grayBorderSecondary,
821
+ }}>
822
+ <Text
823
+ style={{
824
+ fontFamily: AppFonts.interBold,
825
+ fontSize: 9.5,
826
+ color: isSelected
827
+ ? AppColors.white
828
+ : AppColors.grayText,
829
+ }}>
830
+ {s.label}
831
+ </Text>
832
+ </View>
833
+ </TouchableScale>
834
+ );
835
+ })}
836
+ </ScrollView>
837
+ </View>
838
+ )}
839
+
840
+ {/* Advanced Search Quick Query Suggestions Bar */}
841
+ {(isSearchFocused || search.length > 0) && (
842
+ <ScrollView
843
+ horizontal
844
+ showsHorizontalScrollIndicator={false}
845
+ style={{marginBottom: 8, maxHeight: 28}}
846
+ contentContainerStyle={{
847
+ paddingHorizontal: 12,
848
+ flexDirection: 'row',
849
+ alignItems: 'center',
850
+ gap: 5,
851
+ }}>
852
+ {QUICK_API_QUERY_TAGS.map(item => {
853
+ const isSelected = search.includes(item.query);
854
+ return (
855
+ <TouchableScale
856
+ key={item.query}
857
+ onPress={() => {
858
+ if (isSelected) {
859
+ setSearch(prev =>
860
+ prev.replace(item.query, '').trim(),
861
+ );
862
+ } else {
863
+ setSearch(prev =>
864
+ prev ? `${prev} ${item.query}`.trim() : item.query,
865
+ );
866
+ }
867
+ }}>
868
+ <View
869
+ style={{
870
+ paddingHorizontal: 7,
871
+ paddingVertical: 2.5,
872
+ borderRadius: 5,
873
+ backgroundColor: isSelected
874
+ ? AppColors.purple
875
+ : `${AppColors.purple}14`,
876
+ borderWidth: 1,
877
+ borderColor: isSelected
878
+ ? AppColors.purple
879
+ : `${AppColors.purple}30`,
880
+ }}>
881
+ <Text
882
+ style={{
883
+ fontFamily: AppFonts.interBold,
884
+ fontSize: 9.5,
885
+ color: isSelected
886
+ ? AppColors.white
887
+ : AppColors.purple,
888
+ }}>
889
+ {item.label}
890
+ </Text>
891
+ </View>
892
+ </TouchableScale>
893
+ );
894
+ })}
895
+ </ScrollView>
896
+ )}
897
+
898
+ {/* Active Filter Helper Status Bar */}
899
+ {(quickFilter !== 'all' ||
900
+ search.trim().length > 0 ||
901
+ statusFilters.size > 0 ||
902
+ methodFilters.size > 0 ||
903
+ filteredLogs.length !== logs.length) && (
904
+ <View
905
+ style={{
906
+ flexDirection: 'row',
907
+ alignItems: 'center',
908
+ justifyContent: 'space-between',
909
+ marginHorizontal: 12,
910
+ marginBottom: 8,
911
+ paddingHorizontal: 10,
912
+ paddingVertical: 5.5,
913
+ backgroundColor: `${AppColors.purple}10`,
914
+ borderRadius: 8,
915
+ borderWidth: 1,
916
+ borderColor: `${AppColors.purple}25`,
917
+ }}>
918
+ <Text
919
+ style={{
920
+ fontFamily: AppFonts.interMedium,
921
+ fontSize: 10.5,
922
+ color: AppColors.purple,
923
+ }}>
924
+ Showing {filteredLogs.length} of {logs.length} requests
925
+ </Text>
926
+ <TouchableScale
927
+ onPress={() => {
928
+ setSearch('');
929
+ setQuickFilter('all');
930
+ setStatusFilters(new Set());
931
+ setMethodFilters(new Set());
932
+ }}>
933
+ <Text
934
+ style={{
935
+ fontFamily: AppFonts.interBold,
936
+ fontSize: 10.5,
937
+ color: AppColors.errorColor,
938
+ }}>
939
+ Clear All Filters
940
+ </Text>
941
+ </TouchableScale>
942
+ </View>
943
+ )}
944
+
945
+ <Animated.View
946
+ style={[
947
+ filtersAccordion.bodyStyle,
948
+ {overflow: 'hidden'},
949
+ ]}>
950
+ <View style={styles.filtersContainer}>
951
+ <Text style={styles.filtersHeading}>
952
+ STATUS
953
+ </Text>
954
+ <ScrollView
955
+ horizontal
956
+ showsHorizontalScrollIndicator={false}
957
+ contentContainerStyle={
958
+ styles.statusRowContent
959
+ }>
960
+ {STATUS_FILTERS.map(filter => {
961
+ const isAll = filter === 'ALL';
962
+ const active = isAll
963
+ ? statusFilters.size === 0
964
+ : statusFilters.has(filter);
965
+ const meta = getStatusMeta(filter);
966
+ return (
967
+ <FilterChip
968
+ key={filter}
969
+ label={filter}
970
+ color={meta.color}
971
+ Icon={meta.Icon}
972
+ active={active}
973
+ onPress={() => {
974
+ if (isAll) {
975
+ setStatusFilters(new Set());
976
+ } else {
977
+ setStatusFilters(prev => {
978
+ const next = new Set(prev);
979
+ next.has(filter)
980
+ ? next.delete(filter)
981
+ : next.add(filter);
982
+ return next;
983
+ });
984
+ }
985
+ }}
986
+ />
987
+ );
988
+ })}
989
+ </ScrollView>
990
+
991
+ <Text
992
+ style={[
993
+ styles.filtersHeading,
994
+ {marginTop: 16},
995
+ ]}>
996
+ METHOD
997
+ </Text>
998
+ <ScrollView
999
+ horizontal
1000
+ showsHorizontalScrollIndicator={false}
1001
+ contentContainerStyle={
1002
+ styles.statusRowContent
1003
+ }>
1004
+ {availableMethods.map(filter => {
1005
+ const isAll = filter === 'ALL';
1006
+ const active = isAll
1007
+ ? methodFilters.size === 0
1008
+ : methodFilters.has(filter as Method);
1009
+ const methodColor =
1010
+ METHOD_COLORS[filter as Method] ??
1011
+ METHOD_COLORS.ALL;
1012
+ return (
1013
+ <FilterChip
1014
+ key={filter}
1015
+ label={filter}
1016
+ color={methodColor}
1017
+ badge={methodColor}
1018
+ active={active}
1019
+ onPress={() => {
1020
+ if (isAll) {
1021
+ setMethodFilters(new Set());
1022
+ } else {
1023
+ setMethodFilters(prev => {
1024
+ const next = new Set(prev);
1025
+ next.has(filter as Method)
1026
+ ? next.delete(filter as Method)
1027
+ : next.add(filter as Method);
1028
+ return next;
1029
+ });
1030
+ }
1031
+ }}
1032
+ />
1033
+ );
1034
+ })}
1035
+ </ScrollView>
1036
+ </View>
1037
+ </Animated.View>
1038
+
1039
+ {(search ||
1040
+ statusFilters.size > 0 ||
1041
+ methodFilters.size > 0) && (
1042
+ <Text style={styles.resultCount}>
1043
+ {filteredLogs.length === logs.length
1044
+ ? `${logs.length} requests`
1045
+ : `${filteredLogs.length} of ${logs.length} filtered requests`}
1046
+ </Text>
1047
+ )}
1048
+ </View>
1049
+ }
1050
+ ListEmptyComponent={
1051
+ <EmptyState
1052
+ isSearch={
1053
+ search.length > 0 || statusFilters.size > 0
1054
+ }
1055
+ />
1056
+ }
1057
+ ListFooterComponent={
1058
+ groupedData.length > 0 ? (
1059
+ <EndOfListFooter count={filteredLogs.length} label="requests" />
1060
+ ) : null
1061
+ }
1062
+ contentContainerStyle={[
1063
+ styles.listContent,
1064
+ filteredLogs.length === 0 && {flexGrow: 1},
1065
+ ]}
1066
+ keyboardShouldPersistTaps="handled"
1067
+ />
1068
+ {/* #2 — always-visible scroll-to-top, bottom right */}
1069
+ <TouchableScale
1070
+ onPress={() => {
1071
+ try {
1072
+ apisListRef.current?.scrollToOffset({
1073
+ offset: 0,
1074
+ animated: true,
1075
+ });
1076
+ } catch {
1077
+ try {
1078
+ apisListRef.current?.scrollToIndex({
1079
+ index: 0,
1080
+ animated: true,
1081
+ });
1082
+ } catch {}
1083
+ }
1084
+ }}
1085
+ hitSlop={12}
1086
+ style={styles.scrollTopBtn}>
1087
+ <View style={{transform: [{rotate: '180deg'}]}}>
1088
+ <ChevronIcon color={AppColors.white} size={18} />
1089
+ </View>
1090
+ </TouchableScale>
1091
+ </View>
1092
+ );
1093
+ });
1094
+
1095
+ export default NetworkTab;