archy/neode-ui/src/utils/openExternal.ts
archipelago cc2c06c6dc
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s
fix(neode-ui): companion app opens every app in the native WebView, never an iframe
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>
2026-07-22 04:54:20 -04:00

58 lines
2.0 KiB
TypeScript

/**
* Open a URL in the device's real browser.
*
* In a normal mobile/desktop browser this is just `window.open(_blank)`. Inside
* the Android companion app the page runs in a WebView where `window.open` is
* unreliable (noopener/noreferrer can suppress onCreateWindow), so the native
* shell injects a `window.ArchipelagoNative.openExternal(url)` bridge that hands
* the URL to an ACTION_VIEW intent. We prefer the bridge when present and fall
* back to `window.open` otherwise — so the working mobile-browser path is
* untouched.
*/
interface ArchipelagoNativeBridge {
openExternal?: (url: string) => void
openInApp?: (url: string) => void
}
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()
if (native && typeof native.openExternal === 'function') {
native.openExternal(url)
return
}
window.open(url, '_blank', 'noopener,noreferrer')
}
/**
* Launch an app that can't be embedded in an iframe (X-Frame-Options) from a
* mobile surface — with NO "this app opens in a tab" interstitial.
*
* - Android companion: hand it to the in-app WebView (`openInApp`) so it stays
* inside Archipelago with the native back/forward/reload/close controls.
* - Plain mobile browser (PWA): open directly in a new browser tab.
*/
export function openInAppOrNewTab(url: string): void {
if (!url) return
const native = nativeBridge()
if (native && typeof native.openInApp === 'function') {
native.openInApp(url)
return
}
window.open(url, '_blank', 'noopener,noreferrer')
}