react-native-debug-toolkit 3.2.3 → 3.2.5

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 (74) hide show
  1. package/android/src/main/java/com/reactnativedebugtoolkit/DebugToolkitDevConnectModule.java +80 -10
  2. package/ios/DebugToolkitDevConnect.mm +84 -19
  3. package/lib/commonjs/core/initialize.js +15 -3
  4. package/lib/commonjs/core/initialize.js.map +1 -1
  5. package/lib/commonjs/features/devConnect/DevConnectTab.js +121 -58
  6. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
  7. package/lib/commonjs/features/devConnect/devConnectPreferences.js +6 -2
  8. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -1
  9. package/lib/commonjs/features/devConnect/devConnectUtils.js +8 -0
  10. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -1
  11. package/lib/commonjs/features/devConnect/index.js +33 -5
  12. package/lib/commonjs/features/devConnect/index.js.map +1 -1
  13. package/lib/commonjs/features/devConnect/nativeDevConnect.js +26 -0
  14. package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -1
  15. package/lib/commonjs/ui/DebugView.js +10 -2
  16. package/lib/commonjs/ui/DebugView.js.map +1 -1
  17. package/lib/commonjs/utils/DaemonClient.js +5 -0
  18. package/lib/commonjs/utils/DaemonClient.js.map +1 -1
  19. package/lib/module/core/initialize.js +16 -4
  20. package/lib/module/core/initialize.js.map +1 -1
  21. package/lib/module/features/devConnect/DevConnectTab.js +122 -59
  22. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
  23. package/lib/module/features/devConnect/devConnectPreferences.js +6 -2
  24. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -1
  25. package/lib/module/features/devConnect/devConnectUtils.js +7 -0
  26. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -1
  27. package/lib/module/features/devConnect/index.js +30 -7
  28. package/lib/module/features/devConnect/index.js.map +1 -1
  29. package/lib/module/features/devConnect/nativeDevConnect.js +24 -0
  30. package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -1
  31. package/lib/module/ui/DebugView.js +11 -3
  32. package/lib/module/ui/DebugView.js.map +1 -1
  33. package/lib/module/utils/DaemonClient.js +5 -0
  34. package/lib/module/utils/DaemonClient.js.map +1 -1
  35. package/lib/typescript/src/core/initialize.d.ts +4 -2
  36. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  37. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts +1 -1
  38. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
  39. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -1
  40. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +1 -0
  41. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -1
  42. package/lib/typescript/src/features/devConnect/index.d.ts +1 -0
  43. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -1
  44. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +2 -0
  45. package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -1
  46. package/lib/typescript/src/features/devConnect/types.d.ts +5 -1
  47. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -1
  48. package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
  49. package/lib/typescript/src/utils/DaemonClient.d.ts +2 -0
  50. package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -1
  51. package/package.json +2 -10
  52. package/src/core/initialize.ts +17 -5
  53. package/src/features/devConnect/DevConnectTab.tsx +120 -45
  54. package/src/features/devConnect/devConnectPreferences.ts +7 -2
  55. package/src/features/devConnect/devConnectUtils.ts +8 -0
  56. package/src/features/devConnect/index.ts +31 -7
  57. package/src/features/devConnect/nativeDevConnect.ts +28 -0
  58. package/src/features/devConnect/types.ts +9 -1
  59. package/src/ui/DebugView.tsx +12 -3
  60. package/src/utils/DaemonClient.ts +7 -0
  61. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +0 -248
  62. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +0 -1
  63. package/lib/commonjs/features/devConnect/cameraKit.js +0 -54
  64. package/lib/commonjs/features/devConnect/cameraKit.js.map +0 -1
  65. package/lib/module/features/devConnect/DevConnectQrScanner.js +0 -243
  66. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +0 -1
  67. package/lib/module/features/devConnect/cameraKit.js +0 -49
  68. package/lib/module/features/devConnect/cameraKit.js.map +0 -1
  69. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +0 -10
  70. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +0 -1
  71. package/lib/typescript/src/features/devConnect/cameraKit.d.ts +0 -47
  72. package/lib/typescript/src/features/devConnect/cameraKit.d.ts.map +0 -1
  73. package/src/features/devConnect/DevConnectQrScanner.tsx +0 -214
  74. package/src/features/devConnect/cameraKit.ts +0 -93
