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";7import { storageSet } from "../../shared/storage";89interface OnboardingCard {10  headline: string;11  body: string[];12  warning: string | null;13  mobileCard?: boolean;14}1516const BASE_CARDS: OnboardingCard[] = [17  {18    headline: "Your passwords\nlive here.\nNowhere else.",19    body: [20      "No account. No server. No one but you can ever see your vault.",21      "Everything is encrypted on your device with a password only you know.",22    ],23    warning: null,24  },25  {26    headline: "One rule.",27    body: [28      "Your master password is yours alone. Peach cannot recover it. There is no reset.",29      "Choose something you will remember. Write it down somewhere safe.",30    ],31    warning: "If you forget it, your vault cannot be opened.",32  },33  {34    headline: "Everything\nyou need.",35    body: [36      "Passwords, passkeys, cards, secure notes, two-factor codes.",37      "Autofills on every site. Generates strong passwords. Detects breaches.",38      "Free forever. Pro adds sync, mobile, and passkeys.",39    ],40    warning: null,41  },42];4344function getAutofillPlugin() {45  try {46    return registerPlugin<{47      openAutofillSettings(): Promise<void>;48    }>("Autofill");49  } catch {50    return null;51  }52}5354const AutofillPlugin = getAutofillPlugin();5556const MOBILE_CARD: OnboardingCard = {57  headline: "Enable autofill.",58  body: [59    "Peach fills passwords in Chrome, Safari, and every app on your phone.",60  ],61  warning: null,62  mobileCard: true,63};6465const CARDS: OnboardingCard[] = Capacitor.isNativePlatform()66  ? [...BASE_CARDS, MOBILE_CARD]67  : BASE_CARDS;6869const CARD_EXIT_MS = 120;70const CARD_ENTER_GAP_MS = 60;7172interface OnboardingProps {73  onComplete: () => void | Promise<void>;74}7576export function Onboarding(props: OnboardingProps) {77  const [card, setCard] = createSignal(0);78  const [exiting, setExiting] = createSignal(false);79  const [completing, setCompleting] = createSignal(false);8081  async function advance() {82    if (exiting() || completing()) return;83    // Mobile card has its own buttons — don't advance on click84    if (CARDS[card()]?.mobileCard) return;85    if (card() < CARDS.length - 1) {86      setExiting(true);87      window.setTimeout(() => {88        setCard((c) => c + 1);89        setExiting(false);90      }, CARD_EXIT_MS + CARD_ENTER_GAP_MS);91    } else {92      setCompleting(true);93      try {94        await storageSet("peach_onboarded", "true");95      } catch {96        // storage unavailable — still complete the flow97      }98      window.setTimeout(() => {99        void props.onComplete();100      }, 200);101    }102  }103104  async function handleEnableAutofill() {105    if (AutofillPlugin) {106      await AutofillPlugin.openAutofillSettings();107    }108    setCompleting(true);109    try {110      await storageSet("peach_onboarded", "true");111    } catch {112      // storage unavailable — still complete the flow113    }114    window.setTimeout(() => {115      void props.onComplete();116    }, 200);117  }118119  async function handleSkipAutofill() {120    setCompleting(true);121    try {122      await storageSet("peach_onboarded", "true");123    } catch {124      // storage unavailable — still complete the flow125    }126    window.setTimeout(() => {127      void props.onComplete();128    }, 200);129  }130131  return (132    <section133      class="peach-screen peach-theme peach-onboarding"134      classList={{ "is-completing": completing() }}135    >136      <RuneBackground opacity={0.07} />137138      <header class="peach-screen-header peach-onboarding-header">139        <PeachWordmark class="peach-wordmark-wrap" />140      </header>141142      <div143        class="peach-onboarding-card"144        classList={{ "is-exiting": exiting() }}145        onClick={advance}146        role="button"147        tabIndex={0}148        onKeyDown={(e) => {149          if (e.key === "Enter" || e.key === " ") {150            e.preventDefault();151            advance();152          }153        }}154      >155        <h2 class="peach-onboarding-card-headline">156          {CARDS[card()]!.headline}157        </h2>158        <hr class="peach-onboarding-divider" />159        <For each={CARDS[card()]!.body}>160          {(para) => <p class="peach-onboarding-body">{para}</p>}161        </For>162        <Show when={CARDS[card()]!.warning}>163          <p class="peach-onboarding-warning">164            <span class="peach-onboarding-warning-icon" aria-hidden="true">⚠</span>165            <span>{CARDS[card()]!.warning}</span>166          </p>167        </Show>168169        <Show when={CARDS[card()]?.mobileCard}>170          <div class="peach-onboarding-mobile-card-buttons">171            <button172              class="peach-btn peach-btn--accent peach-btn--md"173              onClick={(e) => { e.stopPropagation(); handleEnableAutofill(); }}174            >175              Enable Peach autofill176            </button>177            <button178              class="peach-btn peach-btn--subtle peach-btn--md"179              onClick={(e) => { e.stopPropagation(); handleSkipAutofill(); }}180            >181              Skip for now182            </button>183          </div>184        </Show>185      </div>186187      <div class="peach-onboarding-nav">188        <div class="peach-onboarding-dots" aria-hidden="true">189          <For each={CARDS}>190            {(_, i) => (191              <div192                class="peach-onboarding-dot"193                classList={{ "is-active": i() === card() }}194              />195            )}196          </For>197        </div>198        <button199          class="peach-onboarding-next"200          onClick={(e) => {201            e.stopPropagation();202            advance();203          }}204          disabled={completing()}205        >206          {card() === CARDS.length - 1 ? "Get started →" : "next →"}207        </button>208      </div>209    </section>210  );211}