SyncPairing.tsx

6.2 KB · raw

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    // Pairing server keeps running in the background per spec — the user102    // showed the code, so a peer can still complete pairing after the popup103    // closes. The background clears it on success or vault-lock.104  });105106  // expiresAt is read by callers that want to display a countdown; keep the107  // signal even though this view itself only uses it as a refresh trigger.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}