From d259f3cbb9ae73f95d33168196fd15cf54380ade Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 31 Aug 2026 12:56:32 +0100 Subject: [PATCH 1/8] fix(web): companion-gate the store banner + manual intro trigger (#61 residual) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the auto-popup was companion-gated — inside the companion WebView users still saw the 'install the companion' banner in the App Store and could pop the intro overlay through it. Gates all three paths on isCompanionApp(): CompanionBanner self-hides, openCompanionIntro() is a no-op, and the manual-open watcher in CompanionIntroOverlay refuses to open (the overlay's raw window check also moves to the canonical helper so there is exactly one detection). No APK change. --- .../src/components/CompanionIntroOverlay.vue | 11 ++++- .../__tests__/useCompanionIntro.test.ts | 41 +++++++++++++++++++ neode-ui/src/composables/useCompanionIntro.ts | 7 ++++ .../src/views/discover/CompanionBanner.vue | 9 +++- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 neode-ui/src/composables/__tests__/useCompanionIntro.test.ts diff --git a/neode-ui/src/components/CompanionIntroOverlay.vue b/neode-ui/src/components/CompanionIntroOverlay.vue index 6e904dda..01a915d1 100644 --- a/neode-ui/src/components/CompanionIntroOverlay.vue +++ b/neode-ui/src/components/CompanionIntroOverlay.vue @@ -143,6 +143,7 @@ import { ref, onMounted, onUnmounted, watch } from 'vue' import * as QRCode from 'qrcode' import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro' import { companionIntroRequested } from '@/composables/useCompanionIntro' +import { isCompanionApp } from '@/utils/openExternal' import { useLoginTransitionStore } from '@/stores/loginTransition' import { useServerStore } from '@/stores/server' import { rpcClient } from '@/api/rpc-client' @@ -205,10 +206,12 @@ const POST_INTRO_GRACE_MS = 2000 let calmTicker: ReturnType | null = null -// Running inside the companion app's own WebView (it injects this JS bridge). +// Running inside the companion app's own WebView (it injects the JS bridge — +// detected with the canonical helper, not a raw window check, so the gate +// is identical everywhere the question is asked). // The "get the companion app" pitch is nonsense there — the user is already in // it. Server management for connected companions lives in the NESMenu instead. -const IN_COMPANION_APP = typeof (window as { ArchipelagoNative?: unknown }).ArchipelagoNative !== 'undefined' +const IN_COMPANION_APP = isCompanionApp() onMounted(() => { if (IN_COMPANION_APP) return @@ -247,9 +250,13 @@ function maybeShow() { } // Manual open (App Store banner etc.) — ignores the once-per-browser gate. +// The trigger itself is already a no-op inside the companion (useCompanionIntro), +// and this watcher refuses to open there too, so no caller can ever pop the +// install pitch inside the app it installs (#61). watch(companionIntroRequested, (requested) => { if (!requested) return companionIntroRequested.value = false + if (IN_COMPANION_APP) return if (calmTicker) { clearInterval(calmTicker) calmTicker = null diff --git a/neode-ui/src/composables/__tests__/useCompanionIntro.test.ts b/neode-ui/src/composables/__tests__/useCompanionIntro.test.ts new file mode 100644 index 00000000..4f814df6 --- /dev/null +++ b/neode-ui/src/composables/__tests__/useCompanionIntro.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, afterEach, beforeEach } from 'vitest' +import { companionIntroRequested, openCompanionIntro } from '../useCompanionIntro' + +// #61: the manual intro trigger (App Store banner etc.) must be a no-op inside +// the companion app's WebView — the "install the companion" pitch is nonsense +// where the user is already running it. The auto-popup was already gated +// (CompanionIntroOverlay.onMounted); openCompanionIntro is the second, manual +// path and the banner render (CompanionBanner) the third. + +type TestWindow = Window & { ArchipelagoNative?: unknown } +const w = window as TestWindow + +beforeEach(() => { + companionIntroRequested.value = false +}) + +afterEach(() => { + delete w.ArchipelagoNative +}) + +describe('openCompanionIntro', () => { + it('raises the manual intro request in a plain browser/PWA', () => { + expect(companionIntroRequested.value).toBe(false) + openCompanionIntro() + expect(companionIntroRequested.value).toBe(true) + }) + + it('is a no-op inside the companion app (bridge with openInApp)', () => { + w.ArchipelagoNative = { openInApp: () => {}, openExternal: () => {} } + openCompanionIntro() + expect(companionIntroRequested.value).toBe(false) + }) + + it('still fires when the bridge exists but is not the companion shell', () => { + // Partial bridge (no openInApp) is not the companion app — a future + // embedder must still see the pitch. + w.ArchipelagoNative = { openExternal: () => {} } + openCompanionIntro() + expect(companionIntroRequested.value).toBe(true) + }) +}) diff --git a/neode-ui/src/composables/useCompanionIntro.ts b/neode-ui/src/composables/useCompanionIntro.ts index b05867c0..1998a8b2 100644 --- a/neode-ui/src/composables/useCompanionIntro.ts +++ b/neode-ui/src/composables/useCompanionIntro.ts @@ -1,13 +1,20 @@ import { ref } from 'vue' +import { isCompanionApp } from '@/utils/openExternal' + /** * Cross-view trigger for the Remote Companion intro/pairing modal * (CompanionIntroOverlay, mounted once in Dashboard.vue). Views like the * App Store banner call openCompanionIntro() to pop it on demand — this * bypasses the once-per-browser auto-show gate. + * + * Inside the companion app's own WebView the whole pitch is nonsense — the + * user is already running it — so the trigger is a no-op there (#61: the + * auto-popup was gated, this manual path and CompanionBanner weren't). */ export const companionIntroRequested = ref(false) export function openCompanionIntro(): void { + if (isCompanionApp()) return companionIntroRequested.value = true } diff --git a/neode-ui/src/views/discover/CompanionBanner.vue b/neode-ui/src/views/discover/CompanionBanner.vue index 75f7b4aa..86a5defd 100644 --- a/neode-ui/src/views/discover/CompanionBanner.vue +++ b/neode-ui/src/views/discover/CompanionBanner.vue @@ -1,8 +1,11 @@