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 = true;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 void startShow();172 });173174 onCleanup(() => {175 if (!Capacitor.isNativePlatform()) {176 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);177 }178 clearExpiryTimer();179 180 181 182 });183184 return (185 <div class="peach-overlay-view ov-enter-right">186 <div class="peach-overlay-header">187 <button188 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"189 onClick={() => {190 191 192 193 void sendMessage({ type: "STOP_PAIRING_SERVER" });194 props.onClose();195 }}196 aria-label="Close"197 >198 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">199 <path d="M6 1L1 6L6 11" />200 </svg>201 </button>202 <span class="peach-overlay-title">add a device</span>203 <div class="peach-overlay-spacer" />204 </div>205206 <div class="peach-pairing-body">207 <Show when={pairedDevice()}>208 {(device) => (209 <div class="peach-pairing-content">210 <div class="peach-pairing-success-icon">211 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">212 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />213 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />214 </svg>215 </div>216 <p class="peach-pairing-headline">Paired!</p>217 <p class="peach-pairing-subline peach-pairing-subline--center">218 {device().displayName}219 </p>220 <button221 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"222 onClick={props.onClose}223 >224 Done225 </button>226 </div>227 )}228 </Show>229230 <Show when={!pairedDevice() && mode() === "show"}>231 <div class="peach-pairing-content peach-pairing-code-screen">232 <p class="peach-pairing-headline">Add a device</p>233234 <Show when={code()} fallback={235 <Show when={!showError()} fallback={236 <button237 class="peach-pairing-code peach-pairing-code--expired"238 onClick={() => void refreshCode()}239 >240 refresh241 </button>242 }>243 <div class="peach-pairing-code peach-pairing-code--loading">244 <Spinner />245 </div>246 </Show>247 }>248 {(c) => (249 <div class="peach-pairing-code" aria-label="pairing code">250 {formatCode(c())}251 </div>252 )}253 </Show>254255 <p class="peach-pairing-subline peach-pairing-subline--center">256 Open Peach on your other device, tap257 <br />258 Add a device, and enter this code.259 </p>260 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>261262 <Show when={showError()}>263 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>264 </Show>265266 <Show when={SHOW_MODE_AVAILABLE}>267 <button268 class="peach-pairing-toggle"269 onClick={() => void switchTo("enter")}270 >271 I have a code to enter →272 </button>273 </Show>274 </div>275 </Show>276277 <Show when={!pairedDevice() && mode() === "enter"}>278 <div class="peach-pairing-content peach-pairing-code-screen">279 <p class="peach-pairing-headline">Add a device</p>280281 <input282 class="peach-pairing-code-input"283 type="text"284 inputmode="numeric"285 autocomplete="off"286 autocorrect="off"287 spellcheck={false}288 placeholder="000 000"289 value={enteredCode()}290 onInput={onEnterInput}291 disabled={connecting()}292 aria-label="enter pairing code"293 />294295 <button296 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"297 onClick={() => void connect()}298 disabled={digitsEntered().length !== 6 || connecting()}299 >300 {connecting() ? "Connecting…" : "Connect →"}301 </button>302303 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>304305 <Show when={enterError()}>306 <p class="peach-pairing-error peach-pairing-error--muted">{enterError()}</p>307 </Show>308309 <Show when={SHOW_MODE_AVAILABLE}>310 <button311 class="peach-pairing-toggle"312 onClick={() => void switchTo("show")}313 disabled={connecting()}314 >315 ← Show my code instead316 </button>317 </Show>318 </div>319 </Show>320 </div>321 </div>322 );323}