From aad6faa6d2df9902bfd9ef493acda12acd36816c Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 20 Jul 2026 06:07:35 +0100 Subject: [PATCH] fix(ui): harden companion-overlay WireGuard provisioning and hide it in the companion app - Never show the get-the-app pitch inside the companion WebView itself - Don't guess peer-exists when list-peers is unreachable: try create, fall back to peer-config on already-exists errors - Translate raw 'Failed to fetch' into an actionable network hint and add a Try again button instead of a dead-end error Co-Authored-By: Claude Fable 5 --- .../src/components/CompanionIntroOverlay.vue | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index f4ac9019..5461fe39 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -161,6 +161,14 @@

{{ wgError }}

+ @@ -318,7 +326,15 @@ const POST_INTRO_GRACE_MS = 2000 let calmTicker: ReturnType | null = null +// Running inside the companion app's own WebView (it injects this JS bridge). +// The "get the companion app" pitch is nonsense there — the user is already in +// it — and the WG steps mid-pairing race the phone's changing network (the +// "Failed to fetch" dead-end QR). Server/tunnel management for connected +// companions lives in the NESMenu instead. +const IN_COMPANION_APP = typeof (window as { ArchipelagoNative?: unknown }).ArchipelagoNative !== 'undefined' + onMounted(() => { + if (IN_COMPANION_APP) return try { if (localStorage.getItem(STORAGE_KEY) !== '1') { setTimeout(maybeShow, BASE_DELAY_MS) @@ -481,12 +497,28 @@ async function loadWgPeer() { try { const listed = await rpcClient .call<{ peers: { name: string }[] }>({ method: 'vpn.list-peers' }) - .catch(() => ({ peers: [] as { name: string }[] })) - const exists = (listed.peers || []).some((p) => p.name === WG_PEER_NAME) - const res = await rpcClient.call<{ config: string; peer_ip: string }>({ - method: exists ? 'vpn.peer-config' : 'vpn.create-peer', - params: { name: WG_PEER_NAME }, - }) + .catch(() => null) + // list-peers unreachable → don't guess "doesn't exist": create-peer on an + // existing name would fail. Try create first, fall back to peer-config. + const exists = listed === null + ? null + : (listed.peers || []).some((p) => p.name === WG_PEER_NAME) + let res: { config: string; peer_ip: string } + if (exists === true) { + res = await rpcClient.call({ method: 'vpn.peer-config', params: { name: WG_PEER_NAME } }) + } else { + try { + res = await rpcClient.call({ method: 'vpn.create-peer', params: { name: WG_PEER_NAME } }) + } catch (e) { + // Peer already provisioned on a previous visit (list failed or raced). + const msg = e instanceof Error ? e.message : '' + if (/exist|duplicate/i.test(msg)) { + res = await rpcClient.call({ method: 'vpn.peer-config', params: { name: WG_PEER_NAME } }) + } else { + throw e + } + } + } wgConfig.value = res.config wgQrDataUrl.value = await QRCode.toDataURL(res.config, { width: 512, @@ -498,12 +530,24 @@ async function loadWgPeer() { }, }) } catch (e) { - wgError.value = e instanceof Error ? e.message : 'Failed to generate the tunnel config' + // fetch()'s raw "Failed to fetch" means the node itself was unreachable — + // on this screen that's usually the phone's network mid-change (WiFi drop, + // or a half-configured tunnel already routing 10.44.0.0/16). Say so, and + // leave a Retry path instead of a dead end. + const raw = e instanceof Error ? e.message : '' + wgError.value = /failed to fetch|networkerror|load failed|abort/i.test(raw) + ? "Can't reach your node. Check the phone is on the same network as the node (and any half-set-up tunnel is switched off), then tap Try again." + : raw || 'Failed to generate the tunnel config' } finally { wgLoading.value = false } } +function retryWgPeer() { + wgError.value = '' + void loadWgPeer() +} + // Same-device path: hand the config to the WireGuard app as an importable // .conf file (a phone can't scan its own screen). function downloadWgConfig() {