react-native-debug-toolkit 3.2.0 → 3.2.2

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 (66) hide show
  1. package/README.md +13 -2
  2. package/README.zh-CN.md +13 -2
  3. package/android/build.gradle +34 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +70 -0
  6. package/android/src/main/java/com/reactnativedebugtoolkit/ReactNativeDebugToolkitPackage.java +25 -0
  7. package/ios/DebugToolkitDevConnect.mm +67 -0
  8. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +94 -26
  9. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -1
  10. package/lib/commonjs/features/devConnect/DevConnectTab.js +261 -75
  11. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
  12. package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -5
  13. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
  14. package/lib/commonjs/features/devConnect/devConnectUtils.js +99 -15
  15. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
  16. package/lib/commonjs/features/devConnect/index.js +39 -2
  17. package/lib/commonjs/features/devConnect/index.js.map +1 -1
  18. package/lib/commonjs/features/devConnect/nativeDevConnect.js +110 -0
  19. package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -0
  20. package/lib/commonjs/features/devConnect/platformDetect.js +7 -11
  21. package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -1
  22. package/lib/commonjs/utils/debugPreferences.js +43 -6
  23. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  24. package/lib/module/features/devConnect/DevConnectQrScanner.js +95 -27
  25. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -1
  26. package/lib/module/features/devConnect/DevConnectTab.js +265 -79
  27. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
  28. package/lib/module/features/devConnect/devConnectPreferences.js +33 -6
  29. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
  30. package/lib/module/features/devConnect/devConnectUtils.js +94 -15
  31. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
  32. package/lib/module/features/devConnect/index.js +11 -3
  33. package/lib/module/features/devConnect/index.js.map +1 -1
  34. package/lib/module/features/devConnect/nativeDevConnect.js +104 -0
  35. package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -0
  36. package/lib/module/features/devConnect/platformDetect.js +8 -12
  37. package/lib/module/features/devConnect/platformDetect.js.map +1 -1
  38. package/lib/module/utils/debugPreferences.js +43 -6
  39. package/lib/module/utils/debugPreferences.js.map +1 -1
  40. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +3 -2
  41. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -1
  42. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
  43. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +6 -0
  44. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
  45. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +18 -1
  46. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
  47. package/lib/typescript/src/features/devConnect/index.d.ts +2 -2
  48. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
  49. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +17 -0
  50. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -0
  51. package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -1
  52. package/lib/typescript/src/features/devConnect/types.d.ts +3 -0
  53. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
  54. package/lib/typescript/src/utils/debugPreferences.d.ts +2 -0
  55. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  56. package/package.json +4 -1
  57. package/react-native-debug-toolkit.podspec +18 -0
  58. package/src/features/devConnect/DevConnectQrScanner.tsx +90 -28
  59. package/src/features/devConnect/DevConnectTab.tsx +257 -55
  60. package/src/features/devConnect/devConnectPreferences.ts +50 -5
  61. package/src/features/devConnect/devConnectUtils.ts +122 -15
  62. package/src/features/devConnect/index.ts +13 -0
  63. package/src/features/devConnect/nativeDevConnect.ts +128 -0
  64. package/src/features/devConnect/platformDetect.ts +8 -13
  65. package/src/features/devConnect/types.ts +3 -0
  66. package/src/utils/debugPreferences.ts +49 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
4
4
  "description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -10,6 +10,9 @@
10
10
  "lib",
11
11
  "bin",
12
12
  "node",
13
+ "ios",
14
+ "android",
15
+ "react-native-debug-toolkit.podspec",
13
16
  "README.md",
14
17
  "LICENSE",
15
18
  "!**/__tests__",
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'react-native-debug-toolkit'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.homepage = package['homepage']
11
+ s.license = package['license']
12
+ s.author = package['author']
13
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
14
+
15
+ s.platforms = { :ios => '12.0' }
16
+ s.source_files = 'ios/**/*.{h,m,mm}'
17
+ s.dependency 'React-Core'
18
+ end
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  Modal,
4
4
  Pressable,
