fix(pwa): keep mobile OAuth consent from being swallowed by the installed app (#701)
## Problem On mobile, starting an OAuth connection from ChatGPT (MCP connector) opens the **installed Whisper Money PWA** and lands on the **dashboard** instead of the OAuth consent screen — so the connection can never be approved. ## Cause The web app manifest declares `scope: "/"`, so the installed PWA claims the entire origin, including `/oauth/authorize`. On Android the OAuth link gets captured into the app, and the default launch behavior just **focuses the already-open window (the dashboard) and drops the incoming URL** — the consent page never loads. Narrowing `scope` to exclude `/oauth` isn't viable: the app's routes are flat (`/dashboard`, `/transactions`, `/accounts`, …) and share no prefix, so a narrower scope would push half the app out to the browser. ## Fix - `site.webmanifest`: add `launch_handler.client_mode: "focus-existing"` so the launcher hands the app the captured target URL via the launch queue. - `app.tsx`: a `launchQueue` consumer that navigates to the target URL when it's an `/oauth/` path. Every other launch is a no-op (keeps its place). Low blast radius — additive manifest field + a guard scoped to `/oauth/` paths; existing (desktop) OAuth is unaffected. ## Verification Cannot be tested off-device. After deploy, on the phone: 1. **Reinstall the PWA** (remove from home screen, re-add) so Android re-fetches the manifest. 2. Retry the ChatGPT OAuth connect → should land on the "Connect ChatGPT to Whisper Money" consent screen. Depends on `launch_handler`/`launchQueue` (Chrome/Edge 102+, covers nearly all Android).
This commit is contained in:
parent
4c83cb8b33
commit
2cca291773
|
|
@ -6,6 +6,9 @@
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"id": "/",
|
"id": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
|
"launch_handler": {
|
||||||
|
"client_mode": "focus-existing"
|
||||||
|
},
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"theme_color": "#1b1b18",
|
"theme_color": "#1b1b18",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,38 @@ import { __, setTranslations } from './utils/i18n';
|
||||||
|
|
||||||
installChunkLoadRecovery();
|
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({
|
Sentry.init({
|
||||||
dsn: import.meta.env.SENTRY_LARAVEL_DSN,
|
dsn: import.meta.env.SENTRY_LARAVEL_DSN,
|
||||||
environment: import.meta.env.MODE,
|
environment: import.meta.env.MODE,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue