react-native-debug-toolkit 3.2.1 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +13 -2
  2. package/README.zh-CN.md +13 -2
  3. package/android/build.gradle +34 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +70 -0
  6. package/android/src/main/java/com/reactnativedebugtoolkit/ReactNativeDebugToolkitPackage.java +25 -0
  7. package/ios/DebugToolkitDevConnect.mm +67 -0
  8. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +18 -7
  9. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -1
  10. package/lib/commonjs/features/devConnect/DevConnectTab.js +232 -161
  11. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
  12. package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -5
  13. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
  14. package/lib/commonjs/features/devConnect/devConnectUtils.js +99 -15
  15. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
  16. package/lib/commonjs/features/devConnect/index.js +39 -2
  17. package/lib/commonjs/features/devConnect/index.js.map +1 -1
  18. package/lib/commonjs/features/devConnect/nativeDevConnect.js +110 -0
  19. package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -0
  20. package/lib/commonjs/features/devConnect/platformDetect.js +7 -11
  21. package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -1
  22. package/lib/commonjs/utils/debugPreferences.js +43 -6
  23. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  24. package/lib/module/features/devConnect/DevConnectQrScanner.js +18 -7
  25. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -1
  26. package/lib/module/features/devConnect/DevConnectTab.js +235 -164
  27. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
  28. package/lib/module/features/devConnect/devConnectPreferences.js +33 -6
  29. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
  30. package/lib/module/features/devConnect/devConnectUtils.js +94 -15
  31. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
  32. package/lib/module/features/devConnect/index.js +11 -3
  33. package/lib/module/features/devConnect/index.js.map +1 -1
  34. package/lib/module/features/devConnect/nativeDevConnect.js +104 -0
  35. package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -0
  36. package/lib/module/features/devConnect/platformDetect.js +8 -12
  37. package/lib/module/features/devConnect/platformDetect.js.map +1 -1
  38. package/lib/module/utils/debugPreferences.js +43 -6
  39. package/lib/module/utils/debugPreferences.js.map +1 -1
  40. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +3 -2
  41. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -1
  42. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
  43. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +6 -0
  44. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
  45. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +18 -1
  46. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
  47. package/lib/typescript/src/features/devConnect/index.d.ts +2 -2
  48. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
  49. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +17 -0
  50. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -0
  51. package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -1
  52. package/lib/typescript/src/features/devConnect/types.d.ts +3 -0
  53. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
  54. package/lib/typescript/src/utils/debugPreferences.d.ts +2 -0
  55. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  56. package/package.json +4 -1
  57. package/react-native-debug-toolkit.podspec +18 -0
  58. package/src/features/devConnect/DevConnectQrScanner.tsx +20 -9
  59. package/src/features/devConnect/DevConnectTab.tsx +227 -105
  60. package/src/features/devConnect/devConnectPreferences.ts +50 -5
  61. package/src/features/devConnect/devConnectUtils.ts +122 -15
  62. package/src/features/devConnect/index.ts +13 -0
  63. package/src/features/devConnect/nativeDevConnect.ts +128 -0
  64. package/src/features/devConnect/platformDetect.ts +8 -13
  65. package/src/features/devConnect/types.ts +3 -0
  66. package/src/utils/debugPreferences.ts +49 -4
@@ -12,7 +12,6 @@ import {
12
12
 
13
13
  import type { DebugFeatureRenderProps } from '../../types';
14
14
  import { Colors } from '../../ui/theme/colors';
15
- import { copyToComputer } from '../../utils/copyToComputer';
16
15
  import {
17
16
  buildDeviceDaemonEndpoint,
18
17
  daemonClient,
@@ -20,53 +19,115 @@ import {
20
19
  normalizeDaemonSettings,
21
20
  type DaemonSettings,
22
21
  } from '../../utils/DaemonClient';
23
- import { buildMetroUrls, normalizeComputerHost } from './devConnectUtils';
24
- import { saveComputerHost } from './devConnectPreferences';
22
+ import {
23
+ DEFAULT_DAEMON_PORT,
24
+ DEFAULT_METRO_PORT,
25
+ buildDaemonDeviceHost,
26
+ buildMetroTarget,
27
+ buildMetroUrls,
28
+ normalizeComputerHost,
29
+ normalizePort,
30
+ parseComputerTarget,
31
+ type ParsedComputerTarget,
32
+ } from './devConnectUtils';
33
+ import {
34
+ saveComputerTarget,
35
+ saveDaemonPort,
36
+ saveMetroPort,
37
+ } from './devConnectPreferences';
38
+ import { applyMetroBundle, resetMetroBundle } from './nativeDevConnect';
25
39
  import type { DevConnectState } from './types';
26
40
  import { DevConnectQrScanner } from './DevConnectQrScanner';
27
41
 
28
42
  const CONNECTION_TIMEOUT_MS = 2000;
29
- const METRO_PORT = '8081';
30
43
 
31
44
  type SyncUiState = 'idle' | 'checking' | 'connected' | 'retrying' | 'failed' | 'running';
32
45
 
46
+ function getSimulatorMetroHost(): string {
47
+ return Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
48
+ }
49
+
50
+ function describeMetroFailure(result: { reason: string; error?: string }): string {
51
+ if (result.reason === 'native_unavailable') {
52
+ return 'Native DevConnect not installed. Rebuild app after installing native module.';
53
+ }
54
+ if (result.reason === 'metro_unreachable') {
55
+ return result.error ? `Metro not reachable: ${result.error}` : 'Metro not reachable. Start Metro on that port.';
56
+ }
57
+ if (result.reason === 'fetch_unavailable') {
58
+ return 'Cannot check Metro because fetch is unavailable.';
59
+ }
60
+ if (result.reason === 'invalid_target') {
61
+ return 'Enter a valid computer IP and Metro port.';
62
+ }
63
+ return result.error ? `Metro switch failed: ${result.error}` : 'Metro switch failed.';
64
+ }
65
+
33
66
  export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectState>) {
34
67
  const inputRef = useRef<TextInput>(null);
35
68
  const [computerHost, setComputerHost] = useState(snapshot.computerHost);
69
+ const [metroPort, setMetroPort] = useState(snapshot.metroPort);
70
+ const [daemonPort, setDaemonPort] = useState(snapshot.daemonPort);
36
71
  const [streaming, setStreaming] = useState(snapshot.streaming);
37
72
  const [syncState, setSyncState] = useState<SyncUiState>(snapshot.streaming ? 'running' : 'idle');
38
73
  const [message, setMessage] = useState<string | null>(null);
39
74
  const [sending, setSending] = useState(false);
75
+ const [metroBusy, setMetroBusy] = useState(false);
40
76
  const [qrVisible, setQrVisible] = useState(false);
41
77
 
42
78
  const isSim = snapshot.isSimulator;
43
79
 
44
80
  useEffect(() => {
45
81
  setComputerHost(snapshot.computerHost);
82
+ setMetroPort(snapshot.metroPort);
83
+ setDaemonPort(snapshot.daemonPort);
46
84
  setStreaming(snapshot.streaming);
47
85
  setSyncState(snapshot.streaming ? 'running' : 'idle');
48
- }, [snapshot.computerHost, snapshot.streaming]);
86
+ }, [snapshot.computerHost, snapshot.daemonPort, snapshot.metroPort, snapshot.streaming]);
49
87
 
88
+ const metroHost = isSim ? getSimulatorMetroHost() : computerHost;
89
+ const metroTarget = useMemo(
90
+ () => buildMetroTarget(metroHost, metroPort),
91
+ [metroHost, metroPort],
92
+ );
50
93
  const metroUrls = useMemo(
51
- () => isSim
52
- ? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
53
- : buildMetroUrls(computerHost),
54
- [isSim, computerHost],
94
+ () => buildMetroUrls(metroHost, metroPort),
95
+ [metroHost, metroPort],
55
96
  );
56
97
 
