接入移动端原生扫码能力
新增 scanner.scanQrCode HostBridge 契约与二维码结果归一。 接入 Expo Camera 扫码 overlay 并保留 H5 浏览器扫码回退。 让 Tauri 白名单识别扫码 method 但不声明桌面扫码能力。 同步原生壳门禁、Expo 权限检查、方案文档和共享决策记录。
This commit is contained in:
@@ -8,6 +8,8 @@ const appPath = new URL('../App.tsx', import.meta.url);
|
||||
const appSource = fs.readFileSync(appPath, 'utf8');
|
||||
const shellAppPath = new URL('../src/shell/ShellApp.tsx', import.meta.url);
|
||||
const shellAppSource = fs.readFileSync(shellAppPath, 'utf8');
|
||||
const qrScannerOverlayPath = new URL('../src/shell/QrScannerOverlay.tsx', import.meta.url);
|
||||
const qrScannerOverlaySource = fs.readFileSync(qrScannerOverlayPath, 'utf8');
|
||||
const bridgePath = new URL('../src/host-bridge/bridge.ts', import.meta.url);
|
||||
const bridgeSource = fs.readFileSync(bridgePath, 'utf8');
|
||||
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
|
||||
@@ -421,6 +423,7 @@ for (const [scriptName, expected] of Object.entries({
|
||||
for (const [dependency, expected] of Object.entries({
|
||||
'@expo/metro-runtime': '^56.0.15',
|
||||
expo: '^56.0.12',
|
||||
'expo-camera': '56.0.8',
|
||||
'expo-clipboard': '^56.0.4',
|
||||
'expo-document-picker': '^56.0.4',
|
||||
'expo-file-system': '^56.0.8',
|
||||
@@ -455,6 +458,7 @@ for (const [dependency, expected] of Object.entries({
|
||||
for (const [dependency, expected] of Object.entries({
|
||||
'@expo/metro-runtime': '56.0.15',
|
||||
expo: '56.0.12',
|
||||
'expo-camera': '56.0.8',
|
||||
'expo-clipboard': '56.0.4',
|
||||
'expo-document-picker': '56.0.4',
|
||||
'expo-file-system': '56.0.8',
|
||||
@@ -551,6 +555,7 @@ const sharedPayloadBoundaryImports = [
|
||||
'HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES',
|
||||
'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES',
|
||||
'HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES',
|
||||
'normalizeHostBridgeQrCodeValue',
|
||||
'HOST_BRIDGE_TEXT_MIME_TYPES',
|
||||
];
|
||||
for (const boundaryImport of sharedPayloadBoundaryImports) {
|
||||
@@ -574,6 +579,7 @@ const forbiddenLocalPayloadBoundaryDeclarations = [
|
||||
'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES',
|
||||
'HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES',
|
||||
'HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES',
|
||||
'HOST_BRIDGE_QR_CODE_VALUE_MAX_LENGTH',
|
||||
'HOST_BRIDGE_TEXT_MIME_TYPES',
|
||||
'HOST_BRIDGE_IMAGE_MIME_TYPES',
|
||||
'HOST_BRIDGE_AUDIO_MIME_TYPES',
|
||||
@@ -1119,6 +1125,7 @@ if (shellAppSource.includes('127.0.0.1:3000')) {
|
||||
}
|
||||
|
||||
for (const dependency of [
|
||||
'expo-camera',
|
||||
'expo-file-system',
|
||||
'expo-document-picker',
|
||||
'expo-image-picker',
|
||||
@@ -1152,6 +1159,20 @@ if (Array.isArray(imagePickerPlugin)) {
|
||||
}
|
||||
}
|
||||
|
||||
const cameraPlugin = appConfig.plugins?.find((plugin) =>
|
||||
Array.isArray(plugin) ? plugin[0] === 'expo-camera' : plugin === 'expo-camera',
|
||||
);
|
||||
if (!cameraPlugin) {
|
||||
throw new Error('mobile shell camera plugin is missing');
|
||||
}
|
||||
|
||||
if (Array.isArray(cameraPlugin)) {
|
||||
const pluginOptions = cameraPlugin[1] ?? {};
|
||||
if (typeof pluginOptions.cameraPermission !== 'string') {
|
||||
throw new Error('mobile shell camera permission text is missing');
|
||||
}
|
||||
}
|
||||
|
||||
const notificationsPlugin = appConfig.plugins?.find((plugin) =>
|
||||
Array.isArray(plugin) ? plugin[0] === 'expo-notifications' : plugin === 'expo-notifications',
|
||||
);
|
||||
@@ -1210,6 +1231,7 @@ for (const snippet of [
|
||||
'file.exportImage',
|
||||
'file.importImage',
|
||||
'file.captureImage',
|
||||
'scanner.scanQrCode',
|
||||
'file.importAudio',
|
||||
'file.exportAudio',
|
||||
'clipboard.readText',
|
||||
@@ -1228,6 +1250,12 @@ for (const snippet of [
|
||||
'ImagePicker.launchCameraAsync',
|
||||
'ImagePicker.requestMediaLibraryPermissionsAsync',
|
||||
'ImagePicker.requestCameraPermissionsAsync',
|
||||
'scanQrCode',
|
||||
'normalizeHostBridgeQrCodeValue',
|
||||
'completeQrCodeScan',
|
||||
'cancelQrCodeScan',
|
||||
'failQrCodeScan',
|
||||
'subscribeQrScannerState',
|
||||
'MOBILE_AUDIO_DOCUMENT_PICKER_TYPES',
|
||||
"'audio/*'",
|
||||
'File(asset.uri)',
|
||||
@@ -1257,6 +1285,27 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'CameraView',
|
||||
'Camera.requestCameraPermissionsAsync',
|
||||
'type BarcodeScanningResult',
|
||||
'onBarcodeScanned',
|
||||
'barcodeScannerSettings',
|
||||
"barcodeTypes: ['qr']",
|
||||
'completeQrCodeScan(result.data)',
|
||||
'failQrCodeScan',
|
||||
'cancelQrCodeScan',
|
||||
'subscribeQrScannerState',
|
||||
]) {
|
||||
if (!qrScannerOverlaySource.includes(snippet)) {
|
||||
throw new Error(`mobile shell QR scanner overlay missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shellAppSource.includes('<QrScannerOverlay />')) {
|
||||
throw new Error('mobile shell ShellApp must render the QR scanner overlay');
|
||||
}
|
||||
|
||||
const capabilityQuerySnippet = "capabilities: MOBILE_HOST_CAPABILITIES";
|
||||
if (shellAppSource.includes(capabilityQuerySnippet)) {
|
||||
throw new Error('mobile shell URL must resolve platform-aware capabilities');
|
||||
@@ -1323,6 +1372,7 @@ for (const capability of [
|
||||
'file.exportImage',
|
||||
'file.importImage',
|
||||
'file.captureImage',
|
||||
'scanner.scanQrCode',
|
||||
'file.importAudio',
|
||||
'file.exportAudio',
|
||||
'haptics.impact',
|
||||
|
||||
@@ -169,7 +169,11 @@ assertEqual(
|
||||
'resize',
|
||||
'Android software keyboard layout mode',
|
||||
);
|
||||
assertSameList(expoConfig.android?.permissions ?? [], [], 'Android explicit permissions');
|
||||
assertSameList(
|
||||
expoConfig.android?.permissions ?? [],
|
||||
['android.permission.CAMERA'],
|
||||
'Android explicit permissions',
|
||||
);
|
||||
assertIncludes(
|
||||
expoConfig.android?.blockedPermissions,
|
||||
'android.permission.MANAGE_EXTERNAL_STORAGE',
|
||||
@@ -262,6 +266,16 @@ assertEqual(
|
||||
'image picker microphone permission',
|
||||
);
|
||||
|
||||
const cameraPlugin = findPlugin('expo-camera');
|
||||
if (!Array.isArray(cameraPlugin)) {
|
||||
throw new Error('Expo config camera plugin is missing options');
|
||||
}
|
||||
assertEqual(
|
||||
typeof cameraPlugin[1]?.cameraPermission,
|
||||
'string',
|
||||
'camera permission text type',
|
||||
);
|
||||
|
||||
const notificationsPlugin = findPlugin('expo-notifications');
|
||||
if (!Array.isArray(notificationsPlugin)) {
|
||||
throw new Error('Expo config notifications plugin is missing options');
|
||||
|
||||
Reference in New Issue
Block a user