react-native-debug-toolkit 3.1.4 → 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 +88 -65
- package/README.zh-CN.md +87 -64
- package/bin/debug-toolkit.js +10 -2
- 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/features/network/NetworkLogTab.js +7 -3
- package/lib/commonjs/features/network/NetworkLogTab.js.map +1 -1
- 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/DaemonClient.js +25 -1
- package/lib/commonjs/utils/DaemonClient.js.map +1 -1
- package/lib/commonjs/utils/debugPreferences.js +2 -1
- package/lib/commonjs/utils/debugPreferences.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +1 -0
- package/lib/commonjs/utils/deviceReport.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/features/network/NetworkLogTab.js +7 -3
- package/lib/module/features/network/NetworkLogTab.js.map +1 -1
- 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/DaemonClient.js +25 -1
- package/lib/module/utils/DaemonClient.js.map +1 -1
- package/lib/module/utils/debugPreferences.js +2 -1
- package/lib/module/utils/debugPreferences.js.map +1 -1
- package/lib/module/utils/deviceReport.js +1 -0
- package/lib/module/utils/deviceReport.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/features/network/NetworkLogTab.d.ts.map +1 -1
- 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/DaemonClient.d.ts +1 -0
- package/lib/typescript/src/utils/DaemonClient.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/lib/typescript/src/utils/deviceReport.d.ts +6 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -1
- package/node/daemon/src/console/console.html +166 -27
- package/node/daemon/src/store.js +45 -6
- package/package.json +15 -3
- 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/features/network/NetworkLogTab.tsx +6 -3
- 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/DaemonClient.ts +26 -1
- package/src/utils/debugPreferences.ts +1 -0
- package/src/utils/deviceReport.ts +8 -1
- 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/node/daemon/src/store.js
CHANGED
|
@@ -28,15 +28,33 @@ function slugPart(value) {
|
|
|
28
28
|
.slice(0, 80) || 'unknown';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function ipTail(ip) {
|
|
32
|
+
if (!ip || typeof ip !== 'string') return '0';
|
|
33
|
+
const parts = ip.split('.');
|
|
34
|
+
return parts.length >= 2 ? parts[parts.length - 1] : slugPart(ip);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isSimulatorIp(ip) {
|
|
38
|
+
return ip === '127.0.0.1' || ip === '::1' || ip === '10.0.2.2' || ip === 'localhost';
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function createDeviceId(report, source) {
|
|
32
42
|
const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
|
|
33
43
|
? report.device
|
|
34
44
|
: {};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const platform = slugPart(device.platform);
|
|
46
|
+
const ip = source && source.ip ? String(source.ip) : '';
|
|
47
|
+
const sim = isSimulatorIp(ip);
|
|
48
|
+
let model = slugPart(device.model);
|
|
49
|
+
if (model === 'unknown' && platform !== 'unknown') {
|
|
50
|
+
model = sim ? 'sim' : 'device';
|
|
51
|
+
}
|
|
52
|
+
const ver = device.appVersion ? slugPart(device.appVersion) : '';
|
|
53
|
+
const tail = sim ? 'sim' : ipTail(ip);
|
|
54
|
+
const parts = [platform, model];
|
|
55
|
+
if (ver && ver !== 'unknown') parts.push(ver);
|
|
56
|
+
parts.push(tail);
|
|
57
|
+
return parts.join('_');
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
function readPersistedDevices(storagePath, maxDevices) {
|
|
@@ -92,6 +110,18 @@ function createMemoryStore(options = {}) {
|
|
|
92
110
|
const deviceId = createDeviceId(report, source);
|
|
93
111
|
const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
|
|
94
112
|
const existing = existingIndex >= 0 ? devices[existingIndex] : null;
|
|
113
|
+
const reportSessionId = report.session ? report.session.id : null;
|
|
114
|
+
if (reportSessionId && report.logs) {
|
|
115
|
+
Object.entries(report.logs).forEach(function(pair) {
|
|
116
|
+
if (!Array.isArray(pair[1])) return;
|
|
117
|
+
report.logs[pair[0]] = pair[1].map(function(entry) {
|
|
118
|
+
if (entry && typeof entry === 'object' && !entry.sessionId) {
|
|
119
|
+
return Object.assign({}, entry, { sessionId: reportSessionId });
|
|
120
|
+
}
|
|
121
|
+
return entry;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
95
125
|
const deviceLog = {
|
|
96
126
|
deviceId,
|
|
97
127
|
firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
|
|
@@ -99,6 +129,7 @@ function createMemoryStore(options = {}) {
|
|
|
99
129
|
receivedAt,
|
|
100
130
|
source,
|
|
101
131
|
device: report.device || null,
|
|
132
|
+
session: report.session || null,
|
|
102
133
|
report,
|
|
103
134
|
logCount: createLogCount(report),
|
|
104
135
|
};
|
|
@@ -124,6 +155,7 @@ function createMemoryStore(options = {}) {
|
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
const deltaLogs = (delta && delta.logs) || {};
|
|
158
|
+
const currentSessionId = deviceLog.session ? deviceLog.session.id : null;
|
|
127
159
|
Object.entries(deltaLogs).forEach(([type, entries]) => {
|
|
128
160
|
if (!Array.isArray(entries)) {
|
|
129
161
|
return;
|
|
@@ -131,7 +163,13 @@ function createMemoryStore(options = {}) {
|
|
|
131
163
|
if (!deviceLog.report.logs[type]) {
|
|
132
164
|
deviceLog.report.logs[type] = [];
|
|
133
165
|
}
|
|
134
|
-
|
|
166
|
+
const tagged = entries.map(function(entry) {
|
|
167
|
+
if (entry && typeof entry === 'object' && currentSessionId && !entry.sessionId) {
|
|
168
|
+
return Object.assign({}, entry, { sessionId: currentSessionId });
|
|
169
|
+
}
|
|
170
|
+
return entry;
|
|
171
|
+
});
|
|
172
|
+
deviceLog.report.logs[type].push(...tagged);
|
|
135
173
|
});
|
|
136
174
|
|
|
137
175
|
deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
|
|
@@ -153,6 +191,7 @@ function createMemoryStore(options = {}) {
|
|
|
153
191
|
receivedAt: deviceLog.receivedAt,
|
|
154
192
|
device: deviceLog.device || null,
|
|
155
193
|
source: deviceLog.source || null,
|
|
194
|
+
session: deviceLog.session || null,
|
|
156
195
|
logCount: deviceLog.logCount,
|
|
157
196
|
}));
|
|
158
197
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "A local-first React Native
|
|
3
|
+
"version": "3.2.0",
|
|
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",
|
|
7
7
|
"types": "lib/typescript/src/index.d.ts",
|
|
@@ -30,8 +30,12 @@
|
|
|
30
30
|
"keywords": [
|
|
31
31
|
"react-native",
|
|
32
32
|
"debug",
|
|
33
|
+
"log-bridge",
|
|
34
|
+
"web-console",
|
|
35
|
+
"ai-debugging",
|
|
33
36
|
"toolkit",
|
|
34
37
|
"http-inspector",
|
|
38
|
+
"mcp",
|
|
35
39
|
"development-tools",
|
|
36
40
|
"floating-panel"
|
|
37
41
|
],
|
|
@@ -48,11 +52,19 @@
|
|
|
48
52
|
"peerDependencies": {
|
|
49
53
|
"@react-native-clipboard/clipboard": ">=1.0.0",
|
|
50
54
|
"react": ">=18.0.0",
|
|
51
|
-
"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"
|
|
52
58
|
},
|
|
53
59
|
"peerDependenciesMeta": {
|
|
54
60
|
"@react-native-clipboard/clipboard": {
|
|
55
61
|
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"react-native-camera-kit": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"expo-camera": {
|
|
67
|
+
"optional": true
|
|
56
68
|
}
|
|
57
69
|
},
|
|
58
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
|
+
});
|