57
98
  const handleHostChange = useCallback((value: string) => {
58
99
  setComputerHost(value);
59
- const normalized = normalizeComputerHost(value);
60
- if (normalized) {
61
- saveComputerHost(normalized).catch(() => {});
100
+ const target = parseComputerTarget(value);
101
+ if (target) {
102
+ setMetroPort(target.metroPort);
103
+ saveComputerTarget(value).catch(() => {});
62
104
  }
63
105
  setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
64
106
  setMessage(null);
65
107
  }, []);
66
108
 
67
- const handleQrHost = useCallback((host: string) => {
68
- setComputerHost(host);
69
- saveComputerHost(host).catch(() => {});
109
+ const handleMetroPortChange = useCallback((value: string) => {
110
+ setMetroPort(value);
111
+ const normalized = normalizePort(value);
112
+ if (normalized) {
113
+ saveMetroPort(normalized).catch(() => {});
114
+ }
115
+ setMessage(null);
116
+ }, []);
117
+
118
+ const handleDaemonPortChange = useCallback((value: string) => {
119
+ setDaemonPort(value);
120
+ const normalized = normalizePort(value);
121
+ if (normalized) {
122
+ saveDaemonPort(normalized).catch(() => {});
123
+ }
124
+ setMessage(null);
125
+ }, []);
126
+
127
+ const handleQrTarget = useCallback((target: ParsedComputerTarget) => {
128
+ setComputerHost(target.computerHost);
129
+ setMetroPort(target.metroPort);
130
+ saveComputerTarget(`${target.computerHost}:${target.metroPort}`).catch(() => {});
70
131
  setMessage('Computer IP updated from QR code.');
71
132
  }, []);
72
133
 
@@ -75,22 +136,28 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
75
136
  setMessage('Enter your computer IP first.');
76
137
  return false;
77
138
  }
139
+ if (!normalizePort(daemonPort)) {
140
+ setMessage('Enter a valid desktop logs port.');
141
+ return false;
142
+ }
78
143
  return true;
79
- }, [computerHost, isSim]);
144
+ }, [computerHost, daemonPort, isSim]);
80
145
 
81
146
  const configureDaemon = useCallback(() => {
82
147
  const normalizedHost = isSim ? '' : (normalizeComputerHost(computerHost) ?? '');
148
+ const normalizedDaemonPort = normalizePort(daemonPort) ?? DEFAULT_DAEMON_PORT;
149
+ const deviceHost = isSim ? '' : buildDaemonDeviceHost(normalizedHost, normalizedDaemonPort);
83
150
  const settings: DaemonSettings = {
84
151
  mode: isSim ? 'simulator' : 'device',
85
152
  endpoint: '',
86
- deviceHost: normalizedHost,
153
+ deviceHost,
87
154
  token: '',
88
155
  };
89
156
  daemonClient.configure(settings);
90
157
  const normalized = normalizeDaemonSettings(settings);
91
- const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(normalizedHost));
158
+ const endpoint = normalized.endpoint || (isSim ? getDefaultDaemonEndpoint() : buildDeviceDaemonEndpoint(deviceHost));
92
159
  return { ...normalized, endpoint };
93
- }, [computerHost, isSim]);
160
+ }, [computerHost, daemonPort, isSim]);
94
161
 
95
162
  const toggleLiveSync = useCallback(async () => {
96
163
  if (streaming) {
@@ -102,7 +169,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
102
169
  return;
103
170
  }
104
171
 
105
- if (!validateSettings()) return;
172
+ if (!validateSettings()) {
173
+ return;
174
+ }
106
175
 
107
176
  const daemonOptions = configureDaemon();
108
177
  setMessage('Checking desktop connection...');
@@ -144,7 +213,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
144
213
  }, [configureDaemon, streaming, validateSettings]);
145
214
 
146
215
  const sendOnce = useCallback(async () => {
147
- if (!validateSettings()) return;
216
+ if (!validateSettings()) {
217
+ return;
218
+ }
148
219
 
149
220
  const daemonOptions = configureDaemon();
150
221
  setSending(true);
@@ -177,18 +248,51 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
177
248
  }
178
249
  }, [configureDaemon, validateSettings]);
179
250
 
