1import { createSignal, onCleanup, onMount, Show } from "solid-js";2import { sendMessage } from "../App";3import type { PairedDevice } from "../../shared/sync-types";45const SHOW_MODE_AVAILABLE = true;67interface SyncPairingProps {8 onClose: () => void;9 onPaired: (device: PairedDevice) => void;10}1112type Mode = "show" | "enter";1314function Spinner() {15 return <span class="peach-pairing-spinner" />;16}1718function formatCode(code: string): string {19 if (code.length <= 3) return code;20 return `${code.slice(0, 3)} ${code.slice(3)}`;21}2223export function SyncPairingView(props: SyncPairingProps) {24 const [mode, setMode] = createSignal<Mode>(SHOW_MODE_AVAILABLE ? "show" : "enter");25 const [code, setCode] = createSignal<string | null>(null);26 const [expiresAt, setExpiresAt] = createSignal<number | null>(null);27 const [showError, setShowError] = createSignal<string | null>(null);2829 const [enteredCode, setEnteredCode] = createSignal("");30 const [connecting, setConnecting] = createSignal(false);31 const [enterError, setEnterError] = createSignal<string | null>(null);3233 const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);3435 let expiryTimer: ReturnType<typeof setTimeout> | null = null;3637 function clearExpiryTimer() {38 if (expiryTimer !== null) {39 clearTimeout(expiryTimer);40 expiryTimer = null;41 }42 }4344 function handleRuntimeMessage(msg: unknown) {45 const m = msg as { type?: string; payload?: { deviceName?: string } };46 if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {47 48 const device: PairedDevice = {49 deviceId: m.payload.deviceName,50 displayName: m.payload.deviceName,51 x25519PublicKey: "",52 pairedAt: Date.now(),53 };54 clearExpiryTimer();55 setCode(null);56 setExpiresAt(null);57 setPairedDevice(device);58 props.onPaired(device);59 }60 }6162 async function startShow() {63 setShowError(null);64 setCode(null);65 setExpiresAt(null);66 clearExpiryTimer();67 const res = await sendMessage({ type: "START_PAIRING_SERVER" });68 const response = res as {69 ok: boolean;70 error?: string;71 data?: { token: string; expiresAt: number };72 };73 if (!response.ok || !response.data) {74 setShowError(75 response.error === "pro_required"76 ? "Sync requires Peach Pro."77 : "Couldn't start the pairing server.",78 );79 return;80 }81 setCode(response.data.token);82 setExpiresAt(response.data.expiresAt);83 const remaining = Math.max(0, response.data.expiresAt - Date.now());84 expiryTimer = setTimeout(() => {85 setCode(null);86 setExpiresAt(null);87 setShowError("Code expired — tap to refresh.");88 }, remaining);89 }9091 async function stopShow() {92 clearExpiryTimer();93 setCode(null);94 setExpiresAt(null);95 await sendMessage({ type: "STOP_PAIRING_SERVER" });96 }9798 async function refreshCode() {99 await stopShow();100 await startShow();101 }102103 async function switchTo(target: Mode) {104 if (mode() === target) return;105 if (target === "enter") {106 await stopShow();107 setEnterError(null);108 setEnteredCode("");109 setMode("enter");110 } else {111 setEnterError(null);112 setMode("show");113 await startShow();114 }115 }116117 function onEnterInput(e: InputEvent) {118 const target = e.currentTarget as HTMLInputElement;119 const raw = target.value.replace(/[^0-9]/g, "").slice(0, 6);120 setEnteredCode(formatCode(raw));121 if (enterError()) setEnterError(null);122 }123124 function digitsEntered(): string {125 return enteredCode().replace(/\D/g, "");126 }127128 async function connect() {129 const token = digitsEntered();130 if (token.length !== 6) return;131 setConnecting(true);132 setEnterError(null);133 const res = await sendMessage({134 type: "CONNECT_WITH_CODE",135 payload: { token },136 });137 setConnecting(false);138 const response = res as { ok: boolean; error?: string; data?: { deviceName?: string } };139 if (!response.ok) {140 const msg =141 response.error === "no peer responded to pairing code" ||142 (response.error ?? "").includes("resolve failed")143 ? "Couldn't find that device. Make sure both are on the same WiFi."144 : response.error === "pair_rejected"145 ? "The other device declined."146 : response.error === "invalid_code"147 ? "Enter all six digits."148 : "Couldn't connect. Try again.";149 setEnterError(msg);150 return;151 }152 const name = response.data?.deviceName ?? "Peach";153 const device: PairedDevice = {154 deviceId: name,155 displayName: name,156 x25519PublicKey: "",157 pairedAt: Date.now(),158 };159 setPairedDevice(device);160 props.onPaired(device);161 }162163 onMount(() => {164 chrome.runtime.onMessage.addListener(handleRuntimeMessage);165 void startShow();166 });167168 onCleanup(() => {169 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);170 clearExpiryTimer();171 172 173 174 });175176 return (177 <div class="peach-overlay-view ov-enter-right">178 <div class="peach-overlay-header">179 <button180 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"181 onClick={() => {182 183 184 185 void sendMessage({ type: "STOP_PAIRING_SERVER" });186 props.onClose();187 }}188 aria-label="Close"189 >190 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">191 <path d="M6 1L1 6L6 11" />192 </svg>193 </button>194 <span class="peach-overlay-title">add a device</span>195 <div class="peach-overlay-spacer" />196 </div>197198 <div class="peach-pairing-body">199 <Show when={pairedDevice()}>200 {(device) => (201 <div class="peach-pairing-content">202 <div class="peach-pairing-success-icon">203 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">204 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />205 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />206 </svg>207 </div>208 <p class="peach-pairing-headline">Paired!</p>209 <p class="peach-pairing-subline peach-pairing-subline--center">210 {device().displayName}211 </p>212 <button213 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"214 onClick={props.onClose}215 >216 Done217 </button>218 </div>219 )}220 </Show>221222 <Show when={!pairedDevice() && mode() === "show"}>223 <div class="peach-pairing-content peach-pairing-code-screen">224 <p class="peach-pairing-headline">Add a device</p>225226 <Show when={code()} fallback={227 <Show when={!showError()} fallback={228 <button229 class="peach-pairing-code peach-pairing-code--expired"230 onClick={() => void refreshCode()}231 >232 refresh233 </button>234 }>235 <div class="peach-pairing-code peach-pairing-code--loading">236 <Spinner />237 </div>238 </Show>239 }>240 {(c) => (241 <div class="peach-pairing-code" aria-label="pairing code">242 {formatCode(c())}243 </div>244 )}245 </Show>246247 <p class="peach-pairing-subline peach-pairing-subline--center">248 Open Peach on your other device, tap249 <br />250 Add a device, and enter this code.251 </p>252 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>253254 <Show when={showError()}>255 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>256 </Show>257258 <Show when={SHOW_MODE_AVAILABLE}>259 <button260 class="peach-pairing-toggle"261 onClick={() => void switchTo("enter")}262 >263 I have a code to enter →264 </button>265 </Show>266 </div>267 </Show>268269 <Show when={!pairedDevice() && mode() === "enter"}>270 <div class="peach-pairing-content peach-pairing-code-screen">271 <p class="peach-pairing-headline">Add a device</p>272273 <input274 class="peach-pairing-code-input"275 type="text"276 inputmode="numeric"277 autocomplete="off"278 autocorrect="off"279 spellcheck={false}280 placeholder="000 000"281 value={enteredCode()}282 onInput={onEnterInput}283 disabled={connecting()}284 aria-label="enter pairing code"285 />286287 <button288 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"289 onClick={() => void connect()}290 disabled={digitsEntered().length !== 6 || connecting()}291 >292 {connecting() ? "Connecting…" : "Connect →"}293 </button>294295 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>296297 <Show when={enterError()}>298 <p class="peach-pairing-error peach-pairing-error--muted">{enterError()}</p>299 </Show>300301 <Show when={SHOW_MODE_AVAILABLE}>302 <button303 class="peach-pairing-toggle"304 onClick={() => void switchTo("show")}305 disabled={connecting()}306 >307 ← Show my code instead308 </button>309 </Show>310 </div>311 </Show>312 </div>313 </div>314 );315}