Package: @proton/browser-transport (via @proton/web-sdk) — reproduced on 5.0.0, code path unchanged on 5.1.0-rc-3
Summary
On Android Chrome, after the user signs in WebAuth, the browser is relaunched on a fresh tab (or reloads) instead of returning to the originating tab. The originating tab holds the pending ConnectWallet() / transact() promise, so from the user's point of view login never completes — they sign, land on a new/blank page, and the dApp is still unauthenticated.
Root cause
generateReturnUrl() in browser-transport hard-codes the return path per user-agent. For Android Chrome it returns a bare app scheme:
if (isAndroid() && isChromeMobile()) {
return 'android-app://com.android.chrome';
}
This value is set as the return_path info key on the request (onSessionRequest and the same-device transact path). After signing, WebAuth navigates to android-app://com.android.chrome. Because the intent targets Chrome by package name with no target URL, Android launches Chrome fresh — a new tab / the new-tab page — rather than foregrounding the existing tab that initiated the request.
Contrast with the iOS Safari branch, which returns location.href + '#' + — a fragment-only change on the same URL, so Safari returns to the same tab without reload. The Android branches have no equivalent "same-tab" guarantee.
Why it fully breaks completion
The session/signature is created and delivered via the callback channel, but the dApp's awaiting promise lives in the original tab. When Android opens a different tab (and freezes the original, backgrounded tab's JS), the awaiting flow is orphaned. There is no configuration lever to change this: the transport constructor reads only requestAccount and walletType — return_path is always the hard-coded generateReturnUrl() output, never sourced from options.
Secondary inconsistency worth flagging: isMobile() (used to auto-trigger the deep link) relies on typeof window.orientation !== 'undefined', which is removed in modern Chrome for Android, so isMobile() returns false there — while isAndroid()/isChromeMobile() (UA-based) still return true. The two detectors disagree on the exact platform that's failing, so the return path is emitted but the same-device trigger logic doesn't treat the session as mobile.
Steps to reproduce
- dApp using @proton/web-sdk v5 with WebAuth, opened in Chrome on Android.
- Call ConnectWallet() (or session.transact()), choose WebAuth.
- Approve/sign in WebAuth.
- Observed: Chrome opens a new tab / reloads; the originating tab never receives the completed flow → dApp stays unauthenticated.
- Expected: return to the originating tab (as on iOS Safari) so the pending promise resolves.
Environment
- @proton/web-sdk, @proton/browser-transport, @proton/link — 5.0.0
- Android, Chrome (stock, mobile UA)
Suggested fixes (any one)
- Make return_path configurable via transport options (e.g. returnUrl), so apps can pass their real window.location.href and let the app-side session-restore complete the flow.
- Android same-tab return: fall back to a same-URL return (like the iOS fragment trick) instead of android-app://, since the bare package intent spawns a new tab.
- Fix the isMobile() detector to not depend on the removed window.orientation (use the UA-based isAndroid()/isAppleHandheld() signals consistently).
Workaround (app-side, no SDK change)
Persist the session with a stable storagePrefix and, on mount, silently restore it (restoreSession: true, no popup). This completes login regardless of which tab Android lands on.
Package: @proton/browser-transport (via @proton/web-sdk) — reproduced on 5.0.0, code path unchanged on 5.1.0-rc-3
Summary
On Android Chrome, after the user signs in WebAuth, the browser is relaunched on a fresh tab (or reloads) instead of returning to the originating tab. The originating tab holds the pending ConnectWallet() / transact() promise, so from the user's point of view login never completes — they sign, land on a new/blank page, and the dApp is still unauthenticated.
Root cause
generateReturnUrl() in browser-transport hard-codes the return path per user-agent. For Android Chrome it returns a bare app scheme:
if (isAndroid() && isChromeMobile()) {
return 'android-app://com.android.chrome';
}
This value is set as the return_path info key on the request (onSessionRequest and the same-device transact path). After signing, WebAuth navigates to android-app://com.android.chrome. Because the intent targets Chrome by package name with no target URL, Android launches Chrome fresh — a new tab / the new-tab page — rather than foregrounding the existing tab that initiated the request.
Contrast with the iOS Safari branch, which returns location.href + '#' + — a fragment-only change on the same URL, so Safari returns to the same tab without reload. The Android branches have no equivalent "same-tab" guarantee.
Why it fully breaks completion
The session/signature is created and delivered via the callback channel, but the dApp's awaiting promise lives in the original tab. When Android opens a different tab (and freezes the original, backgrounded tab's JS), the awaiting flow is orphaned. There is no configuration lever to change this: the transport constructor reads only requestAccount and walletType — return_path is always the hard-coded generateReturnUrl() output, never sourced from options.
Secondary inconsistency worth flagging: isMobile() (used to auto-trigger the deep link) relies on typeof window.orientation !== 'undefined', which is removed in modern Chrome for Android, so isMobile() returns false there — while isAndroid()/isChromeMobile() (UA-based) still return true. The two detectors disagree on the exact platform that's failing, so the return path is emitted but the same-device trigger logic doesn't treat the session as mobile.
Steps to reproduce
Environment
Suggested fixes (any one)
Workaround (app-side, no SDK change)
Persist the session with a stable storagePrefix and, on mount, silently restore it (restoreSession: true, no popup). This completes login regardless of which tab Android lands on.