@@ -28,16 +28,15 @@ import {
28
28
  normalizeComputerHost,
29
29
  normalizePort,
30
30
  parseComputerTarget,
31
- type ParsedComputerTarget,
32
31
  } from './devConnectUtils';
33
32
  import {
33
+ saveComputerHost,
34
34
  saveComputerTarget,
35
35
  saveDaemonPort,
36
36
  saveMetroPort,
37
37
  } from './devConnectPreferences';
38
38
  import { applyMetroBundle, resetMetroBundle } from './nativeDevConnect';
39
- import type { DevConnectState } from './types';
40
- import { DevConnectQrScanner } from './DevConnectQrScanner';
39
+ import type { DevConnectFeatureControls, DevConnectSettingsPatch, DevConnectState } from './types';
41
40
 
42
41
  const CONNECTION_TIMEOUT_MS = 2000;
43
42
 
@@ -63,7 +62,7 @@ function describeMetroFailure(result: { reason: string; error?: string }): strin
63
62
  return result.error ? `Metro switch failed: ${result.error}` : 'Metro switch failed.';
64
63
  }
65
64
 
66
- export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectState>) {
65
+ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<DevConnectState>) {
67
66
  const inputRef = useRef<TextInput>(null);
68
67
  const [computerHost, setComputerHost] = useState(snapshot.computerHost);
69
68
  const [metroPort, setMetroPort] = useState(snapshot.metroPort);
@@ -73,17 +72,29 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
73
72
  const [message, setMessage] = useState<string | null>(null);
74
73
  const [sending, setSending] = useState(false);
75
74
  const [metroBusy, setMetroBusy] = useState(false);
76
- const [qrVisible, setQrVisible] = useState(false);
77
75
 
78
76
  const isSim = snapshot.isSimulator;
79
77
 
78
+ const updateFeatureSettings = useCallback((patch: DevConnectSettingsPatch) => {
79
+ (feature as unknown as DevConnectFeatureControls).updateSettings?.(patch);
80
+ }, [feature]);
81
+
80
82
  useEffect(() => {
81
83
  setComputerHost(snapshot.computerHost);
84
+ }, [snapshot.computerHost]);
85
+
86
+ useEffect(() => {
82
87
  setMetroPort(snapshot.metroPort);
88
+ }, [snapshot.metroPort]);
89
+
90
+ useEffect(() => {
83
91
  setDaemonPort(snapshot.daemonPort);
92
+ }, [snapshot.daemonPort]);
93
+
94
+ useEffect(() => {
84
95
  setStreaming(snapshot.streaming);
85
96
  setSyncState(snapshot.streaming ? 'running' : 'idle');
86
- }, [snapshot.computerHost, snapshot.daemonPort, snapshot.metroPort, snapshot.streaming]);
97
+ }, [snapshot.streaming]);
87
98
 
88
99
  const metroHost = isSim ? getSimulatorMetroHost() : computerHost;
89
100
  const metroTarget = useMemo(
@@ -100,36 +111,97 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
100
111
  const target = parseComputerTarget(value);
101
112
  if (target) {
102
113
  setMetroPort(target.metroPort);
103
- saveComputerTarget(value).catch(() => {});
114
+ saveComputerTarget(value)
115
+ .then((savedTarget) => {
116
+ if (savedTarget) {
117
+ updateFeatureSettings({
118
+ computerHost: savedTarget.computerHost,
119
+ metroPort: savedTarget.metroPort,
120
+ });
121
+ }
122
+ })
123
+ .catch(() => {});
104
124
  }
105
125
  setSyncState((prev) => (prev === 'failed' ? 'idle' : prev));
106
126
  setMessage(null);
107
- }, []);
127
+ }, [updateFeatureSettings]);
108
128
 
