Skip to content

F5.3 + F5.6 — Unified Deck Scanner (HID + Camera QR) #325

@jbourdin

Description

@jbourdin

Context

Event participants use phones to confirm deck hand-offs and returns, but no scanning infrastructure exists yet. This issue tracks the combined implementation of F5.3 (USB HID scanner) and F5.6 (Camera QR scanner) as a unified system.

Both methods feed into the same onScan callback with context-aware behavior scoped to pages where scanning has a concrete purpose.

Design principles:

  • No "scan anywhere" camera buttons — deck label QR codes are URLs, so any phone's native QR scanner handles generic deck lookup. Camera scan buttons in the app are reserved for event operations where the scan triggers a specific workflow.
  • HID always responds — since HID scans are intentional (physical barcode aim), they always produce a result. On contextual pages, the scan triggers the relevant action. On non-contextual pages, a lightweight deck info modal appears.
  • HID and camera share the same routing logic — the input method differs (passive keypress vs on-demand camera) but the context-aware behavior is identical on contextual pages.
  • Quick actions over navigation — on the event show page, scanning triggers inline confirmation modals (one tap to complete) rather than navigating to the borrow detail page. Optimized for batch operations during event ending phase.

Technicality docs: docs/technicalities/scanner.md, docs/technicalities/camera_scanner.md

Related issues: #177 (F5.3), #174 (F5.6)


Device Visibility Rules

  • Desktop: HID scanner only (passive, zero visible UI). Context-aware routing is active — HID scans on the walk-up page fill the form, on event show they trigger inline actions, on any other page they show a deck info modal.
  • Mobile: Camera scan buttons appear only on contextual pages (walk-up, event show, event decks, borrow detail) for users with a relevant role. HID scanner also active (passive) for paired Bluetooth scanners.
  • No profile toggle, no device detection beyond responsive breakpoint for button visibility.

Scan Matrix

Convention: "Staff" always implies "Staff or Organizer" throughout.

Two scan types: Deck QR (shortTag from label) and Player QR (future F1.12, Play Pokemon JWT).

Walk-up / direct lend (/event/{id}/walk-up)

Who: Staff/Organizer or deck owner with registered decks at this event.
Camera buttons: Mobile only. HID: Same behavior on desktop.

# Scanned Action Counterpart? UI
1 Deck Fill deck autocomplete → scan player for borrower Inline button next to deck field (C)
2 Player Fill borrower autocomplete → scan deck for deck field Inline button next to borrower field (D)
3 Both (any order) Both fields filled → ready to submit Auto-focus next empty field + highlight (N)

After submit: Stay on walk-up page with cleared fields (ready for next lend), not redirect to event show.

Prerequisite: Walk-up route access must be extended from staff/organizer-only to include deck owners with registered decks at the event. Tracked separately.

Event show (/event/{id})

Who: Staff/Organizer or deck owner with registered decks at this event.
Camera button: Single scan button (F) in staff actions area, mobile only. HID: Same behavior on desktop.

# Scanned Action Counterpart?
4 Deck (lent at event) Inline confirmation modal: "Deck X is lent to Player Y. Confirm return?" → one tap → stay on page No
5 Deck (approved at event) Inline confirmation modal: "Deck X is approved for Player Y. Confirm hand-off?" → one tap → stay on page No
6 Deck (registered, available) Redirect to walk-up with deck pre-filled → scan player on walk-up
7 Deck (not registered at event) Navigate to deck detail page No
8a Player (has approved borrows) Modal: list of decks to hand off; if one → confirm directly Optional → scan deck to verify
8b Player (has pending requests) Modal: list of pending requests to approve/deny No
8c Player (registered, no active borrows) Redirect to walk-up with borrower pre-filled → scan deck to complete
8d Organizer only Player (not registered) Modal: propose register player or walk-up borrow

Quick action design (cases 4, 5): Scanning a lent or approved deck opens a lightweight confirmation modal over the event page. On confirm, the action is performed via POST and the event page refreshes in place. This avoids the navigate → act → navigate back cycle and is critical for batch returns during the ending phase.

Event decks (/event/{id}/decks)

Who: Borrower.
Camera button: Scan button (G) near filters, mobile only. HID: Same behavior on desktop.

# Scanned Action Counterpart?
9 Deck Filter/search by shortTag to find the deck → open borrow request form No

If the deck is on another page (pagination), the scan triggers a search/filter rather than scrolling.

Borrow detail (/borrow/{id})

Who: Staff/Organizer or deck owner, when borrow status is Approved or Lent.
Camera button: "Verify deck" button (E) near action buttons, mobile only. HID: Same behavior on desktop.

# Scanned Action Counterpart?
10 Deck (matches borrow) Verification → trigger next action (hand-off / return) with visual feedback No
10b Deck (does not match) Toast error: "This deck doesn't match this borrow" No

Default (any other page) — HID only

