566a2cd79953

fix: Separate Pro activation and replay animations (v3.6.91)

- Restore full-screen Pro celebration after applying a license in Settings - Keep already-Pro star and peach clicks on the same in-card replay path - Stop Peach click propagation and reset replay state before each run - Slow and soften the Settings Stack replay timing Verification: npm test -- --run src/popup/views/contracts.test.tsx; npm run typecheck; npm run build

time
2026-05-21 22:26 UTC · 5 months ago
machine
koh2
parent
c5af595106b5
8 files manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+2 −2package.json+1 −1src/popup/App.tsx+2 −7src/popup/styles/settings-stack.css+10 −10src/popup/views/Settings.tsx+12 −5src/popup/views/contracts.test.tsx+80 −0
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach",5-  "version": "3.6.90",5+  "version": "3.6.91",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "3.6.90",4+  "version": "3.6.91",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package-lock.json+2 −2
@@ -1,12 +1,12 @@11 {22   "name": "peach",3-  "version": "3.6.90",3+  "version": "3.6.91",44   "lockfileVersion": 3,55   "requires": true,66   "packages": {77     "": {88       "name": "peach",9-      "version": "3.6.90",9+      "version": "3.6.91",1010       "dependencies": {1111         "@bokuweb/zstd-wasm": "^0.0.27",1212         "@stablelib/blake2s": "^2.0.1",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.90",3+  "version": "3.6.91",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/App.tsx+2 −7
@@ -1411,13 +1411,8 @@14111411                     const should = await sendMessage({ type: "SHOULD_PLAY_CELEBRATION" });14121412                     if ((should as { ok: boolean; data?: boolean }).data === true) {14131413                       await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });1414-                      // When activating from Settings, the in-deck fan animation1415-                      // IS the celebration — skip the full-screen overlay so1416-                      // Settings isn't unmounted mid-animation.1417-                      if (view() !== "settings") {1418-                        setView("pro-celebration");1419-                        window.setTimeout(() => setView("settings"), 2500);1420-                      }1414+                      setView("pro-celebration");1415+                      window.setTimeout(() => setView("settings"), 2500);14211416                     }14221417                   }}14231418                   onBack={() => {

view file

src/popup/styles/settings-stack.css+10 −10
@@ -633,27 +633,27 @@633633   transition: none !important;634634 }635635 636-/* ── Fan animation: fast card transitions during the pro celebration cycle ── */636+/* ── Fan animation: measured card transitions during the Pro replay cycle ── */637637 .stk-deck.is-fanning .stk-card {638638   transition:639-    transform 70ms ease,640-    height 70ms ease,641-    box-shadow 70ms ease,642-    opacity 70ms ease;639+    transform 140ms cubic-bezier(0.22, 1, 0.36, 1),640+    height 140ms cubic-bezier(0.22, 1, 0.36, 1),641+    box-shadow 140ms ease,642+    opacity 140ms ease;643643 }644644 645645 /* ── Card shake when the pro card lands after the fan ── */646646 .stk-card.is-celebrating {647647   transition: none !important;648-  animation: stk-card-celebrate 680ms cubic-bezier(0.34, 1.56, 0.64, 1) both;648+  animation: stk-card-celebrate 880ms cubic-bezier(0.22, 1, 0.36, 1) both;649649 }650650 651651 @keyframes stk-card-celebrate {652652   0%   { transform: translateY(var(--stk-card-top)); }653-  22%  { transform: translateY(calc(var(--stk-card-top) - 16px)) rotate(-2.5deg); }654-  44%  { transform: translateY(calc(var(--stk-card-top) + 7px)) rotate(1.8deg); }655-  66%  { transform: translateY(calc(var(--stk-card-top) - 7px)) rotate(-1.2deg); }656-  82%  { transform: translateY(calc(var(--stk-card-top) + 3px)); }653+  24%  { transform: translateY(calc(var(--stk-card-top) - 10px)) rotate(-1.3deg); }654+  48%  { transform: translateY(calc(var(--stk-card-top) + 5px)) rotate(0.9deg); }655+  70%  { transform: translateY(calc(var(--stk-card-top) - 4px)) rotate(-0.45deg); }656+  86%  { transform: translateY(calc(var(--stk-card-top) + 2px)); }657657   100% { transform: translateY(var(--stk-card-top)); }658658 }659659 

view file

src/popup/views/Settings.tsx+12 −5
@@ -75,6 +75,8 @@7575 const FEEDBACK_CATEGORIES = ["Bug report", "Feature request", "Question", "Other"];7676 const FEEDBACK_DATA_PERMISSION = "technicalAndInteraction";7777 const MAX_FEEDBACK_MESSAGE_LENGTH = 4000;78+const PRO_CELEBRATION_CARD_STEP_MS = 150;79+const PRO_CELEBRATION_THANK_YOU_MS = 3200;7880 7981 function sanitizeFeedbackMessage(value: string): string {8082   return value@@ -154,20 +156,26 @@154156 155157   function triggerCelebration() {156158     if (isFanning()) return;159+    setJustActivated(false);157160     setIsFanning(true);158161     setActiveCard("pro");159162     const fanOrder: CardId[] = ["protection", "vault", "sync", "appearance", "shortcuts", "feedback"];160163     fanOrder.forEach((id, i) => {161-      window.setTimeout(() => setActiveCard(id), (i + 1) * 80);164+      window.setTimeout(() => setActiveCard(id), (i + 1) * PRO_CELEBRATION_CARD_STEP_MS);162165     });163166     window.setTimeout(() => {164167       setActiveCard("pro");165168       setIsFanning(false);166169       setJustActivated(true);167-      window.setTimeout(() => setJustActivated(false), 2800);168-    }, (fanOrder.length + 1) * 80);170+      window.setTimeout(() => setJustActivated(false), PRO_CELEBRATION_THANK_YOU_MS);171+    }, (fanOrder.length + 1) * PRO_CELEBRATION_CARD_STEP_MS);169172   }170173 174+  function handleProCelebrationClick(e: MouseEvent) {175+    e.stopPropagation();176+    triggerCelebration();177+  }178+171179   const [checkingBreaches, setCheckingBreaches] = createSignal(false);172180   const [breachResult, setBreachResult] = createSignal<string | null>(null);173181   const [licenseInfo, setLicenseInfo] = createSignal<LicenseInfo | null>(null);@@ -1578,7 +1586,6 @@15781586       await handleActivateLicense();15791587       if ((licenseInfo()?.valid ?? false) && licenseInfo()?.plan === "pro") {15801588         await props.onLicenseChange?.();1581-        triggerCelebration();15821589       }15831590     }15841591 @@ -1674,7 +1681,7 @@16741681                 class="peach-pro-active-mark-wrap"16751682                 type="button"16761683                 aria-label="Celebrate"1677-                onClick={() => triggerCelebration()}1684+                onClick={handleProCelebrationClick}16781685               >16791686                 <PeachMark class="peach-pro-active-mark" />16801687                 <Show when={justActivated()}>

