/**
* QRScannerScreen β camera-based QR code scanner for mobile login.
*
* Opens the device camera and scans for QR codes containing a
* `docuelevate://qr-login?token=...&server=...` payload. On successful
* scan the token is claimed via the API and the user is signed in.
*/
import { CameraView, useCameraPermissions } from "expo-camera";
import { useRouter } from "expo-router";
import React, { useCallback, useRef, useState } from "react";
import {
ActivityIndicator,
Alert,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import { useAuth } from "../context/AuthContext";
export default function QRScannerScreen() {
const { signInWithQR } = useAuth();
const router = useRouter();
const [permission, requestPermission] = useCameraPermissions();
const [scanned, setScanned] = useState(false);
const [processing, setProcessing] = useState(false);
const processingRef = useRef(false);
const handleBarCodeScanned = useCallback(
async (result: { data: string }) => {
// Prevent duplicate scans while processing
if (processingRef.current) return;
const { data } = result;
// Only accept docuelevate:// QR codes
if (!data.startsWith("docuelevate://qr-login")) return;
processingRef.current = true;
setScanned(true);
setProcessing(true);
try {
const url = new URL(data);
const token = url.searchParams.get("token");
const server = url.searchParams.get("server");
if (!token || !server) {
Alert.alert("Invalid QR Code", "This QR code does not contain valid login information.");
setScanned(false);
processingRef.current = false;
setProcessing(false);
return;
}
await signInWithQR(server, token);
// signInWithQR updates AuthContext β AuthGuard redirects to main app
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "QR login failed";
Alert.alert("QR Login Failed", message);
setScanned(false);
processingRef.current = false;
setProcessing(false);
}
},
[signInWithQR]
);
// Permissions not yet determined
if (!permission) {
return (
);
}
// Permission denied
if (!permission.granted) {
return (
Camera access is required to scan QR codes.
Grant Camera Access
router.back()}
style={styles.backLink}
accessibilityRole="button"
accessibilityLabel="Go back"
>
β Back
);
}
return (
{/* Overlay with scan area indicator */}
{processing ? (
Signing inβ¦
) : (
Point your camera at the QR code{"\n"}shown on the DocuElevate web app
)}
router.back()}
style={styles.cancelButton}
accessibilityRole="button"
accessibilityLabel="Cancel QR scan"
>
Cancel
);
}
const SCAN_AREA_SIZE = 250;
const CORNER_SIZE = 24;
const CORNER_WIDTH = 3;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#000",
},
camera: {
flex: 1,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f3f4f6",
padding: 24,
},
permissionText: {
fontSize: 16,
color: "#374151",
textAlign: "center",
marginBottom: 20,
},
permissionButton: {
backgroundColor: "#1e40af",
borderRadius: 8,
paddingVertical: 14,
paddingHorizontal: 24,
minHeight: 48,
alignItems: "center",
justifyContent: "center",
},
permissionButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
backLink: {
marginTop: 20,
minHeight: 44,
alignItems: "center",
justifyContent: "center",
},
backLinkText: {
fontSize: 14,
color: "#6b7280",
},
overlay: {
...StyleSheet.absoluteFillObject,
},
overlayTop: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
},
overlayMiddle: {
flexDirection: "row",
height: SCAN_AREA_SIZE,
},
overlaySide: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
},
scanArea: {
width: SCAN_AREA_SIZE,
height: SCAN_AREA_SIZE,
},
corner: {
position: "absolute",
width: CORNER_SIZE,
height: CORNER_SIZE,
},
cornerTopLeft: {
top: 0,
left: 0,
borderTopWidth: CORNER_WIDTH,
borderLeftWidth: CORNER_WIDTH,
borderColor: "#fff",
},
cornerTopRight: {
top: 0,
right: 0,
borderTopWidth: CORNER_WIDTH,
borderRightWidth: CORNER_WIDTH,
borderColor: "#fff",
},
cornerBottomLeft: {
bottom: 0,
left: 0,
borderBottomWidth: CORNER_WIDTH,
borderLeftWidth: CORNER_WIDTH,
borderColor: "#fff",
},
cornerBottomRight: {
bottom: 0,
right: 0,
borderBottomWidth: CORNER_WIDTH,
borderRightWidth: CORNER_WIDTH,
borderColor: "#fff",
},
overlayBottom: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
alignItems: "center",
paddingTop: 32,
},
statusContainer: {
flexDirection: "row",
alignItems: "center",
},
statusText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
marginLeft: 10,
},
instructionText: {
color: "#fff",
fontSize: 15,
textAlign: "center",
lineHeight: 22,
},
cancelButton: {
marginTop: 24,
paddingVertical: 12,
paddingHorizontal: 32,
borderRadius: 8,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.5)",
minHeight: 44,
alignItems: "center",
justifyContent: "center",
},
cancelButtonText: {
color: "#fff",
fontSize: 15,
fontWeight: "500",
},
});