import QRCode from 'qrcode'; import { Component, createSignal, For, onCleanup, onMount, Show } from 'solid-js'; import { sendMessage } from '../App'; import type { PairedDevice } from '../../shared/sync-types'; import { getHasPaired, setHasPaired } from '../../background/sync-storage'; import { SyncInfoSheet } from '../components/SyncInfoSheet'; // ── Shared ────────────────────────────────────────────────────────────────── function formatRelativeTime(ts: number): string { const diff = Date.now() - ts; const mins = Math.floor(diff / 60_000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins} ${mins === 1 ? 'minute' : 'minutes'} ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs} ${hrs === 1 ? 'hour' : 'hours'} ago`; const days = Math.floor(hrs / 24); if (days < 7) return `${days} ${days === 1 ? 'day' : 'days'} ago`; return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } // ── Sync Hub — States A / B / C ────────────────────────────────────────────── interface SyncPairingProps { isPro: boolean; onAddDevice: () => void; onLearnPro: () => void; onBack: () => void; } export const SyncPairingView: Component = (props) => { const [pairedDevices, setPairedDevices] = createSignal([]); const [hasPaired, setHasPairedSignal] = createSignal(false); const [syncStatus, setSyncStatus] = createSignal<'up_to_date' | 'unreachable' | 'syncing' | 'never'>('never'); const [lastSyncAt, setLastSyncAt] = createSignal(null); const [syncing, setSyncing] = createSignal(false); const [showAdvanced, setShowAdvanced] = createSignal(false); const [resetting, setResetting] = createSignal(false); const [showInfoSheet, setShowInfoSheet] = createSignal(false); onMount(async () => { const [paired, devices, syncInfo] = await Promise.all([ getHasPaired(), sendMessage({ type: 'GET_PAIRED_DEVICES' }), sendMessage({ type: 'GET_SYNC_STATUS' }), ]); setHasPairedSignal(paired); if (!paired && !props.isPro) setShowInfoSheet(true); const d = devices as { ok: boolean; data?: { devices?: PairedDevice[] } }; if (d?.ok) setPairedDevices(d.data?.devices ?? []); const s = syncInfo as { ok: boolean; data?: { status?: 'up_to_date' | 'unreachable' | 'never'; lastSyncAt?: number | null } }; if (s?.ok) { setSyncStatus(s.data?.status ?? 'never'); setLastSyncAt(s.data?.lastSyncAt ?? null); } }); async function handleSyncNow() { setSyncing(true); const result = await sendMessage({ type: 'TRIGGER_SYNC' }); setSyncing(false); const r = result as { ok: boolean; syncedCount?: number }; if (r?.ok && (r.syncedCount ?? 0) > 0) { setLastSyncAt(Date.now()); setSyncStatus('up_to_date'); const devices = await sendMessage({ type: 'GET_PAIRED_DEVICES' }); const d = devices as { ok: boolean; devices?: PairedDevice[] }; if (d?.ok) setPairedDevices(d.devices ?? []); } } async function handleResetIdentity() { if (!confirm('This will unpair all devices. Are you sure?')) return; setResetting(true); await sendMessage({ type: 'RESET_SYNC_IDENTITY' }); setResetting(false); setPairedDevices([]); setHasPairedSignal(false); } return (
setShowInfoSheet(false)} onLearnPro={() => { setShowInfoSheet(false); props.onLearnPro(); }} />
SYNC
{/* ── State A: Never paired ── */}

sync your vault.

Peach can transfer your vault to another device instantly — just scan a code.

Continuous sync across multiple devices requires Peach Pro.

{/* ── States B + C: Has paired ── */} {/* Sync status — Pro only */}
Not on a compatible network
} >
Last synced {formatRelativeTime(lastSyncAt()!)}
{/* Paired devices */} 0}>
{(device) => (
{device.displayName} {device.lastSyncAt ? formatRelativeTime(device.lastSyncAt) : 'Paired — never synced'}
)}
{/* Action buttons */}
{/* State B: upgrade prompt */}

Want automatic sync? Upgrade to Peach Pro to keep all your devices up to date in the background.

{/* State C: network note */}

Sync works on your home and trusted networks. Some public or corporate networks block device connections.

{/* ── Advanced (all states, collapsed) ── */}

Reset sync identity

Generates a new keypair and unpairs every device.

); }; // ── Add Device — QR pairing flow ───────────────────────────────────────────── interface AddDeviceProps { onClose: () => void; onPaired: (device: PairedDevice) => void; } function Spinner() { return ; } export function AddDeviceView(props: AddDeviceProps) { 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); void setHasPaired(); 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("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 — peer can complete after popup closes. }); 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()}

); }