react-native-debug-toolkit 3.1.5 → 3.2.0
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 +12 -5
- package/README.zh-CN.md +12 -5
- package/lib/commonjs/core/initialize.js +5 -3
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +146 -0
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/commonjs/features/devConnect/DevConnectTab.js +426 -0
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -0
- package/lib/commonjs/features/devConnect/cameraKit.js +54 -0
- package/lib/commonjs/features/devConnect/cameraKit.js.map +1 -0
- package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -0
- package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -0
- package/lib/commonjs/features/devConnect/devConnectUtils.js +53 -0
- package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -0
- package/lib/commonjs/features/devConnect/index.js +92 -0
- package/lib/commonjs/features/devConnect/index.js.map +1 -0
- package/lib/commonjs/features/devConnect/platformDetect.js +30 -0
- package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -0
- package/lib/commonjs/features/devConnect/types.js +2 -0
- package/lib/commonjs/features/devConnect/types.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/DebugView.js +1 -0
- package/lib/commonjs/ui/DebugView.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +0 -25
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/utils/debugPreferences.js +2 -1
- package/lib/commonjs/utils/debugPreferences.js.map +1 -1
- package/lib/module/core/initialize.js +5 -3
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectQrScanner.js +141 -0
- package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/module/features/devConnect/DevConnectTab.js +421 -0
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -0
- package/lib/module/features/devConnect/cameraKit.js +49 -0
- package/lib/module/features/devConnect/cameraKit.js.map +1 -0
- package/lib/module/features/devConnect/devConnectPreferences.js +29 -0
- package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -0
- package/lib/module/features/devConnect/devConnectUtils.js +47 -0
- package/lib/module/features/devConnect/devConnectUtils.js.map +1 -0
- package/lib/module/features/devConnect/index.js +52 -0
- package/lib/module/features/devConnect/index.js.map +1 -0
- package/lib/module/features/devConnect/platformDetect.js +26 -0
- package/lib/module/features/devConnect/platformDetect.js.map +1 -0
- package/lib/module/features/devConnect/types.js +2 -0
- package/lib/module/features/devConnect/types.js.map +1 -0
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/DebugView.js +1 -0
- package/lib/module/ui/DebugView.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +1 -26
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/utils/debugPreferences.js +2 -1
- package/lib/module/utils/debugPreferences.js.map +1 -1
- package/lib/typescript/src/core/initialize.d.ts +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +9 -0
- package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts +5 -0
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/cameraKit.d.ts +47 -0
- package/lib/typescript/src/features/devConnect/cameraKit.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +12 -0
- package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/index.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/platformDetect.d.ts +2 -0
- package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -0
- package/lib/typescript/src/features/devConnect/types.d.ts +7 -0
- package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/feature.d.ts +1 -1
- package/lib/typescript/src/types/feature.d.ts.map +1 -1
- package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/utils/debugPreferences.d.ts +1 -0
- package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
- package/node/daemon/src/console/console.html +63 -15
- package/package.json +10 -2
- package/src/core/initialize.ts +7 -1
- package/src/features/devConnect/DevConnectQrScanner.tsx +122 -0
- package/src/features/devConnect/DevConnectTab.tsx +357 -0
- package/src/features/devConnect/cameraKit.ts +93 -0
- package/src/features/devConnect/devConnectPreferences.ts +33 -0
- package/src/features/devConnect/devConnectUtils.ts +59 -0
- package/src/features/devConnect/index.ts +64 -0
- package/src/features/devConnect/platformDetect.ts +26 -0
- package/src/features/devConnect/types.ts +6 -0
- package/src/index.ts +2 -0
- package/src/types/feature.ts +2 -1
- package/src/ui/DebugView.tsx +1 -0
- package/src/ui/panel/DebugPanel.tsx +1 -23
- package/src/utils/debugPreferences.ts +1 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +0 -495
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +0 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +0 -490
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +0 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +0 -8
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +0 -1
- package/src/ui/panel/StreamingSettingsModal.tsx +0 -528
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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",
|
|
@@ -52,11 +52,19 @@
|
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@react-native-clipboard/clipboard": ">=1.0.0",
|
|
54
54
|
"react": ">=18.0.0",
|
|
55
|
-
"react-native": ">=0.72.0"
|
|
55
|
+
"react-native": ">=0.72.0",
|
|
56
|
+
"react-native-camera-kit": ">=18.0.0",
|
|
57
|
+
"expo-camera": ">=15.0.0"
|
|
56
58
|
},
|
|
57
59
|
"peerDependenciesMeta": {
|
|
58
60
|
"@react-native-clipboard/clipboard": {
|
|
59
61
|
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-native-camera-kit": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"expo-camera": {
|
|
67
|
+
"optional": true
|
|
60
68
|
}
|
|
61
69
|
},
|
|
62
70
|
"devDependencies": {
|
package/src/core/initialize.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createTrackFeature } from '../features/track';
|
|
|
11
11
|
import type { TrackFeatureConfig } from '../features/track';
|
|
12
12
|
import { createEnvironmentFeature } from '../features/environment';
|
|
13
13
|
import { createClipboardFeature } from '../features/clipboard';
|
|
14
|
+
import { createDevConnectFeature, restoreDevConnectSettingsToDaemon } from '../features/devConnect';
|
|
14
15
|
import { daemonClient } from '../utils/DaemonClient';
|
|
15
16
|
import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
|
|
16
17
|
|
|
@@ -25,6 +26,7 @@ export interface FeatureConfigs {
|
|
|
25
26
|
track?: boolean | TrackFeatureConfig;
|
|
26
27
|
environment?: Parameters<typeof createEnvironmentFeature>[0];
|
|
27
28
|
clipboard?: boolean;
|
|
29
|
+
devConnect?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface InitializeOptions {
|
|
@@ -43,6 +45,7 @@ const featureRegistry: Record<BuiltInFeatureName, (config?: any) => AnyDebugFeat
|
|
|
43
45
|
track: createTrackFeature,
|
|
44
46
|
environment: createEnvironmentFeature,
|
|
45
47
|
clipboard: createClipboardFeature,
|
|
48
|
+
devConnect: createDevConnectFeature,
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
const DEFAULT_FEATURES: BuiltInFeatureName[] = [
|
|
@@ -52,6 +55,7 @@ const DEFAULT_FEATURES: BuiltInFeatureName[] = [
|
|
|
52
55
|
'zustand',
|
|
53
56
|
'track',
|
|
54
57
|
'clipboard',
|
|
58
|
+
'devConnect',
|
|
55
59
|
];
|
|
56
60
|
|
|
57
61
|
function resolveFeatureConfigs(configs: FeatureConfigs): AnyDebugFeature[] {
|
|
@@ -119,7 +123,9 @@ export function initializeDebugToolkit(
|
|
|
119
123
|
DebugToolkit.hideLauncher();
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
restoreDevConnectSettingsToDaemon()
|
|
127
|
+
.then(() => daemonClient.restore(), () => daemonClient.restore())
|
|
128
|
+
.catch(() => {});
|
|
123
129
|
|
|
124
130
|
return DebugToolkit;
|
|
125
131
|
} catch (error) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { Colors } from '../../ui/theme/colors';
|
|
12
|
+
import {
|
|
13
|
+
getScannerModule,
|
|
14
|
+
type CameraKitReadCodeEvent,
|
|
15
|
+
type ExpoCameraScanResult,
|
|
16
|
+
} from './cameraKit';
|
|
17
|
+
import { parseMetroQrPayload } from './devConnectUtils';
|
|
18
|
+
|
|
19
|
+
interface DevConnectQrScannerProps {
|
|
20
|
+
visible: boolean;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onScanHost: (host: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
|
|
26
|
+
const scannedRef = useRef(false);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const scanner = getScannerModule();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (visible) {
|
|
32
|
+
scannedRef.current = false;
|
|
33
|
+
setError(null);
|
|
34
|
+
}
|
|
35
|
+
}, [visible]);
|
|
36
|
+
|
|
37
|
+
const handleScanned = useCallback((rawValue: string) => {
|
|
38
|
+
if (scannedRef.current) return;
|
|
39
|
+
if (typeof rawValue !== 'string') return;
|
|
40
|
+
|
|
41
|
+
const parsed = parseMetroQrPayload(rawValue);
|
|
42
|
+
if (!parsed) {
|
|
43
|
+
setError('QR code does not contain a supported Metro URL.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
scannedRef.current = true;
|
|
48
|
+
setError(null);
|
|
49
|
+
onScanHost(parsed.computerHost);
|
|
50
|
+
onClose();
|
|
51
|
+
}, [onClose, onScanHost]);
|
|
52
|
+
|
|
53
|
+
const handleCameraKitRead = useCallback((event: CameraKitReadCodeEvent) => {
|
|
54
|
+
handleScanned(event.nativeEvent?.codeStringValue ?? '');
|
|
55
|
+
}, [handleScanned]);
|
|
56
|
+
|
|
57
|
+
const handleExpoScanned = useCallback((result: ExpoCameraScanResult) => {
|
|
58
|
+
handleScanned(result.value ?? '');
|
|
59
|
+
}, [handleScanned]);
|
|
60
|
+
|
|
61
|
+
if (!visible || !scanner) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Modal visible={visible} animationType="slide" onRequestClose={onClose}>
|
|
65
|
+
<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}
|
|
84
|
+
<View style={styles.footer}>
|
|
85
|
+
{error ? <Text style={styles.error}>{error}</Text> : <Text style={styles.hint}>Scan a Metro QR code.</Text>}
|
|
86
|
+
<TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
|
|
87
|
+
<Text style={styles.closeButtonText}>Close</Text>
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
</View>
|
|
90
|
+
<Pressable style={styles.topClose} onPress={onClose}>
|
|
91
|
+
<Text style={styles.topCloseText}>Close</Text>
|
|
92
|
+
</Pressable>
|
|
93
|
+
</View>
|
|
94
|
+
</Modal>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const styles = StyleSheet.create({
|
|
99
|
+
container: { flex: 1, backgroundColor: '#000' },
|
|
100
|
+
camera: { flex: 1 },
|
|
101
|
+
footer: { padding: 16, backgroundColor: Colors.surface },
|
|
102
|
+
hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
|
|
103
|
+
error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
|
|
104
|
+
closeButton: {
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
paddingVertical: 11,
|
|
108
|
+
borderRadius: 10,
|
|
109
|
+
backgroundColor: Colors.primary,
|
|
110
|
+
},
|
|
111
|
+
closeButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
|
112
|
+
topClose: {
|
|
113
|
+
position: 'absolute',
|
|
114
|
+
top: 48,
|
|
115
|
+
right: 16,
|
|
116
|
+
paddingHorizontal: 12,
|
|
117
|
+
paddingVertical: 8,
|
|
118
|
+
borderRadius: 8,
|
|
119
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
120
|
+
},
|
|
121
|
+
topCloseText: { color: '#fff', fontSize: 13, fontWeight: '600' },
|
|
122
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
KeyboardAvoidingView,
|
|
4
|
+
Platform,
|
|
5
|
+
ScrollView,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
View,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
import type { DebugFeatureRenderProps } from '../../types';
|
|
14
|
+
import { Colors } from '../../ui/theme/colors';
|
|
15
|
+
import { copyToComputer } from '../../utils/copyToComputer';
|
|
16
|
+
import {
|
|
17
|
+
buildDeviceDaemonEndpoint,
|
|
18
|
+
daemonClient,
|
|
19
|
+
getDefaultDaemonEndpoint,
|
|
20
|
+
normalizeDaemonSettings,
|
|
21
|
+
type DaemonSettings,
|
|
22
|
+
} from '../../utils/DaemonClient';
|
|
23
|
+
import { buildMetroUrls, normalizeComputerHost } from './devConnectUtils';
|
|
24
|
+
import { saveComputerHost } from './devConnectPreferences';
|
|
25
|
+
import type { DevConnectState } from './types';
|
|
26
|
+
import { DevConnectQrScanner } from './DevConnectQrScanner';
|
|
27
|
+
|
|
28
|
+
const CONNECTION_TIMEOUT_MS = 2000;
|
|
29
|
+
const METRO_PORT = '8081';
|
|
30
|
+
|
|
31
|
+
type SyncUiState = 'idle' | 'checking' | 'connected' | 'retrying' | 'failed' | 'running';
|
|
32
|
+
|
|
33
|
+
export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectState>) {
|
|
34
|
+
const inputRef = useRef<TextInput>(null);
|
|
35
|
+
const [computerHost, setComputerHost] = useState(snapshot.computerHost);
|
|
36
|
+
const [streaming, setStreaming] = useState(snapshot.streaming);
|
|
37
|
+
const [syncState, setSyncState] = useState<SyncUiState>(snapshot.streaming ? 'running' : 'idle');
|
|
38
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
39
|
+
const [sending, setSending] = useState(false);
|
|
40
|
+
const [qrVisible, setQrVisible] = useState(false);
|
|
41
|
+
|
|
42
|
+
const isSim = snapshot.isSimulator;
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
setComputerHost(snapshot.computerHost);
|
|
46
|
+
setStreaming(snapshot.streaming);
|
|
47
|
+
setSyncState(snapshot.streaming ? 'running' : 'idle');
|
|
48
|
+
}, [snapshot.computerHost, snapshot.streaming]);
|
|
49
|
+
|
|
50
|
+
const metroUrls = isSim
|
|
51
|
+
? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
|
|
52
|
+
: buildMetroUrls(computerHost);
|
|
53
|
+
|
|
54
|
+
const handleHostChange = useCallback((value: string) => {
|
|
55
|
+
setComputerHost(value);
|
|
56
|
+
const normalized = normalizeComputerHost(value);
|
|
57
|
+
if (normalized) {
|
|
58
|
+
saveComputerHost(normalized).catch(() => {});
|
|
59
|
+
}
|
|
60
|
+
setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
|
|
61
|
+
setMessage(null);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleQrHost = useCallback((host: string) => {
|
|
65
|
+
setComputerHost(host);
|
|
66
|
+
saveComputerHost(host).catch(() => {});
|
|
67
|
+
setMessage('Computer IP updated from QR code.');
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const validateSettings = useCallback((): boolean => {
|
|
71
|
+
if (!isSim && !normalizeComputerHost(computerHost)) {
|
|
72
|
+
setMessage('Enter your computer IP first.');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}, [computerHost, isSim]);
|
|
77
|
+
|
|
78
|
+
const configureDaemon = useCallback(() => {
|
|
79
|
+
const normalizedHost = isSim ? '' : (normalizeComputerHost(computerHost) ?? '');
|
|
80
|
+
const settings: DaemonSettings = {
|
|
81
|
+
mode: isSim ? 'simulator' : 'device',
|
|
82
|
+
endpoint: '',
|
|
83
|
+
deviceHost: normalizedHost,
|
|
84
|
+
token: '',
|
|
85
|
+
};
|
|
86
|
+
daemonClient.configure(settings);
|
|
87
|
+
const normalized = normalizeDaemonSettings(settings);
|
|
88
|
+
const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(normalizedHost));
|
|
89
|
+
return { ...normalized, endpoint };
|
|
90
|
+
}, [computerHost, isSim]);
|
|
91
|
+
|
|
92
|
+
const toggleLiveSync = useCallback(async () => {
|
|
93
|
+
if (streaming) {
|
|
94
|
+
daemonClient.disconnect();
|
|
95
|
+
daemonClient.setStreamingEnabled(false);
|
|
96
|
+
setStreaming(false);
|
|
97
|
+
setSyncState('idle');
|
|
98
|
+
setMessage(null);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!validateSettings()) return;
|
|
103
|
+
|
|
104
|
+
const daemonOptions = configureDaemon();
|
|
105
|
+
setMessage('Checking desktop connection...');
|
|
106
|
+
setSyncState('checking');
|
|
107
|
+
|
|
108
|
+
const connection = await daemonClient.checkConnection({
|
|
109
|
+
...daemonOptions,
|
|
110
|
+
timeoutMs: CONNECTION_TIMEOUT_MS,
|
|
111
|
+
});
|
|
112
|
+
if (!connection.ok) {
|
|
113
|
+
setStreaming(false);
|
|
114
|
+
setSyncState('failed');
|
|
115
|
+
setMessage('Cannot reach desktop. Try /health in phone browser.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
daemonClient.setStreamingEnabled(true);
|
|
120
|
+
daemonClient.connect({
|
|
121
|
+
...daemonOptions,
|
|
122
|
+
timeoutMs: 3000,
|
|
123
|
+
onStatus: (status) => {
|
|
124
|
+
if (status.state === 'connected') {
|
|
125
|
+
setStreaming(true);
|
|
126
|
+
setSyncState('connected');
|
|
127
|
+
setMessage(null);
|
|
128
|
+
} else if (status.state === 'retrying') {
|
|
129
|
+
setSyncState('retrying');
|
|
130
|
+
setMessage('Desktop not reachable. Retrying...');
|
|
131
|
+
} else if (status.state === 'failed') {
|
|
132
|
+
setStreaming(false);
|
|
133
|
+
setSyncState('failed');
|
|
134
|
+
setMessage(status.reason === 'auth' ? 'Desktop token rejected.' : 'Desktop not reachable after multiple retries.');
|
|
135
|
+
} else {
|
|
136
|
+
setSyncState('checking');
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
setStreaming(true);
|
|
141
|
+
}, [configureDaemon, streaming, validateSettings]);
|
|
142
|
+
|
|
143
|
+
const sendOnce = useCallback(async () => {
|
|
144
|
+
if (!validateSettings()) return;
|
|
145
|
+
|
|
146
|
+
const daemonOptions = configureDaemon();
|
|
147
|
+
setSending(true);
|
|
148
|
+
setMessage('Checking desktop connection...');
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const connection = await daemonClient.checkConnection({
|
|
152
|
+
...daemonOptions,
|
|
153
|
+
timeoutMs: CONNECTION_TIMEOUT_MS,
|
|
154
|
+
});
|
|
155
|
+
if (!connection.ok) {
|
|
156
|
+
setMessage('Cannot reach desktop. Try /health in phone browser.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setMessage('Sending logs...');
|
|
161
|
+
const result = await daemonClient.reportOnce({
|
|
162
|
+
...daemonOptions,
|
|
163
|
+
timeoutMs: 2000,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (result.ok) {
|
|
167
|
+
const totalLogs = Object.values(result.logCount ?? {}).reduce((total, count) => total + count, 0);
|
|
168
|
+
setMessage(`Sent ${totalLogs} logs.`);
|
|
169
|
+
} else {
|
|
170
|
+
setMessage(result.error ? `Send failed: ${result.error}` : 'Send failed.');
|
|
171
|
+
}
|
|
172
|
+
} finally {
|
|
173
|
+
setSending(false);
|
|
174
|
+
}
|
|
175
|
+
}, [configureDaemon, validateSettings]);
|
|
176
|
+
|
|
177
|
+
const copyUrl = useCallback((label: string, url: string) => {
|
|
178
|
+
copyToComputer(url, { label });
|
|
179
|
+
setMessage('Copied to computer output.');
|
|
180
|
+
setTimeout(() => setMessage(null), 1500);
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
|
|
184
|
+
const busy = sending || syncState === 'checking';
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
188
|
+
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
|
|
189
|
+
|
|
190
|
+
{isSim ? (
|
|
191
|
+
<View style={styles.badge}>
|
|
192
|
+
<Text style={styles.badgeText}>Simulator — using localhost</Text>
|
|
193
|
+
</View>
|
|
194
|
+
) : (
|
|
195
|
+
<View style={styles.section}>
|
|
196
|
+
<Text style={styles.label}>Computer IP</Text>
|
|
197
|
+
<View style={styles.inputRow}>
|
|
198
|
+
<TextInput
|
|
199
|
+
ref={inputRef}
|
|
200
|
+
style={styles.input}
|
|
201
|
+
value={computerHost}
|
|
202
|
+
onChangeText={handleHostChange}
|
|
203
|
+
placeholder="192.168.1.10"
|
|
204
|
+
placeholderTextColor={Colors.textLight}
|
|
205
|
+
autoCapitalize="none"
|
|
206
|
+
autoCorrect={false}
|
|
207
|
+
keyboardType="numbers-and-punctuation"
|
|
208
|
+
returnKeyType="done"
|
|
209
|
+
onSubmitEditing={() => inputRef.current?.blur()}
|
|
210
|
+
editable={!streaming}
|
|
211
|
+
/>
|
|
212
|
+
{snapshot.qrAvailable ? (
|
|
213
|
+
<TouchableOpacity style={styles.scanButton} onPress={() => setQrVisible(true)} disabled={streaming} activeOpacity={0.7}>
|
|
214
|
+
<Text style={styles.scanButtonText}>Scan</Text>
|
|
215
|
+
</TouchableOpacity>
|
|
216
|
+
) : null}
|
|
217
|
+
</View>
|
|
218
|
+
</View>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
<View style={styles.actions}>
|
|
222
|
+
<TouchableOpacity
|
|
223
|
+
style={[styles.primaryButton, (!canConnect || busy) && styles.buttonDisabled]}
|
|
224
|
+
onPress={toggleLiveSync}
|
|
225
|
+
disabled={!canConnect || busy}
|
|
226
|
+
activeOpacity={0.75}
|
|
227
|
+
>
|
|
228
|
+
<Text style={styles.primaryButtonText}>
|
|
229
|
+
{streaming ? 'Stop' : busy ? 'Checking...' : 'Live Sync'}
|
|
230
|
+
</Text>
|
|
231
|
+
</TouchableOpacity>
|
|
232
|
+
<TouchableOpacity
|
|
233
|
+
style={[styles.secondaryButton, (!canConnect || busy) && styles.buttonDisabled]}
|
|
234
|
+
onPress={sendOnce}
|
|
235
|
+
disabled={!canConnect || busy}
|
|
236
|
+
activeOpacity={0.75}
|
|
237
|
+
>
|
|
238
|
+
<Text style={styles.secondaryButtonText}>
|
|
239
|
+
{sending ? 'Sending...' : 'Send Once'}
|
|
240
|
+
</Text>
|
|
241
|
+
</TouchableOpacity>
|
|
242
|
+
</View>
|
|
243
|
+
|
|
244
|
+
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
245
|
+
|
|
246
|
+
<View style={styles.section}>
|
|
247
|
+
<Text style={styles.sectionTitle}>Metro Bundler</Text>
|
|
248
|
+
{metroUrls ? (
|
|
249
|
+
<>
|
|
250
|
+
<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>
|
|
255
|
+
</View>
|
|
256
|
+
<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>
|
|
261
|
+
</View>
|
|
262
|
+
</>
|
|
263
|
+
) : (
|
|
264
|
+
<Text style={styles.hint}>Enter a computer IP to show Metro URLs.</Text>
|
|
265
|
+
)}
|
|
266
|
+
</View>
|
|
267
|
+
</ScrollView>
|
|
268
|
+
<DevConnectQrScanner
|
|
269
|
+
visible={qrVisible}
|
|
270
|
+
onClose={() => setQrVisible(false)}
|
|
271
|
+
onScanHost={handleQrHost}
|
|
272
|
+
/>
|
|
273
|
+
</KeyboardAvoidingView>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const styles = StyleSheet.create({
|
|
278
|
+
container: { flex: 1 },
|
|
279
|
+
scrollContent: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 24 },
|
|
280
|
+
badge: {
|
|
281
|
+
paddingHorizontal: 12,
|
|
282
|
+
paddingVertical: 8,
|
|
283
|
+
borderRadius: 8,
|
|
284
|
+
backgroundColor: `${Colors.primary}15`,
|
|
285
|
+
borderWidth: 1,
|
|
286
|
+
borderColor: `${Colors.primary}30`,
|
|
287
|
+
marginBottom: 14,
|
|
288
|
+
},
|
|
289
|
+
badgeText: { fontSize: 13, fontWeight: '500', color: Colors.primary },
|
|
290
|
+
section: { marginBottom: 14 },
|
|
291
|
+
sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 8 },
|
|
292
|
+
label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
|
|
293
|
+
inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
294
|
+
input: {
|
|
295
|
+
flex: 1,
|
|
296
|
+
backgroundColor: Colors.surface,
|
|
297
|
+
borderWidth: 1,
|
|
298
|
+
borderColor: Colors.border,
|
|
299
|
+
borderRadius: 8,
|
|
300
|
+
paddingHorizontal: 12,
|
|
301
|
+
paddingVertical: 10,
|
|
302
|
+
fontSize: 14,
|
|
303
|
+
color: Colors.text,
|
|
304
|
+
fontFamily: 'Courier',
|
|
305
|
+
},
|
|
306
|
+
scanButton: {
|
|
307
|
+
minWidth: 62,
|
|
308
|
+
alignItems: 'center',
|
|
309
|
+
justifyContent: 'center',
|
|
310
|
+
paddingHorizontal: 12,
|
|
311
|
+
paddingVertical: 10,
|
|
312
|
+
borderRadius: 8,
|
|
313
|
+
backgroundColor: Colors.surface,
|
|
314
|
+
borderWidth: 1,
|
|
315
|
+
borderColor: Colors.primary,
|
|
316
|
+
},
|
|
317
|
+
scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
|
|
318
|
+
actions: { flexDirection: 'row', gap: 10, marginTop: 4, marginBottom: 12 },
|
|
319
|
+
primaryButton: {
|
|
320
|
+
flex: 1,
|
|
321
|
+
alignItems: 'center',
|
|
322
|
+
justifyContent: 'center',
|
|
323
|
+
paddingVertical: 11,
|
|
324
|
+
borderRadius: 10,
|
|
325
|
+
backgroundColor: Colors.primary,
|
|
326
|
+
},
|
|
327
|
+
primaryButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
|
328
|
+
secondaryButton: {
|
|
329
|
+
flex: 1,
|
|
330
|
+
alignItems: 'center',
|
|
331
|
+
justifyContent: 'center',
|
|
332
|
+
paddingVertical: 11,
|
|
333
|
+
borderRadius: 10,
|
|
334
|
+
backgroundColor: Colors.surface,
|
|
335
|
+
borderWidth: 1,
|
|
336
|
+
borderColor: Colors.border,
|
|
337
|
+
},
|
|
338
|
+
secondaryButtonText: { color: Colors.primary, fontSize: 14, fontWeight: '600' },
|
|
339
|
+
buttonDisabled: { opacity: 0.5 },
|
|
340
|
+
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',
|
|
345
|
+
backgroundColor: Colors.surface,
|
|
346
|
+
borderWidth: 1,
|
|
347
|
+
borderColor: Colors.border,
|
|
348
|
+
borderRadius: 8,
|
|
349
|
+
paddingLeft: 12,
|
|
350
|
+
paddingRight: 4,
|
|
351
|
+
paddingVertical: 4,
|
|
352
|
+
marginBottom: 8,
|
|
353
|
+
},
|
|
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' },
|
|
357
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
// ---- react-native-camera-kit types ----
|
|
5
|
+
|
|
6
|
+
export interface CameraKitReadCodeEvent {
|
|
7
|
+
nativeEvent?: {
|
|
8
|
+
codeStringValue?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CameraKitCameraProps {
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
cameraType?: unknown;
|
|
15
|
+
scanBarcode?: boolean;
|
|
16
|
+
onReadCode?: (event: CameraKitReadCodeEvent) => void;
|
|
17
|
+
showFrame?: boolean;
|
|
18
|
+
laserColor?: string;
|
|
19
|
+
frameColor?: string;
|
|
20
|
+
allowedBarcodeTypes?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CameraKitModule {
|
|
24
|
+
Camera: ComponentType<CameraKitCameraProps>;
|
|
25
|
+
CameraType?: { Back?: unknown };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---- expo-camera types ----
|
|
29
|
+
|
|
30
|
+
export interface ExpoCameraScanResult {
|
|
31
|
+
boundingBox?: unknown;
|
|
32
|
+
cornerPoints?: unknown;
|
|
33
|
+
type?: string;
|
|
34
|
+
value?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ExpoCameraModule {
|
|
38
|
+
Camera: ComponentType<{
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
onBarCodeScanned?: (result: ExpoCameraScanResult) => void;
|
|
41
|
+
barCodeScannerSettings?: { barCodeTypes: string[] };
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- Unified scanner ----
|
|
46
|
+
|
|
47
|
+
export type ScannerKind = 'camera-kit' | 'expo-camera';
|
|
48
|
+
|
|
49
|
+
export interface ScannerModule {
|
|
50
|
+
kind: ScannerKind;
|
|
51
|
+
CameraKit?: CameraKitModule;
|
|
52
|
+
ExpoCamera?: ExpoCameraModule;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cached: ScannerModule | null | false = false;
|
|
56
|
+
|
|
57
|
+
function tryCameraKit(): ScannerModule | null {
|
|
58
|
+
try {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
60
|
+
const mod = require('react-native-camera-kit') as Partial<CameraKitModule>;
|
|
61
|
+
if (mod.Camera) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'camera-kit',
|
|
64
|
+
CameraKit: { Camera: mod.Camera, CameraType: mod.CameraType },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
} catch { /* not installed */ }
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function tryExpoCamera(): ScannerModule | null {
|
|
72
|
+
try {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
74
|
+
const mod = require('expo-camera') as Partial<ExpoCameraModule>;
|
|
75
|
+
if (mod.Camera) {
|
|
76
|
+
return {
|
|
77
|
+
kind: 'expo-camera',
|
|
78
|
+
ExpoCamera: { Camera: mod.Camera },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
} catch { /* not installed */ }
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getScannerModule(): ScannerModule | null {
|
|
86
|
+
if (cached !== false) return cached;
|
|
87
|
+
cached = tryCameraKit() ?? tryExpoCamera();
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isCameraKitAvailable(): boolean {
|
|
92
|
+
return getScannerModule() !== null;
|
|
93
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { daemonClient } from '../../utils/DaemonClient';
|
|
2
|
+
import { getPreference, KEYS, setPreference } from '../../utils/debugPreferences';
|
|
3
|
+
import { normalizeComputerHost } from './devConnectUtils';
|
|
4
|
+
import { isSimulator } from './platformDetect';
|
|
5
|
+
|
|
6
|
+
export interface DevConnectPreferences {
|
|
7
|
+
computerHost: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadDevConnectPreferences(): Promise<DevConnectPreferences> {
|
|
11
|
+
const storedHost = await getPreference(KEYS.computerHost);
|
|
12
|
+
return {
|
|
13
|
+
computerHost: storedHost ? normalizeComputerHost(storedHost) ?? '' : '',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function saveComputerHost(value: string): Promise<string | null> {
|
|
18
|
+
const normalized = normalizeComputerHost(value);
|
|
19
|
+
if (!normalized) return null;
|
|
20
|
+
await setPreference(KEYS.computerHost, normalized);
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function restoreDevConnectSettingsToDaemon(): Promise<void> {
|
|
25
|
+
const preferences = await loadDevConnectPreferences();
|
|
26
|
+
const mode = isSimulator() ? 'simulator' as const : 'device' as const;
|
|
27
|
+
daemonClient.configure({
|
|
28
|
+
mode,
|
|
29
|
+
endpoint: '',
|
|
30
|
+
deviceHost: mode === 'simulator' ? '' : preferences.computerHost,
|
|
31
|
+
token: '',
|
|
32
|
+
});
|
|
33
|
+
}
|