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.
Files changed (103) hide show
  1. package/README.md +12 -5
  2. package/README.zh-CN.md +12 -5
  3. package/lib/commonjs/core/initialize.js +5 -3
  4. package/lib/commonjs/core/initialize.js.map +1 -1
  5. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js +203 -0
  6. package/lib/commonjs/features/devConnect/DevConnectQrScanner.js.map +1 -0
  7. package/lib/commonjs/features/devConnect/DevConnectTab.js +541 -0
  8. package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -0
  9. package/lib/commonjs/features/devConnect/cameraKit.js +54 -0
  10. package/lib/commonjs/features/devConnect/cameraKit.js.map +1 -0
  11. package/lib/commonjs/features/devConnect/devConnectPreferences.js +35 -0
  12. package/lib/commonjs/features/devConnect/devConnectPreferences.js.map +1 -0
  13. package/lib/commonjs/features/devConnect/devConnectUtils.js +53 -0
  14. package/lib/commonjs/features/devConnect/devConnectUtils.js.map +1 -0
  15. package/lib/commonjs/features/devConnect/index.js +92 -0
  16. package/lib/commonjs/features/devConnect/index.js.map +1 -0
  17. package/lib/commonjs/features/devConnect/platformDetect.js +30 -0
  18. package/lib/commonjs/features/devConnect/platformDetect.js.map +1 -0
  19. package/lib/commonjs/features/devConnect/types.js +2 -0
  20. package/lib/commonjs/features/devConnect/types.js.map +1 -0
  21. package/lib/commonjs/index.js +7 -0
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/ui/DebugView.js +1 -0
  24. package/lib/commonjs/ui/DebugView.js.map +1 -1
  25. package/lib/commonjs/ui/panel/DebugPanel.js +0 -25
  26. package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
  27. package/lib/commonjs/utils/debugPreferences.js +2 -1
  28. package/lib/commonjs/utils/debugPreferences.js.map +1 -1
  29. package/lib/module/core/initialize.js +5 -3
  30. package/lib/module/core/initialize.js.map +1 -1
  31. package/lib/module/features/devConnect/DevConnectQrScanner.js +198 -0
  32. package/lib/module/features/devConnect/DevConnectQrScanner.js.map +1 -0
  33. package/lib/module/features/devConnect/DevConnectTab.js +536 -0
  34. package/lib/module/features/devConnect/DevConnectTab.js.map +1 -0
  35. package/lib/module/features/devConnect/cameraKit.js +49 -0
  36. package/lib/module/features/devConnect/cameraKit.js.map +1 -0
  37. package/lib/module/features/devConnect/devConnectPreferences.js +29 -0
  38. package/lib/module/features/devConnect/devConnectPreferences.js.map +1 -0
  39. package/lib/module/features/devConnect/devConnectUtils.js +47 -0
  40. package/lib/module/features/devConnect/devConnectUtils.js.map +1 -0
  41. package/lib/module/features/devConnect/index.js +52 -0
  42. package/lib/module/features/devConnect/index.js.map +1 -0
  43. package/lib/module/features/devConnect/platformDetect.js +26 -0
  44. package/lib/module/features/devConnect/platformDetect.js.map +1 -0
  45. package/lib/module/features/devConnect/types.js +2 -0
  46. package/lib/module/features/devConnect/types.js.map +1 -0
  47. package/lib/module/index.js +1 -1
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/ui/DebugView.js +1 -0
  50. package/lib/module/ui/DebugView.js.map +1 -1
  51. package/lib/module/ui/panel/DebugPanel.js +1 -26
  52. package/lib/module/ui/panel/DebugPanel.js.map +1 -1
  53. package/lib/module/utils/debugPreferences.js +2 -1
  54. package/lib/module/utils/debugPreferences.js.map +1 -1
  55. package/lib/typescript/src/core/initialize.d.ts +1 -0
  56. package/lib/typescript/src/core/initialize.d.ts.map +1 -1
  57. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts +9 -0
  58. package/lib/typescript/src/features/devConnect/DevConnectQrScanner.d.ts.map +1 -0
  59. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts +5 -0
  60. package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -0
  61. package/lib/typescript/src/features/devConnect/cameraKit.d.ts +47 -0
  62. package/lib/typescript/src/features/devConnect/cameraKit.d.ts.map +1 -0
  63. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts +7 -0
  64. package/lib/typescript/src/features/devConnect/devConnectPreferences.d.ts.map +1 -0
  65. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts +12 -0
  66. package/lib/typescript/src/features/devConnect/devConnectUtils.d.ts.map +1 -0
  67. package/lib/typescript/src/features/devConnect/index.d.ts +7 -0
  68. package/lib/typescript/src/features/devConnect/index.d.ts.map +1 -0
  69. package/lib/typescript/src/features/devConnect/platformDetect.d.ts +2 -0
  70. package/lib/typescript/src/features/devConnect/platformDetect.d.ts.map +1 -0
  71. package/lib/typescript/src/features/devConnect/types.d.ts +7 -0
  72. package/lib/typescript/src/features/devConnect/types.d.ts.map +1 -0
  73. package/lib/typescript/src/index.d.ts +2 -0
  74. package/lib/typescript/src/index.d.ts.map +1 -1
  75. package/lib/typescript/src/types/feature.d.ts +1 -1
  76. package/lib/typescript/src/types/feature.d.ts.map +1 -1
  77. package/lib/typescript/src/ui/DebugView.d.ts.map +1 -1
  78. package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
  79. package/lib/typescript/src/utils/debugPreferences.d.ts +1 -0
  80. package/lib/typescript/src/utils/debugPreferences.d.ts.map +1 -1
  81. package/node/daemon/src/console/console.html +63 -15
  82. package/package.json +10 -2
  83. package/src/core/initialize.ts +7 -1
  84. package/src/features/devConnect/DevConnectQrScanner.tsx +173 -0
  85. package/src/features/devConnect/DevConnectTab.tsx +437 -0
  86. package/src/features/devConnect/cameraKit.ts +93 -0
  87. package/src/features/devConnect/devConnectPreferences.ts +33 -0
  88. package/src/features/devConnect/devConnectUtils.ts +59 -0
  89. package/src/features/devConnect/index.ts +64 -0
  90. package/src/features/devConnect/platformDetect.ts +26 -0
  91. package/src/features/devConnect/types.ts +6 -0
  92. package/src/index.ts +2 -0
  93. package/src/types/feature.ts +2 -1
  94. package/src/ui/DebugView.tsx +1 -0
  95. package/src/ui/panel/DebugPanel.tsx +1 -23
  96. package/src/utils/debugPreferences.ts +1 -0
  97. package/lib/commonjs/ui/panel/StreamingSettingsModal.js +0 -495
  98. package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +0 -1
  99. package/lib/module/ui/panel/StreamingSettingsModal.js +0 -490
  100. package/lib/module/ui/panel/StreamingSettingsModal.js.map +0 -1
  101. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +0 -8
  102. package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +0 -1
  103. package/src/ui/panel/StreamingSettingsModal.tsx +0 -528
