// ChirpPairingFlow — the audio pairing screen. // // Implements the three-act bird animation specified in the brief §6.2: // Act 1 (Waiting) — desktop bird idles, soft pulsing chirp ring // Act 2 (Contact) — mobile bird hops in, sound-wave arcs in both // directions, head-tilt reaction // Act 3 (Paired) — sound waves resolve into a glowing connection dot, // both birds wing-flutter, copy switches to "Paired!" // // The acts are bound to the `PairingPhase` enum from sync-types; the // background coordinator drives the phase forward as the audio + Noise // rounds make progress. The component stays a pure renderer: it does not // touch the microphone or the WebSocket itself. import { Component, Show, createEffect, createSignal } from "solid-js"; import type { PairingProgress } from "../../shared/sync-types"; export interface ChirpPairingFlowProps { progress: PairingProgress; /** Called when the user taps Retry on a terminal-error state. */ onRetry: () => void; /** Called when the user taps Cancel/back on the idle/listening state. */ onCancel: () => void; /** Called once the paired-state dismissal animation completes. */ onDismiss: () => void; } const PAIRED_DISMISS_MS = 2500; export const ChirpPairingFlow: Component = (props) => { const [dismissArmed, setDismissArmed] = createSignal(false); createEffect(() => { if (props.progress.phase === "paired" && !dismissArmed()) { setDismissArmed(true); const t = setTimeout(() => props.onDismiss(), PAIRED_DISMISS_MS); // No need to clean up on unmount — onDismiss is a no-op if parent unmounted. void t; } }); return (
props.progress.phase === "paired" && props.onDismiss()}>

Update your browser's site settings to allow Peach to access the microphone, then try again.

); }; function canCancel(phase: PairingProgress["phase"]): boolean { return phase === "listening" || phase === "chirping" || phase === "handshake"; } function isTerminalError(phase: PairingProgress["phase"]): boolean { return ( phase === "timeout" || phase === "handshake_failed" || phase === "version_mismatch" || phase === "mic_denied" ); } const PairingCopy: Component<{ progress: PairingProgress }> = (props) => { return (

{headline(props.progress)}

{subtext(props.progress)}

); }; function headline(p: PairingProgress): string { switch (p.phase) { case "idle": case "listening": case "chirping": return "Hold your phone near your laptop"; case "handshake": return "Pairing…"; case "paired": return "Paired!"; case "timeout": return "Couldn’t find your phone"; case "mic_denied": return "Microphone access needed"; case "version_mismatch": return "Update Peach on both devices"; case "handshake_failed": return "Something went wrong"; } } function subtext(p: PairingProgress): string { switch (p.phase) { case "idle": case "listening": case "chirping": return "Peach will pair automatically when your phone is close"; case "handshake": return "Hold still — exchanging keys"; case "paired": return p.peerDeviceName ? `Connected to ${p.peerDeviceName}` : "Connected"; case "timeout": return "Make sure Peach is open on your phone and try again"; case "mic_denied": return "Peach uses your microphone to pair. No audio is recorded or stored."; case "version_mismatch": return "The other device is running a different protocol version"; case "handshake_failed": return p.errorMessage ?? "Please try again"; } } // --------------------------------------------------------------------------- // Inline SVG birds — kept deliberately schematic so the focus stays on the // motion. A future polish pass can swap these for the production // illustrations without changing the component shape. // --------------------------------------------------------------------------- const DesktopBird: Component<{ phase: PairingProgress["phase"] }> = (props) => { return ( ); }; const MobileBird: Component<{ phase: PairingProgress["phase"] }> = (props) => { return ( ); }; const SoundWaves: Component<{ phase: PairingProgress["phase"] }> = (props) => { return ( ); }; const ConnectionDot: Component<{ phase: PairingProgress["phase"] }> = (props) => { return (
); };