566a2cd79953
fix: Separate Pro activation and replay animations (v3.6.91)
- time
- 2026-05-21 22:26 UTC · 5 months ago
- machine
- koh2
- parent
- c5af595106b5
↓ peach-browser-566a2cd7.tar.gzatlas at this save
files changed8 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 −0manifest.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": {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",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",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": {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={() => {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 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()}>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 () => {