fix(pwa): keep mobile OAuth consent from being swallowed by the installed app

The web app manifest declares scope "/", so the installed PWA claims the whole
origin including /oauth/authorize. On Android an OAuth link (e.g. ChatGPT
connecting over MCP) is captured into the app, and the default launch behavior
focuses the already-open window (the dashboard) and drops the incoming URL, so
the consent screen never loads.

Set launch_handler.client_mode to "focus-existing" and add a launchQueue
consumer that navigates to the captured target URL when it is an /oauth/ path.
Non-OAuth launches keep their place. Scope can't simply exclude /oauth because
the app's routes are flat and share no prefix.
This commit is contained in:
Víctor Falcón 2026-07-18 19:30:36 +02:00
parent 89174af4e6
commit d3fd824a6a
2 changed files with 35 additions and 0 deletions

View File

@ -6,6 +6,9 @@
"scope": "/",
"id": "/",
"display": "standalone",
"launch_handler": {
"client_mode": "focus-existing"
},
"orientation": "portrait-primary",
"theme_color": "#1b1b18",
"background_color": "#ffffff",

View File

@ -34,6 +34,38 @@ import { __, setTranslations } from './utils/i18n';
installChunkLoadRecovery();
// The installed PWA claims the whole origin (manifest scope "/"), so on mobile an
// OAuth link (e.g. from ChatGPT connecting over MCP) gets captured into the app.
// Paired with launch_handler "focus-existing", the launcher hands us the target
// URL instead of dropping it and showing the dashboard — navigate there so the
// OAuth consent screen actually loads. Non-OAuth launches keep their place.
type LaunchParams = { targetURL?: string };
const launchQueue = (
window as Window & {
launchQueue?: {
setConsumer(consumer: (params: LaunchParams) => void): void;
};
}
).launchQueue;
if (launchQueue) {
launchQueue.setConsumer(({ targetURL }) => {
if (!targetURL) {
return;
}
const target = new URL(targetURL);
if (
target.pathname.startsWith('/oauth/') &&
target.href !== window.location.href
) {
window.location.href = targetURL;
}
});
}
Sentry.init({
dsn: import.meta.env.SENTRY_LARAVEL_DSN,
environment: import.meta.env.MODE,