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.
- package/README.md +13 -2
- package/README.zh-CN.md +13 -2
- package/android/build.gradle +34 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +70 -0
- package/android/src/main/java/com/reactnativedebugtoolkit/ReactNativeDebugToolkitPackage.java +25 -0
- package/ios/DebugToolkitDevConnect.mm +67 -0
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +94 -26
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -1
- package/lib/commonjs/features/devConnect/DevConnectTab.js +261 -75
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -5
- package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
- package/lib/commonjs/features/devConnect/devConnectUtils.js +99 -15
- package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
- package/lib/commonjs/features/devConnect/index.js +39 -2
- package/lib/commonjs/features/devConnect/index.js.map +1 -1
- package/lib/commonjs/features/devConnect/nativeDevConnect.js +110 -0
- package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -0
- package/lib/commonjs/features/devConnect/platformDetect.js +7 -11
- package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -1
- package/lib/commonjs/utils/debugPreferences.js +43 -6
- package/lib/commonjs/utils/debugPreferences.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectQrScanner.js +95 -27
- package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectTab.js +265 -79
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/module/features/devConnect/devConnectPreferences.js +33 -6
- package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
- package/lib/module/features/devConnect/devConnectUtils.js +94 -15
- package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
- package/lib/module/features/devConnect/index.js +11 -3
- package/lib/module/features/devConnect/index.js.map +1 -1
- package/lib/module/features/devConnect/nativeDevConnect.js +104 -0
- package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -0
- package/lib/module/features/devConnect/platformDetect.js +8 -12
- package/lib/module/features/devConnect/platformDetect.js.map +1 -1
- package/lib/module/utils/debugPreferences.js +43 -6
- package/lib/module/utils/debugPreferences.js.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +3 -2
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +6 -0
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +18 -1
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/index.d.ts +2 -2
- package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +17 -0
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/types.d.ts +3 -0
- package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/debugPreferences.d.ts +2 -0
- package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
- package/package.json +4 -1
- package/react-native-debug-toolkit.podspec +18 -0
- package/src/features/devConnect/DevConnectQrScanner.tsx +90 -28
- package/src/features/devConnect/DevConnectTab.tsx +257 -55
- package/src/features/devConnect/devConnectPreferences.ts +50 -5
- package/src/features/devConnect/devConnectUtils.ts +122 -15
- package/src/features/devConnect/index.ts +13 -0
- package/src/features/devConnect/nativeDevConnect.ts +128 -0
- package/src/features/devConnect/platformDetect.ts +8 -13
- package/src/features/devConnect/types.ts +3 -0
- 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.
|
|
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
|
-
|
|
55
|
+
onScanTarget: (target: ParsedComputerTarget) => void;
|
|
23
56
|
}
|
|
24
57
|
|
|
25
|
-
export function DevConnectQrScanner({ visible, onClose,
|
|
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)
|
|
39
|
-
|
|
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
|
-
|
|
88
|
+
onScanTarget({
|
|
89
|
+
computerHost: parsed.computerHost,
|
|
90
|
+
metroPort: parsed.metroPort,
|
|
91
|
+
});
|
|
50
92
|
onClose();
|
|
51
|
-
}, [onClose,
|
|
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
|
-
|
|
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
|
-
{
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
{
|
|
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 {
|
|
24
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
57
|
-
if (
|
|
58
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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(
|
|
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())
|
|
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())
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
|
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}>
|
|
248
|
-
{
|
|
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.
|
|
252
|
-
<
|
|
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.
|
|
258
|
-
<
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
349
|
-
|
|
350
|
-
paddingRight: 4,
|
|
351
|
-
paddingVertical: 4,
|
|
534
|
+
borderRadius: 10,
|
|
535
|
+
padding: 12,
|
|
352
536
|
marginBottom: 8,
|
|
353
537
|
},
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
});
|