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)
constants.ts — all thresholds from technicality docs
parseScanValue.ts — extract shortTag from URL or raw input + tests
useScannerDetection.ts — keypress timing heuristic hook + tests
DeckInfoModal.tsx — lightweight deck info popup (O)
- API endpoint:
GET /api/deck/{shortTag}/summary → deck name, archetype, owner, status, active borrow
deck-scanner.tsx entry point — HID listener with context-aware routing; default context shows deck info modal
- Twig:
{% block scanner_data %} in base.html.twig (default context) + overrides in contextual templates
- Translation keys for deck info modal
Phase 2 — Camera scanner modal (mobile)
- Install
html5-qrcode
useCameraScanner.ts — Html5Qrcode lifecycle, rear camera, 60s timeout
CameraScannerModal.tsx — Mantine Modal, fullscreen, error states + tests
- Translation keys for camera errors
Phase 3 — Walk-up integration (cases 1-3)
AsyncAutocomplete.tsx — add useImperativeHandle for selectByValue
walk-up-autocomplete.tsx — listen for deck-scanner:scan CustomEvent
- Inline scan buttons C, D (mobile only) next to autocomplete fields
- Auto-focus feedback (N) — highlight + prompt counterpart scan
EventController.php — extend walk-up access to deck owners with registered decks
- After-submit: stay on walk-up with cleared fields
Phase 4 — Event show inline actions (cases 4-8)
- Event scan button (F) in staff actions area (mobile only)
- Deck scan: API to check deck status at event
- Quick return modal (H) — case 4
- Quick hand-off modal (H2) — case 5
- Available deck → redirect to walk-up pre-filled — case 6
- Player scan (requires F1.12): borrow list (I), pending (J), action choice (K) — deferred
Phase 5 — Remaining contexts (cases 9-10)
- Event decks: scan button (G) + filter/search (L) — case 9
- 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.
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
onScancallback with context-aware behavior scoped to pages where scanning has a concrete purpose.Design principles:
Technicality docs:
docs/technicalities/scanner.md,docs/technicalities/camera_scanner.mdRelated issues: #177 (F5.3), #174 (F5.6)
Device Visibility Rules
Scan Matrix
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.
After submit: Stay on walk-up page with cleared fields (ready for next lend), not redirect to event show.
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.
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.
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.
Default (any other page) — HID only
Who: Any authenticated user.
Camera: No button (native phone QR handles this). HID: Shows deck info modal.
Key Patterns
Context-aware routing for both HID and camera: The page context (
data-context+data-event-idon 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
Scan result elements
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.
Deck.nameDeck.shortTagDeck.archetypeDeck.owner.screenNameDismiss by clicking outside, Escape, or close button. No destructive actions.
New Files
assets/scanner/constants.tsassets/scanner/parseScanValue.tsassets/scanner/useScannerDetection.tsassets/scanner/useCameraScanner.tsassets/components/CameraScannerModal.tsxassets/components/DeckInfoModal.tsxassets/deck-scanner.tsxassets/test/parse-scan-value.test.tsassets/test/use-scanner-detection.test.tsassets/test/camera-scanner-modal.test.tsxModified Files
package.jsonhtml5-qrcodedependencywebpack.config.jsdeck_scannerentry pointtemplates/base.html.twigdeck_scannerentry +{% block scanner_data %}for contexttemplates/event/walk_up.html.twigtemplates/event/show.html.twigtemplates/event/available_decks.html.twigtemplates/borrow/show.html.twigsrc/Controller/EventController.phpsrc/Controller/DeckSearchController.phpassets/walk-up-autocomplete.tsxdeck-scanner:scanCustomEvent, auto-fillassets/components/AsyncAutocomplete.tsxuseImperativeHandlefor programmatic selectiontranslations/messages.en.xlfapp.scanner.*translation keystranslations/messages.fr.xlfImplementation Phases
Phase 1 — Core plumbing (HID scanner, context-aware + deck info modal)
constants.ts— all thresholds from technicality docsparseScanValue.ts— extract shortTag from URL or raw input + testsuseScannerDetection.ts— keypress timing heuristic hook + testsDeckInfoModal.tsx— lightweight deck info popup (O)GET /api/deck/{shortTag}/summary→ deck name, archetype, owner, status, active borrowdeck-scanner.tsxentry point — HID listener with context-aware routing; default context shows deck info modal{% block scanner_data %}inbase.html.twig(default context) + overrides in contextual templatesPhase 2 — Camera scanner modal (mobile)
html5-qrcodeuseCameraScanner.ts— Html5Qrcode lifecycle, rear camera, 60s timeoutCameraScannerModal.tsx— Mantine Modal, fullscreen, error states + testsPhase 3 — Walk-up integration (cases 1-3)
AsyncAutocomplete.tsx— adduseImperativeHandleforselectByValuewalk-up-autocomplete.tsx— listen fordeck-scanner:scanCustomEventEventController.php— extend walk-up access to deck owners with registered decksPhase 4 — Event show inline actions (cases 4-8)
Phase 5 — Remaining contexts (cases 9-10)
Architecture
Context-aware routing (shared by HID and camera)
Cross-Island Communication
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_borrowAdditional keys for Phase 4-5: player scan messages, action choice modals.
Prerequisites / Separate Issues
EventControlleraccess check onapp_event_walk_upmust be extended from staff/organizer to include deck owners with registered decks at the event. Backend change tracked separately.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).GET /api/event/{id}/deck-status?shortTag=X— returns borrow status at a specific event. Used by Phase 4 (event show context). UsesBorrowRepository::findActiveBorrowForDeckAtEvent().