1import { createSignal, For, onCleanup, onMount, Show } from "solid-js";2import { sendMessage } from "../App";3import type { DiscoveredPeer, PairedDevice, PairingStep } from "../../shared/sync-types";45interface SyncPairingProps {6 onClose: () => void;7 onPaired: (device: PairedDevice) => void;8}910function Spinner() {11 return <span class="peach-pairing-spinner" />;12}1314export function SyncPairingView(props: SyncPairingProps) {15 const [step, setStep] = createSignal<PairingStep>({ stage: "idle" });1617 const [discoveredPeers, setDiscoveredPeers] = createSignal<DiscoveredPeer[]>([]);18 const [discoveryActive, setDiscoveryActive] = createSignal(false);19 const [discoveryError, setDiscoveryError] = createSignal<string | null>(null);20 const [pairingPeerId, setPairingPeerId] = createSignal<string | null>(null);2122 onMount(() => {23 void startDiscovery();24 });2526 onCleanup(() => {27 if (discoveryActive()) void sendMessage({ type: "STOP_PEER_DISCOVERY" });28 });2930 function handleRuntimeMessage(msg: unknown) {31 const m = msg as { type?: string; peer?: DiscoveredPeer; deviceId?: string };32 if (m?.type === "SYNC_PEER_DISCOVERED" && m.peer) {33 const peer = m.peer;34 setDiscoveredPeers((prev) => {35 if (prev.some((p) => p.deviceId === peer.deviceId)) return prev;36 return [...prev, peer];37 });38 }39 if (m?.type === "SYNC_PEER_LOST" && m.deviceId) {40 const id = m.deviceId;41 setDiscoveredPeers((prev) => prev.filter((p) => p.deviceId !== id));42 }43 }4445 async function startDiscovery() {46 chrome.runtime.onMessage.addListener(handleRuntimeMessage);47 onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));4849 const res = await sendMessage({ type: "START_PEER_DISCOVERY" });50 const response = res as { ok: boolean; error?: string };51 if (response.ok) {52 setDiscoveryActive(true);53 setDiscoveryError(null);54 } else {55 setDiscoveryError(response.error ?? "discovery_failed");56 }57 }5859 async function handlePairWithPeer(peer: DiscoveredPeer) {60 setPairingPeerId(peer.deviceId);61 setStep({ stage: "connecting", peerName: peer.deviceName });62 const res = await sendMessage({63 type: "INITIATE_PAIR_WITH_PEER",64 payload: { peer },65 });66 const response = res as { ok: boolean; error?: string };67 setPairingPeerId(null);68 if (!response.ok) {69 setStep({ stage: "error", message: response.error ?? "pair_failed" });70 return;71 }72 const paired: PairedDevice = {73 deviceId: peer.deviceId,74 displayName: peer.deviceName,75 x25519PublicKey: peer.x25519PublicKey,76 pairedAt: Date.now(),77 };78 setStep({ stage: "done", device: paired });79 props.onPaired(paired);80 }8182 return (83 <div class="peach-overlay-view ov-enter-right">84 <div class="peach-overlay-header">85 <button86 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"87 onClick={props.onClose}88 aria-label="Close"89 >90 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">91 <path d="M6 1L1 6L6 11" />92 </svg>93 </button>94 <span class="peach-overlay-title">add a device</span>95 <div class="peach-overlay-spacer" />96 </div>9798 <div class="peach-pairing-body">99 <Show when={step().stage === "idle"}>100 <div class="peach-pairing-content peach-pairing-discover">101 <p class="peach-pairing-headline">Add a device</p>102 <Show when={discoveryError()}>103 <p class="peach-pairing-error">104 {discoveryError() === "pro_required"105 ? "Sync requires Peach Pro."106 : "Native sync host is not installed yet."}107 </p>108 </Show>109 <Show when={!discoveryError() && discoveredPeers().length === 0}>110 <p class="peach-pairing-status">111 <Spinner />112 Searching for Peach on your local network…113 </p>114 </Show>115 <For each={discoveredPeers()}>116 {(peer) => (117 <div class="peach-pairing-peer-card">118 <div class="peach-pairing-peer-info">119 <span class="peach-pairing-peer-name">{peer.deviceName}</span>120 <span class="peach-pairing-peer-meta">121 {peer.alreadyPaired ? "Already paired ✓" : "Found on network"}122 </span>123 </div>124 <Show when={!peer.alreadyPaired}>125 <button126 class="peach-btn peach-btn--accent peach-btn--sm"127 onClick={() => void handlePairWithPeer(peer)}128 disabled={pairingPeerId() !== null}129 >130 {pairingPeerId() === peer.deviceId ? "…" : "Pair →"}131 </button>132 </Show>133 </div>134 )}135 </For>136 <p class="peach-pairing-subline peach-pairing-subline--center">137 Open Peach on the other device. Both devices need to be on the138 same WiFi network.139 </p>140 </div>141 </Show>142143 <Show when={step().stage === "connecting" || step().stage === "verifying"}>144 <div class="peach-pairing-content">145 <Spinner />146 <p class="peach-pairing-status">147 {step().stage === "connecting" ? "Connecting…" : "Verifying…"}148 </p>149 </div>150 </Show>151152 <Show when={step().stage === "done"}>153 <div class="peach-pairing-content">154 <div class="peach-pairing-success-icon">155 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">156 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />157 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />158 </svg>159 </div>160 <p class="peach-pairing-headline">Paired!</p>161 <p class="peach-pairing-subline peach-pairing-subline--center">162 {(step() as { stage: "done"; device: PairedDevice }).device.displayName}163 </p>164 <button165 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"166 onClick={props.onClose}167 >168 Done169 </button>170 </div>171 </Show>172173 <Show when={step().stage === "error"}>174 <div class="peach-pairing-content">175 <div class="peach-pairing-error-icon">176 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">177 <circle cx="16" cy="16" r="14" stroke="var(--peach-breach)" stroke-width="1.5" />178 <path d="M11 11L21 21M21 11L11 21" stroke="var(--peach-breach)" stroke-width="1.5" stroke-linecap="round" />179 </svg>180 </div>181 <p class="peach-pairing-headline">Pairing failed</p>182 <p class="peach-pairing-subline peach-pairing-subline--center peach-pairing-error-msg">183 {(step() as { stage: "error"; message: string }).message}184 </p>185 <button186 class="peach-btn peach-btn--ghost peach-btn--md"187 onClick={() => setStep({ stage: "idle" })}188 >189 Try again190 </button>191 </div>192 </Show>193 </div>194 </div>195 );196}