import { For, Show, createSignal } from "solid-js"; import { storageSet } from "../../shared/storage"; import { PeachWordmark } from "../components/PeachMark"; import { RuneBackground } from "../components/RuneBackground"; interface OnboardingCard { headline: string; body: string[]; warning: string | null; } const BASE_CARDS: OnboardingCard[] = [ { headline: "Welcome to\nPeach Passwords!", body: [ "Your passwords live here. Nowhere else.", "No account. No server ever accesses your vault. No one but you can see it.", "Everything is encrypted on your device with a password only you know.", ], warning: null, }, { headline: "Two keys.", body: [ "Your master password is yours alone. Peach never sees it.", "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.", ], warning: "Lose both and your vault cannot be opened.", }, { headline: "Everything\nyou need.", body: [ "Free includes passwords, cards, secure notes, two-factor codes, autofill, strong passwords, and breach checks.", "Pro adds encrypted sync, Peach Codex paper restore, passkeys, advanced vault health, canary entries, and duress tools.", ], warning: null, }, ]; const CARDS: OnboardingCard[] = BASE_CARDS; /** Index of the card that carries the create/adopt/restore actions. Converges * every path out of onboarding, so a "back" from those flows should resume * here — not rewind to card 0. */ export const ONBOARDING_LAST_CARD = CARDS.length - 1; /** Where the current card index is persisted so closing the popup * mid-onboarding resumes at the same card instead of restarting. */ export const ONBOARDING_CARD_STORAGE_KEY = "peach_onboarding_card"; const CARD_EXIT_MS = 120; const CARD_ENTER_GAP_MS = 60; interface OnboardingProps { onComplete: () => void | Promise; onRestore?: () => void | Promise; onAdopt?: () => void | Promise; /** Card index to start on. App passes ONBOARDING_LAST_CARD when returning * from the create-vault/adopt/restore flows so the user lands back where * they left instead of restarting at the first card. */ initialCard?: number; } export function Onboarding(props: OnboardingProps) { const [card, setCard] = createSignal(props.initialCard ?? 0); const [exiting, setExiting] = createSignal(false); const [completing, setCompleting] = createSignal(false); // Remember where the user got to so a popup close mid-onboarding resumes // here instead of rewinding to the opening card. function persistCard(index: number) { void storageSet(ONBOARDING_CARD_STORAGE_KEY, String(index)).catch(() => {}); } async function advance() { if (exiting() || completing()) return; if (card() < CARDS.length - 1) { setExiting(true); window.setTimeout(() => { setCard((c) => c + 1); persistCard(card()); setExiting(false); }, CARD_EXIT_MS + CARD_ENTER_GAP_MS); } else { setCompleting(true); // Don't persist peach_onboarded here. App.tsx persists it only when a // vault actually exists — so a user who quits mid-vault-creation // returns to the start of onboarding instead of getting stranded on // the create-vault screen with no back path. window.setTimeout(() => { void props.onComplete(); }, 200); } } function goBack() { if (exiting() || completing()) return; if (card() === 0) return; setExiting(true); window.setTimeout(() => { setCard((c) => c - 1); persistCard(card()); setExiting(false); }, CARD_EXIT_MS + CARD_ENTER_GAP_MS); } return (
{ // Final card: creation is an explicit choice; a stray card-body tap // must not silently start vault creation. if (card() === CARDS.length - 1) return; advance(); }} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { if (card() === CARDS.length - 1) return; e.preventDefault(); advance(); } }} >

{CARDS[card()]!.headline}


{(para) =>

{para}

}

{CARDS[card()]!.warning}

Already using Peach?

0} fallback={
}>
); }