1import QRCode from 'qrcode';2import { createSignal, onCleanup, onMount, Show } from "solid-js";3import { sendMessage } from "../App";4import type { PairedDevice } from "../../shared/sync-types";56interface SyncPairingProps {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 [qrDataUrl, setQrDataUrl] = createSignal<string | null>(null);17 const [timeLeft, setTimeLeft] = createSignal<number>(0);18 const [expiresAt, setExpiresAt] = createSignal<number | null>(null);19 const [showError, setShowError] = createSignal<string | null>(null);2021 const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);2223 let expiryTimer: ReturnType<typeof setTimeout> | null = null;24 let countdownInterval: ReturnType<typeof setInterval> | null = null;2526 function clearExpiryTimer() {27 if (expiryTimer !== null) {28 clearTimeout(expiryTimer);29 expiryTimer = null;30 }31 if (countdownInterval !== null) {32 clearInterval(countdownInterval);33 countdownInterval = null;34 }35 }3637 function handleRuntimeMessage(msg: unknown) {38 const m = msg as { type?: string; payload?: { deviceName?: string } };39 if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {40 const device: PairedDevice = {41 deviceId: m.payload.deviceName,42 displayName: m.payload.deviceName,43 x25519PublicKey: "",44 pairedAt: Date.now(),45 };46 clearExpiryTimer();47 setQrDataUrl(null);48 setExpiresAt(null);49 setTimeLeft(0);50 setPairedDevice(device);51 props.onPaired(device);52 }53 }5455 async function startShow() {56 setShowError(null);57 setQrDataUrl(null);58 setExpiresAt(null);59 setTimeLeft(0);60 clearExpiryTimer();61 const res = await sendMessage({ type: "START_PAIRING_SERVER" });62 const response = res as {63 ok: boolean;64 error?: string;65 data?: { token: string; expiresAt: number };66 };67 if (!response.ok || !response.data) {68 setShowError(69 response.error === "pro_required"70 ? "Sync requires Peach Pro."71 : "Couldn't start the pairing server.",72 );73 return;74 }7576 const { token, expiresAt: expAt } = response.data;77 const url = await QRCode.toDataURL('peach-pair:' + token, {78 width: 240,79 margin: 2,80 color: {81 dark: '#E8E0D0',82 light: '#00000000',83 },84 });85 setQrDataUrl(url);86 setExpiresAt(expAt);8788 const remaining = Math.max(0, expAt - Date.now());89 setTimeLeft(Math.floor(remaining / 1000));9091 countdownInterval = setInterval(() => {92 const secs = Math.max(0, Math.floor((expAt - Date.now()) / 1000));93 setTimeLeft(secs);94 if (secs === 0 && countdownInterval !== null) {95 clearInterval(countdownInterval);96 countdownInterval = null;97 }98 }, 1000);99100 expiryTimer = setTimeout(() => {101 setQrDataUrl(null);102 setExpiresAt(null);103 setTimeLeft(0);104 setShowError("Code expired — tap to refresh.");105 }, remaining);106 }107108 async function stopShow() {109 clearExpiryTimer();110 setQrDataUrl(null);111 setExpiresAt(null);112 setTimeLeft(0);113 await sendMessage({ type: "STOP_PAIRING_SERVER" });114 }115116 async function generateCode() {117 await stopShow();118 await startShow();119 }120121 onMount(() => {122 chrome.runtime.onMessage.addListener(handleRuntimeMessage);123 void startShow();124 });125126 onCleanup(() => {127 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);128 clearExpiryTimer();129 130 131 132 });133134 135 136 void expiresAt;137138 return (139 <div class="peach-overlay-view ov-enter-right">140 <div class="peach-overlay-header">141 <button142 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"143 onClick={() => {144 void sendMessage({ type: "STOP_PAIRING_SERVER" });145 props.onClose();146 }}147 aria-label="Close"148 >149 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">150 <path d="M6 1L1 6L6 11" />151 </svg>152 </button>153 <span class="peach-overlay-title">add a device</span>154 <div class="peach-overlay-spacer" />155 </div>156157 <div class="peach-pairing-body">158 <Show when={pairedDevice()}>159 {(device) => (160 <div class="peach-pairing-content">161 <div class="peach-pairing-success-icon">162 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">163 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />164 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />165 </svg>166 </div>167 <p class="peach-pairing-headline">Paired!</p>168 <p class="peach-pairing-subline peach-pairing-subline--center">169 {device().displayName}170 </p>171 <button172 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"173 onClick={props.onClose}174 >175 Done176 </button>177 </div>178 )}179 </Show>180181 <Show when={!pairedDevice()}>182 <div class="peach-pairing-content peach-pairing-code-screen">183 <p class="peach-pairing-headline">Add a device</p>184185 <Show when={qrDataUrl()} fallback={186 <Show when={!showError()} fallback={187 <button188 class="peach-pairing-code peach-pairing-code--expired"189 onClick={() => void generateCode()}190 >191 refresh192 </button>193 }>194 <div class="peach-pairing-code peach-pairing-code--loading">195 <Spinner />196 </div>197 </Show>198 }>199 {(url) => (200 <div class="peach-pairing-qr-container">201 <img202 class="peach-pairing-qr"203 src={url()}204 alt="Pairing QR"205 />206 <p class="peach-pairing-instruction">207 Open Peach on your phone → Add a device → Scan this code.208 </p>209 <Show when={timeLeft() > 0}>210 <p class="peach-pairing-expiry">211 {Math.floor(timeLeft() / 60)}:{String(timeLeft() % 60).padStart(2, '0')}212 </p>213 </Show>214 <Show when={timeLeft() === 0}>215 <button class="peach-pairing-regen" onClick={() => void generateCode()}>216 Generate new code217 </button>218 </Show>219 </div>220 )}221 </Show>222223 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>224225 <Show when={showError()}>226 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>227 </Show>228 </div>229 </Show>230 </div>231 </div>232 );233}