1import { createSignal, onCleanup, onMount, Show } from "solid-js";2import { sendMessage } from "../App";3import type { PairedDevice } 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}1314function formatCode(code: string): string {15 if (code.length <= 3) return code;16 return `${code.slice(0, 3)} ${code.slice(3)}`;17}1819export function SyncPairingView(props: SyncPairingProps) {20 const [code, setCode] = createSignal<string | null>(null);21 const [expiresAt, setExpiresAt] = createSignal<number | null>(null);22 const [showError, setShowError] = createSignal<string | null>(null);2324 const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);2526 let expiryTimer: ReturnType<typeof setTimeout> | null = null;2728 function clearExpiryTimer() {29 if (expiryTimer !== null) {30 clearTimeout(expiryTimer);31 expiryTimer = null;32 }33 }3435 function handleRuntimeMessage(msg: unknown) {36 const m = msg as { type?: string; payload?: { deviceName?: string } };37 if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {38 const device: PairedDevice = {39 deviceId: m.payload.deviceName,40 displayName: m.payload.deviceName,41 x25519PublicKey: "",42 pairedAt: Date.now(),43 };44 clearExpiryTimer();45 setCode(null);46 setExpiresAt(null);47 setPairedDevice(device);48 props.onPaired(device);49 }50 }5152 async function startShow() {53 setShowError(null);54 setCode(null);55 setExpiresAt(null);56 clearExpiryTimer();57 const res = await sendMessage({ type: "START_PAIRING_SERVER" });58 const response = res as {59 ok: boolean;60 error?: string;61 data?: { token: string; expiresAt: number };62 };63 if (!response.ok || !response.data) {64 setShowError(65 response.error === "pro_required"66 ? "Sync requires Peach Pro."67 : "Couldn't start the pairing server.",68 );69 return;70 }71 setCode(response.data.token);72 setExpiresAt(response.data.expiresAt);73 const remaining = Math.max(0, response.data.expiresAt - Date.now());74 expiryTimer = setTimeout(() => {75 setCode(null);76 setExpiresAt(null);77 setShowError("Code expired — tap to refresh.");78 }, remaining);79 }8081 async function stopShow() {82 clearExpiryTimer();83 setCode(null);84 setExpiresAt(null);85 await sendMessage({ type: "STOP_PAIRING_SERVER" });86 }8788 async function refreshCode() {89 await stopShow();90 await startShow();91 }9293 onMount(() => {94 chrome.runtime.onMessage.addListener(handleRuntimeMessage);95 void startShow();96 });9798 onCleanup(() => {99 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);100 clearExpiryTimer();101 102 103 104 });105106 107 108 void expiresAt;109110 return (111 <div class="peach-overlay-view ov-enter-right">112 <div class="peach-overlay-header">113 <button114 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"115 onClick={() => {116 void sendMessage({ type: "STOP_PAIRING_SERVER" });117 props.onClose();118 }}119 aria-label="Close"120 >121 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">122 <path d="M6 1L1 6L6 11" />123 </svg>124 </button>125 <span class="peach-overlay-title">add a device</span>126 <div class="peach-overlay-spacer" />127 </div>128129 <div class="peach-pairing-body">130 <Show when={pairedDevice()}>131 {(device) => (132 <div class="peach-pairing-content">133 <div class="peach-pairing-success-icon">134 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">135 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />136 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />137 </svg>138 </div>139 <p class="peach-pairing-headline">Paired!</p>140 <p class="peach-pairing-subline peach-pairing-subline--center">141 {device().displayName}142 </p>143 <button144 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"145 onClick={props.onClose}146 >147 Done148 </button>149 </div>150 )}151 </Show>152153 <Show when={!pairedDevice()}>154 <div class="peach-pairing-content peach-pairing-code-screen">155 <p class="peach-pairing-headline">Add a device</p>156157 <Show when={code()} fallback={158 <Show when={!showError()} fallback={159 <button160 class="peach-pairing-code peach-pairing-code--expired"161 onClick={() => void refreshCode()}162 >163 refresh164 </button>165 }>166 <div class="peach-pairing-code peach-pairing-code--loading">167 <Spinner />168 </div>169 </Show>170 }>171 {(c) => (172 <div class="peach-pairing-code" aria-label="pairing code">173 {formatCode(c())}174 </div>175 )}176 </Show>177178 <p class="peach-pairing-subline peach-pairing-subline--center">179 Open Peach on your other device and scan180 <br />181 this code to pair.182 </p>183 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>184185 <Show when={showError()}>186 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>187 </Show>188 </div>189 </Show>190 </div>191 </div>192 );193}