@@ -14,29 +14,68 @@ import {
14
14
  type CameraKitReadCodeEvent,
15
15
  type ExpoCameraScanResult,
16
16
  } from './cameraKit';
17
- import { parseMetroQrPayload } from './devConnectUtils';
17
+ import { parseMetroQrPayload, type ParsedComputerTarget } from './devConnectUtils';
18
+
19
+ // ─── Camera Error Boundary ─────────────────────────────────
20
+
21
+ interface CameraBoundaryProps {
22
+ children: React.ReactNode;
23
+ onCameraError: (msg: string) => void;
24
+ }
25
+
26
+ interface CameraBoundaryState {
27
+ hasError: boolean;
28
+ }
29
+
30
+ class CameraErrorBoundary extends Component<CameraBoundaryProps, CameraBoundaryState> {
31
+ state: CameraBoundaryState = { hasError: false };
32
+
33
+ static getDerivedStateFromError(): CameraBoundaryState {
34
+ return { hasError: true };
35
+ }
36
+
37
+ componentDidCatch(error: Error) {
38
+ console.warn('[DevConnect] Camera error:', error.message);
39
+ this.props.onCameraError(error.message || 'Camera failed to initialize.');
40
+ }
41
+
42
+ render() {
43
+ if (this.state.hasError) {
44
+ return null;
45
+ }
46
+ return this.props.children;
47
+ }
48
+ }
49
+
50
+ // ─── QR Scanner ─────────────────────────────────────────────
18
51
 
19
52
  interface DevConnectQrScannerProps {
20
53
  visible: boolean;
21
54
  onClose: () => void;
22
- onScanHost: (host: string) => void;
55
+ onScanTarget: (target: ParsedComputerTarget) => void;
23
56
  }
24
57
 
25
- export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
58
+ export function DevConnectQrScanner({ visible, onClose, onScanTarget }: DevConnectQrScannerProps) {
26
59
  const scannedRef = useRef(false);
27
60
  const [error, setError] = useState<string | null>(null);
61
+ const [cameraFailed, setCameraFailed] = useState(false);
28
62
  const scanner = getScannerModule();
29
63
 
30
64
  useEffect(() => {
31
65
  if (visible) {
32
66
  scannedRef.current = false;
33
67
  setError(null);
68
+ setCameraFailed(false);
34
69
  }
35
70
  }, [visible]);
36
71
 
37
72
  const handleScanned = useCallback((rawValue: string) => {
38
- if (scannedRef.current) return;
39
- if (typeof rawValue !== 'string') return;
73
+ if (scannedRef.current) {
74
+ return;
75
+ }
76
+ if (typeof rawValue !== 'string') {
77
+ return;
78
+ }
40
79
 
41
80
  const parsed = parseMetroQrPayload(rawValue);
42
81
  if (!parsed) {
@@ -46,9 +85,12 @@ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnect
46
85
 
47
86
  scannedRef.current = true;
48
87
  setError(null);
49
- onScanHost(parsed.computerHost);
88
+ onScanTarget({
89
+ computerHost: parsed.computerHost,
90
+ metroPort: parsed.metroPort,
91
+ });
50
92
  onClose();
51
- }, [onClose, onScanHost]);
93
+ }, [onClose, onScanTarget]);
52
94
 
53
95
  const handleCameraKitRead = useCallback((event: CameraKitReadCodeEvent) => {
54
96
  handleScanned(event.nativeEvent?.codeStringValue ?? '');
@@ -58,31 +100,48 @@ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnect
58
100
  handleScanned(result.value ?? '');
59
101
  }, [handleScanned]);
60
102
 
61
- if (!visible || !scanner) return null;
103
+ const handleCameraError = useCallback((_msg: string) => {
104
+ setCameraFailed(true);
105
+ }, []);
106
+
107
+ if (!visible || !scanner) {
108
+ return null;
109
+ }
62
110
 
