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,491 @@
1
+ import {Method} from '../types';
2
+ import React, {useEffect, useRef} from 'react';
3
+ import {
4
+ Alert,
5
+ Animated,
6
+ Linking,
7
+ View,
8
+ Pressable,
9
+ Text,
10
+ StyleSheet,
11
+ } from 'react-native';
12
+ import Svg, {Path} from 'react-native-svg';
13
+ import {AppColors} from '../styles/AppColors';
14
+ import {METHOD_COLORS} from '../constants';
15
+ import {
16
+ getStatusColor,
17
+ getDurationColor,
18
+ formatDateTime,
19
+ getSize,
20
+ } from '../helpers';
21
+ import {
22
+ CalendarIcon,
23
+ ClockIcon,
24
+ SizeIcon,
25
+ GlobeIcon,
26
+ CircleCheckIcon,
27
+ CircleAlertIcon,
28
+ CircleXIcon,
29
+ RepeatIcon,
30
+ } from './NetworkIcons';
31
+ import {AppFonts} from '../styles/AppFonts';
32
+ import styles from '../styles';
33
+ import {LogCardProps} from '../types';
34
+ import HighlightText from './HighlightText';
35
+ import TouchableScale from './TouchableScale';
36
+ import {useTranslation} from '../i18n';
37
+
38
+ const LogCard = React.memo(function LogCard({
39
+ item,
40
+ onPress,
41
+ timelineMinStart,
42
+ timelineTotalRange,
43
+ isNew,
44
+ isSelected,
45
+ onToggleSelect,
46
+ searchStr,
47
+ }: LogCardProps) {
48
+ const {t} = useTranslation();
49
+ const methodColor = METHOD_COLORS[item.method as Method] ?? METHOD_COLORS.ALL;
50
+
51
+ const handleOpenUrl = (e?: any) => {
52
+ e?.stopPropagation?.();
53
+ Alert.alert(
54
+ t('common.openInBrowser') || 'Open in Browser',
55
+ `${t('common.openInBrowserPrompt') || 'Are you sure you want to open this URL in your external browser?'}\n\n${item.url}`,
56
+ [
57
+ {text: t('common.cancel') || 'Cancel', style: 'cancel'},
58
+ {
59
+ text: t('common.open') || 'Open',
60
+ onPress: () => {
61
+ Linking.canOpenURL(item.url)
62
+ .then(supported => {
63
+ if (supported) {
64
+ Linking.openURL(item.url);
65
+ } else {
66
+ Linking.openURL(item.url).catch(() => {});
67
+ }
68
+ })
69
+ .catch(() => {});
70
+ },
71
+ },
72
+ ],
73
+ );
74
+ };
75
+
76
+ const isFailed =
77
+ item.status === 0 || (item.status != null && item.status >= 400);
78
+ const isLoading = item.status == null;
79
+ const statusColor = getStatusColor(item.status);
80
+ const durationColor = getDurationColor(item.duration);
81
+
82
+ const cardStatusColor = isLoading
83
+ ? AppColors.darkOrange
84
+ : isFailed
85
+ ? AppColors.errorColor
86
+ : AppColors.greenColor;
87
+
88
+ const leftPercent =
89
+ timelineTotalRange > 0
90
+ ? ((item.startTime - timelineMinStart) / timelineTotalRange) * 100
91
+ : 0;
92
+ const widthPercent =
93
+ timelineTotalRange > 0
94
+ ? ((item.duration || 10) / timelineTotalRange) * 100
95
+ : 100;
96
+
97
+ const shimmerOpacity = useRef(new Animated.Value(isNew ? 0.35 : 0)).current;
98
+ useEffect(() => {
99
+ if (isNew) {
100
+ Animated.timing(shimmerOpacity, {
101
+ toValue: 0,
102
+ duration: 1200,
103
+ useNativeDriver: true,
104
+ }).start();
105
+ }
106
+ }, [isNew]);
107
+
108
+ const renderStatusIcon = () => {
109
+ if (isLoading) {
110
+ return <ClockIcon color={AppColors.darkOrange} size={9} />;
111
+ }
112
+ const num = typeof item.status === 'number' ? item.status : parseInt(String(item.status || 0), 10);
113
+ if (num >= 200 && num < 300) {
114
+ return <CircleCheckIcon color={AppColors.greenColor} size={9} />;
115
+ }
116
+ if (num >= 300 && num < 400) {
117
+ return <RepeatIcon color={AppColors.amber700} size={9} />;
118
+ }
119
+ if (num >= 400 && num < 500) {
120
+ return <CircleAlertIcon color={AppColors.darkOrange} size={9} />;
121
+ }
122
+ return <CircleXIcon color={AppColors.errorColor} size={9} />;
123
+ };
124
+
125
+ const triggeredAt = formatDateTime(item.startTime);
126
+ const isJson = item.url.split('?')[0].toLowerCase().endsWith('.json');
127
+
128
+ return (
129
+ <TouchableScale
130
+ onPress={onPress}
131
+ style={[
132
+ styles.card,
133
+ {
134
+ borderLeftWidth: 3.5,
135
+ borderLeftColor: cardStatusColor,
136
+ },
137
+ ]}>
138
+ <View style={styles.cardBody}>
139
+ {/* Row 1: Header (Checkbox, Serial, Method Badge, Client Tag, Status Pill) */}
140
+ <View style={styles.cardHeaderRow}>
141
+ <View style={styles.cardHeaderLeft}>
142
+ <Pressable
143
+ onPress={() => onToggleSelect(item.id)}
144
+ hitSlop={12}
145
+ style={[
146
+ styles.smallCheckbox,
147
+ isSelected && styles.smallCheckboxChecked,
148
+ ]}>
149
+ {isSelected && (
150
+ <Svg width={9} height={9} viewBox="0 0 24 24" fill="none">
151
+ <Path
152
+ d="M20 6L9 17l-5-5"
153
+ stroke={AppColors.white}
154
+ strokeWidth="4"
155
+ strokeLinecap="round"
156
+ strokeLinejoin="round"
157
+ />
158
+ </Svg>
159
+ )}
160
+ </Pressable>
161
+
162
+ <Text style={styles.serialNumber}>#{item.id + 1}</Text>
163
+
164
+ <View
165
+ style={[styles.methodBadge, {backgroundColor: methodColor}]}>
166
+ <Text style={[styles.methodBadgeText, {color: AppColors.white}]}>
167
+ {item.method}
168
+ </Text>
169
+ </View>
170
+
171
+ {item.client && (
172
+ <View
173
+ style={[
174
+ styles.chip,
175
+ {
176
+ backgroundColor:
177
+ item.client === 'axios'
178
+ ? `${AppColors.purple}14`
179
+ : item.client === 'apollo' || item.client === 'graphql'
180
+ ? `${AppColors.pink500}14`
181
+ : item.client === 'xhr'
182
+ ? `${AppColors.amber500}14`
183
+ : `${AppColors.sky500}14`,
184
+ borderColor:
185
+ item.client === 'axios'
186
+ ? `${AppColors.purple}30`
187
+ : item.client === 'apollo' || item.client === 'graphql'
188
+ ? `${AppColors.pink500}30`
189
+ : item.client === 'xhr'
190
+ ? `${AppColors.amber500}30`
191
+ : `${AppColors.sky500}30`,
192
+ paddingHorizontal: 4.5,
193
+ paddingVertical: 1,
194
+ borderRadius: 4,
195
+ },
196
+ ]}>
197
+ <Text
198
+ style={[
199
+ styles.chipText,
200
+ {
201
+ fontFamily: AppFonts.interBold,
202
+ fontSize: 8.5,
203
+ color:
204
+ item.client === 'axios'
205
+ ? AppColors.purple
206
+ : item.client === 'apollo' || item.client === 'graphql'
207
+ ? AppColors.pink500
208
+ : item.client === 'xhr'
209
+ ? AppColors.amber700
210
+ : AppColors.sky600,
211
+ },
212
+ ]}>
213
+ {item.client.toUpperCase()}
214
+ </Text>
215
+ </View>
216
+ )}
217
+
218
+ {/* Protocol / HTTPS Badge */}
219
+ <View
220
+ style={[
221
+ styles.chip,
222
+ {
223
+ backgroundColor: item.url.toLowerCase().startsWith('https')
224
+ ? AppColors.mintGreenBg
225
+ : AppColors.amberWarmBg,
226
+ borderColor: item.url.toLowerCase().startsWith('https')
227
+ ? AppColors.mintGreenBorder
228
+ : AppColors.amberWarmBorder,
229
+ paddingHorizontal: 4,
230
+ paddingVertical: 1,
231
+ borderRadius: 4,
232
+ },
233
+ ]}>
234
+ <Text
235
+ style={[
236
+ styles.chipText,
237
+ {
238
+ fontFamily: AppFonts.interBold,
239
+ fontSize: 8,
240
+ color: item.url.toLowerCase().startsWith('https')
241
+ ? AppColors.mintGreenText
242
+ : AppColors.amber700,
243
+ },
244
+ ]}>
245
+ {item.url.toLowerCase().startsWith('https') ? 'HTTPS' : 'HTTP'}
246
+ </Text>
247
+ </View>
248
+
249
+ {/* GraphQL Indicator */}
250
+ {(item.url.toLowerCase().includes('graphql') ||
251
+ item.client === 'apollo' ||
252
+ item.client === 'graphql') && (
253
+ <View
254
+ style={[
255
+ styles.chip,
256
+ {
257
+ backgroundColor: AppColors.roseBg,
258
+ borderColor: AppColors.roseBorder,
259
+ paddingHorizontal: 4,
260
+ paddingVertical: 1,
261
+ borderRadius: 4,
262
+ },
263
+ ]}>
264
+ <Text
265
+ style={[
266
+ styles.chipText,
267
+ {
268
+ fontFamily: AppFonts.interBold,
269
+ fontSize: 8,
270
+ color: AppColors.pink600,
271
+ },
272
+ ]}>
273
+ GQL
274
+ </Text>
275
+ </View>
276
+ )}
277
+
278
+ {/* In-Line Duration / Latency Pill */}
279
+ {item.duration != null && !isFailed && (
280
+ <View
281
+ style={[
282
+ styles.chip,
283
+ {
284
+ backgroundColor: `${durationColor}12`,
285
+ borderColor: `${durationColor}2E`,
286
+ paddingHorizontal: 4.5,
287
+ paddingVertical: 1,
288
+ borderRadius: 4,
289
+ },
290
+ ]}>
291
+ <Text
292
+ style={[
293
+ styles.chipText,
294
+ {
295
+ fontFamily: AppFonts.interBold,
296
+ fontSize: 8.5,
297
+ color: durationColor,
298
+ },
299
+ ]}>
300
+ {item.duration}ms
301
+ </Text>
302
+ </View>
303
+ )}
304
+ </View>
305
+
306
+ <View
307
+ style={[
308
+ styles.statusPill,
309
+ {
310
+ flexDirection: 'row',
311
+ alignItems: 'center',
312
+ gap: 3.5,
313
+ backgroundColor: isFailed
314
+ ? `${AppColors.errorColor}15`
315
+ : isLoading
316
+ ? `${AppColors.darkOrange}15`
317
+ : `${statusColor}15`,
318
+ borderColor: isFailed
319
+ ? `${AppColors.errorColor}30`
320
+ : isLoading
321
+ ? `${AppColors.darkOrange}30`
322
+ : `${statusColor}30`,
323
+ },
324
+ ]}>
325
+ {renderStatusIcon()}
326
+ <Text
327
+ style={[
328
+ styles.statusPillText,
329
+ {
330
+ color: isFailed
331
+ ? AppColors.errorColor
332
+ : isLoading
333
+ ? AppColors.darkOrange
334
+ : statusColor,
335
+ },
336
+ ]}>
337
+ {isLoading
338
+ ? '...'
339
+ : isFailed
340
+ ? (item.status ? `${item.status}` : t('network.statusFailed'))
341
+ : item.status}
342
+ </Text>
343
+ </View>
344
+ </View>
345
+
346
+ {/* Row 2: Full URL Capsule Container (Clickable with link prompt) */}
347
+ <View style={styles.cardSlugBox}>
348
+ <Pressable
349
+ onPress={handleOpenUrl}
350
+ style={styles.slugLeft}
351
+ hitSlop={6}>
352
+ <View
353
+ style={{
354
+ width: 17,
355
+ height: 17,
356
+ borderRadius: 4,
357
+ backgroundColor: `${AppColors.skyBlue}1A`,
358
+ alignItems: 'center',
359
+ justifyContent: 'center',
360
+ marginRight: 4,
361
+ }}>
362
+ <GlobeIcon color={AppColors.skyBlue} size={11} />
363
+ </View>
364
+ <HighlightText
365
+ text={item.url}
366
+ search={searchStr}
367
+ style={[styles.slugText, {color: AppColors.skyBlue, textDecorationLine: 'underline'}]}
368
+ highlightStyle={styles.highlight}
369
+ numberOfLines={2}
370
+ ellipsizeMode="middle"
371
+ />
372
+ </Pressable>
373
+
374
+ <View style={styles.slugRight}>
375
+ {isJson && (
376
+ <View
377
+ style={[
378
+ styles.chip,
379
+ {
380
+ backgroundColor: `${AppColors.darkOrange}12`,
381
+ borderColor: `${AppColors.darkOrange}28`,
382
+ },
383
+ ]}>
384
+ <Text style={[styles.chipText, {color: AppColors.darkOrange}]}>
385
+ .json
386
+ </Text>
387
+ </View>
388
+ )}
389
+
390
+ {item.duplicateCount != null && item.duplicateCount > 1 && (
391
+ <View
392
+ style={[
393
+ styles.chip,
394
+ {
395
+ backgroundColor: `${AppColors.purple}12`,
396
+ borderColor: `${AppColors.purple}28`,
397
+ },
398
+ ]}>
399
+ <Text
400
+ style={[
401
+ styles.chipText,
402
+ {color: AppColors.purple, fontWeight: '700'},
403
+ ]}>
404
+ ×{item.duplicateCount}
405
+ </Text>
406
+ </View>
407
+ )}
408
+ </View>
409
+ </View>
410
+
411
+ {/* Row 3: Footer (Timestamp on Left, Response Size & Duration on Right) */}
412
+ <View style={styles.cardFooterRow}>
413
+ <View style={styles.cardDateRow}>
414
+ <CalendarIcon color={AppColors.grayTextWeak} size={10} />
415
+ <Text style={styles.cardDateText}>{triggeredAt}</Text>
416
+ </View>
417
+
418
+ <View style={styles.cardFooterRight}>
419
+ {item.response != null && (
420
+ <View
421
+ style={[
422
+ styles.chip,
423
+ {
424
+ backgroundColor: `${AppColors.purple}12`,
425
+ borderColor: `${AppColors.purple}2E`,
426
+ },
427
+ ]}>
428
+ <SizeIcon color={AppColors.purple} size={9} />
429
+ <Text
430
+ style={[
431
+ styles.chipText,
432
+ {color: AppColors.purple, fontSize: 9.5},
433
+ ]}>
434
+ {getSize(item.response)}
435
+ </Text>
436
+ </View>
437
+ )}
438
+
439
+ {item.duration != null && !isFailed && (
440
+ <View
441
+ style={[
442
+ styles.chip,
443
+ {
444
+ backgroundColor: `${durationColor}15`,
445
+ borderColor: `${durationColor}30`,
446
+ },
447
+ ]}>
448
+ <ClockIcon color={durationColor} size={9} />
449
+ <Text style={[styles.chipText, {color: durationColor, fontSize: 9.5}]}>
450
+ {item.duration}ms
451
+ </Text>
452
+ </View>
453
+ )}
454
+ </View>
455
+ </View>
456
+
457
+ {/* Mini Latency Timing Waterfall Bar */}
458
+ {typeof item.duration === 'number' && item.duration > 0 && !isFailed && (
459
+ <View style={{marginTop: 6, gap: 2}}>
460
+ <View
461
+ style={{
462
+ height: 2.5,
463
+ width: '100%',
464
+ backgroundColor: AppColors.grayBorderSecondary,
465
+ borderRadius: 1.5,
466
+ overflow: 'hidden',
467
+ }}>
468
+ <View
469
+ style={{
470
+ height: '100%',
471
+ width: `${Math.min(100, Math.max(6, (item.duration / 1200) * 100))}%`,
472
+ backgroundColor: durationColor,
473
+ borderRadius: 1.5,
474
+ }}
475
+ />
476
+ </View>
477
+ </View>
478
+ )}
479
+ </View>
480
+ <Animated.View
481
+ pointerEvents="none"
482
+ style={[
483
+ StyleSheet.absoluteFill,
484
+ {backgroundColor: methodColor, opacity: shimmerOpacity},
485
+ ]}
486
+ />
487
+ </TouchableScale>
488
+ );
489
+ });
490
+
491
+ export default LogCard;
@@ -0,0 +1,178 @@
1
+ import React from 'react';
2
+ import {StyleSheet, Text, View, TextStyle, Linking} from 'react-native';
3
+ import {AppColors} from '../styles/AppColors';
4
+ import {AppFonts} from '../styles/AppFonts';
5
+ import {escapeRegex, handleOpenExternalLink} from '../helpers';
6
+
7
+ export interface LogSyntaxHighlighterProps {
8
+ text: string;
9
+ search?: string;
10
+ numberOfLines?: number;
11
+ style?: TextStyle | TextStyle[];
12
+ detectLinks?: boolean;
13
+ }
14
+
15
+ interface Token {
16
+ type:
17
+ | 'key'
18
+ | 'string'
19
+ | 'number'
20
+ | 'boolean'
21
+ | 'null'
22
+ | 'url'
23
+ | 'bracket'
24
+ | 'tag'
25
+ | 'plain';
26
+ value: string;
27
+ }
28
+
29
+ const TOKEN_REGEX =
30
+ /("(?:\\.|[^"\\])*"(?=\s*:))|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(\b(?:true|false)\b)|(\b(?:null|undefined|NaN)\b)|(https?:\/\/[^\s"',]+)|(\[[\w\s_-]+\])|([{}[\],:])|([^"'\d\s{}[\],:]+|[ \t\r\n]+)/g;
31
+
32
+ export const tokenizeText = (input: string): Token[] => {
33
+ if (!input) return [];
34
+ const tokens: Token[] = [];
35
+ let match: RegExpExecArray | null;
36
+ TOKEN_REGEX.lastIndex = 0;
37
+
38
+ while ((match = TOKEN_REGEX.exec(input)) !== null) {
39
+ if (match[1]) {
40
+ tokens.push({type: 'key', value: match[1]});
41
+ } else if (match[2]) {
42
+ tokens.push({type: 'string', value: match[2]});
43
+ } else if (match[3]) {
44
+ tokens.push({type: 'number', value: match[3]});
45
+ } else if (match[4]) {
46
+ tokens.push({type: 'boolean', value: match[4]});
47
+ } else if (match[5]) {
48
+ tokens.push({type: 'null', value: match[5]});
49
+ } else if (match[6]) {
50
+ tokens.push({type: 'url', value: match[6]});
51
+ } else if (match[7]) {
52
+ tokens.push({type: 'tag', value: match[7]});
53
+ } else if (match[8]) {
54
+ tokens.push({type: 'bracket', value: match[8]});
55
+ } else if (match[9]) {
56
+ tokens.push({type: 'plain', value: match[9]});
57
+ }
58
+ }
59
+
60
+ return tokens;
61
+ };
62
+
63
+ const getTokenColor = (type: Token['type']): string => {
64
+ switch (type) {
65
+ case 'key':
66
+ return AppColors.sky600 || '#0369a1';
67
+ case 'string':
68
+ return AppColors.emerald600 || '#059669';
69
+ case 'number':
70
+ return AppColors.amber600 || '#d97706';
71
+ case 'boolean':
72
+ return AppColors.purple || '#7c3aed';
73
+ case 'null':
74
+ return AppColors.red600 || '#dc2626';
75
+ case 'url':
76
+ return '#0284c7';
77
+ case 'tag':
78
+ return AppColors.violet600 || '#7c3aed';
79
+ case 'bracket':
80
+ return AppColors.slate500 || '#64748b';
81
+ case 'plain':
82
+ default:
83
+ return AppColors.primaryBlack || '#1e293b';
84
+ }
85
+ };
86
+
87
+ export const LogSyntaxHighlighter: React.FC<LogSyntaxHighlighterProps> = React.memo(
88
+ ({text, search, numberOfLines, style, detectLinks = true}) => {
89
+ if (!text) return null;
90
+
91
+ const tokens = React.useMemo(() => tokenizeText(text), [text]);
92
+
93
+ const renderToken = (token: Token, tokenIdx: number) => {
94
+ const baseColor = getTokenColor(token.type);
95
+ const isUrl = token.type === 'url';
96
+ const isKey = token.type === 'key';
97
+
98
+ const tokenStyle: TextStyle = {
99
+ color: baseColor,
100
+ fontFamily: isKey ? AppFonts.interBold : AppFonts.interRegular,
101
+ textDecorationLine: isUrl ? 'underline' : 'none',
102
+ };
103
+
104
+ if (!search || search.trim().length === 0) {
105
+ return (
106
+ <Text
107
+ key={tokenIdx}
108
+ style={tokenStyle}
109
+ onPress={
110
+ isUrl && detectLinks
111
+ ? () => handleOpenExternalLink(token.value)
112
+ : undefined
113
+ }>
114
+ {token.value}
115
+ </Text>
116
+ );
117
+ }
118
+
119
+ const regex = new RegExp(`(${escapeRegex(search.trim())})`, 'gi');
120
+ const parts = token.value.split(regex);
121
+
122
+ return (
123
+ <Text key={tokenIdx} style={tokenStyle}>
124
+ {parts.map((part, partIdx) => {
125
+ const isMatch =
126
+ part.toLowerCase() === search.trim().toLowerCase();
127
+ return isMatch ? (
128
+ <Text
129
+ key={partIdx}
130
+ style={[
131
+ tokenStyle,
132
+ styles.searchHighlight,
133
+ ]}>
134
+ {part}
135
+ </Text>
136
+ ) : (
137
+ <Text
138
+ key={partIdx}
139
+ style={tokenStyle}
140
+ onPress={
141
+ isUrl && detectLinks
142
+ ? () => handleOpenExternalLink(token.value)
143
+ : undefined
144
+ }>
145
+ {part}
146
+ </Text>
147
+ );
148
+ })}
149
+ </Text>
150
+ );
151
+ };
152
+
153
+ return (
154
+ <Text
155
+ style={[styles.containerText, style]}
156
+ numberOfLines={numberOfLines}>
157
+ {tokens.map(renderToken)}
158
+ </Text>
159
+ );
160
+ },
161
+ );
162
+
163
+ const styles = StyleSheet.create({
164
+ containerText: {
165
+ fontFamily: AppFonts.interRegular,
166
+ fontSize: 12,
167
+ lineHeight: 18,
168
+ color: AppColors.primaryBlack,
169
+ },
170
+ searchHighlight: {
171
+ backgroundColor: '#fef08a',
172
+ color: '#854d0e',
173
+ fontFamily: AppFonts.interBold,
174
+ borderRadius: 2,
175
+ },
176
+ });
177
+
178
+ export default LogSyntaxHighlighter;