229 lines
5.2 KiB
TypeScript
229 lines
5.2 KiB
TypeScript
import { CameraView, useCameraPermissions } from "expo-camera";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Modal,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
|
|
interface ScanQRCodeProps {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
onScanned: (data: string) => void;
|
|
}
|
|
|
|
export default function ScanQRCode({
|
|
visible,
|
|
onClose,
|
|
onScanned,
|
|
}: ScanQRCodeProps) {
|
|
const [permission, requestPermission] = useCameraPermissions();
|
|
const [scanned, setScanned] = useState(false);
|
|
const cameraRef = useRef(null);
|
|
|
|
// Request camera permission when component mounts or when visible changes to true
|
|
useEffect(() => {
|
|
if (visible && !permission?.granted) {
|
|
requestPermission();
|
|
}
|
|
}, [visible, permission, requestPermission]);
|
|
|
|
// Reset scanned state when modal opens
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setScanned(false);
|
|
}
|
|
}, [visible]);
|
|
|
|
const handleBarCodeScanned = ({
|
|
type,
|
|
data,
|
|
}: {
|
|
type: string;
|
|
data: string;
|
|
}) => {
|
|
if (!scanned) {
|
|
setScanned(true);
|
|
onScanned(data);
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (!permission) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide">
|
|
<View style={styles.container}>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color="#0000ff" />
|
|
<Text style={styles.loadingText}>
|
|
Requesting camera permission...
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
if (!permission.granted) {
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide">
|
|
<View style={styles.container}>
|
|
<View style={styles.permissionContainer}>
|
|
<Text style={styles.permissionTitle}>
|
|
Camera Permission Required
|
|
</Text>
|
|
<Text style={styles.permissionText}>
|
|
This app needs camera access to scan QR codes. Please allow camera
|
|
access in your settings.
|
|
</Text>
|
|
<Pressable
|
|
style={styles.button}
|
|
onPress={() => requestPermission()}
|
|
>
|
|
<Text style={styles.buttonText}>Request Permission</Text>
|
|
</Pressable>
|
|
<Pressable
|
|
style={[styles.button, styles.cancelButton]}
|
|
onPress={onClose}
|
|
>
|
|
<Text style={styles.buttonText}>Cancel</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Modal visible={visible} transparent animationType="slide">
|
|
<CameraView
|
|
ref={cameraRef}
|
|
style={styles.camera}
|
|
onBarcodeScanned={handleBarCodeScanned}
|
|
barcodeScannerSettings={{
|
|
barcodeTypes: ["qr"],
|
|
}}
|
|
>
|
|
<View style={styles.overlay}>
|
|
<View style={styles.unfocusedContainer} />
|
|
<View style={styles.focusedRow}>
|
|
<View style={styles.focusedContainer} />
|
|
</View>
|
|
<View style={styles.unfocusedContainer} />
|
|
|
|
<View style={styles.bottomContainer}>
|
|
<Text style={styles.scanningText}>
|
|
{/* Align QR code within the frame */}
|
|
Đặt mã QR vào khung hình
|
|
</Text>
|
|
<Pressable style={styles.closeButton} onPress={onClose}>
|
|
<Text style={styles.closeButtonText}>✕ Đóng</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</CameraView>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
loadingContainer: {
|
|
alignItems: "center",
|
|
gap: 10,
|
|
},
|
|
loadingText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
},
|
|
permissionContainer: {
|
|
backgroundColor: "#fff",
|
|
marginHorizontal: 20,
|
|
borderRadius: 12,
|
|
padding: 20,
|
|
alignItems: "center",
|
|
gap: 15,
|
|
},
|
|
permissionTitle: {
|
|
fontSize: 18,
|
|
fontWeight: "600",
|
|
color: "#333",
|
|
},
|
|
permissionText: {
|
|
fontSize: 14,
|
|
color: "#666",
|
|
textAlign: "center",
|
|
lineHeight: 20,
|
|
},
|
|
button: {
|
|
backgroundColor: "#007AFF",
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 30,
|
|
borderRadius: 8,
|
|
width: "100%",
|
|
alignItems: "center",
|
|
},
|
|
cancelButton: {
|
|
backgroundColor: "#666",
|
|
},
|
|
buttonText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
fontWeight: "600",
|
|
},
|
|
camera: {
|
|
flex: 1,
|
|
},
|
|
overlay: {
|
|
flex: 1,
|
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
},
|
|
unfocusedContainer: {
|
|
flex: 1,
|
|
},
|
|
focusedRow: {
|
|
height: "80%",
|
|
width: "100%",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
focusedContainer: {
|
|
aspectRatio: 1,
|
|
width: "70%",
|
|
borderColor: "#00ff00",
|
|
borderWidth: 3,
|
|
borderRadius: 10,
|
|
},
|
|
bottomContainer: {
|
|
flex: 1,
|
|
justifyContent: "flex-end",
|
|
alignItems: "center",
|
|
paddingBottom: 40,
|
|
gap: 20,
|
|
},
|
|
scanningText: {
|
|
color: "#fff",
|
|
fontSize: 16,
|
|
fontWeight: "500",
|
|
},
|
|
closeButton: {
|
|
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 20,
|
|
borderRadius: 8,
|
|
},
|
|
closeButtonText: {
|
|
color: "#fff",
|
|
fontSize: 14,
|
|
fontWeight: "600",
|
|
},
|
|
});
|