接入移动端原生扫码能力
新增 scanner.scanQrCode HostBridge 契约与二维码结果归一。 接入 Expo Camera 扫码 overlay 并保留 H5 浏览器扫码回退。 让 Tauri 白名单识别扫码 method 但不声明桌面扫码能力。 同步原生壳门禁、Expo 权限检查、方案文档和共享决策记录。
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Camera,
|
||||
CameraView,
|
||||
type BarcodeScanningResult,
|
||||
} from 'expo-camera';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import {
|
||||
cancelQrCodeScan,
|
||||
completeQrCodeScan,
|
||||
failQrCodeScan,
|
||||
subscribeQrScannerState,
|
||||
} from '../host-bridge/scanner';
|
||||
|
||||
export function QrScannerOverlay() {
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [requestKey, setRequestKey] = useState(0);
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [scanCompleted, setScanCompleted] = useState(false);
|
||||
|
||||
useEffect(() => subscribeQrScannerState((state) => {
|
||||
setIsActive(state.active);
|
||||
setRequestKey(state.requestKey);
|
||||
}), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
setHasPermission(false);
|
||||
setScanCompleted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let disposed = false;
|
||||
setHasPermission(false);
|
||||
setScanCompleted(false);
|
||||
void Camera.requestCameraPermissionsAsync()
|
||||
.then((permission) => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
if (!permission.granted) {
|
||||
failQrCodeScan('camera permission denied');
|
||||
return;
|
||||
}
|
||||
setHasPermission(true);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!disposed) {
|
||||
failQrCodeScan('camera permission unavailable');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [isActive, requestKey]);
|
||||
|
||||
const handleBarcodeScanned = useCallback(
|
||||
(result: BarcodeScanningResult) => {
|
||||
if (scanCompleted || result.type !== 'qr') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (completeQrCodeScan(result.data)) {
|
||||
setScanCompleted(true);
|
||||
}
|
||||
},
|
||||
[scanCompleted],
|
||||
);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{hasPermission ? (
|
||||
<CameraView
|
||||
style={styles.camera}
|
||||
facing="back"
|
||||
onBarcodeScanned={scanCompleted ? undefined : handleBarcodeScanned}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ['qr'],
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View pointerEvents="none" style={styles.frame} />
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="关闭扫码"
|
||||
onPress={cancelQrCodeScan}
|
||||
style={styles.closeButton}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>关闭</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: '#0c0a09',
|
||||
},
|
||||
camera: {
|
||||
flex: 1,
|
||||
},
|
||||
frame: {
|
||||
position: 'absolute',
|
||||
top: '22%',
|
||||
right: 42,
|
||||
bottom: '22%',
|
||||
left: 42,
|
||||
borderWidth: 2,
|
||||
borderColor: '#fffdf9',
|
||||
borderRadius: 8,
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 20,
|
||||
minWidth: 76,
|
||||
minHeight: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fffdf9',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
closeButtonText: {
|
||||
color: '#211a16',
|
||||
fontSize: 15,
|
||||
fontWeight: '700',
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
shouldBlockMobileWebViewNavigationRequest,
|
||||
} from './webViewPolicy';
|
||||
import { parseMobileWebViewHistoryStateMessage } from './webViewHistory';
|
||||
import { QrScannerOverlay } from './QrScannerOverlay';
|
||||
|
||||
function buildHostBridgeMessageScript(message: unknown) {
|
||||
return `window.dispatchEvent(new MessageEvent('message', { data: ${JSON.stringify(
|
||||
@@ -371,6 +372,7 @@ export default function ShellApp() {
|
||||
</Pressable>
|
||||
</View>
|
||||
) : null}
|
||||
<QrScannerOverlay />
|
||||
</SafeAreaView>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user