@@ -346,6 +346,7 @@ header h1 span{color:var(--text3);font-weight:400}
346
346
  padding:0 8px;height:100%;display:flex;align-items:center;justify-content:center;
347
347
  color:var(--text3);font-size:10px;transition:transform .2s,color .15s;
348
348
  }
349
+ .log-expand.open{transform:rotate(90deg)}
349
350
  .log-entry:hover .log-expand{color:var(--cyan)}
350
351
  .log-entry.expanded .log-expand{transform:rotate(90deg);color:var(--cyan)}
351
352
 
@@ -363,15 +364,20 @@ header h1 span{color:var(--text3);font-weight:400}
363
364
  .detail-sections{display:flex;flex-direction:column;gap:10px}
364
365
  .detail-section{
365
366
  border:1px solid var(--border);border-radius:var(--radius);
366
- background:rgba(8,12,22,.35);overflow:hidden;
367
+ background:var(--bg);overflow:hidden;
367
368
  }
368
369
  .detail-section-header{
369
370
  display:flex;align-items:center;justify-content:space-between;
370
- padding:7px 12px;border-bottom:1px solid var(--border);
371
- background:rgba(0,229,255,.03);
371
+ padding:9px 14px 9px 18px;border-bottom:1px solid var(--border);
372
+ background:linear-gradient(135deg,rgba(0,229,255,.07) 0%,rgba(0,229,255,.02) 100%);
373
+ position:relative;
374
+ }
375
+ .detail-section-header::before{
376
+ content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
377
+ background:var(--cyan);border-radius:0 2px 2px 0;
372
378
  }
