tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d0be97b528636836e4d9b1b9250cf085de751e85
parent 7ee252421988d7b3a38dc650986a3b4c3321a823
Author: Benjamin VanderSloot <bvandersloot@mozilla.com>
Date:   Tue, 28 Oct 2025 20:29:18 +0000

Bug 1989268 - Elevate the permission dialog for wallet custom schemes on Android - r=android-reviewers,manuel,Roger

Differential Revision: https://phabricator.services.mozilla.com/D265265

Diffstat:
Mmobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt | 26++++++++++++++++++++++----
Mmobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt | 2+-
2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt b/mobile/android/android-components/components/feature/app-links/src/main/java/mozilla/components/feature/app/links/AppLinksFeature.kt @@ -30,6 +30,11 @@ import mozilla.components.support.ktx.android.content.appName // Minimum time for dialog to settle before accepting user interactions. internal const val MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT: Int = 500 // 0.5 seconds +internal val WALLET_SCHEMES: Array<String> = arrayOf( + "openid4vp", "mdoc", "mdoc-openid4vp", "haip", + "eudi-wallet", "eudi-openid4vp", "openid-credential-offer", +) + /** * This feature implements observer for handling redirects to external apps. The users are asked to * confirm their intention before leaving the app if in private session. These include the Android @@ -112,10 +117,11 @@ class AppLinksFeature( if (appIntent == null) return val isPrivate = sessionState.content.private + val isWallet = isWalletLink(url, appIntent) val isAuthenticationFlow = AppLinksInterceptor.isAuthentication(sessionState, appIntent.component?.packageName) - if (shouldBypassPrompt(isPrivate, isAuthenticationFlow, fragmentManager)) { + if (shouldBypassPrompt(isPrivate, isWallet, isAuthenticationFlow, fragmentManager)) { openApp(appIntent) return } @@ -131,6 +137,7 @@ class AppLinksFeature( appIntent = appIntent, appName = appName, isPrivate = isPrivate, + isWallet = isWallet, fragmentManager = fragmentManager, ) } @@ -138,10 +145,11 @@ class AppLinksFeature( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun shouldBypassPrompt( isPrivate: Boolean, + isWallet: Boolean, isAuthenticationFlow: Boolean, fragmentManager: FragmentManager?, ): Boolean { - val shouldShowPrompt = isPrivate || shouldPrompt() + val shouldShowPrompt = isPrivate || isWallet || shouldPrompt() return fragmentManager == null || !shouldShowPrompt || isAuthenticationFlow } @@ -175,6 +183,7 @@ class AppLinksFeature( ) } + @Suppress("LongParameterList") private fun showRedirectDialog( sessionState: SessionState, url: String, @@ -182,13 +191,14 @@ class AppLinksFeature( appIntent: Intent, appName: String?, isPrivate: Boolean, + isWallet: Boolean, fragmentManager: FragmentManager?, ) { if (fragmentManager == null) { return } - getOrCreateDialog(isPrivate, url, appName).apply { + getOrCreateDialog(isPrivate, isWallet, url, appName).apply { onConfirmRedirect = { isCheckboxTicked -> if (isCheckboxTicked) { alwaysOpenCheckboxAction?.invoke() @@ -204,6 +214,7 @@ class AppLinksFeature( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun getOrCreateDialog( isPrivate: Boolean, + isWallet: Boolean, url: String, targetAppName: String?, ): RedirectDialogFragment { @@ -241,7 +252,7 @@ class AppLinksFeature( return SimpleRedirectDialogFragment.newInstance( dialogTitleString = dialogTitle, dialogMessageString = dialogMessage, - showCheckbox = if (isPrivate) false else alwaysOpenCheckboxAction != null, + showCheckbox = if (isPrivate || isWallet) false else alwaysOpenCheckboxAction != null, maxSuccessiveDialogMillisLimit = MAX_SUCCESSIVE_DIALOG_MILLIS_LIMIT, ) } @@ -258,4 +269,11 @@ class AppLinksFeature( private fun findPreviousDialogFragment(): RedirectDialogFragment? { return fragmentManager?.findFragmentByTag(FRAGMENT_TAG) as? RedirectDialogFragment } + + private fun isWalletLink(url: String, appIntent: Intent?): Boolean { + val urlScheme = url.toUri().scheme?.lowercase() + val intentScheme = appIntent?.data?.scheme?.lowercase() + return (urlScheme != null && WALLET_SCHEMES.contains(urlScheme)) || + (intentScheme != null && WALLET_SCHEMES.contains(intentScheme)) + } } diff --git a/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt b/mobile/android/android-components/components/feature/app-links/src/test/java/mozilla/components/feature/app/links/AppLinksFeatureTest.kt @@ -382,7 +382,7 @@ class AppLinksFeatureTest { verify(mockDialog).showNow(eq(mockFragmentManager), anyString()) - doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, "", null) + doReturn(mockDialog).`when`(feature).getOrCreateDialog(false, false, "", null) doReturn(mockDialog).`when`(mockFragmentManager).findFragmentByTag(RedirectDialogFragment.FRAGMENT_TAG) feature.handleAppIntent(tab, intentUrl, mock(), null, null) verify(mockDialog, times(1)).showNow(mockFragmentManager, RedirectDialogFragment.FRAGMENT_TAG)