Banquet email input floats above keyboard; scan only within the reticle
All checks were successful
Build Android APK / build-apk (push) Successful in 43m56s

- Move the banquet manual-email input to the top of the scanner (under the mode
  tabs) so the on-screen keyboard, which covers the bottom, never hides it.
- Restrict QR detection to the centered reticle square: native filters codes by
  their reported position (fails open if geometry is unavailable); web crops the
  central square of the frame before detecting. Codes elsewhere in view are
  ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hank 2026-07-09 06:42:07 +00:00
parent 65aa878192
commit 7a6653b658
3 changed files with 89 additions and 28 deletions

View file

@ -1,6 +1,6 @@
import { useRef } from "react";
import { StyleSheet, View, Text, Pressable } from "react-native";
import { CameraView, useCameraPermissions } from "expo-camera";
import { StyleSheet, View, Text, Pressable, type LayoutChangeEvent } from "react-native";
import { CameraView, useCameraPermissions, type BarcodeScanningResult } from "expo-camera";
import { theme } from "../lib/theme";
export interface QRScannerProps {
@ -8,10 +8,48 @@ export interface QRScannerProps {
active: boolean;
}
/** Native (Android/iOS) scanner using expo-camera. */
// Side of the accept square, matching the visible reticle (index.tsx uses 240),
// with a little tolerance so a code aimed inside the box always registers.
const RETICLE = 260;
/** Native (Android/iOS) scanner using expo-camera. Only accepts codes whose
* position falls within the centered reticle square. */
export default function QRScanner({ onScan, active }: QRScannerProps) {
const [permission, requestPermission] = useCameraPermissions();
const lastScan = useRef<{ code: string; at: number }>({ code: "", at: 0 });
const layout = useRef({ w: 0, h: 0 });
const onLayout = (e: LayoutChangeEvent) => {
layout.current = { w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height };
};
// True if the scanned code sits inside the centered reticle. Fails OPEN when
// geometry is missing/unknown so scanning never silently breaks.
const inReticle = (res: BarcodeScanningResult): boolean => {
const { w, h } = layout.current;
if (!w || !h) return true;
const pts = res.cornerPoints as { x: number; y: number }[] | undefined;
let cx: number | undefined;
let cy: number | undefined;
if (pts && pts.length) {
cx = pts.reduce((s, p) => s + p.x, 0) / pts.length;
cy = pts.reduce((s, p) => s + p.y, 0) / pts.length;
} else {
const b: any = (res as any).bounds;
if (b?.origin && b?.size) {
cx = b.origin.x + b.size.width / 2;
cy = b.origin.y + b.size.height / 2;
}
}
if (cx === undefined || cy === undefined) return true;
// Some platforms report normalized [0,1] coords — scale to view size.
if (cx <= 1 && cy <= 1) {
cx *= w;
cy *= h;
}
const half = RETICLE / 2;
return Math.abs(cx - w / 2) <= half && Math.abs(cy - h / 2) <= half;
};
if (!permission) {
return <View style={styles.fill} />;
@ -31,10 +69,13 @@ export default function QRScanner({ onScan, active }: QRScannerProps) {
<CameraView
style={styles.fill}
facing="back"
onLayout={onLayout}
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
onBarcodeScanned={
active
? ({ data }) => {
? (res) => {
if (!inReticle(res)) return; // ignore codes outside the square
const data = res.data;
const now = Date.now();
// Debounce repeated frames of the same code.
if (data === lastScan.current.code && now - lastScan.current.at < 3000) return;