Onboarding.tsx

4.6 KB · raw

1import { For, Show, createSignal } from "solid-js";2import { PeachWordmark } from "../components/PeachMark";3import { RuneBackground } from "../components/RuneBackground";45interface OnboardingCard {6  headline: string;7  body: string[];8  warning: string | null;9}1011const BASE_CARDS: OnboardingCard[] = [12  {13    headline: "Your passwords\nlive here.\nNowhere else.",14    body: [15      "No account. No server. No one but you can ever see your vault.",16      "Everything is encrypted on your device with a password only you know.",17    ],18    warning: null,19  },20  {21    headline: "Two keys.",22    body: [23      "Your master password is yours alone. Peach never sees it.",24      "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.",25    ],26    warning: "Lose both and your vault cannot be opened.",27  },28  {29    headline: "Everything\nyou need.",30    body: [31      "Free includes passwords, cards, secure notes, two-factor codes, autofill, strong passwords, and breach checks.",32      "Pro adds encrypted sync, Peach Codex paper restore, passkeys, advanced vault health, canary entries, and duress tools.",33    ],34    warning: null,35  },36];3738const CARDS: OnboardingCard[] = BASE_CARDS;3940const CARD_EXIT_MS = 120;41const CARD_ENTER_GAP_MS = 60;4243interface OnboardingProps {44  onComplete: () => void | Promise<void>;45}4647export function Onboarding(props: OnboardingProps) {48  const [card, setCard] = createSignal(0);49  const [exiting, setExiting] = createSignal(false);50  const [completing, setCompleting] = createSignal(false);5152  async function advance() {53    if (exiting() || completing()) return;54    if (card() < CARDS.length - 1) {55      setExiting(true);56      window.setTimeout(() => {57        setCard((c) => c + 1);58        setExiting(false);59      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);60    } else {61      setCompleting(true);62      // Don't persist peach_onboarded here. App.tsx persists it only when a63      // vault actually exists — so a user who quits mid-vault-creation64      // returns to the start of onboarding instead of getting stranded on65      // the create-vault screen with no back path.66      window.setTimeout(() => {67        void props.onComplete();68      }, 200);69    }70  }7172  function goBack() {73    if (exiting() || completing()) return;74    if (card() === 0) return;75    setExiting(true);76    window.setTimeout(() => {77      setCard((c) => c - 1);78      setExiting(false);79    }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);80  }8182  return (83    <section84      class="peach-screen peach-theme peach-onboarding"85      classList={{ "is-completing": completing() }}86    >87      <RuneBackground opacity={0.07} />8889      <header class="peach-screen-header peach-onboarding-header">90        <PeachWordmark class="peach-wordmark-wrap" />91      </header>9293      <div94        class="peach-onboarding-card"95        classList={{ "is-exiting": exiting() }}96        onClick={advance}97        role="button"98        tabIndex={0}99        onKeyDown={(e) => {100          if (e.key === "Enter" || e.key === " ") {101            e.preventDefault();102            advance();103          }104        }}105      >106        <h2 class="peach-onboarding-card-headline">107          {CARDS[card()]!.headline}108        </h2>109        <hr class="peach-onboarding-divider" />110        <For each={CARDS[card()]!.body}>111          {(para) => <p class="peach-onboarding-body">{para}</p>}112        </For>113        <Show when={CARDS[card()]!.warning}>114          <p class="peach-onboarding-warning">115            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>116            <span>{CARDS[card()]!.warning}</span>117          </p>118        </Show>119      </div>120121      <div class="peach-onboarding-nav">122        <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>123          <button124            class="peach-onboarding-back"125            onClick={(e) => {126              e.stopPropagation();127              goBack();128            }}129            disabled={completing()}130          >131            ← back132          </button>133        </Show>134        <div class="peach-onboarding-dots" aria-hidden="true">135          <For each={CARDS}>136            {(_, i) => (137              <div138                class="peach-onboarding-dot"139                classList={{ "is-active": i() === card() }}140              />141            )}142          </For>143        </div>144        <button145          class="peach-onboarding-next"146          onClick={(e) => {147            e.stopPropagation();148            advance();149          }}150          disabled={completing()}151        >152          {card() === CARDS.length - 1 ? "Get started →" : "next →"}153        </button>154      </div>155    </section>156  );157}