Skip to content

Android Chrome: hard-coded android-app:// return path opens a new tab after signing, breaking same-device session completion #58

Description

@SuperstrongBE

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

  1. dApp using @proton/web-sdk v5 with WebAuth, opened in Chrome on Android.
  2. Call ConnectWallet() (or session.transact()), choose WebAuth.
  3. Approve/sign in WebAuth.
  4. Observed: Chrome opens a new tab / reloads; the originating tab never receives the completed flow → dApp stays unauthenticated.
  5. 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)

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions