react-native-debug-toolkit 3.1.5 → 3.2.1
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 +203 -0
- package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/commonjs/features/devConnect/DevConnectTab.js +541 -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 +198 -0
- package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -0
- package/lib/module/features/devConnect/DevConnectTab.js +536 -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 +173 -0
- package/src/features/devConnect/DevConnectTab.tsx +437 -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
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, 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 = useMemo(
|
|
51
|
+
() => isSim
|
|
52
|
+
? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
|
|
53
|
+
: buildMetroUrls(computerHost),
|
|
54
|
+
[isSim, computerHost],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const handleHostChange = useCallback((value: string) => {
|
|
58
|
+
setComputerHost(value);
|
|
59
|
+
const normalized = normalizeComputerHost(value);
|
|
60
|
+
if (normalized) {
|
|
61
|
+
saveComputerHost(normalized).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
|
|
64
|
+
setMessage(null);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const handleQrHost = useCallback((host: string) => {
|
|
68
|
+
setComputerHost(host);
|
|
69
|
+
saveComputerHost(host).catch(() => {});
|
|
70
|
+
setMessage('Computer IP updated from QR code.');
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const validateSettings = useCallback((): boolean => {
|
|
74
|
+
if (!isSim && !normalizeComputerHost(computerHost)) {
|
|
75
|
+
setMessage('Enter your computer IP first.');
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}, [computerHost, isSim]);
|
|
80
|
+
|
|
81
|
+
const configureDaemon = useCallback(() => {
|
|
82
|
+
const normalizedHost = isSim ? '' : (normalizeComputerHost(computerHost) ?? '');
|
|
83
|
+
const settings: DaemonSettings = {
|
|
84
|
+
mode: isSim ? 'simulator' : 'device',
|
|
85
|
+
endpoint: '',
|
|
86
|
+
deviceHost: normalizedHost,
|
|
87
|
+
token: '',
|
|
88
|
+
};
|
|
89
|
+
daemonClient.configure(settings);
|
|
90
|
+
const normalized = normalizeDaemonSettings(settings);
|
|
91
|
+
const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(normalizedHost));
|
|
92
|
+
return { ...normalized, endpoint };
|
|
93
|
+
}, [computerHost, isSim]);
|
|
94
|
+
|
|
95
|
+
const toggleLiveSync = useCallback(async () => {
|
|
96
|
+
if (streaming) {
|
|
97
|
+
daemonClient.disconnect();
|
|
98
|
+
daemonClient.setStreamingEnabled(false);
|
|
99
|
+
setStreaming(false);
|
|
100
|
+
setSyncState('idle');
|
|
101
|
+
setMessage(null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!validateSettings()) return;
|
|
106
|
+
|
|
107
|
+
const daemonOptions = configureDaemon();
|
|
108
|
+
setMessage('Checking desktop connection...');
|
|
109
|
+
setSyncState('checking');
|
|
110
|
+
|
|
111
|
+
const connection = await daemonClient.checkConnection({
|
|
112
|
+
...daemonOptions,
|
|
113
|
+
timeoutMs: CONNECTION_TIMEOUT_MS,
|
|
114
|
+
});
|
|
115
|
+
if (!connection.ok) {
|
|
116
|
+
setStreaming(false);
|
|
117
|
+
setSyncState('failed');
|
|
118
|
+
setMessage('Cannot reach desktop. Try /health in phone browser.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
daemonClient.setStreamingEnabled(true);
|
|
123
|
+
daemonClient.connect({
|
|
124
|
+
...daemonOptions,
|
|
125
|
+
timeoutMs: 3000,
|
|
126
|
+
onStatus: (status) => {
|
|
127
|
+
if (status.state === 'connected') {
|
|
128
|
+
setStreaming(true);
|
|
129
|
+
setSyncState('connected');
|
|
130
|
+
setMessage(null);
|
|
131
|
+
} else if (status.state === 'retrying') {
|
|
132
|
+
setSyncState('retrying');
|
|
133
|
+
setMessage('Desktop not reachable. Retrying...');
|
|
134
|
+
} else if (status.state === 'failed') {
|
|
135
|
+
setStreaming(false);
|
|
136
|
+
setSyncState('failed');
|
|
137
|
+
setMessage(status.reason === 'auth' ? 'Desktop token rejected.' : 'Desktop not reachable after multiple retries.');
|
|
138
|
+
} else {
|
|
139
|
+
setSyncState('checking');
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
setStreaming(true);
|
|
144
|
+
}, [configureDaemon, streaming, validateSettings]);
|
|
145
|
+
|
|
146
|
+
const sendOnce = useCallback(async () => {
|
|
147
|
+
if (!validateSettings()) return;
|
|
148
|
+
|
|
149
|
+
const daemonOptions = configureDaemon();
|
|
150
|
+
setSending(true);
|
|
151
|
+
setMessage('Checking desktop connection...');
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const connection = await daemonClient.checkConnection({
|
|
155
|
+
...daemonOptions,
|
|
156
|
+
timeoutMs: CONNECTION_TIMEOUT_MS,
|
|
157
|
+
});
|
|
158
|
+
if (!connection.ok) {
|
|
159
|
+
setMessage('Cannot reach desktop. Try /health in phone browser.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setMessage('Sending logs...');
|
|
164
|
+
const result = await daemonClient.reportOnce({
|
|
165
|
+
...daemonOptions,
|
|
166
|
+
timeoutMs: 2000,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (result.ok) {
|
|
170
|
+
const totalLogs = Object.values(result.logCount ?? {}).reduce((total, count) => total + count, 0);
|
|
171
|
+
setMessage(`Sent ${totalLogs} logs.`);
|
|
172
|
+
} else {
|
|
173
|
+
setMessage(result.error ? `Send failed: ${result.error}` : 'Send failed.');
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
setSending(false);
|
|
177
|
+
}
|
|
178
|
+
}, [configureDaemon, validateSettings]);
|
|
179
|
+
|
|
180
|
+
const messageTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
|
181
|
+
|
|
182
|
+
const copyUrl = useCallback((label: string, url: string) => {
|
|
183
|
+
copyToComputer(url, { label });
|
|
184
|
+
setMessage('Copied to computer output.');
|
|
185
|
+
clearTimeout(messageTimerRef.current);
|
|
186
|
+
messageTimerRef.current = setTimeout(() => setMessage(null), 1500);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
useEffect(() => () => clearTimeout(messageTimerRef.current), []);
|
|
190
|
+
|
|
191
|
+
const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
|
|
192
|
+
const busy = sending || syncState === 'checking';
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
|
196
|
+
<ScrollView keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
|
|
197
|
+
|
|
198
|
+
{isSim ? (
|
|
199
|
+
<View style={styles.badge}>
|
|
200
|
+
<Text style={styles.badgeText}>Simulator — using localhost</Text>
|
|
201
|
+
</View>
|
|
202
|
+
) : (
|
|
203
|
+
<View style={styles.section}>
|
|
204
|
+
<Text style={styles.label}>Computer IP</Text>
|
|
205
|
+
<View style={styles.inputRow}>
|
|
206
|
+
<TextInput
|
|
207
|
+
ref={inputRef}
|
|
208
|
+
style={styles.input}
|
|
209
|
+
value={computerHost}
|
|
210
|
+
onChangeText={handleHostChange}
|
|
211
|
+
placeholder="192.168.1.10"
|
|
212
|
+
placeholderTextColor={Colors.textLight}
|
|
213
|
+
autoCapitalize="none"
|
|
214
|
+
autoCorrect={false}
|
|
215
|
+
keyboardType="numbers-and-punctuation"
|
|
216
|
+
returnKeyType="done"
|
|
217
|
+
onSubmitEditing={() => inputRef.current?.blur()}
|
|
218
|
+
editable={!streaming}
|
|
219
|
+
/>
|
|
220
|
+
{snapshot.qrAvailable ? (
|
|
221
|
+
<TouchableOpacity style={styles.scanButton} onPress={() => setQrVisible(true)} disabled={streaming} activeOpacity={0.7}>
|
|
222
|
+
<Text style={styles.scanButtonText}>Scan</Text>
|
|
223
|
+
</TouchableOpacity>
|
|
224
|
+
) : null}
|
|
225
|
+
</View>
|
|
226
|
+
</View>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
<View style={styles.actions}>
|
|
230
|
+
<TouchableOpacity
|
|
231
|
+
style={[styles.primaryButton, (!canConnect || busy) && styles.buttonDisabled]}
|
|
232
|
+
onPress={toggleLiveSync}
|
|
233
|
+
disabled={!canConnect || busy}
|
|
234
|
+
activeOpacity={0.75}
|
|
235
|
+
>
|
|
236
|
+
<Text style={styles.primaryButtonText}>
|
|
237
|
+
{streaming ? 'Stop' : busy ? 'Checking...' : 'Live Sync'}
|
|
238
|
+
</Text>
|
|
239
|
+
</TouchableOpacity>
|
|
240
|
+
<TouchableOpacity
|
|
241
|
+
style={[styles.secondaryButton, (!canConnect || busy) && styles.buttonDisabled]}
|
|
242
|
+
onPress={sendOnce}
|
|
243
|
+
disabled={!canConnect || busy}
|
|
244
|
+
activeOpacity={0.75}
|
|
245
|
+
>
|
|
246
|
+
<Text style={styles.secondaryButtonText}>
|
|
247
|
+
{sending ? 'Sending...' : 'Send Once'}
|
|
248
|
+
</Text>
|
|
249
|
+
</TouchableOpacity>
|
|
250
|
+
</View>
|
|
251
|
+
|
|
252
|
+
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
253
|
+
|
|
254
|
+
<View style={styles.section}>
|
|
255
|
+
<Text style={styles.sectionTitle}>Remote JS Bundle</Text>
|
|
256
|
+
<Text style={styles.sectionDesc}>
|
|
257
|
+
Load JavaScript from your computer instead of the bundled file. Requires app restart.
|
|
258
|
+
</Text>
|
|
259
|
+
|
|
260
|
+
{!metroUrls ? (
|
|
261
|
+
<View style={styles.stepCard}>
|
|
262
|
+
<Text style={styles.stepHint}>Enter your computer IP above to get started.</Text>
|
|
263
|
+
</View>
|
|
264
|
+
) : (
|
|
265
|
+
<>
|
|
266
|
+
<View style={styles.stepCard}>
|
|
267
|
+
<View style={styles.stepHeader}>
|
|
268
|
+
<Text style={styles.stepNumber}>1</Text>
|
|
269
|
+
<Text style={styles.stepTitle}>Copy bundle URL</Text>
|
|
270
|
+
</View>
|
|
271
|
+
<Text style={styles.stepDesc}>Use this URL as your remote JS bundle location:</Text>
|
|
272
|
+
<View style={styles.urlRow}>
|
|
273
|
+
<Text style={styles.urlText} numberOfLines={1}>{metroUrls.httpUrl}</Text>
|
|
274
|
+
<TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Metro URL', metroUrls.httpUrl)} activeOpacity={0.7}>
|
|
275
|
+
<Text style={styles.copyButtonText}>Copy</Text>
|
|
276
|
+
</TouchableOpacity>
|
|
277
|
+
</View>
|
|
278
|
+
<View style={styles.urlRow}>
|
|
279
|
+
<Text style={styles.urlLabel}>Expo</Text>
|
|
280
|
+
<Text style={styles.urlText} numberOfLines={1}>{metroUrls.expUrl}</Text>
|
|
281
|
+
<TouchableOpacity style={styles.copyButton} onPress={() => copyUrl('Expo URL', metroUrls.expUrl)} activeOpacity={0.7}>
|
|
282
|
+
<Text style={styles.copyButtonText}>Copy</Text>
|
|
283
|
+
</TouchableOpacity>
|
|
284
|
+
</View>
|
|
285
|
+
</View>
|
|
286
|
+
|
|
287
|
+
<View style={styles.stepCard}>
|
|
288
|
+
<View style={styles.stepHeader}>
|
|
289
|
+
<Text style={styles.stepNumber}>2</Text>
|
|
290
|
+
<Text style={styles.stepTitle}>Configure remote debugging</Text>
|
|
291
|
+
</View>
|
|
292
|
+
<Text style={styles.stepDesc}>
|
|
293
|
+
{isSim
|
|
294
|
+
? 'Simulator uses localhost automatically. Enable remote debugging in Dev Menu.'
|
|
295
|
+
: 'In Dev Menu, set the bundle URL to the copied address.'}
|
|
296
|
+
</Text>
|
|
297
|
+
</View>
|
|
298
|
+
|
|
299
|
+
<View style={styles.stepCard}>
|
|
300
|
+
<View style={styles.stepHeader}>
|
|
301
|
+
<Text style={styles.stepNumber}>3</Text>
|
|
302
|
+
<Text style={styles.stepTitle}>Restart the app</Text>
|
|
303
|
+
</View>
|
|
304
|
+
<Text style={styles.stepDesc}>
|
|
305
|
+
Close and reopen the app to load from Metro. Make sure Metro is running on your computer.
|
|
306
|
+
</Text>
|
|
307
|
+
</View>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</View>
|
|
311
|
+
</ScrollView>
|
|
312
|
+
<DevConnectQrScanner
|
|
313
|
+
visible={qrVisible}
|
|
314
|
+
onClose={() => setQrVisible(false)}
|
|
315
|
+
onScanHost={handleQrHost}
|
|
316
|
+
/>
|
|
317
|
+
</KeyboardAvoidingView>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const styles = StyleSheet.create({
|
|
322
|
+
container: { flex: 1 },
|
|
323
|
+
scrollContent: { paddingHorizontal: 16, paddingTop: 12, paddingBottom: 24 },
|
|
324
|
+
badge: {
|
|
325
|
+
paddingHorizontal: 12,
|
|
326
|
+
paddingVertical: 8,
|
|
327
|
+
borderRadius: 8,
|
|
328
|
+
backgroundColor: `${Colors.primary}15`,
|
|
329
|
+
borderWidth: 1,
|
|
330
|
+
borderColor: `${Colors.primary}30`,
|
|
331
|
+
marginBottom: 14,
|
|
332
|
+
},
|
|
333
|
+
badgeText: { fontSize: 13, fontWeight: '500', color: Colors.primary },
|
|
334
|
+
section: { marginBottom: 14 },
|
|
335
|
+
sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 4 },
|
|
336
|
+
sectionDesc: { fontSize: 12, color: Colors.textSecondary, marginBottom: 10, lineHeight: 17 },
|
|
337
|
+
label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
|
|
338
|
+
inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
339
|
+
input: {
|
|
340
|
+
flex: 1,
|
|
341
|
+
backgroundColor: Colors.surface,
|
|
342
|
+
borderWidth: 1,
|
|
343
|
+
borderColor: Colors.border,
|
|
344
|
+
borderRadius: 8,
|
|
345
|
+
paddingHorizontal: 12,
|
|
346
|
+
paddingVertical: 10,
|
|
347
|
+
fontSize: 14,
|
|
348
|
+
color: Colors.text,
|
|
349
|
+
fontFamily: 'Courier',
|
|
350
|
+
},
|
|
351
|
+
scanButton: {
|
|
352
|
+
minWidth: 62,
|
|
353
|
+
alignItems: 'center',
|
|
354
|
+
justifyContent: 'center',
|
|
355
|
+
paddingHorizontal: 12,
|
|
356
|
+
paddingVertical: 10,
|
|
357
|
+
borderRadius: 8,
|
|
358
|
+
backgroundColor: Colors.surface,
|
|
359
|
+
borderWidth: 1,
|
|
360
|
+
borderColor: Colors.primary,
|
|
361
|
+
},
|
|
362
|
+
scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
|
|
363
|
+
actions: { flexDirection: 'row', gap: 10, marginTop: 4, marginBottom: 12 },
|
|
364
|
+
primaryButton: {
|
|
365
|
+
flex: 1,
|
|
366
|
+
alignItems: 'center',
|
|
367
|
+
justifyContent: 'center',
|
|
368
|
+
paddingVertical: 11,
|
|
369
|
+
borderRadius: 10,
|
|
370
|
+
backgroundColor: Colors.primary,
|
|
371
|
+
},
|
|
372
|
+
primaryButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
|
|
373
|
+
secondaryButton: {
|
|
374
|
+
flex: 1,
|
|
375
|
+
alignItems: 'center',
|
|
376
|
+
justifyContent: 'center',
|
|
377
|
+
paddingVertical: 11,
|
|
378
|
+
borderRadius: 10,
|
|
379
|
+
backgroundColor: Colors.surface,
|
|
380
|
+
borderWidth: 1,
|
|
381
|
+
borderColor: Colors.border,
|
|
382
|
+
},
|
|
383
|
+
secondaryButtonText: { color: Colors.primary, fontSize: 14, fontWeight: '600' },
|
|
384
|
+
buttonDisabled: { opacity: 0.5 },
|
|
385
|
+
message: { fontSize: 12, lineHeight: 17, color: Colors.textSecondary, marginBottom: 12 },
|
|
386
|
+
hint: { fontSize: 12, color: Colors.textLight },
|
|
387
|
+
stepCard: {
|
|
388
|
+
backgroundColor: Colors.surface,
|
|
389
|
+
borderWidth: 1,
|
|
390
|
+
borderColor: Colors.border,
|
|
391
|
+
borderRadius: 10,
|
|
392
|
+
padding: 12,
|
|
393
|
+
marginBottom: 8,
|
|
394
|
+
},
|
|
395
|
+
stepHint: { fontSize: 12, color: Colors.textSecondary, lineHeight: 17 },
|
|
396
|
+
stepHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 },
|
|
397
|
+
stepNumber: {
|
|
398
|
+
width: 20,
|
|
399
|
+
height: 20,
|
|
400
|
+
borderRadius: 10,
|
|
401
|
+
backgroundColor: Colors.primary,
|
|
402
|
+
color: '#fff',
|
|
403
|
+
fontSize: 11,
|
|
404
|
+
fontWeight: '700',
|
|
405
|
+
textAlign: 'center',
|
|
406
|
+
lineHeight: 20,
|
|
407
|
+
marginRight: 8,
|
|
408
|
+
overflow: 'hidden',
|
|
409
|
+
},
|
|
410
|
+
stepTitle: { fontSize: 13, fontWeight: '600', color: Colors.text },
|
|
411
|
+
stepDesc: { fontSize: 12, color: Colors.textSecondary, lineHeight: 17, marginBottom: 8 },
|
|
412
|
+
urlLabel: {
|
|
413
|
+
fontSize: 10,
|
|
414
|
+
fontWeight: '600',
|
|
415
|
+
color: Colors.primary,
|
|
416
|
+
backgroundColor: `${Colors.primary}15`,
|
|
417
|
+
paddingHorizontal: 6,
|
|
418
|
+
paddingVertical: 2,
|
|
419
|
+
borderRadius: 4,
|
|
420
|
+
marginRight: 6,
|
|
421
|
+
},
|
|
422
|
+
urlRow: {
|
|
423
|
+
flexDirection: 'row',
|
|
424
|
+
alignItems: 'center',
|
|
425
|
+
backgroundColor: Colors.surface,
|
|
426
|
+
borderWidth: 1,
|
|
427
|
+
borderColor: Colors.border,
|
|
428
|
+
borderRadius: 8,
|
|
429
|
+
paddingLeft: 12,
|
|
430
|
+
paddingRight: 4,
|
|
431
|
+
paddingVertical: 4,
|
|
432
|
+
marginBottom: 8,
|
|
433
|
+
},
|
|
434
|
+
urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text, paddingVertical: 6 },
|
|
435
|
+
copyButton: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 6, backgroundColor: Colors.primary },
|
|
436
|
+
copyButtonText: { color: '#fff', fontSize: 12, fontWeight: '600' },
|
|
437
|
+
});
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const METRO_PORT = '8081';
|
|
2
|
+
|
|
3
|
+
export interface MetroUrls {
|
|
4
|
+
expUrl: string;
|
|
5
|
+
httpUrl: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParsedMetroQrPayload {
|
|
9
|
+
computerHost: string;
|
|
10
|
+
source: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isValidIpv4(host: string): boolean {
|
|
14
|
+
const parts = host.split('.');
|
|
15
|
+
if (parts.length !== 4) return false;
|
|
16
|
+
|
|
17
|
+
return parts.every((part) => {
|
|
18
|
+
if (!/^\d{1,3}$/.test(part)) return false;
|
|
19
|
+
const value = Number(part);
|
|
20
|
+
return value >= 0 && value <= 255 && String(value) === part;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toUrlInput(raw: string): string {
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)) {
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
return `http://${trimmed}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeComputerHost(raw: string): string | null {
|
|
33
|
+
const trimmed = raw.trim();
|
|
34
|
+
if (!trimmed) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(toUrlInput(trimmed));
|
|
38
|
+
const host = parsed.hostname.trim();
|
|
39
|
+
return isValidIpv4(host) ? host : null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildMetroUrls(rawHost: string): MetroUrls | null {
|
|
46
|
+
const host = normalizeComputerHost(rawHost);
|
|
47
|
+
if (!host) return null;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
expUrl: `exp://${host}:${METRO_PORT}`,
|
|
51
|
+
httpUrl: `http://${host}:${METRO_PORT}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseMetroQrPayload(payload: string): ParsedMetroQrPayload | null {
|
|
56
|
+
const computerHost = normalizeComputerHost(payload);
|
|
57
|
+
if (!computerHost) return null;
|
|
58
|
+
return { computerHost, source: payload };
|
|
59
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DevConnectTab } from './DevConnectTab';
|
|
2
|
+
import { isCameraKitAvailable } from './cameraKit';
|
|
3
|
+
import { loadDevConnectPreferences } from './devConnectPreferences';
|
|
4
|
+
import { isSimulator } from './platformDetect';
|
|
5
|
+
import { daemonClient } from '../../utils/DaemonClient';
|
|
6
|
+
import type { DebugFeature, DebugFeatureListener } from '../../types';
|
|
7
|
+
import type { DevConnectState } from './types';
|
|
8
|
+
|
|
9
|
+
export type { DevConnectState } from './types';
|
|
10
|
+
export {
|
|
11
|
+
buildMetroUrls,
|
|
12
|
+
normalizeComputerHost,
|
|
13
|
+
parseMetroQrPayload,
|
|
14
|
+
} from './devConnectUtils';
|
|
15
|
+
export {
|
|
16
|
+
loadDevConnectPreferences,
|
|
17
|
+
restoreDevConnectSettingsToDaemon,
|
|
18
|
+
saveComputerHost,
|
|
19
|
+
} from './devConnectPreferences';
|
|
20
|
+
|
|
21
|
+
export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
|
|
22
|
+
const listeners = new Set<DebugFeatureListener>();
|
|
23
|
+
let state: DevConnectState = {
|
|
24
|
+
isSimulator: isSimulator(),
|
|
25
|
+
computerHost: '',
|
|
26
|
+
qrAvailable: isCameraKitAvailable(),
|
|
27
|
+
streaming: daemonClient.isConnected(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const notify = () => {
|
|
31
|
+
state = {
|
|
32
|
+
...state,
|
|
33
|
+
streaming: daemonClient.isConnected(),
|
|
34
|
+
};
|
|
35
|
+
listeners.forEach((listener) => listener());
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'devConnect',
|
|
40
|
+
label: 'DevConnect',
|
|
41
|
+
renderContent: DevConnectTab,
|
|
42
|
+
setup() {
|
|
43
|
+
loadDevConnectPreferences().then((preferences) => {
|
|
44
|
+
state = {
|
|
45
|
+
...state,
|
|
46
|
+
computerHost: preferences.computerHost,
|
|
47
|
+
};
|
|
48
|
+
notify();
|
|
49
|
+
}).catch(() => {
|
|
50
|
+
notify();
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
getSnapshot: () => state,
|
|
54
|
+
cleanup() {
|
|
55
|
+
listeners.clear();
|
|
56
|
+
},
|
|
57
|
+
subscribe(listener) {
|
|
58
|
+
listeners.add(listener);
|
|
59
|
+
return () => {
|
|
60
|
+
listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|