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 mode: "initiate" | "accept";7 onClose: () => void;8 onPaired: (device: PairedDevice) => void;9}1011function Spinner() {12 return <span class="peach-pairing-spinner" />;13}1415export function SyncPairingView(props: SyncPairingProps) {16 const [step, setStep] = createSignal<PairingStep>({ stage: "idle" });17 const [codeInput, setCodeInput] = createSignal("");18 const [codeError, setCodeError] = createSignal<string | null>(null);1920 21 22 23 const [discoveredPeers, setDiscoveredPeers] = createSignal<DiscoveredPeer[]>([]);24 const [discoveryActive, setDiscoveryActive] = createSignal(false);25 const [discoveryError, setDiscoveryError] = createSignal<string | null>(null);26 const [pairingPeerId, setPairingPeerId] = createSignal<string | null>(null);2728 onMount(() => {29 if (props.mode !== "initiate") return;30 void startDiscovery();31 });3233 onCleanup(() => {34 if (discoveryActive()) void sendMessage({ type: "STOP_PEER_DISCOVERY" });35 });3637 function handleRuntimeMessage(msg: unknown) {38 const m = msg as { type?: string; peer?: DiscoveredPeer; deviceId?: string };39 if (m?.type === "SYNC_PEER_DISCOVERED" && m.peer) {40 const peer = m.peer;41 setDiscoveredPeers((prev) => {42 if (prev.some((p) => p.deviceId === peer.deviceId)) return prev;43 return [...prev, peer];44 });45 }46 if (m?.type === "SYNC_PEER_LOST" && m.deviceId) {47 const id = m.deviceId;48 setDiscoveredPeers((prev) => prev.filter((p) => p.deviceId !== id));49 }50 }5152 async function startDiscovery() {53 chrome.runtime.onMessage.addListener(handleRuntimeMessage);54 onCleanup(() => chrome.runtime.onMessage.removeListener(handleRuntimeMessage));5556 const res = await sendMessage({ type: "START_PEER_DISCOVERY" });57 const response = res as { ok: boolean; error?: string };58 if (response.ok) {59 setDiscoveryActive(true);60 setDiscoveryError(null);61 } else {62 setDiscoveryError(response.error ?? "discovery_failed");63 }64 }6566 async function handlePairWithPeer(peer: DiscoveredPeer) {67 setPairingPeerId(peer.deviceId);68 setStep({ stage: "connecting", peerName: peer.deviceName });69 const res = await sendMessage({70 type: "INITIATE_PAIR_WITH_PEER",71 payload: { peer },72 });73 const response = res as { ok: boolean; error?: string };74 setPairingPeerId(null);75 if (!response.ok) {76 setStep({ stage: "error", message: response.error ?? "pair_failed" });77 return;78 }79 const paired: PairedDevice = {80 deviceId: peer.deviceId,81 displayName: peer.deviceName,82 x25519PublicKey: peer.x25519PublicKey,83 pairedAt: Date.now(),84 };85 setStep({ stage: "done", device: paired });86 props.onPaired(paired);87 }8889 async function handleConnect() {90 const code = codeInput().replace(/\D/g, "").slice(0, 6);91 if (code.length !== 6) {92 setCodeError("Enter a 6-digit code");93 return;94 }95 setCodeError(null);96 setStep({ stage: "connecting", peerName: "" });9798 const res = await sendMessage({99 type: "ACCEPT_PAIRING",100 payload: { code },101 });102 const response = res as { ok: boolean; data?: PairedDevice; error?: string };103104 if (!response.ok) {105 setStep({ stage: "error", message: response.error ?? "Connection failed" });106 return;107 }108 setStep({ stage: "verifying" });109110 if (response.data) {111 setStep({ stage: "done", device: response.data });112 props.onPaired(response.data);113 }114 }115116 function handleCodeInput(e: InputEvent) {117 const target = e.currentTarget as HTMLInputElement;118 const value = target.value.replace(/\D/g, "").slice(0, 6);119 setCodeInput(value);120 setCodeError(null);121 }122123 return (124 <div class="peach-overlay-view ov-enter-right">125 <div class="peach-overlay-header">126 <button127 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"128 onClick={props.onClose}129 aria-label="Close"130 >131 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">132 <path d="M6 1L1 6L6 11" />133 </svg>134 </button>135 <span class="peach-overlay-title">pair devices</span>136 <div class="peach-overlay-spacer" />137 </div>138139 <div class="peach-pairing-body">140 <Show when={props.mode === "initiate" && step().stage !== "done" && step().stage !== "error" && step().stage !== "connecting" && step().stage !== "verifying"}>141 <div class="peach-pairing-content peach-pairing-discover">142 <p class="peach-pairing-headline">Pair a device</p>143 <Show when={discoveryError()}>144 <p class="peach-pairing-error">145 {discoveryError() === "pro_required"146 ? "Sync requires Peach Pro."147 : "Native sync host is not installed yet."}148 </p>149 </Show>150 <Show when={!discoveryError() && discoveredPeers().length === 0}>151 <p class="peach-pairing-status">152 <Spinner />153 Searching for Peach on your local network...154 </p>155 </Show>156 <For each={discoveredPeers()}>157 {(peer) => (158 <div class="peach-pairing-peer-card">159 <div class="peach-pairing-peer-info">160 <span class="peach-pairing-peer-name">{peer.deviceName}</span>161 <span class="peach-pairing-peer-meta">162 {peer.alreadyPaired ? "Already paired ✓" : "Found on network"}163 </span>164 </div>165 <Show when={!peer.alreadyPaired}>166 <button167 class="peach-btn peach-btn--accent peach-btn--sm"168 onClick={() => void handlePairWithPeer(peer)}169 disabled={pairingPeerId() !== null}170 >171 {pairingPeerId() === peer.deviceId ? "..." : "Pair →"}172 </button>173 </Show>174 </div>175 )}176 </For>177 <p class="peach-pairing-subline peach-pairing-subline--center">178 Make sure both devices are on the same Wi-Fi network.179 </p>180 </div>181 </Show>182183 <Show when={props.mode === "accept"}>184 <Show when={step().stage === "idle" || step().stage === "entering_code"}>185 <div class="peach-pairing-content">186 <p class="peach-pairing-headline">Pair with another device</p>187 <p class="peach-pairing-subline">188 Enter the 6-digit code shown on the other device.189 </p>190 <div class="peach-pairing-code-inputs">191 {Array.from({ length: 6 }, (_, i) => (192 <input193 class={`peach-pairing-code-box ${codeError() ? "has-error" : ""}`}194 type="text"195 inputMode="numeric"196 maxLength={1}197 value={codeInput()[i] ?? ""}198 onInput={(e) => {199 const current = codeInput();200 const digit = (e.currentTarget as HTMLInputElement).value.replace(/\D/g, "").slice(-1);201 const newValue = current.slice(0, i) + digit + current.slice(i + 1);202 setCodeInput(newValue.slice(0, 6));203 setCodeError(null);204 if (digit && i < 5) {205 const inputs = document.querySelectorAll(".peach-pairing-code-box");206 (inputs[i + 1] as HTMLInputElement)?.focus();207 }208 }}209 onKeyDown={(e) => {210 if (e.key === "Backspace" && !codeInput()[i] && i > 0) {211 const inputs = document.querySelectorAll(".peach-pairing-code-box");212 (inputs[i - 1] as HTMLInputElement)?.focus();213 }214 }}215 />216 ))}217 </div>218 <Show when={codeError()}>219 <p class="peach-pairing-error">{codeError()}</p>220 </Show>221 <button222 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"223 onClick={() => void handleConnect()}224 disabled={codeInput().length !== 6}225 >226 Connect227 </button>228 </div>229 </Show>230231 <Show when={step().stage === "connecting" || step().stage === "verifying"}>232 <div class="peach-pairing-content">233 <Spinner />234 <p class="peach-pairing-status">235 {step().stage === "connecting" ? "Connecting..." : "Verifying..."}236 </p>237 </div>238 </Show>239 </Show>240241 <Show when={step().stage === "done"}>242 <div class="peach-pairing-content">243 <div class="peach-pairing-success-icon">244 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">245 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />246 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />247 </svg>248 </div>249 <p class="peach-pairing-headline">Paired!</p>250 <p class="peach-pairing-subline peach-pairing-subline--center">251 {(step() as { stage: "done"; device: PairedDevice }).device.displayName}252 </p>253 <button254 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"255 onClick={props.onClose}256 >257 Done258 </button>259 </div>260 </Show>261262 <Show when={step().stage === "error"}>263 <div class="peach-pairing-content">264 <div class="peach-pairing-error-icon">265 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">266 <circle cx="16" cy="16" r="14" stroke="var(--peach-breach)" stroke-width="1.5" />267 <path d="M11 11L21 21M21 11L11 21" stroke="var(--peach-breach)" stroke-width="1.5" stroke-linecap="round" />268 </svg>269 </div>270 <p class="peach-pairing-headline">Pairing failed</p>271 <p class="peach-pairing-subline peach-pairing-subline--center peach-pairing-error-msg">272 {(step() as { stage: "error"; message: string }).message}273 </p>274 <button275 class="peach-btn peach-btn--ghost peach-btn--md"276 onClick={() => setStep({ stage: "idle" })}277 >278 Try again279 </button>280 </div>281 </Show>282 </div>283 </div>284 );285}