react-native-debug-toolkit 3.2.0 → 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.
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  Modal,
4
4
  Pressable,
@@ -16,6 +16,37 @@ import {
16
16
  } from './cameraKit';
17
17
  import { parseMetroQrPayload } from './devConnectUtils';
18
18
 
19
+ // ─── Camera Error Boundary ─────────────────────────────────
20
+
21
+ interface CameraBoundaryProps {
22
+ children: React.ReactNode;
23
+ onCameraError: (msg: string) => void;
24
+ }
25
+
26
+ interface CameraBoundaryState {
27
+ hasError: boolean;
28
+ }
29
+
30
+ class CameraErrorBoundary extends Component<CameraBoundaryProps, CameraBoundaryState> {
31
+ state: CameraBoundaryState = { hasError: false };
32
+
33
+ static getDerivedStateFromError(): CameraBoundaryState {
34
+ return { hasError: true };
35
+ }
36
+
37
+ componentDidCatch(error: Error) {
38
+ console.warn('[DevConnect] Camera error:', error.message);
39
+ this.props.onCameraError(error.message || 'Camera failed to initialize.');
40
+ }
41
+
42
+ render() {
43
+ if (this.state.hasError) return null;
44
+ return this.props.children;
45
+ }
46
+ }
47
+
48
+ // ─── QR Scanner ─────────────────────────────────────────────
49
+
19
50
  interface DevConnectQrScannerProps {
20
51
  visible: boolean;
21
52
  onClose: () => void;
@@ -25,12 +56,14 @@ interface DevConnectQrScannerProps {
25
56
  export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
26
57
  const scannedRef = useRef(false);
27
58
  const [error, setError] = useState<string | null>(null);
59
+ const [cameraFailed, setCameraFailed] = useState(false);
28
60
  const scanner = getScannerModule();
29
61
 
30
62
  useEffect(() => {
31
63
  if (visible) {
32
64
  scannedRef.current = false;
33
65
  setError(null);
66
+ setCameraFailed(false);
34
67
  }
35
68
  }, [visible]);
36
69
 
@@ -58,31 +91,46 @@ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnect
58
91
  handleScanned(result.value ?? '');
59
92
  }, [handleScanned]);
60
93
 
94
+ const handleCameraError = useCallback((_msg: string) => {
95
+ setCameraFailed(true);
96
+ }, []);
97
+
61
98
  if (!visible || !scanner) return null;
62
99
 
63
100
  return (
64
101
  <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
65
102
  <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}
103
+ {!cameraFailed && (
104
+ <CameraErrorBoundary onCameraError={handleCameraError}>
105
+ {scanner.kind === 'camera-kit' && scanner.CameraKit ? (
106
+ <scanner.CameraKit.Camera
107
+ style={styles.camera}
108
+ cameraType={scanner.CameraKit.CameraType?.Back}
109
+ scanBarcode
110
+ onReadCode={handleCameraKitRead}
111
+ showFrame
112
+ laserColor={Colors.primary}
113
+ frameColor={Colors.primary}
114
+ allowedBarcodeTypes={['qr']}
115
+ />
116
+ ) : scanner.kind === 'expo-camera' && scanner.ExpoCamera ? (
117
+ <scanner.ExpoCamera.Camera
118
+ style={styles.camera}
119
+ onBarCodeScanned={handleExpoScanned}
120
+ barCodeScannerSettings={{ barCodeTypes: ['qr'] }}
121
+ />
122
+ ) : null}
123
+ </CameraErrorBoundary>
124
+ )}
125
+ {cameraFailed && (
126
+ <View style={styles.cameraFallback}>
127
+ <Text style={styles.cameraFallbackText}>Camera unavailable.</Text>
128
+ <Text style={styles.cameraFallbackHint}>Please enter computer IP manually.</Text>
129
+ </View>
130
+ )}
84
131
  <View style={styles.footer}>
85
- {error ? <Text style={styles.error}>{error}</Text> : <Text style={styles.hint}>Scan a Metro QR code.</Text>}
132
+ {!cameraFailed && !error && <Text style={styles.hint}>Scan a Metro QR code.</Text>}
133
+ {error && <Text style={styles.error}>{error}</Text>}
86
134
  <TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
