12345678910111213141516import {17 Component,18 Show,19 createSignal,20 onCleanup,21 onMount,22} from "solid-js";23import { sendMessage } from "../../shared/bridge";24import { PairingQrCode } from "../components/PairingQrCode";25import { SyncModeSelector } from "../components/SyncModeSelector";2627export interface PairDeviceViewProps {28 isPro: boolean;29 onBack(): void;30 onLearnPro(): void;31 32 onProChanged?: () => void;33}3435type SharePhase = "opening" | "invitation" | "sas" | "paired" | "error";3637export const PairDeviceView: Component<PairDeviceViewProps> = (props) => {38 const [sharePhase, setSharePhase] = createSignal<SharePhase>("opening");39 const [invitation, setInvitation] = createSignal("");40 const [sourceSas, setSourceSas] = createSignal("");41 const [receiverName, setReceiverName] = createSignal("New device");42 const [shareError, setShareError] = createSignal("");43 const [changingRelay, setChangingRelay] = createSignal(false);4445 async function startShareFlow(): Promise<void> {46 setSharePhase("opening");47 setShareError("");48 setInvitation("");49 setSourceSas("");50 try {51 const response = (await sendMessage({52 type: "START_ADOPTION_SOURCE",53 })) as {54 ok: boolean;55 data?: { invitation?: string };56 error?: string;57 };58 if (!response.ok || typeof response.data?.invitation !== "string") {59 throw new Error(response.error ?? "adoption_source_failed");60 }61 setInvitation(response.data.invitation);62 setSharePhase("invitation");63 } catch (error) {64 setShareError(error instanceof Error ? error.message : "adoption_source_failed");65 setSharePhase("error");66 }67 }6869 async function stopShareFlow(): Promise<void> {70 await sendMessage({ type: "CANCEL_ADOPTION_SOURCE" }).catch(() => undefined);71 }7273 74 async function restartShareFlow(): Promise<void> {75 await stopShareFlow();76 await startShareFlow();77 }7879 onMount(() => {80 void startShareFlow();8182 83 84 85 if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {86 const handler = (msg: { type?: string; payload?: unknown }) => {87 if (msg?.type === "ADOPTION_SOURCE_SAS") {88 const p = msg.payload as {89 sas?: string;90 receiverDeviceName?: string;91 } | undefined;92 if (typeof p?.sas === "string") {93 setSourceSas(p.sas);94 setReceiverName(95 typeof p.receiverDeviceName === "string" && p.receiverDeviceName96 ? p.receiverDeviceName97 : "New device",98 );99 setSharePhase("sas");100 }101 } else if (msg?.type === "ADOPTION_SOURCE_RESULT") {102 const p = msg.payload as {103 status?: "complete" | "error";104 error?: string;105 } | undefined;106 if (p?.status === "complete") setSharePhase("paired");107 else if (p?.status === "error") {108 setShareError(p.error ?? "adoption_failed");109 setSharePhase("error");110 }111 }112 };113 chrome.runtime.onMessage.addListener(handler);114 onCleanup(() => chrome.runtime.onMessage.removeListener(handler));115 }116117 118 119 onCleanup(() => {120 if (sharePhase() !== "paired") {121 void sendMessage({ type: "CANCEL_ADOPTION_SOURCE" });122 }123 });124 });125126 const backArrow = (127 <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">128 <path d="M10 3 L5 8 L10 13" />129 </svg>130 );131132 function errorCopy(): string {133 switch (shareError()) {134 case "vault_locked":135 return "Unlock Peach first, then try again.";136 case "sync_disabled":137 return "Sync is turned off. Choose a relay in Settings → Sync, then try again.";138 case "private_relay_unset":139 return "Private relay is selected but has no URL yet. Enter it in Settings → Sync, then try again.";140 default:141 return "The secure pairing session ended. Start again with a fresh code.";142 }143 }144145 function liveText(): string {146 switch (sharePhase()) {147 case "opening": return "Opening a secure pairing room";148 case "invitation": return "Scan this QR on the empty device";149 case "sas": return "Confirm the code matches";150 case "paired": return "Device added";151 case "error": return "Could not add the device";152 }153 }154155 return (156 <div157 class="peach-pair-pair peach-desktop-pair peach-adoption-source"158 data-phase={sharePhase()}159 role="dialog"160 aria-label="Add a device to this vault"161 >162 <header class="peach-pair-head">163 <button164 class="peach-pair-back"165 aria-label="Back"166 onClick={() => { void stopShareFlow(); props.onBack(); }}167 >168 {backArrow}169 </button>170 <div class="peach-pair-title">Add a new device</div>171 <div class="peach-pair-head-spacer" />172 </header>173174 <Show when={sharePhase() === "opening"}>175 <div class="peach-desktop-searching">176 <div class="peach-pair-headline">Opening a secure pairing room…</div>177 <div class="peach-pair-sub">The code appears once this browser is waiting in the relay.</div>178 </div>179 </Show>180181 <Show when={sharePhase() === "invitation"}>182 <div class="peach-adoption-source-invitation">183 <Show when={!changingRelay()}>184 <div class="peach-pair-headline">Pair a device</div>185 <div class="peach-pair-sub">186 This extension has the vault. Scan this QR on the empty device,187 then compare the 8-digit safety code.188 </div>189 <PairingQrCode value={invitation()} size={176} />190 </Show>191 {192193194}195 <SyncModeSelector196 compact197 onExpand={setChangingRelay}198 onChange={() => void restartShareFlow()}199 />200 </div>201 </Show>202203 <Show when={sharePhase() === "paired"}>204 <div class="peach-desktop-paired">205 <div class="peach-pair-headline">Device added</div>206 <div class="peach-pair-sub">The new device can now sync this vault through the relay.</div>207 <div class="peach-pair-action">208 <button class="peach-btn peach-btn-primary" onClick={props.onBack}>Done</button>209 </div>210 </div>211 </Show>212213 <Show when={sharePhase() === "error"}>214 <div class="peach-desktop-error">215 <div class="peach-pair-headline">Could not add the device</div>216 <div class="peach-pair-sub">{errorCopy()}</div>217 <div class="peach-pair-action">218 <button class="peach-btn peach-btn-ghost" onClick={props.onBack}>Cancel</button>219 <button class="peach-btn peach-btn-primary" onClick={() => void startShareFlow()}>Try again</button>220 </div>221 </div>222 </Show>223224 <Show when={sharePhase() === "sas"}>225 <div class="peach-desktop-searching">226 <div class="peach-pair-headline">Compare the 8-digit safety code</div>227 <div class="peach-pair-sub">228 If {receiverName()} shows the same code, tap “They match” there. Peach229 will finish only after both devices save the pairing.230 </div>231 <div class="peach-adoption-sas" aria-label="Pairing confirmation code">232 <strong>{sourceSas()}</strong>233 </div>234 </div>235 </Show>236237 <div class="peach-sr-only" aria-live="polite">{liveText()}</div>238 </div>239 );240};