# DOX — packages/client/src/components/pairing-approval

Files in this directory. One row per source file. See change: add-pairing-approval-dialog.

| File | Purpose |
|------|---------|
| `__tests__/PairingApprovalDialog.test.tsx` | E8 short code on submit, E9 input format, F3 mismatch + attempts left, F4 locked/expired/no_pending, success auto-close, F14 a11y + initial focus, X9 inert metadata + "reported by proxy", X13 transport failure. Knock-aware rows: address-first title, "Says it is", unencrypted callout, tier radio group. See change: add-lan-knock-pairing. |
| `__tests__/PairingApprovalHost.test.tsx` | F5 queue, F4 handled-elsewhere toast, F8 reconnect catch-up, F9 grant precedence (held, not dismissed), F10 paired-device browser never fetches, X10 list fetch failure retried on next hint, close→dismissed + Review reopens, deny. Injects `api`/`store`/`grantStore`/`getBearer`. |
| `PairingApprovalDialog.tsx` | Operator dialog on client-utils `Dialog` (`size="md"`, `testId="pairing-dialog"`). Exports `PairingApprovalDialog({entry, now, queued, onApprove, onDeny, onDismiss, onDone, onHandledElsewhere})`, `formatCode`, `SUCCESS_AUTO_CLOSE_MS` (4s). Never shows the confirm code; typed 8 digits (`pairing-code-input`, `inputMode=numeric`, `autocomplete=off`, grouped 4+4), validation on submit only ("Enter all 8 digits"). Device name >64 UTF-8 bytes refused locally (`MAX_NAME_BYTES`). Phases form → locked (`pairing-dialog-locked`) / expired (`pairing-dialog-expired`) / success (`pairing-dialog-success`, auto-close) via pure `nextFromOutcome` (D6); `no_pending` → `onHandledElsewhere`. Terminal phases offer only Close (`pairing-dialog-done`). Deny = neutral `Dialog.Action` disabled while busy (like Approve). `DeviceDetails` renders untrusted UA/host/IP as text only; XFF labelled "(reported by proxy)". Initial focus deferred one task past the Dialog focus trap onto the code field. Knock-aware: address-first title/rows (the address is the only operator-verifiable fact), self-declared name shown under "Says it is", unencrypted callout, tier radio group defaulting `operate` with a tier-aware consequence line; `onApprove(code, label, tier?)`. See change: add-lan-knock-pairing. |
| `PairingApprovalHost.tsx` | App-wide mount (both App returns; lazy-loaded by App → own chunk with the dialog). Exports `PairingApprovalHost({onMessage, ws, store?, grantStore?, api?, getBearer?})`. Refetches `listPending()` on mount, on `pair_pending_changed` and on every non-null `ws`; `fetchSeq` ref drops out-of-order (stale) responses; skips entirely when `getDeviceBearer()` set (D1/D5). One dialog (preferred → oldest non-dismissed), `queued` count. Holds back (not dismissed) while `grantPromptStore` queue non-empty. `answered` ref marks requests this tab is answering (cleared on any non-terminal approve failure — `TERMINAL_ERRORS` = locked_out/expired/no_pending); any other open id vanishing → close + "Pairing request handled in another window." toast. Close → `store.dismiss`. Deny `no_pending` → handled-elsewhere; other deny failure → dialog error. Forwards the dialog's `tier` to `approvePending`. See change: add-lan-knock-pairing. |
