react-native-inapp-inspector 1.1.28 → 1.1.29

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 (57) hide show
  1. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.java +29 -80
  2. package/dist/commonjs/components/AppHeaderLogo.js +12 -15
  3. package/dist/commonjs/components/ConsoleLogCard.js +31 -7
  4. package/dist/commonjs/components/CopyButton.js +15 -1
  5. package/dist/commonjs/components/DomainHeader.js +56 -28
  6. package/dist/commonjs/components/HighlightText.js +7 -1
  7. package/dist/commonjs/components/Inspector/AnalyticsTab.js +35 -21
  8. package/dist/commonjs/components/Inspector/InspectorHeader.js +17 -3
  9. package/dist/commonjs/components/Inspector/LogDetail.js +8 -5
  10. package/dist/commonjs/components/Inspector/MainScreen.js +4 -0
  11. package/dist/commonjs/components/Inspector/NetworkDetail.js +24 -24
  12. package/dist/commonjs/components/JsonViewer.js +25 -3
  13. package/dist/commonjs/components/LogCard.js +103 -11
  14. package/dist/commonjs/components/Toast.d.ts +3 -0
  15. package/dist/commonjs/components/Toast.js +152 -0
  16. package/dist/commonjs/components/TouchableScale.d.ts +1 -0
  17. package/dist/commonjs/components/TouchableScale.js +3 -3
  18. package/dist/commonjs/components/TreeNode.js +1 -1
  19. package/dist/commonjs/constants/index.js +6 -6
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/helpers/index.d.ts +4 -0
  23. package/dist/commonjs/helpers/index.js +91 -92
  24. package/dist/commonjs/helpers/toast.d.ts +4 -0
  25. package/dist/commonjs/helpers/toast.js +20 -0
  26. package/dist/commonjs/i18n/locales/en.json +4 -1
  27. package/dist/commonjs/styles/index.d.ts +60 -14
  28. package/dist/commonjs/styles/index.js +71 -26
  29. package/dist/esm/components/AppHeaderLogo.js +12 -15
  30. package/dist/esm/components/ConsoleLogCard.js +33 -9
  31. package/dist/esm/components/CopyButton.js +15 -1
  32. package/dist/esm/components/DomainHeader.js +56 -28
  33. package/dist/esm/components/HighlightText.js +7 -1
  34. package/dist/esm/components/Inspector/AnalyticsTab.js +36 -22
  35. package/dist/esm/components/Inspector/InspectorHeader.js +18 -4
  36. package/dist/esm/components/Inspector/LogDetail.js +10 -7
  37. package/dist/esm/components/Inspector/MainScreen.js +4 -0
  38. package/dist/esm/components/Inspector/NetworkDetail.js +25 -25
  39. package/dist/esm/components/JsonViewer.js +25 -3
  40. package/dist/esm/components/LogCard.js +105 -13
  41. package/dist/esm/components/Toast.d.ts +3 -0
  42. package/dist/esm/components/Toast.js +117 -0
  43. package/dist/esm/components/TouchableScale.d.ts +1 -0
  44. package/dist/esm/components/TouchableScale.js +3 -3
  45. package/dist/esm/components/TreeNode.js +1 -1
  46. package/dist/esm/constants/index.js +6 -6
  47. package/dist/esm/constants/version.d.ts +1 -1
  48. package/dist/esm/constants/version.js +1 -1
  49. package/dist/esm/helpers/index.d.ts +4 -0
  50. package/dist/esm/helpers/index.js +86 -92
  51. package/dist/esm/helpers/toast.d.ts +4 -0
  52. package/dist/esm/helpers/toast.js +15 -0
  53. package/dist/esm/i18n/locales/en.json +4 -1
  54. package/dist/esm/styles/index.d.ts +60 -14
  55. package/dist/esm/styles/index.js +71 -26
  56. package/ios/NetworkInspectorModule.m +0 -10
  57. package/package.json +4 -1
@@ -1,9 +1,9 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { Animated, View, Pressable, Text, StyleSheet } from 'react-native';
2
+ import { Alert, Animated, Linking, View, Pressable, Text, StyleSheet, } from 'react-native';
3
3
  import Svg, { Path } from 'react-native-svg';
4
4
  import { AppColors } from '../styles/AppColors';
5
5
  import { METHOD_COLORS } from '../constants';