63
111
  return (
64
112
  <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
65
113
  <View style={styles.container}>
66
- {scanner.kind === 'camera-kit' && scanner.CameraKit ? (
67
- <scanner.CameraKit.Camera
68
- style={styles.camera}
69
- cameraType={scanner.CameraKit.CameraType?.Back}
70
- scanBarcode
71
- onReadCode={handleCameraKitRead}
72
- showFrame
73
- laserColor={Colors.primary}
74
- frameColor={Colors.primary}
75
- allowedBarcodeTypes={['qr']}
76
- />
77
- ) : scanner.kind === 'expo-camera' && scanner.ExpoCamera ? (
78
- <scanner.ExpoCamera.Camera
79
- style={styles.camera}
80
- onBarCodeScanned={handleExpoScanned}
81
- barCodeScannerSettings={{ barCodeTypes: ['qr'] }}
82
- />
83
- ) : null}
114
+ {!cameraFailed && (
115
+ <CameraErrorBoundary onCameraError={handleCameraError}>
116
+ {scanner.kind === 'camera-kit' && scanner.CameraKit ? (
117
+ <scanner.CameraKit.Camera
118
+ style={styles.camera}
119
+ cameraType={scanner.CameraKit.CameraType?.Back}
120
+ scanBarcode
121
+ onReadCode={handleCameraKitRead}
122
+ showFrame
123
+ laserColor={Colors.primary}
124
+ frameColor={Colors.primary}
125
+ allowedBarcodeTypes={['qr']}
126
+ />
127
+ ) : scanner.kind === 'expo-camera' && scanner.ExpoCamera ? (
128
+ <scanner.ExpoCamera.Camera
129
+ style={styles.camera}
130
+ onBarCodeScanned={handleExpoScanned}
131
+ barCodeScannerSettings={{ barCodeTypes: ['qr'] }}
132
+ />
133
+ ) : null}
134
+ </CameraErrorBoundary>
135
+ )}
136
+ {cameraFailed && (
137
+ <View style={styles.cameraFallback}>
138
+ <Text style={styles.cameraFallbackText}>Camera unavailable.</Text>
139
+ <Text style={styles.cameraFallbackHint}>Please enter computer IP manually.</Text>
140
+ </View>
141
+ )}
84
142
  <View style={styles.footer}>
85
- {error ? <Text style={styles.error}>{error}</Text> : <Text style={styles.hint}>Scan a Metro QR code.</Text>}
143
+ {!cameraFailed && !error && <Text style={styles.hint}>Scan a Metro QR code.</Text>}
144
+ {error && <Text style={styles.error}>{error}</Text>}
86
145
  <TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
87
146
  <Text style={styles.closeButtonText}>Close</Text>
88
147
  </TouchableOpacity>
@@ -98,6 +157,9 @@ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnect
98
157
  const styles = StyleSheet.create({
99
158
  container: { flex: 1, backgroundColor: '#000' },
100
159
  camera: { flex: 1 },
160
+ cameraFallback: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
161
+ cameraFallbackText: { fontSize: 16, color: '#fff', fontWeight: '600', marginBottom: 8 },
162
+ cameraFallbackHint: { fontSize: 13, color: 'rgba(255,255,255,0.6)', textAlign: 'center' },
101
163
  footer: { padding: 16, backgroundColor: Colors.surface },
102
164
  hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
103
165
  error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  KeyboardAvoidingView,
4
4
  Platform,
@@ -12,7 +12,6 @@ import {
12
12
 
13
13
  import type { DebugFeatureRenderProps } from '../../types';
14
14
  import { Colors } from '../../ui/theme/colors';
15
- import { copyToComputer } from '../../utils/copyToComputer';
16
15
  import {
17
16
  buildDeviceDaemonEndpoint,
18
17
  daemonClient,
@@ -20,50 +19,115 @@ import {
20
19
  normalizeDaemonSettings,
21
20
  type DaemonSettings,
22
21
  } from '../../utils/DaemonClient';
23
- import { buildMetroUrls, normalizeComputerHost } from './devConnectUtils';
24
- import { saveComputerHost } from './devConnectPreferences';
22
+ import {
23
+ DEFAULT_DAEMON_PORT,
24
+ DEFAULT_METRO_PORT,
25
+ buildDaemonDeviceHost,
26
+ buildMetroTarget,
27
+ buildMetroUrls,
28
+ normalizeComputerHost,
29
+ normalizePort,
30
+ parseComputerTarget,
31
+ type ParsedComputerTarget,
32
+ } from './devConnectUtils';
33
+ import {
34
+ saveComputerTarget,
35
+ saveDaemonPort,
36
+ saveMetroPort,
37
+ } from './devConnectPreferences';
38
+ import { applyMetroBundle, resetMetroBundle } from './nativeDevConnect';
25
39
  import type { DevConnectState } from './types';
26
40
  import { DevConnectQrScanner } from './DevConnectQrScanner';
27
41
 
28
42
  const CONNECTION_TIMEOUT_MS = 2000;
29
- const METRO_PORT = '8081';
30
43
 
31
44
  type SyncUiState = 'idle' | 'checking' | 'connected' | 'retrying' | 'failed' | 'running';
32
45
 
46
+ function getSimulatorMetroHost(): string {
47
+ return Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
48
+ }
49
+
50
+ function describeMetroFailure(result: { reason: string; error?: string }): string {
51
+ if (result.reason === 'native_unavailable') {
52
+ return 'Native DevConnect not installed. Rebuild app after installing native module.';
53
+ }
54
+ if (result.reason === 'metro_unreachable') {
55
+ return result.error ? `Metro not reachable: ${result.error}` : 'Metro not reachable. Start Metro on that port.';
56
+ }
57
+ if (result.reason === 'fetch_unavailable') {
58
+ return 'Cannot check Metro because fetch is unavailable.';
59
+ }
60
+ if (result.reason === 'invalid_target') {
61
+ return 'Enter a valid computer IP and Metro port.';
62
+ }
63
+ return result.error ? `Metro switch failed: ${result.error}` : 'Metro switch failed.';
64
+ }
65
+
33
66
  export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectState>) {
34
67
  const inputRef = useRef<TextInput>(null);
35
68
  const [computerHost, setComputerHost] = useState(snapshot.computerHost);
69
+ const [metroPort, setMetroPort] = useState(snapshot.metroPort);
70
+ const [daemonPort, setDaemonPort] = useState(snapshot.daemonPort);
36
71
  const [streaming, setStreaming] = useState(snapshot.streaming);
37
72
  const [syncState, setSyncState] = useState<SyncUiState>(snapshot.streaming ? 'running' : 'idle');
38
73
  const [message, setMessage] = useState<string | null>(null);
39
74
  const [sending, setSending] = useState(false);
75
+ const [metroBusy, setMetroBusy] = useState(false);
40
76
  const [qrVisible, setQrVisible] = useState(false);
41
77
 
42
78
  const isSim = snapshot.isSimulator;
43
79
 
44
80
  useEffect(() => {
45
81
  setComputerHost(snapshot.computerHost);
82
+ setMetroPort(snapshot.metroPort);
83
+ setDaemonPort(snapshot.daemonPort);
46
84
  setStreaming(snapshot.streaming);
47
85
  setSyncState(snapshot.streaming ? 'running' : 'idle');
48
- }, [snapshot.computerHost, snapshot.streaming]);
86
+ }, [snapshot.computerHost, snapshot.daemonPort, snapshot.metroPort, snapshot.streaming]);
49
87
 
50
- const metroUrls = isSim
51
- ? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
52
- : buildMetroUrls(computerHost);
88
+ const metroHost = isSim ? getSimulatorMetroHost() : computerHost;
89
+ const metroTarget = useMemo(
90
+ () => buildMetroTarget(metroHost, metroPort),
91
+ [metroHost, metroPort],
92
+ );
93
+ const metroUrls = useMemo(
94
+ () => buildMetroUrls(metroHost, metroPort),
95
+ [metroHost, metroPort],
96
+ );
53
97
 
54
98
  const handleHostChange = useCallback((value: string) => {
55
99
  setComputerHost(value);
56
- const normalized = normalizeComputerHost(value);
57
- if (normalized) {
58
- saveComputerHost(normalized).catch(() => {});
100
+ const target = parseComputerTarget(value);
101
+ if (target) {
102
+ setMetroPort(target.metroPort);
103
+ saveComputerTarget(value).catch(() => {});
59
104
  }
60
105
  setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
61
106
  setMessage(null);
62
107
  }, []);
63
108
 
64
- const handleQrHost = useCallback((host: string) => {
65
- setComputerHost(host);
66
- saveComputerHost(host).catch(() => {});
109
+ const handleMetroPortChange = useCallback((value: string) => {
110
+ setMetroPort(value);
111
+ const normalized = normalizePort(value);
112
+ if (normalized) {
113
+ saveMetroPort(normalized).catch(() => {});
114
+ }
115
+ setMessage(null);
116
+ }, []);
117
+
118
+ const handleDaemonPortChange = useCallback((value: string) => {
119
+ setDaemonPort(value);
120
+ const normalized = normalizePort(value);
121
+ if (normalized) {
122
+ saveDaemonPort(normalized).catch(() => {});
123
+ }
124
+ setMessage(null);
125
+ }, []);
126
+
127
+ const handleQrTarget = useCallback((target: ParsedComputerTarget) => {
128
+ setComputerHost(target.computerHost);
129
+ setMetroPort(target.metroPort);
130
+ saveComputerTarget(`${target.computerHost}:${target.metroPort}`).catch(() => {});
67
131
  setMessage('Computer IP updated from QR code.');
68
132
  }, []);
69
133
 
@@ -72,22 +136,28 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
72
136
  setMessage('Enter your computer IP first.');
73
137
  return false;
74
138
  }
139
+ if (!normalizePort(daemonPort)) {
140
+ setMessage('Enter a valid desktop logs port.');
141
+ return false;
142
+ }
75
143
  return true;
76
- }, [computerHost, isSim]);
144
+ }, [computerHost, daemonPort, isSim]);
77
145
 
78
146
  const configureDaemon = useCallback(() => {
79
147
  const normalizedHost = isSim ? '' : (normalizeComputerHost(computerHost) ?? '');
148
+ const normalizedDaemonPort = normalizePort(daemonPort) ?? DEFAULT_DAEMON_PORT;
149
+ const deviceHost = isSim ? '' : buildDaemonDeviceHost(normalizedHost, normalizedDaemonPort);
80
150
  const settings: DaemonSettings = {
81
151
  mode: isSim ? 'simulator' : 'device',
82
152
  endpoint: '',
83
- deviceHost: normalizedHost,
153
+ deviceHost,
84
154
  token: '',
85
155
  };
86
156
  daemonClient.configure(settings);
87
157
  const normalized = normalizeDaemonSettings(settings);
88
- const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(normalizedHost));
158
+ const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(deviceHost));
89
159
  return { ...normalized, endpoint };
90
- }, [computerHost, isSim]);
160
+ }, [computerHost, daemonPort, isSim]);
91
161
 
