Onboarding.tsx

6.8 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={() => {99          // Final card: creation is an explicit choice; a stray card-body tap100          // must not silently start vault creation.101          if (card() === CARDS.length - 1) return;102          advance();103        }}104        role="button"105        tabIndex={0}106        onKeyDown={(e) => {107          if (e.key === "Enter" || e.key === " ") {108            if (card() === CARDS.length - 1) return;109            e.preventDefault();110            advance();111          }112        }}113      >114        <h2 class="peach-onboarding-card-headline">115          {CARDS[card()]!.headline}116        </h2>117        <hr class="peach-onboarding-divider" />118        <For each={CARDS[card()]!.body}>119          {(para) => <p class="peach-onboarding-body">{para}</p>}120        </For>121        <Show when={CARDS[card()]!.warning}>122          <p class="peach-onboarding-warning">123            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>124            <span>{CARDS[card()]!.warning}</span>125          </p>126        </Show>127        <Show when={card() === CARDS.length - 1}>128          <div class="peach-onboarding-choice-row">129            <button130              type="button"131              class="peach-onboarding-choice peach-onboarding-choice--create"132              onClick={(e) => {133                e.stopPropagation();134                void advance();135              }}136            >137              <span class="peach-onboarding-choice-label">Create a new vault</span>138              <span class="peach-onboarding-choice-detail">139                Set your master password and start saving.140              </span>141            </button>142          </div>143          <p class="peach-onboarding-choice-heading">Already using Peach?</p>144          <div class="peach-onboarding-choice-row">145            <button146              type="button"147              class="peach-onboarding-choice peach-onboarding-choice--adopt"148              onClick={(e) => {149                e.stopPropagation();150                void props.onAdopt?.();151              }}152            >153              <span class="peach-onboarding-choice-label">154                Pair with another device155                <span class="peach-pro-badge">Pro</span>156              </span>157              <span class="peach-onboarding-choice-detail">158                Scan a QR and confirm one 8-digit code.159              </span>160            </button>161            <button162              type="button"163              class="peach-onboarding-choice"164              onClick={(e) => {165                e.stopPropagation();166                void props.onRestore?.();167              }}168            >169              <span class="peach-onboarding-choice-label">Restore a backup</span>170              <span class="peach-onboarding-choice-detail">171                Use PeachScript, a .peach file, CSV, or Codex.172              </span>173            </button>174          </div>175        </Show>176      </div>177178      <div class="peach-onboarding-nav">179        <Show when={card() > 0} fallback={<div class="peach-onboarding-back-spacer" />}>180          <button181            class="peach-onboarding-back"182            onClick={(e) => {183              e.stopPropagation();184              goBack();185            }}186            disabled={completing()}187          >188            ← back189          </button>190        </Show>191        <div class="peach-onboarding-dots" aria-hidden="true">192          <For each={CARDS}>193            {(_, i) => (194              <div195                class="peach-onboarding-dot"196                classList={{ "is-active": i() === card() }}197              />198            )}199          </For>200        </div>201        <button202          class="peach-onboarding-next"203          onClick={(e) => {204            e.stopPropagation();205            advance();206          }}207          disabled={completing()}208        >209          {card() === CARDS.length - 1 ? "Create vault" : "next →"}210        </button>211      </div>212    </section>213  );214}