From 2498fbe51856e6f87a7ca22f5b7d8745aef8fc3b Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 24 May 2026 18:00:21 +0200 Subject: [PATCH] fix(activities): pause scanner after each decode, require tap to scan next MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a pause gate the qr-scanner's 5-fps decode loop instantly fires another scan on whatever QR is still in frame — most visibly, the ticket that just registered immediately re-fires as "already scanned this session". The operator at the door never gets a beat to confirm the result or act on it (let the attendee in, deny entry, redirect to manual lookup, etc.). useTicketScanner gains an `isPaused` ref that flips to true the moment a decode resolves (success, error, or duplicate-session de-dup) and gates further `onDecode` calls. The camera keeps streaming so resumption is instant — only the decode handler is muted. The page replaces the small "Dismiss" button with a full-width "Scan next" CTA below the result banner. Same place every time so the operator's hand can stay in muscle memory; disabled while the in-flight RPC is still sending. Result banner upgrades to a slightly larger icon + label so the success/failure is readable at arm's length over the venue. `clearScanned` also resets `isPaused` so the operator can recover from a stuck state via the "Clear list" affordance. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../composables/useTicketScanner.ts | 32 +++++++++++--- .../activities/views/ScanTicketsPage.vue | 42 ++++++++++++------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/modules/activities/composables/useTicketScanner.ts b/src/modules/activities/composables/useTicketScanner.ts index 638b8bc..50006a1 100644 --- a/src/modules/activities/composables/useTicketScanner.ts +++ b/src/modules/activities/composables/useTicketScanner.ts @@ -39,6 +39,17 @@ export function useTicketScanner(activityId: Ref) { const isProcessing = ref(false) const lastScan = ref(null) + /** + * Set to `true` immediately after a decode resolves (success or + * failure) and stays true until the operator dismisses or taps + * "Scan next". While paused, further `onDecode` calls are dropped + * — the camera keeps streaming for instant resume but the result + * banner sticks so the door can confirm the outcome before + * moving on. Without this, the 5-fps decode loop instantly + * fires "already scanned this session" on the very ticket that + * just succeeded. + */ + const isPaused = ref(false) const scanned = useLocalStorage( () => `activities_scanned_${activityId.value}`, [], @@ -51,7 +62,7 @@ export function useTicketScanner(activityId: Ref) { } async function onDecode(qrText: string): Promise { - if (isProcessing.value) return + if (isProcessing.value || isPaused.value) return const ticketId = parseTicketId(qrText).trim() if (!ticketId) return @@ -60,6 +71,7 @@ export function useTicketScanner(activityId: Ref) { // of the camera for multiple decode frames. if (scanned.value.some(r => r.ticketId === ticketId)) { lastScan.value = { status: 'duplicate-session', ticketId } + isPaused.value = true return } @@ -90,24 +102,32 @@ export function useTicketScanner(activityId: Ref) { } } finally { isProcessing.value = false + // Pause the decode loop regardless of outcome. The operator + // taps "Scan next" to resume; this guarantees they see the + // banner and can correct (let the attendee in, deny entry, + // etc.) before the next QR comes into frame. + isPaused.value = true } } + function resume() { + lastScan.value = null + isPaused.value = false + } + function clearScanned() { scanned.value = [] lastScan.value = null - } - - function dismissLastScan() { - lastScan.value = null + isPaused.value = false } return { isProcessing, + isPaused, lastScan, scanned, onDecode, + resume, clearScanned, - dismissLastScan, } } diff --git a/src/modules/activities/views/ScanTicketsPage.vue b/src/modules/activities/views/ScanTicketsPage.vue index 4a2d74e..67c51f5 100644 --- a/src/modules/activities/views/ScanTicketsPage.vue +++ b/src/modules/activities/views/ScanTicketsPage.vue @@ -1,7 +1,7 @@