6
- import { getStatusColor, getDurationColor, getPath, getBaseUrl, formatDateTime, getSize, } from '../helpers';
6
+ import { getStatusColor, getDurationColor, formatDateTime, getSize, } from '../helpers';
7
7
  import { CalendarIcon, ClockIcon, SizeIcon } from './NetworkIcons';
8
8
  import { AppFonts } from '../styles/AppFonts';
9
9
  import styles from '../styles';
@@ -13,6 +13,27 @@ import { useTranslation } from '../i18n';
13
13
  const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, timelineTotalRange, isNew, isSelected, onToggleSelect, searchStr, }) {
14
14
  const { t } = useTranslation();
15
15
  const methodColor = METHOD_COLORS[item.method] ?? METHOD_COLORS.ALL;
16
+ const handleOpenUrl = (e) => {
17
+ e?.stopPropagation?.();
18
+ Alert.alert(t('common.openInBrowser') || 'Open in Browser', `${t('common.openInBrowserPrompt') || 'Are you sure you want to open this URL in your external browser?'}\n\n${item.url}`, [
19
+ { text: t('common.cancel') || 'Cancel', style: 'cancel' },
20
+ {
21
+ text: t('common.open') || 'Open',
22
+ onPress: () => {
23
+ Linking.canOpenURL(item.url)
24
+ .then(supported => {
25
+ if (supported) {
26
+ Linking.openURL(item.url);
27
+ }
28
+ else {
29
+ Linking.openURL(item.url).catch(() => { });
30
+ }
31
+ })
32
+ .catch(() => { });
33
+ },
34
+ },
35
+ ]);
36
+ };
16
37
  const isFailed = item.status === 0 || (item.status != null && item.status >= 400);
17
38
  const isLoading = item.status == null;
18
39
  const statusColor = getStatusColor(item.status);
@@ -38,10 +59,6 @@ const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, t
38
59
  }).start();
39
60
  }
40
61
  }, [isNew]);
41
- const path = getPath(item.url);
42
- const baseUrl = getBaseUrl(item.url) || item.url;
43
- const slug = path && path !== '/' ? path : item.url;
44
- const showSlug = slug !== baseUrl && slug !== item.url;
45
62
  const triggeredAt = formatDateTime(item.startTime);
46
63
  const isJson = item.url.split('?')[0].toLowerCase().endsWith('.json');
47
64
  return (<TouchableScale onPress={onPress} style={[
@@ -52,7 +69,7 @@ const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, t
52
69
  },
53
70
  ]}>
54
71
  <View style={styles.cardBody}>
55
- {/* Row 1: Header (Checkbox, Serial, Method Badge, Base URL / Host with HTTPS, Status Pill) */}
72
+ {/* Row 1: Header (Checkbox, Serial, Method Badge, Client Tag, Status Pill) */}
56
73
  <View style={styles.cardHeaderRow}>
57
74
  <View style={styles.cardHeaderLeft}>
58
75
  <Pressable onPress={() => onToggleSelect(item.id)} hitSlop={12} style={[
@@ -112,7 +129,82 @@ const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, t
112
129
  </Text>
113
130
  </View>)}
114
131
 
