1import QRCode from 'qrcode';2import { createSignal, onCleanup, onMount, Show } from "solid-js";3import { sendMessage } from "../App";4import type { PairedDevice } from "../../shared/sync-types";5import { setHasPaired } from "../../background/sync-storage";67interface SyncPairingProps {8 onClose: () => void;9 onPaired: (device: PairedDevice) => void;10}1112function Spinner() {13 return <span class="peach-pairing-spinner" />;14}1516export function SyncPairingView(props: SyncPairingProps) {17 const [qrDataUrl, setQrDataUrl] = createSignal<string | null>(null);18 const [timeLeft, setTimeLeft] = createSignal<number>(0);19 const [expiresAt, setExpiresAt] = createSignal<number | null>(null);20 const [showError, setShowError] = createSignal<string | null>(null);2122 const [pairedDevice, setPairedDevice] = createSignal<PairedDevice | null>(null);2324 let expiryTimer: ReturnType<typeof setTimeout> | null = null;25 let countdownInterval: ReturnType<typeof setInterval> | null = null;2627 function clearExpiryTimer() {28 if (expiryTimer !== null) {29 clearTimeout(expiryTimer);30 expiryTimer = null;31 }32 if (countdownInterval !== null) {33 clearInterval(countdownInterval);34 countdownInterval = null;35 }36 }3738 function handleRuntimeMessage(msg: unknown) {39 const m = msg as { type?: string; payload?: { deviceName?: string } };40 if (m?.type === "PAIRING_COMPLETED" && m.payload?.deviceName) {41 const device: PairedDevice = {42 deviceId: m.payload.deviceName,43 displayName: m.payload.deviceName,44 x25519PublicKey: "",45 pairedAt: Date.now(),46 };47 clearExpiryTimer();48 setQrDataUrl(null);49 setExpiresAt(null);50 setTimeLeft(0);51 setPairedDevice(device);52 void setHasPaired();53 props.onPaired(device);54 }55 }5657 async function startShow() {58 setShowError(null);59 setQrDataUrl(null);60 setExpiresAt(null);61 setTimeLeft(0);62 clearExpiryTimer();63 const res = await sendMessage({ type: "START_PAIRING_SERVER" });64 const response = res as {65 ok: boolean;66 error?: string;67 data?: { token: string; expiresAt: number };68 };69 if (!response.ok || !response.data) {70 setShowError("Couldn't start the pairing server.");71 return;72 }7374 const { token, expiresAt: expAt } = response.data;75 const url = await QRCode.toDataURL('peach-pair:' + token, {76 width: 240,77 margin: 2,78 color: {79 dark: '#E8E0D0',80 light: '#00000000',81 },82 });83 setQrDataUrl(url);84 setExpiresAt(expAt);8586 const remaining = Math.max(0, expAt - Date.now());87 setTimeLeft(Math.floor(remaining / 1000));8889 countdownInterval = setInterval(() => {90 const secs = Math.max(0, Math.floor((expAt - Date.now()) / 1000));91 setTimeLeft(secs);92 if (secs === 0 && countdownInterval !== null) {93 clearInterval(countdownInterval);94 countdownInterval = null;95 }96 }, 1000);9798 expiryTimer = setTimeout(() => {99 setQrDataUrl(null);100 setExpiresAt(null);101 setTimeLeft(0);102 setShowError("Code expired — tap to refresh.");103 }, remaining);104 }105106 async function stopShow() {107 clearExpiryTimer();108 setQrDataUrl(null);109 setExpiresAt(null);110 setTimeLeft(0);111 await sendMessage({ type: "STOP_PAIRING_SERVER" });112 }113114 async function generateCode() {115 await stopShow();116 await startShow();117 }118119 onMount(() => {120 chrome.runtime.onMessage.addListener(handleRuntimeMessage);121 void startShow();122 });123124 onCleanup(() => {125 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);126 clearExpiryTimer();127 128 129 130 });131132 133 134 void expiresAt;135136 return (137 <div class="peach-overlay-view ov-enter-right">138 <div class="peach-overlay-header">139 <button140 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"141 onClick={() => {142 void sendMessage({ type: "STOP_PAIRING_SERVER" });143 props.onClose();144 }}145 aria-label="Close"146 >147 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">148 <path d="M6 1L1 6L6 11" />149 </svg>150 </button>151 <span class="peach-overlay-title">add a device</span>152 <div class="peach-overlay-spacer" />153 </div>154155 <div class="peach-pairing-body">156 <Show when={pairedDevice()}>157 {(device) => (158 <div class="peach-pairing-content">159 <div class="peach-pairing-success-icon">160 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">161 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />162 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />163 </svg>164 </div>165 <p class="peach-pairing-headline">Paired!</p>166 <p class="peach-pairing-subline peach-pairing-subline--center">167 {device().displayName}168 </p>169 <button170 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"171 onClick={props.onClose}172 >173 Done174 </button>175 </div>176 )}177 </Show>178179 <Show when={!pairedDevice()}>180 <div class="peach-pairing-content peach-pairing-code-screen">181 <p class="peach-pairing-headline">Add a device</p>182183 <Show when={qrDataUrl()} fallback={184 <Show when={!showError()} fallback={185 <button186 class="peach-pairing-code peach-pairing-code--expired"187 onClick={() => void generateCode()}188 >189 refresh190 </button>191 }>192 <div class="peach-pairing-code peach-pairing-code--loading">193 <Spinner />194 </div>195 </Show>196 }>197 {(url) => (198 <div class="peach-pairing-qr-container">199 <img200 class="peach-pairing-qr"201 src={url()}202 alt="Pairing QR"203 />204 <p class="peach-pairing-instruction">205 Open Peach on your phone → Add a device → Scan this code.206 </p>207 <Show when={timeLeft() > 0}>208 <p class="peach-pairing-expiry">209 {Math.floor(timeLeft() / 60)}:{String(timeLeft() % 60).padStart(2, '0')}210 </p>211 </Show>212 <Show when={timeLeft() === 0}>213 <button class="peach-pairing-regen" onClick={() => void generateCode()}>214 Generate new code215 </button>216 </Show>217 </div>218 )}219 </Show>220221 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>222223 <Show when={showError()}>224 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>225 </Show>226 </div>227 </Show>228 </div>229 </div>230 );231}