Onboarding.tsx

5.8 KB · raw

1import { For, Show, createSignal } from "solid-js";2import { Capacitor } from "@capacitor/core";34import { PeachWordmark } from "../components/PeachMark";5import { RuneBackground } from "../components/RuneBackground";6import { registerPlugin } from "@capacitor/core";78interface OnboardingCard {9  headline: string;10  body: string[];11  warning: string | null;12  mobileCard?: boolean;13}1415const BASE_CARDS: OnboardingCard[] = [16  {17    headline: "Your passwords\nlive here.\nNowhere else.",18    body: [19      "No account. No server. No one but you can ever see your vault.",20      "Everything is encrypted on your device with a password only you know.",21    ],22    warning: null,23  },24  {25    headline: "One rule.",26    body: [27      "Your master password is yours alone. Peach cannot recover it. There is no reset.",28      "Choose something you will remember. Write it down somewhere safe.",29    ],30    warning: "If you forget it, your vault cannot be opened.",31  },32  {33    headline: "Everything\nyou need.",34    body: [35      "Passwords, passkeys, cards, secure notes, two-factor codes.",36      "Autofills on every site. Generates strong passwords. Detects breaches.",37      "Free forever. Pro adds sync, mobile, and passkeys.",38    ],39    warning: null,40  },41];4243function getAutofillPlugin() {44  try {45    return registerPlugin<{46      openAutofillSettings(): Promise<void>;47    }>("Autofill");48  } catch {49    return null;50  }51}5253const AutofillPlugin = getAutofillPlugin();5455const MOBILE_CARD: OnboardingCard = {56  headline: "Enable autofill.",57  body: [58    "Peach fills passwords in Chrome, Safari, and every app on your phone.",59  ],60  warning: null,61  mobileCard: true,62};6364const CARDS: OnboardingCard[] = Capacitor.isNativePlatform()65  ? [...BASE_CARDS, MOBILE_CARD]66  : BASE_CARDS;6768const CARD_EXIT_MS = 120;69const CARD_ENTER_GAP_MS = 60;7071interface OnboardingProps {72  onComplete: () => void | Promise<void>;73}7475export function Onboarding(props: OnboardingProps) {76  const [card, setCard] = createSignal(0);77  const [exiting, setExiting] = createSignal(false);78  const [completing, setCompleting] = createSignal(false);7980  function advance() {81    if (exiting() || completing()) return;82    // Mobile card has its own buttons — don't advance on click83    if (CARDS[card()]?.mobileCard) return;84    if (card() < CARDS.length - 1) {85      setExiting(true);86      window.setTimeout(() => {87        setCard((c) => c + 1);88        setExiting(false);89      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);90    } else {91      setCompleting(true);92      try {93        chrome.storage.local.set({ peach_onboarded: true });94      } catch {95        // storage unavailable — still complete the flow96      }97      window.setTimeout(() => {98        void props.onComplete();99      }, 200);100    }101  }102103  function handleEnableAutofill() {104    if (AutofillPlugin) {105      void AutofillPlugin.openAutofillSettings();106    }107    setCompleting(true);108    try {109      chrome.storage.local.set({ peach_onboarded: true });110    } catch {111      // storage unavailable — still complete the flow112    }113    window.setTimeout(() => {114      void props.onComplete();115    }, 200);116  }117118  function handleSkipAutofill() {119    setCompleting(true);120    try {121      chrome.storage.local.set({ peach_onboarded: true });122    } catch {123      // storage unavailable — still complete the flow124    }125    window.setTimeout(() => {126      void props.onComplete();127    }, 200);128  }129130  return (131    <section132      class="peach-screen peach-theme peach-onboarding"133      classList={{ "is-completing": completing() }}134    >135      <RuneBackground opacity={0.07} />136137      <header class="peach-screen-header peach-onboarding-header">138        <PeachWordmark class="peach-wordmark-wrap" />139      </header>140141      <div142        class="peach-onboarding-card"143        classList={{ "is-exiting": exiting() }}144        onClick={advance}145        role="button"146        tabIndex={0}147        onKeyDown={(e) => {148          if (e.key === "Enter" || e.key === " ") {149            e.preventDefault();150            advance();151          }152        }}153      >154        <h2 class="peach-onboarding-card-headline">155          {CARDS[card()]!.headline}156        </h2>157        <hr class="peach-onboarding-divider" />158        <For each={CARDS[card()]!.body}>159          {(para) => <p class="peach-onboarding-body">{para}</p>}160        </For>161        <Show when={CARDS[card()]!.warning}>162          <p class="peach-onboarding-warning">163            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>164            <span>{CARDS[card()]!.warning}</span>165          </p>166        </Show>167168        <Show when={CARDS[card()]?.mobileCard}>169          <div class="peach-onboarding-mobile-card-buttons">170            <button171              class="peach-btn peach-btn--accent peach-btn--md"172              onClick={(e) => { e.stopPropagation(); handleEnableAutofill(); }}173            >174              Enable Peach autofill175            </button>176            <button177              class="peach-btn peach-btn--subtle peach-btn--md"178              onClick={(e) => { e.stopPropagation(); handleSkipAutofill(); }}179            >180              Skip for now181            </button>182          </div>183        </Show>184      </div>185186      <div class="peach-onboarding-nav">187        <div class="peach-onboarding-dots" aria-hidden="true">188          <For each={CARDS}>189            {(_, i) => (190              <div191                class="peach-onboarding-dot"192                classList={{ "is-active": i() === card() }}193              />194            )}195          </For>196        </div>197        <button198          class="peach-onboarding-next"199          onClick={(e) => {200            e.stopPropagation();201            advance();202          }}203          disabled={completing()}204        >205          {card() === CARDS.length - 1 ? "Get started →" : "next →"}206        </button>207      </div>208    </section>209  );210}