Khởi tạo ban đầu

This commit is contained in:
Tran Anh Tuan
2025-11-28 16:59:57 +07:00
parent 2911be97b2
commit 4ba46a7df2
131 changed files with 28066 additions and 0 deletions

239
components/ScanQRCode.tsx Normal file
View File

@@ -0,0 +1,239 @@
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);
// Dùng ref để chặn quét nhiều lần trong cùng một frame/event loop
const hasScannedRef = useRef(false);
// 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]);
// Mỗi khi reset scanned state thì reset luôn ref guard
useEffect(() => {
if (!scanned) {
hasScannedRef.current = false;
}
}, [scanned]);
const handleBarCodeScanned = ({
type,
data,
}: {
type: string;
data: string;
}) => {
// Nếu đã scan rồi, bỏ qua
if (hasScannedRef.current || scanned) return;
hasScannedRef.current = true;
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}
// Chỉ gắn handler khi chưa scan để ngắt lắng nghe ngay lập tức sau khi quét thành công
onBarcodeScanned={scanned ? undefined : 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 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",
},
});