109
129
  const handleMetroPortChange = useCallback((value: string) => {
110
130
  setMetroPort(value);
111
131
  const normalized = normalizePort(value);
112
132
  if (normalized) {
113
- saveMetroPort(normalized).catch(() => {});
133
+ saveMetroPort(normalized)
134
+ .then(() => updateFeatureSettings({ metroPort: normalized }))
135
+ .catch(() => {});
114
136
  }
115
137
  setMessage(null);
116
- }, []);
138
+ }, [updateFeatureSettings]);
117
139
 
118
140
  const handleDaemonPortChange = useCallback((value: string) => {
119
141
  setDaemonPort(value);
120
142
  const normalized = normalizePort(value);
121
143
  if (normalized) {
122
- saveDaemonPort(normalized).catch(() => {});
144
+ saveDaemonPort(normalized)
145
+ .then(() => updateFeatureSettings({ daemonPort: normalized }))
146
+ .catch(() => {});
123
147
  }
124
148
  setMessage(null);
125
- }, []);
149
+ }, [updateFeatureSettings]);
150
+
151
+ const persistConnectionSettings = useCallback(async (): Promise<boolean> => {
152
+ const normalizedDaemonPort = normalizePort(daemonPort);
153
+ if (!normalizedDaemonPort) {
154
+ setMessage('Enter a valid desktop logs port.');
155
+ return false;
156
+ }
157
+
158
+ const patch: DevConnectSettingsPatch = { daemonPort: normalizedDaemonPort };
159
+ const writes: Array<Promise<unknown>> = [saveDaemonPort(normalizedDaemonPort)];
160
+ setDaemonPort(normalizedDaemonPort);
161
+
162
+ if (!isSim) {
163
+ const normalizedHost = normalizeComputerHost(computerHost);
164
+ if (!normalizedHost) {
165
+ setMessage('Enter your computer IP first.');
166
+ return false;
167
+ }
168
+ patch.computerHost = normalizedHost;
169
+ writes.push(saveComputerHost(normalizedHost));
170
+ setComputerHost(normalizedHost);
171
+ }
172
+
173
+ const normalizedMetroPort = normalizePort(metroPort);
174
+ if (normalizedMetroPort) {
175
+ patch.metroPort = normalizedMetroPort;
176
+ writes.push(saveMetroPort(normalizedMetroPort));
177
+ setMetroPort(normalizedMetroPort);
178
+ }
179
+
180
+ await Promise.all(writes);
181
+ updateFeatureSettings(patch);
182
+ return true;
183
+ }, [computerHost, daemonPort, isSim, metroPort, updateFeatureSettings]);
184
+
185
+ const persistMetroSettings = useCallback(async (): Promise<boolean> => {
186
+ if (!metroTarget) {
187
+ setMessage('Enter a valid computer IP and Metro port.');
188
+ return false;
189
+ }
126
190
 
127
- const handleQrTarget = useCallback((target: ParsedComputerTarget) => {
128
- setComputerHost(target.computerHost);
129
- setMetroPort(target.metroPort);
130
- saveComputerTarget(`${target.computerHost}:${target.metroPort}`).catch(() => {});
131
- setMessage('Computer IP updated from QR code.');
132
- }, []);
191
+ const patch: DevConnectSettingsPatch = { metroPort: metroTarget.port };
192
+ const writes: Array<Promise<unknown>> = [saveMetroPort(metroTarget.port)];
193
+ setMetroPort(metroTarget.port);
194
+
195
+ if (!isSim) {
196
+ patch.computerHost = metroTarget.host;
197
+ writes.push(saveComputerTarget(metroTarget.hostPort));
198
+ setComputerHost(metroTarget.host);
199
+ }
200
+
201
+ await Promise.all(writes);
202
+ updateFeatureSettings(patch);
203
+ return true;
204
+ }, [isSim, metroTarget, updateFeatureSettings]);
133
205
 
