Happy path
-
Staff opens the staff console at the door.
Lands on the event's guest-list view (day-of-operations trunk). Tap the scan affordance — a button or quick-access icon at the top of the screen.
-
Camera viewfinder opens.
Full-screen camera view with a square reticule guide. The phone's flashlight toggle is available in low light. Scan happens continuously — staff doesn't need to tap a button to capture; the QR is read as soon as it's framed.
-
Guest presents QR; viewfinder reads it; haptic feedback fires.
A short vibration (
navigator.vibrateon Android; ignored on iOS where vibration is privileged) signals successful read. The screen transitions to the guest confirmation screen within ~300ms. -
Confirmation screen.
Guest's name (large), photo if available, access type, any per-guest notes (e.g., "VIP table 3"). Two actions: Check in (primary) and Cancel (secondary, returns to scan view).
-
Tap Check In. Server records.
Brief success animation. Auto-returns to scan view ready for the next guest. The just-checked-in row updates in the staff console list (live-sync).
Failure modes
Two staff scan the same guest simultaneously
Trigger: guest with QR walks past staff A's station; staff B at the next station scans them at the same moment.
Server-side idempotency: the second request returns 200 with body indicating "already checked in at <time> by <name>". Both staff see the same final state. Neither sees an error. Audit log records both attempts but only one check-in.
Recovery: Idempotent. Both staff get clean acknowledgments.
Network drops mid-rush
Trigger: 200 guests in 10 minutes; venue WiFi flakes. Check-ins start failing on the network layer.
Per the day-of-operations trunk's offline contract: check-ins queue locally with a per-attempt timestamp. Staff sees the offline banner. Confirmation screens still show success to the attendee (don't lie to a real human at the door). When network returns, queue flushes; conflicts surface for review.
Recovery: Auto-flush on reconnect.
QR is expired (event-day check-in window is closed)
Trigger: the event ended an hour ago; a late guest scans their pre-event QR.
Server returns 410 with code CHECK_IN_WINDOW_CLOSED. Confirmation screen shows: "<Event name> check-in is closed. (Event ended <X minutes/hours ago>.)" Staff can override if they have permission (one-time button "Force check in"); otherwise the guest is informed.
Recovery: Staff override (audit-logged) OR guest is turned away gracefully.
Tampered QR — signature invalid
Trigger: someone tries to scan a QR that's been edited (a friend tried to forge an extra ticket).
Server's HMAC verification fails. The screen shows: "We couldn't recognize that code." Same message as for an unknown / malformed QR — anti-probing principle. Staff is shown a small "Search by name instead" affordance. Server-side log entry at higher severity for security review.
Recovery: Search by name; staff investigates.
Guest is on the waitlist, not confirmed
Trigger: guest holds an invitation QR but their RSVP is in waitlist state (capacity was met earlier).
Confirmation screen shows: "<Guest name> is on the waitlist." Staff has options: Promote to confirmed (if they have permission AND there's now capacity), Decline entry, Send to lobby. The guest is not silently checked in.
Recovery: Staff judgment with a clear UI.
Wrong event — QR is for a different event in the same workspace
Trigger: guest brings a QR for last week's event by mistake.
Server validates token against the current event-id; mismatch returns 404 with code QR_FOR_DIFFERENT_EVENT. Confirmation screen: "This QR is for <Other event name>, not <Current event>." Staff can search by name to see if the guest is on the right list.
Recovery: Search by name on the correct list.
Camera unavailable / permission denied
Trigger: staff's phone has not granted camera permission to the staff app.
Per the day-of-operations trunk: scan view shows "Camera unavailable" message and routes to name search. The trunk's kiosk-camera-unavailable attribute applies to the staff console too — same fallback pattern. The branch does not need to re-spec this failure.
Recovery: Name search.
QR reads but server returns 500
Trigger: QR is valid; events worker is degraded.
Per the trunk's soft-success-on-503 pattern: confirmation screen shows the guest's check-in attempt, queues locally, kiosk-success animation fires. When server recovers, queue drains. Staff sees a small "queued — will sync" indicator that they can ignore unless it persists.
Recovery: Auto-drain.
Already-checked-in re-scan
Trigger: a guest who's been inside and stepped out tries to re-enter; their QR is scanned again.
Confirmation screen shows: "<Guest> already checked in at <time>." Staff has a clear "Allow re-entry" button (no-op on database — they're already checked in). No duplicate row created, no audit event for "already checked in" beyond a log entry.
Recovery: Allow re-entry; no state change needed.
Phone goes to lock screen mid-scan
Trigger: staff's phone locks after 30 seconds of inactivity. Camera permission revoked when re-opening.
Re-opening the app should restore the scan view if it was the active screen. Camera re-prompts only if iOS has revoked permission entirely. The harness can't fully test lock-screen behavior in headless Playwright — flagged as a manual-verification probe.
Recovery: Re-grant permission if needed; resume.
Edge cases
QR on Apple Wallet pass
Apple Wallet QR codes are standard QR — same scan path. The wallet pass also auto-displays the QR when the phone is at the venue's geofence (organizer-configured). The branch doesn't need special handling.
Printed badge with worn / smudged QR
Camera autofocus + the QR error correction (Reed-Solomon) handle moderate damage. If a scan fails 3 times in a row, staff gets a "Try name search" suggestion.
Guest with multiple QRs (registered multiple times under different access types)
Each registration has its own QR. Scanning either one checks in the corresponding registration row. Staff sees which registration they're checking in (access type pill).
Bulk check-in (group of 5 arriving together)
Out of scope for this branch. EF-067 wireless badge printing covers some bulk scenarios. For QR-based bulk: scan each in turn — the success animation is short enough (~300ms) that 5 guests can be processed in ~10 seconds.
Page evaluation
| Surface | Discoverability | Error UX | Layout | Orientation |
|---|---|---|---|---|
| Staff console · scan affordance | Camera-icon button at the top of the staff-console, always visible. | If camera permission has not been granted, button label updates to "Enable camera" and tap takes user to settings deep-link. | Top right of the console header, thumb-reachable on mobile. | Icon plus "Scan" label so first-time users know what it does. |
| Camera viewfinder | Reticule guide centered. Flashlight toggle and Cancel button visible. | Camera errors render as overlay messages without breaking the surface. | Full-screen. Hardware orientation respected (portrait / landscape). | "Point at QR code" hint visible on first open of the session. |
| Guest confirmation screen | Guest name is the largest visual element. Check-in button is the primary action. | If guest is in unusual state (waitlist / wrong event / already-checked-in), the confirmation screen explicitly shows that state INSTEAD of the normal Check In button. | Full-screen card. Photo, name, access type, notes, action buttons. | Past-tense success message: "Welcome, <name>!" after check-in. |
Acceptance signals
- Server-side check-in: POST
/v1/admin/events/:id/check-insreturns 200 (or 200-with-already-checked-in body) on a valid QR. event_guests.attendance_status=checked_infor the resolved guest_id.- Audit log row exists with actor=staff_id, action=check-in, target=guest_id.
- Confirmation screen renders within 300ms of QR read.
- Live-sync: the staff console's guest list reflects the check-in within 2s.
- No console errors at severity ≥
warn.
Stable test attributes
Branch-specific only; trunk and component attributes inherited.
Visibility teeth. Each attribute must be present AND effectively visible when the relevant state is active. Hiding without removal is a Ratchet violation.
| data-test | Where | Purpose |
|---|---|---|
scan-cta | Staff console header | Tap to open camera viewfinder |
scan-viewfinder | Full-screen overlay | Camera + reticule |
scan-flashlight-toggle | Inside viewfinder | Flashlight on/off |
scan-cancel-cta | Inside viewfinder | Return to staff console |
scan-hint | Inside viewfinder | "Point at QR code" first-open hint |
scan-confirm-screen | After successful read | Guest confirmation screen |
scan-confirm-name | Inside scan-confirm-screen | Large guest name |
scan-confirm-access-type | Inside scan-confirm-screen | Access type pill |
scan-confirm-photo | Inside scan-confirm-screen | Guest photo if available |
scan-confirm-notes | Inside scan-confirm-screen | Per-guest notes (VIP, vegetarian, etc.) |
scan-confirm-checkin-cta | Inside scan-confirm-screen | Primary check-in button |
scan-confirm-cancel-cta | Inside scan-confirm-screen | Return to scan view |
scan-success | After check-in | Brief success animation; auto-returns to scan view |
scan-window-closed | QR-but-window-closed state | "<Event> check-in is closed" with optional Force check-in |
scan-force-checkin-cta | Inside scan-window-closed (permission-gated) | Override button for staff with permission |
scan-waitlisted-state | QR resolves to waitlisted guest | Three-button decision UI |
scan-promote-from-waitlist | Inside scan-waitlisted-state | Promote to confirmed |
scan-decline-entry | Inside scan-waitlisted-state | Decline entry |
scan-send-to-lobby | Inside scan-waitlisted-state | Send to lobby for later |
scan-wrong-event | QR for different event | "This QR is for <Other event>" |
scan-already-checked-in | Re-scan | "Already checked in at <time>" with allow-re-entry button |
scan-allow-reentry-cta | Inside scan-already-checked-in | No-op confirmation |
scan-invalid-qr | Tampered or unknown QR | Anti-probing-safe message + name search affordance |
scan-search-by-name-fallback | Inside any error state | Quick link to name search |
Agent test plan
Inherits day-of-operations trunk preconditions. Camera-based probes are limited in headless Playwright — fixtures provide pre-decoded QR payload as a query parameter to bypass the actual camera read.
Probe list
- scan-cta-visible: assert scan-cta visible on staff-console
- valid-qr-scan: navigate to /scan?token=${fixture.validQr}, assert scan-confirm-screen visible AND scan-confirm-name contains guest name AND scan-confirm-checkin-cta visible
- check-in-success: click scan-confirm-checkin-cta, assert API POST returns 200 AND scan-success visible AND auto-return to scan view
- two-staff-idempotent: stub two parallel POST check-ins, assert both return 200 with idempotent body
- expired-window: with token from event ended 2h ago, assert scan-window-closed visible AND scan-force-checkin-cta visible IF user has permission
- tampered-qr: with tampered token, assert scan-invalid-qr visible AND content byte-identical to invalid-qr fixture
- waitlisted-three-options: with token whose guest is waitlisted, assert scan-waitlisted-state visible AND all three options visible
- wrong-event: with token for different event, assert scan-wrong-event visible AND scan-search-by-name-fallback visible
- already-checked-in-reentry: scan a guest who's already checked in, assert scan-already-checked-in visible AND scan-allow-reentry-cta visible
- network-drop-queues: stub POST /check-ins to abort, scan and check in, assert kiosk-offline-banner visible (inherited from trunk) AND scan-success still shows
- 503-soft-success: stub POST to 503, assert scan-success appears for staff (queued internally)
- live-sync-updates-list: check in guest, observe staff-counts-checked-in increments within 2s