import { createSignal, onCleanup, onMount, Show } from "solid-js"; import { Capacitor } from "@capacitor/core"; import { sendMessage } from "../App"; import type { PairedDevice } from "../../shared/sync-types"; // Both desktop and Android can now host or enter a 6-digit code session. // Android's host path uses a Kotlin TCP server + UDP beacon plus a JS // Noise NK responder (`capacitor-pairing-server.ts`). const SHOW_MODE_AVAILABLE = true; interface SyncPairingProps { onClose: () => void; onPaired: (device: PairedDevice) => void; } type Mode = "show" | "enter"; function Spinner() { return ; } function formatCode(code: string): string { if (code.length <= 3) return code; return `${code.slice(0, 3)} ${code.slice(3)}`; } export function SyncPairingView(props: SyncPairingProps) { const [mode, setMode] = createSignal(SHOW_MODE_AVAILABLE ? "show" : "enter"); const [code, setCode] = createSignal(null); const [expiresAt, setExpiresAt] = createSignal(null); const [showError, setShowError] = createSignal(null); const [enteredCode, setEnteredCode] = createSignal(""); const [connecting, setConnecting] = createSignal(false); const [enterError, setEnterError] = createSignal(null); const [pairedDevice, setPairedDevice] = createSignal(null); let expiryTimer: ReturnType | null = null; function clearExpiryTimer() { if (expiryTimer !== null) { clearTimeout(expiryTimer); expiryTimer = null; } } function handleRuntimeMessage(msg: unknown) { const m = msg as { type?: string; payload?: { deviceName?: string } }; if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) { // Background completed an auto-accept while we were on the show screen. const device: PairedDevice = { deviceId: m.payload.deviceName, displayName: m.payload.deviceName, x25519PublicKey: "", pairedAt: Date.now(), }; clearExpiryTimer(); setCode(null); setExpiresAt(null); setPairedDevice(device); props.onPaired(device); } } async function startShow() { setShowError(null); setCode(null); setExpiresAt(null); clearExpiryTimer(); const res = await sendMessage({ type: "START_PAIRING_SERVER" }); const response = res as { ok: boolean; error?: string; data?: { token: string; expiresAt: number }; }; if (!response.ok || !response.data) { setShowError( response.error === "pro_required" ? "Sync requires Peach Pro." : "Couldn't start the pairing server.", ); return; } setCode(response.data.token); setExpiresAt(response.data.expiresAt); const remaining = Math.max(0, response.data.expiresAt - Date.now()); expiryTimer = setTimeout(() => { setCode(null); setExpiresAt(null); setShowError("Code expired — tap to refresh."); }, remaining); } async function stopShow() { clearExpiryTimer(); setCode(null); setExpiresAt(null); await sendMessage({ type: "STOP_PAIRING_SERVER" }); } async function refreshCode() { await stopShow(); await startShow(); } async function switchTo(target: Mode) { if (mode() === target) return; if (target === "enter") { await stopShow(); setEnterError(null); setEnteredCode(""); setMode("enter"); } else { setEnterError(null); setMode("show"); await startShow(); } } function onEnterInput(e: InputEvent) { const target = e.currentTarget as HTMLInputElement; const raw = target.value.replace(/[^0-9]/g, "").slice(0, 6); setEnteredCode(formatCode(raw)); if (enterError()) setEnterError(null); } function digitsEntered(): string { return enteredCode().replace(/\D/g, ""); } async function connect() { const token = digitsEntered(); if (token.length !== 6) return; setConnecting(true); setEnterError(null); const res = await sendMessage({ type: "CONNECT_WITH_CODE", payload: { token }, }); setConnecting(false); const response = res as { ok: boolean; error?: string; data?: { deviceName?: string } }; if (!response.ok) { const msg = response.error === "no peer responded to pairing code" || (response.error ?? "").includes("resolve failed") ? "Couldn't find that device. Make sure both are on the same WiFi." : response.error === "pair_rejected" ? "The other device declined." : response.error === "invalid_code" ? "Enter all six digits." : "Couldn't connect. Try again."; setEnterError(msg); return; } const name = response.data?.deviceName ?? "Peach"; const device: PairedDevice = { deviceId: name, displayName: name, x25519PublicKey: "", pairedAt: Date.now(), }; setPairedDevice(device); props.onPaired(device); } onMount(() => { if (!Capacitor.isNativePlatform()) { chrome.runtime.onMessage.addListener(handleRuntimeMessage); } void startShow(); }); onCleanup(() => { if (!Capacitor.isNativePlatform()) { chrome.runtime.onMessage.removeListener(handleRuntimeMessage); } clearExpiryTimer(); // Pairing server keeps running in the background per spec — the user // showed the code, so a peer can still complete pairing after the popup // closes. The background clears it on success or vault-lock. }); return ( { // Closing the popup is fine; if user explicitly hits back, also // tear down the active pairing server so they don't leave a token // active without realising. void sendMessage({ type: "STOP_PAIRING_SERVER" }); props.onClose(); }} aria-label="Close" > add a device {(device) => ( Paired! {device().displayName} Done )} Add a device void refreshCode()} > refresh }> }> {(c) => ( {formatCode(c())} )} Open Peach on your other device, tap Add a device, and enter this code. Both devices must be on the same WiFi. {showError()} void switchTo("enter")} > I have a code to enter → Add a device void connect()} disabled={digitsEntered().length !== 6 || connecting()} > {connecting() ? "Connecting…" : "Connect →"} Both devices must be on the same WiFi. {enterError()} void switchTo("show")} disabled={connecting()} > ← Show my code instead ); }
Paired!
{device().displayName}
Add a device
Open Peach on your other device, tap Add a device, and enter this code.
Both devices must be on the same WiFi.
{showError()}
{enterError()}