92
162
  const toggleLiveSync = useCallback(async () => {
93
163
  if (streaming) {
@@ -99,7 +169,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
99
169
  return;
100
170
  }
101
171
 
102
- if (!validateSettings()) return;
172
+ if (!validateSettings()) {
173
+ return;
174
+ }
103
175
 
104
176
  const daemonOptions = configureDaemon();
105
177
  setMessage('Checking desktop connection...');
@@ -141,7 +213,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
141
213
  }, [configureDaemon, streaming, validateSettings]);
142
214
 
143
215
  const sendOnce = useCallback(async () => {
144
- if (!validateSettings()) return;
216
+ if (!validateSettings()) {
217
+ return;
218
+ }
145
219
 
146
220
  const daemonOptions = configureDaemon();
147
221
  setSending(true);
@@ -174,13 +248,51 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
174
248
  }
175
249
  }, [configureDaemon, validateSettings]);
176
250
 
177
- const copyUrl = useCallback((label: string, url: string) => {
178
- copyToComputer(url, { label });
179
- setMessage('Copied to computer output.');
180
- setTimeout(() => setMessage(null), 1500);
181
- }, []);
251
+ const applyRemoteBundle = useCallback(async () => {
252
+ if (!metroTarget) {
253
+ setMessage('Enter a valid computer IP and Metro port.');
254
+ return;
255
+ }
256
+ if (!snapshot.nativeMetroAvailable) {
257
+ setMessage(describeMetroFailure({ reason: 'native_unavailable' }));
258
+ return;
259
+ }
260
+
261
+ setMetroBusy(true);
262
+ setMessage('Checking Metro...');
263
+ try {
264
+ const result = await applyMetroBundle(metroTarget.host, metroTarget.port);
265
+ if (result.ok) {
266
+ setMessage(`Using Metro at ${result.hostPort}. Reloading...`);
267
+ } else {
268
+ setMessage(describeMetroFailure(result));
269
+ }
270
+ } finally {
271
+ setMetroBusy(false);
272
+ }
273
+ }, [metroTarget, snapshot.nativeMetroAvailable]);
182
274
 
