Onboarding.tsx

3.9 KB · raw

1import { For, Show, createSignal } from "solid-js";2import { PeachWordmark } from "../components/PeachMark";3import { RuneBackground } from "../components/RuneBackground";4import { storageSet } from "../../shared/storage";56interface OnboardingCard {7  headline: string;8  body: string[];9  warning: string | null;10}1112const BASE_CARDS: OnboardingCard[] = [13  {14    headline: "Your passwords\nlive here.\nNowhere else.",15    body: [16      "No account. No server. No one but you can ever see your vault.",17      "Everything is encrypted on your device with a password only you know.",18    ],19    warning: null,20  },21  {22    headline: "Two keys.",23    body: [24      "Your master password is yours alone. Peach cannot see it or reset it.",25      "Forget it, and the 12 recovery words you save at setup are the only way back in.",26    ],27    warning: "Lose both and your vault cannot be opened.",28  },29  {30    headline: "Everything\nyou need.",31    body: [32      "Passwords, passkeys, cards, secure notes, two-factor codes.",33      "Autofills on every site. Generates strong passwords. Detects breaches.",34      "Free forever. Pro adds sync and passkeys.",35    ],36    warning: null,37  },38];3940const CARDS: OnboardingCard[] = BASE_CARDS;4142const CARD_EXIT_MS = 120;43const CARD_ENTER_GAP_MS = 60;4445interface OnboardingProps {46  onComplete: () => void | Promise<void>;47}4849export function Onboarding(props: OnboardingProps) {50  const [card, setCard] = createSignal(0);51  const [exiting, setExiting] = createSignal(false);52  const [completing, setCompleting] = createSignal(false);5354  async function advance() {55    if (exiting() || completing()) return;56    if (card() < CARDS.length - 1) {57      setExiting(true);58      window.setTimeout(() => {59        setCard((c) => c + 1);60        setExiting(false);61      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);62    } else {63      setCompleting(true);64      try {65        await storageSet("peach_onboarded", "true");66      } catch {67        // storage unavailable — still complete the flow68      }69      window.setTimeout(() => {70        void props.onComplete();71      }, 200);72    }73  }7475  return (76    <section77      class="peach-screen peach-theme peach-onboarding"78      classList={{ "is-completing": completing() }}79    >80      <RuneBackground opacity={0.07} />8182      <header class="peach-screen-header peach-onboarding-header">83        <PeachWordmark class="peach-wordmark-wrap" />84      </header>8586      <div87        class="peach-onboarding-card"88        classList={{ "is-exiting": exiting() }}89        onClick={advance}90        role="button"91        tabIndex={0}92        onKeyDown={(e) => {93          if (e.key === "Enter" || e.key === " ") {94            e.preventDefault();95            advance();96          }97        }}98      >99        <h2 class="peach-onboarding-card-headline">100          {CARDS[card()]!.headline}101        </h2>102        <hr class="peach-onboarding-divider" />103        <For each={CARDS[card()]!.body}>104          {(para) => <p class="peach-onboarding-body">{para}</p>}105        </For>106        <Show when={CARDS[card()]!.warning}>107          <p class="peach-onboarding-warning">108            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>109            <span>{CARDS[card()]!.warning}</span>110          </p>111        </Show>112      </div>113114      <div class="peach-onboarding-nav">115        <div class="peach-onboarding-dots" aria-hidden="true">116          <For each={CARDS}>117            {(_, i) => (118              <div119                class="peach-onboarding-dot"120                classList={{ "is-active": i() === card() }}121              />122            )}123          </For>124        </div>125        <button126          class="peach-onboarding-next"127          onClick={(e) => {128            e.stopPropagation();129            advance();130          }}131          disabled={completing()}132        >133          {card() === CARDS.length - 1 ? "Get started →" : "next →"}134        </button>135      </div>136    </section>137  );138}