134
206
  const validateSettings = useCallback((): boolean => {
135
207
  if (!isSim && !normalizeComputerHost(computerHost)) {
@@ -172,6 +244,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
172
244
  if (!validateSettings()) {
173
245
  return;
174
246
  }
247
+ if (!(await persistConnectionSettings())) {
248
+ return;
249
+ }
175
250
 
176
251
  const daemonOptions = configureDaemon();
177
252
  setMessage('Checking desktop connection...');
@@ -210,12 +285,15 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
210
285
  },
211
286
  });
212
287
  setStreaming(true);
213
- }, [configureDaemon, streaming, validateSettings]);
288
+ }, [configureDaemon, persistConnectionSettings, streaming, validateSettings]);
214
289
 
215
290
  const sendOnce = useCallback(async () => {
216
291
  if (!validateSettings()) {
217
292
  return;
218
293
  }
294
+ if (!(await persistConnectionSettings())) {
295
+ return;
296
+ }
219
297
 
220
298
  const daemonOptions = configureDaemon();
221
299
  setSending(true);
@@ -246,7 +324,7 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
246
324
  } finally {
247
325
  setSending(false);
248
326
  }
249
- }, [configureDaemon, validateSettings]);
327
+ }, [configureDaemon, persistConnectionSettings, validateSettings]);
250
328
 
251
329
  const applyRemoteBundle = useCallback(async () => {
252
330
  if (!metroTarget) {
@@ -261,6 +339,9 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
261
339
  setMetroBusy(true);
262
340
  setMessage('Checking Metro...');
263
341
  try {
342
+ if (!(await persistMetroSettings())) {
343
+ return;
344
+ }
264
345
  const result = await applyMetroBundle(metroTarget.host, metroTarget.port);
265
346
  if (result.ok) {
266
347
  setMessage(`Using Metro at ${result.hostPort}. Reloading...`);
@@ -270,7 +351,7 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
270
351
  } finally {
271
352
  setMetroBusy(false);
272
353
  }
273
- }, [metroTarget, snapshot.nativeMetroAvailable]);
354
+ }, [metroTarget, persistMetroSettings, snapshot.nativeMetroAvailable]);
274
355
 
275
356
  const resetRemoteBundle = useCallback(async () => {
276
357
  if (!snapshot.nativeMetroAvailable) {
@@ -294,6 +375,8 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
294
375
  const canConnect = isSim || (Boolean(normalizeComputerHost(computerHost)) && Boolean(normalizePort(daemonPort)));
295
376
  const canUseMetro = Boolean(metroTarget) && snapshot.nativeMetroAvailable && !metroBusy;
296
377
  const busy = sending || syncState === 'checking';
378
+ const subnetPrefix = snapshot.subnetPrefix;
379
+ const ipPlaceholder = subnetPrefix ? `${subnetPrefix}...` : '192.168.1.10';
297
380
 
298
381
  return (
299
382
  <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
@@ -312,7 +395,7 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
312
395
  style={styles.input}
313
396
  value={computerHost}
314
397
  onChangeText={handleHostChange}
315
- placeholder="192.168.1.10"
398
+ placeholder={ipPlaceholder}
316
399
  placeholderTextColor={Colors.textLight}
317
400
  autoCapitalize="none"
318
401
  autoCorrect={false}
@@ -321,12 +404,19 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
321
404
  onSubmitEditing={() => inputRef.current?.blur()}
322
405
  editable={!streaming}
323
406
  />
324
- {snapshot.qrAvailable ? (
325
- <TouchableOpacity style={styles.scanButton} onPress={() => setQrVisible(true)} disabled={streaming} activeOpacity={0.7}>
326
- <Text style={styles.scanButtonText}>Scan</Text>
327
- </TouchableOpacity>
328
- ) : null}
329
407
  </View>
408
+ {subnetPrefix && !computerHost ? (
409
+ <TouchableOpacity
410
+ style={styles.subnetHint}
411
+ onPress={() => {
412
+ setComputerHost(subnetPrefix);
413
+ inputRef.current?.focus();
414
+ }}
415
+ activeOpacity={0.6}
416
+ >
417
+ <Text style={styles.subnetHintText}>Tap to fill: {subnetPrefix}</Text>
418
+ </TouchableOpacity>
419
+ ) : null}
330
420
  </View>
331
421
  )}
332
422
 
@@ -438,11 +528,6 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
438
528
  ) : null}
