fix(neode-ui): companion app opens every app in the native WebView, never an iframe
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s
Regression: only X-Frame-Options apps were routed to the companion's in-app WebView; iframeable apps fell through to the iframe session, which is slower on the phone and loses the native back/forward/reload controls. Now any launch inside the companion (ArchipelagoNative.openInApp bridge present) goes to the WebView — both openSession and the legacy open() overlay fallback. Mobile web/PWA keeps the iframe session. With regression tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
97fbaf8818
commit
cc2c06c6dc
@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
// vi.hoisted runs before vi.mock hoisting
|
||||
@ -43,6 +43,40 @@ describe('useAppLauncherStore', () => {
|
||||
expect(store.title).toBe('')
|
||||
})
|
||||
|
||||
describe('companion app (native bridge)', () => {
|
||||
const openInApp = vi.fn()
|
||||
beforeEach(() => {
|
||||
;(window as any).ArchipelagoNative = { openInApp, openExternal: vi.fn() }
|
||||
openInApp.mockClear()
|
||||
})
|
||||
afterEach(() => {
|
||||
delete (window as any).ArchipelagoNative
|
||||
})
|
||||
|
||||
it('openSession hands iframeable apps to the native WebView, never the iframe session', () => {
|
||||
const store = useAppLauncherStore()
|
||||
store.openSession('filebrowser')
|
||||
expect(openInApp).toHaveBeenCalledWith(expect.stringContaining(':8083'))
|
||||
expect(store.panelAppId).toBeNull()
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(mockWindowOpen).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('openSession carries deep-link paths into the WebView URL', () => {
|
||||
const store = useAppLauncherStore()
|
||||
store.openSession('mempool', { path: '/tx/abc123' })
|
||||
expect(openInApp).toHaveBeenCalledWith(expect.stringContaining('/tx/abc123'))
|
||||
expect(store.panelAppId).toBeNull()
|
||||
})
|
||||
|
||||
it('open() never falls through to the iframe overlay', () => {
|
||||
const store = useAppLauncherStore()
|
||||
store.open({ url: 'http://192.168.1.228:9999', title: 'Unknown app' })
|
||||
expect(openInApp).toHaveBeenCalledWith('http://192.168.1.228:9999')
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('routes known port apps to full-page session', () => {
|
||||
const store = useAppLauncherStore()
|
||||
|
||||
|
||||
@ -3,7 +3,9 @@ import { ref, watch } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { recordAppLaunch } from '@/utils/appUsage'
|
||||
import { requestExternalOpen } from '@/api/remote-relay'
|
||||
import { openInAppOrNewTab } from '@/utils/openExternal'
|
||||
import { openInAppOrNewTab, isCompanionApp } from '@/utils/openExternal'
|
||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { IS_DEMO, isDemoApp, isDemoExternal, demoAppUrl } from '@/composables/useDemoIntro'
|
||||
|
||||
/**
|
||||
@ -215,6 +217,19 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
recordAppLaunch(appId)
|
||||
const mobile = isMobileViewport()
|
||||
|
||||
// Companion app: EVERY app opens in the native in-app WebView — never an
|
||||
// iframe. The WebView is more performant on the phone and carries the
|
||||
// native back/forward/reload/close controls. Plain mobile browsers (PWA)
|
||||
// keep the iframe session below.
|
||||
if (!IS_DEMO && isCompanionApp()) {
|
||||
const runtimeUrl = useAppStore().data?.['package-data']?.[appId]?.installed?.['interface-addresses']?.main?.['lan-address'] || undefined
|
||||
const launchUrl = directAppUrl(appId) || resolveAppUrl(appId, opts.path, runtimeUrl)
|
||||
if (launchUrl) {
|
||||
openInAppOrNewTab(launchUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Demo: apps backed by a real external site that blocks iframing open
|
||||
// externally; everything else demoable renders in the in-app session.
|
||||
if (IS_DEMO && isDemoExternal(appId)) {
|
||||
@ -309,6 +324,13 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Companion app: never fall through to the iframe overlay — hand the URL
|
||||
// to the native in-app WebView instead (see openSession).
|
||||
if (!IS_DEMO && isCompanionApp()) {
|
||||
openInAppOrNewTab(launchUrl)
|
||||
return
|
||||
}
|
||||
|
||||
previousActiveElement = (document.activeElement as HTMLElement) || null
|
||||
url.value = launchUrl
|
||||
title.value = payload.title
|
||||
|
||||
@ -18,6 +18,16 @@ function nativeBridge(): ArchipelagoNativeBridge | undefined {
|
||||
return (window as unknown as { ArchipelagoNative?: ArchipelagoNativeBridge }).ArchipelagoNative
|
||||
}
|
||||
|
||||
/**
|
||||
* True when running inside the Android companion app (native WebView shell).
|
||||
* The shell injects `window.ArchipelagoNative`; a plain mobile browser / PWA
|
||||
* never has it.
|
||||
*/
|
||||
export function isCompanionApp(): boolean {
|
||||
const native = nativeBridge()
|
||||
return !!native && typeof native.openInApp === 'function'
|
||||
}
|
||||
|
||||
export function openExternalUrl(url: string): void {
|
||||
if (!url) return
|
||||
const native = nativeBridge()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user