// ChirpSync — the v3 Settings → Sync surface. // // Three modes the user can be in: // // 1. Locked behind Pro (free user) — soft upsell card, "Learn more" routes // to the License section. // 2. Idle — list of paired devices + "Pair a device" hero button. Stale // devices (>7 days since last sync) show a soft warning indicator; // vouched devices (received via device manifest, v2 brief §4.2) show // an "Added automatically" badge. // 3. Pairing — ChirpPairingFlow takes over the full surface and drives // the three-act bird animation in lockstep with the audio protocol. // // The pairing orchestrator lives in `popup/chirp-pair.ts`; this view is // the binding between that orchestrator, the background-message // CHIRP_* handlers, and the Settings navigation surrounding it. import { Component, Show, createMemo, createSignal, For, onMount, } from "solid-js"; import { sendMessage } from "../../shared/bridge"; import type { DeviceType, PairedDevice, PairingProgress, } from "../../shared/sync-types"; import { ChirpPairingFlow, DesktopBird, MobileBird } from "../components/ChirpPairingFlow"; import { ChirpPairingFailure, runChirpPairing, type ChirpPairingDeps, type PairingRole, } from "../chirp-pair"; import { getLocalSubnet } from "../../shared/local-network"; import { isNativePlatform } from "../../shared/native"; import { openCapacitorPairingServer } from "../chirp-pair-capacitor"; export interface ChirpSyncViewProps { isPro: boolean; onBack(): void; onLearnPro(): void; } const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; type Mode = "rest" | "pairing"; export const ChirpSyncView: Component = (props) => { const [devices, setDevices] = createSignal([]); const [mode, setMode] = createSignal("rest"); const [pairing, setPairing] = createSignal({ phase: "idle" }); const [error, setError] = createSignal(null); const [confirmUnpair, setConfirmUnpair] = createSignal(null); onMount(() => { void refresh(); }); async function refresh(): Promise { if (!props.isPro) return; const res = await sendMessage({ type: "CHIRP_LIST_PAIRED_DEVICES" }); const data = (res as { ok: boolean; data?: { devices: PairedDevice[] } }).data; setDevices(data?.devices ?? []); } async function handleStartPairing(): Promise { setError(null); setPairing({ phase: "idle" }); setMode("pairing"); const deps: ChirpPairingDeps = { onProgress: (p) => setPairing(p), fetchLocalIdentity: async () => { const res = await sendMessage({ type: "CHIRP_GET_LOCAL_IDENTITY" }); const r = res as { ok: boolean; data?: Parameters[0] & object; error?: string }; if (!r.ok || !r.data) throw new Error(r.error ?? "identity_unavailable"); // The handler returns a LocalIdentity; we trust the background's shape. return r.data as unknown as Awaited>; }, completePairing: async (paired) => { const res = await sendMessage({ type: "CHIRP_COMPLETE_PAIRING", payload: { paired }, }); const r = res as { ok: boolean; error?: string }; if (!r.ok) throw new Error(r.error ?? "complete_pairing_failed"); }, getLocalSubnet, openPairingTransport: async (endpoint) => { // Native WebSocket transport for the pairing-mode peer scan. The // chirp-pair orchestrator only uses send/receive/close, so a thin // adapter over the platform WebSocket suffices. return new Promise((resolve, reject) => { let ws: WebSocket; try { ws = new WebSocket(`ws://${endpoint.ip}:${endpoint.port}/`); } catch (err) { reject(err); return; } const inbox: Uint8Array[] = []; const waiters: Array<(b: Uint8Array) => void> = []; let closed = false; ws.binaryType = "arraybuffer"; const timer = setTimeout(() => { try { ws.close(); } catch { /* ignore */ } reject(new Error("pairing_transport_timeout")); }, 2_000); ws.onopen = () => { clearTimeout(timer); resolve({ async send(data: Uint8Array) { ws.send(data as Uint8Array); }, receive(): Promise { if (closed) return Promise.reject(new Error("closed")); const v = inbox.shift(); if (v) return Promise.resolve(v); return new Promise((res) => waiters.push(res)); }, async close() { try { ws.close(); } catch { /* ignore */ } }, }); }; ws.onmessage = (ev) => { if (!(ev.data instanceof ArrayBuffer)) return; const bytes = new Uint8Array(ev.data); const w = waiters.shift(); if (w) w(bytes); else inbox.push(bytes); }; ws.onerror = () => { clearTimeout(timer); reject(new Error("pairing_transport_error")); }; ws.onclose = () => { closed = true; while (waiters.length > 0) waiters.shift()!(new Uint8Array(0)); }; }); }, }; const role: PairingRole = isNativePlatform() ? "initiator" : "responder"; if (role === "initiator") deps.openPairingServer = openCapacitorPairingServer; try { await runChirpPairing(deps, role); await refresh(); } catch (err) { if (err instanceof ChirpPairingFailure) { // The orchestrator already pushed the terminal phase into // `pairing()`; the inline error copy will render. No further // action needed — the user can tap "Try again" or "Cancel". return; } setError((err as Error).message); setMode("rest"); } } async function handleUnpair(deviceId: string): Promise { await sendMessage({ type: "CHIRP_UNPAIR_DEVICE", payload: { deviceId } }); setConfirmUnpair(null); await refresh(); } function relativeTime(when: number | undefined): string { if (!when) return "Not synced yet"; const elapsed = Date.now() - when; if (elapsed < 60_000) return "Just now"; const minutes = Math.floor(elapsed / 60_000); if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days} day${days === 1 ? "" : "s"} ago`; return "Not seen recently"; } function isStale(d: PairedDevice): boolean { if (d.lastSyncedAt === undefined) return false; return Date.now() - d.lastSyncedAt > STALE_THRESHOLD_MS; } // --- Sub-views -------------------------------------------------- const proGateView = () => (

Sync

A little bird that carries your vault

Chirp Sync pairs your phone with your laptop using a quick inaudible chirp — then your vault stays in sync across devices on your local network. No QR codes, no relay servers, no cloud accounts.

Peach Pro · one-time purchase

); const restView = () => (
Sync
ready
{devices().length === 0 ? "Bring your phone close" : "Sync is ready"}
{devices().length === 0 ? "Peach pairs over a short, inaudible chirp — no codes, no servers." : "Pair another device, or manage the ones already connected below."}
0}>

Paired devices

    {(d) => (
  • {d.deviceName ?? "Unnamed device"} {relativeTime(d.lastSyncedAt)} Added automatically Not seen recently
} > )} ); const pairingView = () => ( setMode("rest")} onRetry={() => void handleStartPairing()} onDismiss={() => { setMode("rest"); void refresh(); }} /> ); const currentMode = createMemo(() => { if (!props.isPro) return "gated"; if (mode() === "pairing") return "pairing"; return "rest"; }); return ( {pairingView()} } > {proGateView()} ); }; const DeviceTypeIcon: Component<{ type: DeviceType }> = (props) => { return (
); };