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
4 files src/background/messages.ts+14 −0src/popup/App.tsx+46 −2src/popup/views/Panic.tsx+146 −0src/shared/types.ts+1 −0
src/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;

view file

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"}>

view file

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+}

view file

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

view file