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 84 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 97 }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 113 }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 125 }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}