react-native-inapp-inspector 2.2.1 → 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 (181) hide show
  1. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  2. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  10. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  11. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  12. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  13. package/android/.gradle/9.2.0/gc.properties +0 -0
  14. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  15. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  16. package/android/.gradle/vcs-1/gc.properties +0 -0
  17. package/android/build/reports/problems/problems-report.html +659 -0
  18. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +28 -6
  19. package/dist/commonjs/analytics.d.ts +7 -0
  20. package/dist/commonjs/analytics.js +28 -0
  21. package/dist/commonjs/bundle.d.ts +6 -0
  22. package/dist/commonjs/bundle.js +20 -0
  23. package/dist/commonjs/console.d.ts +7 -0
  24. package/dist/commonjs/console.js +20 -0
  25. package/dist/commonjs/constants/version.d.ts +1 -1
  26. package/dist/commonjs/constants/version.js +1 -1
  27. package/dist/commonjs/crash.d.ts +7 -0
  28. package/dist/commonjs/crash.js +27 -0
  29. package/dist/commonjs/customHooks/analyticsLogger.d.ts +3 -0
  30. package/dist/commonjs/customHooks/analyticsLogger.js +23 -2
  31. package/dist/commonjs/customHooks/consoleLogger.d.ts +3 -0
  32. package/dist/commonjs/customHooks/consoleLogger.js +24 -4
  33. package/dist/commonjs/customHooks/crashHandler.d.ts +2 -0
  34. package/dist/commonjs/customHooks/crashHandler.js +15 -1
  35. package/dist/commonjs/customHooks/networkLogger.d.ts +3 -0
  36. package/dist/commonjs/customHooks/networkLogger.js +23 -2
  37. package/dist/commonjs/customHooks/performanceTracker.d.ts +3 -0
  38. package/dist/commonjs/customHooks/performanceTracker.js +34 -3
  39. package/dist/commonjs/customHooks/reduxLogger.d.ts +3 -0
  40. package/dist/commonjs/customHooks/reduxLogger.js +24 -4
  41. package/dist/commonjs/helpers/index.d.ts +1 -0
  42. package/dist/commonjs/helpers/index.js +1 -0
  43. package/dist/commonjs/helpers/memoryManager.d.ts +29 -0
  44. package/dist/commonjs/helpers/memoryManager.js +112 -0
  45. package/dist/commonjs/helpers/telemetry.js +4 -4
  46. package/dist/commonjs/index.d.ts +7 -1
  47. package/dist/commonjs/index.js +36 -2
  48. package/dist/commonjs/network.d.ts +6 -0
  49. package/dist/commonjs/network.js +17 -0
  50. package/dist/commonjs/performance.d.ts +6 -0
  51. package/dist/commonjs/performance.js +33 -0
  52. package/dist/commonjs/redux.d.ts +5 -0
  53. package/dist/commonjs/redux.js +24 -0
  54. package/dist/esm/analytics.d.ts +7 -0
  55. package/dist/esm/analytics.js +6 -0
  56. package/dist/esm/bundle.d.ts +6 -0
  57. package/dist/esm/bundle.js +6 -0
  58. package/dist/esm/console.d.ts +7 -0
  59. package/dist/esm/console.js +6 -0
  60. package/dist/esm/constants/version.d.ts +1 -1
  61. package/dist/esm/constants/version.js +1 -1
  62. package/dist/esm/crash.d.ts +7 -0
  63. package/dist/esm/crash.js +6 -0
  64. package/dist/esm/customHooks/analyticsLogger.d.ts +3 -0
  65. package/dist/esm/customHooks/analyticsLogger.js +19 -1
  66. package/dist/esm/customHooks/consoleLogger.d.ts +3 -0
  67. package/dist/esm/customHooks/consoleLogger.js +20 -3
  68. package/dist/esm/customHooks/crashHandler.d.ts +2 -0
  69. package/dist/esm/customHooks/crashHandler.js +12 -0
  70. package/dist/esm/customHooks/networkLogger.d.ts +3 -0
  71. package/dist/esm/customHooks/networkLogger.js +19 -1
  72. package/dist/esm/customHooks/performanceTracker.d.ts +3 -0
  73. package/dist/esm/customHooks/performanceTracker.js +30 -2
  74. package/dist/esm/customHooks/reduxLogger.d.ts +3 -0
  75. package/dist/esm/customHooks/reduxLogger.js +20 -3
  76. package/dist/esm/helpers/index.d.ts +1 -0
  77. package/dist/esm/helpers/index.js +1 -0
  78. package/dist/esm/helpers/memoryManager.d.ts +29 -0
  79. package/dist/esm/helpers/memoryManager.js +104 -0
  80. package/dist/esm/helpers/telemetry.js +4 -4
  81. package/dist/esm/index.d.ts +7 -1
  82. package/dist/esm/index.js +17 -3
  83. package/dist/esm/network.d.ts +6 -0
  84. package/dist/esm/network.js +5 -0
  85. package/dist/esm/performance.d.ts +6 -0
  86. package/dist/esm/performance.js +6 -0
  87. package/dist/esm/redux.d.ts +5 -0
  88. package/dist/esm/redux.js +5 -0
  89. package/package.json +53 -2
  90. package/src/analytics.ts +35 -0
  91. package/src/bundle.ts +25 -0
  92. package/src/components/AnalyticsDetail.tsx +865 -0
  93. package/src/components/AnalyticsEventCard.tsx +441 -0
  94. package/src/components/AnalyticsGraph.tsx +583 -0
  95. package/src/components/AnimatedEntrance.tsx +64 -0
  96. package/src/components/AppHeaderLogo.tsx +109 -0
  97. package/src/components/BrandCircleIcon.tsx +144 -0
  98. package/src/components/BrandSquareIcon.tsx +144 -0
  99. package/src/components/CodeSnippet.tsx +725 -0
  100. package/src/components/ConsoleLogCard.tsx +628 -0
  101. package/src/components/CopyButton.tsx +87 -0
  102. package/src/components/DiffViewer.tsx +82 -0
  103. package/src/components/DomainHeader.tsx +237 -0
  104. package/src/components/EmptyState.tsx +73 -0
  105. package/src/components/EndOfListFooter.tsx +100 -0
  106. package/src/components/ErrorBoundary.tsx +688 -0
  107. package/src/components/HeadersSection.tsx +192 -0
  108. package/src/components/HighlightText.tsx +100 -0
  109. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  110. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  111. package/src/components/Inspector/BundleTab.tsx +4731 -0
  112. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  113. package/src/components/Inspector/CrashDetail.tsx +941 -0
  114. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  115. package/src/components/Inspector/CrashTab.tsx +871 -0
  116. package/src/components/Inspector/FabLauncher.tsx +80 -0
  117. package/src/components/Inspector/InspectorContext.tsx +28 -0
  118. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  119. package/src/components/Inspector/LogDetail.tsx +1427 -0
  120. package/src/components/Inspector/MainScreen.tsx +258 -0
  121. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  122. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  123. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  124. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  125. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  126. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  127. package/src/components/Inspector/ReduxTab.tsx +954 -0
  128. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  129. package/src/components/Inspector/TabBar.tsx +187 -0
  130. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  131. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  132. package/src/components/JsonViewer.tsx +486 -0
  133. package/src/components/LogCard.tsx +491 -0
  134. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  135. package/src/components/MetaAccordion.tsx +340 -0
  136. package/src/components/MiniBarChart.tsx +42 -0
  137. package/src/components/MiniLineChart.tsx +33 -0
  138. package/src/components/NetworkIcons.tsx +2118 -0
  139. package/src/components/SectionHeader.tsx +113 -0
  140. package/src/components/SegmentedTabs.tsx +83 -0
  141. package/src/components/Slider.tsx +300 -0
  142. package/src/components/SourcePageCard.tsx +148 -0
  143. package/src/components/Toast.tsx +131 -0
  144. package/src/components/TouchableScale.tsx +91 -0
  145. package/src/components/TreeNode.tsx +186 -0
  146. package/src/console.ts +24 -0
  147. package/src/constants/index.ts +38 -0
  148. package/src/constants/version.ts +3 -0
  149. package/src/crash.ts +32 -0
  150. package/src/customHooks/analyticsLogger.ts +336 -0
  151. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  152. package/src/customHooks/consoleLogger.ts +497 -0
  153. package/src/customHooks/crashHandler.ts +944 -0
  154. package/src/customHooks/logFilters.ts +32 -0
  155. package/src/customHooks/networkLogger.ts +419 -0
  156. package/src/customHooks/performanceTracker.ts +1014 -0
  157. package/src/customHooks/reduxLogger.ts +406 -0
  158. package/src/customHooks/useAccordion.tsx +60 -0
  159. package/src/decorators/index.ts +184 -0
  160. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  161. package/src/helpers/index.ts +860 -0
  162. package/src/helpers/memoryManager.ts +138 -0
  163. package/src/helpers/searchQueryParser.ts +283 -0
  164. package/src/helpers/settingsStore.ts +171 -0
  165. package/src/helpers/telemetry.ts +505 -0
  166. package/src/helpers/toast.ts +17 -0
  167. package/src/i18n/index.ts +69 -0
  168. package/src/i18n/locales/en.json +1052 -0
  169. package/src/index.tsx +2336 -0
  170. package/src/native/NativeInspector.ts +397 -0
  171. package/src/native/NativeNetworkInspector.ts +24 -0
  172. package/src/network.ts +22 -0
  173. package/src/performance.ts +38 -0
  174. package/src/redux.ts +23 -0
  175. package/src/styles/AppColors.ts +408 -0
  176. package/src/styles/AppFonts.ts +8 -0
  177. package/src/styles/common.ts +209 -0
  178. package/src/styles/index.ts +1570 -0
  179. package/src/types/enums.ts +194 -0
  180. package/src/types/index.ts +34 -0
  181. 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;