12345678910111213141516import {17 Component,18 Show,19 createSignal,20 onCleanup,21 onMount,22} from "solid-js";23import { sendMessage } from "../../shared/bridge";24import { DesktopBird, MobileBird } from "../components/PairingBirds";25import { PairingQrCode } from "../components/PairingQrCode";26import { SyncModeSelector } from "../components/SyncModeSelector";2728export interface PairDeviceViewProps {29 isPro: boolean;30 onBack(): void;31 onLearnPro(): void;32 33 onProChanged?: () => void;34}3536type SharePhase = "opening" | "invitation" | "sas" | "paired" | "error";3738export const PairDeviceView: Component<PairDeviceViewProps> = (props) => {39 const [sharePhase, setSharePhase] = createSignal<SharePhase>("opening");40 const [invitation, setInvitation] = createSignal("");41 const [sourceSas, setSourceSas] = createSignal("");42 const [receiverName, setReceiverName] = createSignal("New device");43 const [shareError, setShareError] = createSignal("");44 const [changingRelay, setChangingRelay] = createSignal(false);4546 async function startShareFlow(): Promise<void> {47 setSharePhase("opening");48 setShareError("");49 setInvitation("");50 setSourceSas("");51 try {52 const response = (await sendMessage({53 type: "START_ADOPTION_SOURCE",54 })) as {55 ok: boolean;56 data?: { invitation?: string };57 error?: string;58 };59 if (!response.ok || typeof response.data?.invitation !== "string") {60 throw new Error(response.error ?? "adoption_source_failed");61 }62 setInvitation(response.data.invitation);63 setSharePhase("invitation");64 } catch (error) {65 setShareError(error instanceof Error ? error.message : "adoption_source_failed");66 setSharePhase("error");67 }68 }6970 async function stopShareFlow(): Promise<void> {71 await sendMessage({ type: "CANCEL_ADOPTION_SOURCE" }).catch(() => undefined);72 }7374 75 async function restartShareFlow(): Promise<void> {76 await stopShareFlow();77 await startShareFlow();78 }7980 onMount(() => {81 void startShareFlow();8283 84 85 86 if (typeof chrome !== "undefined" && chrome.runtime?.onMessage) {87 const handler = (msg: { type?: string; payload?: unknown }) => {88 if (msg?.type === "ADOPTION_SOURCE_SAS") {89 const p = msg.payload as {90 sas?: string;91 receiverDeviceName?: string;92 } | undefined;93 if (typeof p?.sas === "string") {94 setSourceSas(p.sas);95 setReceiverName(96 typeof p.receiverDeviceName === "string" && p.receiverDeviceName97 ? p.receiverDeviceName98 : "New device",99 );100 setSharePhase("sas");101 }102 } else if (msg?.type === "ADOPTION_SOURCE_RESULT") {103 const p = msg.payload as {104 status?: "complete" | "error";105 error?: string;106 } | undefined;107 if (p?.status === "complete") setSharePhase("paired");108 else if (p?.status === "error") {109 setShareError(p.error ?? "adoption_failed");110 setSharePhase("error");111 }112 }113 };114 chrome.runtime.onMessage.addListener(handler);115 onCleanup(() => chrome.runtime.onMessage.removeListener(handler));116 }117118 119 120 onCleanup(() => {121 if (sharePhase() !== "paired") {122 void sendMessage({ type: "CANCEL_ADOPTION_SOURCE" });123 }124 });125 });126127 const backArrow = (128 <svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">129 <path d="M10 3 L5 8 L10 13" />130 </svg>131 );132133 134 const birdStage = (phase: SharePhase) => (135 <div class={`peach-pair-stage ${phase === "sas" || phase === "paired" ? "act-handshake" : "act-listening"}`} aria-hidden="true">136 <div class="peach-pair-bird-slot peach-pair-bird-slot-desktop">137 <DesktopBird pose={phase === "paired" ? "happy" : "chirping"} />138 </div>139 <div class="peach-pair-bird-slot peach-pair-bird-slot-mobile">140 <MobileBird pose={phase === "paired" ? "happy" : "attentive"} />141 </div>142 </div>143 );144145 function errorCopy(): string {146 switch (shareError()) {147 case "vault_locked":148 return "Unlock Peach first, then try again.";149 case "sync_disabled":150 return "Sync is turned off. Choose a relay in Settings → Sync, then try again.";151 case "private_relay_unset":152 return "Private relay is selected but has no URL yet. Enter it in Settings → Sync, then try again.";153 default:154 return "The secure pairing session ended. Start again with a fresh code.";155 }156 }157158 function liveText(): string {159 switch (sharePhase()) {160 case "opening": return "Opening a secure pairing room";161 case "invitation": return "Scan this QR on the empty device";162 case "sas": return "Confirm the code matches";163 case "paired": return "Device added";164 case "error": return "Could not add the device";165 }166 }167168 return (169 <div170 class="peach-pair-pair peach-desktop-pair peach-adoption-source"171 data-phase={sharePhase()}172 role="dialog"173 aria-label="Add a device to this vault"174 >175 <header class="peach-pair-head">176 <button177 class="peach-pair-back"178 aria-label="Back"179 onClick={() => { void stopShareFlow(); props.onBack(); }}180 >181 {backArrow}182 </button>183 <div class="peach-pair-title">Add a new device</div>184 <div class="peach-pair-head-spacer" />185 </header>186187 {birdStage(sharePhase())}188189 <Show when={sharePhase() === "opening"}>190 <div class="peach-desktop-searching">191 <div class="peach-pair-headline">Opening a secure pairing room…</div>192 <div class="peach-pair-sub">The code appears once this browser is waiting in the relay.</div>193 </div>194 </Show>195196 <Show when={sharePhase() === "invitation"}>197 <div class="peach-adoption-source-invitation">198 <Show when={!changingRelay()}>199 <div class="peach-pair-headline">Pair a device</div>200 <div class="peach-pair-sub">201 This extension has the vault. Scan this QR on the empty device,202 then compare the 8-digit safety code.203 </div>204 <PairingQrCode value={invitation()} size={176} />205 </Show>206 {207208209}210 <SyncModeSelector211 compact212 onExpand={setChangingRelay}213 onChange={() => void restartShareFlow()}214 />215 </div>216 </Show>217218 <Show when={sharePhase() === "paired"}>219 <div class="peach-desktop-paired">220 <div class="peach-pair-headline">Device added</div>221 <div class="peach-pair-sub">The new device can now sync this vault through the relay.</div>222 <div class="peach-pair-action">223 <button class="peach-btn peach-btn-primary" onClick={props.onBack}>Done</button>224 </div>225 </div>226 </Show>227228 <Show when={sharePhase() === "error"}>229 <div class="peach-desktop-error">230 <div class="peach-pair-headline">Could not add the device</div>231 <div class="peach-pair-sub">{errorCopy()}</div>232 <div class="peach-pair-action">233 <button class="peach-btn peach-btn-ghost" onClick={props.onBack}>Cancel</button>234 <button class="peach-btn peach-btn-primary" onClick={() => void startShareFlow()}>Try again</button>235 </div>236 </div>237 </Show>238239 <Show when={sharePhase() === "sas"}>240 <div class="peach-desktop-searching">241 <div class="peach-pair-headline">Compare the 8-digit safety code</div>242 <div class="peach-pair-sub">243 If {receiverName()} shows the same code, tap “They match” there. Peach244 will finish only after both devices save the pairing.245 </div>246 <div class="peach-adoption-sas" aria-label="Pairing confirmation code">247 <strong>{sourceSas()}</strong>248 </div>249 </div>250 </Show>251252 <div class="peach-sr-only" aria-live="polite">{liveText()}</div>253 </div>254 );255};