87
135
  <Text style={styles.closeButtonText}>Close</Text>
88
136
  </TouchableOpacity>
@@ -98,6 +146,9 @@ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnect
98
146
  const styles = StyleSheet.create({
99
147
  container: { flex: 1, backgroundColor: '#000' },
100
148
  camera: { flex: 1 },
149
+ cameraFallback: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
150
+ cameraFallbackText: { fontSize: 16, color: '#fff', fontWeight: '600', marginBottom: 8 },
151
+ cameraFallbackHint: { fontSize: 13, color: 'rgba(255,255,255,0.6)', textAlign: 'center' },
101
152
  footer: { padding: 16, backgroundColor: Colors.surface },
102
153
  hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
103
154
  error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  KeyboardAvoidingView,
4
4
  Platform,
@@ -47,9 +47,12 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
47
47
  setSyncState(snapshot.streaming ? 'running' : 'idle');
48
48
  }, [snapshot.computerHost, snapshot.streaming]);
49
49
 
50
- const metroUrls = isSim
51
- ? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
52
- : buildMetroUrls(computerHost);
50
+ const metroUrls = useMemo(
51
+ () => isSim
52
+ ? { expUrl: `exp://localhost:${METRO_PORT}`, httpUrl: `http://localhost:${METRO_PORT}` }
53
+ : buildMetroUrls(computerHost),
54
+ [isSim, computerHost],
55
+ );
53
56
 
54
57
  const handleHostChange = useCallback((value: string) => {
55
58
  setComputerHost(value);
@@ -174,12 +177,17 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
174
177
  }
175
178
  }, [configureDaemon, validateSettings]);
176
179
 
180
+ const messageTimerRef = useRef<ReturnType<typeof setTimeout>>();
181
+
177
182
  const copyUrl = useCallback((label: string, url: string) => {
178
183
  copyToComputer(url, { label });
179
184
  setMessage('Copied to computer output.');
180
- setTimeout(() => setMessage(null), 1500);
185
+ clearTimeout(messageTimerRef.current);
186
+ messageTimerRef.current = setTimeout(() => setMessage(null), 1500);
181
187
  }, []);
182
188
 
189
+ useEffect(() => () => clearTimeout(messageTimerRef.current), []);
190
+
183
191
  const canConnect = isSim || Boolean(normalizeComputerHost(computerHost));
184
192
  const busy = sending || syncState === 'checking';
185
193
 
@@ -244,24 +252,60 @@ export function DevConnectTab({ snapshot }: DebugFeatureRenderProps<DevConnectSt
244
252
  {message ? <Text style={styles.message}>{message}</Text> : null}
245
253
 
246
254
  <View style={styles.section}>
247
- <Text style={styles.sectionTitle}>Metro Bundler</Text>
248
- {metroUrls ? (
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
+ ) : (
249
265
  <>
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>
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>
255
285
  </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>
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>
261
307
  </View>
262
308
  </>
263
- ) : (
264
- <Text style={styles.hint}>Enter a computer IP to show Metro URLs.</Text>
265
309
  )}
266
310
  </View>
267
311
  </ScrollView>
@@ -288,7 +332,8 @@ const styles = StyleSheet.create({
288
332
  },
289
333
  badgeText: { fontSize: 13, fontWeight: '500', color: Colors.primary },
290
334
  section: { marginBottom: 14 },
291
- sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 8 },
335
+ sectionTitle: { fontSize: 14, fontWeight: '600', color: Colors.text, marginBottom: 4 },
336
+ sectionDesc: { fontSize: 12, color: Colors.textSecondary, marginBottom: 10, lineHeight: 17 },
292
337
  label: { fontSize: 13, fontWeight: '500', color: Colors.textSecondary, marginBottom: 6 },
293
338
  inputRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
294
339
  input: {
@@ -339,6 +384,41 @@ const styles = StyleSheet.create({
339
384
  buttonDisabled: { opacity: 0.5 },
340
385
  message: { fontSize: 12, lineHeight: 17, color: Colors.textSecondary, marginBottom: 12 },
341
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
+ },
342
422
  urlRow: {
343
423
  flexDirection: 'row',
344
424
  alignItems: 'center',