183
- const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
275
+ const resetRemoteBundle = useCallback(async () => {
276
+ if (!snapshot.nativeMetroAvailable) {
277
+ setMessage(describeMetroFailure({ reason: 'native_unavailable' }));
278
+ return;
279
+ }
280
+
281
+ setMetroBusy(true);
282
+ try {
283
+ const result = await resetMetroBundle();
284
+ if (result.ok) {
285
+ setMessage('Metro host reset. Reloading...');
286
+ } else {
287
+ setMessage(describeMetroFailure(result));
288
+ }
289
+ } finally {
290
+ setMetroBusy(false);
291
+ }
292
+ }, [snapshot.nativeMetroAvailable]);
293
+
294
+ const canConnect = isSim || (Boolean(normalizeComputerHost(computerHost)) && Boolean(normalizePort(daemonPort)));
295
+ const canUseMetro = Boolean(metroTarget) && snapshot.nativeMetroAvailable && !metroBusy;
184
296
  const busy = sending || syncState === 'checking';
185
297
 
186
298
  return (
@@ -189,7 +301,7 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
189
301
 
190
302
  {isSim ? (
191
303
  <View style={styles.badge}>
192
- <Text style={styles.badgeText}>Simulator using localhost</Text>
304
+ <Text style={styles.badgeText}>Simulator/emulator - using {getSimulatorMetroHost()}</Text>
193
305
  </View>
194
306
  ) : (
195
307
  <View style={styles.section}>
@@ -218,6 +330,40 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
218
330
  </View>
219
331
  )}
220
332
 
333
+ <View style={styles.section}>
334
+ <Text style={styles.label}>Ports</Text>
335
+ <View style={styles.portRow}>
336
+ <View style={styles.portField}>
337
+ <Text style={styles.portLabel}>Metro</Text>
338
+ <TextInput
339
+ style={styles.portInput}
340
+ value={metroPort}
341
+ onChangeText={handleMetroPortChange}
342
+ placeholder={DEFAULT_METRO_PORT}
343
+ placeholderTextColor={Colors.textLight}
344
+ autoCapitalize="none"
345
+ autoCorrect={false}
346
+ keyboardType="number-pad"
347
+ returnKeyType="done"
348
+ />
349
+ </View>
350
+ <View style={styles.portField}>
351
+ <Text style={styles.portLabel}>Logs</Text>
352
+ <TextInput
353
+ style={styles.portInput}
354
+ value={daemonPort}
355
+ onChangeText={handleDaemonPortChange}
356
+ placeholder={DEFAULT_DAEMON_PORT}
357
+ placeholderTextColor={Colors.textLight}
358
+ autoCapitalize="none"
359
+ autoCorrect={false}
360
+ keyboardType="number-pad"
361
+ returnKeyType="done"
362
+ />
363
+ </View>
364
+ </View>
365
+ </View>
366
+
221
367
  <View style={styles.actions}>
222
368
  <TouchableOpacity
223
369
  style={[styles.primaryButton, (!canConnect || busy) && styles.buttonDisabled]}
@@ -244,31 +390,58 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
244
390
  {message ? <Text style={styles.message}>{message}</Text> : null}
245
391
 
246
392
  <View style={styles.section}>
247
- <Text style={styles.sectionTitle}>Metro Bundler</Text>
248
- {metroUrls ? (
249
- <>
393
+ <Text style={styles.sectionTitle}>Remote JS Bundle</Text>
394
+ <Text style={styles.sectionDesc}>
395
+ Apply this Metro host to React Native dev settings and reload the app.
396
+ </Text>
397
+
398
+ {!metroUrls ? (
399
+ <View style={styles.stepCard}>
400
+ <Text style={styles.stepHint}>Enter your computer IP and Metro port to get started.</Text>
401
+ </View>
402
+ ) : (
403
+ <View style={styles.stepCard}>
250
404
  <View style={styles.urlRow}>
251
- <Text style={styles.urlText} numberOfLines={1}>{metroUrls.expUrl}</Text>
252
- <TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Metro exp URL', metroUrls.expUrl)} activeOpacity={0.7}>
253
- <Text style={styles.copyButtonText}>Copy</Text>
254
- </TouchableOpacity>
405
+ <Text style={styles.urlLabel}>HTTP</Text>
406
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.httpUrl}</Text>
255
407
  </View>
256
408
  <View style={styles.urlRow}>
257
- <Text style={styles.urlText} numberOfLines={1}>{metroUrls.httpUrl}</Text>
258
- <TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Metro HTTP URL', metroUrls.httpUrl)} activeOpacity={0.7}>
259
- <Text style={styles.copyButtonText}>Copy</Text>
260
- </TouchableOpacity>
409
+ <Text style={styles.urlLabel}>Expo</Text>
410
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.expUrl}</Text>
261
411
  </View>
262
- </>
263
- ) : (
264
- <Text style={styles.hint}>Enter a computer IP to show Metro URLs.</Text>
412
+ </View>
265
413
  )}
414
+
415
+ <View style={styles.actions}>
416
+ <TouchableOpacity
417
+ style={[styles.primaryButton, !canUseMetro && styles.buttonDisabled]}
418
+ onPress={applyRemoteBundle}
419
+ disabled={!canUseMetro}
420
+ activeOpacity={0.75}
421
+ >
422
+ <Text style={styles.primaryButtonText}>
423
+ {metroBusy ? 'Checking...' : 'Use Metro Bundle'}
424
+ </Text>
425
+ </TouchableOpacity>
426
+ <TouchableOpacity
427
+ style={[styles.secondaryButton, (!snapshot.nativeMetroAvailable || metroBusy) && styles.buttonDisabled]}
428
+ onPress={resetRemoteBundle}
429
+ disabled={!snapshot.nativeMetroAvailable || metroBusy}
430
+ activeOpacity={0.75}
431
+ >
432
+ <Text style={styles.secondaryButtonText}>Reset</Text>
433
+ </TouchableOpacity>
434
+ </View>
435
+
436
+ {!snapshot.nativeMetroAvailable ? (
437
+ <Text style={styles.hint}>Native DevConnect requires pod install / Gradle sync and app rebuild.</Text>
438
+ ) : null}
266
439
  </View>
267
440
  </ScrollView>
268
441
  <DevConnectQrScanner
269
442
  visible={qrVisible}
270
443
  onClose={() => setQrVisible(false)}
271
- onScanHost={handleQrHost}
444
+ onScanTarget={handleQrTarget}
272
445
  />
273
446
  </KeyboardAvoidingView>
274
447
  );