115
- <HighlightText text={baseUrl || item.url} search={searchStr} style={styles.cardHostText} highlightStyle={styles.highlight} numberOfLines={1} ellipsizeMode="tail"/>
132
+ {/* Protocol / HTTPS Badge */}
133
+ <View style={[
134
+ styles.chip,
135
+ {
136
+ backgroundColor: item.url.toLowerCase().startsWith('https')
137
+ ? '#ECFDF5'
138
+ : '#FFFBEB',
139
+ borderColor: item.url.toLowerCase().startsWith('https')
140
+ ? '#A7F3D0'
141
+ : '#FDE68A',
142
+ paddingHorizontal: 4,
143
+ paddingVertical: 1,
144
+ borderRadius: 4,
145
+ },
146
+ ]}>
147
+ <Text style={[
148
+ styles.chipText,
149
+ {
150
+ fontFamily: AppFonts.interBold,
151
+ fontSize: 8,
152
+ color: item.url.toLowerCase().startsWith('https')
153
+ ? '#059669'
154
+ : '#D97706',
155
+ },
156
+ ]}>
157
+ {item.url.toLowerCase().startsWith('https') ? 'HTTPS' : 'HTTP'}
158
+ </Text>
159
+ </View>
160
+
161
+ {/* GraphQL Indicator */}
162
+ {(item.url.toLowerCase().includes('graphql') ||
163
+ item.client === 'apollo' ||
164
+ item.client === 'graphql') && (<View style={[
165
+ styles.chip,
166
+ {
167
+ backgroundColor: '#FDF2F8',
168
+ borderColor: '#FBCFE8',
169
+ paddingHorizontal: 4,
170
+ paddingVertical: 1,
171
+ borderRadius: 4,
172
+ },
173
+ ]}>
174
+ <Text style={[
175
+ styles.chipText,
176
+ {
177
+ fontFamily: AppFonts.interBold,
178
+ fontSize: 8,
179
+ color: '#DB2777',
180
+ },
181
+ ]}>
182
+ GQL
183
+ </Text>
184
+ </View>)}
185
+
186
+ {/* In-Line Duration / Latency Pill */}
187
+ {item.duration != null && !isFailed && (<View style={[
188
+ styles.chip,
189
+ {
190
+ backgroundColor: `${durationColor}12`,
191
+ borderColor: `${durationColor}2E`,
192
+ paddingHorizontal: 4.5,
193
+ paddingVertical: 1,
194
+ borderRadius: 4,
195
+ },
196
+ ]}>
197
+ <Text style={[
198
+ styles.chipText,
199
+ {
200
+ fontFamily: AppFonts.interBold,
201
+ fontSize: 8.5,
202
+ color: durationColor,
203
+ },
204
+ ]}>
205
+ {item.duration}ms
206
+ </Text>
207
+ </View>)}
116
208
  </View>
117
209
 
