diff --git a/packages/proton-browser-transport/src/transport.ts b/packages/proton-browser-transport/src/transport.ts index bdf11cd..5468bfd 100644 --- a/packages/proton-browser-transport/src/transport.ts +++ b/packages/proton-browser-transport/src/transport.ts @@ -8,7 +8,7 @@ import type { SigningRequest, } from '@proton/link' import {Storage} from './storage' -import {generateReturnUrl, isMobile, parseErrorMessage} from './utils' +import {closeDuplicateReturnTab, generateReturnUrl, isMobile, parseErrorMessage} from './utils' import {type BrowserTransportOptions, SkipToManual} from './types' import GenerateQrCode from './qrcode' import {WebRenderer} from '@proton/web-renderer' @@ -22,6 +22,7 @@ export class BrowserTransport implements LinkTransport { private requestStatus: boolean private requestAccount: string private walletType: string + private returnUrl?: string | (() => string) private activeRequest?: SigningRequest // eslint-disable-next-line no-unused-vars private activeCancel?: (reason: string | Error) => void @@ -33,8 +34,11 @@ export class BrowserTransport implements LinkTransport { this.requestAccount = options.requestAccount || '' this.walletType = options.walletType || 'proton' this.storage = new Storage(options.storagePrefix || 'proton-link') + this.returnUrl = options.returnUrl this.showingManual = false + closeDuplicateReturnTab() + if (options.ui) { this.ui = options.ui } else { @@ -46,13 +50,20 @@ export class BrowserTransport implements LinkTransport { this.ui?.showLoading() } + private getReturnUrl(): string { + if (typeof this.returnUrl === 'function') { + return this.returnUrl() + } + return this.returnUrl || generateReturnUrl() + } + public onSessionRequest( session: LinkSession, request: SigningRequest, cancel: (_reason: string | Error) => void ) { if (session.metadata.sameDevice) { - request.setInfoKey('return_path', generateReturnUrl()) + request.setInfoKey('return_path', this.getReturnUrl()) } if (session.type === 'fallback') { @@ -232,7 +243,7 @@ export class BrowserTransport implements LinkTransport { } ) { const sameDeviceRequest = request.clone() - const returnUrl = generateReturnUrl() + const returnUrl = this.getReturnUrl() sameDeviceRequest.setInfoKey('same_device', true) sameDeviceRequest.setInfoKey('return_path', returnUrl) diff --git a/packages/proton-browser-transport/src/types.ts b/packages/proton-browser-transport/src/types.ts index caa12d2..6202b64 100644 --- a/packages/proton-browser-transport/src/types.ts +++ b/packages/proton-browser-transport/src/types.ts @@ -11,6 +11,11 @@ export interface BrowserTransportOptions { walletType?: string /** Local storage prefix, defaults to `proton-link`. */ storagePrefix?: string + /** + * Return url (or factory) set as `return_path` on same-device requests, + * overriding the per-user-agent default from `generateReturnUrl()`. + */ + returnUrl?: string | (() => string) ui?: UIRenderer } diff --git a/packages/proton-browser-transport/src/utils.ts b/packages/proton-browser-transport/src/utils.ts index b3bbd81..5ebddd5 100644 --- a/packages/proton-browser-transport/src/utils.ts +++ b/packages/proton-browser-transport/src/utils.ts @@ -53,7 +53,8 @@ export function isAndroidWebView() { } export function isMobile() { - return typeof window.orientation !== 'undefined' || navigator.userAgent.indexOf('IEMobile') !== -1 + // window.orientation is removed in modern Chrome for Android, rely on UA-based checks + return isAndroid() || isAppleHandheld() || navigator.userAgent.indexOf('IEMobile') !== -1 } /** Generate a return url that Proton will redirect back to w/o reload. */ @@ -100,13 +101,32 @@ export function generateReturnUrl() { return 'android-app://webview' } - if (isAndroid() && isChromeMobile()) { - return 'android-app://com.android.chrome' + // stock Chrome on Android: a bare `android-app://com.android.chrome` intent relaunches + // Chrome on a new tab instead of returning to the originating one, orphaning the pending + // request — fall through to the current URL so the flow completes on the dApp's origin + if (isAndroid()) { + // the wallet's return navigation always opens a NEW Chrome tab (Android cannot focus + // the originating one); tag the URL so the duplicate tab can close itself on load and + // drop the user back on the original tab (see closeDuplicateReturnTab) + return window.location.href.split('#')[0] + RETURN_TAB_FRAGMENT } return window.location.href } +export const RETURN_TAB_FRAGMENT = '#webauth-return' + +/** + * Close this tab if it was spawned by the wallet's return redirect on Android. + * Tabs opened by an external app intent are allowed to close themselves, which + * lands the user back on the originating tab where the pending request completes. + */ +export function closeDuplicateReturnTab() { + if (isAndroid() && window.location.hash.startsWith(RETURN_TAB_FRAGMENT)) { + window.close() + } +} + export function parseErrorMessage(error: any) { let errorMessage: string