180
- const messageTimerRef = useRef<ReturnType<typeof setTimeout>>();
251
+ const applyRemoteBundle = useCallback(async () => {
252
+ if (!metroTarget) {
253
+ setMessage('Enter a valid computer IP and Metro port.');
254
+ return;
255
+ }
256
+ if (!snapshot.nativeMetroAvailable) {
257
+ setMessage(describeMetroFailure({ reason: 'native_unavailable' }));
258
+ return;
259
+ }
181
260
 
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
- }, []);
261
+ setMetroBusy(true);
262
+ setMessage('Checking Metro...');
263
+ try {
264
+ const result = await applyMetroBundle(metroTarget.host, metroTarget.port);
265
+ if (result.ok) {
266
+ setMessage(`Using Metro at ${result.hostPort}. Reloading...`);
267
+ } else {
268
+ setMessage(describeMetroFailure(result));
269
+ }
270
+ } finally {
271
+ setMetroBusy(false);
272
+ }
273
+ }, [metroTarget, snapshot.nativeMetroAvailable]);
188
274
 
189
- useEffect(() => () => clearTimeout(messageTimerRef.current), []);
275
+ const resetRemoteBundle = useCallback(async () => {
276
+ if (!snapshot.nativeMetroAvailable) {
277
+ setMessage(describeMetroFailure({ reason: 'native_unavailable' }));
278
+ return;
279
+ }
190
280
 
191
- const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
281
+ setMetroBusy(true);
282
+ try {
283
+ const result = await resetMetroBundle();
284
+ if (result.ok) {
285
+ setMessage('Metro host reset. Reloading...');
286
+ } else {
287
+ setMessage(describeMetroFailure(result));
288
+ }
289
+ } finally {
290
+ setMetroBusy(false);
291
+ }
292
+ }, [snapshot.nativeMetroAvailable]);
293
+
294
+ const canConnect = isSim || (Boolean(normalizeComputerHost(computerHost)) && Boolean(normalizePort(daemonPort)));
295
+ const canUseMetro = Boolean(metroTarget) && snapshot.nativeMetroAvailable && !metroBusy;
192
296
  const busy = sending || syncState === 'checking';
193
297
 
194
298
  return (
@@ -197,7 +301,7 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
197
301
 
198
302
  {isSim ? (
199
303
  <View style={styles.badge}>
200
- <Text style={styles.badgeText}>Simulator using localhost</Text>
304
+ <Text style={styles.badgeText}>Simulator/emulator - using {getSimulatorMetroHost()}</Text>
201
305
  </View>
202
306
  ) : (
203
307
  <View style={styles.section}>
@@ -226,6 +330,40 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
226
330
  </View>
227
331
  )}
228
332
 
333
+ <View style={styles.section}>
334
+ <Text style={styles.label}>Ports</Text>
335
+ <View style={styles.portRow}>
336
+ <View style={styles.portField}>
337
+ <Text style={styles.portLabel}>Metro</Text>
338
+ <TextInput
339
+ style={styles.portInput}
340
+ value={metroPort}
341
+ onChangeText={handleMetroPortChange}
342
+ placeholder={DEFAULT_METRO_PORT}
343
+ placeholderTextColor={Colors.textLight}
344
+ autoCapitalize="none"
345
+ autoCorrect={false}
346
+ keyboardType="number-pad"
347
+ returnKeyType="done"
348
+ />
349
+ </View>
350
+ <View style={styles.portField}>
351
+ <Text style={styles.portLabel}>Logs</Text>
352
+ <TextInput
353
+ style={styles.portInput}
354
+ value={daemonPort}
355
+ onChangeText={handleDaemonPortChange}
356
+ placeholder={DEFAULT_DAEMON_PORT}
357
+ placeholderTextColor={Colors.textLight}
358
+ autoCapitalize="none"
359
+ autoCorrect={false}
360
+ keyboardType="number-pad"
361
+ returnKeyType="done"
362
+ />
363
+ </View>
364
+ </View>
365
+ </View>
366
+
229
367
  <View style={styles.actions}>
230
368
  <TouchableOpacity
231
369
  style={[styles.primaryButton, (!canConnect || busy) && styles.buttonDisabled]}
@@ -254,65 +392,56 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
254
392
  <View style={styles.section}>
255
393
  <Text style={styles.sectionTitle}>Remote JS Bundle</Text>
256
394
  <Text style={styles.sectionDesc}>
257
- Load JavaScript from your computer instead of the bundled file. Requires app restart.
395
+ Apply this Metro host to React Native dev settings and reload the app.
258
396
  </Text>
259
397
 
260
398
  {!metroUrls ? (
261
399
  <View style={styles.stepCard}>
262
- <Text style={styles.stepHint}>Enter your computer IP above to get started.</Text>
400
+ <Text style={styles.stepHint}>Enter your computer IP and Metro port to get started.</Text>
263
401
  </View>
264
402
  ) : (
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>
403
+ <View style={styles.stepCard}>
404
+ <View style={styles.urlRow}>
405
+ <Text style={styles.urlLabel}>HTTP</Text>
406
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.httpUrl}</Text>
297
407
  </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>
408
+ <View style={styles.urlRow}>
409
+ <Text style={styles.urlLabel}>Expo</Text>
410
+ <Text style={styles.urlText} numberOfLines={1}>{metroUrls.expUrl}</Text>
307
411
  </View>
308
- </>
412
+ </View>
309
413
  )}
