800e374c624b
feat: UI-3 + UI-4 Sync settings — pending vouched-device Allow/Reject rows (CONFIRM/REJECT) with live badge, rotate-sync-identity danger button + confirm dialog (ROTATE_SYNC_IDENTITY) routing to new recovery words
- time
- 2026-06-10 03:41 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- audit fix integration
- parent
- b5317629a444
↓ peach-browser-800e374c.tar.gzatlas at this save
files changed1 file
src/popup/views/Settings.tsx+143 −1src/popup/views/Settings.tsx+143 −1
@@ -1,4 +1,4 @@1-import { createEffect, createMemo, createSignal, For, Match, onMount, Show, Switch } from "solid-js";1+import { createEffect, createMemo, createSignal, For, Match, onCleanup, onMount, Show, Switch } from "solid-js";22 import { sendMessage } from "../App";33 import { PeachMark } from "../components/PeachMark";44 import { StackDeck } from "../components/settings/StackDeck";@@ -258,6 +258,62 @@258258 const [passkeyState, setPasskeyState] = createSignal<PasskeyProviderState | null>(null);259259 const [passkeySaving, setPasskeySaving] = createSignal(false);260260 261+ // UI-3 / MV-7: devices vouched by another of the user's devices, awaiting262+ // explicit confirmation before they become sync targets.263+ interface PendingVouched { deviceId: string; deviceName: string }264+ const [pendingVouched, setPendingVouched] = createSignal<PendingVouched[]>([]);265+ const [vouchedBusy, setVouchedBusy] = createSignal<string | null>(null);266+ // UI-4 / MV-8: rotate sync identity confirmation dialog.267+ const [rotateConfirmOpen, setRotateConfirmOpen] = createSignal(false);268+ const [rotating, setRotating] = createSignal(false);269+ const [rotateError, setRotateError] = createSignal<string | null>(null);270+271+ async function loadPendingVouched() {272+ const r = (await sendMessage({ type: "GET_PENDING_VOUCHED_DEVICES" })) as {273+ ok: boolean;274+ data?: PendingVouched[];275+ };276+ if (r.ok) setPendingVouched(r.data ?? []);277+ }278+ async function confirmVouched(deviceId: string) {279+ setVouchedBusy(deviceId);280+ try {281+ await sendMessage({ type: "CONFIRM_VOUCHED_DEVICE", payload: { deviceId } });282+ await loadPendingVouched();283+ } finally {284+ setVouchedBusy(null);285+ }286+ }287+ async function rejectVouched(deviceId: string) {288+ setVouchedBusy(deviceId);289+ try {290+ await sendMessage({ type: "REJECT_VOUCHED_DEVICE", payload: { deviceId } });291+ await loadPendingVouched();292+ } finally {293+ setVouchedBusy(null);294+ }295+ }296+ async function doRotateSyncIdentity() {297+ setRotateError(null);298+ setRotating(true);299+ try {300+ const r = (await sendMessage({ type: "ROTATE_SYNC_IDENTITY" })) as {301+ ok: boolean;302+ error?: string;303+ };304+ if (!r.ok) {305+ setRotateError(r.error ?? "Rotation failed.");306+ return;307+ }308+ setRotateConfirmOpen(false);309+ // Backend set peach_recovery_pending; reuse the existing recovery-words310+ // display (its GET_RECOVERY_WORDS fetch is exempted during pending).311+ setShowNewRecoveryWords(true);312+ } finally {313+ setRotating(false);314+ }315+ }316+261317 const [dedupCount, setDedupCount] = createSignal<number | null>(null);262318 const [dedupRunning, setDedupRunning] = createSignal(false);263319 const [dedupDone, setDedupDone] = createSignal<number | null>(null);@@ -372,6 +428,19 @@372428 // when the user navigates in. Loading it lazily keeps Settings boot fast.373429 void loadVaultHistory();374430 431+ // UI-3 / MV-7: surface devices a paired peer vouched for, and refresh the432+ // list live if a VOUCHED_DEVICE_PENDING broadcast arrives while open.433+ void loadPendingVouched();434+ const onBroadcast = (msg: unknown) => {435+ if ((msg as { type?: string } | null)?.type === "VOUCHED_DEVICE_PENDING") {436+ void loadPendingVouched();437+ }438+ };439+ chrome.runtime?.onMessage?.addListener(onBroadcast);440+ onCleanup(() => {441+ try { chrome.runtime?.onMessage?.removeListener(onBroadcast); } catch { /* ignore */ }442+ });443+375444 if ((licenseRes as { ok: boolean }).ok) {376445 setLicenseInfo((licenseRes as { ok: boolean; data?: LicenseInfo }).data ?? null);377446 }@@ -1321,9 +1390,82 @@13211390 pair13221391 </button>13231392 </div>1393+ {/* UI-3 / MV-7: devices vouched by a paired peer awaiting confirmation. */}1394+ <Show when={pendingVouched().length > 0}>1395+ <p class="peach-settings-sub-label">1396+ PENDING DEVICES1397+ <span class="peach-pro-badge" style="background:#C8A840;color:#1a1a1a">{pendingVouched().length}</span>1398+ </p>1399+ <For each={pendingVouched()}>1400+ {(dev) => (1401+ <div class="peach-settings-row peach-settings-row--top">1402+ <div class="peach-settings-row-main">1403+ <span>{dev.deviceName}</span>1404+ <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1405+ Was added by another of your devices. Allow it to sync with this vault?1406+ </span>1407+ </div>1408+ <div style="display:flex;gap:6px;flex-shrink:0">1409+ <button1410+ class="peach-btn peach-btn--sm"1411+ disabled={vouchedBusy() === dev.deviceId}1412+ onClick={() => void confirmVouched(dev.deviceId)}1413+ >Allow</button>1414+ <button1415+ class="peach-btn peach-btn--subtle peach-btn--sm"1416+ disabled={vouchedBusy() === dev.deviceId}1417+ onClick={() => void rejectVouched(dev.deviceId)}1418+ >Reject</button>1419+ </div>1420+ </div>1421+ )}1422+ </For>1423+ </Show>1424+13241425 <div class="peach-info-notice" style="margin-top:8px">13251426 Sync uses the same zero-knowledge vault format as backup and restore; pairing authorizes a device, not a cloud account.13261427 </div>1428+1429+ {/* UI-4 / MV-8: rotate sync identity (re-keys the vault, invalidates lost devices). */}1430+ <p class="peach-settings-sub-label">RECOVERY</p>1431+ <Show1432+ when={rotateConfirmOpen()}1433+ fallback={1434+ <div class="peach-settings-row peach-settings-row--top">1435+ <div class="peach-settings-row-main">1436+ <span>Rotate sync identity</span>1437+ <span class="peach-settings-value" style="font-size:11px;line-height:1.5;color:var(--peach-t2)">1438+ Use this if a paired device was lost or stolen. Re-keys the vault so the old device's key is useless.1439+ </span>1440+ </div>1441+ <button1442+ class="peach-btn peach-btn--subtle peach-btn--sm"1443+ style="color:var(--peach-breach);border-color:color-mix(in srgb,var(--peach-breach) 40%,transparent)"1444+ onClick={() => { setRotateError(null); setRotateConfirmOpen(true); }}1445+ >rotate</button>1446+ </div>1447+ }1448+ >1449+ <div class="peach-info-notice" style="margin-top:8px;border-color:color-mix(in srgb,var(--peach-breach) 40%,transparent)">1450+ This will disconnect all paired devices — they will need to pair again — and your 12 recovery words will change. You'll be shown the new words next.1451+ <Show when={rotateError()}>1452+ <div style="color:var(--peach-breach);margin-top:6px">{rotateError()}</div>1453+ </Show>1454+ <div style="display:flex;gap:6px;margin-top:10px">1455+ <button1456+ class="peach-btn peach-btn--subtle peach-btn--sm"1457+ disabled={rotating()}1458+ onClick={() => setRotateConfirmOpen(false)}1459+ >Cancel</button>1460+ <button1461+ class="peach-btn peach-btn--sm"1462+ style="background:var(--peach-breach);border-color:var(--peach-breach)"1463+ disabled={rotating()}1464+ onClick={() => void doRotateSyncIdentity()}1465+ >{rotating() ? "Rotating…" : "Rotate sync identity"}</button>1466+ </div>1467+ </div>1468+ </Show>13271469 </div>13281470 </div>13291471 );