698feaf6f97c
add Panic three-step flow, DESTROY_VAULT handler, Settings danger zone, panic CSS
- time
- 2026-04-28 18:27 UTC · 6 months ago
- machine
- koh2
- parent
- 3e34a4a87aa1
↓ peach-browser-698feaf6.tar.gzatlas at this save
files changed4 files
src/background/messages.ts+14 −0src/popup/App.tsx+46 −2src/popup/views/Panic.tsx+146 −0src/shared/types.ts+1 −0src/background/messages.ts+14 −0
@@ -614,6 +614,20 @@614614 const declined = await vault.isCardDeclined(last4, expMonth, expYear);615615 return { ok: true, data: declined };616616 }617+ case "DESTROY_VAULT": {618+ // Irreversible by design. Lock zeroes the derived key + in-memory entries;619+ // storage.local.clear() wipes every Peach key (slots, settings, license,620+ // onboarded flag, snapshots, sync state, declined cards, recall, etc.);621+ // session.clear() wipes pending passkey requests + pro-notice queues.622+ // Caller (Panic.tsx) handles UI navigation back to onboarding.623+ vault.lock();624+ await chrome.storage.local.clear();625+ if (chrome.storage.session) {626+ await chrome.storage.session.clear();627+ }628+ return { ok: true };629+ }630+617631 case "SUBMIT_FEEDBACK": {618632 const { category, message, version, browser } = msg.payload as {619633 category: string;src/popup/App.tsx+46 −2
@@ -28,6 +28,7 @@2828 const SettingsView = lazy(() => import("./views/Settings").then(m => ({ default: m.SettingsView })));2929 const HealthView = lazy(() => import("./views/Health").then(m => ({ default: m.HealthView })));3030 const TrashView = lazy(() => import("./views/TrashView").then(m => ({ default: m.TrashView })));31+const PanicView = lazy(() => import("./views/Panic").then(m => ({ default: m.PanicView })));3132 3233 type View =3334 | "lock"@@ -39,7 +40,8 @@3940 | "import"4041 | "export"4142 | "health"42- | "trash";43+ | "trash"44+ | "panic";4345 type BootState = "loading" | "ready";4446 4547 const canarySuppressMap = new Map<string, boolean>();@@ -199,6 +201,10 @@199201 const [listSearchSeedNonce, setListSearchSeedNonce] = createSignal(0);200202 const [pendingCelebration, setPendingCelebration] = createSignal(false);201203 const [pendingProNotice, setPendingProNotice] = createSignal<{ feature: string } | null>(null);204+ // True while Panic Step 1 has bounced the user out to Export. When Export's205+ // onBack fires we return to Panic instead of Settings so the user keeps the206+ // destruction context they came from.207+ const [returnToPanic, setReturnToPanic] = createSignal(false);202208 203209 let contentPhaseTimer: number | undefined;204210 @@ -1062,6 +1068,7 @@10621068 onBreachCheckDone={loadEntries}10631069 onOpenHealth={openHealth}10641070 onOpenFileImport={handleOpenFileImport}1071+ onOpenPanic={() => setView("panic")}10651072 />10661073 </Suspense>10671074 </Show>@@ -1080,7 +1087,44 @@10801087 </Show>10811088 <Show when={view() === "export"}>10821089 <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1083- <ExportView onBack={handleBack} />1090+ <ExportView1091+ onBack={() => {1092+ if (returnToPanic()) {1093+ setReturnToPanic(false);1094+ setView("panic");1095+ } else {1096+ handleBack();1097+ }1098+ }}1099+ />1100+ </Suspense>1101+ </Show>1102+ <Show when={view() === "panic"}>1103+ <Suspense fallback={<div class="peach-loading"><PeachWordmark /></div>}>1104+ <PanicView1105+ onCancel={() => setView("settings")}1106+ onOpenExport={() => {1107+ setReturnToPanic(true);1108+ setView("export");1109+ }}1110+ onDestroyed={async () => {1111+ clearCanarySuppressions();1112+ setEntries([]);1113+ setSelectedEntry(null);1114+ setEditEntry(undefined);1115+ setHasVault(false);1116+ setOnboarded(false);1117+ setProStatus(false);1118+ setPendingPasskey(null);1119+ setUpgradeHighlight(null);1120+ setSettingsInitialSection(null);1121+ setPendingProNotice(null);1122+ setPendingCelebration(false);1123+ setReturnToPanic(false);1124+ setContentPhase("idle");1125+ setView("lock");1126+ }}1127+ />10841128 </Suspense>10851129 </Show>10861130 <Show when={view() === "health"}>src/popup/views/Panic.tsx+146 −0
@@ -0,0 +1,146 @@1+import { createSignal, Show, Switch, Match } from "solid-js";2+import { sendMessage } from "../App";3+4+interface PanicProps {5+ onCancel: () => void;6+ onOpenExport: () => void;7+ onDestroyed: () => void | Promise<void>;8+}9+10+export function PanicView(props: PanicProps) {11+ const [step, setStep] = createSignal<1 | 2 | 3>(1);12+ const [confirmText, setConfirmText] = createSignal("");13+ const [destroying, setDestroying] = createSignal(false);14+15+ const canDestroy = () => confirmText() === "DESTROY";16+17+ async function handleDestroy() {18+ if (destroying()) return;19+ setDestroying(true);20+ const res = await sendMessage({ type: "DESTROY_VAULT" });21+ if (!res?.ok) {22+ setDestroying(false);23+ return;24+ }25+ await props.onDestroyed();26+ }27+28+ return (29+ <Switch>30+ <Match when={step() === 1}>31+ <div class="peach-panic">32+ <h2 class="peach-panic-title">Before you destroy your vault</h2>33+ <p class="peach-panic-body">34+ Once destroyed, your vault cannot be recovered. Ever. There is no35+ undo, no support ticket, no way back.36+ </p>37+ <p class="peach-panic-body">38+ If you haven't already, export your vault now. It takes ten seconds39+ and you'll be glad you did.40+ </p>41+ <button42+ class="peach-panic-button peach-panic-button--primary"43+ onClick={() => props.onOpenExport()}44+ >45+ Export my vault first46+ </button>47+ <div class="peach-panic-divider"></div>48+ <p class="peach-panic-body">49+ I have already exported my vault or I don't want to.50+ </p>51+ <button class="peach-panic-link" onClick={() => setStep(2)}>52+ Continue to destruction →53+ </button>54+ <div class="peach-panic-actions">55+ <button56+ class="peach-panic-button peach-panic-button--ghost"57+ onClick={() => props.onCancel()}58+ >59+ Back to settings60+ </button>61+ </div>62+ </div>63+ </Match>64+65+ <Match when={step() === 2}>66+ <div class="peach-panic peach-panic-step2">67+ <span class="peach-panic-icon" aria-hidden="true">⚠</span>68+ <h2 class="peach-panic-title peach-panic-title--severe">69+ This will permanently destroy your vault.70+ </h2>71+ <p class="peach-panic-body peach-panic-body--severe">72+ Every password, card, passkey, note, and setting will be wiped from73+ this device. This cannot be undone.74+ </p>75+ <p class="peach-panic-confirm-label">Type DESTROY to confirm:</p>76+ <input77+ class="peach-panic-confirm-input"78+ classList={{ "peach-panic-confirm-input--match": canDestroy() }}79+ type="text"80+ value={confirmText()}81+ onInput={(e) => setConfirmText(e.currentTarget.value)}82+ autocomplete="off"83+ spellcheck={false}84+ autocapitalize="off"85+ aria-label="Type DESTROY to confirm"86+ />87+ <div class="peach-panic-actions">88+ <button89+ class="peach-panic-button peach-panic-button--danger"90+ disabled={!canDestroy()}91+ onClick={() => setStep(3)}92+ >93+ Yes, destroy my vault94+ </button>95+ <button96+ class="peach-panic-button peach-panic-button--ghost"97+ onClick={() => {98+ setConfirmText("");99+ props.onCancel();100+ }}101+ >102+ Cancel — keep my vault103+ </button>104+ </div>105+ </div>106+ </Match>107+108+ <Match when={step() === 3}>109+ <div class="peach-panic peach-panic-step3">110+ <div class="peach-panic-bar">THIS IS YOUR LAST CHANCE</div>111+ <p class="peach-panic-body peach-panic-body--final-intro">112+ Your vault is about to be permanently destroyed.113+ </p>114+ <p class="peach-panic-body peach-panic-body--final">115+ There is no recovery.116+ <br />117+ There is no support.118+ <br />119+ There is no undo.120+ </p>121+ <div class="peach-panic-divider"></div>122+ <div class="peach-panic-actions">123+ <button124+ class="peach-panic-final-button"125+ disabled={destroying()}126+ onClick={() => void handleDestroy()}127+ >128+ {destroying() ? "Destroying…" : "Destroy my vault forever"}129+ </button>130+ <Show when={!destroying()}>131+ <button132+ class="peach-panic-button peach-panic-button--ghost"133+ onClick={() => {134+ setConfirmText("");135+ setStep(1);136+ }}137+ >138+ No — take me back139+ </button>140+ </Show>141+ </div>142+ </div>143+ </Match>144+ </Switch>145+ );146+}src/shared/types.ts+1 −0
@@ -218,6 +218,7 @@218218 | "INITIATE_PASSWORD_CHANGE"219219 | "DEACTIVATE_LICENSE"220220 | "GET_IDENTITIES"221+ | "DESTROY_VAULT"221222 // Content-script-bound message types — sent BY the background TO content222223 // scripts. They never reach messages.ts's switch. Kept in this union so223224 // existing call sites type-check; do not add cases for them in the