439
529
  </View>
440
530
  </ScrollView>
441
- <DevConnectQrScanner
442
- visible={qrVisible}
443
- onClose={() => setQrVisible(false)}
444
- onScanTarget={handleQrTarget}
445
- />
446
531
  </KeyboardAvoidingView>
447
532
  );
448
533
  }
@@ -464,7 +549,9 @@ const styles = StyleSheet.create({
464
549
  sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 4 },
465
550
  sectionDesc: { fontSize: 12, color: Colors.textSecondary, marginBottom: 10, lineHeight: 17 },
466
551
  label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
467
- inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
552
+ inputRow: { flexDirection: 'row', alignItems: 'center' },
553
+ subnetHint: { marginTop: 6 },
554
+ subnetHintText: { fontSize: 12, color: Colors.primary, fontWeight: '500' },
468
555
  input: {
469
556
  flex: 1,
470
557
  backgroundColor: Colors.surface,
@@ -477,18 +564,6 @@ const styles = StyleSheet.create({
477
564
  color: Colors.text,
478
565
  fontFamily: 'Courier',
479
566
  },
480
- scanButton: {
481
- minWidth: 62,
482
- alignItems: 'center',
483
- justifyContent: 'center',
484
- paddingHorizontal: 12,
485
- paddingVertical: 10,
486
- borderRadius: 8,
487
- backgroundColor: Colors.surface,
488
- borderWidth: 1,
489
- borderColor: Colors.primary,
490
- },
491
- scanButtonText: { color: Colors.primary, fontSize: 13, fontWeight: '600' },
492
567
  portRow: { flexDirection: 'row', gap: 10 },
493
568
  portField: { flex: 1 },
494
569
  portLabel: { fontSize: 11, color: Colors.textSecondary, marginBottom: 4 },
@@ -40,8 +40,13 @@ export async function saveComputerTarget(value: string): Promise<ParsedComputerT
40
40
  }
41
41
 
42
42
  export async function saveComputerHost(value: string): Promise<string | null> {
43
- const target = await saveComputerTarget(value);
44
- return target?.computerHost ?? null;
43
+ const host = normalizeComputerHost(value);
44
+ if (!host) {
45
+ return null;
46
+ }
47
+
48
+ await setPreference(KEYS.computerHost, host);
49
+ return host;
45
50
  }
46
51
 
47
52
  export async function saveMetroPort(value: string): Promise<string | null> {
@@ -164,3 +164,11 @@ export function buildDaemonDeviceHost(computerHost: string, daemonPort: string):
164
164
  const port = normalizePort(daemonPort) ?? DEFAULT_DAEMON_PORT;
165
165
  return port === DEFAULT_DAEMON_PORT ? host : `${host}:${port}`;
166
166
  }
167
+
168
+ export function extractSubnetPrefix(ip: string): string | null {
169
+ if (!isValidIpv4(ip)) {
170
+ return null;
171
+ }
172
+ const parts = ip.split('.');
173
+ return `${parts[0]}.${parts[1]}.${parts[2]}.`;
174
+ }
@@ -1,12 +1,11 @@
1
1
  import { DevConnectTab } from './DevConnectTab';
2
- import { isCameraKitAvailable } from './cameraKit';
3
2
  import { loadDevConnectPreferences } from './devConnectPreferences';
4
- import { DEFAULT_DAEMON_PORT, DEFAULT_METRO_PORT } from './devConnectUtils';
5
- import { isNativeDevConnectAvailable } from './nativeDevConnect';
3
+ import { DEFAULT_DAEMON_PORT, DEFAULT_METRO_PORT, extractSubnetPrefix } from './devConnectUtils';
4
+ import { getDeviceLocalIp, isNativeDevConnectAvailable } from './nativeDevConnect';
6
5
  import { isSimulator } from './platformDetect';
7
6
  import { daemonClient } from '../../utils/DaemonClient';
8
7
  import type { DebugFeature, DebugFeatureListener } from '../../types';
9
- import type { DevConnectState } from './types';
8
+ import type { DevConnectFeatureControls, DevConnectSettingsPatch, DevConnectState } from './types';
10
9
 
11
10
  export type { DevConnectState } from './types';
12
11
  export {
@@ -24,6 +23,7 @@ export {
24
23
  saveDaemonPort,
25
24
  saveMetroPort,
26
25
  } from './devConnectPreferences';
26
+ export { nativeIsDebugBuild } from './nativeDevConnect';
27
27
 
28
28
  export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
29
29
  const listeners = new Set<DebugFeatureListener>();
@@ -32,7 +32,6 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
32
32
  computerHost: '',
33
33
  metroPort: DEFAULT_METRO_PORT,
34
34
  daemonPort: DEFAULT_DAEMON_PORT,
35
- qrAvailable: isCameraKitAvailable(),
36
35
  nativeMetroAvailable: isNativeDevConnectAvailable(),
37
36
  streaming: daemonClient.isConnected(),
38
37
  };
@@ -45,12 +44,21 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
45
44
  listeners.forEach((listener) => listener());
46
45
  };
47
46
 
48
- return {
47
+ const updateSettings = (patch: DevConnectSettingsPatch) => {
48
+ state = {
49
+ ...state,
50
+ ...patch,
51
+ };
52
+ notify();
53
+ };
54
+
55
+ const feature: DebugFeature<DevConnectState> & DevConnectFeatureControls = {
49
56
  name: 'devConnect',
50
57
  label: 'DevConnect',
51
58
  renderContent: DevConnectTab,
52
59
  setup() {
53
- loadDevConnectPreferences().then((preferences) => {
60
+ daemonClient.setOnConnectionChange(() => notify());
61
+ loadDevConnectPreferences().then(async (preferences) => {
54
62
  state = {
55
63
  ...state,
56
64
  computerHost: preferences.computerHost,
@@ -58,6 +66,19 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
58
66
  daemonPort: preferences.daemonPort,
59
67
  nativeMetroAvailable: isNativeDevConnectAvailable(),
60
68
  };
69
+
70
+ if (!state.isSimulator) {
71
+ try {
72
+ const localIp = await getDeviceLocalIp();
73
+ if (localIp) {
74
+ const prefix = extractSubnetPrefix(localIp);
75
+ if (prefix) {
76
+ state = { ...state, subnetPrefix: prefix };
77
+ }
78
+ }
79
+ } catch { /* subnetPrefix stays undefined */ }
80
+ }
81
+
61
82
  notify();
62
83
  }).catch(() => {
63
84
  notify();
@@ -73,5 +94,8 @@ export const createDevConnectFeature = (): DebugFeature<DevConnectState> => {
73
94
  listeners.delete(listener);
74
95
  };
75
96
  },
97
+ updateSettings,
76
98
  };
99
+
100
+ return feature;
77
101
  };
@@ -6,6 +6,8 @@ interface DebugToolkitDevConnectNativeModule {
6
6
  applyMetroHost: (hostPort: string) => Promise<{ hostPort?: string } | void>;
7
7
  resetMetroHost: () => Promise<void>;
8
8
  getMetroHost?: () => Promise<string | null>;
9
+ getLocalIp?: () => Promise<string | null>;
10
+ isDebugBuild?: () => Promise<boolean>;
9
11
  }
10
12
 
11
13
  type MetroBundleFailureReason =
@@ -124,3 +126,29 @@ export async function resetMetroBundle(): Promise<MetroBundleResult | { ok: true
124
126
  };
125
127
  }
126
128
  }
129
+
130
+ export async function getDeviceLocalIp(): Promise<string | null> {
131
+ const nativeModule = getNativeModule();
132
+ if (!nativeModule?.getLocalIp) {
133
+ return null;
134
+ }
135
+ try {
136
+ const ip = await nativeModule.getLocalIp();
137
+ return typeof ip === 'string' ? ip : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ export async function nativeIsDebugBuild(): Promise<boolean | null> {
144
+ const nativeModule = getNativeModule();
145
+ if (!nativeModule?.isDebugBuild) {
146
+ return null;
147
+ }
148
+ try {
149
+ const result = await nativeModule.isDebugBuild();
150
+ return typeof result === 'boolean' ? result : null;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
@@ -3,7 +3,15 @@ export interface DevConnectState {
3
3
  computerHost: string;
4
4
  metroPort: string;
5
5
  daemonPort: string;
6
- qrAvailable: boolean;
6
+ subnetPrefix?: string;
7
7
  nativeMetroAvailable: boolean;
8
8
  streaming: boolean;
9
9
  }
10
+
11
+ export type DevConnectSettingsPatch = Partial<
12
+ Pick<DevConnectState, 'computerHost' | 'metroPort' | 'daemonPort'>
13
+ >;
14
+
15
+ export interface DevConnectFeatureControls {
16
+ updateSettings?: (patch: DevConnectSettingsPatch) => void;
17
+ }
@@ -1,4 +1,4 @@
1
- import React, { useEffect } from 'react';
1
+ import React, { useEffect, useRef } from 'react';
2
2
  import { DebugToolkitProvider } from '../core/DebugToolkitProvider';
3
3
  import { initializeDebugToolkit } from '../core/initialize';
4
4
  import type { FeatureConfigs } from '../core/initialize';
@@ -42,6 +42,8 @@ export function DebugView({
42
42
  environments,
43
43
  enabled,
44
44
  }: DebugViewProps) {
45
+ const destroyRef = useRef<(() => void) | null>(null);
46
+
45
47
  useEffect(() => {
46
48
  // Build feature config: all enabled by default, user overrides take precedence
47
49
  const resolvedFeatures: FeatureConfigs = {
@@ -61,13 +63,20 @@ export function DebugView({
61
63
  }
62
64
 
63
65
  // Initialize toolkit
64
- const toolkit = initializeDebugToolkit({
66
+ let cancelled = false;
67
+ initializeDebugToolkit({
65
68
  features: resolvedFeatures,
66
69
  enabled,
70
+ }).then((toolkit) => {
71
+ if (!cancelled) {
72
+ destroyRef.current = () => toolkit.destroy();
73
+ }
67
74
  });
68
75
 
69
76
  return () => {
70
- toolkit.destroy();
77
+ cancelled = true;
78
+ destroyRef.current?.();
79
+ destroyRef.current = null;
71
80
  };
72
81
  // eslint-disable-next-line react-hooks/exhaustive-deps
73
82
  }, []);
@@ -186,6 +186,7 @@ export class DaemonClient {
186
186
  private _onEndpointDetected: ((url: string) => void) | undefined;
187
187
  private _restorePromise: Promise<void> | null = null;
188
188
  private _sessionId: SessionInfo | null = null;
189
+ private _onConnectionChange: (() => void) | undefined;
189
190
 
190
191
  constructor(options: DaemonClientOptions) {
191
192
  this._fetch = options.fetch;
@@ -315,6 +316,7 @@ export class DaemonClient {
315
316
  ).remove;
316
317
 
317
318
  this._stream = state;
319
+ this._onConnectionChange?.();
318
320
  this.emitStatus({ state: 'connecting' });
319
321
  this.enqueueSendFullReport();
320
322
  }
@@ -324,6 +326,7 @@ export class DaemonClient {
324
326
  const state = this._stream;
325
327
  this._stream = null;
326
328
  this._sessionId = null;
329
+ this._onConnectionChange?.();
327
330
 
328
331
  if (state.debounceTimer) clearTimeout(state.debounceTimer);
329
332
  if (state.retryTimer) clearTimeout(state.retryTimer);
@@ -360,6 +363,10 @@ export class DaemonClient {
360
363
  this._onEndpointDetected = callback;
361
364
  }
362
365
 
366
+ setOnConnectionChange(callback: (() => void) | undefined): void {
367
+ this._onConnectionChange = callback;
368
+ }
369
+
363
370
  // --- Restore (init-time reconnect) ---
364
371
 
365
372
  async restore(): Promise<void> {