Onboarding.tsx

4.5 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 never sees it.",25      "If you forget it, your 12 recovery words can reset it from settings — save them somewhere safe.",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, passkeys, and advanced vault health.",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  function goBack() {76    if (exiting() || completing()) return;77    if (card() === 0) return;78    setExiting(true);79    window.setTimeout(() => {80      setCard((c) => c - 1);81      setExiting(false);82    }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);83  }8485  return (86    <section87      class="peach-screen peach-theme peach-onboarding"88      classList={{ "is-completing": completing() }}89    >90      <RuneBackground opacity={0.07} />9192      <header class="peach-screen-header peach-onboarding-header">93        <PeachWordmark class="peach-wordmark-wrap" />94      </header>9596      <div97        class="peach-onboarding-card"98        classList={{ "is-exiting": exiting() }}99        onClick={advance}100        role="button"101        tabIndex={0}102        onKeyDown={(e) => {103          if (e.key === "Enter" || e.key === " ") {104            e.preventDefault();105            advance();106          }107        }}108      >109        <h2 class="peach-onboarding-card-headline">110          {CARDS[card()]!.headline}111        </h2>112        <hr class="peach-onboarding-divider" />113        <For each={CARDS[card()]!.body}>114          {(para) => <p class="peach-onboarding-body">{para}</p>}115        </For>116        <Show when={CARDS[card()]!.warning}>117          <p class="peach-onboarding-warning">118            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>119            <span>{CARDS[card()]!.warning}</span>120          </p>121        </Show>122      </div>123124      <div class="peach-onboarding-nav">125        <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>126          <button127            class="peach-onboarding-back"128            onClick={(e) => {129              e.stopPropagation();130              goBack();131            }}132            disabled={completing()}133          >134            ← back135          </button>136        </Show>137        <div class="peach-onboarding-dots" aria-hidden="true">138          <For each={CARDS}>139            {(_, i) => (140              <div141                class="peach-onboarding-dot"142                classList={{ "is-active": i() === card() }}143              />144            )}145          </For>146        </div>147        <button148          class="peach-onboarding-next"149          onClick={(e) => {150            e.stopPropagation();151            advance();152          }}153          disabled={completing()}154        >155          {card() === CARDS.length - 1 ? "Get started →" : "next →"}156        </button>157      </div>158    </section>159  );160}