14 file(s), +441 / -183 lines
| Path | Role | + | - |
|---|---|---|---|
| .env.example | Source | +0 | -6 |
| messages/en.json | Config | +9 | -9 |
| messages/hi.json | Config | +9 | -9 |
| package.json | Config | +1 | -0 |
| pnpm-lock.yaml | Config | +12 | -0 |
| src/components/settings/channels/channels-page.tsx | Component | +3 | -1 |
| src/components/settings/channels/enrollment-code-card.stories.tsx | Component | +43 | -11 |
| src/components/settings/channels/enrollment-code-card.test.tsx | Component | +74 | -51 |
| src/components/settings/channels/enrollment-code-card.tsx | Component | +59 | -82 |
| src/components/settings/channels/enrollment-code-details.tsx | Component | +87 | -0 |
| src/components/settings/channels/enrollment-qr.stories.tsx | Component | +36 | -0 |
| src/components/settings/channels/enrollment-qr.tsx | Component | +60 | -0 |
| src/components/settings/channels/enrollment-timer-pill.tsx | Component | +40 | -0 |
| tests/unit/channels-page.test.tsx | Test | +8 | -14 |
The enrolment half of My connection now leads with a scannable QR. Scanning with the phone you want to connect is self-verifying: the phone that scans is the phone that sends, which is exactly what the ceremony binds. The QR collapses the desktop → phone device switch into one gesture.
Connect WhatsApp → scan the QR with the phone to connect (or tap Open WhatsApp with code on that phone) → send the pre-filled one-time code → keep the page open while it confirms the binding.
| Surface | Change |
|---|---|
| Primary action |
A fixed 216px QR column renders real SVG modules via
<QRCodeSVG> (qrcode.react@4.2.0, version-pinned; ISC;
zero transitive deps). No canvas, no data: URL, no
dangerouslySetInnerHTML, no image-rendering: pixelated.
|
| Link source |
The QR and the Open WhatsApp with code link both encode
enrollment.deepLink — the server-built wa.me link
(Story-3062). The component never receives a phone number, so the
browser can never re-encode a wrong number into a pixel-identical dead QR.
|
| Deleted |
NEXT_PUBLIC_WHATSAPP_BUSINESS_NUMBER, the "15553039991"
literal fallback, plausibleWhatsAppNumber(), and the module-load env
read are gone. The desktop-fallback line now uses enrollment.displayNumber
from the server.
|
| Countdown |
A status pill in the header, driven by the existing use-countdown hook
off expiresAt: amber while live, red once it lapses
(the same red the revoked badge uses). Announced with aria-live="polite"
role="status" and tabular-nums so it does not jitter.
|
| Expiry | Struck-through code, faded QR with an Expired veil, red timer, hidden send affordances, and the Get a new code action in the code column. |
| Structure (design-matched) |
The card composes three siblings — EnrollmentQr (the cream 216px column
with a framed QR), EnrollmentCodeDetails (the “Or send this
code” eyebrow, code, copy, and live/expired affordances), and
EnrollmentTimerPill — laid out as the mock's header / body-grid /
footer sections with dividers. The card owns the single countdown so one
expired verdict veils the QR, strikes the code, and reddens the timer in
one place. All colours map to existing tokens (cream =
--surface-app-page-subtle, amber = --surface-app-warning);
the three deliberate deviations from the mock are SVG-not-pixelated-img, Vani-not-Mira,
and server-displayNumber-not-hardcoded.
|
SVG avoids CSP coupling (img-src data: is one hand-edit away — see LI-5), is
present in the server-rendered HTML instead of appearing after hydration, stays sharp at
every device pixel ratio (QR scanning depends on edge contrast), and is reviewable in
Storybook. Pure black-on-white is the library default and is required for scan
reliability, so no colour token is passed to the QR.
This Story restructures channels-page.tsx — the exact file Bug-3064 fixed —
but touches only the <EnrollmentCodeCard> props (adds
deepLink, displayNumber). The refetchInterval
predicate still calls shouldPollForBinding(enrollment, bindings, Date.now())
unchanged, and presentation.ts / presentation.test.ts are
byte-for-byte unchanged — including the test that proves polling stops once the code
expires. Both pass.
Settings/Channels/EnrollmentCodeCard → Live — scannable QR, code, copy, countdown.Settings/Channels/EnrollmentCodeCard → Under 2 minutes — the countdown near expiry.Settings/Channels/EnrollmentCodeCard → Expired — veiled QR, struck code, regenerate.Settings/Channels/EnrollmentCodeCard → Hindi — Devanagari against the fixed-width chrome.Settings/Channels/EnrollmentQr → Live / Expired — the isolated 216px column.<svg> (no <canvas>, no img[src^="data:"]).Open WhatsApp with code link href is exactly the server deepLink.displayNumber and the countdown is a polite status region.4000 while live and false once bound.qrTitle / qrScanHint keys.
Storybook proves the SVG draws; it cannot prove the encoded link is right. A QR encoding
the wrong payload is pixel-identical to one that works — the only proof is a completed
bind. This build was verified by unit + integration tests and Storybook only; a real
phone scan against a live enrolment code was not performed in this
environment. The Bug-3179 acceptance step — scan with a phone, confirm WhatsApp opens with
the code pre-filled, confirm the message completes a bind — remains open and must be done
on a deployed environment (UAT M-01) before this is marked done. The design makes the
encoded payload correct-by-construction (it is the server deepLink, byte-for-byte),
which is what the automated link-equality assertion pins.