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("Couldn't start the pairing server.");69 return;70 }7172 const { token, expiresAt: expAt } = response.data;73 const url = await QRCode.toDataURL('peach-pair:' + token, {74 width: 240,75 margin: 2,76 color: {77 dark: '#E8E0D0',78 light: '#00000000',79 },80 });81 setQrDataUrl(url);82 setExpiresAt(expAt);8384 const remaining = Math.max(0, expAt - Date.now());85 setTimeLeft(Math.floor(remaining / 1000));8687 countdownInterval = setInterval(() => {88 const secs = Math.max(0, Math.floor((expAt - Date.now()) / 1000));89 setTimeLeft(secs);90 if (secs === 0 && countdownInterval !== null) {91 clearInterval(countdownInterval);92 countdownInterval = null;93 }94 }, 1000);9596 expiryTimer = setTimeout(() => {97 setQrDataUrl(null);98 setExpiresAt(null);99 setTimeLeft(0);100 setShowError("Code expired — tap to refresh.");101 }, remaining);102 }103104 async function stopShow() {105 clearExpiryTimer();106 setQrDataUrl(null);107 setExpiresAt(null);108 setTimeLeft(0);109 await sendMessage({ type: "STOP_PAIRING_SERVER" });110 }111112 async function generateCode() {113 await stopShow();114 await startShow();115 }116117 onMount(() => {118 chrome.runtime.onMessage.addListener(handleRuntimeMessage);119 void startShow();120 });121122 onCleanup(() => {123 chrome.runtime.onMessage.removeListener(handleRuntimeMessage);124 clearExpiryTimer();125 126 127 128 });129130 131 132 void expiresAt;133134 return (135 <div class="peach-overlay-view ov-enter-right">136 <div class="peach-overlay-header">137 <button138 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"139 onClick={() => {140 void sendMessage({ type: "STOP_PAIRING_SERVER" });141 props.onClose();142 }}143 aria-label="Close"144 >145 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">146 <path d="M6 1L1 6L6 11" />147 </svg>148 </button>149 <span class="peach-overlay-title">add a device</span>150 <div class="peach-overlay-spacer" />151 </div>152153 <div class="peach-pairing-body">154 <Show when={pairedDevice()}>155 {(device) => (156 <div class="peach-pairing-content">157 <div class="peach-pairing-success-icon">158 <svg width="32" height="32" viewBox="0 0 32 32" fill="none">159 <circle cx="16" cy="16" r="14" stroke="var(--peach-health-ok)" stroke-width="1.5" />160 <path d="M10 16L14 20L22 12" stroke="var(--peach-health-ok)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />161 </svg>162 </div>163 <p class="peach-pairing-headline">Paired!</p>164 <p class="peach-pairing-subline peach-pairing-subline--center">165 {device().displayName}166 </p>167 <button168 class="peach-btn peach-btn--accent peach-btn--md peach-lock-submit"169 onClick={props.onClose}170 >171 Done172 </button>173 </div>174 )}175 </Show>176177 <Show when={!pairedDevice()}>178 <div class="peach-pairing-content peach-pairing-code-screen">179 <p class="peach-pairing-headline">Add a device</p>180181 <Show when={qrDataUrl()} fallback={182 <Show when={!showError()} fallback={183 <button184 class="peach-pairing-code peach-pairing-code--expired"185 onClick={() => void generateCode()}186 >187 refresh188 </button>189 }>190 <div class="peach-pairing-code peach-pairing-code--loading">191 <Spinner />192 </div>193 </Show>194 }>195 {(url) => (196 <div class="peach-pairing-qr-container">197 <img198 class="peach-pairing-qr"199 src={url()}200 alt="Pairing QR"201 />202 <p class="peach-pairing-instruction">203 Open Peach on your phone → Add a device → Scan this code.204 </p>205 <Show when={timeLeft() > 0}>206 <p class="peach-pairing-expiry">207 {Math.floor(timeLeft() / 60)}:{String(timeLeft() % 60).padStart(2, '0')}208 </p>209 </Show>210 <Show when={timeLeft() === 0}>211 <button class="peach-pairing-regen" onClick={() => void generateCode()}>212 Generate new code213 </button>214 </Show>215 </div>216 )}217 </Show>218219 <p class="peach-pairing-hint">Both devices must be on the same WiFi.</p>220221 <Show when={showError()}>222 <p class="peach-pairing-error peach-pairing-error--muted">{showError()}</p>223 </Show>224 </div>225 </Show>226 </div>227 </div>228 );229}