1import { createSignal, onCleanup, onMount, Show } from "solid-js";2import { Capacitor } from "@capacitor/core";3import { sendMessage } from "../App";4import type { PairedDevice } from "../../shared/sync-types";56789const SHOW_MODE_AVAILABLE = !Capacitor.isNativePlatform();1011interface SyncPairingProps {12 onClose: () => void;13 onPaired: (device: PairedDevice) => void;14}1516type Mode = "show" | "enter";1718function Spinner() {19 return <span class="peach-pairing-spinner" />;20}2122function formatCode(code: string): string {23 if (code.length <= 3) return code;24 return `${code.slice(0, 3)} ${code.slice(3)}`;25}2627export function SyncPairingView(props: SyncPairingProps) {28 const [mode, setMode] = createSignal<Mode>(SHOW_MODE_AVAILABLE ? "show" : "enter");29 const [code, setCode] = createSignal<string | null>(null);30 const [expiresAt, setExpiresAt] = createSignal<number | null>(null);31 const [showError, setShowError] = createSignal<string | null>(null);3233 const [enteredCode, setEnteredCode] = createSignal("");34 const [connecting, setConnecting] = createSignal(false);35 const [enterError, setEnterError] = createSignal<string | null>(null);3637 const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);3839 let expiryTimer: ReturnType<typeof setTimeout> | null = null;4041 function clearExpiryTimer() {42 if (expiryTimer !== null) {43 clearTimeout(expiryTimer);44 expiryTimer = null;45 }46 }4748 function handleRuntimeMessage(msg: unknown) {49 const m = msg as { type?: string; payload?: { deviceName?: string } };50 if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {51 52 const device: PairedDevice = {53 deviceId: m.payload.deviceName,54 displayName: m.payload.deviceName,55 x25519PublicKey: "",56 pairedAt: Date.now(),57 };58 clearExpiryTimer();59 setCode(null);60 setExpiresAt(null);61 setPairedDevice(device);62 props.onPaired(device);63 }64 }6566 async function startShow() {67 setShowError(null);68 setCode(null);69 setExpiresAt(null);70 clearExpiryTimer();71 const res = await sendMessage({ type: "START_PAIRING_SERVER" });72 const response = res as {73 ok: boolean;74 error?: string;75 data?: { token: string; expiresAt: number };76 };77 if (!response.ok || !response.data) {78 setShowError(79 response.error === "pro_required"80 ? "Sync requires Peach Pro."81 : "Couldn't start the pairing server.",82 );83 return;84 }85 setCode(response.data.token);86 setExpiresAt(response.data.expiresAt);87 const remaining = Math.max(0, response.data.expiresAt - Date.now());88 expiryTimer = setTimeout(() => {89 setCode(null);90 setExpiresAt(null);91 setShowError("Code expired — tap to refresh.");92 }, remaining);93 }9495 async function stopShow() {96 clearExpiryTimer();97 setCode(null);98 setExpiresAt(null);99 await sendMessage({ type: "STOP_PAIRING_SERVER" });100 }101102 async function refreshCode() {103 await stopShow();104 await startShow();105 }106107 async function switchTo(target: Mode) {108 if (mode() === target) return;109 if (target === "enter") {110 await stopShow();111 setEnterError(null);112 setEnteredCode("");113 setMode("enter");114 } else {115 setEnterError(null);116 setMode("show");117 await startShow();118 }119 }120121 function onEnterInput(e: InputEvent) {122 const target = e.currentTarget as HTMLInputElement;123 const raw = target.value.replace(/[^0-9]/g, "").slice(0, 6);124 setEnteredCode(formatCode(raw));125 if (enterError()) setEnterError(null);126 }127128 function digitsEntered(): string {129 return enteredCode().replace(/\D/g, "");130 }131132 async function connect() {133 const token = digitsEntered();134 if (token.length !== 6) return;135 setConnecting(true);136 setEnterError(null);137 const res = await sendMessage({138 type: "CONNECT_WITH_CODE",139 payload: { token },140 });141 setConnecting(false);142 const response = res as { ok: boolean; error?: string; data?: { deviceName?: string } };143 if (!response.ok) {144 const msg =145 response.error === "no peer responded to pairing code" ||146 (response.error ?? "").includes("resolve failed")147 ? "Couldn't find that device. Make sure both are on the same WiFi."148 : response.error === "pair_rejected"149 ? "The other device declined."150 : response.error === "invalid_code"151 ? "Enter all six digits."152 : "Couldn't connect. Try again.";153 setEnterError(msg);154 return;155 }156 const name = response.data?.deviceName ?? "Peach";157 const device: PairedDevice = {158 deviceId: name,159 displayName: name,160 x25519PublicKey: "",161 pairedAt: Date.now(),162 };163 setPairedDevice(device);164 props.onPaired(device);165 }166167 onMount(() => {168 if (!Capacitor.isNativePlatform()) {169 chrome.runtime.onMessage.addListener(handleRuntimeMessage);170 }171 if (SHOW_MODE_AVAILABLE) {172 void startShow();173 }174 });175176 onCleanup(() => {177 if (!Capacitor.isNativePlatform()) {178 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);179 }180 clearExpiryTimer();181 182 183 184 });185186 return (187 <div class="peach-overlay-view ov-enter-right">188 <div class="peach-overlay-header">189 <button190 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"191 onClick={() => {192 193 194 195 void sendMessage({ type: "STOP_PAIRING_SERVER" });196 props.onClose();197 }}198 aria-label="Close"199 >200 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">201 <path d="M6 1L1 6L6 11" />202 </svg>203 </button>204 <span class="peach-overlay-title">add a device</span>205 <div class="peach-overlay-spacer" />206 </div>207208 <div class="peach-pairing-body">209 <Show when={pairedDevice()}>210 {(device) => (211 <div class="peach-pairing-content">212 <div class="peach-pairing-success-icon">213 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">214 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />215 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />216 </svg>217 </div>218 <p class="peach-pairing-headline">Paired!</p>219 <p class="peach-pairing-subline peach-pairing-subline--center">220 {device().displayName}221 </p>222 <button223 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"224 onClick={props.onClose}225 >226 Done227 </button>228 </div>229 )}230 </Show>231232 <Show when={!pairedDevice() && mode() === "show"}>233 <div class="peach-pairing-content peach-pairing-code-screen">234 <p class="peach-pairing-headline">Add a device</p>235236 <Show when={code()} fallback={237 <Show when={!showError()} fallback={238 <button239 class="peach-pairing-code peach-pairing-code--expired"240 onClick={() => void refreshCode()}241 >242 refresh243 </button>244 }>245 <div class="peach-pairing-code peach-pairing-code--loading">246 <Spinner />247 </div>248 </Show>249 }>250 {(c) => (251 <div class="peach-pairing-code" aria-label="pairing code">252 {formatCode(c())}253 </div>254 )}255 </Show>256257 <p class="peach-pairing-subline peach-pairing-subline--center">258 Open Peach on your other device, tap259 <br />260 Add a device, and enter this code.261 </p>262 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>263264 <Show when={showError()}>265 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>266 </Show>267268 <Show when={SHOW_MODE_AVAILABLE}>269 <button270 class="peach-pairing-toggle"271 onClick={() => void switchTo("enter")}272 >273 I have a code to enter →274 </button>275 </Show>276 </div>277 </Show>278279 <Show when={!pairedDevice() && mode() === "enter"}>280 <div class="peach-pairing-content peach-pairing-code-screen">281 <p class="peach-pairing-headline">Add a device</p>282283 <input284 class="peach-pairing-code-input"285 type="text"286 inputmode="numeric"287 autocomplete="off"288 autocorrect="off"289 spellcheck={false}290 placeholder="000 000"291 value={enteredCode()}292 onInput={onEnterInput}293 disabled={connecting()}294 aria-label="enter pairing code"295 />296297 <button298 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"299 onClick={() => void connect()}300 disabled={digitsEntered().length !== 6 || connecting()}301 >302 {connecting() ? "Connecting…" : "Connect →"}303 </button>304305 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>306307 <Show when={enterError()}>308 <p class="peach-pairing-error peach-pairing-error--muted">{enterError()}</p>309 </Show>310311 <Show when={SHOW_MODE_AVAILABLE}>312 <button313 class="peach-pairing-toggle"314 onClick={() => void switchTo("show")}315 disabled={connecting()}316 >317 ← Show my code instead318 </button>319 </Show>320 </div>321 </Show>322 </div>323 </div>324 );325}