414
+
415
+ <View style={styles.actions}>
416
+ <TouchableOpacity
417
+ style={[styles.primaryButton, !canUseMetro && styles.buttonDisabled]}
418
+ onPress={applyRemoteBundle}
419
+ disabled={!canUseMetro}
420
+ activeOpacity={0.75}
421
+ >
422
+ <Text style={styles.primaryButtonText}>
423
+ {metroBusy ? 'Checking...' : 'Use Metro Bundle'}
424
+ </Text>
425
+ </TouchableOpacity>
426
+ <TouchableOpacity
427
+ style={[styles.secondaryButton, (!snapshot.nativeMetroAvailable || metroBusy) && styles.buttonDisabled]}
428
+ onPress={resetRemoteBundle}
429
+ disabled={!snapshot.nativeMetroAvailable || metroBusy}
430
+ activeOpacity={0.75}
431
+ >
432
+ <Text style={styles.secondaryButtonText}>Reset</Text>
433
+ </TouchableOpacity>
434
+ </View>
435
+
436
+ {!snapshot.nativeMetroAvailable ? (
437
+ <Text style={styles.hint}>Native DevConnect requires pod install / Gradle sync and app rebuild.</Text>
438
+ ) : null}
310
439
  </View>
311
440
  </ScrollView>
312
441
  <DevConnectQrScanner
313
442
  visible={qrVisible}
314
443
  onClose={() => setQrVisible(false)}
315
- onScanHost={handleQrHost}
444
+ onScanTarget={handleQrTarget}
316
445
  />
317
446
  </KeyboardAvoidingView>
318
447
  );
