import QRCode from 'qrcode'; import { createSignal, onCleanup, onMount, Show } from "solid-js"; import { sendMessage } from "../App"; import type { PairedDevice } from "../../shared/sync-types"; interface SyncPairingProps { onClose: () => void; onPaired: (device: PairedDevice) => void; } function Spinner() { return ; } export function SyncPairingView(props: SyncPairingProps) { const [qrDataUrl, setQrDataUrl] = createSignal(null); const [timeLeft, setTimeLeft] = createSignal(0); const [expiresAt, setExpiresAt] = createSignal(null); const [showError, setShowError] = createSignal(null); const [pairedDevice, setPairedDevice] = createSignal(null); let expiryTimer: ReturnType | null = null; let countdownInterval: ReturnType | null = null; function clearExpiryTimer() { if (expiryTimer !== null) { clearTimeout(expiryTimer); expiryTimer = null; } if (countdownInterval !== null) { clearInterval(countdownInterval); countdownInterval = null; } } function handleRuntimeMessage(msg: unknown) { const m = msg as { type?: string; payload?: { deviceName?: string } }; if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) { const device: PairedDevice = { deviceId: m.payload.deviceName, displayName: m.payload.deviceName, x25519PublicKey: "", pairedAt: Date.now(), }; clearExpiryTimer(); setQrDataUrl(null); setExpiresAt(null); setTimeLeft(0); setPairedDevice(device); props.onPaired(device); } } async function startShow() { setShowError(null); setQrDataUrl(null); setExpiresAt(null); setTimeLeft(0); 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; } const { token, expiresAt: expAt } = response.data; const url = await QRCode.toDataURL('peach-pair:' + token, { width: 240, margin: 2, color: { dark: '#E8E0D0', light: '#00000000', }, }); setQrDataUrl(url); setExpiresAt(expAt); const remaining = Math.max(0, expAt - Date.now()); setTimeLeft(Math.floor(remaining / 1000)); countdownInterval = setInterval(() => { const secs = Math.max(0, Math.floor((expAt - Date.now()) / 1000)); setTimeLeft(secs); if (secs === 0 && countdownInterval !== null) { clearInterval(countdownInterval); countdownInterval = null; } }, 1000); expiryTimer = setTimeout(() => { setQrDataUrl(null); setExpiresAt(null); setTimeLeft(0); setShowError("Code expired — tap to refresh."); }, remaining); } async function stopShow() { clearExpiryTimer(); setQrDataUrl(null); setExpiresAt(null); setTimeLeft(0); await sendMessage({ type: "STOP_PAIRING_SERVER" }); } async function generateCode() { await stopShow(); await startShow(); } onMount(() => { chrome.runtime.onMessage.addListener(handleRuntimeMessage); void startShow(); }); onCleanup(() => { 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. }); // expiresAt is stored for completeness; clearExpiryTimer is the primary // expiry mechanism. void expiresAt; return (
add a device
{(device) => (

Paired!

{device().displayName}

)}

Add a device

void generateCode()} > refresh }>
}> {(url) => (
Pairing QR

Open Peach on your phone → Add a device → Scan this code.

0}>

{Math.floor(timeLeft() / 60)}:{String(timeLeft() % 60).padStart(2, '0')}

)}

Both devices must be on the same WiFi.

{showError()}

); }