123456789101112131415161718import {19 Component,20 Show,21 createEffect,22 createMemo,23 createSignal,24 For,25 onCleanup,26 onMount,27} from "solid-js";28import { sendMessage } from "../../shared/bridge";29import type {30 DeviceType,31 PairedDevice,32 PairingProgress,33} from "../../shared/sync-types";34import { ChirpPairingFlow, DesktopBird, MobileBird } from "../components/ChirpPairingFlow";35import {36 ChirpPairingFailure,37 runChirpPairing,38 type ChirpPairingDeps,39 type PairingRole,40} from "../chirp-pair";41import { getLocalSubnet } from "../../shared/local-network";42import { isNativePlatform } from "../../shared/native";43import { openCapacitorPairingServer } from "../chirp-pair-capacitor";4445export interface ChirpSyncViewProps {46 isPro: boolean;47 onBack(): void;48 onLearnPro(): void;49 50515253 initialPair?: "chirp" | "desktop" | null;54}5556const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;5758type Mode = "rest" | "pairing";59606162type PairKind = "chirp" | "desktop" | null;6364656667type DesktopPhase = "enter" | "connecting" | "sas" | "paired" | "error";68type DesktopPairingError =69 | "invalid_code"70 | "no_host_found"71 | "handshake_failed"72 | "sas_mismatch"73 | "vault_locked";7475const DESKTOP_PAIRED_DISMISS_MS = 2_000;7677function desktopErrorCopy(error: DesktopPairingError | "unknown"): { headline: string; sub: string } {78 switch (error) {79 case "invalid_code":80 81 return {82 headline: "Code not found or expired",83 sub: "Check the code on your computer and try again. Codes rotate, so grab a fresh one if it's been a while.",84 };85 case "no_host_found":86 return {87 headline: "Couldn't find your computer",88 sub: "Make sure both devices are on the same Wi-Fi, then run “peach-sync pair” in a terminal to start pairing.",89 };90 case "sas_mismatch":91 return {92 headline: "Codes don't match",93 sub: "For your safety, pairing was stopped. Start over and compare the two codes carefully.",94 };95 case "vault_locked":96 return {97 headline: "Unlock Peach first",98 sub: "Your vault needs to be unlocked before you can pair a device.",99 };100 case "handshake_failed":101 default:102 return {103 headline: "Pairing failed",104 sub: "Something went wrong while connecting. Please try again.",105 };106 }107}108109export const ChirpSyncView: Component<ChirpSyncViewProps> = (props) => {110 const [devices, setDevices] = createSignal<PairedDevice[]>([]);111 112 113 const [mode, setMode] = createSignal<Mode>(props.initialPair ? "pairing" : "rest");114 const [pairing, setPairing] = createSignal<PairingProgress>({ phase: "idle" });115 const [error, setError] = createSignal<string | null>(null);116 const [confirmUnpair, setConfirmUnpair] = createSignal<string | null>(null);117 const [syncStatus, setSyncStatus] = createSignal<"idle" | "syncing" | "error">("idle");118 const [syncFlash, setSyncFlash] = createSignal<string | null>(null);119 120 121 122 const [sasCode, setSasCode] = createSignal<string | null>(null);123 const [sasResolve, setSasResolve] = createSignal<((ok: boolean) => void) | null>(null);124125 function resolveSAS(ok: boolean) {126 const r = sasResolve();127 setSasCode(null);128 setSasResolve(null);129 r?.(ok);130 }131132 133 134 135 const [pairKind, setPairKind] = createSignal<PairKind>(props.initialPair ?? null);136 const [desktopPhase, setDesktopPhase] = createSignal<DesktopPhase>("enter");137 const [desktopCode, setDesktopCode] = createSignal("");138 const [desktopSas, setDesktopSas] = createSignal<string | null>(null);139 const [desktopError, setDesktopError] = createSignal<DesktopPairingError | "unknown">("unknown");140 const [desktopDevice, setDesktopDevice] = createSignal<PairedDevice | undefined>(undefined);141 const [desktopBusy, setDesktopBusy] = createSignal(false);142143 const desktopCodeReady = () => /^\d{6}$/.test(desktopCode());144145 146 function pairViaChirp(): void {147 setError(null);148 setMode("pairing");149 startChirpFlow();150 }151152 function pairViaDesktop(): void {153 setError(null);154 setMode("pairing");155 startDesktopFlow();156 }157158 function startDesktopFlow(): void {159 setDesktopCode("");160 setDesktopSas(null);161 setDesktopError("unknown");162 setDesktopDevice(undefined);163 setDesktopBusy(false);164 setDesktopPhase("enter");165 setPairKind("desktop");166 }167168 function startChirpFlow(): void {169 setPairKind("chirp");170 void handleStartPairing();171 }172173 function onDesktopCodeInput(value: string): void {174 setDesktopCode(value.replace(/\D/g, "").slice(0, 6));175 }176177 async function handleDesktopConnect(): Promise<void> {178 if (!desktopCodeReady() || desktopBusy()) return;179 setDesktopBusy(true);180 setDesktopPhase("connecting");181 try {182 const res = (await sendMessage({183 type: "PAIR_WITH_DESKTOP",184 payload: { code: desktopCode() },185 })) as { ok: boolean; error?: string };186 187 188 if (!res.ok) {189 setDesktopError((res.error as DesktopPairingError) ?? "unknown");190 setDesktopPhase("error");191 }192 } catch {193 setDesktopError("unknown");194 setDesktopPhase("error");195 } finally {196 setDesktopBusy(false);197 }198 }199200 async function respondDesktopConfirm(confirmed: boolean): Promise<void> {201 if (desktopBusy()) return;202 setDesktopBusy(true);203 204 205 if (confirmed) setDesktopPhase("connecting");206 else { setDesktopError("sas_mismatch"); setDesktopPhase("error"); }207 try {208 await sendMessage({ type: "DESKTOP_PAIR_CONFIRM", payload: { confirmed } });209 } finally {210 setDesktopBusy(false);211 }212 }213214 215 216 createEffect(() => {217 if (desktopPhase() !== "paired") return;218 const t = setTimeout(() => props.onBack(), DESKTOP_PAIRED_DISMISS_MS);219 onCleanup(() => clearTimeout(t));220 });221222 async function handleSyncNow(): Promise<void> {223 setSyncStatus("syncing");224 setSyncFlash(null);225 const res = await sendMessage({226 type: "TRIGGER_SYNC",227 payload: { reason: "manual", force: true },228 }) as { ok: boolean; error?: string };229 230 231 setTimeout(async () => {232 const state = await sendMessage({ type: "GET_SYNC_STATE" }) as {233 ok: boolean;234 data?: { status: "idle" | "syncing" | "error"; entriesReceived?: number; error?: string };235 };236 const s = state.data;237 if (!s) return;238 setSyncStatus(s.status);239 if (s.status === "idle") {240 setSyncFlash(241 (s.entriesReceived ?? 0) > 0242 ? `Synced — ${s.entriesReceived} entries updated`243 : "Vault up to date",244 );245 setTimeout(() => setSyncFlash(null), 3000);246 await refresh();247 } else if (s.status === "error") {248 setSyncFlash(`Sync failed — ${s.error ?? "no mobile host reachable"}`);249 setTimeout(() => setSyncFlash(null), 4000);250 }251 }, 250);252 if (!res.ok) {253 setSyncStatus("error");254 setSyncFlash(`Sync failed — ${res.error ?? "unknown"}`);255 setTimeout(() => setSyncFlash(null), 4000);256 }257 }258259 onMount(() => {260 void refresh();261262 263 if (props.initialPair === "chirp") pairViaChirp();264 else if (props.initialPair === "desktop") pairViaDesktop();265266 267 268 269 if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {270 const handler = (msg: { type?: string; payload?: unknown }) => {271 if (pairKind() !== "desktop") return;272 if (msg?.type === "DESKTOP_PAIR_SAS") {273 const p = msg.payload as { sas?: string } | undefined;274 if (typeof p?.sas === "string") { setDesktopSas(p.sas); setDesktopPhase("sas"); }275 } else if (msg?.type === "DESKTOP_PAIR_RESULT") {276 const p = msg.payload as277 | { status: "complete"; device: PairedDevice }278 | { status: "error"; error: DesktopPairingError }279 | undefined;280 if (p?.status === "complete") { setDesktopDevice(p.device); setDesktopPhase("paired"); }281 else if (p?.status === "error") { setDesktopError(p.error); setDesktopPhase("error"); }282 }283 };284 chrome.runtime.onMessage.addListener(handler);285 onCleanup(() => chrome.runtime.onMessage.removeListener(handler));286 }287 });288289 async function refresh(): Promise<void> {290 if (!props.isPro) return;291 const res = await sendMessage({ type: "CHIRP_LIST_PAIRED_DEVICES" });292 const data = (res as { ok: boolean; data?: { devices: PairedDevice[] } }).data;293 setDevices(data?.devices ?? []);294 }295296 async function handleStartPairing(): Promise<void> {297 setError(null);298299 300 301 302 303 if (typeof chrome !== "undefined" && chrome.permissions?.request) {304 const granted = await chrome.permissions.request({305 permissions: ["audioCapture" as chrome.runtime.ManifestPermissions],306 });307 if (!granted) {308 setPairing({ phase: "mic_denied" });309 setMode("pairing");310 return;311 }312 }313314 setPairing({ phase: "idle" });315 setMode("pairing");316317 const deps: ChirpPairingDeps = {318 onProgress: (p) => setPairing(p),319 fetchLocalIdentity: async () => {320 const res = await sendMessage({ type: "CHIRP_GET_LOCAL_IDENTITY" });321 const r = res as { ok: boolean; data?: Parameters<ChirpPairingDeps["onProgress"]>[0] & object; error?: string };322 if (!r.ok || !r.data) throw new Error(r.error ?? "identity_unavailable");323 324 return r.data as unknown as Awaited<ReturnType<ChirpPairingDeps["fetchLocalIdentity"]>>;325 },326 completePairing: async (paired) => {327 const res = await sendMessage({328 type: "CHIRP_COMPLETE_PAIRING",329 payload: { paired },330 });331 const r = res as { ok: boolean; error?: string };332 if (!r.ok) throw new Error(r.error ?? "complete_pairing_failed");333 },334 confirmSAS: (sas) =>335 new Promise<boolean>((resolve) => {336 setSasResolve(() => resolve);337 setSasCode(sas);338 }),339 getLocalSubnet,340 openPairingTransport: async (endpoint) => {341 342 343 344 return new Promise((resolve, reject) => {345 let ws: WebSocket;346 try {347 ws = new WebSocket(`ws://${endpoint.ip}:${endpoint.port}/`);348 } catch (err) {349 reject(err);350 return;351 }352 const inbox: Uint8Array[] = [];353 const waiters: Array<(b: Uint8Array) => void> = [];354 let closed = false;355 ws.binaryType = "arraybuffer";356 const timer = setTimeout(() => {357 try { ws.close(); } catch { }358 reject(new Error("pairing_transport_timeout"));359 }, 2_000);360 ws.onopen = () => {361 clearTimeout(timer);362 resolve({363 async send(data: Uint8Array) {364 ws.send(data as Uint8Array<ArrayBuffer>);365 },366 receive(): Promise<Uint8Array> {367 if (closed) return Promise.reject(new Error("closed"));368 const v = inbox.shift();369 if (v) return Promise.resolve(v);370 return new Promise<Uint8Array>((res) => waiters.push(res));371 },372 async close() {373 try { ws.close(); } catch { }374 },375 });376 };377 ws.onmessage = (ev) => {378 if (!(ev.data instanceof ArrayBuffer)) return;379 const bytes = new Uint8Array(ev.data);380 const w = waiters.shift();381 if (w) w(bytes); else inbox.push(bytes);382 };383 ws.onerror = () => {384 clearTimeout(timer);385 reject(new Error("pairing_transport_error"));386 };387 ws.onclose = () => {388 closed = true;389 while (waiters.length > 0) waiters.shift()!(new Uint8Array(0));390 };391 });392 },393 };394395 const role: PairingRole = isNativePlatform() ? "initiator" : "responder";396 if (role === "initiator") deps.openPairingServer = openCapacitorPairingServer;397398 try {399 await runChirpPairing(deps, role);400 await refresh();401 } catch (err) {402 if (err instanceof ChirpPairingFailure) {403 404 405 406 return;407 }408 setError((err as Error).message);409 setMode("rest");410 }411 }412413 async function handleUnpair(deviceId: string): Promise<void> {414 await sendMessage({ type: "CHIRP_UNPAIR_DEVICE", payload: { deviceId } });415 setConfirmUnpair(null);416 await refresh();417 }418419 function relativeTime(when: number | undefined): string {420 if (!when) return "Not synced yet";421 const elapsed = Date.now() - when;422 if (elapsed < 60_000) return "Just now";423 const minutes = Math.floor(elapsed / 60_000);424 if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;425 const hours = Math.floor(minutes / 60);426 if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;427 const days = Math.floor(hours / 24);428 if (days < 7) return `${days} day${days === 1 ? "" : "s"} ago`;429 return "Not seen recently";430 }431432 function isStale(d: PairedDevice): boolean {433 if (d.lastSyncedAt === undefined) return false;434 return Date.now() - d.lastSyncedAt > STALE_THRESHOLD_MS;435 }436437 438439 const proGateView = () => (440 <div class="peach-chirp-settings peach-chirp-settings--gated">441 <header class="peach-chirp-settings-header">442 <button class="peach-icon-btn" aria-label="Back" onClick={() => props.onBack()}>443 <svg width="18" height="18" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">444 <path d="M11 4 L5 9 L11 14" />445 </svg>446 </button>447 <h1 class="peach-chirp-settings-title">Sync</h1>448 </header>449 <div class="peach-chirp-pro-card">450 <div class="peach-chirp-pro-stage" aria-hidden="true">451 <svg class="peach-chirp-pro-chirp" viewBox="0 0 200 120" fill="none" stroke="currentColor"452 stroke-linecap="round" stroke-linejoin="round">453 <path d="M 124 36 Q 150 30 168 44" stroke-width="1.6" opacity="0.9" />454 <path d="M 128 52 Q 162 50 184 64" stroke-width="1.3" opacity="0.55" />455 <path d="M 124 68 Q 158 76 178 92" stroke-width="1.1" opacity="0.32" />456 </svg>457 <div class="peach-chirp-pro-bird-slot">458 <MobileBird pose="idle" />459 </div>460 </div>461 <h2 class="peach-chirp-pro-headline">A little bird that carries your vault</h2>462 <p class="peach-chirp-pro-body">463 Pairs over an inaudible chirp. Syncs locally across your devices.464 No codes, no servers, no cloud.465 </p>466 <button class="peach-btn peach-btn-primary peach-chirp-pro-cta" onClick={() => props.onLearnPro()}>467 Unlock with Peach Pro468 </button>469 <p class="peach-chirp-pro-foot">Peach Pro · one-time purchase</p>470 </div>471 </div>472 );473474 const restView = () => (475 <div class="peach-chirp-pair" data-phase="idle" role="region" aria-label="Sync">476 <header class="peach-chirp-head">477 <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>478 <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">479 <path d="M10 3 L5 8 L10 13" />480 </svg>481 </button>482 <div class="peach-chirp-title">Sync</div>483 <div class="peach-chirp-head-spacer" />484 </header>485486 <div class="peach-chirp-chip-row">487 <div class="peach-chirp-chip" data-phase="idle">488 <span class="peach-chirp-chip-dot" />489 <span class="peach-chirp-chip-text">ready</span>490 </div>491 </div>492493 <Show when={error()}>494 <div class="peach-chirp-settings-error" role="alert">{error()}</div>495 </Show>496497 <div class="peach-chirp-stage act-idle" aria-hidden="true">498 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">499 <DesktopBird pose="idle" />500 </div>501 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">502 <MobileBird pose="hidden" />503 </div>504 </div>505506 <div class="peach-chirp-copy-host">507 <div class="peach-chirp-copy peach-chirp-copy-rest">508 <div class="peach-chirp-headline">509 {devices().length === 0 ? "Pair a device" : "Sync is ready"}510 </div>511 <div class="peach-chirp-sub">512 {devices().length === 0513 ? "Sync over your local network. No cloud, no account, no servers."514 : "Pair another device, or manage the ones already connected below."}515 </div>516 </div>517 </div>518519 <Show when={devices().length > 0}>520 <section class="peach-chirp-paired-section">521 <h2 class="peach-chirp-paired-heading">Paired devices</h2>522 <ul class="peach-chirp-device-list">523 <For each={devices()}>524 {(d) => (525 <li class="peach-chirp-device-row" data-stale={isStale(d) ? "true" : "false"}>526 <DeviceTypeIcon type={d.deviceType ?? "extension"} />527 <div class="peach-chirp-device-info">528 <span class="peach-chirp-device-name">{d.deviceName ?? "Unnamed device"}</span>529 <span class="peach-chirp-device-meta">530 {relativeTime(d.lastSyncedAt)}531 <Show when={d.trustSource === "vouched"}>532 <span class="peach-chirp-device-badge">Added automatically</span>533 </Show>534 <Show when={isStale(d)}>535 <span class="peach-chirp-device-stale">Not seen recently</span>536 </Show>537 </span>538 </div>539 <Show540 when={confirmUnpair() !== d.deviceId}541 fallback={542 <div class="peach-chirp-unpair-confirm">543 <button class="peach-btn peach-btn-ghost peach-btn-sm" onClick={() => setConfirmUnpair(null)}>544 Cancel545 </button>546 <button class="peach-btn peach-btn-destructive peach-btn-sm" onClick={() => void handleUnpair(d.deviceId)}>547 Remove548 </button>549 </div>550 }551 >552 <button553 class="peach-icon-btn peach-chirp-device-remove"554 aria-label={`Unpair ${d.deviceName}`}555 onClick={() => setConfirmUnpair(d.deviceId)}556 >557 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">558 <path d="M3 3 L11 11 M11 3 L3 11" />559 </svg>560 </button>561 </Show>562 </li>563 )}564 </For>565 </ul>566 </section>567 </Show>568569 <Show when={devices().length > 0}>570 <div class="peach-chirp-sync-now-row">571 <button572 class="peach-btn peach-btn-ghost peach-btn-sm peach-chirp-sync-now"573 disabled={syncStatus() === "syncing"}574 onClick={() => void handleSyncNow()}575 >576 {syncStatus() === "syncing" ? "Syncing…" : "Sync now"}577 </button>578 <Show when={syncFlash()}>579 <span class="peach-chirp-sync-flash" aria-live="polite">580 {syncFlash()}581 </span>582 </Show>583 </div>584 </Show>585586 <div class="peach-pair-method-list peach-pair-method-list--rest">587 <button class="peach-pair-method" onClick={() => pairViaChirp()}>588 <span class="peach-pair-method-icon" aria-hidden="true">589 <svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">590 <path d="M3 8 L6 8 L10.5 4 L10.5 16 L6 12 L3 12 Z" />591 <path d="M14 7.5 Q15.6 10 14 12.5" />592 <path d="M16.4 5.5 Q19 10 16.4 14.5" opacity="0.5" />593 </svg>594 </span>595 <span class="peach-pair-method-text">596 <span class="peach-pair-method-title">Same room</span>597 <span class="peach-pair-method-sub">Pair a nearby phone or browser by chirp</span>598 </span>599 </button>600 <button class="peach-pair-method" onClick={() => pairViaDesktop()}>601 <span class="peach-pair-method-icon" aria-hidden="true">602 <svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">603 <rect x="2" y="4" width="16" height="10" rx="1.4" />604 <path d="M7 17 L13 17" />605 <path d="M9 14 L9 17 M11 14 L11 17" />606 </svg>607 </span>608 <span class="peach-pair-method-text">609 <span class="peach-pair-method-title">Enter a code</span>610 <span class="peach-pair-method-sub">Type the code from your desktop sync helper</span>611 </span>612 </button>613 </div>614615 </div>616 );617618 const backArrow = (619 <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">620 <path d="M10 3 L5 8 L10 13" />621 </svg>622 );623624 625 626 const desktopBirdStage = (active: boolean) => (627 <div class={`peach-chirp-stage ${active ? "act-handshake" : "act-listening"}`} aria-hidden="true">628 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-desktop">629 <DesktopBird pose="chirping" />630 </div>631 <div class="peach-chirp-bird-slot peach-chirp-bird-slot-mobile">632 <MobileBird pose="attentive" />633 </div>634 </div>635 );636637 function desktopLiveText(): string {638 switch (desktopPhase()) {639 case "enter": return "Enter the pairing code from your computer";640 case "connecting": return "Searching for the helper";641 case "sas": return "Confirm the code matches on your computer";642 case "paired": return "Paired";643 case "error": return desktopErrorCopy(desktopError()).headline;644 }645 }646647 648 const desktopView = () => (649 <div class="peach-chirp-pair peach-desktop-pair" data-phase={desktopPhase()} role="dialog" aria-label="Pair a desktop helper">650 <header class="peach-chirp-head">651 <button class="peach-chirp-back" aria-label="Back" onClick={() => props.onBack()}>{backArrow}</button>652 <div class="peach-chirp-title">Enter a code</div>653 <div class="peach-chirp-head-spacer" />654 </header>655656 {desktopBirdStage(desktopPhase() !== "enter")}657658 <Show when={desktopPhase() === "enter"}>659 <div class="peach-desktop-enter">660 <div class="peach-chirp-headline">Enter the code from your computer</div>661 <div class="peach-chirp-sub">662 Open the Peach helper on your computer and type the 6-digit code it shows.663 </div>664 <label class="peach-label" for="desktop-pair-code">pairing code</label>665 <input666 id="desktop-pair-code"667 class="peach-underline-input peach-unlock-input peach-code-input"668 type="text"669 inputmode="numeric"670 autocomplete="one-time-code"671 maxlength={6}672 placeholder="000000"673 aria-label="6-digit pairing code"674 value={desktopCode()}675 ref={(el) => setTimeout(() => el.focus(), 80)}676 onInput={(e) => onDesktopCodeInput((e.target as HTMLInputElement).value)}677 onKeyDown={(e) => { if (e.key === "Enter") void handleDesktopConnect(); }}678 />679 <div class="peach-chirp-action">680 <button class="peach-btn peach-btn-ghost" onClick={() => props.onBack()}>Cancel</button>681 <button682 class="peach-btn peach-btn-primary peach-desktop-connect"683 disabled={!desktopCodeReady() || desktopBusy()}684 onClick={() => void handleDesktopConnect()}685 >686 Connect687 </button>688 </div>689 </div>690 </Show>691692 <Show when={desktopPhase() === "connecting"}>693 <div class="peach-desktop-searching">694 <div class="peach-chirp-headline">Searching for helper…</div>695 <div class="peach-chirp-sub">Connecting to the Peach helper on your computer.</div>696 </div>697 </Show>698699 <Show when={desktopPhase() === "sas"}>700 <div class="peach-desktop-confirm" style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:8px 18px 16px;text-align:center;">701 <div class="peach-chirp-headline">Confirm this code matches on your computer:</div>702 <div703 class="peach-desktop-sas"704 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;"705 >706 {desktopSas()}707 </div>708 <div class="peach-chirp-sub" style="max-width:240px;">709 Only continue if this 6-digit code is identical on both screens.710 </div>711 <div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:240px;">712 <button class="peach-btn peach-btn-primary" disabled={desktopBusy()} onClick={() => void respondDesktopConfirm(true)}>713 Confirm714 </button>715 <button class="peach-btn peach-btn-ghost" disabled={desktopBusy()} onClick={() => void respondDesktopConfirm(false)}>716 Cancel717 </button>718 </div>719 </div>720 </Show>721722 <Show when={desktopPhase() === "paired"}>723 <div class="peach-desktop-paired">724 <div class="peach-chirp-headline">Paired!</div>725 <div class="peach-chirp-sub">726 {desktopDevice()?.deviceName ? `Connected to ${desktopDevice()!.deviceName}` : "Connected to your computer"}727 </div>728 <div class="peach-chirp-action">729 <button class="peach-btn peach-btn-primary" onClick={() => props.onBack()}>Done</button>730 </div>731 </div>732 </Show>733734 <Show when={desktopPhase() === "error"}>735 <div class="peach-desktop-error">736 <div class="peach-chirp-headline">{desktopErrorCopy(desktopError()).headline}</div>737 <div class="peach-chirp-sub">{desktopErrorCopy(desktopError()).sub}</div>738 <div class="peach-chirp-action">739 <button class="peach-btn peach-btn-ghost" onClick={() => props.onBack()}>Cancel</button>740 <button class="peach-btn peach-btn-primary" onClick={() => startDesktopFlow()}>Try again</button>741 </div>742 </div>743 </Show>744745 <div class="peach-sr-only" aria-live="polite">{desktopLiveText()}</div>746 </div>747 );748749 const chirpPairingView = () => (750 <Show751 when={sasCode()}752 fallback={753 <ChirpPairingFlow754 progress={pairing()}755 onCancel={() => props.onBack()}756 onRetry={() => void handleStartPairing()}757 onDismiss={() => props.onBack()}758 />759 }760 >761 {762}763 <div764 class="chirp-sas-verify"765 style="display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 18px;text-align:center;"766 >767 <div style="font-size:14px;font-weight:600;color:var(--peach-text);">Verify it's really your device</div>768 <div style="font-size:11.5px;line-height:1.45;color:var(--peach-text-muted);max-width:240px;">769 Confirm this code matches the one shown on your other device before pairing.770 </div>771 <div 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;">772 {sasCode()}773 </div>774 <div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:240px;">775 <button776 class="peach-btn peach-btn--accent peach-btn--md"777 type="button"778 onClick={() => resolveSAS(true)}779 >780 codes match — pair781 </button>782 <button783 class="peach-btn peach-btn--ghost peach-btn--md"784 type="button"785 onClick={() => resolveSAS(false)}786 >787 they don't match788 </button>789 </div>790 </div>791 </Show>792 );793794 795 796 const pairingView = () => (797 <Show when={pairKind() === "desktop"} fallback={chirpPairingView()}>798 {desktopView()}799 </Show>800 );801802 const currentMode = createMemo(() => {803 if (!props.isPro) return "gated";804 if (mode() === "pairing") return "pairing";805 return "rest";806 });807808 return (809 <Show810 when={currentMode() === "gated"}811 fallback={812 <Show when={currentMode() === "pairing"} fallback={restView()}>813 {pairingView()}814 </Show>815 }816 >817 {proGateView()}818 </Show>819 );820};821822const DeviceTypeIcon: Component<{ type: DeviceType }> = (props) => {823 return (824 <div class="peach-chirp-device-icon" data-type={props.type}>825 <Show when={props.type === "android" || props.type === "ios"}>826 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">827 <rect x="6" y="2" width="8" height="16" rx="1.5" />828 <circle cx="10" cy="15.4" r="0.6" fill="currentColor" />829 </svg>830 </Show>831 <Show when={props.type === "extension" || props.type === "desktop"}>832 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round">833 <rect x="2" y="4" width="16" height="10" rx="1.4" />834 <path d="M7 17 L13 17" />835 <path d="M9 14 L9 17 M11 14 L11 17" />836 </svg>837 </Show>838 </div>839 );840};