373
379
  .detail-section-title{
374
- font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
380
+ font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
375
381
  color:var(--cyan);font-family:var(--font-mono);
376
382
  }
377
383
  .detail-section-copy{
@@ -380,7 +386,7 @@ header h1 span{color:var(--text3);font-weight:400}
380
386
  transition:color .15s;
381
387
  }
382
388
  .detail-section-copy:hover{color:var(--cyan)}
383
- .detail-section-body{padding:0}
389
+ .detail-section-body{padding:10px 14px 10px 18px}
384
390
 
385
391
  /* Detail key-value table */
386
392
  .detail-table{width:100%;border-collapse:collapse}
@@ -429,25 +435,30 @@ header h1 span{color:var(--text3);font-weight:400}
429
435
  .network-meta-line span{white-space:nowrap}
430
436
 
431
437
  /* Collapsible sections */
432
- .collapse-section{border:1px solid var(--border);border-radius:var(--radius);background:rgba(8,12,22,.35);overflow:hidden;margin-bottom:0}
438
+ .collapse-section{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg);overflow:hidden;margin-bottom:0}
433
439
  .collapse-header{
434
440
  display:flex;align-items:center;justify-content:space-between;
435
- padding:7px 12px;border-bottom:1px solid var(--border);
436
- background:rgba(0,229,255,.03);cursor:pointer;user-select:none;
441
+ padding:9px 14px 9px 18px;border-bottom:1px solid var(--border);
442
+ background:linear-gradient(135deg,rgba(0,229,255,.07) 0%,rgba(0,229,255,.02) 100%);
443
+ cursor:pointer;user-select:none;position:relative;
444
+ }
445
+ .collapse-header::before{
446
+ content:'';position:absolute;left:0;top:0;bottom:0;width:3px;
447
+ background:var(--cyan);border-radius:0 2px 2px 0;
437
448
  }
438
- .collapse-header:hover{background:rgba(0,229,255,.06)}
439
- .collapse-header-left{display:flex;align-items:center;gap:6px}
449
+ .collapse-header:hover{background:linear-gradient(135deg,rgba(0,229,255,.12) 0%,rgba(0,229,255,.04) 100%)}
450
+ .collapse-header-left{display:flex;align-items:center;gap:8px}
440
451
  .collapse-arrow{
441
- font-size:9px;color:var(--text3);transition:transform .2s;display:inline-block;
452
+ font-size:10px;color:var(--cyan);transition:transform .2s;display:inline-block;opacity:.7;
442
453
  }
443
- .collapse-arrow.open{transform:rotate(90deg)}
454
+ .collapse-arrow.open{transform:rotate(90deg);opacity:1}
444
455
  .collapse-title{
445
- font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
456
+ font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.1em;
446
457
  color:var(--cyan);font-family:var(--font-mono);
447
458
  }
448
459
  .collapse-body{display:none;padding:0}
449
460
  .collapse-body.open{display:block}
