Onboarding.tsx

6.0 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 ever accesses your vault. No one but you can see it.",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  onRestore?: () => void | Promise<void>;46  onAdopt?: () => 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      // Don't persist peach_onboarded here. App.tsx persists it only when a65      // vault actually exists — so a user who quits mid-vault-creation66      // returns to the start of onboarding instead of getting stranded on67      // the create-vault screen with no back path.68      window.setTimeout(() => {69        void props.onComplete();70      }, 200);71    }72  }7374  function goBack() {75    if (exiting() || completing()) return;76    if (card() === 0) return;77    setExiting(true);78    window.setTimeout(() => {79      setCard((c) => c - 1);80      setExiting(false);81    }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);82  }8384  return (85    <section86      class="peach-screen peach-theme peach-onboarding"87      classList={{ "is-completing": completing() }}88    >89      <RuneBackground opacity={0.07} />9091      <header class="peach-screen-header peach-onboarding-header">92        <PeachWordmark class="peach-wordmark-wrap" />93      </header>9495      <div96        class="peach-onboarding-card"97        classList={{ "is-exiting": exiting() }}98        onClick={advance}99        role="button"100        tabIndex={0}101        onKeyDown={(e) => {102          if (e.key === "Enter" || e.key === " ") {103            e.preventDefault();104            advance();105          }106        }}107      >108        <h2 class="peach-onboarding-card-headline">109          {CARDS[card()]!.headline}110        </h2>111        <hr class="peach-onboarding-divider" />112        <For each={CARDS[card()]!.body}>113          {(para) => <p class="peach-onboarding-body">{para}</p>}114        </For>115        <Show when={CARDS[card()]!.warning}>116          <p class="peach-onboarding-warning">117            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>118            <span>{CARDS[card()]!.warning}</span>119          </p>120        </Show>121        <Show when={card() === CARDS.length - 1}>122          <p class="peach-onboarding-choice-heading">Already using Peach?</p>123          <div class="peach-onboarding-choice-row">124            <button125              type="button"126              class="peach-onboarding-choice peach-onboarding-choice--adopt"127              onClick={(e) => {128                e.stopPropagation();129                void props.onAdopt?.();130              }}131            >132              <span class="peach-onboarding-choice-label">133                Pair with another device134                <span class="peach-pro-badge">Pro</span>135              </span>136              <span class="peach-onboarding-choice-detail">137                Scan a QR and confirm one 8-digit code.138              </span>139            </button>140            <button141              type="button"142              class="peach-onboarding-choice"143              onClick={(e) => {144                e.stopPropagation();145                void props.onRestore?.();146              }}147            >148              <span class="peach-onboarding-choice-label">Restore a backup</span>149              <span class="peach-onboarding-choice-detail">150                Use PeachScript, a .peach file, CSV, or Codex.151              </span>152            </button>153          </div>154        </Show>155      </div>156157      <div class="peach-onboarding-nav">158        <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>159          <button160            class="peach-onboarding-back"161            onClick={(e) => {162              e.stopPropagation();163              goBack();164            }}165            disabled={completing()}166          >167            ← back168          </button>169        </Show>170        <div class="peach-onboarding-dots" aria-hidden="true">171          <For each={CARDS}>172            {(_, i) => (173              <div174                class="peach-onboarding-dot"175                classList={{ "is-active": i() === card() }}176              />177            )}178          </For>179        </div>180        <button181          class="peach-onboarding-next"182          onClick={(e) => {183            e.stopPropagation();184            advance();185          }}186          disabled={completing()}187        >188          {card() === CARDS.length - 1 ? "Create vault" : "next →"}189        </button>190      </div>191    </section>192  );193}