b44e8d8142cc
Pro activation celebration — plays once on first popup open after license activation
- time
- 2026-04-27 13:43 UTC · 6 months ago
- machine
- koh2
- parent
- f2a05b205496
↓ peach-browser-b44e8d81.tar.gzatlas at this save
files changed5 files
src/background/license.ts+13 −0src/background/messages.ts+10 −0src/popup/App.tsx+40 −5src/popup/styles/popup.css+66 −0src/popup/views/ProCelebration.tsx+18 −0src/background/license.ts+13 −0
@@ -166,3 +166,16 @@166166 export async function deactivateLicense(): Promise<void> {167167 await chrome.storage.local.remove("peach_license");168168 }169+170+const CELEBRATION_KEY = "peach_pro_celebrated";171+172+export async function shouldPlayCelebration(): Promise<boolean> {173+ const info = await getLicenseInfo();174+ if (!info.valid || info.plan !== "pro") return false;175+ const result = await chrome.storage.local.get(CELEBRATION_KEY) as Record<string, boolean | undefined>;176+ return !result[CELEBRATION_KEY];177+}178+179+export async function markCelebrationPlayed(): Promise<void> {180+ await chrome.storage.local.set({ [CELEBRATION_KEY]: true });181+}src/background/messages.ts+10 −0
@@ -17,6 +17,8 @@1717 deactivateLicense,1818 getLicenseInfo,1919 isProUser,20+ markCelebrationPlayed,21+ shouldPlayCelebration,2022 } from "./license";2123 import {2224 getPasskeyProviderState,@@ -403,6 +405,14 @@403405 }404406 case "DEACTIVATE_LICENSE": {405407 await deactivateLicense();408+ return { ok: true };409+ }410+ case "SHOULD_PLAY_CELEBRATION": {411+ const should = await shouldPlayCelebration();412+ return { ok: true, data: should };413+ }414+ case "MARK_CELEBRATION_PLAYED": {415+ await markCelebrationPlayed();406416 return { ok: true };407417 }408418 case "GET_IDENTITIES": {src/popup/App.tsx+40 −5
@@ -2,6 +2,7 @@22 import { ACCENT_COLORS } from "../shared/types";33 import type { AccentColorName, PasskeyEntry, PendingPasskeyRequest, ProFeature, VaultEntry } from "../shared/types";44 import { LockView } from "./views/Lock";5+import { ProCelebration } from "./views/ProCelebration";56 import { VaultView, type SortOrder } from "./views/Vault";67 import { EntryView } from "./views/Entry";78 import { AddView } from "./views/Add";@@ -31,6 +32,7 @@3132 3233 type View =3334 | "lock"35+ | "pro-celebration"3436 | "entry"3537 | "list"3638 | "add"@@ -192,6 +194,7 @@192194 const [sortOrder, setSortOrder] = createSignal<SortOrder>("url-first");193195 const [listSearchSeed, setListSearchSeed] = createSignal("");194196 const [listSearchSeedNonce, setListSearchSeedNonce] = createSignal(0);197+ const [pendingCelebration, setPendingCelebration] = createSignal(false);195198 196199 let contentPhaseTimer: number | undefined;197200 @@ -281,10 +284,11 @@281284 // so it must be populated before openVault runs. Previously refreshCurrentTab282285 // ran after openVault, leaving popups that booted already-unlocked falling283286 // back to loadedEntries[0] instead of the domain match.284- const [unlockResponse, vaultResponse] = await Promise.all([287+ const [unlockResponse, vaultResponse, , celebrationResponse] = await Promise.all([285288 sendMessage({ type: "IS_UNLOCKED" }),286289 sendMessage({ type: "HAS_VAULT" }),287290 refreshCurrentTab(),291+ sendMessage({ type: "SHOULD_PLAY_CELEBRATION" }),288292 ]);289293 const unlocked = Boolean(290294 (unlockResponse as { ok: boolean; data?: boolean }).data@@ -292,11 +296,25 @@292296 const vaultExists = Boolean(293297 (vaultResponse as { ok: boolean; data?: boolean }).data294298 );299+ const shouldCelebrate = Boolean(300+ (celebrationResponse as { ok: boolean; data?: boolean }).data301+ );295302 setHasVault(vaultExists);296303 await Promise.all([restoreSettings(), refreshProStatus()]);297304 305+ // Mark played before any animation so a crash mid-sequence never replays it306+ if (shouldCelebrate) {307+ await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });308+ }309+298310 if (unlocked && vaultExists) {299- await openVault();311+ await openVault(undefined, shouldCelebrate);312+ if (shouldCelebrate) {313+ setView("pro-celebration");314+ window.setTimeout(() => setView("entry"), 2500);315+ }316+ } else if (shouldCelebrate) {317+ setPendingCelebration(true);300318 }301319 302320 await refreshPendingPasskey();@@ -416,6 +434,9 @@416434 !e.metaKey &&417435 !e.altKey;418436 437+ // No input during the pro celebration sequence438+ if (v === "pro-celebration") return;439+419440 // Help dialog: Escape closes it; eat the keypress so it doesn't420441 // also navigate away from the underlying view.421442 if (showShortcutsHelp()) {@@ -556,7 +577,7 @@556577 return loadedEntries;557578 }558579 559- async function openVault(preferredEntryId?: string) {580+ async function openVault(preferredEntryId?: string, skipViewSet = false) {560581 // Purge expired trash entries on each vault open561582 const retDays = trashRetentionDays();562583 if (retDays > 0) {@@ -600,14 +621,23 @@600621 setContentTransitioning(false);601622 setContentPhase("enter");602623 queueContentIdle();603- setView("entry");624+ if (!skipViewSet) setView("entry");604625 }605626 606627 async function handleUnlock() {607628 setHasVault(true);608629 triggerPipPulse();609630 await restoreSettings();610- await openVault();631+ const celebrating = pendingCelebration();632+ if (celebrating) {633+ await sendMessage({ type: "MARK_CELEBRATION_PLAYED" });634+ setPendingCelebration(false);635+ }636+ await openVault(undefined, celebrating);637+ if (celebrating) {638+ setView("pro-celebration");639+ window.setTimeout(() => setView("entry"), 2500);640+ }611641 }612642 613643 function handleSelectEntry(entry: VaultEntry) {@@ -763,6 +793,10 @@763793 />764794 }765795 >796+ <Show797+ when={view() !== "pro-celebration"}798+ fallback={<ProCelebration />}799+ >766800 <section767801 id="hd-shell"768802 class="peach-shell peach-theme"@@ -989,6 +1023,7 @@9891023 </Suspense>9901024 </Show>9911025 </section>1026+ </Show>9921027 </Show>9931028 </Show>9941029 </Show>src/popup/styles/popup.css+66 −0
@@ -5252,3 +5252,69 @@52525252 background: var(--peach-accent);52535253 transition: width 200ms ease-out;52545254 }5255+5256+/* ── Pro activation celebration ── */5257+@keyframes peach-pro-bloom {5258+ 0% { filter: drop-shadow(0 0 0px var(--peach-glow)); }5259+ 40% { filter: drop-shadow(0 0 24px var(--peach-accent))5260+ drop-shadow(0 0 48px var(--peach-glow)); }5261+ 100% { filter: drop-shadow(0 0 0px var(--peach-glow)); }5262+}5263+5264+@keyframes peach-pro-wordmark-in {5265+ from { opacity: 0; transform: translateY(4px); }5266+ to { opacity: 1; transform: translateY(0); }5267+}5268+5269+@keyframes peach-pro-fade-out {5270+ from { opacity: 1; }5271+ to { opacity: 0; }5272+}5273+5274+.peach-pro-celebration {5275+ background:5276+ radial-gradient(120% 80% at 0% 0%, var(--shell-accent-a) 0%, transparent 65%),5277+ radial-gradient(85% 100% at 100% 100%, var(--shell-accent-b) 0%, transparent 70%),5278+ var(--peach-bg);5279+ border: 1px solid var(--peach-t3);5280+ box-shadow:5281+ 0 2px 8px rgba(0,0,0,0.55),5282+ 0 14px 48px rgba(0,0,0,0.7),5283+ 0 0 0 1px rgba(240,232,216,0.04);5284+ overflow: hidden;5285+ width: 320px;5286+ height: 520px;5287+ display: flex;5288+ flex-direction: column;5289+ align-items: center;5290+ justify-content: center;5291+ gap: 8px;5292+ padding: 32px 24px;5293+ text-align: center;5294+}5295+5296+.peach-pro-celebration.is-leaving {5297+ animation: peach-pro-fade-out 400ms ease forwards;5298+}5299+5300+.peach-pro-bloom-target {5301+ font-size: 48px;5302+ animation: peach-pro-bloom 600ms cubic-bezier(0.16, 1, 0.3, 1) forwards;5303+ margin-bottom: 12px;5304+}5305+5306+.peach-pro-wordmark {5307+ font-family: "Cormorant Garamond", "Times New Roman", serif;5308+ font-size: 22px;5309+ font-style: italic;5310+ font-weight: 400;5311+ color: var(--peach-accent);5312+ animation: peach-pro-wordmark-in 400ms cubic-bezier(0.16, 1, 0.3, 1) 500ms both;5313+}5314+5315+.peach-pro-thankyou {5316+ font-size: 12px;5317+ font-style: italic;5318+ color: var(--peach-t2);5319+ animation: peach-pro-wordmark-in 400ms cubic-bezier(0.16, 1, 0.3, 1) 800ms both;5320+}src/popup/views/ProCelebration.tsx+18 −0
@@ -0,0 +1,18 @@1+import { createSignal, onCleanup, onMount } from "solid-js";2+3+export function ProCelebration() {4+ const [leaving, setLeaving] = createSignal(false);5+6+ onMount(() => {7+ const t = window.setTimeout(() => setLeaving(true), 1800);8+ onCleanup(() => window.clearTimeout(t));9+ });10+11+ return (12+ <div class="peach-pro-celebration" classList={{ "is-leaving": leaving() }}>13+ <div class="peach-pro-bloom-target">🍑</div>14+ <div class="peach-pro-wordmark">peach pro</div>15+ <div class="peach-pro-thankyou">thank you.</div>16+ </div>17+ );18+}