118
210
  <View style={[
@@ -149,12 +241,12 @@ const LogCard = React.memo(function LogCard({ item, onPress, timelineMinStart, t
149
241
  </View>
150
242
  </View>
151
243
 
152
- {/* Row 2: Full URL Capsule Container */}
244
+ {/* Row 2: Full URL Capsule Container (Clickable with link prompt) */}
153
245
  <View style={styles.cardSlugBox}>
154
- <View style={styles.slugLeft}>
155
- <Text style={styles.slugTag}>URL</Text>
156
- <HighlightText text={item.url} search={searchStr} style={styles.slugText} highlightStyle={styles.highlight} numberOfLines={2} ellipsizeMode="middle"/>
157
- </View>
246
+ <Pressable onPress={handleOpenUrl} style={styles.slugLeft} hitSlop={6}>
247
+ <Text style={styles.slugTag}>URL ↗</Text>
248
+ <HighlightText text={item.url} search={searchStr} style={[styles.slugText, { color: AppColors.skyBlue, textDecorationLine: 'underline' }]} highlightStyle={styles.highlight} numberOfLines={2} ellipsizeMode="middle"/>
249
+ </Pressable>
158
250
 
159
251
  <View style={styles.slugRight}>
160
252
  {isJson && (<View style={[
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare const Toast: React.MemoExoticComponent<() => React.JSX.Element>;
3
+ export default Toast;
@@ -0,0 +1,117 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Animated, StyleSheet, Text, View, Platform } from 'react-native';
3
+ import { subscribeToast } from '../helpers/toast';
4
+ import { CheckIcon } from './NetworkIcons';
5
+ import { AppColors } from '../styles/AppColors';
6
+ import { AppFonts } from '../styles/AppFonts';
7
+ const Toast = React.memo(() => {
8
+ const [toastMessage, setToastMessage] = useState(null);
9
+ const opacityAnim = useRef(new Animated.Value(0)).current;
10
+ const translateYAnim = useRef(new Animated.Value(20)).current;
11
+ const hideTimerRef = useRef(null);
12
+ useEffect(() => {
13
+ const unsubscribe = subscribeToast((message) => {
14
+ if (hideTimerRef.current) {
15
+ clearTimeout(hideTimerRef.current);
16
+ }
17
+ setToastMessage(message);
18
+ Animated.parallel([
19
+ Animated.timing(opacityAnim, {
20
+ toValue: 1,
21
+ duration: 180,
22
+ useNativeDriver: true,
23
+ }),
24
+ Animated.spring(translateYAnim, {
25
+ toValue: 0,
26
+ tension: 80,
27
+ friction: 8,
28
+ useNativeDriver: true,
29
+ }),
30
+ ]).start();
31
+ hideTimerRef.current = setTimeout(() => {
32
+ Animated.parallel([
33
+ Animated.timing(opacityAnim, {
34
+ toValue: 0,
35
+ duration: 200,
36
+ useNativeDriver: true,
37
+ }),
38
+ Animated.timing(translateYAnim, {
39
+ toValue: 20,
40
+ duration: 200,
41
+ useNativeDriver: true,
42
+ }),
43
+ ]).start(() => {
44
+ setToastMessage(null);
45
+ });
46
+ }, 2000);
47
+ });
48
+ return () => {
49
+ unsubscribe();
50
+ if (hideTimerRef.current) {
51
+ clearTimeout(hideTimerRef.current);
52
+ }
53
+ };
54
+ }, [opacityAnim, translateYAnim]);
55
+ if (!toastMessage) {
56
+ return null;
57
+ }
58
+ return (<Animated.View pointerEvents="none" style={[
59
+ styles.toastContainer,
60
+ {
61
+ opacity: opacityAnim,
62
+ transform: [{ translateY: translateYAnim }],
63
+ },
64
+ ]}>
65
+ <View style={styles.toastCard}>
66
+ <View style={styles.iconCircle}>
67
+ <CheckIcon color={AppColors.white} size={11}/>
68
+ </View>
69
+ <Text style={styles.toastText} numberOfLines={2}>
70
+ {toastMessage}
71
+ </Text>
72
+ </View>
73
+ </Animated.View>);
74
+ });
75
+ const styles = StyleSheet.create({
76
+ toastContainer: {
77
+ position: 'absolute',
78
+ bottom: Platform.OS === 'ios' ? 44 : 28,
79
+ left: 20,
80
+ right: 20,
81
+ alignItems: 'center',
82
+ justifyContent: 'center',
83
+ zIndex: 999999,
84
+ },
85
+ toastCard: {
86
+ flexDirection: 'row',
87
+ alignItems: 'center',
88
+ backgroundColor: '#0F172AEE', // dark slate glass
89
+ paddingVertical: 10,
90
+ paddingHorizontal: 16,
91
+ borderRadius: 24,
92
+ shadowColor: '#000000',
93
+ shadowOffset: { width: 0, height: 6 },
94
+ shadowOpacity: 0.28,
95
+ shadowRadius: 10,
96
+ elevation: 8,
97
+ borderWidth: 1,
98
+ borderColor: '#33415588',
99
+ maxWidth: '90%',
100
+ gap: 9,
101
+ },
102
+ iconCircle: {
103
+ width: 18,
104
+ height: 18,
105
+ borderRadius: 9,
106
+ backgroundColor: AppColors.greenColor,
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ },
110
+ toastText: {
111
+ color: AppColors.white,
112
+ fontFamily: AppFonts.interBold,
113
+ fontSize: 12.5,
114
+ letterSpacing: -0.1,
115
+ },
116
+ });
117
+ export default Toast;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  declare const TouchableScale: React.NamedExoticComponent<{
3
3
  onPress?: () => void;
4
+ onLongPress?: () => void;
4
5
  style?: any;
5
6
  children?: React.ReactNode;
6
7
  hitSlop?: any;
@@ -1,8 +1,8 @@
1
1
  import React, { useRef } from 'react';
2
2
  import { Animated, Pressable, StyleSheet, Platform } from 'react-native';
3
- const TouchableScale = React.memo(function TouchableScale({ onPress, style, children, hitSlop, disabled, }) {
3
+ const TouchableScale = React.memo(function TouchableScale({ onPress, onLongPress, style, children, hitSlop, disabled, }) {
4
4
  if (Platform.OS === 'android') {
5
- return (<Pressable disabled={disabled} onPress={onPress} hitSlop={hitSlop} style={({ pressed }) => [
5
+ return (<Pressable disabled={disabled} onPress={onPress} onLongPress={onLongPress} hitSlop={hitSlop} style={({ pressed }) => [
6
6
  style,
7
7
  { opacity: pressed ? 0.75 : 1 },
8
8
  ]}>
@@ -38,7 +38,7 @@ const TouchableScale = React.memo(function TouchableScale({ onPress, style, chil
38
38
  flexShrink: flattenedStyle.flexShrink,
39
39
  gap: flattenedStyle.gap,
40
40
  };
41
- return (<Pressable disabled={disabled} style={style} onPressIn={() => animatePress(true)} onPressOut={() => animatePress(false)} onPress={onPress} hitSlop={hitSlop}>
41
+ return (<Pressable disabled={disabled} style={style} onPressIn={() => animatePress(true)} onPressOut={() => animatePress(false)} onPress={onPress} onLongPress={onLongPress} hitSlop={hitSlop}>
42
42
  <Animated.View style={[{ opacity, transform: [{ scale }] }, layoutStyle]}>
43
43
  {children}
44
44
  </Animated.View>
@@ -8,7 +8,7 @@ import { ChevronIcon, DocIcon } from './NetworkIcons';
8
8
  import { AppColors } from '../styles/AppColors';
9
9
  import styles from '../styles';
10
10
  const TreeNode = React.memo(function TreeNode({ data, name, level = 0, search, forceOpen, defaultExpandDepth, }) {
11
- const [localOpen, setLocalOpen] = useState(forceOpen || level < (defaultExpandDepth ?? 1));
11
+ const [localOpen, setLocalOpen] = useState(forceOpen || (defaultExpandDepth !== undefined ? level < defaultExpandDepth : false));
12
12
  const open = localOpen;
13
13
  const isObject = typeof data === 'object' && data !== null;
14
14
  const isArray = Array.isArray(data);
@@ -9,12 +9,12 @@ export const STATUS_FILTERS = [
9
9
  'Failed',
10
10
  ];
11
11
  export const METHOD_COLORS = {
12
- ALL: AppColors.grayText,
13
- GET: AppColors.purple,
14
- POST: AppColors.greenColor,
15
- PUT: AppColors.lightOrange,
16
- PATCH: AppColors.offerPurple,
17
- DELETE: AppColors.errorColor,
12
+ ALL: '#64748B', // Slate
13
+ GET: '#059669', // Emerald
14
+ POST: '#2563EB', // Royal Blue
15
+ PUT: '#D97706', // Amber Gold
16
+ PATCH: '#7C3AED', // Rich Violet
17
+ DELETE: '#DC2626', // Crimson Red
18
18
  };
19
19
  export const DOMAIN_COLORS = AppColors.domainColors;
20
20
  export const DURATION_FAST_MS = 200;
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.1.28";
1
+ export declare const LIB_VERSION = "1.1.29";
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED FILE — do not edit by hand.
2
2
  // Regenerated from package.json on every build by scripts/gen-version.js.
3
- export const LIB_VERSION = '1.1.28';
3
+ export const LIB_VERSION = '1.1.29';
@@ -1,6 +1,7 @@
1
1
  import { NetworkLog, RouteInfo, DiffResult, JsonContent, StackFrameType } from '../types';
2
2
  export declare const getDomainColor: (domain: string) => string;
3
3
  export declare const formatDateTime: (timestamp: number) => string;
4
+ export declare const formatTimestamp: (timestamp: number) => string;
4
5
  export declare const getStatusColor: (status: number | null) => string;
5
6
  export declare const getDurationColor: (duration: number | null) => string;
6
7
  export declare const getSize: (data: unknown) => string;
@@ -44,6 +45,7 @@ export interface ParsedStackFrame {
44
45
  frameType: StackFrameType | 'app' | 'dependency' | 'runtime' | 'native';
45
46
  isUserCode: boolean;
46
47
  isRuntimeNoise: boolean;
48
+ rawFilePath?: string;
47
49
  lineNumber?: string;
48
50
  columnNumber?: string;
49
51
  isOrigin?: boolean;
@@ -51,6 +53,8 @@ export interface ParsedStackFrame {
51
53
  }
52
54
  /** Parses a stack trace line to extract function name, file name, extension (.tsx/.jsx/.ts), line, and column numbers */
53
55
  export declare const parseStackLine: (rawLine: string, isOrigin?: boolean) => ParsedStackFrame;
56
+ /** Opens a file and line number directly in VS Code */
57
+ export declare const openInVSCode: (filePath: string, lineNumber?: string | number, columnNumber?: string | number) => void;
54
58
  export declare const ANALYTICS_EVENT_PALETTE: string[];
55
59
  export declare const getEventColor: (name: string) => string;
56
60
  export { getEventCategory, registerGAPlugin, type GAEventCategory, type GAPlugin, } from './gaAnalyticsRegistry';
@@ -1,4 +1,6 @@
1
- import { Clipboard, Platform, ToastAndroid, Alert, NativeModules, Linking, TurboModuleRegistry, } from 'react-native';
1
+ import { Platform, ToastAndroid, Alert, NativeModules, Linking, } from 'react-native';
2
+ import Clipboard from '@react-native-clipboard/clipboard';
3
+ import { showToast } from './toast';
2
4
  // Stylesheet
3
5
  import { AppColors } from '../styles/AppColors';
4
6
  // Constants
@@ -23,6 +25,19 @@ export const formatDateTime = (timestamp) => {
23
25
  const seconds = pad(date.getSeconds());
24
26
  return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
25
27
  };
28
+ export const formatTimestamp = (timestamp) => {
29
+ try {
30
+ const date = new Date(timestamp);
31
+ const hours = date.getHours().toString().padStart(2, '0');
32
+ const minutes = date.getMinutes().toString().padStart(2, '0');
33
+ const seconds = date.getSeconds().toString().padStart(2, '0');
34
+ const ms = date.getMilliseconds().toString().padStart(3, '0');
35
+ return `${hours}:${minutes}:${seconds}.${ms}`;
36
+ }
37
+ catch {
38
+ return '—';
39
+ }
40
+ };
26
41
  export const getStatusColor = (status) => {
27
42
  if (!status || status === 0)
28
43
  return AppColors.errorColor;
@@ -52,104 +67,43 @@ export const getSize = (data) => {
52
67
  return '—';
53
68
  }
54
69
  };
55
- // Safely detect and load clipboard natively without static require() calls that Metro fails on
56
- const getClipboardModule = () => {
57
- // 1. Check our built-in native module (NetworkInspectorModule)
58
- try {
59
- const tmReg = TurboModuleRegistry || globalThis.__turboModuleProxy;
60
- const inspectorMod = NativeModules?.NetworkInspectorModule ||
61
- (tmReg?.get ? tmReg.get('NetworkInspectorModule') : null);
62
- if (inspectorMod && typeof inspectorMod.copyToClipboard === 'function') {
63
- return {
64
- setString: (str) => inspectorMod.copyToClipboard(str),
65
- setStringAsync: (str) => inspectorMod.copyToClipboard(str),
66
- };
67
- }
70
+ export const copyToClipboard = (value, label) => {
71
+ const resolved = typeof value === 'function' ? value() : value;
72
+ let textToCopy = '';
73
+ if (typeof resolved === 'string') {
74
+ textToCopy = resolved;
68
75
  }
69
- catch { }
70
- // 2. Check TurboModuleRegistry (TurboModules / New Architecture)
71
- try {
72
- const tmReg = TurboModuleRegistry || globalThis.__turboModuleProxy;
73
- if (tmReg?.get) {
74
- const tm = tmReg.get('RNCClipboard') ||
75
- tmReg.get('NativeClipboard') ||
76
- tmReg.get('ExpoClipboard');
77
- if (tm &&
78
- (typeof tm.setString === 'function' ||
79
- typeof tm.setStringAsync === 'function')) {
80
- return tm;
81
- }
76
+ else {
77
+ try {
78
+ textToCopy = JSON.stringify(resolved, null, 2);
79
+ }
80
+ catch {
81
+ textToCopy = String(resolved);
82
82
  }
83
83
  }
84
- catch { }
85
- // 3. Check legacy NativeModules table (Bridge / Old Architecture)
84
+ // Use @react-native-clipboard/clipboard npm package
86
85
  try {
87
- if (NativeModules?.RNCClipboard &&
88
- typeof NativeModules.RNCClipboard.setString === 'function') {
89
- return NativeModules.RNCClipboard;
90
- }
91
- if (NativeModules?.ExpoClipboard &&
92
- typeof NativeModules.ExpoClipboard.setStringAsync === 'function') {
93
- return NativeModules.ExpoClipboard;
86
+ if (typeof Clipboard?.setString === 'function') {
87
+ Clipboard.setString(textToCopy);
94
88
  }
95
- if (NativeModules?.Clipboard &&
96
- typeof NativeModules.Clipboard.setString === 'function') {
97
- return NativeModules.Clipboard;
89
+ else if (typeof Clipboard?.default?.setString === 'function') {
90
+ Clipboard.default.setString(textToCopy);
98
91
  }
99
92
  }
100
- catch { }
101
- // 4. Check legacy React Native core Clipboard
102
- try {
103
- if (Clipboard && typeof Clipboard.setString === 'function') {
104
- return Clipboard;
93
+ catch (err) {
94
+ if (__DEV__) {
95
+ console.warn('[NetworkInspector] Clipboard.setString failed:', err);
105
96
  }
106
97
  }
107
- catch { }
108
- return null;
109
- };
110
- export const copyToClipboard = (value, label) => {
111
- const resolved = typeof value === 'function' ? value() : value;
112
- const text = typeof resolved === 'string' ? resolved : JSON.stringify(resolved, null, 2);
113
- const textToCopy = text ?? '';
114
- let copied = false;
98
+ // Trigger floating in-app bottom toast notification
115
99
  try {
116
- const cb = getClipboardModule();
117
- if (cb) {
118
- if (typeof cb.setStringAsync === 'function') {
119
- cb.setStringAsync(textToCopy);
120
- copied = true;
121
- }
122
- else if (typeof cb.setString === 'function') {
123
- cb.setString(textToCopy);
124
- copied = true;
125
- }
126
- }
100
+ showToast(label ? `${label} copied to clipboard` : 'Copied to clipboard');
127
101
  }
128
102
  catch { }
129
- // Built-in native module direct invocation
130
- if (!copied) {
131
- try {
132
- const inspectorMod = NativeModules?.NetworkInspectorModule;
133
- if (inspectorMod && typeof inspectorMod.copyToClipboard === 'function') {
134
- inspectorMod.copyToClipboard(textToCopy);
135
- copied = true;
136
- }
137
- }
138
- catch { }
139
- }
140
- if (!copied) {
141
- try {
142
- if (typeof navigator !== 'undefined' && navigator?.clipboard?.writeText) {
143
- navigator.clipboard.writeText(textToCopy);
144
- copied = true;
145
- }
146
- }
147
- catch { }
148
- }
149
- // Visual toast feedback on Android if ToastAndroid exists
103
+ // Native Android Toast fallback
150
104
  try {
151
105
  if (Platform.OS === 'android' && ToastAndroid?.show) {
152
- ToastAndroid.show(`${label} copied`, ToastAndroid.SHORT);
106
+ ToastAndroid.show(label ? `${label} copied to clipboard` : 'Copied to clipboard', ToastAndroid.SHORT);
153
107
  }
154
108
  }
155
109
  catch { }
@@ -535,11 +489,16 @@ export const parseStackLine = (rawLine, isOrigin = false) => {
535
489
  (functionName === 'next' && isNative);
536
490
  const isDependency = cleanPath.includes('node_modules') ||
537
491
  cleanPath.includes('react-native/Libraries') ||
538
- (cleanPath.includes('react-native-inapp-inspector') && !cleanPath.includes('/example/'));
492
+ (cleanPath.includes('react-native-inapp-inspector') &&
493
+ !cleanPath.includes('/example/'));
539
494
  const isUserCode = !isNative &&
540
495
  !isInternalBytecode &&
541
496
  !isDependency &&
542
- (fileExt === 'tsx' || fileExt === 'jsx' || fileExt === 'ts' || fileExt === 'js' || !fileName.includes('.bundle'));
497
+ (fileExt === 'tsx' ||
498
+ fileExt === 'jsx' ||
499
+ fileExt === 'ts' ||
500
+ fileExt === 'js' ||
501
+ !fileName.includes('.bundle'));
543
502
  const frameType = isUserCode
544
503
  ? 'app'
545
504
  : isDependency
@@ -547,7 +506,10 @@ export const parseStackLine = (rawLine, isOrigin = false) => {
547
506
  : isNative
548
507
  ? 'native'
549
508
  : 'runtime';
550
- const isRuntimeNoise = isInternalBytecode || isNative || functionName === 'asyncGeneratorStep' || functionName === '_next';
509
+ const isRuntimeNoise = isInternalBytecode ||
510
+ isNative ||
511
+ functionName === 'asyncGeneratorStep' ||
512
+ functionName === '_next';
551
513
  // Format clean relative project path
552
514
  let relativePath = cleanPath;
553
515
  if (relativePath.includes('/example/')) {
@@ -567,6 +529,7 @@ export const parseStackLine = (rawLine, isOrigin = false) => {
567
529
  functionName,
568
530
  fileName,
569
531
  fullPath: relativePath,
532
+ rawFilePath: fullPath,
570
533
  fileExt,
571
534
  frameType,
572
535
  isUserCode,
@@ -577,6 +540,30 @@ export const parseStackLine = (rawLine, isOrigin = false) => {
577
540
  copyableLocation,
578
541
  };
579
542
  };
543
+ /** Opens a file and line number directly in VS Code */
544
+ export const openInVSCode = (filePath, lineNumber, columnNumber) => {
545
+ const line = lineNumber ? `:${lineNumber}` : '';
546
+ const col = columnNumber ? `:${columnNumber}` : '';
547
+ const cleanPath = filePath.replace(/^file:\/\//, '');
548
+ const vscodeUrl = `vscode://file/${cleanPath.replace(/^\/+/, '')}${line}${col}`;
549
+ Linking.canOpenURL(vscodeUrl)
550
+ .then(supported => {
551
+ if (supported) {
552
+ Linking.openURL(vscodeUrl);
553
+ }
554
+ else {
555
+ Linking.openURL(vscodeUrl).catch(() => {
556
+ const insidersUrl = `vscode-insiders://file/${cleanPath.replace(/^\/+/, '')}${line}${col}`;
557
+ Linking.openURL(insidersUrl).catch(() => {
558
+ Alert.alert('Open in VS Code', `Target location:\n${cleanPath}${line}${col}\n\nPlease ensure VS Code is installed.`, [{ text: 'OK' }]);
559
+ });
560
+ });
561
+ }
562
+ })
563
+ .catch(() => {
564
+ Linking.openURL(vscodeUrl).catch(() => { });
565
+ });
566
+ };
580
567
  // ─── Analytics Helpers ────────────────────────────────────────────────────────
581
568
  export const ANALYTICS_EVENT_PALETTE = [
582
569
  AppColors.googleBlue,
@@ -631,7 +618,11 @@ export const getCategoryColors = (category) => {
631
618
  export const getRuntimeDiagnostics = () => {
632
619
  const isHermes = typeof global.HermesInternal !== 'undefined';
633
620
  const isV8 = typeof global._v8runtime !== 'undefined';
634
- const engineType = isHermes ? 'hermes' : isV8 ? 'v8' : 'jsc';
621
+ const engineType = isHermes
622
+ ? 'hermes'
623
+ : isV8
624
+ ? 'v8'
625
+ : 'jsc';
635
626
  const isFabric = typeof global.nativeFabricUIManager !== 'undefined' ||
636
627
  Boolean(global.__turboModuleProxy);
637
628
  const archType = isFabric ? 'fabric' : 'paper';
@@ -641,11 +632,14 @@ export const getRuntimeDiagnostics = () => {
641
632
  const hermesStats = global.HermesInternal?.getInstrumentedStats?.();
642
633
  if (hermesStats?.js_heap_size) {
643
634
  usedHeapMb = Number((hermesStats.js_heap_size / (1024 * 1024)).toFixed(1));
644
- totalAllocMb = Number(((hermesStats.js_allocated_bytes || hermesStats.js_heap_size * 1.6) / (1024 * 1024)).toFixed(1));
635
+ totalAllocMb = Number(((hermesStats.js_allocated_bytes || hermesStats.js_heap_size * 1.6) /
636
+ (1024 * 1024)).toFixed(1));
645
637
  }
646
638
  else if (global.performance?.memory?.usedJSHeapSize) {
647
- usedHeapMb = Number((global.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1));
648
- totalAllocMb = Number((global.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1));
639
+ usedHeapMb = Number((global.performance.memory.usedJSHeapSize /
640
+ (1024 * 1024)).toFixed(1));
641
+ totalAllocMb = Number((global.performance.memory.totalJSHeapSize /
642
+ (1024 * 1024)).toFixed(1));
649
643
  }
650
644
  }
651
645
  catch { }
@@ -0,0 +1,4 @@
1
+ type ToastListener = (message: string) => void;
2
+ export declare const showToast: (message: string) => void;
3
+ export declare const subscribeToast: (listener: ToastListener) => () => void;
4
+ export {};
@@ -0,0 +1,15 @@
1
+ const listeners = new Set();
2
+ export const showToast = (message) => {
3
+ listeners.forEach(fn => {
4
+ try {
5
+ fn(message);
6
+ }
7
+ catch { }
8
+ });
9
+ };
10
+ export const subscribeToast = (listener) => {
11
+ listeners.add(listener);
12
+ return () => {
13
+ listeners.delete(listener);
14
+ };
15
+ };
@@ -15,7 +15,10 @@
15
15
  "expandAll": "Expand All",
16
16
  "viewOnNpm": "View on NPM",
17
17
  "later": "Later",
18
- "source": "SOURCE"
18
+ "source": "SOURCE",
19
+ "open": "Open",
20
+ "openInBrowser": "Open in Browser",
21
+ "openInBrowserPrompt": "Are you sure you want to open this URL in your external browser?"
19
22
  },
20
23
  "header": {
21
24
  "updateAvailableTitle": "Update Available",