view file

src/popup/views/contracts.test.tsx+80 −0
@@ -141,6 +141,35 @@141141     };142142   }143143 144+  function settingsProps(overrides: Partial<Parameters<typeof SettingsView>[0]> = {}) {145+    return {146+      entriesCount: 1,147+      breachMonitoring: true,148+      lockOnClose: true,149+      autoLockMinutes: 5,150+      trashRetentionDays: 30,151+      theme: "dark" as const,152+      accentColor: "peach" as const,153+      textSize: "medium" as const,154+      reduceMotion: false,155+      onBreachMonitoringChange: vi.fn(),156+      onLockOnCloseChange: vi.fn(),157+      onAutoLockMinutesChange: vi.fn(),158+      onTrashRetentionChange: vi.fn(),159+      onThemeChange: vi.fn(),160+      onAccentChange: vi.fn(),161+      onTextSizeChange: vi.fn(),162+      onReduceMotionChange: vi.fn(),163+      shortcuts: DEFAULT_BINDINGS,164+      onShortcutsChange: vi.fn(),165+      onBack: vi.fn(),166+      onImport: vi.fn(),167+      onExport: vi.fn(),168+      onBreachCheckDone: vi.fn(),169+      ...overrides,170+    };171+  }172+144173   async function selectImportFile(145174     container: HTMLElement,146175     contents = '{"export":"test"}'@@ -429,6 +458,57 @@429458     expect(container.textContent).toContain("Someone will process it soon");430459     expect(container.querySelector(".peach-feedback-textarea")).toBeNull();431460     expect(container.querySelector(".peach-feedback-submit")).toBeNull();461+  });462+463+  it("Settings keeps license activation on the full-screen celebration path", async () => {464+    const onLicenseChange = vi.fn(async () => undefined);465+    sendMessageMock.mockImplementation(async (msg: unknown) => {466+      const type = (msg as { type?: string }).type;467+      if (type === "GET_LICENSE") return { ok: true, data: { valid: false, plan: "free" } };468+      if (type === "ACTIVATE_LICENSE") {469+        return { ok: true, data: { valid: true, plan: "pro", issued: "2026-05-21" } };470+      }471+      if (type === "GET_ENTRIES") return { ok: true, data: [] };472+      return { ok: true, data: null };473+    });474+475+    const container = mount(<SettingsView {...settingsProps({ isPro: false, onLicenseChange })} />);476+    await flushView();477+478+    inputText(479+      container.querySelector(".peach-pro-key-inline-input") as HTMLInputElement,480+      "PEACH-TEST-KEY",481+    );482+    const apply = container.querySelector(".peach-pro-key-apply") as HTMLButtonElement;483+    apply.click();484+485+    await vi.waitFor(() => expect(onLicenseChange).toHaveBeenCalledTimes(1));486+    expect(container.querySelector(".stk-deck")?.classList.contains("is-fanning")).toBe(false);487+    expect(container.textContent).not.toContain("thank you ✦");488+  });489+490+  it("Settings Pro star and peach clicks use the same in-card replay path", async () => {491+    sendMessageMock.mockImplementation(async (msg: unknown) => {492+      const type = (msg as { type?: string }).type;493+      if (type === "GET_LICENSE") return { ok: true, data: { valid: true, plan: "pro", issued: "2026-05-21" } };494+      if (type === "GET_LICENSE_KEY") return { ok: true, data: "PEACH-TEST-KEY" };495+      if (type === "GET_ENTRIES") return { ok: true, data: [] };496+      return { ok: true, data: null };497+    });498+499+    const onLicenseChangeFromStar = vi.fn();500+    const starContainer = mount(<SettingsView {...settingsProps({ isPro: true, onLicenseChange: onLicenseChangeFromStar })} />);501+    await flushView();502+    (starContainer.querySelector(".stk-card-icon--btn") as HTMLButtonElement).click();503+    expect(starContainer.querySelector(".stk-deck")?.classList.contains("is-fanning")).toBe(true);504+    expect(onLicenseChangeFromStar).not.toHaveBeenCalled();505+506+    const onLicenseChangeFromPeach = vi.fn();507+    const peachContainer = mount(<SettingsView {...settingsProps({ isPro: true, onLicenseChange: onLicenseChangeFromPeach })} />);508+    await flushView();509+    (peachContainer.querySelector(".peach-pro-active-mark-wrap") as HTMLButtonElement).click();510+    expect(peachContainer.querySelector(".stk-deck")?.classList.contains("is-fanning")).toBe(true);511+    expect(onLicenseChangeFromPeach).not.toHaveBeenCalled();432512   });433513 434514   it("Settings feedback does not transmit when Firefox data consent is denied", async () => {

view file