@@ -288,7 +461,8 @@ const styles = StyleSheet.create({
288
461
  },
289
462
  badgeText: { fontSize: 13, fontWeight: '500', color: Colors.primary },
290
463
  section: { marginBottom: 14 },
291
- sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 8 },
464
+ sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 4 },
465
+ sectionDesc: { fontSize: 12, color: Colors.textSecondary, marginBottom: 10, lineHeight: 17 },
292
466
  label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
293
467
  inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
294
468
  input: {
@@ -315,6 +489,20 @@ const styles = StyleSheet.create({
315
489
  borderColor: Colors.primary,
316
490
  },
317
491
  scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
492
+ portRow: { flexDirection: 'row', gap: 10 },
493
+ portField: { flex: 1 },
494
+ portLabel: { fontSize: 11, color: Colors.textSecondary, marginBottom: 4 },
495
+ portInput: {
496
+ backgroundColor: Colors.surface,
497
+ borderWidth: 1,
498
+ borderColor: Colors.border,
499
+ borderRadius: 8,
500
+ paddingHorizontal: 12,
501
+ paddingVertical: 9,
502
+ fontSize: 13,
503
+ color: Colors.text,
504
+ fontFamily: 'Courier',
505
+ },
318
506
  actions: { flexDirection: 'row', gap: 10, marginTop: 4, marginBottom: 12 },
319
507
  primaryButton: {
320
508
  flex: 1,
@@ -338,20 +526,34 @@ const styles = StyleSheet.create({
338
526
  secondaryButtonText: { color: Colors.primary, fontSize: 14, fontWeight: '600' },
339
527
  buttonDisabled: { opacity: 0.5 },
340
528
  message: { fontSize: 12, lineHeight: 17, color: Colors.textSecondary, marginBottom: 12 },
341
- hint: { fontSize: 12, color: Colors.textLight },
342
- urlRow: {
343
- flexDirection: 'row',
344
- alignItems: 'center',
529
+ hint: { fontSize: 12, color: Colors.textLight, lineHeight: 17 },
530
+ stepCard: {
345
531
  backgroundColor: Colors.surface,
346
532
  borderWidth: 1,
347
533
  borderColor: Colors.border,
348
- borderRadius: 8,
349
- paddingLeft: 12,
350
- paddingRight: 4,
351
- paddingVertical: 4,
534
+ borderRadius: 10,
535
+ padding: 12,
352
536
  marginBottom: 8,
353
537
  },
354
- urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text, paddingVertical: 6 },
355
- copyButton: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 6, backgroundColor: Colors.primary },
356
- copyButtonText: { color: '#fff', fontSize: 12, fontWeight: '600' },
538
+ stepHint: { fontSize: 12, color: Colors.textSecondary, lineHeight: 17 },
539
+ urlLabel: {
540
+ minWidth: 40,
541
+ fontSize: 10,
542
+ fontWeight: '600',
543
+ color: Colors.primary,
544
+ backgroundColor: `${Colors.primary}15`,
545
+ paddingHorizontal: 6,
546
+ paddingVertical: 2,
547
+ borderRadius: 4,
548
+ marginRight: 8,
549
+ textAlign: 'center',
550
+ },
551
+ urlRow: {
552
+ flexDirection: 'row',
553
+ alignItems: 'center',
554
+ borderBottomWidth: StyleSheet.hairlineWidth,
555
+ borderBottomColor: Colors.border,
556
+ paddingVertical: 7,
557
+ },
558
+ urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text },
357
559
  });