@@ -360,6 +489,20 @@ const styles = StyleSheet.create({
360
489
  borderColor: Colors.primary,
361
490
  },
362
491
  scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
492
+ portRow: { flexDirection: 'row', gap: 10 },
493
+ portField: { flex: 1 },
494
+ portLabel: { fontSize: 11, color: Colors.textSecondary, marginBottom: 4 },
495
+ portInput: {
496
+ backgroundColor: Colors.surface,
497
+ borderWidth: 1,
498
+ borderColor: Colors.border,
499
+ borderRadius: 8,
500
+ paddingHorizontal: 12,
501
+ paddingVertical: 9,
502
+ fontSize: 13,
503
+ color: Colors.text,
504
+ fontFamily: 'Courier',
505
+ },
363
506
  actions: { flexDirection: 'row', gap: 10, marginTop: 4, marginBottom: 12 },
364
507
  primaryButton: {
365
508
  flex: 1,
@@ -383,7 +526,7 @@ const styles = StyleSheet.create({
383
526
  secondaryButtonText: { color: Colors.primary, fontSize: 14, fontWeight: '600' },
384
527
  buttonDisabled: { opacity: 0.5 },
385
528
  message: { fontSize: 12, lineHeight: 17, color: Colors.textSecondary, marginBottom: 12 },
386
- hint: { fontSize: 12, color: Colors.textLight },
529
+ hint: { fontSize: 12, color: Colors.textLight, lineHeight: 17 },
387
530
  stepCard: {
388
531
  backgroundColor: Colors.surface,
389
532
  borderWidth: 1,
@@ -393,23 +536,8 @@ const styles = StyleSheet.create({
393
536
  marginBottom: 8,
394
537
  },
395
538
  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
539
  urlLabel: {
540
+ minWidth: 40,
413
541
  fontSize: 10,
414
542
  fontWeight: '600',
415
543
  color: Colors.primary,
@@ -417,21 +545,15 @@ const styles = StyleSheet.create({
417
545
  paddingHorizontal: 6,
418
546
  paddingVertical: 2,
419
547
  borderRadius: 4,
420
- marginRight: 6,
548
+ marginRight: 8,
549
+ textAlign: 'center',
421
550
  },
422
551
  urlRow: {
423
552
  flexDirection: 'row',
424
553
  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,
554
+ borderBottomWidth: StyleSheet.hairlineWidth,
555
+ borderBottomColor: Colors.border,
556
+ paddingVertical: 7,
433
557
  },
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' },
558
+ urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text },
437
559
  });
@@ -1,23 +1,66 @@
1
1
  import { daemonClient } from '../../utils/DaemonClient';
2
2
  import { getPreference, KEYS, setPreference } from '../../utils/debugPreferences';
3
- import { normalizeComputerHost } from './devConnectUtils';
3
+ import {
4
+ DEFAULT_DAEMON_PORT,
5
+ DEFAULT_METRO_PORT,
6
+ buildDaemonDeviceHost,
7
+ normalizeComputerHost,
8
+ normalizePort,
9
+ parseComputerTarget,
10
+ type ParsedComputerTarget,
11
+ } from './devConnectUtils';
4
12
  import { isSimulator } from './platformDetect';
5
13
 
6
14
  export interface DevConnectPreferences {
7
15
  computerHost: string;
16
+ metroPort: string;
17
+ daemonPort: string;
8
18
  }
9
19
 
10
20
  export async function loadDevConnectPreferences(): Promise<DevConnectPreferences> {
11
21
  const storedHost = await getPreference(KEYS.computerHost);
22
+ const storedMetroPort = await getPreference(KEYS.metroPort);
23
+ const storedDaemonPort = await getPreference(KEYS.daemonPort);
12
24
  return {
13
25
  computerHost: storedHost ? normalizeComputerHost(storedHost) ?? '' : '',
26
+ metroPort: storedMetroPort ? normalizePort(storedMetroPort) ?? DEFAULT_METRO_PORT : DEFAULT_METRO_PORT,
27
+ daemonPort: storedDaemonPort ? normalizePort(storedDaemonPort) ?? DEFAULT_DAEMON_PORT : DEFAULT_DAEMON_PORT,
14
28
  };
15
29
  }
16
30
 
31
+ export async function saveComputerTarget(value: string): Promise<ParsedComputerTarget | null> {
32
+ const target = parseComputerTarget(value);
33
+ if (!target) {
34
+ return null;
35
+ }
36
+
37
+ await setPreference(KEYS.computerHost, target.computerHost);
38
+ await setPreference(KEYS.metroPort, target.metroPort);
39
+ return target;
40
+ }
41
+
17
42
  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);
43
+ const target = await saveComputerTarget(value);
44
+ return target?.computerHost ?? null;
45
+ }
46
+
47
+ export async function saveMetroPort(value: string): Promise<string | null> {
48
+ const normalized = normalizePort(value);
49
+ if (!normalized) {
50
+ return null;
51
+ }
52
+
53
+ await setPreference(KEYS.metroPort, normalized);
54
+ return normalized;
55
+ }
56
+
57
+ export async function saveDaemonPort(value: string): Promise<string | null> {
58
+ const normalized = normalizePort(value);
59
+ if (!normalized) {
60
+ return null;
61
+ }
62
+
63
+ await setPreference(KEYS.daemonPort, normalized);
21
64
  return normalized;
22
65
  }
23
66
 
@@ -27,7 +70,9 @@ export async function restoreDevConnectSettingsToDaemon(): Promise<void> {
27
70
  daemonClient.configure({
28
71
  mode,
29
72
  endpoint: '',
30
- deviceHost: mode === 'simulator' ? '' : preferences.computerHost,
73
+ deviceHost: mode === 'simulator'
74
+ ? ''
75
+ : buildDaemonDeviceHost(preferences.computerHost, preferences.daemonPort),
31
76
  token: '',
32
77
  });
33
78
  }