From 575e01912c570b92ceaec0b196811438fff371c1 Mon Sep 17 00:00:00 2001 From: Brittany Ellich Date: Mon, 1 Jun 2026 07:13:52 -0700 Subject: [PATCH] docs: design spec for client-side QR decode Co-Authored-By: Claude Opus 4.8 --- .../specs/2026-06-01-qr-decode-design.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-01-qr-decode-design.md diff --git a/docs/superpowers/specs/2026-06-01-qr-decode-design.md b/docs/superpowers/specs/2026-06-01-qr-decode-design.md new file mode 100644 index 0000000..9b833c6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-qr-decode-design.md @@ -0,0 +1,156 @@ +# Design: Client-side QR decode for the camera button + +**Date:** 2026-06-01 +**Status:** Approved, pending implementation plan + +## Summary + +Upgrade the existing mobile "open camera" button from a no-op photo-capture +affordance into a working live QR scanner. Tapping it opens a fullscreen +overlay showing the rear-camera feed; the [jsQR](https://github.com/cozmo/jsQR) +library decodes each video frame; when it detects one of atmo.quest's connect +codes, the page navigates to it and the existing auto-connect flow takes over. + +This builds directly on the prior camera-button work (see +`2026-05-31-mobile-camera-button-design.md`), which deliberately shipped the +button as a v1 stub with no decode step. This spec adds that decode step. + +## Motivation + +atmo.quest's connect flow relies on scanning a QR code. Each profile exposes a +QR that encodes `/c/` (or `/c/l/` for local +accounts); visiting that URL writes the connection and redirects. Today the +in-app camera button opens the native camera capture sheet but discards the +photo — users still have to switch to their phone's standalone camera app to +actually scan. This feature makes the in-app button do the scan itself. + +## Key decisions + +- **Capture mode: live viewfinder.** Use `getUserMedia` with + `facingMode: 'environment'` (rear camera) in a fullscreen overlay, decoding + frames continuously and auto-navigating the instant a valid connect code is + seen. This is the scanner UX people expect for connecting in person. +- **Decoder: jsQR only.** iOS Safari does not support the native + `BarcodeDetector` API, so a pure-JS decoder is required for universal + coverage anyway. jsQR (~40KB, MIT) decodes every frame the same way on all + browsers, giving one code path. Vendored as a static asset. +- **Non-app codes: keep scanning + transient hint.** A decoded QR that is not + an atmo.quest connect code is ignored; the viewfinder stays live and shows a + small transient hint ("not an atmo.quest code — keep scanning"). The overlay + only navigates on a valid connect code or closes on user cancel. + +## Architecture + +No Go handler or route changes. A successful scan is a same-origin navigation +to an existing `/c/...` route. All new code is client-side plus a templ markup +tweak and CSS. + +### Components + +- **`web/resources/static/js/vendor/jsqr.js`** — the vendored jsQR UMD build + (MIT license). New `vendor/` subdirectory under the existing static JS dir. +- **`web/resources/static/js/qr-scan.js`** — new vanilla JS module, matching the + existing plain-JS pattern (`badge-toast.js`, `notes.js`, etc.). Responsible + for: + - attaching a click handler to every `[data-qr-scan]` button on the page; + - building the overlay DOM dynamically (no templ overlay markup required); + - starting/stopping the camera (`getUserMedia`) and explicit track teardown; + - the per-frame decode loop (draw video frame to an offscreen canvas, pass + its `ImageData` to `jsQR`); + - URL matching and navigation; + - the file-input fallback path. + The module no-ops cleanly if no `[data-qr-scan]` element is present. +- **`CameraScanButton` templ component** + (`features/common/layouts/camerabutton.templ`) — changes from a `