Who: Any authenticated user.
Camera: No button (native phone QR handles this). HID: Shows deck info modal.

# Scanned Action
11 Deck Deck info modal — lightweight popup with deck name, archetype, owner, status, active borrow info, action links. Dismiss by clicking outside or Escape.
12 Player (future F1.12) Player info modal — screen name, player ID, active borrows, profile link.

Key Patterns

Context-aware routing for both HID and camera: The page context (data-context + data-event-id on the scanner mount point) drives behavior for both input methods. On contextual pages, behavior is identical for HID and camera. On non-contextual pages, HID shows the info modal while camera has no button.

HID always responds: A physical scan always produces a visible result — either a contextual action or the deck info modal. No silent ignoring.

Two-scan workflows: Only on walk-up (#1-3) and event show → walk-up redirect (#6, #8c). Scan deck + scan player = ready to lend.

Inline confirmations over navigation (#4, #5): Scanning a lent/approved deck on the event show page opens a modal for one-tap confirmation instead of navigating away. Optimized for batch operations (ending phase returns).

Verification scan with error state (#10, #10b): On borrow page, scanning the matching deck triggers the action. Scanning a different deck shows an error toast.


UI Elements

Scan trigger elements

# Element Location Visible when Cases
B Camera scanner modal Fullscreen overlay Triggered by any inline button All camera scans
C Deck scan button (inline) Walk-up, next to deck autocomplete Mobile + staff/organizer/deck owner 1, 3
D Borrower scan button (inline) Walk-up, next to borrower autocomplete Mobile + staff/organizer/deck owner 2, 3
E "Verify deck" button Borrow detail, near action buttons Mobile + staff/owner, status Approved or Lent 10
F Event scan button Event show, staff actions area Mobile + staff/organizer/deck owner 4-8
G Deck scan button Event decks, near filters Mobile + borrower 9

Scan result elements

# Element Purpose Cases
H Quick return confirmation modal "Deck X lent to Player Y. Confirm return?" → one tap 4
H2 Quick hand-off confirmation modal "Deck X approved for Player Y. Confirm hand-off?" → one tap 5
I Player borrow list modal List approved borrows for hand-off; auto-confirm if one 8a
J Player pending requests modal List pending requests to approve/deny 8b
K Player action choice modal Register player or walk-up borrow 8d
L Deck filter/highlight Filter event decks by shortTag, open borrow request 9
M Borrow verification feedback Confirm deck matches → trigger action; or toast error on mismatch 10, 10b
N Walk-up auto-focus feedback Highlight filled field, focus next, prompt counterpart scan 1-3
O Deck info modal (HID default) Read-only popup: deck name, archetype, owner, status, active borrow, links 11
P Player info modal (HID default, future) Read-only popup: screen name, player ID, borrows, profile link 12

6 trigger elements (B–G) + 10 result elements (H–P) = 16 UI elements total.

Elements I, J, K, P depend on player QR scanning (F1.12) → deferred.

Deck info modal (O) — detail

Shown when HID scans a deck on any non-contextual page. One API call to fetch data.

Field Source
Deck name Deck.name
Short tag Deck.shortTag
Archetype + sprites Deck.archetype
Owner Deck.owner.screenName
Status Available / Lent / Retired
Active borrow (if any) "Lent to {borrower} at {event}"
Actions "View deck" link, "View borrow" link (if active)

Dismiss by clicking outside, Escape, or close button. No destructive actions.


New Files

File Purpose
assets/scanner/constants.ts All scanner constants (timing thresholds, camera config)
assets/scanner/parseScanValue.ts Extract shortTag from full URL or raw shortTag
assets/scanner/useScannerDetection.ts HID scanner hook (keypress timing heuristic)
assets/scanner/useCameraScanner.ts Camera lifecycle hook (html5-qrcode)
assets/components/CameraScannerModal.tsx Mantine Modal with camera viewfinder (fullscreen on mobile)
assets/components/DeckInfoModal.tsx Lightweight deck info popup for HID default context
assets/deck-scanner.tsx Webpack entry point — activates HID listener, context-aware routing
assets/test/parse-scan-value.test.ts Unit tests for URL parsing
assets/test/use-scanner-detection.test.ts Unit tests for HID timing heuristic
assets/test/camera-scanner-modal.test.tsx Component tests for modal states

Modified Files

File Change
package.json Add html5-qrcode dependency
webpack.config.js Add deck_scanner entry point
templates/base.html.twig Include deck_scanner entry + {% block scanner_data %} for context
templates/event/walk_up.html.twig Override scanner context + inline scan buttons (C, D)
templates/event/show.html.twig Override scanner context + event scan button (F)
templates/event/available_decks.html.twig Deck scan button (G) near filters
templates/borrow/show.html.twig "Verify deck" button (E) near action buttons
src/Controller/EventController.php Extend walk-up access to deck owners with registered decks
src/Controller/DeckSearchController.php New API endpoint for deck info (shortTag → deck summary JSON)
assets/walk-up-autocomplete.tsx Listen for deck-scanner:scan CustomEvent, auto-fill
assets/components/AsyncAutocomplete.tsx Add useImperativeHandle for programmatic selection
translations/messages.en.xlf app.scanner.* translation keys
translations/messages.fr.xlf Same keys in French

Implementation Phases

Phase 1 — Core plumbing (HID scanner, context-aware + deck info modal)

  1. constants.ts — all thresholds from technicality docs
  2. parseScanValue.ts — extract shortTag from URL or raw input + tests
  3. useScannerDetection.ts — keypress timing heuristic hook + tests
  4. DeckInfoModal.tsx — lightweight deck info popup (O)
  5. API endpoint: GET /api/deck/{shortTag}/summary → deck name, archetype, owner, status, active borrow
  6. deck-scanner.tsx entry point — HID listener with context-aware routing; default context shows deck info modal
  7. Twig: {% block scanner_data %} in base.html.twig (default context) + overrides in contextual templates
  8. Translation keys for deck info modal

Phase 2 — Camera scanner modal (mobile)

  1. Install html5-qrcode
  2. useCameraScanner.ts — Html5Qrcode lifecycle, rear camera, 60s timeout
  3. CameraScannerModal.tsx — Mantine Modal, fullscreen, error states + tests
  4. Translation keys for camera errors

Phase 3 — Walk-up integration (cases 1-3)

  1. AsyncAutocomplete.tsx — add useImperativeHandle for selectByValue
  2. walk-up-autocomplete.tsx — listen for deck-scanner:scan CustomEvent
  3. Inline scan buttons C, D (mobile only) next to autocomplete fields
  4. Auto-focus feedback (N) — highlight + prompt counterpart scan
  5. EventController.php — extend walk-up access to deck owners with registered decks
  6. After-submit: stay on walk-up with cleared fields

Phase 4 — Event show inline actions (cases 4-8)

  1. Event scan button (F) in staff actions area (mobile only)
  2. Deck scan: API to check deck status at event
  3. Quick return modal (H) — case 4
  4. Quick hand-off modal (H2) — case 5
  5. Available deck → redirect to walk-up pre-filled — case 6
  6. Player scan (requires F1.12): borrow list (I), pending (J), action choice (K) — deferred

Phase 5 — Remaining contexts (cases 9-10)

  1. Event decks: scan button (G) + filter/search (L) — case 9
  2. Borrow detail: verify button (E) + feedback/error (M) — case 10, 10b

Architecture

Context-aware routing (shared by HID and camera)

onScan(rawValue)
  → parseScanValue(rawValue) → shortTag | null
  → if null: ignore (not a recognized QR)
  → switch (context.type):
      'walk-up'     → dispatch CustomEvent('deck-scanner:scan', { shortTag })
      'event-show'  → fetch deck status at event → route to inline action
      'event-decks' → filter deck list by shortTag
      'borrow'      → if shortTag matches → trigger action; else → toast error
      default       → fetch deck summary → show DeckInfoModal (O)

Cross-Island Communication

[Scanner island] --CustomEvent('deck-scanner:scan')--> [Walk-up island / other listeners]

Context is declared via Twig {% block scanner_data %} overrides.


Translation Keys

app.scanner.modal_title, app.scanner.scanning, app.scanner.timeout, app.scanner.try_again, app.scanner.close, app.scanner.error.not_allowed, app.scanner.error.not_found, app.scanner.error.not_readable, app.scanner.error.unknown, app.scanner.deck_not_found, app.scanner.wrong_deck, app.scanner.deck_selected, app.scanner.verify_deck, app.scanner.scan_deck, app.scanner.scan_borrower, app.scanner.confirm_return, app.scanner.confirm_handoff, app.scanner.info.title, app.scanner.info.status, app.scanner.info.owner, app.scanner.info.active_borrow, app.scanner.info.view_deck, app.scanner.info.view_borrow

Additional keys for Phase 4-5: player scan messages, action choice modals.


Prerequisites / Separate Issues

  • Walk-up access for deck owners: EventController access check on app_event_walk_up must be extended from staff/organizer to include deck owners with registered decks at the event. Backend change tracked separately.
  • Deck summary API: GET /api/deck/{shortTag}/summary — returns deck name, archetype, owner, status, active borrow. Used by the deck info modal (Phase 1) and event show deck status check (Phase 4).
  • Deck status at event API: GET /api/event/{id}/deck-status?shortTag=X — returns borrow status at a specific event. Used by Phase 4 (event show context). Uses BorrowRepository::findActiveBorrowForDeckAtEvent().
  • Player QR decoding (F1.12): Cases 2, 3, 8a-8d, 12 depend on F1.12 (Play Pokemon QR JWT decoder). Deferred until F1.12 is implemented.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions