12345678910111213141516import {17 Component,18 Show,19 createEffect,20 createSignal,21 onCleanup,22 onMount,23} from "solid-js";24import { sendMessage } from "../../shared/bridge";25import type { PairedDevice } from "../../shared/sync-types";26import { DesktopBird, MobileBird } from "../components/ChirpPairingFlow";27import { formatPairingCode, parsePairingCode } from "../../shared/pairing-code";2829export interface ChirpSyncViewProps {30 isPro: boolean;31 onBack(): void;32 onLearnPro(): void;33 3435363738 initialPair?: "desktop" | null;39 40 onProChanged?: () => void;41}424344type DesktopPhase = "enter" | "connecting" | "sas" | "paired" | "error";45type DesktopPairingError =46 | "invalid_code"47 | "no_host_found"48 | "handshake_failed"49 | "sas_mismatch"50 | "vault_locked";5152const DESKTOP_PAIRED_DISMISS_MS = 2_000;5354function desktopErrorCopy(error: DesktopPairingError | "unknown"): { headline: string; sub: string } {55 switch (error) {56 case "invalid_code":57 58 59 return {60 headline: "Code not found or expired",61 sub: "Check the code on your phone and try again. Codes expire, so grab a fresh one if it's been a while.",62 };63 case "no_host_found":64 return {65 headline: "Couldn't reach your phone",66 sub: "Make sure both devices are on the same Wi-Fi and the pairing screen is still open on your phone.",67 };68 case "sas_mismatch":69 return {70 headline: "Codes don't match",71 sub: "For your safety, pairing was stopped. Start over and compare the two codes carefully.",72 };73 case "vault_locked":74 return {75 headline: "Unlock Peach first",76 sub: "Your vault needs to be unlocked before you can pair a device.",77 };78 case "handshake_failed":79 default:80 return {81 headline: "Pairing failed",82 sub: "Something went wrong while connecting. Please try again.",83 };84 }85}8687export const ChirpSyncView: Component<ChirpSyncViewProps> = (props) => {88 89 90 91 92 const [desktopPhase, setDesktopPhase] = createSignal<DesktopPhase>("enter");93 const [desktopCode, setDesktopCode] = createSignal("");94 const [desktopSas, setDesktopSas] = createSignal<string | null>(null);95 const [desktopError, setDesktopError] = createSignal<DesktopPairingError | "unknown">("unknown");96 const [desktopDevice, setDesktopDevice] = createSignal<PairedDevice | undefined>(undefined);97 const [desktopBusy, setDesktopBusy] = createSignal(false);9899 100 101 102 const desktopCodeReady = () =>103 parsePairingCode(desktopCode()) !== null || /^\d{6}$/.test(desktopCode());104105 function startDesktopFlow(): void {106 setDesktopCode("");107 setDesktopSas(null);108 setDesktopError("unknown");109 setDesktopDevice(undefined);110 setDesktopBusy(false);111 setDesktopPhase("enter");112 }113114 function onDesktopCodeInput(value: string): void {115 const cleaned = value.toUpperCase().replace(/[^0-9A-Z]/g, "").slice(0, 32);116 117 118 119 setDesktopCode(cleaned.length > 6 ? formatPairingCode(cleaned) : cleaned);120 }121122 async function handleDesktopConnect(): Promise<void> {123 if (!desktopCodeReady() || desktopBusy()) return;124 setDesktopBusy(true);125 setDesktopPhase("connecting");126 try {127 const res = (await sendMessage({128 type: "PAIR_WITH_DESKTOP",129 payload: { code: desktopCode() },130 })) as { ok: boolean; error?: string };131 132 133 if (!res.ok) {134 setDesktopError((res.error as DesktopPairingError) ?? "unknown");135 setDesktopPhase("error");136 }137 } catch {138 setDesktopError("unknown");139 setDesktopPhase("error");140 } finally {141 setDesktopBusy(false);142 }143 }144145 async function respondDesktopConfirm(confirmed: boolean): Promise<void> {146 if (desktopBusy()) return;147 setDesktopBusy(true);148 149 150 if (confirmed) setDesktopPhase("connecting");151 else { setDesktopError("sas_mismatch"); setDesktopPhase("error"); }152 try {153 await sendMessage({ type: "DESKTOP_PAIR_CONFIRM", payload: { confirmed } });154 } finally {155 setDesktopBusy(false);156 }157 }158159 160 161 createEffect(() => {162 if (desktopPhase() !== "paired") return;163 const t = setTimeout(() => props.onBack(), DESKTOP_PAIRED_DISMISS_MS);164 onCleanup(() => clearTimeout(t));165 });166167 onMount(() => {168 169 170 if (props.initialPair === "desktop") startDesktopFlow();171 else props.onBack();172173 174 175 176 if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {177 const handler = (msg: { type?: string; payload?: unknown }) => {178 if (msg?.type === "DESKTOP_PAIR_SAS") {179 const p = msg.payload as { sas?: string } | undefined;180 if (typeof p?.sas === "string") { setDesktopSas(p.sas); setDesktopPhase("sas"); }181 } else if (msg?.type === "DESKTOP_PAIR_RESULT") {182 const p = msg.payload as183 | { status: "complete"; device: PairedDevice }184 | { status: "error"; error: DesktopPairingError }185 | undefined;186 if (p?.status === "complete") { setDesktopDevice(p.device); setDesktopPhase("paired"); }187 else if (p?.status === "error") { setDesktopError(p.error); setDesktopPhase("error"); }188 }189 };190 chrome.runtime.onMessage.addListener(handler);191 onCleanup(() => chrome.runtime.onMessage.removeListener(handler));192 }193 194 195 196 onCleanup(() => {197 if (desktopPhase() === "sas") {198 void sendMessage({ type: "DESKTOP_PAIR_CONFIRM", payload: { confirmed: false } });199 }200 });201 });202203 204205 const backArrow = (206 <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">207 <path d="M10 3 L5 8 L10 13" />208 </svg>209 );210211 212 213 const desktopBirdStage = (active: boolean) => (214 <div class={`peach-chirp-stage ${active ? "act-handshake" : "act-listening"}`} aria-hidden="true">215 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">216 <DesktopBird pose="chirping" />217 </div>218 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">219 <MobileBird pose="attentive" />220 </div>221 </div>222 );223224 function desktopLiveText(): string {225 switch (desktopPhase()) {226 case "enter": return "Enter the pairing code from your phone";227 case "connecting": return "Connecting to your phone";228 case "sas": return "Confirm the code matches on your phone";229 case "paired": return "Paired";230 case "error": return desktopErrorCopy(desktopError()).headline;231 }232 }233234 235 const desktopView = () => (236 <div class="peach-chirp-pair peach-desktop-pair" data-phase={desktopPhase()} role="dialog" aria-label="Pair a device with a code">237 <header class="peach-chirp-head">238 <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>{backArrow}</button>239 <div class="peach-chirp-title">Enter a code</div>240 <div class="peach-chirp-head-spacer" />241 </header>242243 {desktopBirdStage(desktopPhase() !== "enter")}244245 <Show when={desktopPhase() === "enter"}>246 <div class="peach-desktop-enter">247 <div class="peach-chirp-headline">Enter the code from your phone</div>248 <div class="peach-chirp-sub">249 Open Peach on your phone, tap “Pair a device”, and type the code it shows.250 Both devices need to be on the same Wi-Fi.251 </div>252 <label class="peach-label" for="desktop-pair-code">pairing code</label>253 <input254 id="desktop-pair-code"255 class="peach-underline-input peach-unlock-input peach-code-input"256 type="text"257 autocapitalize="characters"258 autocomplete="one-time-code"259 spellcheck={false}260 maxlength={32}261 placeholder="A74H-48J3-KI32-OEP4"262 aria-label="Pairing code"263 value={desktopCode()}264 ref={(el) => setTimeout(() => el.focus(), 80)}265 onInput={(e) => onDesktopCodeInput((e.target as HTMLInputElement).value)}266 onKeyDown={(e) => { if (e.key === "Enter") void handleDesktopConnect(); }}267 />268 <div class="peach-chirp-action">269 <button class="peach-btn peach-btn-ghost" onClick={() => props.onBack()}>Cancel</button>270 <button271 class="peach-btn peach-btn-primary peach-desktop-connect"272 disabled={!desktopCodeReady() || desktopBusy()}273 onClick={() => void handleDesktopConnect()}274 >275 Connect276 </button>277 </div>278 </div>279 </Show>280281 <Show when={desktopPhase() === "connecting"}>282 <div class="peach-desktop-searching">283 <div class="peach-chirp-headline">Connecting…</div>284 <div class="peach-chirp-sub">Reaching your phone over the local network.</div>285 </div>286 </Show>287288 <Show when={desktopPhase() === "sas"}>289 <div class="peach-desktop-confirm" style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:8px 18px 16px;text-align:center;">290 <div class="peach-chirp-headline">Confirm this code matches on your phone:</div>291 <div292 class="peach-desktop-sas"293 style="font-size:30px;font-weight:700;letter-spacing:0.32em;font-family:'IBM Plex Mono',monospace;color:var(--peach-accent);padding:6px 0 2px;"294 >295 {desktopSas()}296 </div>297 <div class="peach-chirp-sub" style="max-width:240px;">298 Only continue if this 6-digit code is identical on both screens. This is299 what proves you're talking to your own device.300 </div>301 <div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:240px;">302 <button class="peach-btn peach-btn-primary" disabled={desktopBusy()} onClick={() => void respondDesktopConfirm(true)}>303 Confirm304 </button>305 <button class="peach-btn peach-btn-ghost" disabled={desktopBusy()} onClick={() => void respondDesktopConfirm(false)}>306 Cancel307 </button>308 </div>309 </div>310 </Show>311312 <Show when={desktopPhase() === "paired"}>313 <div class="peach-desktop-paired">314 <div class="peach-chirp-headline">Paired!</div>315 <div class="peach-chirp-sub">316 {desktopDevice()?.deviceName ? `Connected to ${desktopDevice()!.deviceName}` : "Connected to your phone"}317 </div>318 <div class="peach-chirp-action">319 <button class="peach-btn peach-btn-primary" onClick={() => props.onBack()}>Done</button>320 </div>321 </div>322 </Show>323324 <Show when={desktopPhase() === "error"}>325 <div class="peach-desktop-error">326 <div class="peach-chirp-headline">{desktopErrorCopy(desktopError()).headline}</div>327 <div class="peach-chirp-sub">{desktopErrorCopy(desktopError()).sub}</div>328 <div class="peach-chirp-action">329 <button class="peach-btn peach-btn-ghost" onClick={() => props.onBack()}>Cancel</button>330 <button class="peach-btn peach-btn-primary" onClick={() => startDesktopFlow()}>Try again</button>331 </div>332 </div>333 </Show>334335 <div class="peach-sr-only" aria-live="polite">{desktopLiveText()}</div>336 </div>337 );338339 340 341 return desktopView();342};