450
- .section-body-inner{padding:4px 0}
461
+ .section-body-inner{padding:10px 14px 10px 18px}
451
462
  .method-badge{
452
463
  font-family:var(--font-mono);font-size:11px;font-weight:700;
453
464
  padding:3px 10px;border-radius:3px;letter-spacing:.04em;
@@ -908,6 +919,35 @@ mark{
908
919
  entry.data);
909
920
  }
910
921
 
922
+ function estimateByteSize(value) {
923
+ if (value == null) return 0;
924
+ if (typeof value === 'string') return new Blob([value]).size;
925
+ try { return new Blob([JSON.stringify(value)]).size; } catch { return 0; }
926
+ }
927
+
928
+ function formatSize(bytes) {
929
+ if (!bytes || bytes <= 0) return '';
930
+ if (bytes < 1024) return bytes + ' B';
931
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
932
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
933
+ }
934
+
935
+ function getNetworkSize(entry) {
936
+ var request = readObject(entry.request) || entry;
937
+ var response = readObject(entry.response);
938
+ var respSize = 0;
939
+ if (response) {
940
+ var headers = response.headers;
941
+ if (headers) {
942
+ var cl = headers['content-length'] || headers['Content-Length'] || headers['contentLength'] || headers['ContentLength'];
943
+ if (cl) { var n = parseInt(cl, 10); if (n > 0) respSize = n; }
944
+ }
945
+ if (!respSize && response.data != null) respSize = estimateByteSize(response.data);
946
+ }
947
+ var reqSize = request.body != null ? estimateByteSize(request.body) : 0;
948
+ return { req: reqSize, res: respSize };
949
+ }
950
+
911
951
  function renderNetworkDetails(entry) {
912
952
  var request = readObject(entry.request) || entry;
913
953
  var response = readObject(entry.response);
@@ -924,6 +964,11 @@ mark{
924
964
  metaParts.push(response.status + (response.statusText ? ' ' + response.statusText : ''));
925
965
  }
926
966
  if (entry.duration !== undefined) metaParts.push(entry.duration + 'ms');
967
+ var sizes = getNetworkSize(entry);
968
+ var sizeParts = [];
969
+ if (sizes.req > 0) sizeParts.push('req: ' + formatSize(sizes.req));
970
+ if (sizes.res > 0) sizeParts.push('res: ' + formatSize(sizes.res));
971
+ if (sizeParts.length) metaParts.push(sizeParts.join(' / '));
927
972
  if (entry.timestamp) metaParts.push(formatTimeShort(new Date(entry.timestamp).toISOString()));
928
973
  if (metaParts.length) {
929
974
  html += '<div class="network-meta-line">' + metaParts.map(function(p) { return '<span>' + escapeHtml(p) + '</span>'; }).join('') + '</div>';
@@ -1308,7 +1353,7 @@ mark{
1308
1353
  html += '</div>';
1309
1354
  html += '<div class="log-status">' + statusBadge(entry) + '</div>';
1310
1355
  html += '<div class="log-copy" onclick="event.stopPropagation();copyEntryJSON(\'' + rowId + '\')"><button class="copy-btn" title="Copy entry JSON">&#9112;</button></div>';
1311
- html += '<div class="log-expand">' + (isExpanded ? '&#9654;' : '&#9654;') + '</div>';
1356
+ html += '<div class="log-expand' + (isExpanded ? ' open' : '') + '" id="arrow-' + rowId + '">&#9654;</div>';
1312
1357
  html += '</div>';
1313
1358
  html += '<div class="log-detail" id="detail-' + rowId + '">';
1314
1359
  html += '<div class="log-detail-inner"><div class="detail-sections">';
@@ -1479,12 +1524,15 @@ mark{
1479
1524
  window.toggleRow = function(rowId) {
1480
1525
  var entry = document.getElementById('entry-' + rowId);
1481
1526
  var detail = document.getElementById('detail-' + rowId);
1527
+ var arrow = document.getElementById('arrow-' + rowId);
1482
1528
  if (!entry || !detail) return;
1483
1529
  expandedRows[rowId] = !expandedRows[rowId];
1484
1530
  if (expandedRows[rowId]) {
1485
1531
  entry.classList.add('expanded');
1532
+ if (arrow) arrow.classList.add('open');
1486
1533
  } else {
1487
1534
  entry.classList.remove('expanded');
1535
+ if (arrow) arrow.classList.remove('open');
1488
1536
  }
1489
1537
  };
1490
1538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "3.1.5",
3
+ "version": "3.2.1",
4
4
  "description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -52,11 +52,19 @@
52
52
  "peerDependencies": {
53
53
  "@react-native-clipboard/clipboard": ">=1.0.0",
54
54
  "react": ">=18.0.0",
55
- "react-native": ">=0.72.0"
55
+ "react-native": ">=0.72.0",
56
+ "react-native-camera-kit": ">=18.0.0",
57
+ "expo-camera": ">=15.0.0"
56
58
  },
57
59
  "peerDependenciesMeta": {
58
60
  "@react-native-clipboard/clipboard": {
59
61
  "optional": true
62
+ },
63
+ "react-native-camera-kit": {
64
+ "optional": true
65
+ },
66
+ "expo-camera": {
67
+ "optional": true
60
68
  }
61
69
  },
62
70
  "devDependencies": {
@@ -11,6 +11,7 @@ import { createTrackFeature } from '../features/track';
11
11
  import type { TrackFeatureConfig } from '../features/track';
12
12
  import { createEnvironmentFeature } from '../features/environment';
13
13
  import { createClipboardFeature } from '../features/clipboard';
14
+ import { createDevConnectFeature, restoreDevConnectSettingsToDaemon } from '../features/devConnect';
14
15
  import { daemonClient } from '../utils/DaemonClient';
15
16
  import type { AnyDebugFeature, BuiltInFeatureName } from '../types';
16
17
 
@@ -25,6 +26,7 @@ export interface FeatureConfigs {
25
26
  track?: boolean | TrackFeatureConfig;
26
27
  environment?: Parameters<typeof createEnvironmentFeature>[0];
27
28
  clipboard?: boolean;
29
+ devConnect?: boolean;
28
30
  }
29
31
 
30
32
  export interface InitializeOptions {
@@ -43,6 +45,7 @@ const featureRegistry: Record<BuiltInFeatureName, (config?: any) => AnyDebugFeat
43
45
  track: createTrackFeature,
44
46
  environment: createEnvironmentFeature,
45
47
  clipboard: createClipboardFeature,
48
+ devConnect: createDevConnectFeature,
46
49
  };
47
50
 
48
51
  const DEFAULT_FEATURES: BuiltInFeatureName[] = [
@@ -52,6 +55,7 @@ const DEFAULT_FEATURES: BuiltInFeatureName[] = [
52
55
  'zustand',
53
56
  'track',
54
57
  'clipboard',
58
+ 'devConnect',
55
59
  ];
56
60
 
57
61
  function resolveFeatureConfigs(configs: FeatureConfigs): AnyDebugFeature[] {
@@ -119,7 +123,9 @@ export function initializeDebugToolkit(
119
123
  DebugToolkit.hideLauncher();
120
124
  }
121
125
 
122
- daemonClient.restore().catch(() => {});
126
+ restoreDevConnectSettingsToDaemon()
127
+ .then(() => daemonClient.restore(), () => daemonClient.restore())
128
+ .catch(() => {});
123
129
 
124
130
  return DebugToolkit;
125
131
  } catch (error) {
@@ -0,0 +1,173 @@
1
+ import React, { Component, useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TouchableOpacity,
8
+ View,
9
+ } from 'react-native';
10
+
11
+ import { Colors } from '../../ui/theme/colors';
12
+ import {
13
+ getScannerModule,
14
+ type CameraKitReadCodeEvent,
15
+ type ExpoCameraScanResult,
16
+ } from './cameraKit';
17
+ import { parseMetroQrPayload } from './devConnectUtils';
18
+
19
+ // ─── 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
+
50
+ interface DevConnectQrScannerProps {
51
+ visible: boolean;
52
+ onClose: () => void;
53
+ onScanHost: (host: string) => void;
54
+ }
55
+
56
+ export function DevConnectQrScanner({ visible, onClose, onScanHost }: DevConnectQrScannerProps) {
57
+ const scannedRef = useRef(false);
58
+ const [error, setError] = useState<string | null>(null);
59
+ const [cameraFailed, setCameraFailed] = useState(false);
60
+ const scanner = getScannerModule();
61
+
62
+ useEffect(() => {
63
+ if (visible) {
64
+ scannedRef.current = false;
65
+ setError(null);
66
+ setCameraFailed(false);
67
+ }
68
+ }, [visible]);
69
+
70
+ const handleScanned = useCallback((rawValue: string) => {
71
+ if (scannedRef.current) return;
72
+ if (typeof rawValue !== 'string') return;
73
+
74
+ const parsed = parseMetroQrPayload(rawValue);
75
+ if (!parsed) {
76
+ setError('QR code does not contain a supported Metro URL.');
77
+ return;
78
+ }
79
+
80
+ scannedRef.current = true;
81
+ setError(null);
82
+ onScanHost(parsed.computerHost);
83
+ onClose();
84
+ }, [onClose, onScanHost]);
85
+
86
+ const handleCameraKitRead = useCallback((event: CameraKitReadCodeEvent) => {
87
+ handleScanned(event.nativeEvent?.codeStringValue ?? '');
88
+ }, [handleScanned]);
89
+
90
+ const handleExpoScanned = useCallback((result: ExpoCameraScanResult) => {
91
+ handleScanned(result.value ?? '');
92
+ }, [handleScanned]);
93
+
94
+ const handleCameraError = useCallback((_msg: string) => {
95
+ setCameraFailed(true);
96
+ }, []);
97
+
98
+ if (!visible || !scanner) return null;
99
+
100
+ return (
101
+ <Modal visible={visible} animationType="slide" onRequestClose={onClose}>
102
+ <View style={styles.container}>
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
+ )}
131
+ <View style={styles.footer}>
132
+ {!cameraFailed && !error && <Text style={styles.hint}>Scan a Metro QR code.</Text>}
133
+ {error && <Text style={styles.error}>{error}</Text>}
134
+ <TouchableOpacity style={styles.closeButton} onPress={onClose} activeOpacity={0.7}>
135
+ <Text style={styles.closeButtonText}>Close</Text>
136
+ </TouchableOpacity>
137
+ </View>
138
+ <Pressable style={styles.topClose} onPress={onClose}>
139
+ <Text style={styles.topCloseText}>Close</Text>
140
+ </Pressable>
141
+ </View>
142
+ </Modal>
143
+ );
144
+ }
145
+
146
+ const styles = StyleSheet.create({
147
+ container: { flex: 1, backgroundColor: '#000' },
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' },
152
+ footer: { padding: 16, backgroundColor: Colors.surface },
153
+ hint: { fontSize: 13, color: Colors.textSecondary, marginBottom: 12 },
154
+ error: { fontSize: 13, color: Colors.error, marginBottom: 12 },
155
+ closeButton: {
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ paddingVertical: 11,
159
+ borderRadius: 10,
160
+ backgroundColor: Colors.primary,
161
+ },
162
+ closeButtonText: { color: '#fff', fontSize: 14, fontWeight: '600' },
163
+ topClose: {
164
+ position: 'absolute',
165
+ top: 48,
166
+ right: 16,
167
+ paddingHorizontal: 12,
168
+ paddingVertical: 8,
169
+ borderRadius: 8,
170
+ backgroundColor: 'rgba(0,0,0,0.55)',
171
+ },
172
+ topCloseText: { color: '#fff', fontSize: 13, fontWeight: '600' },
173
+ });