diff --git a/.github/workflows/e2e-nightly-autofill.yml b/.github/workflows/e2e-nightly-autofill.yml index ba67fc39b604..63ac9b12d3d5 100644 --- a/.github/workflows/e2e-nightly-autofill.yml +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -60,7 +60,7 @@ jobs: api-key: ${{ secrets.MOBILE_DEV_API_KEY }} name: ${{ github.sha }} app-file: apk/release.apk - android-api-level: 30 + android-api-level: 33 workspace: .maestro include-tags: autofillNoAuthTests diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index b03d717baead..340bc7de12c9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -174,6 +174,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillCapabilityChecker +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor @@ -3790,51 +3791,14 @@ class BrowserTabViewModelTest { assertTrue(browserViewState().isEmailSignedIn) } - @Test - fun whenEmailSignOutEventThenEmailSignEventCommandSent() = runTest { - emailStateFlow.emit(true) - emailStateFlow.emit(false) - - assertCommandIssuedTimes(2) - } - - @Test - fun whenEmailIsSignedInThenEmailSignEventCommandSent() = runTest { - emailStateFlow.emit(true) - - assertCommandIssued() - } - - @Test - fun whenConsumeAliasThenInjectAddressCommandSent() { - whenever(mockEmailManager.getAlias()).thenReturn("alias") - - testee.usePrivateDuckAddress("", "alias") - - assertCommandIssued { - assertEquals("alias", this.duckAddress) - } - } - - @Test - fun whenUseAddressThenInjectAddressCommandSent() { - whenever(mockEmailManager.getEmailAddress()).thenReturn("address") - - testee.usePersonalDuckAddress("", "address") - - assertCommandIssued { - assertEquals("address", this.duckAddress) - } - } - @Test fun whenShowEmailTooltipIfAddressExistsThenShowEmailTooltipCommandSent() { whenever(mockEmailManager.getEmailAddress()).thenReturn("address") - testee.showEmailProtectionChooseEmailPrompt() + testee.showEmailProtectionChooseEmailPrompt(urlRequest()) assertCommandIssued { - assertEquals("address", this.address) + assertEquals("address", this.duckAddress) } } @@ -3842,7 +3806,7 @@ class BrowserTabViewModelTest { fun whenShowEmailTooltipIfAddressDoesNotExistThenCommandNotSent() { whenever(mockEmailManager.getEmailAddress()).thenReturn(null) - testee.showEmailProtectionChooseEmailPrompt() + testee.showEmailProtectionChooseEmailPrompt(urlRequest()) assertCommandNotIssued() } @@ -4443,16 +4407,6 @@ class BrowserTabViewModelTest { assertShowHistoryCommandSent(expectedStackSize = 10) } - @Test - fun whenReturnNoCredentialsWithPageThenEmitCancelIncomingAutofillRequestCommand() = runTest { - val url = "originalurl.com" - testee.returnNoCredentialsWithPage(url) - - assertCommandIssued { - assertEquals(url, this.url) - } - } - @Test fun whenOnAutoconsentResultReceivedThenSiteUpdated() { updateUrl("http://www.example.com/", "http://twitter.com/explore", true) @@ -6039,6 +5993,8 @@ class BrowserTabViewModelTest { } } + private fun urlRequest() = AutofillWebMessageRequest("", "", "") + private fun givenLoginDetected(domain: String) = LoginDetected(authLoginDomain = "", forwardedToDomain = domain) private fun givenCurrentSite(domain: String): Site { @@ -6191,10 +6147,6 @@ class BrowserTabViewModelTest { fun anyUri(): Uri = any() class FakeCapabilityChecker(var enabled: Boolean) : AutofillCapabilityChecker { - override suspend fun isAutofillEnabledByConfiguration(url: String) = enabled - override suspend fun canInjectCredentialsToWebView(url: String) = enabled - override suspend fun canSaveCredentialsFromWebView(url: String) = enabled - override suspend fun canGeneratePasswordFromWebView(url: String) = enabled - override suspend fun canAccessCredentialManagementScreen() = enabled + override suspend fun canAccessCredentialManagementScreen(): Boolean = enabled } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt index 25fbe9a65401..d4747c0d2f06 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -61,7 +61,6 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT import com.duckduckgo.autoconsent.api.Autoconsent -import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.browser.api.WebViewVersionProvider @@ -118,7 +117,6 @@ class BrowserWebViewClientTest { private val trustedCertificateStore: TrustedCertificateStore = mock() private val webViewHttpAuthStore: WebViewHttpAuthStore = mock() private val thirdPartyCookieManager: ThirdPartyCookieManager = mock() - private val browserAutofillConfigurator: BrowserAutofill.Configurator = mock() private val webResourceRequest: WebResourceRequest = mock() private val webResourceError: WebResourceError = mock() private val ampLinks: AmpLinks = mock() @@ -156,7 +154,6 @@ class BrowserWebViewClientTest { thirdPartyCookieManager, TestScope(), coroutinesTestRule.testDispatcherProvider, - browserAutofillConfigurator, ampLinks, printInjector, internalTestUserChecker, @@ -370,13 +367,6 @@ class BrowserWebViewClientTest { verify(cookieManager).flush() } - @UiThreadTest - @Test - fun whenOnPageStartedCalledThenInjectEmailAutofillJsCalled() { - testee.onPageStarted(webView, null, null) - verify(browserAutofillConfigurator).configureAutofillForCurrentPage(webView, null) - } - @UiThreadTest @Test fun whenShouldOverrideThrowsExceptionThenRecordException() { diff --git a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt deleted file mode 100644 index 2a2cc540789b..000000000000 --- a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.email - -import android.webkit.WebView -import androidx.test.annotation.UiThreadTest -import androidx.test.filters.SdkSuppress -import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.app.autofill.DefaultEmailProtectionJavascriptInjector -import com.duckduckgo.app.autofill.EmailProtectionJavascriptInjector -import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl -import com.duckduckgo.app.browser.R -import com.duckduckgo.autofill.api.Autofill -import com.duckduckgo.autofill.api.AutofillFeature -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory -import com.duckduckgo.feature.toggles.api.Toggle -import java.io.BufferedReader -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.* - -class EmailInjectorJsTest { - - private val mockEmailManager: EmailManager = mock() - private val mockDispatcherProvider: DispatcherProvider = mock() - private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) - private val mockAutofill: Autofill = mock() - private val javascriptInjector: EmailProtectionJavascriptInjector = DefaultEmailProtectionJavascriptInjector() - - lateinit var testee: EmailInjectorJs - - @Before - fun setup() { - testee = - EmailInjectorJs( - mockEmailManager, - DuckDuckGoUrlDetectorImpl(), - mockDispatcherProvider, - autofillFeature, - javascriptInjector, - mockAutofill, - ) - whenever(mockAutofill.isAnException(any())).thenReturn(false) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenInjectAddressThenInjectJsCodeReplacingTheAlias() { - val address = "address" - val jsToEvaluate = getAliasJsToEvaluate().replace("%s", address) - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - autofillFeature.self().setEnabled(Toggle.State(enable = true)) - - testee.injectAddressInEmailField(webView, address, "https://example.com") - - verify(webView).evaluateJavascript(jsToEvaluate, null) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenInjectAddressAndFeatureIsDisabledThenJsCodeNotInjected() { - autofillFeature.self().setEnabled(Toggle.State(enable = true)) - - val address = "address" - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.injectAddressInEmailField(webView, address, "https://example.com") - - verify(webView, never()).evaluateJavascript(any(), any()) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenInjectAddressAndUrlIsAnExceptionThenJsCodeNotInjected() { - whenever(mockAutofill.isAnException(any())).thenReturn(true) - - val address = "address" - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.injectAddressInEmailField(webView, address, "https://example.com") - - verify(webView, never()).evaluateJavascript(any(), any()) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsSignedInThenDoNotEvaluateJsCode() { - whenever(mockEmailManager.isSignedIn()).thenReturn(true) - val jsToEvaluate = getNotifySignOutJsToEvaluate() - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.notifyWebAppSignEvent(webView, "https://example.com") - - verify(webView, never()).evaluateJavascript(jsToEvaluate, null) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { - whenever(mockEmailManager.isSignedIn()).thenReturn(false) - val jsToEvaluate = getNotifySignOutJsToEvaluate() - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.notifyWebAppSignEvent(webView, "https://example.com") - - verify(webView, never()).evaluateJavascript(jsToEvaluate, null) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsDisabledAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { - whenever(mockEmailManager.isSignedIn()).thenReturn(false) - autofillFeature.self().setEnabled(Toggle.State(enable = false)) - - val jsToEvaluate = getNotifySignOutJsToEvaluate() - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.notifyWebAppSignEvent(webView, "https://duckduckgo.com/email") - - verify(webView, never()).evaluateJavascript(jsToEvaluate, null) - } - - @UiThreadTest - @Test - @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsEnabledAndEmailIsNotSignedInThenEvaluateJsCode() { - whenever(mockEmailManager.isSignedIn()).thenReturn(false) - autofillFeature.self().setEnabled(Toggle.State(enable = true)) - - val jsToEvaluate = getNotifySignOutJsToEvaluate() - val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - - testee.notifyWebAppSignEvent(webView, "https://duckduckgo.com/email") - - verify(webView).evaluateJavascript(jsToEvaluate, null) - } - - private fun getAliasJsToEvaluate(): String { - val js = InstrumentationRegistry.getInstrumentation().targetContext.resources.openRawResource(R.raw.inject_alias) - .bufferedReader() - .use { it.readText() } - return "javascript:$js" - } - - private fun getNotifySignOutJsToEvaluate(): String { - val js = - InstrumentationRegistry.getInstrumentation().targetContext.resources.openRawResource(R.raw.signout_autofill) - .bufferedReader() - .use { it.readText() } - return "javascript:$js" - } - - private fun readResource(resourceName: String): BufferedReader? { - return javaClass.classLoader?.getResource(resourceName)?.openStream()?.bufferedReader() - } -} diff --git a/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt b/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt deleted file mode 100644 index d402b019a509..000000000000 --- a/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.autofill - -import android.content.Context -import com.duckduckgo.app.browser.R -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import javax.inject.Inject - -@SingleInstanceIn(AppScope::class) -@ContributesBinding(AppScope::class) -class DefaultEmailProtectionJavascriptInjector @Inject constructor() : EmailProtectionJavascriptInjector { - private lateinit var aliasFunctions: String - private lateinit var signOutFunctions: String - - override fun getAliasFunctions( - context: Context, - alias: String?, - ): String { - if (!this::aliasFunctions.isInitialized) { - aliasFunctions = context.resources.openRawResource(R.raw.inject_alias).bufferedReader().use { it.readText() } - } - return aliasFunctions.replace("%s", alias.orEmpty()) - } - - override fun getSignOutFunctions( - context: Context, - ): String { - if (!this::signOutFunctions.isInitialized) { - signOutFunctions = context.resources.openRawResource(R.raw.signout_autofill).bufferedReader().use { it.readText() } - } - return signOutFunctions - } -} diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 334cd0e784b1..3981de8bba3f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -333,14 +333,17 @@ open class BrowserActivity : DuckDuckGoActivity() { Toast.makeText(applicationContext, R.string.fireDataCleared, Toast.LENGTH_LONG).show() } - if (emailProtectionLinkVerifier.shouldDelegateToInContextView(intent.intentText, currentTab?.inContextEmailProtectionShowing)) { - currentTab?.showEmailProtectionInContextWebFlow(intent.intentText) + val inContextSignupState = currentTab?.inContextEmailProtectionSignupState + if (emailProtectionLinkVerifier.shouldDelegateToInContextView(intent.intentText, inContextSignupState?.showing)) { + currentTab?.resumeEmailProtectionInContextWebFlow( + verificationUrl = intent.intentText, + messageRequestId = inContextSignupState?.requestId!!, + ) Timber.v("Verification link was consumed, so don't allow it to open in a new tab") return } - // the BrowserActivity will automatically clear its stack of activities when being brought to the foreground, so this can no longer be true - currentTab?.inContextEmailProtectionShowing = false + currentTab?.inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) if (launchNewSearch(intent)) { Timber.w("new tab requested") @@ -740,3 +743,9 @@ private class TabList() : ArrayList() { return super.add(element) } } + +// Needed to keep track of in-context email protection signup state +data class InProgressEmailProtectionSignupState( + val showing: Boolean = false, + val requestId: String? = null, +) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 793f2b9b9883..6c80e0fd6232 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -215,12 +215,12 @@ import com.duckduckgo.app.widget.AddWidgetLauncher import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autoconsent.api.AutoconsentCallback -import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenDirectlyViewCredentialsParams import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenShowSuggestionsForSiteParams import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.Callback import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory @@ -230,9 +230,8 @@ import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpHandleVerificationLink -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenNoParams import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenResult -import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpStartScreen import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.ExactMatch import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch @@ -240,10 +239,9 @@ import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCrede import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UsernameMatch import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.UsernameMissing import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog -import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType -import com.duckduckgo.autofill.api.emailprotection.EmailInjector +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.common.ui.DuckDuckGoActivity @@ -350,8 +348,7 @@ class BrowserTabFragment : TrackersAnimatorListener, DownloadConfirmationDialogListener, SitePermissionsGrantedListener, - AutofillEventListener, - EmailProtectionUserPromptListener { + AutofillEventListener { private val supervisorJob = SupervisorJob() @@ -421,9 +418,6 @@ class BrowserTabFragment : @Inject lateinit var thirdPartyCookieManager: ThirdPartyCookieManager - @Inject - lateinit var emailInjector: EmailInjector - @Inject lateinit var browserAutofill: BrowserAutofill @@ -473,9 +467,6 @@ class BrowserTabFragment : @Inject lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory - @Inject - lateinit var duckAddressInjectedResultHandler: DuckAddressLoginCreator - @Inject lateinit var existingCredentialMatchDetector: ExistingCredentialMatchDetector @@ -488,9 +479,6 @@ class BrowserTabFragment : @Inject lateinit var autoconsent: Autoconsent - @Inject - lateinit var autofillCapabilityChecker: AutofillCapabilityChecker - @Inject lateinit var sitePermissionsDialogLauncher: SitePermissionsDialogLauncher @@ -526,6 +514,9 @@ class BrowserTabFragment : @Inject lateinit var clientBrandHintProvider: ClientBrandHintProvider + @Inject + lateinit var autofillMessagePoster: AutofillMessagePoster + @Inject lateinit var subscriptions: Subscriptions @@ -564,7 +555,7 @@ class BrowserTabFragment : * This is needed because the activity stack will be cleared if an external link is opened in our browser * We need to be able to determine if inContextEmailProtection view was showing. If it was, it will consume email verification links. */ - var inContextEmailProtectionShowing: Boolean = false + var inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState() private var urlExtractingWebView: UrlExtractingWebView? = null @@ -653,13 +644,13 @@ class BrowserTabFragment : private val activityResultHandlerEmailProtectionInContextSignup = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> when (result.resultCode) { EmailProtectionInContextSignUpScreenResult.SUCCESS -> { - browserAutofill.inContextEmailProtectionFlowFinished() - inContextEmailProtectionShowing = false + postEmailProtectionFlowFinishedResult(result.data) + inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) } EmailProtectionInContextSignUpScreenResult.CANCELLED -> { - browserAutofill.inContextEmailProtectionFlowFinished() - inContextEmailProtectionShowing = false + postEmailProtectionFlowFinishedResult(result.data) + inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) } else -> { @@ -669,6 +660,12 @@ class BrowserTabFragment : } } + private fun postEmailProtectionFlowFinishedResult(result: Intent?) { + val requestId = result?.getStringExtra(EmailProtectionInContextSignUpScreenResult.RESULT_KEY_REQUEST_ID) ?: return + val message = result.getStringExtra(EmailProtectionInContextSignUpScreenResult.RESULT_KEY_MESSAGE) ?: return + autofillMessagePoster.postMessage(message, requestId) + } + private val errorSnackbar: Snackbar by lazy { binding.browserLayout.makeSnackbarWithNoBottomInset(R.string.crashedWebViewErrorMessage, Snackbar.LENGTH_INDEFINITE) .setBehavior(NonDismissibleBehavior()) @@ -738,17 +735,17 @@ class BrowserTabFragment : private val autofillCallback = object : Callback { override suspend fun onCredentialsAvailableToInject( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, ) { withContext(dispatchers.main()) { - showAutofillDialogChooseCredentials(originalUrl, credentials, triggerType) + showAutofillDialogChooseCredentials(autofillWebMessageRequest, credentials, triggerType) } } override suspend fun onGeneratedPasswordAvailableToUse( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, ) { @@ -756,20 +753,32 @@ class BrowserTabFragment : delay(KEYBOARD_DELAY) withContext(dispatchers.main()) { - showUserAutoGeneratedPasswordDialog(originalUrl, username, generatedPassword) + showUserAutoGeneratedPasswordDialog(autofillWebMessageRequest, username, generatedPassword) } } - override fun noCredentialsAvailable(originalUrl: String) { - viewModel.returnNoCredentialsWithPage(originalUrl) - } - override fun onCredentialsSaved(savedCredentials: LoginCredentials) { viewModel.onShowUserCredentialsSaved(savedCredentials) } + override fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + viewModel.showEmailProtectionChooseEmailPrompt(autofillWebMessageRequest) + } + + override fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + context?.let { + val url = webView?.url ?: return + + val dialog = credentialAutofillDialogFactory.emailProtectionInContextSignUpDialog( + tabId = tabId, + autofillWebMessageRequest = autofillWebMessageRequest, + ) + showDialogHidingPrevious(dialog, EmailProtectionInContextSignUpDialog.TAG, url) + } + } + override suspend fun onCredentialsAvailableToSave( - currentUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, ) { val username = credentials.username @@ -780,6 +789,8 @@ class BrowserTabFragment : return } + val currentUrl = autofillWebMessageRequest.requestOrigin + val matchType = existingCredentialMatchDetector.determine(currentUrl, username, password) Timber.v("MatchType is %s", matchType.javaClass.simpleName) @@ -789,30 +800,30 @@ class BrowserTabFragment : withContext(dispatchers.main()) { when (matchType) { ExactMatch -> Timber.w("Credentials already exist for %s", currentUrl) - UsernameMatch -> showAutofillDialogUpdatePassword(currentUrl, credentials) - UsernameMissing -> showAutofillDialogUpdateUsername(currentUrl, credentials) - NoMatch -> showAutofillDialogSaveCredentials(currentUrl, credentials) - UrlOnlyMatch -> showAutofillDialogSaveCredentials(currentUrl, credentials) + UsernameMatch -> showAutofillDialogUpdatePassword(autofillWebMessageRequest, credentials) + UsernameMissing -> showAutofillDialogUpdateUsername(autofillWebMessageRequest, credentials) + NoMatch -> showAutofillDialogSaveCredentials(autofillWebMessageRequest, credentials) + UrlOnlyMatch -> showAutofillDialogSaveCredentials(autofillWebMessageRequest, credentials) } } } private fun showUserAutoGeneratedPasswordDialog( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, ) { val url = webView?.url ?: return - if (url != originalUrl) { + if (url != autofillWebMessageRequest.originalPageUrl) { Timber.w("WebView url has changed since autofill request; bailing") return } - val dialog = credentialAutofillDialogFactory.autofillGeneratePasswordDialog(url, username, generatedPassword, tabId) - showDialogHidingPrevious(dialog, UseGeneratedPasswordDialog.TAG, originalUrl) + val dialog = credentialAutofillDialogFactory.autofillGeneratePasswordDialog(autofillWebMessageRequest, username, generatedPassword, tabId) + showDialogHidingPrevious(dialog, UseGeneratedPasswordDialog.TAG, autofillWebMessageRequest.originalPageUrl) } private fun showAutofillDialogChooseCredentials( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, ) { @@ -821,12 +832,12 @@ class BrowserTabFragment : return } val url = webView?.url ?: return - if (url != originalUrl) { + if (url != autofillWebMessageRequest.originalPageUrl) { Timber.w("WebView url has changed since autofill request; bailing") return } - val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(url, credentials, triggerType, tabId) - showDialogHidingPrevious(dialog, CredentialAutofillPickerDialog.TAG, originalUrl) + val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(autofillWebMessageRequest, credentials, triggerType, tabId) + showDialogHidingPrevious(dialog, CredentialAutofillPickerDialog.TAG, autofillWebMessageRequest.originalPageUrl) } } @@ -1342,36 +1353,8 @@ class BrowserTabFragment : viewModel.onRefreshRequested(triggeredByUser = false) } - override fun onRejectGeneratedPassword(originalUrl: String) { - rejectGeneratedPassword(originalUrl) - } - - override fun onAcceptGeneratedPassword(originalUrl: String) { - acceptGeneratedPassword(originalUrl) - } - - override fun onUseEmailProtectionPrivateAlias( - originalUrl: String, - duckAddress: String, - ) { - viewModel.usePrivateDuckAddress(originalUrl, duckAddress) - } - - override fun onUseEmailProtectionPersonalAddress( - originalUrl: String, - duckAddress: String, - ) { - viewModel.usePersonalDuckAddress(originalUrl, duckAddress) - } - - override fun onSelectedToSignUpForInContextEmailProtection() { - showEmailProtectionInContextWebFlow() - } - - override fun onEndOfEmailProtectionInContextSignupFlow() { - webView?.let { - browserAutofill.inContextEmailProtectionFlowFinished() - } + override fun onSelectedToSignUpForInContextEmailProtection(autofillWebMessageRequest: AutofillWebMessageRequest) { + showEmailProtectionInContextWebFlow(autofillWebMessageRequest = autofillWebMessageRequest) } override fun onSavedCredentials(credentials: LoginCredentials) { @@ -1382,17 +1365,6 @@ class BrowserTabFragment : viewModel.onShowUserCredentialsUpdated(credentials) } - override fun onNoCredentialsChosenForAutofill(originalUrl: String) { - viewModel.returnNoCredentialsWithPage(originalUrl) - } - - override fun onShareCredentialsForAutofill( - originalUrl: String, - selectedCredentials: LoginCredentials, - ) { - injectAutofillCredentials(originalUrl, selectedCredentials) - } - fun refresh() { webView?.reload() viewModel.onWebViewRefreshed() @@ -1589,16 +1561,8 @@ class BrowserTabFragment : is Command.RequestFileDownload -> requestFileDownload(it.url, it.contentDisposition, it.mimeType, it.requestUserConfirmation) is Command.ChildTabClosed -> processUriForThirdPartyCookies() is Command.CopyAliasToClipboard -> copyAliasToClipboard(it.alias) - is Command.InjectEmailAddress -> injectEmailAddress( - alias = it.duckAddress, - originalUrl = it.originalUrl, - autoSaveLogin = it.autoSaveLogin, - ) - - is Command.ShowEmailProtectionChooseEmailPrompt -> showEmailProtectionChooseEmailDialog(it.address) - is Command.ShowEmailProtectionInContextSignUpPrompt -> showNativeInContextEmailProtectionSignupPrompt() - - is Command.CancelIncomingAutofillRequest -> injectAutofillCredentials(it.url, null) + is Command.ShowEmailProtectionChooseEmailPrompt -> showEmailProtectionChooseEmailDialog(it.duckAddress, it.autofillWebMessageRequest) + is Command.PageChanged -> onPageChanged() is Command.LaunchAutofillSettings -> launchAutofillManagementScreen(it.privacyProtectionEnabled) is Command.EditWithSelectedQuery -> { omnibar.omnibarTextInput.setText(it.query) @@ -1607,9 +1571,6 @@ class BrowserTabFragment : is ShowBackNavigationHistory -> showBackNavigationHistory(it) is NavigationCommand.NavigateToHistory -> navigateBackHistoryStack(it.historyStackIndex) - is Command.EmailSignEvent -> { - notifyEmailSignEvent() - } is Command.PrintLink -> launchPrint(it.url, it.mediaSize) is Command.ShowSitePermissionsDialog -> showSitePermissionsDialog(it.permissionsToRequest, it.request) @@ -1743,6 +1704,11 @@ class BrowserTabFragment : } } + private fun onPageChanged() { + browserAutofill.notifyPageChanged() + hideDialogWithTag(CredentialAutofillPickerDialog.TAG) + } + private fun extractUrlFromAmpLink(initialUrl: String) { context?.let { val client = urlExtractingWebViewClient.get() @@ -1763,35 +1729,6 @@ class BrowserTabFragment : urlExtractingWebView = null } - private fun injectEmailAddress( - alias: String, - originalUrl: String, - autoSaveLogin: Boolean, - ) { - webView?.let { - if (it.url != originalUrl) { - Timber.w("WebView url has changed since autofill request; bailing") - return - } - - emailInjector.injectAddressInEmailField(it, alias, it.url) - - if (autoSaveLogin) { - duckAddressInjectedResultHandler.createLoginForPrivateDuckAddress( - duckAddress = alias, - tabId = tabId, - originalUrl = originalUrl, - ) - } - } - } - - private fun notifyEmailSignEvent() { - webView?.let { - emailInjector.notifyWebAppSignEvent(it, it.url) - } - } - private fun copyAliasToClipboard(alias: String) { context?.let { val clipboard: ClipboardManager? = ContextCompat.getSystemService(it, ClipboardManager::class.java) @@ -2452,11 +2389,6 @@ class BrowserTabFragment : it.setFindListener(this) loginDetector.addLoginDetection(it) { viewModel.loginDetected() } - emailInjector.addJsInterface( - it, - onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, - onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, - ) configureWebViewForBlobDownload(it) configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } @@ -2620,91 +2552,50 @@ class BrowserTabFragment : } private fun configureWebViewForAutofill(it: DuckDuckGoWebView) { - browserAutofill.addJsInterface(it, autofillCallback, this, null, tabId) + launch(dispatchers.main()) { + browserAutofill.addJsInterface(it, autofillCallback, tabId) - autofillFragmentResultListeners.getPlugins().forEach { plugin -> - setFragmentResultListener(plugin.resultKey(tabId)) { _, result -> - context?.let { - plugin.processResult( - result = result, - context = it, - tabId = tabId, - fragment = this@BrowserTabFragment, - autofillCallback = this@BrowserTabFragment, - ) + autofillFragmentResultListeners.getPlugins().forEach { plugin -> + setFragmentResultListener(plugin.resultKey(tabId)) { _, result -> + context?.let { + plugin.processResult( + result = result, + context = it, + tabId = tabId, + fragment = this@BrowserTabFragment, + autofillCallback = this@BrowserTabFragment, + ) + } } } } } - private fun injectAutofillCredentials( - url: String, - credentials: LoginCredentials?, - ) { - webView?.let { - if (it.url != url) { - Timber.w("WebView url has changed since autofill request; bailing") - return - } - browserAutofill.injectCredentials(credentials) - } - } - - private fun acceptGeneratedPassword(url: String) { - webView?.let { - if (it.url != url) { - Timber.w("WebView url has changed since autofill request; bailing") - return - } - browserAutofill.acceptGeneratedPassword() - } - } - - private fun rejectGeneratedPassword(url: String) { - webView?.let { - if (it.url != url) { - Timber.w("WebView url has changed since autofill request; bailing") - return - } - browserAutofill.rejectGeneratedPassword() - } - } - private fun cancelPendingAutofillRequestsToChooseCredentials() { - browserAutofill.cancelPendingAutofillRequestToChooseCredentials() viewModel.cancelPendingAutofillRequestToChooseCredentials() } private fun showAutofillDialogSaveCredentials( - currentUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, ) { - val url = webView?.url ?: return - if (url != currentUrl) return - - val dialog = credentialAutofillDialogFactory.autofillSavingCredentialsDialog(url, credentials, tabId) + val dialog = credentialAutofillDialogFactory.autofillSavingCredentialsDialog(autofillWebMessageRequest, credentials, tabId) showDialogHidingPrevious(dialog, CredentialSavePickerDialog.TAG) } private fun showAutofillDialogUpdatePassword( - currentUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, ) { - val url = webView?.url ?: return - if (url != currentUrl) return - - val dialog = credentialAutofillDialogFactory.autofillSavingUpdatePasswordDialog(url, credentials, tabId) + val dialog = credentialAutofillDialogFactory.autofillSavingUpdatePasswordDialog(autofillWebMessageRequest, credentials, tabId) showDialogHidingPrevious(dialog, CredentialUpdateExistingCredentialsDialog.TAG) } private fun showAutofillDialogUpdateUsername( - currentUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, ) { - val url = webView?.url ?: return - if (url != currentUrl) return - - val dialog = credentialAutofillDialogFactory.autofillSavingUpdateUsernameDialog(url, credentials, tabId) + val dialog = credentialAutofillDialogFactory.autofillSavingUpdateUsernameDialog(autofillWebMessageRequest, credentials, tabId) showDialogHidingPrevious(dialog, CredentialUpdateExistingCredentialsDialog.TAG) } @@ -3209,7 +3100,6 @@ class BrowserTabFragment : if (::popupMenu.isInitialized) popupMenu.dismiss() loginDetectionDialog?.dismiss() automaticFireproofDialog?.dismiss() - browserAutofill.removeJsInterface() destroyWebView() super.onDestroy() } @@ -3444,12 +3334,15 @@ class BrowserTabFragment : // NO OP } - private fun showEmailProtectionChooseEmailDialog(address: String) { + private fun showEmailProtectionChooseEmailDialog( + address: String, + autofillWebMessageRequest: AutofillWebMessageRequest, + ) { context?.let { val url = webView?.url ?: return val dialog = credentialAutofillDialogFactory.autofillEmailProtectionEmailChooserDialog( - url = url, + autofillWebMessageRequest = autofillWebMessageRequest, personalDuckAddress = address, tabId = tabId, ) @@ -3457,34 +3350,28 @@ class BrowserTabFragment : } } - override fun showNativeInContextEmailProtectionSignupPrompt() { + private fun showEmailProtectionInContextWebFlow(autofillWebMessageRequest: AutofillWebMessageRequest) { context?.let { - val url = webView?.url ?: return - - val dialog = credentialAutofillDialogFactory.emailProtectionInContextSignUpDialog( - tabId = tabId, + val params = EmailProtectionInContextSignUpStartScreen(messageRequestId = autofillWebMessageRequest.requestId) + val intent = globalActivityStarter.startIntent(it, params) + activityResultHandlerEmailProtectionInContextSignup.launch(intent) + inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState( + showing = true, + requestId = autofillWebMessageRequest.requestId, ) - showDialogHidingPrevious(dialog, EmailProtectionInContextSignUpDialog.TAG, url) } } - fun showEmailProtectionInContextWebFlow(verificationUrl: String? = null) { + fun resumeEmailProtectionInContextWebFlow(verificationUrl: String?, messageRequestId: String) { + if (verificationUrl == null) return context?.let { - val params = if (verificationUrl == null) { - EmailProtectionInContextSignUpScreenNoParams - } else { - EmailProtectionInContextSignUpHandleVerificationLink(verificationUrl) - } + val params = EmailProtectionInContextSignUpHandleVerificationLink(url = verificationUrl, messageRequestId = messageRequestId) val intent = globalActivityStarter.startIntent(it, params) activityResultHandlerEmailProtectionInContextSignup.launch(intent) - inContextEmailProtectionShowing = true + inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = true, requestId = messageRequestId) } } - override fun showNativeChooseEmailAddressPrompt() { - viewModel.showEmailProtectionChooseEmailPrompt() - } - companion object { private const val CUSTOM_TAB_TOOLBAR_COLOR_ARG = "CUSTOM_TAB_TOOLBAR_COLOR_ARG" private const val TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG = "TAB_DISPLAYED_IN_CUSTOM_TAB_SCREEN_ARG" diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index fbf303ba0edb..6b4267d10cc0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -82,7 +82,6 @@ import com.duckduckgo.app.browser.commands.Command.AskToDisableLoginDetection import com.duckduckgo.app.browser.commands.Command.AskToFireproofWebsite import com.duckduckgo.app.browser.commands.Command.AutocompleteItemRemoved import com.duckduckgo.app.browser.commands.Command.BrokenSiteFeedback -import com.duckduckgo.app.browser.commands.Command.CancelIncomingAutofillRequest import com.duckduckgo.app.browser.commands.Command.CheckSystemLocationPermission import com.duckduckgo.app.browser.commands.Command.ChildTabClosed import com.duckduckgo.app.browser.commands.Command.ConvertBlobToDataUri @@ -95,7 +94,6 @@ import com.duckduckgo.app.browser.commands.Command.DialNumber import com.duckduckgo.app.browser.commands.Command.DismissFindInPage import com.duckduckgo.app.browser.commands.Command.DownloadImage import com.duckduckgo.app.browser.commands.Command.EditWithSelectedQuery -import com.duckduckgo.app.browser.commands.Command.EmailSignEvent import com.duckduckgo.app.browser.commands.Command.ExtractUrlFromCloakedAmpLink import com.duckduckgo.app.browser.commands.Command.FindInPageCommand import com.duckduckgo.app.browser.commands.Command.GenerateWebViewPreviewImage @@ -104,7 +102,6 @@ import com.duckduckgo.app.browser.commands.Command.HideKeyboard import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog import com.duckduckgo.app.browser.commands.Command.HideSSLError import com.duckduckgo.app.browser.commands.Command.HideWebContent -import com.duckduckgo.app.browser.commands.Command.InjectEmailAddress import com.duckduckgo.app.browser.commands.Command.LaunchAddWidget import com.duckduckgo.app.browser.commands.Command.LaunchAutofillSettings import com.duckduckgo.app.browser.commands.Command.LaunchNewTab @@ -115,6 +112,7 @@ import com.duckduckgo.app.browser.commands.Command.OpenAppLink import com.duckduckgo.app.browser.commands.Command.OpenInNewBackgroundTab import com.duckduckgo.app.browser.commands.Command.OpenInNewTab import com.duckduckgo.app.browser.commands.Command.OpenMessageInNewTab +import com.duckduckgo.app.browser.commands.Command.PageChanged import com.duckduckgo.app.browser.commands.Command.PrintLink import com.duckduckgo.app.browser.commands.Command.RefreshUserAgent import com.duckduckgo.app.browser.commands.Command.RequestFileDownload @@ -243,6 +241,7 @@ import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.autofill.api.AutofillCapabilityChecker +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor @@ -593,7 +592,6 @@ class BrowserTabViewModel @Inject constructor( emailManager.signedInFlow().onEach { isSignedIn -> browserViewState.value = currentBrowserViewState().copy(isEmailSignedIn = isSignedIn) - command.value = EmailSignEvent }.launchIn(viewModelScope) observeAccessibilitySettings() @@ -1458,7 +1456,7 @@ class BrowserTabViewModel @Inject constructor( isLinkOpenedInNewTab = false automaticSavedLoginsMonitor.clearAutoSavedLoginId(tabId) - + command.value = PageChanged site?.run { val hasBrowserError = currentBrowserViewState().browserError != OMITTED privacyProtectionsPopupManager.onPageLoaded(url, httpErrorCodeEvents, hasBrowserError) @@ -3023,9 +3021,9 @@ class BrowserTabViewModel @Inject constructor( }.getOrNull() ?: return false } - fun showEmailProtectionChooseEmailPrompt() { + fun showEmailProtectionChooseEmailPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { emailManager.getEmailAddress()?.let { - command.postValue(ShowEmailProtectionChooseEmailPrompt(it)) + command.postValue(ShowEmailProtectionChooseEmailPrompt(it, autofillWebMessageRequest)) } } @@ -3043,23 +3041,6 @@ class BrowserTabViewModel @Inject constructor( } } - /** - * API called after user selected to autofill a private alias into a form - */ - fun usePrivateDuckAddress( - originalUrl: String, - duckAddress: String, - ) { - command.postValue(InjectEmailAddress(duckAddress = duckAddress, originalUrl = originalUrl, autoSaveLogin = true)) - } - - fun usePersonalDuckAddress( - originalUrl: String, - duckAddress: String, - ) { - command.postValue(InjectEmailAddress(duckAddress = duckAddress, originalUrl = originalUrl, autoSaveLogin = false)) - } - fun download(pendingFileDownload: PendingFileDownload) { fileDownloader.enqueueDownload(pendingFileDownload) } @@ -3183,10 +3164,6 @@ class BrowserTabViewModel @Inject constructor( command.postValue(LoadExtractedUrl(extractedUrl = destinationUrl)) } - fun returnNoCredentialsWithPage(originalUrl: String) { - command.postValue(CancelIncomingAutofillRequest(originalUrl)) - } - fun onConfigurationChanged() { browserViewState.value = currentBrowserViewState().copy( forceRenderingTicker = System.currentTimeMillis(), diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index f6a4f14b482d..572a606c7280 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -63,7 +63,6 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT import com.duckduckgo.autoconsent.api.Autoconsent -import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.common.utils.CurrentTimeProvider @@ -97,7 +96,6 @@ class BrowserWebViewClient @Inject constructor( private val thirdPartyCookieManager: ThirdPartyCookieManager, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, - private val browserAutofillConfigurator: BrowserAutofill.Configurator, private val ampLinks: AmpLinks, private val printInjector: PrintInjector, private val internalTestUserChecker: InternalTestUserChecker, @@ -362,7 +360,6 @@ class BrowserWebViewClient @Inject constructor( webViewClientListener?.pageRefreshed(url) } lastPageStarted = url - browserAutofillConfigurator.configureAutofillForCurrentPage(webView, url) jsPlugins.getPlugins().forEach { it.onPageStarted(webView, url, webViewClientListener?.getSite()) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt index 451e6a4f7ef8..a07a513151ef 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -37,6 +37,7 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.js.messaging.api.JsCallbackData @@ -192,19 +193,16 @@ sealed class Command { object ChildTabClosed : Command() class CopyAliasToClipboard(val alias: String) : Command() - class InjectEmailAddress( + class ShowEmailProtectionChooseEmailPrompt( val duckAddress: String, - val originalUrl: String, - val autoSaveLogin: Boolean, + val autofillWebMessageRequest: AutofillWebMessageRequest, ) : Command() - - class ShowEmailProtectionChooseEmailPrompt(val address: String) : Command() + object PageChanged : Command() object ShowEmailProtectionInContextSignUpPrompt : Command() class CancelIncomingAutofillRequest(val url: String) : Command() data class LaunchAutofillSettings(val privacyProtectionEnabled: Boolean) : Command() class EditWithSelectedQuery(val query: String) : Command() class ShowBackNavigationHistory(val history: List) : Command() - object EmailSignEvent : Command() class ShowSitePermissionsDialog( val permissionsToRequest: SitePermissions, val request: PermissionRequest, diff --git a/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt b/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt deleted file mode 100644 index 1999021a1fdf..000000000000 --- a/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2020 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.email - -import android.webkit.WebView -import androidx.annotation.UiThread -import com.duckduckgo.app.autofill.EmailProtectionJavascriptInjector -import com.duckduckgo.app.browser.DuckDuckGoUrlDetector -import com.duckduckgo.app.email.EmailJavascriptInterface.Companion.JAVASCRIPT_INTERFACE_NAME -import com.duckduckgo.autofill.api.Autofill -import com.duckduckgo.autofill.api.AutofillFeature -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.api.emailprotection.EmailInjector -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class EmailInjectorJs @Inject constructor( - private val emailManager: EmailManager, - private val urlDetector: DuckDuckGoUrlDetector, - private val dispatcherProvider: DispatcherProvider, - private val autofillFeature: AutofillFeature, - private val emailProtectionJavascriptInjector: EmailProtectionJavascriptInjector, - private val autofill: Autofill, -) : EmailInjector { - - override fun addJsInterface( - webView: WebView, - onSignedInEmailProtectionPromptShown: () -> Unit, - onInContextEmailProtectionSignupPromptShown: () -> Unit, - ) { - // We always add the interface irrespectively if the feature is enabled or not - webView.addJavascriptInterface( - EmailJavascriptInterface( - emailManager, - webView, - urlDetector, - dispatcherProvider, - autofillFeature, - autofill, - onSignedInEmailProtectionPromptShown, - ), - JAVASCRIPT_INTERFACE_NAME, - ) - } - - @UiThread - override fun injectAddressInEmailField( - webView: WebView, - alias: String?, - url: String?, - ) { - url?.let { - if (isFeatureEnabled() && !autofill.isAnException(url)) { - webView.evaluateJavascript("javascript:${emailProtectionJavascriptInjector.getAliasFunctions(webView.context, alias)}", null) - } - } - } - - @UiThread - override fun notifyWebAppSignEvent( - webView: WebView, - url: String?, - ) { - url?.let { - if (isFeatureEnabled() && isDuckDuckGoUrl(url) && !emailManager.isSignedIn()) { - webView.evaluateJavascript("javascript:${emailProtectionJavascriptInjector.getSignOutFunctions(webView.context)}", null) - } - } - } - - private fun isFeatureEnabled() = autofillFeature.self().isEnabled() - - private fun isDuckDuckGoUrl(url: String?): Boolean = (url != null && urlDetector.isDuckDuckGoEmailUrl(url)) -} diff --git a/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt b/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt deleted file mode 100644 index b17215186201..000000000000 --- a/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2020 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.email - -import android.webkit.JavascriptInterface -import android.webkit.WebView -import com.duckduckgo.app.browser.DuckDuckGoUrlDetector -import com.duckduckgo.autofill.api.Autofill -import com.duckduckgo.autofill.api.AutofillFeature -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.common.utils.DispatcherProvider -import kotlinx.coroutines.runBlocking -import org.json.JSONObject - -class EmailJavascriptInterface( - private val emailManager: EmailManager, - private val webView: WebView, - private val urlDetector: DuckDuckGoUrlDetector, - private val dispatcherProvider: DispatcherProvider, - private val autofillFeature: AutofillFeature, - private val autofill: Autofill, - private val showNativeTooltip: () -> Unit, -) { - - private fun getUrl(): String? { - return runBlocking(dispatcherProvider.main()) { - webView.url - } - } - - private fun isUrlFromDuckDuckGoEmail(): Boolean { - val url = getUrl() - return (url != null && urlDetector.isDuckDuckGoEmailUrl(url)) - } - - private fun isAutofillEnabled() = autofillFeature.self().isEnabled() - - @JavascriptInterface - fun isSignedIn(): String { - return if (isUrlFromDuckDuckGoEmail()) { - emailManager.isSignedIn().toString() - } else { - "" - } - } - - @JavascriptInterface - fun getUserData(): String { - return if (isUrlFromDuckDuckGoEmail()) { - emailManager.getUserData() - } else { - "" - } - } - - @JavascriptInterface - fun getDeviceCapabilities(): String { - return if (isUrlFromDuckDuckGoEmail()) { - JSONObject().apply { - put("addUserData", true) - put("getUserData", true) - put("removeUserData", true) - }.toString() - } else { - "" - } - } - - @JavascriptInterface - fun storeCredentials( - token: String, - username: String, - cohort: String, - ) { - if (isUrlFromDuckDuckGoEmail()) { - emailManager.storeCredentials(token, username, cohort) - } - } - - @JavascriptInterface - fun removeCredentials() { - if (isUrlFromDuckDuckGoEmail()) { - emailManager.signOut() - } - } - - @JavascriptInterface - fun showTooltip() { - getUrl()?.let { - if (isAutofillEnabled() && !autofill.isAnException(it)) { - showNativeTooltip() - } - } - } - - companion object { - const val JAVASCRIPT_INTERFACE_NAME = "EmailInterface" - } -} diff --git a/app/src/main/res/raw/inject_alias.js b/app/src/main/res/raw/inject_alias.js deleted file mode 100644 index 4b938bc50230..000000000000 --- a/app/src/main/res/raw/inject_alias.js +++ /dev/null @@ -1,21 +0,0 @@ -// -// DuckDuckGo -// -// Copyright © 2020 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -(function() { - window.postMessage({type: 'getAliasResponse', alias: '%s'}, window.origin); -})(); \ No newline at end of file diff --git a/app/src/main/res/raw/signout_autofill.js b/app/src/main/res/raw/signout_autofill.js deleted file mode 100644 index 635651815639..000000000000 --- a/app/src/main/res/raw/signout_autofill.js +++ /dev/null @@ -1,21 +0,0 @@ -// -// DuckDuckGo -// -// Copyright © 2020 DuckDuckGo. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -(function() { - window.postMessage({ emailProtectionSignedOut: true }, window.origin); -})(); \ No newline at end of file diff --git a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt deleted file mode 100644 index d5ca5b1ea56c..000000000000 --- a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.app.email - -import android.webkit.WebView -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl -import com.duckduckgo.autofill.api.Autofill -import com.duckduckgo.autofill.api.AutofillFeature -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.feature.toggles.api.Toggle -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@RunWith(AndroidJUnit4::class) -class EmailJavascriptInterfaceTest { - - @get:Rule - val coroutineRule = CoroutineTestRule() - - private val mockEmailManager: EmailManager = mock() - private val mockWebView: WebView = mock() - private lateinit var autofillFeature: AutofillFeature - private val mockAutofill: Autofill = mock() - lateinit var testee: EmailJavascriptInterface - private var counter = 0 - - @Before - fun setup() { - autofillFeature = com.duckduckgo.autofill.api.FakeAutofillFeature.create() - - testee = EmailJavascriptInterface( - mockEmailManager, - mockWebView, - DuckDuckGoUrlDetectorImpl(), - coroutineRule.testDispatcherProvider, - autofillFeature, - mockAutofill, - ) { counter++ } - - autofillFeature.self().setEnabled(Toggle.State(enable = true)) - whenever(mockAutofill.isAnException(any())).thenReturn(false) - } - - @Test - fun whenIsSignedInAndUrlIsDuckDuckGoEmailThenIsSignedInCalled() { - whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) - - testee.isSignedIn() - - verify(mockEmailManager).isSignedIn() - } - - @Test - fun whenIsSignedInAndUrlIsNotDuckDuckGoEmailThenIsSignedInNotCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - - testee.isSignedIn() - - verify(mockEmailManager, never()).isSignedIn() - } - - @Test - fun whenStoreCredentialsAndUrlIsDuckDuckGoEmailThenStoreCredentialsCalledWithCorrectParameters() { - whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) - - testee.storeCredentials("token", "username", "cohort") - - verify(mockEmailManager).storeCredentials("token", "username", "cohort") - } - - @Test - fun whenStoreCredentialsAndUrlIsNotDuckDuckGoEmailThenStoreCredentialsNotCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - - testee.storeCredentials("token", "username", "cohort") - - verify(mockEmailManager, never()).storeCredentials("token", "username", "cohort") - } - - @Test - fun whenGetUserDataAndUrlIsDuckDuckGoEmailThenGetUserDataCalled() { - whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) - - testee.getUserData() - - verify(mockEmailManager).getUserData() - } - - @Test - fun whenGetUserDataAndUrlIsNotDuckDuckGoEmailThenGetUserDataIsNotCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - - testee.getUserData() - - verify(mockEmailManager, never()).getUserData() - } - - @Test - fun whenShowTooltipThenLambdaCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - - testee.showTooltip() - - assertEquals(1, counter) - } - - @Test - fun whenShowTooltipAndFeatureDisabledThenLambdaNotCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - autofillFeature.self().setEnabled(Toggle.State(enable = false)) - - testee.showTooltip() - - assertEquals(0, counter) - } - - @Test - fun whenShowTooltipAndUrlIsAnExceptionThenLambdaNotCalled() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - whenever(mockAutofill.isAnException(any())).thenReturn(true) - - testee.showTooltip() - - assertEquals(0, counter) - } - - @Test - fun whenGetDeviceCapabilitiesAndUrlIsDuckDuckGoEmailThenReturnNonEmptyString() { - whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) - - assert(testee.getDeviceCapabilities().isNotBlank()) - } - - @Test - fun whenGetDeviceCapabilitiesAndUrlIsNotDuckDuckGoEmailThenReturnEmptyString() { - whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - - assert(testee.getDeviceCapabilities().isBlank()) - } - - companion object { - const val DUCKDUCKGO_EMAIL_URL = "https://duckduckgo.com/email" - const val NON_EMAIL_URL = "https://example.com" - } -} diff --git a/app/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionUrlTest.kt b/app/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionUrlTest.kt new file mode 100644 index 000000000000..1a87ac4777f7 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionUrlTest.kt @@ -0,0 +1,32 @@ +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import org.junit.Assert.* +import org.junit.Test + +class EmailProtectionUrlTest { + + @Test + fun whenNotADuckDuckGoAddressThenNotIdentifiedAsEmailProtectionUrl() { + assertFalse(EmailProtectionUrl.isEmailProtectionUrl("https://example.com")) + } + + @Test + fun whenADuckDuckGoAddressButNotEmailThenNotIdentifiedAsEmailProtectionUrl() { + assertFalse(EmailProtectionUrl.isEmailProtectionUrl("https://duckduckgo.com")) + } + + @Test + fun whenIsDuckDuckGoEmailUrlThenIdentifiedAsEmailProtectionUrl() { + assertTrue(EmailProtectionUrl.isEmailProtectionUrl("https://duckduckgo.com/email")) + } + + @Test + fun whenIsDuckDuckGoEmailUrlWithTrailingSlashThenIdentifiedAsEmailProtectionUrl() { + assertTrue(EmailProtectionUrl.isEmailProtectionUrl("https://duckduckgo.com/email/")) + } + + @Test + fun whenIsDuckDuckGoEmailUrlWithExtraUrlPartsThenIdentifiedAsEmailProtectionUrl() { + assertTrue(EmailProtectionUrl.isEmailProtectionUrl("https://duckduckgo.com/email/foo/bar")) + } +} diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCapabilityChecker.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCapabilityChecker.kt index 12109b062c24..678d1c829359 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCapabilityChecker.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCapabilityChecker.kt @@ -17,37 +17,12 @@ package com.duckduckgo.autofill.api /** - * Used to check the status of various Autofill features. - * - * Whether autofill features are enabled depends on a variety of inputs. This class provides a single way to query the status of all of them. + * Used to check the status of Autofill features. + * This is the public API that should be used by the app to check the status of Autofill features. + + * see also: InternalAutofillCapabilityChecker */ interface AutofillCapabilityChecker { - /** - * Whether autofill can inject credentials into a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canInjectCredentialsToWebView(url: String): Boolean - - /** - * Whether autofill can save credentials from a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canSaveCredentialsFromWebView(url: String): Boolean - - /** - * Whether autofill can generate a password into a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canGeneratePasswordFromWebView(url: String): Boolean - - /** - * Whether a user can access the credential management screen. - */ suspend fun canAccessCredentialManagementScreen(): Boolean - - /** - * Whether autofill is configured to be enabled. This is a configuration value, not a user preference. - */ - suspend fun isAutofillEnabledByConfiguration(url: String): Boolean } diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt index 7e58d3eb9036..390e2f3aadee 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillCredentialDialogs.kt @@ -53,8 +53,8 @@ interface CredentialAutofillPickerDialog { const val TAG = "CredentialAutofillPickerDialog" const val KEY_CANCELLED = "cancelled" - const val KEY_URL = "url" const val KEY_CREDENTIALS = "credentials" + const val KEY_URL_REQUEST = "url" const val KEY_TRIGGER_TYPE = "triggerType" const val KEY_TAB_ID = "tabId" } @@ -181,6 +181,7 @@ interface EmailProtectionInContextSignUpDialog { const val TAG = "EmailProtectionInContextSignUpDialog" const val KEY_RESULT = "result" + const val KEY_URL = "url" } } @@ -193,7 +194,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose which saved credential to autofill */ fun autofillSelectCredentialsDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, tabId: String, @@ -203,7 +204,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to save credentials or not */ fun autofillSavingCredentialsDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -212,7 +213,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to update an existing credential's password */ fun autofillSavingUpdatePasswordDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -221,7 +222,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to update an existing credential's username */ fun autofillSavingUpdateUsernameDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -230,7 +231,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to use generated password or not */ fun autofillGeneratePasswordDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, tabId: String, @@ -240,7 +241,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to use their personal duck address or a private alias address */ fun autofillEmailProtectionEmailChooserDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, personalDuckAddress: String, tabId: String, ): DialogFragment @@ -248,7 +249,7 @@ interface CredentialAutofillDialogFactory { /** * Creates a dialog which prompts the user to sign up for Email Protection */ - fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment + fun emailProtectionInContextSignUpDialog(tabId: String, autofillWebMessageRequest: AutofillWebMessageRequest): DialogFragment } private fun prefix( diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillEventListener.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillEventListener.kt index 247c9a520573..08170b770e80 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillEventListener.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillEventListener.kt @@ -25,54 +25,10 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials @MainThread interface AutofillEventListener { - /** - * Called when user chooses to use a generated password when prompted. - * @param originalUrl the URL of the page that prompted the user to use a generated password - */ - fun onAcceptGeneratedPassword(originalUrl: String) - - /** - * Called when user chooses not to use a generated password when prompted. - * @param originalUrl the URL of the page that prompted the user to use a generated password - */ - fun onRejectGeneratedPassword(originalUrl: String) - - /** - * Called when user chooses to autofill their personal duck address. - * @param originalUrl the URL of the page that prompted the user to use their personal duck address - * @param duckAddress the personal duck address that the user chose to autofill - */ - fun onUseEmailProtectionPersonalAddress(originalUrl: String, duckAddress: String) - - /** - * Called when user chooses to autofill a private duck address (private alias). - * @param originalUrl the URL of the page that prompted the user to use a private duck address - * @param duckAddress the private duck address that the user chose to autofill - */ - fun onUseEmailProtectionPrivateAlias(originalUrl: String, duckAddress: String) - /** * Called when user chooses to sign up for in-context email protection. */ - fun onSelectedToSignUpForInContextEmailProtection() - - /** - * Called when the Email Protection in-context flow ends, for any reason - */ - fun onEndOfEmailProtectionInContextSignupFlow() - - /** - * Called when user chooses to autofill a login credential to a web page. - * @param originalUrl the URL of the page that prompted the user to use a login credential - * @param selectedCredentials the login credential that the user chose to autofill - */ - fun onShareCredentialsForAutofill(originalUrl: String, selectedCredentials: LoginCredentials) - - /** - * Called when user chooses not to autofill any login credential to a web page. - * @param originalUrl the URL of the page that prompted the user to use a login credential - */ - fun onNoCredentialsChosenForAutofill(originalUrl: String) + fun onSelectedToSignUpForInContextEmailProtection(autofillWebMessageRequest: AutofillWebMessageRequest) /** * Called when a login credential was saved. This API could be used to show visual confirmation to the user. diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt index 941a1ef02fe1..3d97714c1236 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt @@ -16,98 +16,37 @@ package com.duckduckgo.autofill.api +import android.os.Parcelable import android.webkit.WebView import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType +import kotlinx.parcelize.Parcelize /** * Public interface for accessing and configuring browser autofill functionality for a WebView instance */ interface BrowserAutofill { - interface Configurator { - /** - * Configures autofill for the current webpage. - * This should be called once per page load (e.g., onPageStarted()) - * - * Responsible for injecting the required autofill configuration to the JS layer - */ - fun configureAutofillForCurrentPage( - webView: WebView, - url: String?, - ) - } /** * Adds the native->JS interface to the given WebView * This should be called once per WebView where autofill is to be available in it */ - fun addJsInterface( + suspend fun addJsInterface( webView: WebView, autofillCallback: Callback, - emailProtectionInContextCallback: EmailProtectionUserPromptListener? = null, - emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? = null, tabId: String, ) /** - * Removes the JS interface as a clean-up. Recommended to call from onDestroy() of Fragment/Activity containing the WebView + * Notifies that there has been a change in web page, and the autofill state should be re-evaluated */ - fun removeJsInterface() - - /** - * Communicates with the JS layer to pass the given credentials - * - * @param credentials The credentials to be passed to the JS layer. Can be null to indicate credentials won't be autofilled. - */ - fun injectCredentials(credentials: LoginCredentials?) + fun notifyPageChanged() /** * Cancels any ongoing autofill operations which would show the user the prompt to choose credentials * This would only normally be needed if a user-interaction happened such that showing autofill prompt would be undesirable. */ fun cancelPendingAutofillRequestToChooseCredentials() - - /** - * Informs the JS layer to use the generated password and fill it into the password field(s) - */ - fun acceptGeneratedPassword() - - /** - * Informs the JS layer not to use the generated password - */ - fun rejectGeneratedPassword() - - /** - * Informs the JS layer that the in-context Email Protection flow has finished - */ - fun inContextEmailProtectionFlowFinished() -} - -/** - * Callback for Email Protection prompts, signalling when to show the native UI to the user - */ -interface EmailProtectionUserPromptListener { - - /** - * Called when the user should be shown prompt to sign up for Email Protection - */ - fun showNativeInContextEmailProtectionSignupPrompt() - - /** - * Called when the user should be shown prompt to choose an email address to use for email protection autofill - */ - fun showNativeChooseEmailAddressPrompt() -} - -/** - * Callback for Email Protection events that might happen during the in-context signup flow - */ -interface EmailProtectionInContextSignupFlowListener { - - /** - * Called when the in-context email protection signup flow should be closed - */ - fun closeInContextSignup() } /** @@ -120,7 +59,7 @@ interface Callback { * When this is called, we should present the list to the user for them to choose which one, if any, to autofill. */ suspend fun onCredentialsAvailableToInject( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, ) @@ -130,7 +69,7 @@ interface Callback { * When this is called, we'd typically want to prompt the user if they want to save the credentials. */ suspend fun onCredentialsAvailableToSave( - currentUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, ) @@ -139,18 +78,46 @@ interface Callback { * When this is called, we should present the generated password to the user for them to choose whether to use it or not. */ suspend fun onGeneratedPasswordAvailableToUse( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, ) /** - * Called when we've been asked which credentials we have available to autofill, but the answer is none. + * Called when the user should be shown prompt to choose an email address to use for email protection autofill + */ + fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) + + /** + * Called when the user should be shown prompt to sign up for Email Protection */ - fun noCredentialsAvailable(originalUrl: String) + fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) /** * Called when credentials have been saved, and we want to show the user some visual confirmation. */ fun onCredentialsSaved(savedCredentials: LoginCredentials) } + +/** + * When there is an autofill request to be handled that requires user-interaction, we need to know where the request came from when later responding + * + * This is metadata about the WebMessage request that was received from the JS. + */ +@Parcelize +data class AutofillWebMessageRequest( + /** + * The origin of the request. Note, this may be a different origin than the page the user is currently on if the request came from an iframe + */ + val requestOrigin: String, + + /** + * The user-facing URL of the page where the autofill request originated + */ + val originalPageUrl: String?, + + /** + * The ID of the original request from the JS. This request ID is required in order to later provide a response using the web message reply API + */ + val requestId: String, +) : Parcelable diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/EmailProtectionInContextSignUpScreens.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/EmailProtectionInContextSignUpScreens.kt index df39b2538ce3..563afc6f93c4 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/EmailProtectionInContextSignUpScreens.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/EmailProtectionInContextSignUpScreens.kt @@ -20,15 +20,16 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter /** * Launch params for starting In-Context Email Protection flow + * @param messageRequestId The ID of the original web message that triggered the flow, used to send a reply back to the web page */ -object EmailProtectionInContextSignUpScreenNoParams : GlobalActivityStarter.ActivityParams { - private fun readResolve(): Any = EmailProtectionInContextSignUpScreenNoParams -} +data class EmailProtectionInContextSignUpStartScreen(val messageRequestId: String) : GlobalActivityStarter.ActivityParams /** * Launch params for resuming In-Context Email Protection flow from an email verification link + * @param url The URL of the email verification link + * @param messageRequestId The ID of the original web message that triggered the flow, used to send a reply back to the web page */ -data class EmailProtectionInContextSignUpHandleVerificationLink(val url: String) : GlobalActivityStarter.ActivityParams +data class EmailProtectionInContextSignUpHandleVerificationLink(val url: String, val messageRequestId: String) : GlobalActivityStarter.ActivityParams /** * Activity result codes @@ -36,4 +37,7 @@ data class EmailProtectionInContextSignUpHandleVerificationLink(val url: String) object EmailProtectionInContextSignUpScreenResult { const val SUCCESS = 1 const val CANCELLED = 2 + + const val RESULT_KEY_MESSAGE = "message" + const val RESULT_KEY_REQUEST_ID = "requestId" } diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/emailprotection/EmailInjector.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/emailprotection/EmailInjector.kt deleted file mode 100644 index aafe79802ab9..000000000000 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/emailprotection/EmailInjector.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.api.emailprotection - -import android.webkit.WebView - -interface EmailInjector { - - fun addJsInterface( - webView: WebView, - onSignedInEmailProtectionPromptShown: () -> Unit, - onInContextEmailProtectionSignupPromptShown: () -> Unit, - ) - - fun injectAddressInEmailField( - webView: WebView, - alias: String?, - url: String?, - ) - - fun notifyWebAppSignEvent( - webView: WebView, - url: String?, - ) -} diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt index f004da904785..7b59482c1424 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt @@ -41,6 +41,8 @@ interface PasswordsScreenPromotionPlugin { } companion object { + const val PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING = 50 + const val PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING = 60 const val PRIORITY_KEY_SURVEY = 100 const val PRIORITY_KEY_SYNC_PROMO = 200 } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillDisabledByConfigWarningUI.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillDisabledByConfigWarningUI.kt new file mode 100644 index 000000000000..90c90256373f --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillDisabledByConfigWarningUI.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin +import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin.Companion.PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING +import com.duckduckgo.autofill.impl.databinding.ViewAutofillConfigDisabledWarningBinding +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.ViewScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject + +@ContributesMultibinding(scope = AppScope::class) +@PriorityKey(PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING) +class AutofillDisabledByConfigWarningUI @Inject constructor( + private val internalAutofillCapabilityChecker: InternalAutofillCapabilityChecker, +) : PasswordsScreenPromotionPlugin { + + override suspend fun getView(context: Context, numberSavedPasswords: Int): View? { + val autofillConfigEnabled = internalAutofillCapabilityChecker.isAutofillEnabledByConfiguration("") + if (autofillConfigEnabled) return null + + return AutofillDisabledByConfigWarningView(context) + } +} + +@InjectWith(ViewScope::class) +class AutofillDisabledByConfigWarningView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + private val binding: ViewAutofillConfigDisabledWarningBinding by viewBinding() + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + binding.webViewUnsupportedWarningPanel.isVisible = true + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt new file mode 100644 index 000000000000..8a9c327224a5 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl + +import android.annotation.SuppressLint +import android.webkit.WebView +import androidx.webkit.WebViewCompat +import com.duckduckgo.autofill.impl.configuration.AutofillJavascriptLoader +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject + +interface AutofillJavascriptInjector { + suspend fun addDocumentStartJavascript(webView: WebView) +} + +@ContributesBinding(FragmentScope::class) +class AutofillJavascriptInjectorImpl @Inject constructor( + private val javascriptLoader: AutofillJavascriptLoader, +) : AutofillJavascriptInjector { + + @SuppressLint("RequiresFeature") + override suspend fun addDocumentStartJavascript(webView: WebView) { + val js = javascriptLoader.getAutofillJavascript() + .replace("// INJECT userPreferences HERE", staticJavascript) + + WebViewCompat.addDocumentStartJavaScript(webView, js, setOf("*")) + } + + companion object { + private val staticJavascript = """ + userPreferences = { + "debug": false, + "platform": { + "name": "android" + } + } + """.trimIndent() + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt deleted file mode 100644 index c5ef7fdb189e..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.impl - -import android.webkit.JavascriptInterface -import android.webkit.WebView -import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.autofill.api.AutofillCapabilityChecker -import com.duckduckgo.autofill.api.Callback -import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener -import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener -import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.api.domain.app.LoginTriggerType -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor -import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator -import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials -import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker -import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.AUTOPROMPT -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter -import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials -import com.duckduckgo.autofill.impl.store.InternalAutofillStore -import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository -import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DeleteAutoLogin -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DiscardAutoLoginId -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.PromptToSave -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateSavedAutoLogin -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver -import com.duckduckgo.common.utils.ConflatedJob -import com.duckduckgo.common.utils.DefaultDispatcherProvider -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber - -interface AutofillJavascriptInterface { - - @JavascriptInterface - fun getAutofillData(requestString: String) - - @JavascriptInterface - fun getIncontextSignupDismissedAt(data: String) - - fun injectCredentials(credentials: LoginCredentials) - fun injectNoCredentials() - - fun cancelRetrievingStoredLogins() - - fun acceptGeneratedPassword() - fun rejectGeneratedPassword() - - fun inContextEmailProtectionFlowFinished() - - var callback: Callback? - var emailProtectionInContextCallback: EmailProtectionUserPromptListener? - var emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? - var webView: WebView? - var autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor? - var tabId: String? - - companion object { - const val INTERFACE_NAME = "BrowserAutofill" - } - - @JavascriptInterface - fun closeEmailProtectionTab(data: String) -} - -@ContributesBinding(AppScope::class) -class AutofillStoredBackJavascriptInterface @Inject constructor( - private val requestParser: AutofillRequestParser, - private val autofillStore: InternalAutofillStore, - private val shareableCredentials: ShareableCredentials, - private val autofillMessagePoster: AutofillMessagePoster, - private val autofillResponseWriter: AutofillResponseWriter, - @AppCoroutineScope private val coroutineScope: CoroutineScope, - private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(), - private val currentUrlProvider: UrlProvider = WebViewUrlProvider(dispatcherProvider), - private val autofillCapabilityChecker: AutofillCapabilityChecker, - private val passwordEventResolver: AutogeneratedPasswordEventResolver, - private val emailManager: EmailManager, - private val inContextDataStore: EmailProtectionInContextDataStore, - private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker, - private val loginDeduplicator: AutofillLoginDeduplicator, - private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor, - private val neverSavedSiteRepository: NeverSavedSiteRepository, -) : AutofillJavascriptInterface { - - override var callback: Callback? = null - override var emailProtectionInContextCallback: EmailProtectionUserPromptListener? = null - override var emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? = null - override var webView: WebView? = null - override var autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor? = null - override var tabId: String? = null - - // coroutine jobs tracked for supporting cancellation - private val getAutofillDataJob = ConflatedJob() - private val storeFormDataJob = ConflatedJob() - private val injectCredentialsJob = ConflatedJob() - private val emailProtectionInContextSignupJob = ConflatedJob() - - @JavascriptInterface - override fun getAutofillData(requestString: String) { - Timber.v("BrowserAutofill: getAutofillData called:\n%s", requestString) - getAutofillDataJob += coroutineScope.launch(dispatcherProvider.io()) { - val url = currentUrlProvider.currentUrl(webView) - if (url == null) { - Timber.w("Can't autofill as can't retrieve current URL") - return@launch - } - - if (!autofillCapabilityChecker.canInjectCredentialsToWebView(url)) { - Timber.v("BrowserAutofill: getAutofillData called but feature is disabled") - return@launch - } - - val parseResult = requestParser.parseAutofillDataRequest(requestString) - val request = parseResult.getOrElse { - Timber.w(it, "Unable to parse getAutofillData request") - return@launch - } - - val triggerType = convertTriggerType(request.trigger) - - if (request.mainType != CREDENTIALS) { - handleUnknownRequestMainType(request, url) - return@launch - } - - if (request.isGeneratedPasswordAvailable()) { - handleRequestForPasswordGeneration(url, request) - } else if (request.isAutofillCredentialsRequest()) { - handleRequestForAutofillingCredentials(url, request, triggerType) - } else { - Timber.w("Unable to process request; don't know how to handle request %s", requestString) - } - } - } - - @JavascriptInterface - override fun getIncontextSignupDismissedAt(data: String) { - emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) { - val permanentDismissalTime = inContextDataStore.timestampUserChoseNeverAskAgain() - val installedRecently = recentInstallChecker.isRecentInstall() - val jsonResponse = autofillResponseWriter.generateResponseForEmailProtectionInContextSignup(installedRecently, permanentDismissalTime) - autofillMessagePoster.postMessage(webView, jsonResponse) - } - } - - @JavascriptInterface - override fun closeEmailProtectionTab(data: String) { - emailProtectionInContextSignupFlowCallback?.closeInContextSignup() - } - - @JavascriptInterface - fun showInContextEmailProtectionSignupPrompt(data: String) { - coroutineScope.launch(dispatcherProvider.io()) { - currentUrlProvider.currentUrl(webView)?.let { - val isSignedIn = emailManager.isSignedIn() - - withContext(dispatcherProvider.main()) { - if (isSignedIn) { - emailProtectionInContextCallback?.showNativeChooseEmailAddressPrompt() - } else { - emailProtectionInContextCallback?.showNativeInContextEmailProtectionSignupPrompt() - } - } - } - } - } - - private suspend fun handleRequestForPasswordGeneration( - url: String, - request: AutofillDataRequest, - ) { - callback?.onGeneratedPasswordAvailableToUse(url, request.generatedPassword?.username, request.generatedPassword?.value!!) - } - - private suspend fun handleRequestForAutofillingCredentials( - url: String, - request: AutofillDataRequest, - triggerType: LoginTriggerType, - ) { - val matches = mutableListOf() - val directMatches = autofillStore.getCredentials(url) - val shareableMatches = shareableCredentials.shareableCredentials(url) - Timber.v("Direct matches: %d, shareable matches: %d for %s", directMatches.size, shareableMatches.size, url) - matches.addAll(directMatches) - matches.addAll(shareableMatches) - - val credentials = filterRequestedSubtypes(request, matches) - - val dedupedCredentials = loginDeduplicator.deduplicate(url, credentials) - Timber.v("Original autofill credentials list size: %d, after de-duping: %d", credentials.size, dedupedCredentials.size) - - val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials) - - if (finalCredentialList.isEmpty()) { - callback?.noCredentialsAvailable(url) - } else { - callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType) - } - } - - private fun ensureUsernamesNotNull(credentials: List) = - credentials.map { - if (it.username == null) { - it.copy(username = "") - } else { - it - } - } - - private fun convertTriggerType(trigger: SupportedAutofillTriggerType): LoginTriggerType { - return when (trigger) { - USER_INITIATED -> LoginTriggerType.USER_INITIATED - AUTOPROMPT -> LoginTriggerType.AUTOPROMPT - } - } - - private fun filterRequestedSubtypes( - request: AutofillDataRequest, - credentials: List, - ): List { - return when (request.subType) { - USERNAME -> credentials.filterNot { it.username.isNullOrBlank() } - PASSWORD -> credentials.filterNot { it.password.isNullOrBlank() } - } - } - - private fun handleUnknownRequestMainType( - request: AutofillDataRequest, - url: String, - ) { - Timber.w("Autofill type %s unsupported", request.mainType) - callback?.noCredentialsAvailable(url) - } - - @JavascriptInterface - fun storeFormData(data: String) { - // important to call suppressor as soon as possible - systemAutofillServiceSuppressor.suppressAutofill(webView) - - Timber.i("storeFormData called, credentials provided to be persisted") - - storeFormDataJob += coroutineScope.launch(dispatcherProvider.io()) { - val currentUrl = currentUrlProvider.currentUrl(webView) ?: return@launch - - if (!autofillCapabilityChecker.canSaveCredentialsFromWebView(currentUrl)) { - Timber.v("BrowserAutofill: storeFormData called but feature is disabled") - return@launch - } - - if (neverSavedSiteRepository.isInNeverSaveList(currentUrl)) { - Timber.v("BrowserAutofill: storeFormData called but site is in never save list") - return@launch - } - - val parseResult = requestParser.parseStoreFormDataRequest(data) - val request = parseResult.getOrElse { - Timber.w(it, "Unable to parse storeFormData request") - return@launch - } - - if (!request.isValid()) { - Timber.w("Invalid data from storeFormData") - return@launch - } - - val jsCredentials = JavascriptCredentials(request.credentials!!.username, request.credentials.password) - val credentials = jsCredentials.asLoginCredentials(currentUrl) - - val autologinId = autoSavedLoginsMonitor?.getAutoSavedLoginId(tabId) - Timber.i("Autogenerated? %s, Previous autostored login ID: %s", request.credentials.autogenerated, autologinId) - val autosavedLogin = autologinId?.let { autofillStore.getCredentialsWithId(it) } - - val autogenerated = request.credentials.autogenerated - val actions = passwordEventResolver.decideActions(autosavedLogin, autogenerated) - processStoreFormDataActions(actions, currentUrl, credentials) - } - } - - private suspend fun processStoreFormDataActions( - actions: List, - currentUrl: String, - credentials: LoginCredentials, - ) { - Timber.d("%d actions to take: %s", actions.size, actions.joinToString()) - actions.forEach { - when (it) { - is DeleteAutoLogin -> { - autofillStore.deleteCredentials(it.autologinId) - } - - is DiscardAutoLoginId -> { - autoSavedLoginsMonitor?.clearAutoSavedLoginId(tabId) - } - - is PromptToSave -> { - callback?.onCredentialsAvailableToSave(currentUrl, credentials) - } - - is UpdateSavedAutoLogin -> { - autofillStore.getCredentialsWithId(it.autologinId)?.let { existingCredentials -> - if (isUpdateRequired(existingCredentials, credentials)) { - Timber.v("Update required as not identical to what is already stored. id=%s", it.autologinId) - val toSave = existingCredentials.copy(username = credentials.username, password = credentials.password) - autofillStore.updateCredentials(toSave)?.let { savedCredentials -> - callback?.onCredentialsSaved(savedCredentials) - } - } else { - Timber.v("Update not required as identical to what is already stored. id=%s", it.autologinId) - callback?.onCredentialsSaved(existingCredentials) - } - } - } - } - } - } - - private fun isUpdateRequired( - existingCredentials: LoginCredentials, - credentials: LoginCredentials, - ): Boolean { - return existingCredentials.username != credentials.username || existingCredentials.password != credentials.password - } - - private fun AutofillStoreFormDataRequest?.isValid(): Boolean { - if (this == null || credentials == null) return false - return !(credentials.username.isNullOrBlank() && credentials.password.isNullOrBlank()) - } - - override fun injectCredentials(credentials: LoginCredentials) { - Timber.v("Informing JS layer with credentials selected") - injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) { - val jsCredentials = credentials.asJsCredentials() - val jsonResponse = autofillResponseWriter.generateResponseGetAutofillData(jsCredentials) - Timber.i("Injecting credentials: %s", jsonResponse) - autofillMessagePoster.postMessage(webView, jsonResponse) - } - } - - override fun injectNoCredentials() { - Timber.v("No credentials selected; informing JS layer") - injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) { - autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateEmptyResponseGetAutofillData()) - } - } - - private fun LoginCredentials.asJsCredentials(): JavascriptCredentials { - return JavascriptCredentials( - username = username, - password = password, - ) - } - - override fun cancelRetrievingStoredLogins() { - getAutofillDataJob.cancel() - } - - override fun acceptGeneratedPassword() { - Timber.v("Accepting generated password") - injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) { - autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateResponseForAcceptingGeneratedPassword()) - } - } - - override fun rejectGeneratedPassword() { - Timber.v("Rejecting generated password") - injectCredentialsJob += coroutineScope.launch(dispatcherProvider.io()) { - autofillMessagePoster.postMessage(webView, autofillResponseWriter.generateResponseForRejectingGeneratedPassword()) - } - } - - override fun inContextEmailProtectionFlowFinished() { - emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) { - val json = autofillResponseWriter.generateResponseForEmailProtectionEndOfFlow(emailManager.isSignedIn()) - autofillMessagePoster.postMessage(webView, json) - } - } - - private fun JavascriptCredentials.asLoginCredentials( - url: String, - ): LoginCredentials { - return LoginCredentials( - id = null, - domain = url, - username = username, - password = password, - domainTitle = null, - ) - } - - interface UrlProvider { - suspend fun currentUrl(webView: WebView?): String? - } - - @ContributesBinding(AppScope::class) - class WebViewUrlProvider @Inject constructor(val dispatcherProvider: DispatcherProvider) : UrlProvider { - override suspend fun currentUrl(webView: WebView?): String? { - return withContext(dispatcherProvider.main()) { - webView?.url - } - } - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.kt new file mode 100644 index 000000000000..44e7ea373467 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.isVisible +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin +import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin.Companion.PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING +import com.duckduckgo.autofill.impl.databinding.ViewAutofillWarningSupportBinding +import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.ViewScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject + +@ContributesMultibinding(scope = AppScope::class) +@PriorityKey(PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING) +class AutofillSupportWarningUI @Inject constructor( + private val internalAutofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val autofillSurvey: AutofillSurvey, +) : PasswordsScreenPromotionPlugin { + + override suspend fun getView(context: Context, numberSavedPasswords: Int): View? { + val autofillSupported = internalAutofillCapabilityChecker.webViewSupportsAutofill() + if (autofillSupported) return null + + return AutofillSupportWarningView(context) + } +} + +@InjectWith(ViewScope::class) +class AutofillSupportWarningView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : FrameLayout(context, attrs, defStyle) { + + private val binding: ViewAutofillWarningSupportBinding by viewBinding() + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + binding.webViewUnsupportedWarningPanel.isVisible = true + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt index b29580d8bb3b..58a5b65ab501 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt @@ -16,66 +16,112 @@ package com.duckduckgo.autofill.impl +import android.annotation.SuppressLint import android.webkit.WebView +import androidx.webkit.WebViewCompat +import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.Callback -import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener -import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener -import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import timber.log.Timber @ContributesBinding(FragmentScope::class) class InlineBrowserAutofill @Inject constructor( - private val autofillInterface: AutofillJavascriptInterface, - private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val dispatchers: DispatcherProvider, + private val autofillJavascriptInjector: AutofillJavascriptInjector, + private val webMessageListeners: PluginPoint, + private val autofillFeature: AutofillFeature, + private val webMessageAttacher: AutofillWebMessageAttacher, ) : BrowserAutofill { - override fun addJsInterface( + override suspend fun addJsInterface( webView: WebView, autofillCallback: Callback, - emailProtectionInContextCallback: EmailProtectionUserPromptListener?, - emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener?, tabId: String, ) { - Timber.v("Injecting BrowserAutofill interface") - // Adding the interface regardless if the feature is available or not - webView.addJavascriptInterface(autofillInterface, AutofillJavascriptInterface.INTERFACE_NAME) - autofillInterface.webView = webView - autofillInterface.callback = autofillCallback - autofillInterface.emailProtectionInContextCallback = emailProtectionInContextCallback - autofillInterface.autoSavedLoginsMonitor = autoSavedLoginsMonitor - autofillInterface.tabId = tabId - } + withContext(dispatchers.io()) { + if (!autofillCapabilityChecker.webViewSupportsAutofill()) { + Timber.e("Modern javascript integration is not supported on this WebView version; autofill will not work") + return@withContext + } + + if (!autofillFeature.self().isEnabled()) { + Timber.w("Autofill feature is not enabled in remote config; autofill will not work") + return@withContext + } + + if (!autofillCapabilityChecker.canInjectCredentialsToWebView("")) { + Timber.w("Autofill injection on WebView is not supported; autofill will not work") + return@withContext + } - override fun removeJsInterface() { - autofillInterface.webView = null + configureModernIntegration(webView, autofillCallback, tabId) + } } - override fun injectCredentials(credentials: LoginCredentials?) { - if (credentials == null) { - autofillInterface.injectNoCredentials() - } else { - autofillInterface.injectCredentials(credentials) + private suspend fun configureModernIntegration( + webView: WebView, + autofillCallback: Callback, + tabId: String, + ) { + Timber.d("Autofill: Configuring modern integration with %d message listeners", webMessageListeners.getPlugins().size) + + withContext(dispatchers.main()) { + webMessageListeners.getPlugins().forEach { + webView.addWebMessageListener(it, autofillCallback, tabId) + yield() + } + + autofillJavascriptInjector.addDocumentStartJavascript(webView) } } override fun cancelPendingAutofillRequestToChooseCredentials() { - autofillInterface.cancelRetrievingStoredLogins() + webMessageListeners.getPlugins().forEach { + it.cancelOutstandingRequests() + } } - override fun acceptGeneratedPassword() { - autofillInterface.acceptGeneratedPassword() + private fun WebView.addWebMessageListener( + messageListener: AutofillWebMessageListener, + autofillCallback: Callback, + tabId: String, + ) { + webMessageAttacher.addListener(this, messageListener) + messageListener.callback = autofillCallback + messageListener.tabId = tabId } - override fun rejectGeneratedPassword() { - autofillInterface.rejectGeneratedPassword() + override fun notifyPageChanged() { + webMessageListeners.getPlugins().forEach { it.cancelOutstandingRequests() } } +} - override fun inContextEmailProtectionFlowFinished() { - autofillInterface.inContextEmailProtectionFlowFinished() +interface AutofillWebMessageAttacher { + fun addListener( + webView: WebView, + listener: AutofillWebMessageListener, + ) +} + +@SuppressLint("RequiresFeature") +@ContributesBinding(FragmentScope::class) +class AutofillWebMessageAttacherImpl @Inject constructor() : AutofillWebMessageAttacher { + + @SuppressLint("AddWebMessageListenerUsage") + // suppress AddWebMessageListenerUsage, we don't have access to DuckDuckGoWebView here. + override fun addListener( + webView: WebView, + listener: AutofillWebMessageListener, + ) { + WebViewCompat.addWebMessageListener(webView, listener.key, listener.origins, listener) } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt similarity index 69% rename from autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt rename to autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt index 6b067e1224c1..6ac440fa2639 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt @@ -19,19 +19,57 @@ package com.duckduckgo.autofill.impl import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.InternalTestUserChecker +import com.duckduckgo.autofill.impl.configuration.integration.JavascriptCommunicationSupport import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.withContext +/** + * Used to check the status of various Autofill features. + * + * Whether autofill features are enabled depends on a variety of inputs. This class provides a single way to query the status of all of them. + */ +interface InternalAutofillCapabilityChecker : AutofillCapabilityChecker { + + /** + * Whether autofill is supported in the current environment. + */ + suspend fun webViewSupportsAutofill(): Boolean + + /** + * Whether autofill can inject credentials into a WebView for the given page. + * @param url The URL of the webpage to check. + */ + suspend fun canInjectCredentialsToWebView(url: String): Boolean + + /** + * Whether autofill can save credentials from a WebView for the given page. + * @param url The URL of the webpage to check. + */ + suspend fun canSaveCredentialsFromWebView(url: String): Boolean + + /** + * Whether autofill can generate a password into a WebView for the given page. + * @param url The URL of the webpage to check. + */ + suspend fun canGeneratePasswordFromWebView(url: String): Boolean + + /** + * Whether autofill is configured to be enabled. This is a configuration value, not a user preference. + */ + suspend fun isAutofillEnabledByConfiguration(url: String): Boolean +} + @ContributesBinding(AppScope::class) class AutofillCapabilityCheckerImpl @Inject constructor( private val autofillFeature: AutofillFeature, private val internalTestUserChecker: InternalTestUserChecker, private val autofillGlobalCapabilityChecker: AutofillGlobalCapabilityChecker, + private val javascriptCommunicationSupport: JavascriptCommunicationSupport, private val dispatcherProvider: DispatcherProvider, -) : AutofillCapabilityChecker { +) : InternalAutofillCapabilityChecker { override suspend fun canInjectCredentialsToWebView(url: String): Boolean = withContext(dispatcherProvider.io()) { if (!isSecureAutofillAvailable()) return@withContext false @@ -75,6 +113,10 @@ class AutofillCapabilityCheckerImpl @Inject constructor( return@withContext autofillFeature.canAccessCredentialManagement().isEnabled() } + override suspend fun webViewSupportsAutofill(): Boolean { + return javascriptCommunicationSupport.supportsModernIntegration() + } + private suspend fun isInternalTester(): Boolean { return withContext(dispatcherProvider.io()) { internalTestUserChecker.isInternalTestUser @@ -93,3 +135,8 @@ class AutofillCapabilityCheckerImpl @Inject constructor( private suspend fun isAutofillEnabledByUser() = autofillGlobalCapabilityChecker.isAutofillEnabledByUser() } + +@ContributesBinding(AppScope::class) +class DefaultCapabilityChecker @Inject constructor( + private val capabilityChecker: InternalAutofillCapabilityChecker, +) : AutofillCapabilityChecker by capabilityChecker diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreator.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreator.kt index ef5ea4187e8f..d89645591737 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreator.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreator.kt @@ -17,7 +17,6 @@ package com.duckduckgo.autofill.impl import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor @@ -35,7 +34,7 @@ import timber.log.Timber class RealDuckAddressLoginCreator @Inject constructor( private val autofillStore: InternalAutofillStore, private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, - private val autofillCapabilityChecker: AutofillCapabilityChecker, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, private val neverSavedSiteRepository: NeverSavedSiteRepository, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt index 75fa4a587438..72d7d00ac5cc 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,10 @@ package com.duckduckgo.autofill.impl.configuration -import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials @@ -31,10 +31,7 @@ import javax.inject.Inject import timber.log.Timber interface AutofillRuntimeConfigProvider { - suspend fun getRuntimeConfiguration( - rawJs: String, - url: String?, - ): String + suspend fun getRuntimeConfiguration(url: String?): String } @ContributesBinding(AppScope::class) @@ -42,14 +39,14 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( private val emailManager: EmailManager, private val autofillStore: InternalAutofillStore, private val runtimeConfigurationWriter: RuntimeConfigurationWriter, - private val autofillCapabilityChecker: AutofillCapabilityChecker, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, private val autofillFeature: AutofillFeature, private val shareableCredentials: ShareableCredentials, private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules, private val neverSavedSiteRepository: NeverSavedSiteRepository, ) : AutofillRuntimeConfigProvider { + override suspend fun getRuntimeConfiguration( - rawJs: String, url: String?, ): String { Timber.v("BrowserAutofill: getRuntimeConfiguration called") @@ -66,11 +63,17 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( ) val availableInputTypes = generateAvailableInputTypes(url) - return rawJs - .replace("// INJECT contentScope HERE", contentScope) - .replace("// INJECT userUnprotectedDomains HERE", userUnprotectedDomains) - .replace("// INJECT userPreferences HERE", userPreferences) - .replace("// INJECT availableInputTypes HERE", availableInputTypes) + return """ + { + "type": "getRuntimeConfigurationResponse", + "success": { + $contentScope, + $userPreferences, + $availableInputTypes, + $userUnprotectedDomains + } + } + """.trimIndent() } private suspend fun generateAvailableInputTypes(url: String?): String { @@ -80,7 +83,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(credentialsAvailable, emailAvailable).also { Timber.v("availableInputTypes for %s: \n%s", url, it) } - return "availableInputTypes = $json" + return """"availableInputTypes" : $json""" } private suspend fun determineIfCredentialsAvailable(url: String?): AvailableInputTypeCredentials { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt deleted file mode 100644 index 7d37957ca38d..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.duckduckgo.autofill.impl.configuration - -import android.webkit.WebView -import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.autofill.api.AutofillCapabilityChecker -import com.duckduckgo.autofill.api.BrowserAutofill.Configurator -import com.duckduckgo.common.utils.DefaultDispatcherProvider -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber - -@ContributesBinding(AppScope::class) -class InlineBrowserAutofillConfigurator @Inject constructor( - private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider, - @AppCoroutineScope private val coroutineScope: CoroutineScope, - private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), - private val autofillCapabilityChecker: AutofillCapabilityChecker, - private val autofillJavascriptLoader: AutofillJavascriptLoader, -) : Configurator { - override fun configureAutofillForCurrentPage( - webView: WebView, - url: String?, - ) { - coroutineScope.launch(dispatchers.io()) { - if (canJsBeInjected(url)) { - Timber.v("Injecting autofill JS into WebView for %s", url) - - val rawJs = autofillJavascriptLoader.getAutofillJavascript() - val formatted = autofillRuntimeConfigProvider.getRuntimeConfiguration(rawJs, url) - - withContext(dispatchers.main()) { - webView.evaluateJavascript("javascript:$formatted", null) - } - } else { - Timber.v("Won't inject autofill JS into WebView for: %s", url) - } - } - } - - private suspend fun canJsBeInjected(url: String?): Boolean { - url?.let { - // note, we don't check for autofillEnabledByUser here, as the user-facing preference doesn't cover email - return autofillCapabilityChecker.isAutofillEnabledByConfiguration(it) - } - return false - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt index 14a4d073c304..23277813b7e6 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt @@ -60,7 +60,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateContentScope(): String { return """ - contentScope = { + "contentScope" : { "features": { "autofill": { "state": "enabled", @@ -68,7 +68,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run } }, "unprotectedTemporary": [] - }; + } """.trimIndent() } @@ -77,7 +77,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateUserUnprotectedDomains(): String { return """ - userUnprotectedDomains = []; + "userUnprotectedDomains" : [] """.trimIndent() } @@ -90,7 +90,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run unknownUsernameCategorization: Boolean, ): String { return """ - userPreferences = { + "userPreferences" : { "debug": false, "platform": { "name": "android" @@ -107,12 +107,12 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run "credentials_saving": $credentialSaving, "inlineIcon_credentials": $showInlineKeyIcon, "emailProtection_incontext_signup": $showInContextEmailProtectionSignup, - "unknown_username_categorization": $unknownUsernameCategorization, + "unknown_username_categorization": $unknownUsernameCategorization } } } } - }; + } """.trimIndent() } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt new file mode 100644 index 000000000000..c1f686243ad2 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration + +import androidx.webkit.WebViewFeature +import com.duckduckgo.browser.api.WebViewVersionProvider +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.extensions.compareSemanticVersion +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.withContext +import timber.log.Timber + +interface JavascriptCommunicationSupport { + suspend fun supportsModernIntegration(): Boolean +} + +@ContributesBinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class JavascriptCommunicationSupportImpl @Inject constructor( + private val webViewVersionProvider: WebViewVersionProvider, + private val dispatcherProvider: DispatcherProvider, +) : JavascriptCommunicationSupport { + + override suspend fun supportsModernIntegration(): Boolean = isWebMessageListenerSupported() && isModernSupportAvailable + + private val isModernSupportAvailable by lazy { + autofillRequiredFeatures.forEach { requiredFeature -> + if (!WebViewFeature.isFeatureSupported(requiredFeature)) { + Timber.i("Modern integration is not supported because feature %s is not supported", requiredFeature) + return@lazy false + } + } + + return@lazy true + } + + private suspend fun isWebMessageListenerSupported(): Boolean { + return withContext(dispatcherProvider.io()) { + webViewVersionProvider.getFullVersion() + .compareSemanticVersion(WEB_MESSAGE_LISTENER_WEBVIEW_VERSION)?.let { it >= 0 } ?: false + } && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) + } + + companion object { + private const val WEB_MESSAGE_LISTENER_WEBVIEW_VERSION = "126.0.6478.40" + + /** + * We need all of these to be supported in order to use autofill + */ + private val autofillRequiredFeatures = listOf( + WebViewFeature.DOCUMENT_START_SCRIPT, + WebViewFeature.WEB_MESSAGE_LISTENER, + WebViewFeature.POST_WEB_MESSAGE, + ) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt new file mode 100644 index 000000000000..3d0d49c4ee17 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import android.annotation.SuppressLint +import androidx.annotation.CheckResult +import androidx.collection.LruCache +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebViewCompat +import com.duckduckgo.autofill.api.Callback +import com.duckduckgo.common.utils.ConflatedJob +import java.util.* + +/** + * Base class for handling autofill web messages, which is how we communicate between JS and native code for autofill + * + * Each web message will have a unique key which is used in the JS when initiating a web message. e.g., `window.ddgGetAutofillData.postMessage()` + * And so each listener will declare which key they respond to. + * + * Each listener can also declare which origins they permit messages to come from. by default it will respond to all origins unless overridden. + * + * When a web message is received, there will be a `reply` object attached which is how the listener can respond back to the JS. + * If the listener needs to interact with the user first, it should call [storeReply] which will return it a `requestId`. + * This `requestId` can then be provided later to [AutofillMessagePoster] which will route the message to the correct receiver in the JS. + * + * The recommended way to declare a new web message listener is add a class which extends this abstract base class and + * annotate it with `@ContributesMultibinding(FragmentScope::class)`. This will then be automatically registered and unregistered + * when a new WebView in a tab is initialised or destroyed. See [InlineBrowserAutofill] for where this automatic registration happens. + */ +abstract class AutofillWebMessageListener : WebViewCompat.WebMessageListener { + + /** + * The key that the JS will use to send a message to this listener + * + * The key needs to be agreed upon between JS-layer and native layer. + * See https://app.asana.com/0/1206851683898855/1206851683898855/f for documentation + */ + abstract val key: String + + /** + * By default, a web message listener can be sent messages from all origins. This can be overridden to restrict to specific origins. + */ + open val origins: Set get() = setOf("*") + + lateinit var callback: Callback + lateinit var tabId: String + + internal val job = ConflatedJob() + + /** + * Called when a web message response should be sent back to the JS + * + * @param message the message to send back. The contents of this message will depend on the specific listener and what the JS schema expects. + * @param requestId the requestId that was provided when calling [storeReply] + * @return true if the message was handled by this listener or false if not + */ + @SuppressLint("RequiresFeature") + fun onResponse( + message: String, + requestId: String, + ): Boolean { + val replier = replyMap[requestId] ?: return false + replier.postMessage(message) + replyMap.remove(requestId) + return true + } + + /** + * Store the reply object so that it can be used later to send a response back to the JS + * + * If the listener can respond immediately, it should do so using the `reply` object it has access to. + * If the listener cannot response immediately, e.g., need user interaction first, can store the reply and access it later. + * + * @param reply the reply object to store + * @return a unique requestId that can be used later to send a response back to the JS. + * This requestId must be provided when later sending the message. e.g., provided to [AutofillMessagePoster] alongside the message. + */ + @CheckResult + protected fun storeReply(reply: JavaScriptReplyProxy): String { + return UUID.randomUUID().toString().also { + replyMap.put(it, reply) + } + } + + /** + * Cancel any outstanding requests and clean up resources + */ + fun cancelOutstandingRequests() { + replyMap.evictAll() + job.cancel() + } + + /** + * Store a small list of reply objects, where the requestId is the key. + * Replies are typically disposed of immediately upon using, but in some edge cases we might not respond and the stored replies are stale. + * Using a LRU cache to limit the number of stale replies we'd keep around. + */ + private val replyMap = LruCache(10) + + companion object { + val duckDuckGoOriginOnly = setOf("https://duckduckgo.com") + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillConfig.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillConfig.kt new file mode 100644 index 000000000000..123bee39b5de --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillConfig.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.impl.configuration.AutofillRuntimeConfigProvider +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SuppressLint("RequiresFeature") +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +class WebMessageListenerGetAutofillConfig @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgGetRuntimeConfiguration" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + job += appCoroutineScope.launch(dispatchers.io()) { + val config = autofillRuntimeConfigProvider.getRuntimeConfiguration(sourceOrigin.toString()) + reply.postMessage(config) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillData.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillData.kt new file mode 100644 index 000000000000..2fb87998cf04 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillData.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.domain.app.LoginTriggerType +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker +import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.AUTOPROMPT +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerGetAutofillData @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val requestParser: AutofillRequestParser, + private val autofillStore: InternalAutofillStore, + private val shareableCredentials: ShareableCredentials, + private val loginDeduplicator: AutofillLoginDeduplicator, + private val responseWriter: AutofillResponseWriter, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgGetAutofillData" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + runCatching { + val originalUrl: String? = webView.url + + job += appCoroutineScope.launch(dispatchers.io()) { + val requestId = storeReply(reply) + + getAutofillData( + message.data.toString(), + AutofillWebMessageRequest( + requestOrigin = sourceOrigin.toString(), + originalPageUrl = originalUrl, + requestId = requestId, + ), + ) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private suspend fun getAutofillData(requestString: String, autofillWebMessageRequest: AutofillWebMessageRequest) { + Timber.v("BrowserAutofill: getAutofillData called:\n%s", requestString) + if (autofillWebMessageRequest.originalPageUrl == null) { + Timber.w("Can't autofill as can't retrieve current URL") + return + } + + if (!autofillCapabilityChecker.canInjectCredentialsToWebView(autofillWebMessageRequest.requestOrigin)) { + Timber.v("BrowserAutofill: getAutofillData called but feature is disabled") + return + } + + val parseResult = requestParser.parseAutofillDataRequest(requestString) + val request = parseResult.getOrElse { + Timber.w(it, "Unable to parse getAutofillData request") + return + } + + val triggerType = convertTriggerType(request.trigger) + + if (request.mainType != CREDENTIALS) { + handleUnknownRequestMainType(request, autofillWebMessageRequest) + return + } + + if (request.isGeneratedPasswordAvailable()) { + handleRequestForPasswordGeneration(autofillWebMessageRequest, request) + } else if (request.isAutofillCredentialsRequest()) { + handleRequestForAutofillingCredentials(autofillWebMessageRequest, request, triggerType) + } else { + Timber.w("Unable to process request; don't know how to handle request %s", requestString) + } + } + + private suspend fun handleRequestForPasswordGeneration( + autofillWebMessageRequest: AutofillWebMessageRequest, + request: AutofillDataRequest, + ) { + callback.onGeneratedPasswordAvailableToUse(autofillWebMessageRequest, request.generatedPassword?.username, request.generatedPassword?.value!!) + } + + private fun handleUnknownRequestMainType( + request: AutofillDataRequest, + autofillWebMessageRequest: AutofillWebMessageRequest, + ) { + Timber.w("Autofill type %s unsupported", request.mainType) + onNoCredentialsAvailable(autofillWebMessageRequest) + } + + private suspend fun handleRequestForAutofillingCredentials( + urlRequest: AutofillWebMessageRequest, + request: AutofillDataRequest, + triggerType: LoginTriggerType, + ) { + val matches = mutableListOf() + val directMatches = autofillStore.getCredentials(urlRequest.requestOrigin) + val shareableMatches = shareableCredentials.shareableCredentials(urlRequest.requestOrigin) + Timber.v("Direct matches: %d, shareable matches: %d for %s", directMatches.size, shareableMatches.size, urlRequest.requestOrigin) + matches.addAll(directMatches) + matches.addAll(shareableMatches) + + val credentials = filterRequestedSubtypes(request, matches) + + val dedupedCredentials = loginDeduplicator.deduplicate(urlRequest.requestOrigin, credentials) + Timber.v("Original autofill credentials list size: %d, after de-duping: %d", credentials.size, dedupedCredentials.size) + + val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials) + + if (finalCredentialList.isEmpty()) { + onNoCredentialsAvailable(urlRequest) + } else { + callback.onCredentialsAvailableToInject(urlRequest, finalCredentialList, triggerType) + } + } + + private fun onNoCredentialsAvailable(urlRequest: AutofillWebMessageRequest) { + val message = responseWriter.generateEmptyResponseGetAutofillData() + onResponse(message, urlRequest.requestId) + } + + private fun convertTriggerType(trigger: SupportedAutofillTriggerType): LoginTriggerType { + return when (trigger) { + USER_INITIATED -> LoginTriggerType.USER_INITIATED + AUTOPROMPT -> LoginTriggerType.AUTOPROMPT + } + } + + private fun ensureUsernamesNotNull(credentials: List) = + credentials.map { + if (it.username == null) { + it.copy(username = "") + } else { + it + } + } + + private fun filterRequestedSubtypes( + request: AutofillDataRequest, + credentials: List, + ): List { + return when (request.subType) { + USERNAME -> credentials.filterNot { it.username.isNullOrBlank() } + PASSWORD -> credentials.filterNot { it.password.isNullOrBlank() } + } + } +} diff --git a/browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt similarity index 58% rename from browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt rename to autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt index fdefaa4d20ab..026bfa470306 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,15 @@ * limitations under the License. */ -package com.duckduckgo.app.autofill +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email -import android.content.Context +class EmailProtectionUrl { -interface EmailProtectionJavascriptInjector { + companion object { + fun isEmailProtectionUrl(url: String?): Boolean { + return url?.startsWith(EMAIL_PROTECTION_SETTINGS_URL) == true + } - fun getAliasFunctions( - context: Context, - alias: String?, - ): String - - fun getSignOutFunctions( - context: Context, - ): String + private const val EMAIL_PROTECTION_SETTINGS_URL = "https://duckduckgo.com/email" + } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetAlias.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetAlias.kt new file mode 100644 index 000000000000..cd2f58759ff4 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetAlias.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.Autofill +import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerEmailGetAlias @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val autofillFeature: AutofillFeature, + private val autofill: Autofill, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgEmailProtectionGetAlias" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + val originalUrl: String? = webView.url + + job += appCoroutineScope.launch(dispatchers.io()) { + val requestOrigin = sourceOrigin.toString() + if (!enabled(requestOrigin)) { + return@launch + } + val requestId = storeReply(reply) + callback.showNativeChooseEmailAddressPrompt( + AutofillWebMessageRequest( + requestOrigin = requestOrigin, + originalPageUrl = originalUrl, + requestId = requestId, + ), + ) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private fun enabled(url: String): Boolean { + return autofillFeature.self().isEnabled() && !autofill.isAnException(url) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetCapabilities.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetCapabilities.kt new file mode 100644 index 000000000000..b6a0029c02c4 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetCapabilities.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.invoke +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerEmailGetCapabilities @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgEmailProtectionGetCapabilities" + + override val origins: Set + get() = duckDuckGoOriginOnly + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + if (!EmailProtectionUrl.isEmailProtectionUrl(webView.url)) return + + job += appCoroutineScope.launch(dispatchers.io()) { + reply.postMessage(generateResponse()) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private fun generateResponse(): String { + return """ + { + "success" : { + "addUserData" : true, + "getUserData" : true, + "removeUserData" : true + } + } + """.trimIndent() + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetUserData.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetUserData.kt new file mode 100644 index 000000000000..47017dd2dac9 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetUserData.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerEmailGetUserData @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val emailManager: EmailManager, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgEmailProtectionGetUserData" + + override val origins: Set + get() = duckDuckGoOriginOnly + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + if (!EmailProtectionUrl.isEmailProtectionUrl(webView.url)) return + + job += appCoroutineScope.launch(dispatchers.io()) { + reply.postMessage(generateResponse()) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private fun generateResponse(): String { + val userData = emailManager.getUserData() + return """ + { + "success" : $userData + } + """.trimIndent() + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailRemoveCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailRemoveCredentials.kt new file mode 100644 index 000000000000..e33fe7d5d13b --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailRemoveCredentials.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerEmailRemoveCredentials @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val emailManager: EmailManager, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgEmailProtectionRemoveUserData" + + override val origins: Set + get() = duckDuckGoOriginOnly + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + if (!EmailProtectionUrl.isEmailProtectionUrl(webView.url)) return + + appCoroutineScope.launch(dispatchers.io()) { + emailManager.signOut() + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailStoreCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailStoreCredentials.kt new file mode 100644 index 000000000000..61feb0c08662 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailStoreCredentials.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerEmailStoreCredentials @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val emailManager: EmailManager, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgEmailProtectionStoreUserData" + + override val origins: Set + get() = duckDuckGoOriginOnly + + private val moshi by lazy { Moshi.Builder().add(KotlinJsonAdapterFactory()).build() } + private val requestParser by lazy { moshi.adapter(IncomingMessage::class.java) } + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + if (!EmailProtectionUrl.isEmailProtectionUrl(webView.url)) return + + appCoroutineScope.launch(dispatchers.io()) { + parseIncomingMessage(message.data.toString())?.let { + emailManager.storeCredentials(it.token, it.userName, it.cohort) + Timber.i("Saved email protection credentials for user %s", it.userName) + } + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private fun parseIncomingMessage(message: String): IncomingMessage? { + return kotlin.runCatching { + return requestParser.fromJson(message) + }.onFailure { Timber.w(it, "Failed to parse incoming email protection save message") }.getOrNull() + } + + private data class IncomingMessage( + val token: String, + val userName: String, + val cohort: String, + ) +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerCloseEmailProtectionTab.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerCloseEmailProtectionTab.kt new file mode 100644 index 000000000000..6f6adedc7058 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerCloseEmailProtectionTab.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email.incontext + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import androidx.webkit.WebViewCompat.WebMessageListener +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener.Companion.duckDuckGoOriginOnly +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email.EmailProtectionUrl +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerCloseEmailProtectionTab @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, +) : WebMessageListener { + + lateinit var callback: CloseEmailProtectionTabCallback + + val key: String + get() = "ddgCloseEmailProtectionTab" + + val origins: Set + get() = duckDuckGoOriginOnly + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + if (!EmailProtectionUrl.isEmailProtectionUrl(webView.url)) return + + appCoroutineScope.launch(dispatchers.io()) { + callback.closeNativeInContextEmailProtectionSignup() + } + } + + interface CloseEmailProtectionTabCallback { + suspend fun closeNativeInContextEmailProtectionSignup() + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerGetIncontextSignupDismissedAt.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerGetIncontextSignupDismissedAt.kt new file mode 100644 index 000000000000..d792698d09f9 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerGetIncontextSignupDismissedAt.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email.incontext + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker +import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerGetIncontextSignupDismissedAt @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val autofillResponseWriter: AutofillResponseWriter, + private val inContextDataStore: EmailProtectionInContextDataStore, + private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgGetIncontextSignupDismissedAt" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + job += appCoroutineScope.launch(dispatchers.io()) { + reply.postMessage(generateResponse()) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private suspend fun generateResponse(): String { + val permanentDismissalTime = inContextDataStore.timestampUserChoseNeverAskAgain() + val installedRecently = recentInstallChecker.isRecentInstall() + return autofillResponseWriter.generateResponseForEmailProtectionInContextSignup(installedRecently, permanentDismissalTime) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerShowInContextEmailProtectionSignupPrompt.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerShowInContextEmailProtectionSignupPrompt.kt new file mode 100644 index 000000000000..e004ff35d021 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerShowInContextEmailProtectionSignupPrompt.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email.incontext + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerShowInContextEmailProtectionSignupPrompt @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val emailManager: EmailManager, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgShowInContextEmailProtectionSignupPrompt" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + val originalUrl: String? = webView.url + + job += appCoroutineScope.launch(dispatchers.io()) { + val requestOrigin = sourceOrigin.toString() + val requestId = storeReply(reply) + + val autofillWebMessageRequest = AutofillWebMessageRequest( + requestOrigin = requestOrigin, + originalPageUrl = originalUrl, + requestId = requestId, + ) + showInContextEmailProtectionSignupPrompt(autofillWebMessageRequest) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private fun showInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + appCoroutineScope.launch(dispatchers.io()) { + val isSignedIn = emailManager.isSignedIn() + + withContext(dispatchers.main()) { + if (isSignedIn) { + callback.showNativeChooseEmailAddressPrompt(autofillWebMessageRequest) + } else { + callback.showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest) + } + } + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormData.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormData.kt new file mode 100644 index 000000000000..73bd03268060 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormData.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.password + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository +import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DeleteAutoLogin +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.DiscardAutoLoginId +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.PromptToSave +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions.UpdateSavedAutoLogin +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.FragmentScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +@SingleInstanceIn(FragmentScope::class) +@ContributesMultibinding(FragmentScope::class) +@SuppressLint("RequiresFeature") +class WebMessageListenerStoreFormData @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val neverSavedSiteRepository: NeverSavedSiteRepository, + private val requestParser: AutofillRequestParser, + private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, + private val autofillStore: InternalAutofillStore, + private val passwordEventResolver: AutogeneratedPasswordEventResolver, + private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor, +) : AutofillWebMessageListener() { + + override val key: String + get() = "ddgStoreFormData" + + override fun onPostMessage( + webView: WebView, + message: WebMessageCompat, + sourceOrigin: Uri, + isMainFrame: Boolean, + reply: JavaScriptReplyProxy, + ) { + kotlin.runCatching { + // important to call suppressor as soon as possible + systemAutofillServiceSuppressor.suppressAutofill(webView) + + val originalUrl: String? = webView.url + + appCoroutineScope.launch(dispatchers.io()) { + val requestOrigin = sourceOrigin.toString() + val requestId = storeReply(reply) + storeFormData( + message.data.toString(), + AutofillWebMessageRequest(requestOrigin = requestOrigin, originalPageUrl = originalUrl, requestId = requestId), + ) + } + }.onFailure { + Timber.e(it, "Error while processing autofill web message for %s", key) + } + } + + private suspend fun storeFormData( + data: String, + autofillWebMessageRequest: AutofillWebMessageRequest, + ) { + Timber.i("storeFormData called, credentials provided to be persisted") + + if (autofillWebMessageRequest.originalPageUrl == null) return + + if (!autofillCapabilityChecker.canSaveCredentialsFromWebView(autofillWebMessageRequest.requestOrigin)) { + Timber.v("BrowserAutofill: storeFormData called but feature is disabled") + return + } + + if (neverSavedSiteRepository.isInNeverSaveList(autofillWebMessageRequest.requestOrigin)) { + Timber.v("BrowserAutofill: storeFormData called but site is in never save list") + return + } + + val parseResult = requestParser.parseStoreFormDataRequest(data) + val request = parseResult.getOrElse { + Timber.w(it, "Unable to parse storeFormData request") + return + } + + if (!request.isValid()) { + Timber.w("Invalid data from storeFormData") + return + } + + val jsCredentials = JavascriptCredentials(request.credentials!!.username, request.credentials.password) + val credentials = jsCredentials.asLoginCredentials(autofillWebMessageRequest.requestOrigin) + + val autologinId = autoSavedLoginsMonitor.getAutoSavedLoginId(tabId) + Timber.i("Autogenerated? %s, Previous autostored login ID: %s", request.credentials.autogenerated, autologinId) + val autosavedLogin = autologinId?.let { autofillStore.getCredentialsWithId(it) } + + val autogenerated = request.credentials.autogenerated + val actions = passwordEventResolver.decideActions(autosavedLogin, autogenerated) + processStoreFormDataActions(actions, autofillWebMessageRequest, credentials) + } + + private fun isUpdateRequired( + existingCredentials: LoginCredentials, + credentials: LoginCredentials, + ): Boolean { + return existingCredentials.username != credentials.username || existingCredentials.password != credentials.password + } + + private fun AutofillStoreFormDataRequest?.isValid(): Boolean { + if (this == null || credentials == null) return false + return !(credentials.username.isNullOrBlank() && credentials.password.isNullOrBlank()) + } + + private suspend fun processStoreFormDataActions( + actions: List, + autofillWebMessageRequest: AutofillWebMessageRequest, + credentials: LoginCredentials, + ) { + Timber.d("%d actions to take: %s", actions.size, actions.joinToString()) + actions.forEach { + when (it) { + is DeleteAutoLogin -> { + autofillStore.deleteCredentials(it.autologinId) + } + + is DiscardAutoLoginId -> { + autoSavedLoginsMonitor.clearAutoSavedLoginId(tabId) + } + + is PromptToSave -> { + callback.onCredentialsAvailableToSave(autofillWebMessageRequest, credentials) + } + + is UpdateSavedAutoLogin -> { + autofillStore.getCredentialsWithId(it.autologinId)?.let { existingCredentials -> + if (isUpdateRequired(existingCredentials, credentials)) { + Timber.v("Update required as not identical to what is already stored. id=%s", it.autologinId) + val toSave = existingCredentials.copy(username = credentials.username, password = credentials.password) + autofillStore.updateCredentials(toSave)?.let { savedCredentials -> + callback.onCredentialsSaved(savedCredentials) + } + } else { + Timber.v("Update not required as identical to what is already stored. id=%s", it.autologinId) + callback.onCredentialsSaved(existingCredentials) + } + } + } + } + } + } + + private fun JavascriptCredentials.asLoginCredentials( + url: String, + ): LoginCredentials { + return LoginCredentials( + id = null, + domain = url, + username = username, + password = password, + domainTitle = null, + ) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt index de1f320f8e45..3bedfd1b7bae 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt @@ -25,6 +25,7 @@ import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizer import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher @@ -159,3 +160,6 @@ interface UnusedAutofillResultPlugin @ContributesPluginPoint(scope = ActivityScope::class, boundType = PasswordsScreenPromotionPlugin::class) private interface PasswordsScreenPromotionPluginPoint + +@ContributesPluginPoint(scope = AppScope::class, boundType = AutofillWebMessageListener::class) +interface UnusedAutofillWebMessageListener diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt index 4e446f1c0863..528641767312 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt @@ -22,9 +22,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.DialogEmailProtectionChooseEmailBinding @@ -89,7 +92,7 @@ class EmailProtectionChooseEmailFragment : BottomSheetDialogFragment(), EmailPro Timber.v("User action: %s", resultType::class.java.simpleName) val result = Bundle().also { - it.putString(EmailProtectionChooseEmailDialog.KEY_URL, getOriginalUrl()) + it.putParcelable(KEY_URL, getWebMessageRequest()) it.putParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT, resultType) } @@ -109,19 +112,19 @@ class EmailProtectionChooseEmailFragment : BottomSheetDialogFragment(), EmailPro } private fun getPersonalAddress() = arguments?.getString(KEY_ADDRESS)!! - private fun getOriginalUrl() = arguments?.getString(EmailProtectionChooseEmailDialog.KEY_URL)!! + private fun getWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! companion object { fun instance( personalDuckAddress: String, - url: String, + url: AutofillWebMessageRequest, tabId: String, ): EmailProtectionChooseEmailFragment { val fragment = EmailProtectionChooseEmailFragment() fragment.arguments = Bundle().also { it.putString(KEY_ADDRESS, personalDuckAddress) - it.putString(EmailProtectionChooseEmailDialog.KEY_URL, url) + it.putParcelable(KEY_URL, url) it.putString(KEY_TAB_ID, tabId) } return fragment diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt index 6b9e1e58cf8b..177ad636e030 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt @@ -16,30 +16,35 @@ package com.duckduckgo.autofill.impl.email -import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle -import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LAST_USED_DAY -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog -import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.* +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.Companion.KEY_RESULT +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.Companion.KEY_URL +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.DoNotUseEmailProtection +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePersonalEmailAddress +import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePrivateAliasAddress +import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -47,12 +52,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(AppScope::class) +@ContributesMultibinding(FragmentScope::class) class ResultHandlerEmailProtectionChooseEmail @Inject constructor( - private val appBuildConfig: AppBuildConfig, private val emailManager: EmailManager, private val dispatchers: DispatcherProvider, private val pixel: Pixel, + private val messagePoster: AutofillMessagePoster, + private val loginCreator: DuckAddressLoginCreator, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { @@ -66,52 +72,72 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val userSelection: EmailProtectionChooseEmailDialog.UseEmailResultType = - result.safeGetParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT) ?: return - val originalUrl = result.getString(EmailProtectionChooseEmailDialog.KEY_URL) ?: return + val userSelection = BundleCompat.getParcelable(result, KEY_RESULT, UseEmailResultType::class.java) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return when (userSelection) { UsePersonalEmailAddress -> { - onSelectedToUsePersonalAddress(originalUrl, autofillCallback) + onSelectedToUsePersonalAddress(autofillWebMessageRequest) notifyAutofillListenersDuckAddressFilled() } UsePrivateAliasAddress -> { - onSelectedToUsePrivateAlias(originalUrl, autofillCallback) + onSelectedToUsePrivateAlias(autofillWebMessageRequest, tabId) notifyAutofillListenersDuckAddressFilled() } - DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection() + DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection(autofillWebMessageRequest) } } - private fun onSelectedToUsePersonalAddress(originalUrl: String, autofillCallback: AutofillEventListener) { + private fun onSelectedToUsePersonalAddress(autofillWebMessageRequest: AutofillWebMessageRequest) { appCoroutineScope.launch(dispatchers.io()) { val duckAddress = emailManager.getEmailAddress() ?: return@launch enqueueEmailProtectionPixel(EMAIL_USE_ADDRESS, includeLastUsedDay = true) - withContext(dispatchers.main()) { - autofillCallback.onUseEmailProtectionPersonalAddress(originalUrl, duckAddress) + withContext(dispatchers.io()) { + val message = buildResponseMessage(duckAddress) + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) } emailManager.setNewLastUsedDate() } } - private fun onSelectedToUsePrivateAlias(originalUrl: String, autofillCallback: AutofillEventListener) { + private fun onSelectedToUsePrivateAlias( + autofillWebMessageRequest: AutofillWebMessageRequest, + tabId: String, + ) { appCoroutineScope.launch(dispatchers.io()) { val privateAlias = emailManager.getAlias() ?: return@launch enqueueEmailProtectionPixel(EMAIL_USE_ALIAS, includeLastUsedDay = true) - withContext(dispatchers.main()) { - autofillCallback.onUseEmailProtectionPrivateAlias(originalUrl, privateAlias) - } + val message = buildResponseMessage(privateAlias) + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) + + loginCreator.createLoginForPrivateDuckAddress( + duckAddress = privateAlias, + tabId = tabId, + originalUrl = autofillWebMessageRequest.requestOrigin, + ) emailManager.setNewLastUsedDate() } } - private fun onSelectedNotToUseEmailProtection() { + private fun buildResponseMessage(emailAddress: String): String { + return """ + { + "success": { + "alias": "${emailAddress.removeSuffix("@duck.com")}" + } + } + """.trimIndent() + } + + private fun onSelectedNotToUseEmailProtection(autofillWebMessageRequest: AutofillWebMessageRequest) { + val message = buildResponseMessage("") + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) enqueueEmailProtectionPixel(EMAIL_TOOLTIP_DISMISSED, includeLastUsedDay = false) } @@ -130,15 +156,6 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( ) } - @Suppress("DEPRECATION") - @SuppressLint("NewApi") - private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } - override fun resultKey(tabId: String): String { return EmailProtectionChooseEmailDialog.resultKey(tabId) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignUpWebViewClient.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignUpWebViewClient.kt deleted file mode 100644 index fac79f80a763..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignUpWebViewClient.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.impl.email.incontext - -import android.graphics.Bitmap -import android.webkit.WebView -import android.webkit.WebViewClient -import javax.inject.Inject - -class EmailProtectionInContextSignUpWebViewClient @Inject constructor( - private val callback: NewPageCallback, -) : WebViewClient() { - - interface NewPageCallback { - fun onPageStarted(url: String) - } - - override fun onPageStarted( - view: WebView?, - url: String?, - favicon: Bitmap?, - ) { - url?.let { callback.onPageStarted(it) } - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupActivity.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupActivity.kt index 1fc81290114e..57bae3b15dc1 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupActivity.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupActivity.kt @@ -16,262 +16,30 @@ package com.duckduckgo.autofill.impl.email.incontext -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent import android.os.Bundle -import android.webkit.WebSettings -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.widget.Toolbar -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.fragment.app.commit import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpHandleVerificationLink -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenNoParams -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenResult -import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.api.emailprotection.EmailInjector -import com.duckduckgo.autofill.impl.AutofillJavascriptInterface +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpStartScreen import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ActivityEmailProtectionInContextSignupBinding -import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ExitButtonAction -import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState import com.duckduckgo.common.ui.DuckDuckGoActivity -import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ActivityScope -import com.duckduckgo.navigation.api.getActivityParams -import com.duckduckgo.user.agent.api.UserAgentProvider -import javax.inject.Inject -import kotlinx.coroutines.launch @InjectWith(ActivityScope::class) -@ContributeToActivityStarter(EmailProtectionInContextSignUpScreenNoParams::class) +@ContributeToActivityStarter(EmailProtectionInContextSignUpStartScreen::class) @ContributeToActivityStarter(EmailProtectionInContextSignUpHandleVerificationLink::class) -class EmailProtectionInContextSignupActivity : - DuckDuckGoActivity(), - EmailProtectionInContextSignUpWebChromeClient.ProgressListener, - EmailProtectionInContextSignUpWebViewClient.NewPageCallback { +class EmailProtectionInContextSignupActivity : DuckDuckGoActivity() { val binding: ActivityEmailProtectionInContextSignupBinding by viewBinding() - private val viewModel: EmailProtectionInContextSignupViewModel by bindViewModel() - - @Inject - lateinit var userAgentProvider: UserAgentProvider - - @Inject - lateinit var dispatchers: DispatcherProvider - - @Inject - lateinit var emailInjector: EmailInjector - - @Inject - lateinit var configurator: BrowserAutofill.Configurator - - @Inject - lateinit var autofillInterface: AutofillJavascriptInterface - - @Inject - lateinit var emailManager: EmailManager - - @Inject - lateinit var pixel: Pixel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) - initialiseToolbar() - setTitle(R.string.autofillEmailProtectionInContextSignUpDialogFeatureName) - configureWebView() - configureBackButtonHandler() - observeViewState() - configureEmailManagerObserver() - loadFirstWebpage(intent) - } - - private fun loadFirstWebpage(intent: Intent?) { - val url = intent?.getActivityParams(EmailProtectionInContextSignUpHandleVerificationLink::class.java)?.url ?: STARTING_URL - binding.webView.loadUrl(url) - - if (url == STARTING_URL) { - viewModel.loadedStartingUrl() - } - } - - private fun configureEmailManagerObserver() { - lifecycleScope.launch(dispatchers.main()) { - repeatOnLifecycle(Lifecycle.State.STARTED) { - emailManager.signedInFlow().collect() { signedIn -> - viewModel.signedInStateUpdated(signedIn, binding.webView.url) - } - } - } - } - - private fun observeViewState() { - lifecycleScope.launch(dispatchers.main()) { - repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.viewState.collect { viewState -> - when (viewState) { - is ViewState.CancellingInContextSignUp -> cancelInContextSignUp() - is ViewState.ConfirmingCancellationOfInContextSignUp -> confirmCancellationOfInContextSignUp() - is ViewState.NavigatingBack -> navigateWebViewBack() - is ViewState.ShowingWebContent -> showWebContent(viewState) - is ViewState.ExitingAsSuccess -> closeActivityAsSuccessfulSignup() - } - } - } - } - } - - private fun showWebContent(viewState: ViewState.ShowingWebContent) { - when (viewState.urlActions.exitButton) { - ExitButtonAction.Disabled -> getToolbar().navigationIcon = null - ExitButtonAction.ExitWithConfirmation -> { - getToolbar().run { - setNavigationIconAsCross() - setNavigationOnClickListener { confirmCancellationOfInContextSignUp() } - } - } - - ExitButtonAction.ExitWithoutConfirmation -> { - getToolbar().run { - setNavigationIconAsCross() - setNavigationOnClickListener { - viewModel.userCancelledSignupWithoutConfirmation() - } - } - } - - ExitButtonAction.ExitTreatAsSuccess -> { - getToolbar().run { - setNavigationIconAsCross() - setNavigationOnClickListener { closeActivityAsSuccessfulSignup() } - } - } - } - } - - private fun cancelInContextSignUp() { - setResult(EmailProtectionInContextSignUpScreenResult.CANCELLED) - finish() - } - - private fun closeActivityAsSuccessfulSignup() { - setResult(EmailProtectionInContextSignUpScreenResult.SUCCESS) - finish() - } - - private fun navigateWebViewBack() { - val previousUrl = getPreviousWebPageUrl() - binding.webView.goBack() - viewModel.consumedBackNavigation(previousUrl) - } - - private fun confirmCancellationOfInContextSignUp() { - TextAlertDialogBuilder(this) - .setTitle(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogTitle) - .setPositiveButton(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogPositiveButton) - .setNegativeButton(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogNegativeButton) - .addEventListener( - object : TextAlertDialogBuilder.EventListener() { - override fun onPositiveButtonClicked() { - viewModel.onUserDecidedNotToCancelInContextSignUp() - } - - override fun onNegativeButtonClicked() { - viewModel.onUserConfirmedCancellationOfInContextSignUp() - } - }, - ) - .show() - } - - private fun configureBackButtonHandler() { - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - viewModel.onBackButtonPressed(url = binding.webView.url, canGoBack = binding.webView.canGoBack()) - } - }, - ) - } - - private fun initialiseToolbar() { - with(getToolbar()) { - title = getString(R.string.autofillEmailProtectionInContextSignUpDialogFeatureName) - setNavigationIconAsCross() - setNavigationOnClickListener { onBackPressed() } + supportFragmentManager.commit { + replace(R.id.fragment_container, EmailProtectionInContextSignupFragment()) } } - - private fun Toolbar.setNavigationIconAsCross() { - setNavigationIcon(com.duckduckgo.mobile.android.R.drawable.ic_close_24) - } - - @SuppressLint("SetJavaScriptEnabled") - private fun configureWebView() { - binding.webView.let { - it.webViewClient = EmailProtectionInContextSignUpWebViewClient(this) - it.webChromeClient = EmailProtectionInContextSignUpWebChromeClient(this) - - it.settings.apply { - userAgentString = userAgentProvider.userAgent() - javaScriptEnabled = true - domStorageEnabled = true - loadWithOverviewMode = true - useWideViewPort = true - builtInZoomControls = true - displayZoomControls = false - mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE - setSupportMultipleWindows(true) - databaseEnabled = false - setSupportZoom(true) - } - - it.addJavascriptInterface(autofillInterface, AutofillJavascriptInterface.INTERFACE_NAME) - autofillInterface.webView = it - autofillInterface.emailProtectionInContextSignupFlowCallback = object : EmailProtectionInContextSignupFlowListener { - override fun closeInContextSignup() { - closeActivityAsSuccessfulSignup() - } - } - - emailInjector.addJsInterface(it, {}, {}) - } - } - - companion object { - private const val STARTING_URL = "https://duckduckgo.com/email/start-incontext" - - fun intent(context: Context): Intent { - return Intent(context, EmailProtectionInContextSignupActivity::class.java) - } - } - - override fun onPageStarted(url: String) { - configurator.configureAutofillForCurrentPage(binding.webView, url) - } - - override fun onPageFinished(url: String) { - viewModel.onPageFinished(url) - } - - private fun getPreviousWebPageUrl(): String? { - val webHistory = binding.webView.copyBackForwardList() - val currentIndex = webHistory.currentIndex - if (currentIndex < 0) return null - val previousIndex = currentIndex - 1 - if (previousIndex < 0) return null - return webHistory.getItemAtIndex(previousIndex)?.url - } - - private fun getToolbar() = binding.includeToolbar.toolbar as Toolbar } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt new file mode 100644 index 000000000000..5076f88c9cf6 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.email.incontext + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.webkit.WebSettings +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.webkit.WebViewCompat +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.BrowserAutofill +import com.duckduckgo.autofill.api.Callback +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpHandleVerificationLink +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenResult +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpStartScreen +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.domain.app.LoginTriggerType +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker +import com.duckduckgo.autofill.impl.R +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email.incontext.WebMessageListenerCloseEmailProtectionTab +import com.duckduckgo.autofill.impl.databinding.FragmentEmailProtectionInContextSignupBinding +import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ExitButtonAction +import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState +import com.duckduckgo.common.ui.DuckDuckGoFragment +import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.FragmentViewModelFactory +import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.navigation.api.getActivityParams +import com.duckduckgo.user.agent.api.UserAgentProvider +import javax.inject.Inject +import kotlinx.coroutines.launch + +@InjectWith(FragmentScope::class) +class EmailProtectionInContextSignupFragment : + DuckDuckGoFragment(R.layout.fragment_email_protection_in_context_signup), + EmailProtectionInContextSignUpWebChromeClient.ProgressListener, + WebMessageListenerCloseEmailProtectionTab.CloseEmailProtectionTabCallback { + + @Inject + lateinit var userAgentProvider: UserAgentProvider + + @Inject + lateinit var dispatchers: DispatcherProvider + + @Inject + lateinit var browserAutofill: BrowserAutofill + + @Inject + lateinit var emailManager: EmailManager + + @Inject + lateinit var pixel: Pixel + + @Inject + lateinit var viewModelFactory: FragmentViewModelFactory + + @Inject + lateinit var autofillCapabilityChecker: InternalAutofillCapabilityChecker + + @Inject + lateinit var webMessageListener: WebMessageListenerCloseEmailProtectionTab + + val viewModel by lazy { + ViewModelProvider(requireActivity(), viewModelFactory)[EmailProtectionInContextSignupViewModel::class.java] + } + + private val autofillConfigurationJob = ConflatedJob() + + private val binding: FragmentEmailProtectionInContextSignupBinding by viewBinding() + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + initialiseToolbar() + activity?.setTitle(R.string.autofillEmailProtectionInContextSignUpDialogFeatureName) + configureWebView() + configureBackButtonHandler() + observeViewState() + configureEmailManagerObserver() + loadFirstWebpage(activity?.intent) + } + + private fun loadFirstWebpage(intent: Intent?) { + lifecycleScope.launch(dispatchers.main()) { + autofillConfigurationJob.join() + + val url = intent?.getActivityParams(EmailProtectionInContextSignUpHandleVerificationLink::class.java)?.url ?: STARTING_URL + binding.webView.loadUrl(url) + + if (url == STARTING_URL) { + viewModel.loadedStartingUrl() + } + } + } + + private fun configureEmailManagerObserver() { + lifecycleScope.launch(dispatchers.main()) { + repeatOnLifecycle(Lifecycle.State.STARTED) { + emailManager.signedInFlow().collect { signedIn -> + viewModel.signedInStateUpdated(signedIn, binding.webView.url) + } + } + } + } + + private fun observeViewState() { + lifecycleScope.launch(dispatchers.main()) { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.viewState.collect { viewState -> + when (viewState) { + is ViewState.CancellingInContextSignUp -> cancelInContextSignUp() + is ViewState.ConfirmingCancellationOfInContextSignUp -> confirmCancellationOfInContextSignUp() + is ViewState.NavigatingBack -> navigateWebViewBack() + is ViewState.ShowingWebContent -> showWebContent(viewState) + is ViewState.ExitingAsSuccess -> closeActivityAsSuccessfulSignup() + } + } + } + } + } + + private fun showWebContent(viewState: ViewState.ShowingWebContent) { + when (viewState.urlActions.exitButton) { + ExitButtonAction.Disabled -> getToolbar().navigationIcon = null + ExitButtonAction.ExitWithConfirmation -> { + getToolbar().run { + setNavigationIconAsCross() + setNavigationOnClickListener { confirmCancellationOfInContextSignUp() } + } + } + + ExitButtonAction.ExitWithoutConfirmation -> { + getToolbar().run { + setNavigationIconAsCross() + setNavigationOnClickListener { + viewModel.userCancelledSignupWithoutConfirmation() + } + } + } + + ExitButtonAction.ExitTreatAsSuccess -> { + getToolbar().run { + setNavigationIconAsCross() + setNavigationOnClickListener { + lifecycleScope.launch(dispatchers.io()) { + closeActivityAsSuccessfulSignup() + } + } + } + } + } + } + + private suspend fun cancelInContextSignUp() { + activity?.let { + val intent = viewModel.buildResponseIntent(getMessageRequestId()) + it.setResult(EmailProtectionInContextSignUpScreenResult.CANCELLED, intent) + it.finish() + } + } + + private suspend fun closeActivityAsSuccessfulSignup() { + activity?.let { + val intent = viewModel.buildResponseIntent(getMessageRequestId()) + it.setResult(EmailProtectionInContextSignUpScreenResult.SUCCESS, intent) + it.finish() + } + } + + private fun navigateWebViewBack() { + val previousUrl = getPreviousWebPageUrl() + binding.webView.goBack() + viewModel.consumedBackNavigation(previousUrl) + } + + private fun confirmCancellationOfInContextSignUp() { + context?.let { + TextAlertDialogBuilder(it) + .setTitle(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogTitle) + .setPositiveButton(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogPositiveButton) + .setNegativeButton(R.string.autofillEmailProtectionInContextSignUpConfirmExitDialogNegativeButton) + .addEventListener( + object : TextAlertDialogBuilder.EventListener() { + override fun onPositiveButtonClicked() { + viewModel.onUserDecidedNotToCancelInContextSignUp() + } + + override fun onNegativeButtonClicked() { + viewModel.onUserConfirmedCancellationOfInContextSignUp() + } + }, + ) + .show() + } + } + + private fun configureBackButtonHandler() { + activity?.let { + it.onBackPressedDispatcher.addCallback( + it, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + viewModel.onBackButtonPressed(url = binding.webView.url, canGoBack = binding.webView.canGoBack()) + } + }, + ) + } + } + + private fun initialiseToolbar() { + with(getToolbar()) { + title = getString(R.string.autofillEmailProtectionInContextSignUpDialogFeatureName) + setNavigationIconAsCross() + setNavigationOnClickListener { activity?.onBackPressed() } + } + } + + private fun Toolbar.setNavigationIconAsCross() { + setNavigationIcon(com.duckduckgo.mobile.android.R.drawable.ic_close_24) + } + + private fun getMessageRequestId(): String { + val intent = activity?.intent + return intent?.getActivityParams(EmailProtectionInContextSignUpStartScreen::class.java)?.messageRequestId ?: intent?.getActivityParams( + EmailProtectionInContextSignUpHandleVerificationLink::class.java, + )?.messageRequestId!! + } + + @SuppressLint("SetJavaScriptEnabled", "RequiresFeature", "AddWebMessageListenerUsage") + // suppress AddWebMessageListenerUsage, we are not using DuckDuckGo WebView here. + private fun configureWebView() { + binding.webView.let { + it.webChromeClient = EmailProtectionInContextSignUpWebChromeClient(this) + + it.settings.apply { + userAgentString = userAgentProvider.userAgent() + javaScriptEnabled = true + domStorageEnabled = true + loadWithOverviewMode = true + useWideViewPort = true + builtInZoomControls = true + displayZoomControls = false + mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE + setSupportMultipleWindows(true) + databaseEnabled = false + setSupportZoom(true) + } + + autofillConfigurationJob += lifecycleScope.launch(dispatchers.main()) { + if (!autofillCapabilityChecker.webViewSupportsAutofill()) { + activity?.finish() + return@launch + } + + webMessageListener.callback = this@EmailProtectionInContextSignupFragment + WebViewCompat.addWebMessageListener(it, webMessageListener.key, webMessageListener.origins, webMessageListener) + + browserAutofill.addJsInterface( + webView = it, + tabId = "", + autofillCallback = noOpCallback, + ) + } + } + } + + companion object { + private const val STARTING_URL = "https://duckduckgo.com/email/start-incontext" + } + + override fun onPageFinished(url: String) { + viewModel.onPageFinished(url) + } + + private fun getPreviousWebPageUrl(): String? { + val webHistory = binding.webView.copyBackForwardList() + val currentIndex = webHistory.currentIndex + if (currentIndex < 0) return null + val previousIndex = currentIndex - 1 + if (previousIndex < 0) return null + return webHistory.getItemAtIndex(previousIndex)?.url + } + + private fun getToolbar() = (activity as EmailProtectionInContextSignupActivity).binding.includeToolbar.toolbar + + override suspend fun closeNativeInContextEmailProtectionSignup() { + closeActivityAsSuccessfulSignup() + } + + private val noOpCallback = object : Callback { + override suspend fun onCredentialsAvailableToInject( + autofillWebMessageRequest: AutofillWebMessageRequest, + credentials: List, + triggerType: LoginTriggerType, + ) { + } + + override suspend fun onCredentialsAvailableToSave( + autofillWebMessageRequest: AutofillWebMessageRequest, + credentials: LoginCredentials, + ) { + } + + override suspend fun onGeneratedPasswordAvailableToUse( + autofillWebMessageRequest: AutofillWebMessageRequest, + username: String?, + generatedPassword: String, + ) { + } + + override fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + } + + override fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + } + + override fun onCredentialsSaved(savedCredentials: LoginCredentials) { + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt index 41fc608d3b66..501725fc10d2 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt @@ -16,13 +16,17 @@ package com.duckduckgo.autofill.impl.email.incontext +import android.content.Intent import androidx.core.net.toUri import androidx.lifecycle.ViewModel import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpScreenResult +import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.BackButtonAction.NavigateBack import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.CHOOSE_ADDRESS import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.DEFAULT_URL_ACTIONS +import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.EMAIL_SETTINGS_URL import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.EMAIL_VERIFICATION_LINK_URL import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.IN_CONTEXT_SUCCESS import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.Companion.Urls.REVIEW_INPUT @@ -33,15 +37,18 @@ import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSign import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState.ExitingAsSuccess import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState.NavigatingBack import com.duckduckgo.autofill.impl.email.incontext.EmailProtectionInContextSignupViewModel.ViewState.ShowingWebContent +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_PROTECTION_IN_CONTEXT_MODAL_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_PROTECTION_IN_CONTEXT_MODAL_DISPLAYED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_PROTECTION_IN_CONTEXT_MODAL_EXIT_EARLY_CANCEL import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_PROTECTION_IN_CONTEXT_MODAL_EXIT_EARLY_CONFIRM +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.absoluteString import com.duckduckgo.di.scopes.ActivityScope import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext import timber.log.Timber @ContributesViewModel(ActivityScope::class) @@ -49,6 +56,15 @@ class EmailProtectionInContextSignupViewModel @Inject constructor( private val pixel: Pixel, ) : ViewModel() { + @Inject + lateinit var responseWriter: AutofillResponseWriter + + @Inject + lateinit var emailManager: EmailManager + + @Inject + lateinit var dispatchers: DispatcherProvider + private val _viewState = MutableStateFlow(ShowingWebContent(urlActions = DEFAULT_URL_ACTIONS)) val viewState: StateFlow = _viewState @@ -109,16 +125,27 @@ class EmailProtectionInContextSignupViewModel @Inject constructor( _viewState.value = CancellingInContextSignUp } + suspend fun buildResponseIntent(messageRequestId: String): Intent { + return withContext(dispatchers.io()) { + val isSignedIn = emailManager.isSignedIn() + val message = responseWriter.generateResponseForEmailProtectionEndOfFlow(isSignedIn) + Intent().also { + it.putExtra(EmailProtectionInContextSignUpScreenResult.RESULT_KEY_MESSAGE, message) + it.putExtra(EmailProtectionInContextSignUpScreenResult.RESULT_KEY_REQUEST_ID, messageRequestId) + } + } + } + fun signedInStateUpdated( signedIn: Boolean, url: String?, ) { Timber.i("Now signed in: %s. Current URL is %s", signedIn, url) - if (!signedIn) return + if (!signedIn || url == null) return - if (url?.contains(EMAIL_VERIFICATION_LINK_URL) == true) { - Timber.d("Detected email verification link") + if (url.contains(EMAIL_VERIFICATION_LINK_URL) || url.contains(EMAIL_SETTINGS_URL)) { + Timber.d("Detected email verification link or signed in state") _viewState.value = ExitingAsSuccess } } @@ -168,6 +195,7 @@ class EmailProtectionInContextSignupViewModel @Inject constructor( const val IN_CONTEXT_SUCCESS = "https://duckduckgo.com/email/welcome-incontext" const val EMAIL_VERIFICATION_LINK_URL = "https://duckduckgo.com/email/login?" + const val EMAIL_SETTINGS_URL = "https://duckduckgo.com/email/settings" val DEFAULT_URL_ACTIONS = UrlActions(backButton = NavigateBack, exitButton = ExitWithoutConfirmation) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt index cf078914ed8c..4a0b07f2ff57 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt @@ -16,22 +16,25 @@ package com.duckduckgo.autofill.impl.email.incontext -import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle -import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.Companion.KEY_RESULT +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.* +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.Cancel +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.DoNotShowAgain +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.SignUp import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -39,60 +42,66 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(AppScope::class) +@ContributesMultibinding(FragmentScope::class) class ResultHandlerInContextEmailProtectionPrompt @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, private val dataStore: EmailProtectionInContextDataStore, - private val appBuildConfig: AppBuildConfig, + private val messagePoster: AutofillMessagePoster, ) : AutofillFragmentResultsPlugin { - override fun processResult(result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener) { + override fun processResult( + result: Bundle, + context: Context, + tabId: String, + fragment: Fragment, + autofillCallback: AutofillEventListener, + ) { Timber.d("${this::class.java.simpleName}: processing result") - val userSelection = result.safeGetParcelable(EmailProtectionInContextSignUpDialog.KEY_RESULT) ?: return + val userSelection = BundleCompat.getParcelable(result, KEY_RESULT, EmailProtectionInContextSignUpResult::class.java) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return appCoroutineScope.launch(dispatchers.io()) { when (userSelection) { - SignUp -> signUpSelected(autofillCallback) - Cancel -> cancelled(autofillCallback) - DoNotShowAgain -> doNotAskAgain(autofillCallback) + SignUp -> signUpSelected(autofillCallback, autofillWebMessageRequest) + Cancel -> cancelled(autofillWebMessageRequest) + DoNotShowAgain -> doNotAskAgain(autofillWebMessageRequest) } } } - private suspend fun signUpSelected(autofillCallback: AutofillEventListener) { + private suspend fun signUpSelected( + autofillCallback: AutofillEventListener, + autofillWebMessageRequest: AutofillWebMessageRequest, + ) { withContext(dispatchers.main()) { - autofillCallback.onSelectedToSignUpForInContextEmailProtection() + autofillCallback.onSelectedToSignUpForInContextEmailProtection(autofillWebMessageRequest) } } - private suspend fun doNotAskAgain(autofillCallback: AutofillEventListener) { + private suspend fun doNotAskAgain(autofillWebMessageRequest: AutofillWebMessageRequest) { Timber.i("User selected to not show sign up for email protection again") dataStore.onUserChoseNeverAskAgain() - notifyEndOfFlow(autofillCallback) + notifyEndOfFlow(autofillWebMessageRequest) } - private suspend fun cancelled(autofillCallback: AutofillEventListener) { + private suspend fun cancelled(autofillWebMessageRequest: AutofillWebMessageRequest) { Timber.i("User cancelled sign up for email protection") - notifyEndOfFlow(autofillCallback) + notifyEndOfFlow(autofillWebMessageRequest) } - private suspend fun notifyEndOfFlow(autofillCallback: AutofillEventListener) { - withContext(dispatchers.main()) { - autofillCallback.onEndOfEmailProtectionInContextSignupFlow() - } + private fun notifyEndOfFlow(autofillWebMessageRequest: AutofillWebMessageRequest) { + val message = """ + { + "success": { + "isSignedIn": false + } + } + """.trimIndent() + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) } override fun resultKey(tabId: String): String { return EmailProtectionInContextSignUpDialog.resultKey(tabId) } - - @Suppress("DEPRECATION") - @SuppressLint("NewApi") - private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/prompt/EmailProtectionInContextSignUpPromptFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/prompt/EmailProtectionInContextSignUpPromptFragment.kt index d44b635b2fd2..17cb8d708cc9 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/prompt/EmailProtectionInContextSignUpPromptFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/prompt/EmailProtectionInContextSignUpPromptFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider @@ -29,6 +30,7 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.DialogEmailProtectionInContextSignUpBinding @@ -121,6 +123,7 @@ class EmailProtectionInContextSignUpPromptFragment : BottomSheetDialogFragment() val result = Bundle().also { it.putParcelable(EmailProtectionInContextSignUpDialog.KEY_RESULT, resultType) + it.putParcelable(EmailProtectionInContextSignUpDialog.KEY_URL, getAutofillWebMessageRequest()) } parentFragment?.setFragmentResult(EmailProtectionInContextSignUpDialog.resultKey(getTabId()), result) @@ -133,18 +136,22 @@ class EmailProtectionInContextSignUpPromptFragment : BottomSheetDialogFragment() } private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! + private fun getAutofillWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! companion object { fun instance( tabId: String, + autofillWebMessageRequest: AutofillWebMessageRequest, ): EmailProtectionInContextSignUpPromptFragment { val fragment = EmailProtectionInContextSignUpPromptFragment() fragment.arguments = Bundle().also { it.putString(KEY_TAB_ID, tabId) + it.putParcelable(KEY_URL, autofillWebMessageRequest) } return fragment } private const val KEY_TAB_ID = "tabId" + private const val KEY_URL = "url" } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/AutofillMessagePoster.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/AutofillMessagePoster.kt index 092781fc2475..94544d2a0d1f 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/AutofillMessagePoster.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/AutofillMessagePoster.kt @@ -17,48 +17,28 @@ package com.duckduckgo.autofill.impl.jsbridge import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.core.net.toUri -import androidx.webkit.WebMessageCompat -import androidx.webkit.WebViewCompat -import androidx.webkit.WebViewFeature -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesBinding +import dagger.SingleInstanceIn import javax.inject.Inject -import kotlinx.coroutines.withContext import timber.log.Timber interface AutofillMessagePoster { - suspend fun postMessage( - webView: WebView?, - message: String, - ) + fun postMessage(message: String, requestId: String) } -@ContributesBinding(AppScope::class) +@SuppressLint("RequiresFeature") +@SingleInstanceIn(FragmentScope::class) +@ContributesBinding(FragmentScope::class) class AutofillWebViewMessagePoster @Inject constructor( - private val dispatchers: DispatcherProvider, + private val webMessageListeners: PluginPoint, ) : AutofillMessagePoster { - @SuppressLint("RequiresFeature") - override suspend fun postMessage( - webView: WebView?, - message: String, - ) { - webView?.let { wv -> - withContext(dispatchers.main()) { - if (!WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { - Timber.e("Unable to post web message") - return@withContext - } - - WebViewCompat.postWebMessage(wv, WebMessageCompat(message), WILDCARD_ORIGIN_URL) - } + override fun postMessage(message: String, requestId: String) { + webMessageListeners.getPlugins().firstOrNull { it.onResponse(message, requestId) } ?: { + Timber.w("No listener found for requestId: %s", requestId) } } - - companion object { - private val WILDCARD_ORIGIN_URL = "*".toUri() - } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt index 4d3e20152b03..50d47d0e9569 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt @@ -44,6 +44,10 @@ data class RejectGeneratedPasswordResponse( data class RejectGeneratedPassword(val action: String = "rejectGeneratedPassword") } +data class EmailProtectionSignedInResponse( + val success: Boolean, +) + data class EmptyResponse( val type: String = "getAutofillDataResponse", val success: EmptyCredentialResponse, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt index 6f9fd01ee623..fa742b14aa14 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt @@ -28,6 +28,7 @@ interface AutofillResponseWriter { fun generateEmptyResponseGetAutofillData(): String fun generateResponseForAcceptingGeneratedPassword(): String fun generateResponseForRejectingGeneratedPassword(): String + fun generateResponseForEmailProtectionIsSignedIn(signedIn: Boolean): String fun generateResponseForEmailProtectionInContextSignup(installedRecently: Boolean, permanentlyDismissedAtTimestamp: Long?): String fun generateResponseForEmailProtectionEndOfFlow(isSignedIn: Boolean): String } @@ -39,6 +40,7 @@ class AutofillJsonResponseWriter @Inject constructor(val moshi: Moshi) : Autofil private val autofillDataAdapterCredentialsUnavailable = moshi.adapter(EmptyResponse::class.java).indent(" ") private val autofillDataAdapterAcceptGeneratedPassword = moshi.adapter(AcceptGeneratedPasswordResponse::class.java).indent(" ") private val autofillDataAdapterRejectGeneratedPassword = moshi.adapter(RejectGeneratedPasswordResponse::class.java).indent(" ") + private val emailProtectionSignedIn = moshi.adapter(EmailProtectionSignedInResponse::class.java).indent(" ") private val emailProtectionDataAdapterInContextSignup = moshi.adapter(EmailProtectionInContextSignupDismissedAtResponse::class.java).indent(" ") private val emailDataAdapterInContextEndOfFlow = moshi.adapter(ShowInContextEmailProtectionSignupPromptResponse::class.java).indent(" ") @@ -66,6 +68,11 @@ class AutofillJsonResponseWriter @Inject constructor(val moshi: Moshi) : Autofil return autofillDataAdapterRejectGeneratedPassword.toJson(topLevelResponse) } + override fun generateResponseForEmailProtectionIsSignedIn(signedIn: Boolean): String { + val response = EmailProtectionSignedInResponse(signedIn) + return emailProtectionSignedIn.toJson(response) + } + override fun generateResponseForEmailProtectionInContextSignup(installedRecently: Boolean, permanentlyDismissedAtTimestamp: Long?): String { val response = DismissedAt(isInstalledRecently = installedRecently, permanentlyDismissedAt = permanentlyDismissedAtTimestamp) val topLevelResponse = EmailProtectionInContextSignupDismissedAtResponse(success = response) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt index badc6a00573c..3873de74840d 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.ui import androidx.fragment.app.DialogFragment +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.domain.app.LoginCredentials @@ -35,29 +36,29 @@ import javax.inject.Inject class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialAutofillDialogFactory { override fun autofillSelectCredentialsDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, tabId: String, ): DialogFragment { - return AutofillSelectCredentialsDialogFragment.instance(url, credentials, triggerType, tabId) + return AutofillSelectCredentialsDialogFragment.instance(autofillWebMessageRequest, credentials, triggerType, tabId) } override fun autofillSavingCredentialsDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment { - return AutofillSavingCredentialsDialogFragment.instance(url, credentials, tabId) + return AutofillSavingCredentialsDialogFragment.instance(autofillWebMessageRequest, credentials, tabId) } override fun autofillSavingUpdatePasswordDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment { return AutofillUpdatingExistingCredentialsDialogFragment.instance( - url, + autofillWebMessageRequest, credentials, tabId, CredentialUpdateType.Password, @@ -65,12 +66,12 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA } override fun autofillSavingUpdateUsernameDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): DialogFragment { return AutofillUpdatingExistingCredentialsDialogFragment.instance( - url, + autofillWebMessageRequest, credentials, tabId, CredentialUpdateType.Username, @@ -78,27 +79,27 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA } override fun autofillGeneratePasswordDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, tabId: String, ): DialogFragment { - return AutofillUseGeneratedPasswordDialogFragment.instance(url, username, generatedPassword, tabId) + return AutofillUseGeneratedPasswordDialogFragment.instance(autofillWebMessageRequest, username, generatedPassword, tabId) } override fun autofillEmailProtectionEmailChooserDialog( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, personalDuckAddress: String, tabId: String, ): DialogFragment { return EmailProtectionChooseEmailFragment.instance( personalDuckAddress = personalDuckAddress, - url = url, + url = autofillWebMessageRequest, tabId = tabId, ) } - override fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment { - return EmailProtectionInContextSignUpPromptFragment.instance(tabId) + override fun emailProtectionInContextSignUpDialog(tabId: String, autofillWebMessageRequest: AutofillWebMessageRequest): DialogFragment { + return EmailProtectionInContextSignUpPromptFragment.instance(tabId, autofillWebMessageRequest) } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt index 151aed68cee7..f7b3c3869786 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt @@ -33,6 +33,7 @@ import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Sync import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Unknown import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration @@ -133,6 +134,7 @@ class AutofillSettingsViewModel @Inject constructor( private val autofillBreakageReportSender: AutofillBreakageReportSender, private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore, private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules, + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -410,7 +412,11 @@ class AutofillSettingsViewModel @Inject constructor( fun onViewCreated() { if (combineJob != null) return combineJob = viewModelScope.launch(dispatchers.io()) { - _viewState.value = _viewState.value.copy(autofillEnabled = autofillStore.autofillEnabled) + _viewState.value = _viewState.value.copy( + autofillEnabled = autofillStore.autofillEnabled, + isAutofillSupported = autofillCapabilityChecker.webViewSupportsAutofill() && + autofillCapabilityChecker.isAutofillEnabledByConfiguration(""), + ) val allCredentials = autofillStore.getAllCredentials().distinctUntilChanged() val combined = allCredentials.combine(searchQueryFilter) { credentials, filter -> @@ -779,6 +785,7 @@ class AutofillSettingsViewModel @Inject constructor( val credentialSearchQuery: String = "", val reportBreakageState: ReportBreakageState = ReportBreakageState(), val canShowPromo: Boolean = false, + val isAutofillSupported: Boolean = true, ) data class ReportBreakageState( diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt index 45a54b9a7c6e..a811f5b0b076 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt @@ -332,7 +332,11 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.viewState.collect { state -> - binding.enabledToggle.quietlySetIsChecked(state.autofillEnabled, globalAutofillToggleListener) + if (state.isAutofillSupported) { + binding.enabledToggle.quietlySetIsChecked(state.autofillEnabled, globalAutofillToggleListener) + } else { + binding.enabledToggle.isEnabled = false + } state.logins?.let { credentialsListUpdated(it, state.credentialSearchQuery, state.reportBreakageState.allowBreakageReporting) parentActivity()?.invalidateOptionsMenu() diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt index ba9d28a3c7b3..4522dbe5733f 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt @@ -26,11 +26,17 @@ import android.view.TouchDelegate import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog +import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_PASSWORD +import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_TAB_ID +import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_URL +import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_USERNAME import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ContentAutofillGeneratePasswordDialogBinding import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames @@ -100,9 +106,8 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), private fun configureViews(binding: ContentAutofillGeneratePasswordDialogBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED - val originalUrl = getOriginalUrl() configureCloseButton(binding) - configureGeneratePasswordButton(binding, originalUrl) + configureGeneratePasswordButton(binding) configurePasswordField(binding) configureSubtitleText(binding) } @@ -146,18 +151,15 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), return appBuildConfig.sdkInt <= VERSION_CODES.S_V2 } - private fun configureGeneratePasswordButton( - binding: ContentAutofillGeneratePasswordDialogBinding, - originalUrl: String, - ) { + private fun configureGeneratePasswordButton(binding: ContentAutofillGeneratePasswordDialogBinding) { binding.useSecurePasswordButton.setOnClickListener { pixelNameDialogEvent(GeneratedPasswordAccepted)?.let { pixel.fire(it) } val result = Bundle().also { - it.putString(UseGeneratedPasswordDialog.KEY_URL, originalUrl) + it.putParcelable(KEY_URL, getAutofillWebMessageRequest()) it.putBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED, true) - it.putString(UseGeneratedPasswordDialog.KEY_USERNAME, getUsername()) - it.putString(UseGeneratedPasswordDialog.KEY_PASSWORD, getGeneratedPassword()) + it.putString(KEY_USERNAME, getUsername()) + it.putString(KEY_PASSWORD, getGeneratedPassword()) } parentFragment?.setFragmentResult(UseGeneratedPasswordDialog.resultKey(getTabId()), result) @@ -183,7 +185,7 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), val result = Bundle().also { it.putBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED, false) - it.putString(UseGeneratedPasswordDialog.KEY_URL, getOriginalUrl()) + it.putParcelable(KEY_URL, getAutofillWebMessageRequest()) } parentFragment?.setFragmentResult(UseGeneratedPasswordDialog.resultKey(getTabId()), result) @@ -208,15 +210,15 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), object GeneratedPasswordAccepted : DialogEvent } - private fun getOriginalUrl() = arguments?.getString(UseGeneratedPasswordDialog.KEY_URL)!! - private fun getUsername() = arguments?.getString(UseGeneratedPasswordDialog.KEY_USERNAME) - private fun getGeneratedPassword() = arguments?.getString(UseGeneratedPasswordDialog.KEY_PASSWORD)!! - private fun getTabId() = arguments?.getString(UseGeneratedPasswordDialog.KEY_TAB_ID)!! + private fun getAutofillWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! + private fun getUsername() = arguments?.getString(KEY_USERNAME) + private fun getGeneratedPassword() = arguments?.getString(KEY_PASSWORD)!! + private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! companion object { fun instance( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, username: String?, generatedPassword: String, tabId: String, @@ -224,10 +226,10 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), val fragment = AutofillUseGeneratedPasswordDialogFragment() fragment.arguments = Bundle().also { - it.putString(UseGeneratedPasswordDialog.KEY_URL, url) - it.putString(UseGeneratedPasswordDialog.KEY_USERNAME, username) - it.putString(UseGeneratedPasswordDialog.KEY_PASSWORD, generatedPassword) - it.putString(UseGeneratedPasswordDialog.KEY_TAB_ID, tabId) + it.putParcelable(KEY_URL, autofillWebMessageRequest) + it.putString(KEY_USERNAME, username) + it.putString(KEY_PASSWORD, generatedPassword) + it.putString(KEY_TAB_ID, tabId) } return fragment } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPassword.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPassword.kt index 353e4bf75244..230ecbe2d40c 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPassword.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPassword.kt @@ -18,20 +18,25 @@ package com.duckduckgo.autofill.impl.ui.credential.passwordgeneration import android.content.Context import android.os.Bundle +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog +import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -39,12 +44,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(AppScope::class) +@ContributesMultibinding(FragmentScope::class) class ResultHandlerUseGeneratedPassword @Inject constructor( private val dispatchers: DispatcherProvider, private val autofillStore: InternalAutofillStore, private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, private val existingCredentialMatchDetector: ExistingCredentialMatchDetector, + private val messagePoster: AutofillMessagePoster, + private val responseWriter: AutofillResponseWriter, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { @@ -58,28 +65,43 @@ class ResultHandlerUseGeneratedPassword @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val originalUrl = result.getString(UseGeneratedPasswordDialog.KEY_URL) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return if (result.getBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED)) { appCoroutineScope.launch(dispatchers.io()) { - onUserAcceptedToUseGeneratedPassword(result, tabId, originalUrl, autofillCallback) + onUserAcceptedToUseGeneratedPassword(result, tabId, autofillWebMessageRequest) } } else { appCoroutineScope.launch(dispatchers.main()) { - autofillCallback.onRejectGeneratedPassword(originalUrl) + rejectGeneratedPassword(autofillWebMessageRequest) } } } + fun acceptGeneratedPassword(autofillWebMessageRequest: AutofillWebMessageRequest) { + Timber.v("Accepting generated password") + appCoroutineScope.launch(dispatchers.io()) { + val message = responseWriter.generateResponseForAcceptingGeneratedPassword() + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) + } + } + + private fun rejectGeneratedPassword(autofillWebMessageRequest: AutofillWebMessageRequest) { + Timber.v("Rejecting generated password") + appCoroutineScope.launch(dispatchers.io()) { + val message = responseWriter.generateResponseForRejectingGeneratedPassword() + messagePoster.postMessage(message, autofillWebMessageRequest.requestId) + } + } + private suspend fun onUserAcceptedToUseGeneratedPassword( result: Bundle, tabId: String, - originalUrl: String, - callback: AutofillEventListener, + autofillWebMessageRequest: AutofillWebMessageRequest, ) { val username = result.getString(UseGeneratedPasswordDialog.KEY_USERNAME) val password = result.getString(UseGeneratedPasswordDialog.KEY_PASSWORD) ?: return val autologinId = autoSavedLoginsMonitor.getAutoSavedLoginId(tabId) - val matchType = existingCredentialMatchDetector.determine(originalUrl, username, password) + val matchType = existingCredentialMatchDetector.determine(autofillWebMessageRequest.requestOrigin, username, password) Timber.v( "autoSavedLoginId: %s. Match type against existing entries: %s", autologinId, @@ -87,18 +109,18 @@ class ResultHandlerUseGeneratedPassword @Inject constructor( ) if (autologinId == null) { - saveLoginIfNotAlreadySaved(matchType, originalUrl, username, password, tabId) + saveLoginIfNotAlreadySaved(matchType, autofillWebMessageRequest.requestOrigin, username, password, tabId) } else { val existingAutoSavedLogin = autofillStore.getCredentialsWithId(autologinId) if (existingAutoSavedLogin == null) { Timber.w("Can't find saved login with autosavedLoginId: $autologinId") - saveLoginIfNotAlreadySaved(matchType, originalUrl, username, password, tabId) + saveLoginIfNotAlreadySaved(matchType, autofillWebMessageRequest.requestOrigin, username, password, tabId) } else { updateLoginIfDifferent(existingAutoSavedLogin, username, password) } } withContext(dispatchers.main()) { - callback.onAcceptGeneratedPassword(originalUrl) + acceptGeneratedPassword(autofillWebMessageRequest) } autofilledListeners.getPlugins().forEach { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt index 79424fac69a6..a0de787a15a5 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.core.view.isVisible import androidx.fragment.app.setFragmentResult import androidx.lifecycle.Lifecycle @@ -32,7 +33,11 @@ import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialSavePickerDialog +import com.duckduckgo.autofill.api.CredentialSavePickerDialog.Companion.KEY_CREDENTIALS +import com.duckduckgo.autofill.api.CredentialSavePickerDialog.Companion.KEY_TAB_ID +import com.duckduckgo.autofill.api.CredentialSavePickerDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.autofill.impl.R @@ -180,12 +185,12 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre pixelNameDialogEvent(Accepted, binding.keyFeaturesContainer.isVisible)?.let { pixel.fire(it) } lifecycleScope.launch(dispatcherProvider.io()) { - faviconManager.persistCachedFavicon(getTabId(), getOriginalUrl()) + faviconManager.persistCachedFavicon(getTabId(), getWebMessageRequest().requestOrigin) } val result = Bundle().also { - it.putString(CredentialSavePickerDialog.KEY_URL, getOriginalUrl()) - it.putParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS, getCredentialsToSave()) + it.putParcelable(KEY_URL, getWebMessageRequest()) + it.putParcelable(KEY_CREDENTIALS, getCredentialsToSave()) } parentFragment?.setFragmentResult(CredentialSavePickerDialog.resultKeyUserChoseToSaveCredentials(getTabId()), result) @@ -212,7 +217,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val parentFragmentForResult = parentFragment appCoroutineScope.launch(dispatcherProvider.io()) { - autofillDeclineCounter.userDeclinedToSaveCredentials(getOriginalUrl().extractDomain()) + autofillDeclineCounter.userDeclinedToSaveCredentials(getWebMessageRequest().requestOrigin.extractDomain()) if (autofillDeclineCounter.shouldPromptToDisableAutofill()) { parentFragmentForResult?.setFragmentResult(CredentialSavePickerDialog.resultKeyShouldPromptToDisableAutofill(getTabId()), Bundle()) @@ -224,7 +229,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre private fun onUserChoseNeverSaveThisSite() { pixelNameDialogEvent(Exclude, isOnboardingMode())?.let { pixel.fire(it) } - viewModel.addSiteToNeverSaveList(getOriginalUrl()) + viewModel.addSiteToNeverSaveList(getWebMessageRequest().requestOrigin) // this is another way to refuse saving credentials, so ensure that normal logic still runs onUserRejectedToSaveCredentials() @@ -279,23 +284,23 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre object Exclude : DialogEvent } - private fun getCredentialsToSave() = arguments?.getParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS)!! - private fun getTabId() = arguments?.getString(CredentialSavePickerDialog.KEY_TAB_ID)!! - private fun getOriginalUrl() = arguments?.getString(CredentialSavePickerDialog.KEY_URL)!! + private fun getCredentialsToSave() = BundleCompat.getParcelable(requireArguments(), KEY_CREDENTIALS, LoginCredentials::class.java)!! + private fun getTabId() = requireArguments().getString(KEY_TAB_ID)!! + private fun getWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! companion object { fun instance( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, ): AutofillSavingCredentialsDialogFragment { val fragment = AutofillSavingCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putString(CredentialSavePickerDialog.KEY_URL, url) - it.putParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS, credentials) - it.putString(CredentialSavePickerDialog.KEY_TAB_ID, tabId) + it.putParcelable(KEY_URL, autofillWebMessageRequest) + it.putParcelable(KEY_CREDENTIALS, credentials) + it.putString(KEY_TAB_ID, tabId) } return fragment } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt index 25441bf51617..0bdb84d9b312 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt @@ -16,17 +16,17 @@ package com.duckduckgo.autofill.impl.ui.credential.saving -import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle -import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialSavePickerDialog +import com.duckduckgo.autofill.api.CredentialSavePickerDialog.Companion.KEY_CREDENTIALS +import com.duckduckgo.autofill.api.CredentialSavePickerDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.autofill.impl.store.InternalAutofillStore @@ -46,7 +46,6 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( private val dispatchers: DispatcherProvider, private val declineCounter: AutofillDeclineCounter, private val autofillStore: InternalAutofillStore, - private val appBuildConfig: AppBuildConfig, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : AutofillFragmentResultsPlugin { @@ -61,12 +60,11 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( autofillFireproofDialogSuppressor.autofillSaveOrUpdateDialogVisibilityChanged(visible = false) - val originalUrl = result.getString(CredentialSavePickerDialog.KEY_URL) ?: return - val selectedCredentials = - result.safeGetParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return + val selectedCredentials = BundleCompat.getParcelable(result, KEY_CREDENTIALS, LoginCredentials::class.java) ?: return appCoroutineScope.launch(dispatchers.io()) { - val savedCredentials = autofillStore.saveCredentials(originalUrl, selectedCredentials) + val savedCredentials = autofillStore.saveCredentials(autofillWebMessageRequest.requestOrigin, selectedCredentials) if (savedCredentials != null) { declineCounter.disableDeclineCounter() @@ -77,15 +75,6 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( } } - @Suppress("DEPRECATION") - @SuppressLint("NewApi") - private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } - override fun resultKey(tabId: String): String { return CredentialSavePickerDialog.resultKeyUserChoseToSaveCredentials(tabId) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsDialogFragment.kt index 02eda26dab3a..049320fe7256 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsDialogFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/AutofillSelectCredentialsDialogFragment.kt @@ -22,11 +22,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_CREDENTIALS +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_TAB_ID +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_TRIGGER_TYPE +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_URL_REQUEST import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType import com.duckduckgo.autofill.api.domain.app.LoginTriggerType.AUTOPROMPT @@ -103,8 +109,7 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre private fun configureViews(binding: ContentAutofillSelectCredentialsTooltipBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED - val originalUrl = getOriginalUrl() - configureRecyclerView(originalUrl, binding) + configureRecyclerView(getUrlRequest(), binding) configureCloseButton(binding) } @@ -113,10 +118,10 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre } private fun configureRecyclerView( - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, binding: ContentAutofillSelectCredentialsTooltipBinding, ) { - binding.availableCredentialsRecycler.adapter = configureAdapter(getAvailableCredentials(originalUrl)) + binding.availableCredentialsRecycler.adapter = configureAdapter(getAvailableCredentials(autofillWebMessageRequest)) } private fun configureAdapter(credentials: List): CredentialsPickerRecyclerAdapter { @@ -131,8 +136,8 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val result = Bundle().also { it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, false) - it.putString(CredentialAutofillPickerDialog.KEY_URL, getOriginalUrl()) - it.putParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS, selectedCredentials) + it.putParcelable(KEY_URL_REQUEST, getUrlRequest()) + it.putParcelable(KEY_CREDENTIALS, selectedCredentials) } parentFragment?.setFragmentResult(CredentialAutofillPickerDialog.resultKey(getTabId()), result) @@ -153,7 +158,7 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val result = Bundle().also { it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, true) - it.putString(CredentialAutofillPickerDialog.KEY_URL, getOriginalUrl()) + it.putParcelable(KEY_URL_REQUEST, getUrlRequest()) } parentFragment?.setFragmentResult(CredentialAutofillPickerDialog.resultKey(getTabId()), result) @@ -176,20 +181,20 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre object Selected : DialogEvent } - private fun getAvailableCredentials(originalUrl: String): List { - val unsortedCredentials = arguments?.getParcelableArrayList(CredentialAutofillPickerDialog.KEY_CREDENTIALS)!! - val grouped = autofillSelectCredentialsGrouper.group(originalUrl, unsortedCredentials) + private fun getAvailableCredentials(autofillWebMessageRequest: AutofillWebMessageRequest): List { + val unsortedCredentials = BundleCompat.getParcelableArrayList(requireArguments(), KEY_CREDENTIALS, LoginCredentials::class.java)!! + val grouped = autofillSelectCredentialsGrouper.group(autofillWebMessageRequest.requestOrigin, unsortedCredentials) return autofillSelectCredentialsListBuilder.buildFlatList(grouped) } - private fun getOriginalUrl() = arguments?.getString(CredentialAutofillPickerDialog.KEY_URL)!! - private fun getTriggerType() = arguments?.getSerializable(CredentialAutofillPickerDialog.KEY_TRIGGER_TYPE) as LoginTriggerType - private fun getTabId() = arguments?.getString(CredentialAutofillPickerDialog.KEY_TAB_ID)!! + private fun getUrlRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL_REQUEST, AutofillWebMessageRequest::class.java)!! + private fun getTriggerType() = arguments?.getSerializable(KEY_TRIGGER_TYPE) as LoginTriggerType + private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! companion object { fun instance( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: List, triggerType: LoginTriggerType, tabId: String, @@ -199,10 +204,10 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val fragment = AutofillSelectCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putString(CredentialAutofillPickerDialog.KEY_URL, url) - it.putParcelableArrayList(CredentialAutofillPickerDialog.KEY_CREDENTIALS, cr) - it.putSerializable(CredentialAutofillPickerDialog.KEY_TRIGGER_TYPE, triggerType) - it.putString(CredentialAutofillPickerDialog.KEY_TAB_ID, tabId) + it.putParcelable(KEY_URL_REQUEST, autofillWebMessageRequest) + it.putParcelableArrayList(KEY_CREDENTIALS, cr) + it.putSerializable(KEY_TRIGGER_TYPE, triggerType) + it.putString(KEY_TAB_ID, tabId) } return fragment } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt index 1ef8f2569f5e..20ab22b45d93 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt @@ -16,26 +16,29 @@ package com.duckduckgo.autofill.impl.ui.credential.selecting -import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle -import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_CREDENTIALS +import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog.Companion.KEY_URL_REQUEST import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator +import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -43,17 +46,22 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(AppScope::class) +@ContributesMultibinding(FragmentScope::class) class ResultHandlerCredentialSelection @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, private val pixel: Pixel, private val deviceAuthenticator: DeviceAuthenticator, - private val appBuildConfig: AppBuildConfig, private val autofillStore: InternalAutofillStore, + private val messagePoster: AutofillMessagePoster, + private val autofillResponseWriter: AutofillResponseWriter, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { + override fun resultKey(tabId: String): String { + return CredentialAutofillPickerDialog.resultKey(tabId) + } + override fun processResult( result: Bundle, context: Context, @@ -63,11 +71,11 @@ class ResultHandlerCredentialSelection @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val originalUrl = result.getString(CredentialAutofillPickerDialog.KEY_URL) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL_REQUEST, AutofillWebMessageRequest::class.java) ?: return if (result.getBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED)) { Timber.v("Autofill: User cancelled credential selection") - autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) + injectNoCredentials(autofillWebMessageRequest) return } @@ -75,20 +83,37 @@ class ResultHandlerCredentialSelection @Inject constructor( processAutofillCredentialSelectionResult( result = result, browserTabFragment = fragment, - autofillCallback = autofillCallback, - originalUrl = originalUrl, + autofillWebMessageRequest = autofillWebMessageRequest, ) } } + private fun injectCredentials( + credentials: LoginCredentials, + autofillWebMessageRequest: AutofillWebMessageRequest, + ) { + Timber.v("Informing JS layer with credentials selected") + appCoroutineScope.launch(dispatchers.io()) { + val jsCredentials = credentials.asJsCredentials() + val jsonResponse = autofillResponseWriter.generateResponseGetAutofillData(jsCredentials) + messagePoster.postMessage(jsonResponse, autofillWebMessageRequest.requestId) + } + } + + private fun injectNoCredentials(autofillWebMessageRequest: AutofillWebMessageRequest) { + Timber.v("No credentials selected; informing JS layer") + appCoroutineScope.launch(dispatchers.io()) { + val jsonResponse = autofillResponseWriter.generateEmptyResponseGetAutofillData() + messagePoster.postMessage(jsonResponse, autofillWebMessageRequest.requestId) + } + } + private suspend fun processAutofillCredentialSelectionResult( result: Bundle, browserTabFragment: Fragment, - autofillCallback: AutofillEventListener, - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, ) { - val selectedCredentials: LoginCredentials = - result.safeGetParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS) ?: return + val selectedCredentials = BundleCompat.getParcelable(result, KEY_CREDENTIALS, LoginCredentials::class.java) ?: return selectedCredentials.updateLastUsedTimestamp() @@ -103,19 +128,19 @@ class ResultHandlerCredentialSelection @Inject constructor( Timber.v("Autofill: user selected credential to use, and successfully authenticated") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_SUCCESSFUL) notifyAutofilledListeners() - autofillCallback.onShareCredentialsForAutofill(originalUrl, selectedCredentials) + injectCredentials(selectedCredentials, autofillWebMessageRequest) } DeviceAuthenticator.AuthResult.UserCancelled -> { Timber.d("Autofill: user selected credential to use, but cancelled without authenticating") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_CANCELLED) - autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) + injectNoCredentials(autofillWebMessageRequest) } is DeviceAuthenticator.AuthResult.Error -> { Timber.w("Autofill: user selected credential to use, but there was an error when authenticating: ${it.reason}") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_FAILURE) - autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) + injectNoCredentials(autofillWebMessageRequest) } } } @@ -135,16 +160,10 @@ class ResultHandlerCredentialSelection @Inject constructor( } } - @Suppress("DEPRECATION") - @SuppressLint("NewApi") - private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } - - override fun resultKey(tabId: String): String { - return CredentialAutofillPickerDialog.resultKey(tabId) + private fun LoginCredentials.asJsCredentials(): JavascriptCredentials { + return JavascriptCredentials( + username = username, + password = password, + ) } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt index ee6fd6d155df..f12c0f8700d8 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/AutofillUpdatingExistingCredentialsDialogFragment.kt @@ -22,11 +22,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import androidx.lifecycle.ViewModelProvider import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog +import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_CREDENTIALS +import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_CREDENTIAL_UPDATE_TYPE +import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_TAB_ID +import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor @@ -104,14 +110,14 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm private fun configureViews(binding: ContentAutofillUpdateExistingCredentialsBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED val credentials = getCredentialsToSave() - val originalUrl = getOriginalUrl() + val webMessageRequest = getWebMessageRequest() val updateType = getUpdateType() Timber.v("Update type is $updateType") configureDialogTitle(binding, updateType) configureCloseButtons(binding) configureUpdatedFieldPreview(binding, credentials, updateType) - configureUpdateButton(binding, originalUrl, credentials, updateType) + configureUpdateButton(binding, webMessageRequest, credentials, updateType) } private fun configureDialogTitle( @@ -130,7 +136,7 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm private fun configureUpdateButton( binding: ContentAutofillUpdateExistingCredentialsBinding, - originalUrl: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, updateType: CredentialUpdateType, ) { @@ -143,9 +149,9 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm pixelNameDialogEvent(Updated)?.let { pixel.fire(it) } val result = Bundle().also { - it.putString(CredentialUpdateExistingCredentialsDialog.KEY_URL, originalUrl) - it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIALS, credentials) - it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIAL_UPDATE_TYPE, getUpdateType()) + it.putParcelable(KEY_URL, autofillWebMessageRequest) + it.putParcelable(KEY_CREDENTIALS, credentials) + it.putParcelable(KEY_CREDENTIAL_UPDATE_TYPE, getUpdateType()) } parentFragment?.setFragmentResult(CredentialUpdateExistingCredentialsDialog.resultKeyCredentialUpdated(getTabId()), result) @@ -200,16 +206,15 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm object Updated : DialogEvent } - private fun getCredentialsToSave() = arguments?.getParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIALS)!! - private fun getTabId() = arguments?.getString(CredentialUpdateExistingCredentialsDialog.KEY_TAB_ID)!! - private fun getOriginalUrl() = arguments?.getString(CredentialUpdateExistingCredentialsDialog.KEY_URL)!! - private fun getUpdateType() = - arguments?.getParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIAL_UPDATE_TYPE)!! + private fun getCredentialsToSave() = BundleCompat.getParcelable(requireArguments(), KEY_CREDENTIALS, LoginCredentials::class.java)!! + private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! + private fun getWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! + private fun getUpdateType() = BundleCompat.getParcelable(requireArguments(), KEY_CREDENTIAL_UPDATE_TYPE, CredentialUpdateType::class.java)!! companion object { fun instance( - url: String, + autofillWebMessageRequest: AutofillWebMessageRequest, credentials: LoginCredentials, tabId: String, credentialUpdateType: CredentialUpdateType, @@ -217,10 +222,10 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm val fragment = AutofillUpdatingExistingCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putString(CredentialUpdateExistingCredentialsDialog.KEY_URL, url) - it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIALS, credentials) - it.putString(CredentialUpdateExistingCredentialsDialog.KEY_TAB_ID, tabId) - it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIAL_UPDATE_TYPE, credentialUpdateType) + it.putParcelable(KEY_URL, autofillWebMessageRequest) + it.putParcelable(KEY_CREDENTIALS, credentials) + it.putString(KEY_TAB_ID, tabId) + it.putParcelable(KEY_CREDENTIAL_UPDATE_TYPE, credentialUpdateType) } return fragment } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt index a1b7019fb820..5bf017bf188c 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt @@ -16,19 +16,18 @@ package com.duckduckgo.autofill.impl.ui.credential.updating -import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle -import android.os.Parcelable +import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_CREDENTIALS import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_CREDENTIAL_UPDATE_TYPE +import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor @@ -47,7 +46,6 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( private val autofillFireproofDialogSuppressor: AutofillFireproofDialogSuppressor, private val dispatchers: DispatcherProvider, private val autofillStore: InternalAutofillStore, - private val appBuildConfig: AppBuildConfig, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : AutofillFragmentResultsPlugin { @@ -62,12 +60,12 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( autofillFireproofDialogSuppressor.autofillSaveOrUpdateDialogVisibilityChanged(visible = false) - val selectedCredentials = result.safeGetParcelable(KEY_CREDENTIALS) ?: return - val originalUrl = result.getString(CredentialUpdateExistingCredentialsDialog.KEY_URL) ?: return - val updateType = result.safeGetParcelable(KEY_CREDENTIAL_UPDATE_TYPE) ?: return + val selectedCredentials = BundleCompat.getParcelable(result, KEY_CREDENTIALS, LoginCredentials::class.java) ?: return + val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return + val updateType = BundleCompat.getParcelable(result, KEY_CREDENTIAL_UPDATE_TYPE, CredentialUpdateType::class.java) ?: return appCoroutineScope.launch(dispatchers.io()) { - autofillStore.updateCredentials(originalUrl, selectedCredentials, updateType)?.let { + autofillStore.updateCredentials(autofillWebMessageRequest.requestOrigin, selectedCredentials, updateType)?.let { withContext(dispatchers.main()) { autofillCallback.onUpdatedCredentials(it) } @@ -75,15 +73,6 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( } } - @Suppress("DEPRECATION") - @SuppressLint("NewApi") - private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) - } - override fun resultKey(tabId: String): String { return CredentialUpdateExistingCredentialsDialog.resultKeyCredentialUpdated(tabId) } diff --git a/autofill/autofill-impl/src/main/res/layout/activity_email_protection_in_context_signup.xml b/autofill/autofill-impl/src/main/res/layout/activity_email_protection_in_context_signup.xml index 0494b569d3db..7fe7ceb413b9 100644 --- a/autofill/autofill-impl/src/main/res/layout/activity_email_protection_in_context_signup.xml +++ b/autofill/autofill-impl/src/main/res/layout/activity_email_protection_in_context_signup.xml @@ -28,13 +28,13 @@ app:layout_constraintEnd_toEndOf="parent" layout="@layout/include_default_toolbar" /> - \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/fragment_email_protection_in_context_signup.xml b/autofill/autofill-impl/src/main/res/layout/fragment_email_protection_in_context_signup.xml new file mode 100644 index 000000000000..59bf313e7578 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/fragment_email_protection_in_context_signup.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml b/autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml new file mode 100644 index 000000000000..fffc2217ec91 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml b/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml new file mode 100644 index 000000000000..b0822b6372b3 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index 633f147275b8..97df55f79047 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -17,4 +17,6 @@ Passwords + + Autofill for passwords is currently unavailable. We’re working to restore this in a future app update. \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml index b5ed7705518c..703928b6e4ab 100644 --- a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml @@ -56,7 +56,7 @@ Suggested - Autofill for passwords is unavailable because your version of Android WebView is too old. + Autofill for passwords is unavailable because your version of Android WebView is outdated or incompatible. Clear search input No results for \'%1$s\' diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt deleted file mode 100644 index 172112766201..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.impl - -import android.webkit.WebView -import androidx.test.core.app.ApplicationProvider.getApplicationContext -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.autofill.api.AutofillCapabilityChecker -import com.duckduckgo.autofill.api.Callback -import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.api.domain.app.LoginTriggerType -import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor -import com.duckduckgo.autofill.impl.AutofillStoredBackJavascriptInterface.UrlProvider -import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator -import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextRecentInstallChecker -import com.duckduckgo.autofill.impl.email.incontext.store.EmailProtectionInContextDataStore -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest -import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME -import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter -import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials -import com.duckduckgo.autofill.impl.store.InternalAutofillStore -import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository -import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions -import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver -import com.duckduckgo.common.test.CoroutineTestRule -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Assert.* -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.* - -@RunWith(AndroidJUnit4::class) -class AutofillStoredBackJavascriptInterfaceTest { - - @get:Rule - var coroutineRule = CoroutineTestRule() - - private val requestParser: AutofillRequestParser = mock() - private val autofillStore: InternalAutofillStore = mock() - private val autofillMessagePoster: AutofillMessagePoster = mock() - private val autofillResponseWriter: AutofillResponseWriter = mock() - private val currentUrlProvider: UrlProvider = mock() - private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() - private val passwordEventResolver: AutogeneratedPasswordEventResolver = mock() - private val testSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() - private val coroutineScope: CoroutineScope = TestScope() - private val shareableCredentials: ShareableCredentials = mock() - private val emailManager: EmailManager = mock() - private val inContextDataStore: EmailProtectionInContextDataStore = mock() - private val recentInstallChecker: EmailProtectionInContextRecentInstallChecker = mock() - private val testWebView = WebView(getApplicationContext()) - private val loginDeduplicator: AutofillLoginDeduplicator = NoopDeduplicator() - private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor = mock() - private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() - private lateinit var testee: AutofillStoredBackJavascriptInterface - - private val testCallback = TestCallback() - - @Before - fun setUp() = runTest { - whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) - whenever(autofillCapabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(true) - whenever(autofillCapabilityChecker.canSaveCredentialsFromWebView(any())).thenReturn(true) - whenever(shareableCredentials.shareableCredentials(any())).thenReturn(emptyList()) - whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(false) - testee = AutofillStoredBackJavascriptInterface( - requestParser = requestParser, - autofillStore = autofillStore, - autofillMessagePoster = autofillMessagePoster, - autofillResponseWriter = autofillResponseWriter, - coroutineScope = coroutineScope, - currentUrlProvider = currentUrlProvider, - dispatcherProvider = coroutineRule.testDispatcherProvider, - autofillCapabilityChecker = autofillCapabilityChecker, - passwordEventResolver = passwordEventResolver, - shareableCredentials = shareableCredentials, - emailManager = emailManager, - inContextDataStore = inContextDataStore, - recentInstallChecker = recentInstallChecker, - loginDeduplicator = loginDeduplicator, - systemAutofillServiceSuppressor = systemAutofillServiceSuppressor, - neverSavedSiteRepository = neverSavedSiteRepository, - ) - testee.callback = testCallback - testee.webView = testWebView - testee.autoSavedLoginsMonitor = testSavedLoginsMonitor - - whenever(currentUrlProvider.currentUrl(testWebView)).thenReturn("https://example.com") - whenever(requestParser.parseAutofillDataRequest(any())).thenReturn( - Result.success(AutofillDataRequest(CREDENTIALS, USERNAME, USER_INITIATED, null)), - ) - whenever(autofillResponseWriter.generateEmptyResponseGetAutofillData()).thenReturn("") - whenever(autofillResponseWriter.generateResponseGetAutofillData(any())).thenReturn("") - } - - @Test - fun whenInjectingNoCredentialResponseThenCorrectJsonWriterInvoked() = runTest { - testee.injectNoCredentials() - verify(autofillResponseWriter).generateEmptyResponseGetAutofillData() - verifyMessageSent() - } - - @Test - fun whenInjectingCredentialResponseThenCorrectJsonWriterInvoked() = runTest { - val loginCredentials = LoginCredentials(0, "example.com", "username", "password") - testee.injectCredentials(loginCredentials) - verify(autofillResponseWriter).generateResponseGetAutofillData(any()) - verifyMessageSent() - } - - @Test - fun whenGetAutofillDataCalledNoCredentialsAvailableThenNoCredentialsCallbackInvoked() = runTest { - setupRequestForSubTypeUsername() - whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) - initiateGetAutofillDataRequest() - assertCredentialsUnavailable() - } - - @Test - fun whenGetAutofillDataCalledWithCredentialsAvailableThenCredentialsAvailableCallbackInvoked() = runTest { - whenever(autofillStore.getCredentials(any())).thenReturn(listOf(LoginCredentials(0, "example.com", "username", "password"))) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - } - - @Test - fun whenGetAutofillDataCalledWithCredentialsAvailableWithNullUsernameUsernameConvertedToEmptyString() = runTest { - setupRequestForSubTypePassword() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = "foo"), - loginCredential(username = null, password = "bar"), - loginCredential(username = "foo", password = "bar"), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - - // ensure the list of credentials now has two entries with empty string username (one for each null username) - assertCredentialsContains({ it.username }, "", "") - } - - @Test - fun whenRequestSpecifiesSubtypeUsernameAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest { - setupRequestForSubTypeUsername() - whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) - initiateGetAutofillDataRequest() - assertCredentialsUnavailable() - } - - @Test - fun whenRequestSpecifiesSubtypeUsernameAndNoEntriesWithAUsernameThenNoCredentialsCallbackInvoked() = runTest { - setupRequestForSubTypeUsername() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = "foo"), - loginCredential(username = null, password = "bar"), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsUnavailable() - } - - @Test - fun whenRequestSpecifiesSubtypeUsernameAndSingleEntryWithAUsernameThenCredentialsAvailableCallbackInvoked() = runTest { - setupRequestForSubTypeUsername() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = "foo"), - loginCredential(username = null, password = "bar"), - loginCredential(username = "foo", password = "bar"), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - assertCredentialsContains({ it.username }, "foo") - } - - @Test - fun whenRequestSpecifiesSubtypeUsernameAndMultipleEntriesWithAUsernameThenCredentialsAvailableCallbackInvoked() = runTest { - setupRequestForSubTypeUsername() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = "foo"), - loginCredential(username = "username1", password = "bar"), - loginCredential(username = null, password = "bar"), - loginCredential(username = null, password = "bar"), - loginCredential(username = "username2", password = null), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - assertCredentialsContains({ it.username }, "username1", "username2") - } - - @Test - fun whenRequestSpecifiesSubtypePasswordAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest { - setupRequestForSubTypePassword() - whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) - initiateGetAutofillDataRequest() - assertCredentialsUnavailable() - } - - @Test - fun whenRequestSpecifiesSubtypePasswordAndNoEntriesWithAPasswordThenNoCredentialsCallbackInvoked() = runTest { - setupRequestForSubTypePassword() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = "foo", password = null), - loginCredential(username = "bar", password = null), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsUnavailable() - } - - @Test - fun whenRequestSpecifiesSubtypePasswordAndSingleEntryWithAPasswordThenCredentialsAvailableCallbackInvoked() = runTest { - setupRequestForSubTypePassword() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = null), - loginCredential(username = "foobar", password = null), - loginCredential(username = "foo", password = "bar"), - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - assertCredentialsContains({ it.password }, "bar") - } - - @Test - fun whenRequestSpecifiesSubtypePasswordAndMultipleEntriesWithAPasswordThenCredentialsAvailableCallbackInvoked() = runTest { - setupRequestForSubTypePassword() - whenever(autofillStore.getCredentials(any())).thenReturn( - listOf( - loginCredential(username = null, password = null), - loginCredential(username = "username2", password = null), - loginCredential(username = "username1", password = "password1"), - loginCredential(username = null, password = "password2"), - loginCredential(username = null, password = "password3"), - - ), - ) - initiateGetAutofillDataRequest() - assertCredentialsAvailable() - assertCredentialsContains({ it.password }, "password1", "password2", "password3") - } - - @Test - fun whenStoreFormDataCalledWithNoUsernameThenCallbackInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = null, password = "password") - testee.storeFormData("") - assertNotNull(testCallback.credentialsToSave) - } - - @Test - fun whenStoreFormDataCalledWithNoPasswordThenCallbackInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = "dax@duck.com", password = null) - testee.storeFormData("") - assertNotNull(testCallback.credentialsToSave) - assertEquals("dax@duck.com", testCallback.credentialsToSave!!.username) - } - - @Test - fun whenStoreFormDataCalledWithNullUsernameAndPasswordThenCallbackNotInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = null, password = null) - testee.storeFormData("") - assertNull(testCallback.credentialsToSave) - } - - @Test - fun whenStoreFormDataCalledWithBlankUsernameThenCallbackInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = "password") - testee.storeFormData("") - assertEquals(" ", testCallback.credentialsToSave!!.username) - assertEquals("password", testCallback.credentialsToSave!!.password) - } - - @Test - fun whenStoreFormDataCalledWithBlankPasswordThenCallbackInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = " ") - testee.storeFormData("") - assertEquals("username", testCallback.credentialsToSave!!.username) - assertEquals(" ", testCallback.credentialsToSave!!.password) - } - - @Test - fun whenStoreFormDataCalledButSiteInNeverSaveListThenCallbackNotInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password") - whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(true) - testee.storeFormData("") - assertNull(testCallback.credentialsToSave) - } - - @Test - fun whenStoreFormDataCalledWithBlankUsernameAndBlankPasswordThenCallbackNotInvoked() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = " ") - testee.storeFormData("") - assertNull(testCallback.credentialsToSave) - } - - @Test - fun whenStoreFormDataCalledAndParsingErrorThenExceptionIsContained() = runTest { - configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password") - whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.failure(RuntimeException("Parsing error"))) - testee.storeFormData("") - assertNull(testCallback.credentialsToSave) - } - - private suspend fun configureRequestParserToReturnSaveCredentialRequestType( - username: String?, - password: String?, - ) { - val credentials = AutofillStoreFormDataCredentialsRequest(username = username, password = password) - val topLevelRequest = AutofillStoreFormDataRequest(credentials) - whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.success(topLevelRequest)) - whenever(passwordEventResolver.decideActions(anyOrNull(), any())).thenReturn(listOf(Actions.PromptToSave)) - } - - private fun assertCredentialsContains( - property: (LoginCredentials) -> String?, - vararg expected: String?, - ) { - val numberExpected = expected.size - val numberMatched = testCallback.credentialsToInject?.filter { expected.contains(property(it)) }?.count() - assertEquals("Wrong number of matched properties. Expected $numberExpected but found $numberMatched", numberExpected, numberMatched) - } - - private fun loginCredential( - username: String?, - password: String?, - ) = LoginCredentials(0, "example.com", username, password) - - private suspend fun setupRequestForSubTypeUsername() { - whenever(requestParser.parseAutofillDataRequest(any())).thenReturn( - Result.success(AutofillDataRequest(CREDENTIALS, USERNAME, USER_INITIATED, null)), - ) - } - - private suspend fun setupRequestForSubTypePassword() { - whenever(requestParser.parseAutofillDataRequest(any())).thenReturn( - Result.success(AutofillDataRequest(CREDENTIALS, PASSWORD, USER_INITIATED, null)), - ) - } - - private fun assertCredentialsUnavailable() { - assertNotNull("Callback has not been called", testCallback.credentialsAvailableToInject) - assertFalse(testCallback.credentialsAvailableToInject!!) - } - - private fun assertCredentialsAvailable() { - assertNotNull("Callback has not been called", testCallback.credentialsAvailableToInject) - assertTrue(testCallback.credentialsAvailableToInject!!) - } - - private fun initiateGetAutofillDataRequest() { - testee.getAutofillData("") - } - - private suspend fun verifyMessageSent() { - verify(autofillMessagePoster).postMessage(any(), anyOrNull()) - } - - class TestCallback : Callback { - - // for injection - var credentialsToInject: List? = null - var credentialsAvailableToInject: Boolean? = null - - // for saving - var credentialsToSave: LoginCredentials? = null - - // for password generation - var offeredToGeneratePassword: Boolean = false - - override suspend fun onCredentialsAvailableToInject( - originalUrl: String, - credentials: List, - triggerType: LoginTriggerType, - ) { - credentialsAvailableToInject = true - this.credentialsToInject = credentials - } - - override suspend fun onCredentialsAvailableToSave( - currentUrl: String, - credentials: LoginCredentials, - ) { - credentialsToSave = credentials - } - - override suspend fun onGeneratedPasswordAvailableToUse( - originalUrl: String, - username: String?, - generatedPassword: String, - ) { - offeredToGeneratePassword = true - } - - override fun noCredentialsAvailable(originalUrl: String) { - credentialsAvailableToInject = false - } - - override fun onCredentialsSaved(savedCredentials: LoginCredentials) { - // no-op - } - } - - private class NoopDeduplicator : AutofillLoginDeduplicator { - override fun deduplicate(originalUrl: String, logins: List): List = logins - } -} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt index 0f28994e95a1..9233d7e338ca 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt @@ -1,161 +1,121 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.duckduckgo.autofill.impl +import android.annotation.SuppressLint import android.webkit.WebView -import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.Callback -import com.duckduckgo.autofill.api.EmailProtectionInContextSignupFlowListener -import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener -import com.duckduckgo.autofill.api.domain.app.LoginCredentials -import com.duckduckgo.autofill.api.domain.app.LoginTriggerType -import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor -import com.duckduckgo.autofill.impl.InlineBrowserAutofillTest.FakeAutofillJavascriptInterface.Actions.* -import org.junit.Assert.* -import org.junit.Before +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle.State +import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class InlineBrowserAutofillTest { - private lateinit var testee: InlineBrowserAutofill - private val automaticSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() - private lateinit var autofillJavascriptInterface: FakeAutofillJavascriptInterface - - private lateinit var testWebView: WebView - - private val emailProtectionInContextCallback: EmailProtectionUserPromptListener = mock() - private val emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener = mock() - - private val testCallback = object : Callback { - override suspend fun onCredentialsAvailableToInject( - originalUrl: String, - credentials: List, - triggerType: LoginTriggerType, - ) { - } - - override suspend fun onCredentialsAvailableToSave( - currentUrl: String, - credentials: LoginCredentials, - ) { - } - - override suspend fun onGeneratedPasswordAvailableToUse( - originalUrl: String, - username: String?, - generatedPassword: String, - ) { - } - - override fun noCredentialsAvailable(originalUrl: String) { - } - - override fun onCredentialsSaved(savedCredentials: LoginCredentials) { - } + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val mockWebView: WebView = mock() + private val autofillCallback: Callback = mock() + private val capabilityChecker: InternalAutofillCapabilityChecker = mock() + private val autofillJavascriptInjector: AutofillJavascriptInjector = mock() + private val webMessageAttacher: AutofillWebMessageAttacher = mock() + private val webMessageListeners = mutableListOf() + private val webMessageListenersPlugin: PluginPoint = object : PluginPoint { + override fun getPlugins(): Collection = webMessageListeners } - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - autofillJavascriptInterface = FakeAutofillJavascriptInterface() - testWebView = WebView(getApplicationContext()) - testee = InlineBrowserAutofill(autofillInterface = autofillJavascriptInterface, autoSavedLoginsMonitor = automaticSavedLoginsMonitor) + @Test + fun whenAutofillFeatureFlagDisabledThenDoNotAddJsInterface() = runTest { + val testee = setupConfig(topLevelFeatureEnabled = false) + testee.addJsInterface() + verifyJavascriptNotAdded() } @Test - fun whenRemoveJsInterfaceThenRemoveReferenceToWebview() { - testee.addJsInterface(testWebView, testCallback, emailProtectionInContextCallback, emailProtectionInContextSignupFlowCallback, "tabId") - - assertNotNull(autofillJavascriptInterface.webView) - - testee.removeJsInterface() - - assertNull(autofillJavascriptInterface.webView) + fun whenWebViewDoesNotSupportIntegrationThenDoNotAddJsInterface() = runTest { + val testee = setupConfig(deviceWebViewSupportsAutofill = false) + testee.addJsInterface() + verifyJavascriptNotAdded() } @Test - fun whenInjectCredentialsNullThenInterfaceInjectNoCredentials() { - testee.injectCredentials(null) - - assertEquals(NoCredentialsInjected, autofillJavascriptInterface.lastAction) + fun whenWebViewSupportsIntegrationAndFeatureEnabledThenJsInterfaceIsAdded() = runTest { + val testee = setupConfig() + testee.addJsInterface() + verifyJavascriptIsAdded() } @Test - fun whenInjectCredentialsThenInterfaceCredentialsInjected() { - val toInject = LoginCredentials( - id = 1, - domain = "hello.com", - username = "test", - password = "test123", - ) - testee.injectCredentials(toInject) - - assertEquals(CredentialsInjected(toInject), autofillJavascriptInterface.lastAction) + fun whenPluginsIsEmptyThenJsInterfaceIsAdded() = runTest { + val testee = setupConfig() + webMessageListeners.clear() + testee.addJsInterface() + verifyJavascriptIsAdded() } - class FakeAutofillJavascriptInterface : AutofillJavascriptInterface { - sealed class Actions { - data class GetAutoFillData(val requestString: String) : Actions() - data class CredentialsInjected(val credentials: LoginCredentials) : Actions() - object NoCredentialsInjected : Actions() - } - - var lastAction: Actions? = null - - override fun getAutofillData(requestString: String) { - lastAction = GetAutoFillData(requestString) - } - - override fun injectCredentials(credentials: LoginCredentials) { - lastAction = CredentialsInjected(credentials) - } - - override fun injectNoCredentials() { - lastAction = NoCredentialsInjected - } - - override fun closeEmailProtectionTab(data: String) { - } - - override fun getIncontextSignupDismissedAt(data: String) { - } - - override fun cancelRetrievingStoredLogins() { - } + @Test + fun whenPluginsIsNotEmptyThenIsRegisteredWithWebView() = runTest { + val testee = setupConfig() + val mockMessageListener: AutofillWebMessageListener = mock() + webMessageListeners.add(mockMessageListener) + testee.addJsInterface() + verify(webMessageAttacher).addListener(any(), eq(mockMessageListener)) + } - override fun acceptGeneratedPassword() { - } + private suspend fun verifyJavascriptNotAdded() { + verify(autofillJavascriptInjector, never()).addDocumentStartJavascript(any()) + } - override fun rejectGeneratedPassword() { - } + private suspend fun verifyJavascriptIsAdded() { + verify(autofillJavascriptInjector).addDocumentStartJavascript(any()) + } - override fun inContextEmailProtectionFlowFinished() { - } + private suspend fun InlineBrowserAutofill.addJsInterface() { + addJsInterface(mockWebView, autofillCallback, "tab-id-123") + } - override var callback: Callback? = null - override var emailProtectionInContextCallback: EmailProtectionUserPromptListener? = null - override var emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? = null - override var webView: WebView? = null - override var autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor? = null - override var tabId: String? = null + @SuppressLint("DenyListedApi") + private suspend fun setupConfig( + topLevelFeatureEnabled: Boolean = true, + autofillEnabledByUser: Boolean = true, + canInjectCredentials: Boolean = true, + canSaveCredentials: Boolean = true, + canGeneratePassword: Boolean = true, + canAccessCredentialManagement: Boolean = true, + canIntegrateAutofillInWebView: Boolean = true, + deviceWebViewSupportsAutofill: Boolean = true, + ): InlineBrowserAutofill { + val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) + autofillFeature.self().setEnabled(State(enable = topLevelFeatureEnabled)) + autofillFeature.canInjectCredentials().setEnabled(State(enable = canInjectCredentials)) + autofillFeature.canSaveCredentials().setEnabled(State(enable = canSaveCredentials)) + autofillFeature.canGeneratePasswords().setEnabled(State(enable = canGeneratePassword)) + autofillFeature.canAccessCredentialManagement().setEnabled(State(enable = canAccessCredentialManagement)) + autofillFeature.canIntegrateAutofillInWebView().setEnabled(State(enable = canIntegrateAutofillInWebView)) + + whenever(capabilityChecker.webViewSupportsAutofill()).thenReturn(deviceWebViewSupportsAutofill) + whenever(capabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(canIntegrateAutofillInWebView) + + return InlineBrowserAutofill( + autofillCapabilityChecker = capabilityChecker, + dispatchers = coroutineTestRule.testDispatcherProvider, + autofillJavascriptInjector = autofillJavascriptInjector, + webMessageListeners = webMessageListenersPlugin, + autofillFeature = autofillFeature, + webMessageAttacher = webMessageAttacher, + ) } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt similarity index 87% rename from autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt rename to autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt index ae275a45ddbc..3717452c256a 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt @@ -16,9 +16,11 @@ package com.duckduckgo.autofill.impl +import android.annotation.SuppressLint import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.InternalTestUserChecker +import com.duckduckgo.autofill.impl.configuration.integration.JavascriptCommunicationSupport import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State @@ -40,6 +42,7 @@ class AutofillCapabilityCheckerImplTest { private val internalTestUserChecker: InternalTestUserChecker = mock() private val autofillGlobalCapabilityChecker: AutofillGlobalCapabilityChecker = mock() + private val javascriptCommunicationSupport: JavascriptCommunicationSupport = mock() private lateinit var testee: AutofillCapabilityCheckerImpl @@ -123,6 +126,20 @@ class AutofillCapabilityCheckerImplTest { assertFalse(testee.canGeneratePasswordFromWebView(URL)) } + @Test + fun whenModernJavascriptIntegrationIsSupportedThenSupportsAutofillIsTrue() = runTest { + setupConfig(topLevelFeatureEnabled = true, autofillEnabledByUser = true) + whenever(javascriptCommunicationSupport.supportsModernIntegration()).thenReturn(true) + assertTrue(testee.webViewSupportsAutofill()) + } + + @Test + fun whenModernJavascriptIntegrationIsSupportedThenSupportsAutofillIsFalse() = runTest { + setupConfig(topLevelFeatureEnabled = true, autofillEnabledByUser = true) + whenever(javascriptCommunicationSupport.supportsModernIntegration()).thenReturn(false) + assertFalse(testee.webViewSupportsAutofill()) + } + private suspend fun assertAllSubFeaturesDisabled() { assertFalse(testee.canAccessCredentialManagementScreen()) assertFalse(testee.canGeneratePasswordFromWebView(URL)) @@ -130,6 +147,7 @@ class AutofillCapabilityCheckerImplTest { assertFalse(testee.canSaveCredentialsFromWebView(URL)) } + @SuppressLint("DenyListedApi") private suspend fun setupConfig( topLevelFeatureEnabled: Boolean = false, autofillEnabledByUser: Boolean = false, @@ -155,6 +173,7 @@ class AutofillCapabilityCheckerImplTest { internalTestUserChecker = internalTestUserChecker, autofillGlobalCapabilityChecker = autofillGlobalCapabilityChecker, dispatcherProvider = coroutineTestRule.testDispatcherProvider, + javascriptCommunicationSupport = javascriptCommunicationSupport, ) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt index f16fc89e9679..f384a2e4f3ab 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/RealDuckAddressLoginCreatorTest.kt @@ -16,7 +16,7 @@ package com.duckduckgo.autofill.impl -import com.duckduckgo.autofill.api.AutofillCapabilityChecker +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor import com.duckduckgo.autofill.impl.store.InternalAutofillStore @@ -34,7 +34,7 @@ class RealDuckAddressLoginCreatorTest { private val autofillStore: InternalAutofillStore = mock() private val automaticSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() - private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() private val testee = RealDuckAddressLoginCreator( @@ -126,6 +126,8 @@ class RealDuckAddressLoginCreatorTest { companion object { private const val TAB_ID = "tab-id-123" private const val URL = "example.com" + private const val REQUEST_ID = "request-id-123" + private val AUTOFILL_URL_REQUEST = AutofillWebMessageRequest(URL, URL, REQUEST_ID) private const val DUCK_ADDRESS = "foo@duck.com" } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfiguratorTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfiguratorTest.kt deleted file mode 100644 index 9ac08289d8e8..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfiguratorTest.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2023 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.impl.configuration - -import android.webkit.WebView -import com.duckduckgo.autofill.api.AutofillCapabilityChecker -import com.duckduckgo.common.test.CoroutineTestRule -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -class InlineBrowserAutofillConfiguratorTest { - - @get:Rule var coroutineRule = CoroutineTestRule() - - private lateinit var inlineBrowserAutofillConfigurator: InlineBrowserAutofillConfigurator - - private val autofillRuntimeConfigProvider: AutofillRuntimeConfigProvider = mock() - private val webView: WebView = mock() - private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() - private val autofillJavascriptLoader: AutofillJavascriptLoader = mock() - - @Before - fun before() = runTest { - whenever(autofillJavascriptLoader.getAutofillJavascript()).thenReturn("") - whenever(autofillRuntimeConfigProvider.getRuntimeConfiguration(any(), any())).thenReturn("") - - inlineBrowserAutofillConfigurator = InlineBrowserAutofillConfigurator( - autofillRuntimeConfigProvider, - TestScope(), - coroutineRule.testDispatcherProvider, - autofillCapabilityChecker, - autofillJavascriptLoader, - ) - } - - @Test - fun whenFeatureIsNotEnabledThenDoNotInject() = runTest { - givenFeatureIsDisabled() - inlineBrowserAutofillConfigurator.configureAutofillForCurrentPage(webView, "https://example.com") - - verify(webView, never()).evaluateJavascript("javascript:", null) - } - - @Test - fun whenFeatureIsEnabledThenInject() = runTest { - givenFeatureIsEnabled() - inlineBrowserAutofillConfigurator.configureAutofillForCurrentPage(webView, "https://example.com") - - verify(webView).evaluateJavascript("javascript:", null) - } - - private suspend fun givenFeatureIsEnabled() { - whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) - } - - private suspend fun givenFeatureIsDisabled() { - whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(false) - } -} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt index c6287c4fe137..9499c3f63b3a 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 DuckDuckGo + * Copyright (c) 2024 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,10 @@ package com.duckduckgo.autofill.impl.configuration import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials @@ -49,7 +49,7 @@ class RealAutofillRuntimeConfigProviderTest { private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) private val runtimeConfigurationWriter: RuntimeConfigurationWriter = mock() private val shareableCredentials: ShareableCredentials = mock() - private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() @@ -91,7 +91,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenAutofillNotEnabledThenConfigurationUserPrefsCredentialsIsFalse() = runTest { configureAutofillCapabilities(enabled = false) - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(false) } @@ -99,7 +99,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenAutofillEnabledThenConfigurationUserPrefsCredentialsIsTrue() = runTest { configureAutofillCapabilities(enabled = true) configureNoShareableLogins() - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(true) } @@ -108,14 +108,14 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillCapabilities(enabled = true) configureAutofillAvailableForSite(EXAMPLE_URL) configureNoShareableLogins() - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) verifyKeyIconRequestedToShow() } @Test fun whenNoCredentialsForUrlThenConfigurationInputTypeCredentialsIsFalse() = runTest { configureAutofillEnabledWithNoSavedCredentials(EXAMPLE_URL) - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -139,7 +139,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -163,7 +163,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration("", EXAMPLE_URL) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -188,7 +188,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -213,7 +213,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -238,7 +238,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -263,7 +263,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -287,7 +287,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -311,7 +311,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -326,7 +326,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(true) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -340,7 +340,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(false) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -352,7 +352,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { val url = "example.com" configureAutofillEnabledWithNoSavedCredentials(url) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) verifyCanSaveCredentialsReturnedAs(true) } @@ -362,7 +362,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(neverSavedSiteRepository.isInNeverSaveList(url)).thenReturn(true) - testee.getRuntimeConfiguration("", url) + testee.getRuntimeConfiguration(url) verifyCanSaveCredentialsReturnedAs(true) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt index 2f7b044dea5f..7b3560a0574c 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt @@ -48,7 +48,7 @@ class RealRuntimeConfigurationWriterTest { @Test fun whenGenerateContentScopeTheReturnContentScopeString() { val expectedJson = """ - contentScope = { + "contentScope" : { "features": { "autofill": { "state": "enabled", @@ -56,7 +56,7 @@ class RealRuntimeConfigurationWriterTest { } }, "unprotectedTemporary": [] - }; + } """.trimIndent() assertEquals( expectedJson, @@ -66,7 +66,9 @@ class RealRuntimeConfigurationWriterTest { @Test fun whenGenerateUserUnprotectedDomainsThenReturnUserUnprotectedDomainsString() { - val expectedJson = "userUnprotectedDomains = [];" + val expectedJson = """ + "userUnprotectedDomains" : [] + """.trimIndent() assertEquals( expectedJson, testee.generateUserUnprotectedDomains(), @@ -76,7 +78,7 @@ class RealRuntimeConfigurationWriterTest { @Test fun whenGenerateUserPreferencesThenReturnUserPreferencesString() { val expectedJson = """ - userPreferences = { + "userPreferences" : { "debug": false, "platform": { "name": "android" @@ -93,12 +95,12 @@ class RealRuntimeConfigurationWriterTest { "credentials_saving": true, "inlineIcon_credentials": true, "emailProtection_incontext_signup": true, - "unknown_username_categorization": false, + "unknown_username_categorization": false } } } } - }; + } """.trimIndent() assertEquals( expectedJson, diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt new file mode 100644 index 000000000000..4958df8013d3 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt @@ -0,0 +1,74 @@ +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import android.net.Uri +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import org.junit.Assert.* +import org.junit.Test +import org.mockito.kotlin.mock + +class AutofillWebMessageListenerTest { + + private val mockReply: JavaScriptReplyProxy = mock() + + private val testee = object : AutofillWebMessageListener() { + override val key: String + get() = "testkey" + + override fun onPostMessage( + p0: WebView, + p1: WebMessageCompat, + p2: Uri, + p3: Boolean, + p4: JavaScriptReplyProxy, + ) { + } + + fun testStoreReply(reply: JavaScriptReplyProxy): String { + return storeReply(reply) + } + } + + @Test + fun whenStoreReplyThenGetBackNonNullId() { + assertNotNull(testee.testStoreReply(mockReply)) + } + + @Test + fun whenAttemptResponseWithNoAssociatedReplyThenMessageNotHandled() { + assertFalse(testee.onResponse("message", "unknown-request-id")) + } + + @Test + fun whenAttemptResponseWithAnAssociatedReplyThenMessageIsHandled() { + val requestId = testee.testStoreReply(mockReply) + assertTrue(testee.onResponse("message", requestId)) + } + + @Test + fun whenReplyIsUsedThenItIsCleanedUp() { + val requestId = testee.testStoreReply(mockReply) + assertTrue(testee.onResponse("message", requestId)) + assertFalse(testee.onResponse("message", requestId)) + } + + @Test + fun whenMaxConcurrentRepliesInUseThenAllStillUsable() { + val requestIds = mutableListOf() + repeat(10) { requestIds.add(it, testee.testStoreReply(mockReply)) } + requestIds.forEach { + assertTrue(testee.onResponse("message", it)) + } + } + + @Test + fun whenMaxConcurrentRepliesPlusOneInUseThenAllButFirstIsStillUsable() { + val requestIds = mutableListOf() + repeat(11) { requestIds.add(it, testee.testStoreReply(mockReply)) } + assertFalse(testee.onResponse("message", requestIds.first())) + requestIds.drop(1).forEach { + assertTrue(testee.onResponse("message", it)) + } + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/TestWebMessageListenerCallback.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/TestWebMessageListenerCallback.kt new file mode 100644 index 000000000000..5b44cc1adb05 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/TestWebMessageListenerCallback.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import com.duckduckgo.autofill.api.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.Callback +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.domain.app.LoginTriggerType + +class TestWebMessageListenerCallback : Callback { + + // for injection + var credentialsToInject: List? = null + var credentialsAvailableToInject: Boolean? = null + + // for saving + var credentialsToSave: LoginCredentials? = null + + // for password generation + var offeredToGeneratePassword: Boolean = false + + // for email protection + var showNativeChooseEmailAddressPrompt: Boolean = false + var showNativeInContextEmailProtectionSignupPrompt: Boolean = false + + override suspend fun onCredentialsAvailableToInject( + autofillWebMessageRequest: AutofillWebMessageRequest, + credentials: List, + triggerType: LoginTriggerType, + ) { + credentialsAvailableToInject = true + this.credentialsToInject = credentials + } + + override suspend fun onCredentialsAvailableToSave( + autofillWebMessageRequest: AutofillWebMessageRequest, + credentials: LoginCredentials, + ) { + credentialsToSave = credentials + } + + override suspend fun onGeneratedPasswordAvailableToUse( + autofillWebMessageRequest: AutofillWebMessageRequest, + username: String?, + generatedPassword: String, + ) { + offeredToGeneratePassword = true + } + + override fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + showNativeChooseEmailAddressPrompt = true + } + + override fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + showNativeInContextEmailProtectionSignupPrompt = true + } + + override fun onCredentialsSaved(savedCredentials: LoginCredentials) { + // no-op + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillDataTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillDataTest.kt new file mode 100644 index 000000000000..af6e1ab17985 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillDataTest.kt @@ -0,0 +1,266 @@ +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener + +import android.webkit.WebView +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker +import com.duckduckgo.autofill.impl.deduper.AutofillLoginDeduplicator +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputMainType.CREDENTIALS +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.PASSWORD +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubType.USERNAME +import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class WebMessageListenerGetAutofillDataTest { + + private val shareableCredentials: ShareableCredentials = mock() + private val autofillStore: InternalAutofillStore = mock() + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() + private val requestParser: AutofillRequestParser = mock() + private val webMessageReply: JavaScriptReplyProxy = mock() + private val testCallback = TestWebMessageListenerCallback() + private val mockWebView: WebView = mock() + private val responseWriter: AutofillResponseWriter = mock() + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val testee = WebMessageListenerGetAutofillData( + appCoroutineScope = coroutineTestRule.testScope, + dispatchers = coroutineTestRule.testDispatcherProvider, + autofillCapabilityChecker = autofillCapabilityChecker, + requestParser = requestParser, + autofillStore = autofillStore, + shareableCredentials = shareableCredentials, + loginDeduplicator = NoopDeduplicator(), + responseWriter = responseWriter, + ) + + @Before + fun setup() = runTest { + testee.callback = testCallback + + whenever(mockWebView.url).thenReturn(REQUEST_URL) + whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) + whenever(autofillCapabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(true) + whenever(autofillCapabilityChecker.canSaveCredentialsFromWebView(any())).thenReturn(true) + whenever(shareableCredentials.shareableCredentials(any())).thenReturn(emptyList()) + whenever(responseWriter.generateEmptyResponseGetAutofillData()).thenReturn("") + } + + @Test + fun whenGettingSavedPasswordsNoCredentialsAvailableThenNoCredentialsCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) + initiateGetAutofillDataRequest() + assertCredentialsUnavailable() + } + + @Test + fun whenGettingSavedPasswordsWithCredentialsAvailableThenCredentialsAvailableCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn(listOf(LoginCredentials(0, "example.com", "username", "password"))) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + } + + @Test + fun whenGettingSavedPasswordsWithCredentialsAvailableWithNullUsernameUsernameConvertedToEmptyString() = runTest { + setupRequestForSubTypePassword() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = "foo"), + loginCredential(username = null, password = "bar"), + loginCredential(username = "foo", password = "bar"), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + + // ensure the list of credentials now has two entries with empty string username (one for each null username) + assertCredentialsContains({ it.username }, "", "") + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypeUsernameAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) + initiateGetAutofillDataRequest() + assertCredentialsUnavailable() + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypeUsernameAndNoEntriesWithAUsernameThenNoCredentialsCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = "foo"), + loginCredential(username = null, password = "bar"), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsUnavailable() + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypeUsernameAndSingleEntryWithAUsernameThenCredentialsAvailableCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = "foo"), + loginCredential(username = null, password = "bar"), + loginCredential(username = "foo", password = "bar"), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + assertCredentialsContains({ it.username }, "foo") + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypeUsernameAndMultipleEntriesWithAUsernameThenCorrectCallbackInvoked() = runTest { + setupRequestForSubTypeUsername() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = "foo"), + loginCredential(username = "username1", password = "bar"), + loginCredential(username = null, password = "bar"), + loginCredential(username = null, password = "bar"), + loginCredential(username = "username2", password = null), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + assertCredentialsContains({ it.username }, "username1", "username2") + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypePasswordAndNoEntriesThenNoCredentialsCallbackInvoked() = runTest { + setupRequestForSubTypePassword() + whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) + initiateGetAutofillDataRequest() + assertCredentialsUnavailable() + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypePasswordAndNoEntriesWithAPasswordThenNoCredentialsCallbackInvoked() = runTest { + setupRequestForSubTypePassword() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = "foo", password = null), + loginCredential(username = "bar", password = null), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsUnavailable() + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypePasswordAndSingleEntryWithAPasswordThenCredentialsAvailableCallbackInvoked() = runTest { + setupRequestForSubTypePassword() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = null), + loginCredential(username = "foobar", password = null), + loginCredential(username = "foo", password = "bar"), + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + assertCredentialsContains({ it.password }, "bar") + } + + @Test + fun whenGettingSavedPasswordsAndRequestSpecifiesSubtypePasswordAndMultipleEntriesWithAPasswordThenCorrectCallbackInvoked() = runTest { + setupRequestForSubTypePassword() + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + loginCredential(username = null, password = null), + loginCredential(username = "username2", password = null), + loginCredential(username = "username1", password = "password1"), + loginCredential(username = null, password = "password2"), + loginCredential(username = null, password = "password3"), + + ), + ) + initiateGetAutofillDataRequest() + assertCredentialsAvailable() + assertCredentialsContains({ it.password }, "password1", "password2", "password3") + } + + private fun assertCredentialsUnavailable() { + verify(responseWriter).generateEmptyResponseGetAutofillData() + verify(webMessageReply).postMessage(any()) + } + + private fun assertCredentialsAvailable() { + assertNotNull("Callback has not been called", testCallback.credentialsAvailableToInject) + assertTrue(testCallback.credentialsAvailableToInject!!) + } + + private fun assertCredentialsContains( + property: (LoginCredentials) -> String?, + vararg expected: String?, + ) { + val numberExpected = expected.size + val numberMatched = testCallback.credentialsToInject?.count { expected.contains(property(it)) } + Assert.assertEquals("Wrong number of matched properties. Expected $numberExpected but found $numberMatched", numberExpected, numberMatched) + } + + private fun initiateGetAutofillDataRequest(isMainFrame: Boolean = true) { + testee.onPostMessage( + webView = mockWebView, + message = WebMessageCompat(""), + sourceOrigin = REQUEST_ORIGIN, + isMainFrame = isMainFrame, + reply = webMessageReply, + ) + } + + private fun loginCredential( + username: String?, + password: String?, + ) = LoginCredentials(0, "example.com", username, password) + + private suspend fun setupRequestForSubTypeUsername() { + whenever(requestParser.parseAutofillDataRequest(any())).thenReturn( + Result.success(AutofillDataRequest(CREDENTIALS, USERNAME, USER_INITIATED, null)), + ) + } + + private suspend fun setupRequestForSubTypePassword() { + whenever(requestParser.parseAutofillDataRequest(any())).thenReturn( + Result.success(AutofillDataRequest(CREDENTIALS, PASSWORD, USER_INITIATED, null)), + ) + } + + private class NoopDeduplicator : AutofillLoginDeduplicator { + override fun deduplicate(originalUrl: String, logins: List): List = logins + } + + companion object { + private const val REQUEST_URL = "https://example.com" + private val REQUEST_ORIGIN = REQUEST_URL.toUri() + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormDataTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormDataTest.kt new file mode 100644 index 000000000000..96d44afe8e0a --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormDataTest.kt @@ -0,0 +1,158 @@ +package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.password + +import android.webkit.WebView +import androidx.core.net.toUri +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker +import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.TestWebMessageListenerCallback +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataCredentialsRequest +import com.duckduckgo.autofill.impl.jsbridge.request.AutofillStoreFormDataRequest +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository +import com.duckduckgo.autofill.impl.systemautofill.SystemAutofillServiceSuppressor +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.Actions +import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutogeneratedPasswordEventResolver +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class WebMessageListenerStoreFormDataTest { + + private val testCallback = TestWebMessageListenerCallback() + private val mockWebView: WebView = mock() + private val webMessageReply: JavaScriptReplyProxy = mock() + private val systemAutofillServiceSuppressor: SystemAutofillServiceSuppressor = mock() + private val passwordEventResolver: AutogeneratedPasswordEventResolver = mock() + private val autofillStore: InternalAutofillStore = mock() + private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() + private val requestParser: AutofillRequestParser = mock() + private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val testee = WebMessageListenerStoreFormData( + appCoroutineScope = coroutineTestRule.testScope, + dispatchers = coroutineTestRule.testDispatcherProvider, + autofillCapabilityChecker = autofillCapabilityChecker, + neverSavedSiteRepository = neverSavedSiteRepository, + requestParser = requestParser, + autoSavedLoginsMonitor = autoSavedLoginsMonitor, + autofillStore = autofillStore, + passwordEventResolver = passwordEventResolver, + systemAutofillServiceSuppressor = systemAutofillServiceSuppressor, + ) + + @Before + fun setup() = runTest { + testee.callback = testCallback + testee.tabId = "abc-123" + whenever(mockWebView.url).thenReturn(REQUEST_URL) + whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) + whenever(autofillCapabilityChecker.canSaveCredentialsFromWebView(any())).thenReturn(true) + whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(false) + whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) + } + + @Test + fun whenStoreFormDataCalledWithNoPasswordThenCallbackInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = "dax@duck.com", password = null) + simulateWebMessage() + assertNotNull(testCallback.credentialsToSave) + assertEquals("dax@duck.com", testCallback.credentialsToSave!!.username) + } + + @Test + fun whenStoreFormDataCalledWithNullUsernameAndPasswordThenCallbackNotInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = null, password = null) + simulateWebMessage() + assertNull(testCallback.credentialsToSave) + } + + @Test + fun whenStoreFormDataCalledWithBlankUsernameThenCallbackInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = "password") + simulateWebMessage() + assertEquals(" ", testCallback.credentialsToSave!!.username) + assertEquals("password", testCallback.credentialsToSave!!.password) + } + + @Test + fun whenStoreFormDataCalledWithBlankPasswordThenCallbackInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = " ") + simulateWebMessage() + assertEquals("username", testCallback.credentialsToSave!!.username) + assertEquals(" ", testCallback.credentialsToSave!!.password) + } + + @Test + fun whenStoreFormDataCalledButSiteInNeverSaveListThenCallbackNotInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password") + whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(true) + simulateWebMessage() + assertNull(testCallback.credentialsToSave) + } + + @Test + fun whenStoreFormDataCalledWithBlankUsernameAndBlankPasswordThenCallbackNotInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = " ", password = " ") + simulateWebMessage() + assertNull(testCallback.credentialsToSave) + } + + @Test + fun whenStoreFormDataCalledAndParsingErrorThenExceptionIsContained() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = "username", password = "password") + whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.failure(RuntimeException("Parsing error"))) + simulateWebMessage() + assertNull(testCallback.credentialsToSave) + } + + @Test + fun whenStoreFormDataCalledWithNoUsernameThenCallbackInvoked() = runTest { + configureRequestParserToReturnSaveCredentialRequestType(username = null, password = "password") + simulateWebMessage() + assertNotNull(testCallback.credentialsToSave) + } + + private suspend fun configureRequestParserToReturnSaveCredentialRequestType( + username: String?, + password: String?, + ) { + val credentials = AutofillStoreFormDataCredentialsRequest(username = username, password = password) + val topLevelRequest = AutofillStoreFormDataRequest(credentials) + whenever(requestParser.parseStoreFormDataRequest(any())).thenReturn(Result.success(topLevelRequest)) + whenever(passwordEventResolver.decideActions(anyOrNull(), any())).thenReturn(listOf(Actions.PromptToSave)) + } + + private fun simulateWebMessage(isMainFrame: Boolean = true) { + testee.onPostMessage( + webView = mockWebView, + message = WebMessageCompat(""), + sourceOrigin = REQUEST_ORIGIN, + isMainFrame = isMainFrame, + reply = webMessageReply, + ) + } + + companion object { + private const val REQUEST_URL = "https://example.com" + private val REQUEST_ORIGIN = REQUEST_URL.toUri() + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt index 0215dda1f3db..a5d1bce72fda 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt @@ -23,14 +23,16 @@ import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.COHORT import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LAST_USED_DAY -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.DoNotUseEmailProtection import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePersonalEmailAddress import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePrivateAliasAddress +import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS @@ -41,7 +43,12 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.* +import org.mockito.kotlin.any +import org.mockito.kotlin.argWhere +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class ResultHandlerEmailProtectionChooseEmailTest { @@ -51,16 +58,18 @@ class ResultHandlerEmailProtectionChooseEmailTest { private val context = InstrumentationRegistry.getInstrumentation().targetContext private val callback: AutofillEventListener = mock() - private val appBuildConfig: AppBuildConfig = mock() private val emailManager: EmailManager = mock() private val pixel: Pixel = mock() + private val messagePoster: AutofillMessagePoster = mock() + private val loginCreator: DuckAddressLoginCreator = mock() private val testee = ResultHandlerEmailProtectionChooseEmail( - appBuildConfig = appBuildConfig, emailManager = emailManager, dispatchers = coroutineTestRule.testDispatcherProvider, appCoroutineScope = coroutineTestRule.testScope, pixel = pixel, + messagePoster = messagePoster, + loginCreator = loginCreator, autofilledListeners = FakePluginPoint(), ) @@ -73,17 +82,17 @@ class ResultHandlerEmailProtectionChooseEmailTest { } @Test - fun whenUserSelectedToUsePersonalAddressThenCorrectCallbackInvoked() = runTest { + fun whenUserSelectedToUsePersonalAddressThenCorrectResponsePosted() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onUseEmailProtectionPersonalAddress(any(), any()) + verify(messagePoster).postMessage(argWhere { it.contains(""""alias": "personal-example""") }, any()) } @Test - fun whenUserSelectedToUsePrivateAliasAddressThenCorrectCallbackInvoked() = runTest { + fun whenUserSelectedToUsePrivateAliasAddressThenCorrectResponsePosted() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onUseEmailProtectionPrivateAlias(any(), any()) + verify(messagePoster).postMessage(argWhere { it.contains(""""alias": "private-example""") }, any()) } @Test @@ -146,7 +155,9 @@ class ResultHandlerEmailProtectionChooseEmailTest { result: EmailProtectionChooseEmailDialog.UseEmailResultType?, ): Bundle { return Bundle().also { - it.putString(EmailProtectionChooseEmailDialog.KEY_URL, url) + if (url != null) { + it.putParcelable(EmailProtectionChooseEmailDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) + } it.putParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT, result) } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt index 3c7b7accf969..80f3e91f8826 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt @@ -31,6 +31,7 @@ import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.SettingsActivity import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Sync import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizerImpl import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames @@ -121,6 +122,7 @@ class AutofillSettingsViewModelTest { private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules = mock() private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore = mock() private val urlMatcher = AutofillDomainNameUrlMatcher(UrlUnicodeNormalizerImpl()) + private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() private val testee = AutofillSettingsViewModel( autofillStore = mockStore, @@ -140,18 +142,18 @@ class AutofillSettingsViewModelTest { autofillBreakageReportSender = autofillBreakageReportSender, autofillBreakageReportDataStore = autofillBreakageReportDataStore, autofillBreakageReportCanShowRules = autofillBreakageReportCanShowRules, + autofillCapabilityChecker = autofillCapabilityChecker, ) @Before - fun setup() { + fun setup() = runTest { whenever(webUrlIdentifier.isLikelyAUrl(anyOrNull())).thenReturn(true) - - runTest { - whenever(mockStore.getAllCredentials()).thenReturn(emptyFlow()) - whenever(mockStore.getCredentialCount()).thenReturn(flowOf(0)) - whenever(neverSavedSiteRepository.neverSaveListCount()).thenReturn(emptyFlow()) - whenever(deviceAuthenticator.isAuthenticationRequiredForAutofill()).thenReturn(true) - } + whenever(autofillCapabilityChecker.webViewSupportsAutofill()).thenReturn(true) + whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) + whenever(mockStore.getAllCredentials()).thenReturn(emptyFlow()) + whenever(mockStore.getCredentialCount()).thenReturn(flowOf(0)) + whenever(neverSavedSiteRepository.neverSaveListCount()).thenReturn(emptyFlow()) + whenever(deviceAuthenticator.isAuthenticationRequiredForAutofill()).thenReturn(true) } @Test @@ -922,6 +924,26 @@ class AutofillSettingsViewModelTest { verify(pixel).fire(AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISMISSED) } + @Test + fun whenWebViewDoesNotSupportAutofillThenShowDisabledMode() = runTest { + whenever(autofillCapabilityChecker.webViewSupportsAutofill()).thenReturn(false) + testee.onViewCreated() + testee.viewState.test { + assertEquals(false, this.awaitItem().isAutofillSupported) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenAutofillConfigDisabledThenShowDisabledMode() = runTest { + whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(false) + testee.onViewCreated() + testee.viewState.test { + assertEquals(false, this.awaitItem().autofillEnabled) + cancelAndIgnoreRemainingEvents() + } + } + private fun List.verifyHasCommandToShowDeleteAllConfirmation(expectedNumberOfCredentialsToDelete: Int) { val confirmationCommand = this.firstOrNull { it is LaunchDeleteAllPasswordsConfirmation } assertNotNull(confirmationCommand) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt index 02cdbe160e3d..45ed9e3d3040 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt @@ -21,6 +21,7 @@ import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_ACCEPTED @@ -30,6 +31,8 @@ import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_USER import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.plugins.PluginPoint @@ -51,13 +54,17 @@ class ResultHandlerUseGeneratedPasswordTest { private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() private val existingCredentialMatchDetector: ExistingCredentialMatchDetector = mock() private val callback: AutofillEventListener = mock() + private val messagePoster: AutofillMessagePoster = mock() + private val responseWriter: AutofillResponseWriter = mock() private val testee = ResultHandlerUseGeneratedPassword( dispatchers = coroutineTestRule.testDispatcherProvider, autofillStore = autofillStore, - appCoroutineScope = coroutineTestRule.testScope, autoSavedLoginsMonitor = autoSavedLoginsMonitor, existingCredentialMatchDetector = existingCredentialMatchDetector, + messagePoster = messagePoster, + responseWriter = responseWriter, + appCoroutineScope = coroutineTestRule.testScope, autofilledListeners = FakePluginPoint(), ) @@ -73,18 +80,20 @@ class ResultHandlerUseGeneratedPasswordTest { } @Test - fun whenUserRejectedToUsePasswordThenCorrectCallbackInvoked() { + fun whenUserRejectedToUsePasswordThenCorrectResponsePosted() = runTest { val bundle = bundle("example.com", acceptedGeneratedPassword = false) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onRejectGeneratedPassword("example.com") + verify(responseWriter).generateResponseForRejectingGeneratedPassword() + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test - fun whenUserAcceptedToUsePasswordNoAutoLoginInThenCorrectCallbackInvoked() = runTest { + fun whenUserAcceptedToUsePasswordNoAutoLoginInThenCorrectResponsePosted() = runTest { whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = "pw") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onAcceptGeneratedPassword("example.com") + verify(responseWriter).generateResponseForAcceptingGeneratedPassword() + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test @@ -167,10 +176,12 @@ class ResultHandlerUseGeneratedPasswordTest { } @Test - fun whenUserAcceptedToUsePasswordButPasswordIsNullThenCorrectCallbackNotInvoked() = runTest { + fun whenUserAcceptedToUsePasswordButPasswordIsNullThen() = runTest { val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = null) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback, never()).onAcceptGeneratedPassword("example.com") + + verify(responseWriter, never()).generateResponseForAcceptingGeneratedPassword() + verify(messagePoster, never()).postMessage(any(), any()) } @Test @@ -187,7 +198,9 @@ class ResultHandlerUseGeneratedPasswordTest { password: String? = null, ): Bundle { return Bundle().also { - it.putString(KEY_URL, url) + if (url != null) { + it.putParcelable(KEY_URL, AutofillWebMessageRequest(url, url, "abc-123")) + } it.putBoolean(KEY_ACCEPTED, acceptedGeneratedPassword) it.putString(KEY_USERNAME, username) it.putString(KEY_PASSWORD, password) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt index ac353e319d1b..3bf61138190d 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt @@ -20,8 +20,8 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialSavePickerDialog import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor @@ -45,14 +45,12 @@ class ResultHandlerSaveLoginCredentialsTest { private val autofillFireproofDialogSuppressor: AutofillFireproofDialogSuppressor = mock() private val declineCounter: AutofillDeclineCounter = mock() private val autofillStore: InternalAutofillStore = mock() - private val appBuildConfig: AppBuildConfig = mock() private val testee = ResultHandlerSaveLoginCredentials( autofillFireproofDialogSuppressor = autofillFireproofDialogSuppressor, dispatchers = coroutineTestRule.testDispatcherProvider, declineCounter = declineCounter, autofillStore = autofillStore, - appBuildConfig = appBuildConfig, appCoroutineScope = coroutineTestRule.testScope, ) @@ -108,7 +106,9 @@ class ResultHandlerSaveLoginCredentialsTest { credentials: LoginCredentials?, ): Bundle { return Bundle().also { - it.putString(CredentialSavePickerDialog.KEY_URL, url) + if (url != null) { + it.putParcelable(CredentialSavePickerDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) + } it.putParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS, credentials) } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt index 18f29ff7b9f4..6cfc63c5f72a 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt @@ -21,14 +21,16 @@ import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.deviceauth.FakeAuthenticator import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.plugins.PluginPoint @@ -48,10 +50,11 @@ class ResultHandlerCredentialSelectionTest { private val pixel: Pixel = mock() private val existingCredentialMatchDetector: ExistingCredentialMatchDetector = mock() private val callback: AutofillEventListener = mock() - private val appBuildConfig: AppBuildConfig = mock() private lateinit var deviceAuthenticator: FakeAuthenticator private lateinit var testee: ResultHandlerCredentialSelection private val autofillStore: InternalAutofillStore = mock() + private val messagePoster: AutofillMessagePoster = mock() + private val responseWriter: AutofillResponseWriter = mock() @Before fun setup() = runTest { @@ -65,35 +68,43 @@ class ResultHandlerCredentialSelectionTest { } @Test - fun whenUserRejectedToUseCredentialThenCorrectCallbackInvoked() = runTest { + fun whenUserRejectedToUseCredentialThenCorrectResponsePosted() = runTest { configureSuccessfulAuth() val bundle = bundleForUserCancelling("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onNoCredentialsChosenForAutofill("example.com") + + verify(responseWriter).generateEmptyResponseGetAutofillData() + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test - fun whenUserAcceptedToUseCredentialsAndSuccessfullyAuthenticatedThenCorrectCallbackInvoked() = runTest { + fun whenUserAcceptedToUseCredentialsAndSuccessfullyAuthenticatedThenCorrectResponsePosted() = runTest { configureSuccessfulAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onShareCredentialsForAutofill("example.com", aLogin()) + + verify(responseWriter).generateResponseGetAutofillData(any()) + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test - fun whenUserAcceptedToUseCredentialsAndCancelsAuthenticationThenCorrectCallbackInvoked() = runTest { + fun whenUserAcceptedToUseCredentialsAndCancelsAuthenticationThenCorrectResponsePosted() = runTest { configureCancelledAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onNoCredentialsChosenForAutofill("example.com") + + verify(responseWriter).generateEmptyResponseGetAutofillData() + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test - fun whenUserAcceptedToUseCredentialsAndAuthenticationFailsThenCorrectCallbackInvoked() = runTest { + fun whenUserAcceptedToUseCredentialsAndAuthenticationFailsThenCorrectResponsePosted() = runTest { configureFailedAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(callback).onNoCredentialsChosenForAutofill("example.com") + + verify(responseWriter).generateEmptyResponseGetAutofillData() + verify(messagePoster).postMessage(anyOrNull(), any()) } @Test @@ -101,7 +112,7 @@ class ResultHandlerCredentialSelectionTest { configureSuccessfulAuth() val bundle = bundleMissingCredentials("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verifyNoInteractions(callback) + verifyNoInteractions(messagePoster) } @Test @@ -112,25 +123,25 @@ class ResultHandlerCredentialSelectionTest { verifyNoInteractions(callback) } - private fun bundleForUserCancelling(url: String?): Bundle { + private fun bundleForUserCancelling(url: String): Bundle { return Bundle().also { - it.putString(CredentialAutofillPickerDialog.KEY_URL, url) + it.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, true) } } - private fun bundleForUserAcceptingToAutofill(url: String?): Bundle { + private fun bundleForUserAcceptingToAutofill(url: String): Bundle { return Bundle().also { - it.putString(CredentialAutofillPickerDialog.KEY_URL, url) + it.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, false) it.putParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS, aLogin()) } } private fun bundleMissingUrl(): Bundle = Bundle() - private fun bundleMissingCredentials(url: String?): Bundle { + private fun bundleMissingCredentials(url: String): Bundle { return Bundle().also { - it.putString(CredentialAutofillPickerDialog.KEY_URL, url) + it.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) } } @@ -159,12 +170,17 @@ class ResultHandlerCredentialSelectionTest { appCoroutineScope = coroutineTestRule.testScope, pixel = pixel, deviceAuthenticator = deviceAuthenticator, - appBuildConfig = appBuildConfig, autofillStore = autofillStore, + messagePoster = messagePoster, + autofillResponseWriter = responseWriter, autofilledListeners = FakePluginPoint(), ) } + private fun String.asUrlRequest(): AutofillWebMessageRequest { + return AutofillWebMessageRequest(this, this, "request-id-123") + } + private class FakePluginPoint : PluginPoint { override fun getPlugins(): Collection { return emptyList() diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt index 8d9b3c36c206..46e7bb93b88b 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt @@ -20,8 +20,8 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType.Password @@ -45,13 +45,11 @@ class ResultHandlerUpdateLoginCredentialsTest { private val autofillStore: InternalAutofillStore = mock() private val autofillDialogSuppressor: AutofillFireproofDialogSuppressor = mock() private val callback: AutofillEventListener = mock() - private val appBuildConfig: AppBuildConfig = mock() private val testee = ResultHandlerUpdateLoginCredentials( autofillFireproofDialogSuppressor = autofillDialogSuppressor, dispatchers = coroutineTestRule.testDispatcherProvider, autofillStore = autofillStore, - appBuildConfig = appBuildConfig, appCoroutineScope = coroutineTestRule.testScope, ) @@ -103,7 +101,7 @@ class ResultHandlerUpdateLoginCredentialsTest { updateType: CredentialUpdateType, ): Bundle { return Bundle().also { - if (url != null) it.putString(CredentialUpdateExistingCredentialsDialog.KEY_URL, url) + if (url != null) it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) if (credentials != null) it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIALS, credentials) it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIAL_UPDATE_TYPE, updateType) } diff --git a/autofill/autofill-test/src/main/java/com/duckduckgo/autofill/api/FakeAutofillFeature.kt b/autofill/autofill-test/src/main/java/com/duckduckgo/autofill/api/FakeAutofillFeature.kt deleted file mode 100644 index 303db459c592..000000000000 --- a/autofill/autofill-test/src/main/java/com/duckduckgo/autofill/api/FakeAutofillFeature.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2023 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.api - -import com.duckduckgo.feature.toggles.api.FeatureToggles -import com.duckduckgo.feature.toggles.api.Toggle - -class FakeAutofillFeature private constructor() { - - companion object { - fun create(): AutofillFeature { - return FeatureToggles.Builder() - .store( - object : Toggle.Store { - private val map = mutableMapOf() - - override fun set(key: String, state: Toggle.State) { - map[key] = state - } - - override fun get(key: String): Toggle.State? { - return map[key] - } - }, - ) - .featureName("fakeAutofill") - .build() - .create(AutofillFeature::class.java) - } - } -} diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js index 4ea43dc6470e..0734be99a67b 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js @@ -4591,6 +4591,130 @@ exports.DeviceApi = DeviceApi; },{}],15:[function(require,module,exports){ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AndroidMessagingTransport = exports.AndroidMessagingConfig = void 0; +var _messaging = require("./messaging.js"); +/** + * @module Android Messaging + * + * @description A wrapper for messaging on Android. See example usage in android.transport.js + */ + +/** + * @typedef {import("./messaging").MessagingTransport} MessagingTransport + */ + +/** + * On Android, handlers are added to the window object and are prefixed with `ddg`. The object looks like this: + * + * ```typescript + * { + * onMessage: undefined, + * postMessage: (message) => void, + * addEventListener: (eventType: string, Function) => void, + * removeEventListener: (eventType: string, Function) => void + * } + * ``` + * + * You send messages to `postMessage` and listen with `addEventListener`. Once the event is received, + * we also remove the listener with `removeEventListener`. + * + * @link https://developer.android.com/reference/androidx/webkit/WebViewCompat#addWebMessageListener(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E,androidx.webkit.WebViewCompat.WebMessageListener) + * @implements {MessagingTransport} + */ +class AndroidMessagingTransport { + /** @type {AndroidMessagingConfig} */ + config; + globals = { + capturedHandlers: {} + }; + /** + * @param {AndroidMessagingConfig} config + */ + constructor(config) { + this.config = config; + } + + /** + * Given the method name, returns the related Android handler + * @param {string} methodName + * @returns {AndroidHandler} + * @private + */ + _getHandler(methodName) { + const androidSpecificName = this._getHandlerName(methodName); + if (!(androidSpecificName in window)) { + throw new _messaging.MissingHandler(`Missing android handler: '${methodName}'`, methodName); + } + return window[androidSpecificName]; + } + + /** + * Given the autofill method name, it returns the Android-specific handler name + * @param {string} internalName + * @returns {string} + * @private + */ + _getHandlerName(internalName) { + return 'ddg' + internalName[0].toUpperCase() + internalName.slice(1); + } + + /** + * @param {string} name + * @param {Record} [data] + */ + notify(name) { + let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const handler = this._getHandler(name); + const message = data ? JSON.stringify(data) : ''; + handler.postMessage(message); + } + + /** + * @param {string} name + * @param {Record} [data] + */ + async request(name) { + let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + // Set up the listener first + const handler = this._getHandler(name); + const responseOnce = new Promise(resolve => { + const responseHandler = e => { + handler.removeEventListener('message', responseHandler); + resolve(e.data); + }; + handler.addEventListener('message', responseHandler); + }); + + // Then send the message + this.notify(name, data); + + // And return once the promise resolves + const responseJSON = await responseOnce; + return JSON.parse(responseJSON); + } +} + +/** + * Use this configuration to create an instance of {@link Messaging} for Android + */ +exports.AndroidMessagingTransport = AndroidMessagingTransport; +class AndroidMessagingConfig { + /** + * All the expected Android handler names + * @param {{messageHandlerNames: string[]}} config + */ + constructor(config) { + this.messageHandlerNames = config.messageHandlerNames; + } +} +exports.AndroidMessagingConfig = AndroidMessagingConfig; + +},{"./messaging.js":16}],16:[function(require,module,exports){ +"use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); @@ -4602,6 +4726,7 @@ Object.defineProperty(exports, "WebkitMessagingConfig", { } }); var _webkit = require("./webkit.js"); +var _android = require("./android.js"); /** * @module Messaging * @@ -4660,7 +4785,7 @@ var _webkit = require("./webkit.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig} config + * @param {WebkitMessagingConfig | AndroidMessagingConfig} config */ constructor(config) { this.transport = getTransport(config); @@ -4732,7 +4857,7 @@ class MessagingTransport { } /** - * @param {WebkitMessagingConfig} config + * @param {WebkitMessagingConfig | AndroidMessagingConfig} config * @returns {MessagingTransport} */ exports.MessagingTransport = MessagingTransport; @@ -4740,6 +4865,9 @@ function getTransport(config) { if (config instanceof _webkit.WebkitMessagingConfig) { return new _webkit.WebkitMessagingTransport(config); } + if (config instanceof _android.AndroidMessagingConfig) { + return new _android.AndroidMessagingTransport(config); + } throw new Error('unreachable'); } @@ -4762,7 +4890,7 @@ class MissingHandler extends Error { */ exports.MissingHandler = MissingHandler; -},{"./webkit.js":16}],16:[function(require,module,exports){ +},{"./android.js":15,"./webkit.js":17}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5157,7 +5285,7 @@ function captureGlobals() { }; } -},{"./messaging.js":15}],17:[function(require,module,exports){ +},{"./messaging.js":16}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5288,7 +5416,7 @@ function _safeHostname(inputHostname) { } } -},{"./lib/apple.password.js":18,"./lib/constants.js":19,"./lib/rules-parser.js":20}],18:[function(require,module,exports){ +},{"./lib/apple.password.js":19,"./lib/constants.js":20,"./lib/rules-parser.js":21}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5817,7 +5945,7 @@ class Password { } exports.Password = Password; -},{"./constants.js":19,"./rules-parser.js":20}],19:[function(require,module,exports){ +},{"./constants.js":20,"./rules-parser.js":21}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5837,7 +5965,7 @@ const constants = exports.constants = { DEFAULT_UNAMBIGUOUS_CHARS }; -},{}],20:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -6433,7 +6561,7 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return newPasswordRules; } -},{}],21:[function(require,module,exports){ +},{}],22:[function(require,module,exports){ module.exports={ "163.com": { "password-rules": "minlength: 6; maxlength: 16;" @@ -7465,7 +7593,7 @@ module.exports={ "password-rules": "minlength: 8; maxlength: 32; max-consecutive: 6; required: lower; required: upper; required: digit;" } } -},{}],22:[function(require,module,exports){ +},{}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7529,7 +7657,7 @@ class CredentialsImport { } exports.CredentialsImport = CredentialsImport; -},{"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],23:[function(require,module,exports){ +},{"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7585,7 +7713,7 @@ function createDevice() { return new _ExtensionInterface.ExtensionInterface(globalConfig, deviceApi, settings); } -},{"../packages/device-api/index.js":12,"./DeviceInterface/AndroidInterface.js":24,"./DeviceInterface/AppleDeviceInterface.js":25,"./DeviceInterface/AppleOverlayDeviceInterface.js":26,"./DeviceInterface/ExtensionInterface.js":27,"./DeviceInterface/WindowsInterface.js":29,"./DeviceInterface/WindowsOverlayDeviceInterface.js":30,"./Settings.js":51,"./config.js":66,"./deviceApiCalls/transports/transports.js":74}],24:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./DeviceInterface/AndroidInterface.js":25,"./DeviceInterface/AppleDeviceInterface.js":26,"./DeviceInterface/AppleOverlayDeviceInterface.js":27,"./DeviceInterface/ExtensionInterface.js":28,"./DeviceInterface/WindowsInterface.js":30,"./DeviceInterface/WindowsOverlayDeviceInterface.js":31,"./Settings.js":52,"./config.js":67,"./deviceApiCalls/transports/transports.js":75}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7605,25 +7733,35 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {Promise} */ async getAlias() { - const { - alias - } = await (0, _autofillUtils.sendAndWaitForAnswer)(async () => { - if (this.inContextSignup.isAvailable()) { - const { - isSignedIn - } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); + // If in-context signup is available, do that first + if (this.inContextSignup.isAvailable()) { + const { + isSignedIn + } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); + if (isSignedIn) { // On Android we can't get the input type data again without // refreshing the page, so instead we can mutate it now that we // know the user has Email Protection available. - if (this.globalConfig.availableInputTypes) { - this.globalConfig.availableInputTypes.email = isSignedIn; + if (this.settings.availableInputTypes) { + this.settings.setAvailableInputTypes({ + email: isSignedIn + }); } this.updateForStateChange(); this.onFinishedAutofill(); } - return window.EmailInterface.showTooltip(); - }, 'getAliasResponse'); - return alias; + } + // Then, if successful actually prompt to fill + if (this.settings.availableInputTypes.email) { + const { + alias + } = await this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetAliasCall({ + requiresUserPermission: !this.globalConfig.isApp, + shouldConsumeAliasIfProvided: !this.globalConfig.isApp, + isIncontextSignupAvailable: this.inContextSignup.isAvailable() + })); + return alias ? (0, _autofillUtils.formatDuckAddress)(alias) : undefined; + } } /** @@ -7638,14 +7776,9 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {boolean} */ isDeviceSignedIn() { - // on DDG domains, always check via `window.EmailInterface.isSignedIn()` - if (this.globalConfig.isDDGDomain) { - return window.EmailInterface.isSignedIn() === 'true'; - } - // on non-DDG domains, where `availableInputTypes.email` is present, use it - if (typeof this.globalConfig.availableInputTypes?.email === 'boolean') { - return this.globalConfig.availableInputTypes.email; + if (typeof this.settings.availableInputTypes?.email === 'boolean') { + return this.settings.availableInputTypes.email; } // ...on other domains we assume true because the script wouldn't exist otherwise @@ -7660,15 +7793,7 @@ class AndroidInterface extends _InterfacePrototype.default { * Settings page displays data of the logged in user data */ getUserData() { - let userData = null; - try { - userData = JSON.parse(window.EmailInterface.getUserData()); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } - return Promise.resolve(userData); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetUserDataCall({})); } /** @@ -7676,25 +7801,13 @@ class AndroidInterface extends _InterfacePrototype.default { * Device capabilities determine which functionality is available to the user */ getEmailProtectionCapabilities() { - let deviceCapabilities = null; - try { - deviceCapabilities = JSON.parse(window.EmailInterface.getDeviceCapabilities()); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } - return Promise.resolve(deviceCapabilities); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetCapabilitiesCall({})); } storeUserData(_ref) { let { - addUserData: { - token, - userName, - cohort - } + addUserData } = _ref; - return window.EmailInterface.storeCredentials(token, userName, cohort); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionStoreUserDataCall(addUserData)); } /** @@ -7702,13 +7815,7 @@ class AndroidInterface extends _InterfacePrototype.default { * Provides functionality to log the user out */ removeUserData() { - try { - return window.EmailInterface.removeCredentials(); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionRemoveUserDataCall({})); } /** @@ -7733,7 +7840,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":45,"../UI/controllers/NativeUIController.js":59,"../autofill-utils.js":64,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28}],25:[function(require,module,exports){ +},{"../InContextSignup.js":46,"../UI/controllers/NativeUIController.js":60,"../autofill-utils.js":65,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8089,7 +8196,7 @@ class AppleDeviceInterface extends _InterfacePrototype.default { } exports.AppleDeviceInterface = AppleDeviceInterface; -},{"../../packages/device-api/index.js":12,"../Form/matching.js":44,"../InContextSignup.js":45,"../ThirdPartyProvider.js":52,"../UI/HTMLTooltip.js":57,"../UI/controllers/HTMLTooltipUIController.js":58,"../UI/controllers/NativeUIController.js":59,"../UI/controllers/OverlayUIController.js":60,"../autofill-utils.js":64,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"../deviceApiCalls/additionalDeviceApiCalls.js":70,"./InterfacePrototype.js":28}],26:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../Form/matching.js":45,"../InContextSignup.js":46,"../ThirdPartyProvider.js":53,"../UI/HTMLTooltip.js":58,"../UI/controllers/HTMLTooltipUIController.js":59,"../UI/controllers/NativeUIController.js":60,"../UI/controllers/OverlayUIController.js":61,"../autofill-utils.js":65,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"../deviceApiCalls/additionalDeviceApiCalls.js":71,"./InterfacePrototype.js":29}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8208,7 +8315,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":58,"./AppleDeviceInterface.js":25,"./overlayApi.js":32}],27:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":59,"./AppleDeviceInterface.js":26,"./overlayApi.js":33}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8427,7 +8534,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":44,"../InContextSignup.js":45,"../UI/HTMLTooltip.js":57,"../UI/controllers/HTMLTooltipUIController.js":58,"../autofill-utils.js":64,"./InterfacePrototype.js":28}],28:[function(require,module,exports){ +},{"../Form/matching.js":45,"../InContextSignup.js":46,"../UI/HTMLTooltip.js":58,"../UI/controllers/HTMLTooltipUIController.js":59,"../autofill-utils.js":65,"./InterfacePrototype.js":29}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8696,8 +8803,7 @@ class InterfacePrototype { } async startInit() { if (this.isInitializationStarted) return; - this.alreadyInitialized = true; - await this.settings.refresh(); + this.isInitializationStarted = true; this.addDeviceListeners(); await this.setupAutofill(); this.uiController = this.createUIController(); @@ -9031,11 +9137,19 @@ class InterfacePrototype { let userData; try { userData = await this.getUserData(); - } catch (e) {} + } catch (e) { + if (this.isTestMode()) { + console.log('getUserData failed with', e); + } + } let capabilities; try { capabilities = await this.getEmailProtectionCapabilities(); - } catch (e) {} + } catch (e) { + if (this.isTestMode()) { + console.log('capabilities fetching failed with', e); + } + } // Set up listener for web app actions if (this.globalConfig.isDDGDomain) { @@ -9091,6 +9205,13 @@ class InterfacePrototype { const data = await (0, _autofillUtils.sendAndWaitForAnswer)(_autofillUtils.SIGN_IN_MSG, 'addUserData'); // This call doesn't send a response, so we can't know if it succeeded this.storeUserData(data); + + // Assuming the previous call succeeded, let's update availableInputTypes + if (this.settings.availableInputTypes) { + this.settings.setAvailableInputTypes({ + email: true + }); + } await this.setupAutofill(); await this.settings.refresh(); await this.setupSettingsPage({ @@ -9259,7 +9380,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":12,"../CredentialsImport.js":22,"../EmailProtection.js":33,"../Form/formatters.js":37,"../Form/matching.js":44,"../InputTypes/Credentials.js":46,"../PasswordGenerator.js":49,"../Scanner.js":50,"../Settings.js":51,"../UI/controllers/NativeUIController.js":59,"../autofill-utils.js":64,"../config.js":66,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"../deviceApiCalls/transports/transports.js":74,"../locales/strings.js":99,"./initFormSubmissionsApi.js":31}],29:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../CredentialsImport.js":23,"../EmailProtection.js":34,"../Form/formatters.js":38,"../Form/matching.js":45,"../InputTypes/Credentials.js":47,"../PasswordGenerator.js":50,"../Scanner.js":51,"../Settings.js":52,"../UI/controllers/NativeUIController.js":60,"../autofill-utils.js":65,"../config.js":67,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"../deviceApiCalls/transports/transports.js":75,"../locales/strings.js":100,"./initFormSubmissionsApi.js":32}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9344,13 +9465,13 @@ class WindowsInterface extends _InterfacePrototype.default { return await this.credentialsImport.refresh(); } default: - if (this.globalConfig.isDDGTestMode) { + if (this.isTestMode()) { console.warn('unhandled response', resp); } return this._closeAutofillParent(); } } catch (e) { - if (this.globalConfig.isDDGTestMode) { + if (this.isTestMode()) { if (e instanceof DOMException && e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -9425,7 +9546,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":60,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28}],30:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":61,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9604,7 +9725,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":58,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28,"./overlayApi.js":32}],31:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":59,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29,"./overlayApi.js":33}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9703,7 +9824,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":40,"../autofill-utils.js":64}],32:[function(require,module,exports){ +},{"../Form/label-util.js":41,"../autofill-utils.js":65}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9761,7 +9882,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":68}],33:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":69}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9796,7 +9917,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],34:[function(require,module,exports){ +},{}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9936,7 +10057,7 @@ class Form { } submitHandler() { let via = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown'; - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('Form.submitHandler via:', via, this); } if (this.submitHandlerExecuted) return; @@ -10703,7 +10824,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":64,"../constants.js":67,"./FormAnalyzer.js":35,"./formatters.js":37,"./inputStyles.js":38,"./inputTypeConfig.js":39,"./matching.js":44}],35:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":47,"../autofill-utils.js":65,"../constants.js":68,"./FormAnalyzer.js":36,"./formatters.js":38,"./inputStyles.js":39,"./inputTypeConfig.js":40,"./matching.js":45}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11073,7 +11194,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":64,"../constants.js":67,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching.js":44}],36:[function(require,module,exports){ +},{"../autofill-utils.js":65,"../constants.js":68,"./matching-config/__generated__/compiled-matching-config.js":43,"./matching.js":45}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11638,7 +11759,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],37:[function(require,module,exports){ +},{}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11943,7 +12064,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"./countryNames.js":37,"./matching.js":45}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12034,7 +12155,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":39}],39:[function(require,module,exports){ +},{"./inputTypeConfig.js":40}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12288,7 +12409,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":46,"../InputTypes/CreditCard.js":47,"../InputTypes/Identity.js":48,"../UI/img/ddgPasswordIcon.js":62,"../constants.js":67,"./logo-svg.js":41,"./matching.js":44}],40:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":47,"../InputTypes/CreditCard.js":48,"../InputTypes/Identity.js":49,"../UI/img/ddgPasswordIcon.js":63,"../constants.js":68,"./logo-svg.js":42,"./matching.js":45}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12336,7 +12457,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":44}],41:[function(require,module,exports){ +},{"./matching.js":45}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12369,7 +12490,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],42:[function(require,module,exports){ +},{}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12822,7 +12943,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],43:[function(require,module,exports){ +},{}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12897,7 +13018,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":64,"./matching.js":44}],44:[function(require,module,exports){ +},{"../autofill-utils.js":65,"./matching.js":45}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13889,7 +14010,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":64,"../constants.js":67,"./label-util.js":40,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching-utils.js":43}],45:[function(require,module,exports){ +},{"../autofill-utils.js":65,"../constants.js":68,"./label-util.js":41,"./matching-config/__generated__/compiled-matching-config.js":43,"./matching-utils.js":44}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14021,7 +14142,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":64,"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],46:[function(require,module,exports){ +},{"./autofill-utils.js":65,"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14177,7 +14298,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":64}],47:[function(require,module,exports){ +},{"../autofill-utils.js":65}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14202,7 +14323,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],48:[function(require,module,exports){ +},{}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14248,7 +14369,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":37}],49:[function(require,module,exports){ +},{"../Form/formatters.js":38}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14290,7 +14411,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":17,"../packages/password/rules.json":21}],50:[function(require,module,exports){ +},{"../packages/password/index.js":18,"../packages/password/rules.json":22}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14751,7 +14872,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":34,"./Form/matching.js":44,"./autofill-utils.js":64,"./constants.js":67,"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],51:[function(require,module,exports){ +},{"./Form/Form.js":35,"./Form/matching.js":45,"./autofill-utils.js":65,"./constants.js":68,"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14897,6 +15018,11 @@ class Settings { if (this._runtimeConfiguration) return this._runtimeConfiguration; const runtimeConfig = await this.deviceApi.request(new _deviceApiCalls.GetRuntimeConfigurationCall(null)); this._runtimeConfiguration = runtimeConfig; + + // If the platform sends availableInputTypes here, store them + if (runtimeConfig.availableInputTypes) { + this.setAvailableInputTypes(runtimeConfig.availableInputTypes); + } return this._runtimeConfiguration; } @@ -14912,6 +15038,9 @@ class Settings { if (this.globalConfig.isTopFrame) { return Settings.defaults.availableInputTypes; } + if (this._availableInputTypes) { + return this.availableInputTypes; + } return await this.deviceApi.request(new _deviceApiCalls.GetAvailableInputTypesCall(null)); } catch (e) { if (this.globalConfig.isDDGTestMode) { @@ -15164,7 +15293,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":12,"./autofill-utils.js":64,"./deviceApiCalls/__generated__/deviceApiCalls.js":68,"./deviceApiCalls/__generated__/validators.zod.js":69}],52:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./autofill-utils.js":65,"./deviceApiCalls/__generated__/deviceApiCalls.js":69,"./deviceApiCalls/__generated__/validators.zod.js":70}],53:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15231,7 +15360,7 @@ class ThirdPartyProvider { this.device.scanner.forms.forEach(form => form.recategorizeAllInputs()); } } catch (e) { - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('isDDGTestMode: providerStatusUpdated error: ❌', e); } } @@ -15246,7 +15375,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -15254,7 +15383,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":12,"./Form/matching.js":44,"./deviceApiCalls/__generated__/deviceApiCalls.js":68,"./deviceApiCalls/__generated__/validators.zod.js":69}],53:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./Form/matching.js":45,"./deviceApiCalls/__generated__/deviceApiCalls.js":69,"./deviceApiCalls/__generated__/validators.zod.js":70}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15295,7 +15424,7 @@ ${this.options.css} } var _default = exports.default = CredentialsImportTooltip; -},{"./HTMLTooltip.js":57}],54:[function(require,module,exports){ +},{"./HTMLTooltip.js":58}],55:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15446,7 +15575,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":64,"./HTMLTooltip.js":57}],55:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":47,"../autofill-utils.js":65,"./HTMLTooltip.js":58}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15528,7 +15657,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":64,"./HTMLTooltip.js":57}],56:[function(require,module,exports){ +},{"../autofill-utils.js":65,"./HTMLTooltip.js":58}],57:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15581,7 +15710,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":57}],57:[function(require,module,exports){ +},{"./HTMLTooltip.js":58}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15969,7 +16098,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":44,"../autofill-utils.js":64,"./styles/styles.js":63}],58:[function(require,module,exports){ +},{"../Form/matching.js":45,"../autofill-utils.js":65,"./styles/styles.js":64}],59:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16337,7 +16466,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":39,"../../Form/matching.js":44,"../../autofill-utils.js":64,"../CredentialsImportTooltip.js":53,"../DataHTMLTooltip.js":54,"../EmailHTMLTooltip.js":55,"../EmailSignupHTMLTooltip.js":56,"../HTMLTooltip.js":57,"./UIController.js":61}],59:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":40,"../../Form/matching.js":45,"../../autofill-utils.js":65,"../CredentialsImportTooltip.js":54,"../DataHTMLTooltip.js":55,"../EmailHTMLTooltip.js":56,"../EmailSignupHTMLTooltip.js":57,"../HTMLTooltip.js":58,"./UIController.js":62}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16439,6 +16568,11 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } + case 'none': + { + // do nothing + break; + } default: { if (args.device.isTestMode()) { @@ -16499,7 +16633,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":44,"../../InputTypes/Credentials.js":46,"../../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./UIController.js":61}],60:[function(require,module,exports){ +},{"../../Form/matching.js":45,"../../InputTypes/Credentials.js":47,"../../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./UIController.js":62}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16736,7 +16870,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":44,"./UIController.js":61}],61:[function(require,module,exports){ +},{"../../Form/matching.js":45,"./UIController.js":62}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16820,7 +16954,7 @@ class UIController { } exports.UIController = UIController; -},{}],62:[function(require,module,exports){ +},{}],63:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16837,7 +16971,7 @@ const ddgCcIconBase = exports.ddgCcIconBase = 'data:image/svg+xml;base64,PD94bWw const ddgCcIconFilled = exports.ddgCcIconFilled = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzc2NDMxMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjNzY0MzEwIi8+Cjwvc3ZnPgo='; const ddgIdentityIconBase = exports.ddgIdentityIconBase = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4KPHBhdGggeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSJub25lIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMiAyMWMyLjE0MyAwIDQuMTExLS43NSA1LjY1Ny0yLS42MjYtLjUwNi0xLjMxOC0uOTI3LTIuMDYtMS4yNS0xLjEtLjQ4LTIuMjg1LS43MzUtMy40ODYtLjc1LTEuMi0uMDE0LTIuMzkyLjIxMS0zLjUwNC42NjQtLjgxNy4zMzMtMS41OC43ODMtMi4yNjQgMS4zMzYgMS41NDYgMS4yNSAzLjUxNCAyIDUuNjU3IDJ6bTQuMzk3LTUuMDgzYy45NjcuNDIyIDEuODY2Ljk4IDIuNjcyIDEuNjU1QzIwLjI3OSAxNi4wMzkgMjEgMTQuMTA0IDIxIDEyYzAtNC45Ny00LjAzLTktOS05cy05IDQuMDMtOSA5YzAgMi4xMDQuNzIyIDQuMDQgMS45MzIgNS41NzIuODc0LS43MzQgMS44Ni0xLjMyOCAyLjkyMS0xLjc2IDEuMzYtLjU1NCAyLjgxNi0uODMgNC4yODMtLjgxMSAxLjQ2Ny4wMTggMi45MTYuMzMgNC4yNi45MTZ6TTEyIDIzYzYuMDc1IDAgMTEtNC45MjUgMTEtMTFTMTguMDc1IDEgMTIgMSAxIDUuOTI1IDEgMTJzNC45MjUgMTEgMTEgMTF6bTMtMTNjMCAxLjY1Ny0xLjM0MyAzLTMgM3MtMy0xLjM0My0zLTMgMS4zNDMtMyAzLTMgMyAxLjM0MyAzIDN6bTIgMGMwIDIuNzYxLTIuMjM5IDUtNSA1cy01LTIuMjM5LTUtNSAyLjIzOS01IDUtNSA1IDIuMjM5IDUgNXoiIGZpbGw9IiMwMDAiLz4KPC9zdmc+Cg==`; -},{}],63:[function(require,module,exports){ +},{}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16846,7 +16980,7 @@ Object.defineProperty(exports, "__esModule", { exports.CSS_STYLES = void 0; const CSS_STYLES = exports.CSS_STYLES = ":root {\n color-scheme: light dark;\n}\n\n.wrapper *, .wrapper *::before, .wrapper *::after {\n box-sizing: border-box;\n}\n.wrapper {\n position: fixed;\n top: 0;\n left: 0;\n padding: 0;\n font-family: 'DDG_ProximaNova', 'Proxima Nova', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n -webkit-font-smoothing: antialiased;\n z-index: 2147483647;\n}\n.wrapper--data {\n font-family: 'SF Pro Text', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n}\n.wrapper:not(.top-autofill) .tooltip {\n position: absolute;\n width: 300px;\n max-width: calc(100vw - 25px);\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--data, #topAutofill {\n background-color: rgba(242, 240, 240, 1);\n -webkit-backdrop-filter: blur(40px);\n backdrop-filter: blur(40px);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data, #topAutofill {\n background: rgb(100, 98, 102, .9);\n }\n}\n.tooltip--data {\n padding: 6px;\n font-size: 13px;\n line-height: 14px;\n width: 315px;\n max-height: 290px;\n overflow-y: auto;\n}\n.top-autofill .tooltip--data {\n min-height: 100vh;\n}\n.tooltip--data.tooltip--incontext-signup {\n width: 360px;\n}\n.wrapper:not(.top-autofill) .tooltip--data {\n top: 100%;\n left: 100%;\n border: 0.5px solid rgba(255, 255, 255, 0.2);\n border-radius: 6px;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.32);\n}\n@media (prefers-color-scheme: dark) {\n .wrapper:not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n.wrapper:not(.top-autofill) .tooltip--email {\n top: calc(100% + 6px);\n right: calc(100% - 48px);\n padding: 8px;\n border: 1px solid #D0D0D0;\n border-radius: 10px;\n background-color: #FFFFFF;\n font-size: 14px;\n line-height: 1.3;\n color: #333333;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);\n}\n.tooltip--email__caret {\n position: absolute;\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--email__caret::before,\n.tooltip--email__caret::after {\n content: \"\";\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n display: block;\n border-bottom: 8px solid #D0D0D0;\n position: absolute;\n right: -28px;\n}\n.tooltip--email__caret::before {\n border-bottom-color: #D0D0D0;\n top: -1px;\n}\n.tooltip--email__caret::after {\n border-bottom-color: #FFFFFF;\n top: 0px;\n}\n\n/* Buttons */\n.tooltip__button {\n display: flex;\n width: 100%;\n padding: 8px 8px 8px 0px;\n font-family: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: 6px;\n text-align: left;\n}\n.tooltip__button.currentFocus,\n.wrapper:not(.top-autofill) .tooltip__button:hover {\n background-color: #3969EF;\n color: #FFFFFF;\n}\n\n/* Data autofill tooltip specific */\n.tooltip__button--data {\n position: relative;\n min-height: 48px;\n flex-direction: row;\n justify-content: flex-start;\n font-size: inherit;\n font-weight: 500;\n line-height: 16px;\n text-align: left;\n border-radius: 3px;\n}\n.tooltip--data__item-container {\n max-height: 220px;\n overflow: auto;\n}\n.tooltip__button--data:first-child {\n margin-top: 0;\n}\n.tooltip__button--data:last-child {\n margin-bottom: 0;\n}\n.tooltip__button--data::before {\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 20px 20px;\n background-repeat: no-repeat;\n background-position: center center;\n}\n#provider_locked::after {\n position: absolute;\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 11px 13px;\n background-repeat: no-repeat;\n background-position: right bottom;\n}\n.tooltip__button--data.currentFocus:not(.tooltip__button--data--bitwarden)::before,\n.wrapper:not(.top-autofill) .tooltip__button--data:not(.tooltip__button--data--bitwarden):hover::before {\n filter: invert(100%);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before,\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before {\n filter: invert(100%);\n opacity: .9;\n }\n}\n.tooltip__button__text-container {\n margin: auto 0;\n}\n.label {\n display: block;\n font-weight: 400;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.8);\n font-size: 13px;\n line-height: 1;\n}\n.label + .label {\n margin-top: 2px;\n}\n.label.label--medium {\n font-weight: 500;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.9);\n}\n.label.label--small {\n font-size: 11px;\n font-weight: 400;\n letter-spacing: 0.06px;\n color: rgba(0,0,0,0.6);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data .label {\n color: #ffffff;\n }\n .tooltip--data .label--medium {\n color: #ffffff;\n }\n .tooltip--data .label--small {\n color: #cdcdcd;\n }\n}\n.tooltip__button.currentFocus .label,\n.wrapper:not(.top-autofill) .tooltip__button:hover .label {\n color: #FFFFFF;\n}\n\n.tooltip__button--manage {\n font-size: 13px;\n padding: 5px 9px;\n border-radius: 3px;\n margin: 0;\n}\n\n/* Icons */\n.tooltip__button--data--credentials::before,\n.tooltip__button--data--credentials__current::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNS4zMzQgNi42NjdhMiAyIDAgMSAwIDAgNCAyIDIgMCAwIDAgMC00Wm0tLjY2NyAyYS42NjcuNjY3IDAgMSAxIDEuMzMzIDAgLjY2Ny42NjcgMCAwIDEtMS4zMzMgMFoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNjY3IDRhNS4zMzMgNS4zMzMgMCAwIDAtNS4xODggNi41NzhsLTUuMjg0IDUuMjg0YS42NjcuNjY3IDAgMCAwLS4xOTUuNDcxdjNjMCAuMzY5LjI5OC42NjcuNjY3LjY2N2gyLjY2NmMuNzM3IDAgMS4zMzQtLjU5NyAxLjMzNC0xLjMzM1YxOGguNjY2Yy43MzcgMCAxLjMzNC0uNTk3IDEuMzM0LTEuMzMzdi0xLjMzNEgxMmMuMTc3IDAgLjM0Ni0uMDcuNDcxLS4xOTVsLjY4OC0uNjg4QTUuMzMzIDUuMzMzIDAgMSAwIDE0LjY2NyA0Wm0tNCA1LjMzM2E0IDQgMCAxIDEgMi41NTUgMy43MzIuNjY3LjY2NyAwIDAgMC0uNzEzLjE1bC0uNzg1Ljc4NUgxMGEuNjY3LjY2NyAwIDAgMC0uNjY3LjY2N3YySDhhLjY2Ny42NjcgMCAwIDAtLjY2Ny42NjZ2MS4zMzRoLTJ2LTIuMDU4bDUuMzY1LTUuMzY0YS42NjcuNjY3IDAgMCAwIC4xNjMtLjY3NyAzLjk5NiAzLjk5NiAwIDAgMS0uMTk0LTEuMjM1WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--credentials__new::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNOC4wNDcgNC42MjVDNy45MzcgNC4xMjUgNy44NjIgNCA3LjUgNGMtLjM2MiAwLS40MzguMTI1LS41NDcuNjI1LS4wNjguMzEtLjE3NyAxLjMzOC0uMjUxIDIuMDc3LS43MzguMDc0LTEuNzY3LjE4My0yLjA3Ny4yNTEtLjUuMTEtLjYyNS4xODQtLjYyNS41NDcgMCAuMzYyLjEyNS40MzcuNjI1LjU0Ny4zMS4wNjcgMS4zMzYuMTc3IDIuMDc0LjI1LjA3My43NjcuMTg1IDEuODQyLjI1NCAyLjA3OC4xMS4zNzUuMTg1LjYyNS41NDcuNjI1LjM2MiAwIC40MzgtLjEyNS41NDctLjYyNS4wNjgtLjMxLjE3Ny0xLjMzNi4yNS0yLjA3NC43NjctLjA3MyAxLjg0Mi0uMTg1IDIuMDc4LS4yNTQuMzc1LS4xMS42MjUtLjE4NS42MjUtLjU0NyAwLS4zNjMtLjEyNS0uNDM4LS42MjUtLjU0Ny0uMzEtLjA2OC0xLjMzOS0uMTc3LTIuMDc3LS4yNTEtLjA3NC0uNzM5LS4xODMtMS43NjctLjI1MS0yLjA3N1oiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0xNC42ODEgNS4xOTljLS43NjYgMC0xLjQ4Mi4yMS0yLjA5My41NzhhLjYzNi42MzYgMCAwIDEtLjY1NS0xLjA5IDUuMzQgNS4zNCAwIDEgMSAxLjMwMiA5LjcyMmwtLjc3NS43NzZhLjYzNi42MzYgMCAwIDEtLjQ1LjE4NmgtMS4zOTh2MS42NWMwIC40OTMtLjQuODkzLS44OTMuODkzSDguNTc4djEuMTQxYzAgLjQ5NC0uNC44OTMtLjg5NC44OTNINC42MzZBLjYzNi42MzYgMCAwIDEgNCAxOS4zMTNWMTYuMjZjMC0uMTY5LjA2Ny0uMzMuMTg2LS40NWw1LjU2Mi01LjU2MmEuNjM2LjYzNiAwIDEgMSAuOS45bC01LjM3NiA1LjM3NXYyLjE1M2gyLjAzNHYtMS4zOTljMC0uMzUuMjg1LS42MzYuNjM2LS42MzZIOS4zNHYtMS45MDdjMC0uMzUxLjI4NC0uNjM2LjYzNS0uNjM2aDEuNzcxbC44NjQtLjg2M2EuNjM2LjYzNiAwIDAgMSAuNjY4LS4xNDcgNC4wNjkgNC4wNjkgMCAxIDAgMS40MDItNy44OVoiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTEzLjYyNSA4LjQ5OWExLjg3NSAxLjg3NSAwIDEgMSAzLjc1IDAgMS44NzUgMS44NzUgMCAwIDEtMy43NSAwWm0xLjg3NS0uNjI1YS42MjUuNjI1IDAgMSAwIDAgMS4yNS42MjUuNjI1IDAgMCAwIDAtMS4yNVoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNjI1IDEyLjEyNWEuNjI1LjYyNSAwIDEgMCAwLTEuMjUuNjI1LjYyNSAwIDAgMCAwIDEuMjVaIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--creditCards::before {\n background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzAwMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjMDAwIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--identities::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4=');\n}\n.tooltip__button--data--credentials.tooltip__button--data--bitwarden::before,\n.tooltip__button--data--credentials__current.tooltip__button--data--bitwarden::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iOCIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE4LjU2OTYgNS40MzM1NUMxOC41MDg0IDUuMzc0NDIgMTguNDM0NyA1LjMyNzYzIDE4LjM1MzEgNS4yOTYxMUMxOC4yNzE1IDUuMjY0NiAxOC4xODM3IDUuMjQ5MDQgMTguMDk1MyA1LjI1MDQxSDUuOTIxOTFDNS44MzMyNiA1LjI0NzI5IDUuNzQ0OTMgNS4yNjIwNSA1LjY2MzA0IDUuMjkzNjdDNS41ODExNSA1LjMyNTI5IDUuNTA3NjUgNS4zNzMwMiA1LjQ0NzYyIDUuNDMzNTVDNS4zMjE3IDUuNTUwMTMgNS4yNTA2NSA1LjcwODE1IDUuMjUgNS44NzMxVjEzLjM4MjFDNS4yNTMzNiAxMy45NTM1IDUuMzc0MDggMTQuNTE5MSA1LjYwNTcyIDE1LjA0ODdDNS44MTkzMSAxNS41NzI4IDYuMTEyMDcgMTYuMDY2MSA2LjQ3NTI0IDE2LjUxMzlDNi44NDIgMTYuOTY4MyA3LjI1OTI5IDE3LjM4NTcgNy43MjAyNSAxNy43NTkzQzguMTQwNTMgMTguMTI1NiA4LjU4OTcxIDE4LjQ2MjMgOS4wNjQwNyAxOC43NjY2QzkuNDU5MzEgMTkuMDIzIDkuOTEzODMgMTkuMjc5NCAxMC4zNDg2IDE5LjUxNzVDMTAuNzgzNCAxOS43NTU2IDExLjA5OTYgMTkuOTIwNCAxMS4yNzc0IDE5Ljk5MzdDMTEuNDU1MyAyMC4wNjY5IDExLjYxMzQgMjAuMTQwMiAxMS43MTIyIDIwLjE5NTFDMTEuNzk5MiAyMC4yMzEzIDExLjg5MzUgMjAuMjUgMTEuOTg4OCAyMC4yNUMxMi4wODQyIDIwLjI1IDEyLjE3ODUgMjAuMjMxMyAxMi4yNjU1IDIwLjE5NTFDMTIuNDIxMiAyMC4xMzYzIDEyLjU3MjkgMjAuMDY5IDEyLjcyIDE5Ljk5MzdDMTIuNzcxMSAxOS45Njc0IDEyLjgzMzUgMTkuOTM2NiAxMi45MDY5IDE5LjkwMDRDMTMuMDg5MSAxOS44MTA1IDEzLjMzODggMTkuNjg3MiAxMy42NDg5IDE5LjUxNzVDMTQuMDgzNiAxOS4yNzk0IDE0LjUxODQgMTkuMDIzIDE0LjkzMzQgMTguNzY2NkMxNS40MDQgMTguNDU3NyAxNS44NTI4IDE4LjEyMTIgMTYuMjc3MiAxNy43NTkzQzE2LjczMzEgMTcuMzgwOSAxNy4xNDk5IDE2Ljk2NCAxNy41MjIyIDE2LjUxMzlDMTcuODc4IDE2LjA2MTcgMTguMTcwMiAxNS41NjkzIDE4LjM5MTcgMTUuMDQ4N0MxOC42MjM0IDE0LjUxOTEgMTguNzQ0MSAxMy45NTM1IDE4Ljc0NzQgMTMuMzgyMVY1Ljg3MzFDMTguNzU1NyA1Ljc5MjE0IDE4Ljc0MzkgNS43MTA1IDE4LjcxMzEgNS42MzQzNUMxOC42ODIzIDUuNTU4MiAxOC42MzMyIDUuNDg5NTQgMTguNTY5NiA1LjQzMzU1Wk0xNy4wMDg0IDEzLjQ1NTNDMTcuMDA4NCAxNi4xODQyIDEyLjAwODYgMTguNTI4NSAxMi4wMDg2IDE4LjUyODVWNi44NjIwOUgxNy4wMDg0VjEzLjQ1NTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');\n}\n#provider_locked:after {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMSAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgNy42MDA1N1Y3LjYwMjVWOS41MjI1QzEgMTAuMDgwMSAxLjIyMTUxIDEwLjYxNDkgMS42MTU4MSAxMS4wMDkyQzIuMDEwMSAxMS40MDM1IDIuNTQ0ODggMTEuNjI1IDMuMTAyNSAxMS42MjVINy4yNzI1QzcuNTQ4NjEgMTEuNjI1IDcuODIyMDEgMTEuNTcwNiA4LjA3NzA5IDExLjQ2NUM4LjMzMjE4IDExLjM1OTMgOC41NjM5NiAxMS4yMDQ0IDguNzU5MTkgMTEuMDA5MkM4Ljk1NDQzIDEwLjgxNCA5LjEwOTMgMTAuNTgyMiA5LjIxNDk2IDEwLjMyNzFDOS4zMjA2MiAxMC4wNzIgOS4zNzUgOS43OTg2MSA5LjM3NSA5LjUyMjVMOS4zNzUgNy42MDI1TDkuMzc1IDcuNjAwNTdDOS4zNzQxNSA3LjE2MTMxIDkuMjM1NzQgNi43MzMzNSA4Ljk3OTIyIDYuMzc2NzhDOC44NzY4MyA2LjIzNDQ2IDguNzU3NjggNi4xMDYzNyA4LjYyNSA1Ljk5NDg5VjUuMTg3NUM4LjYyNSA0LjI3NTgyIDguMjYyODQgMy40MDE0OCA3LjYxODE4IDIuNzU2ODJDNi45NzM1MiAyLjExMjE2IDYuMDk5MTggMS43NSA1LjE4NzUgMS43NUM0LjI3NTgyIDEuNzUgMy40MDE0OCAyLjExMjE2IDIuNzU2ODIgMi43NTY4MkMyLjExMjE2IDMuNDAxNDggMS43NSA0LjI3NTgyIDEuNzUgNS4xODc1VjUuOTk0ODlDMS42MTczMiA2LjEwNjM3IDEuNDk4MTcgNi4yMzQ0NiAxLjM5NTc4IDYuMzc2NzhDMS4xMzkyNiA2LjczMzM1IDEuMDAwODUgNy4xNjEzMSAxIDcuNjAwNTdaTTQuOTY4NyA0Ljk2ODdDNS4wMjY5NCA0LjkxMDQ3IDUuMTA1MzIgNC44NzY5OSA1LjE4NzUgNC44NzUwN0M1LjI2OTY4IDQuODc2OTkgNS4zNDgwNiA0LjkxMDQ3IDUuNDA2MyA0Ljk2ODdDNS40NjU0MiA1LjAyNzgzIDUuNDk5MDQgNS4xMDc3NCA1LjUgNS4xOTEzVjUuNUg0Ljg3NVY1LjE5MTNDNC44NzU5NiA1LjEwNzc0IDQuOTA5NTggNS4wMjc4MyA0Ljk2ODcgNC45Njg3WiIgZmlsbD0iIzIyMjIyMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');\n}\n\nhr {\n display: block;\n margin: 5px 9px;\n border: none; /* reset the border */\n border-top: 1px solid rgba(0,0,0,.1);\n}\n\nhr:first-child {\n display: none;\n}\n\n@media (prefers-color-scheme: dark) {\n hr {\n border-top: 1px solid rgba(255,255,255,.2);\n }\n}\n\n#privateAddress {\n align-items: flex-start;\n}\n#personalAddress::before,\n#privateAddress::before,\n#incontextSignup::before,\n#personalAddress.currentFocus::before,\n#personalAddress:hover::before,\n#privateAddress.currentFocus::before,\n#privateAddress:hover::before {\n filter: none;\n /* This is the same icon as `daxBase64` in `src/Form/logo-svg.js` */\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTY0IDEyOGMzNS4zNDYgMCA2NC0yOC42NTQgNjQtNjRzLTI4LjY1NC02NC02NC02NC02NCAyOC42NTQtNjQgNjQgMjguNjU0IDY0IDY0IDY0eiIgZmlsbD0iI2RlNTgzMyIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im03MyAxMTEuNzVjMC0uNS4xMjMtLjYxNC0xLjQ2Ni0zLjc4Mi00LjIyNC04LjQ1OS04LjQ3LTIwLjM4NC02LjU0LTI4LjA3NS4zNTMtMS4zOTctMy45NzgtNTEuNzQ0LTcuMDQtNTMuMzY1LTMuNDAyLTEuODEzLTcuNTg4LTQuNjktMTEuNDE4LTUuMzMtMS45NDMtLjMxLTQuNDktLjE2NC02LjQ4Mi4xMDUtLjM1My4wNDctLjM2OC42ODMtLjAzLjc5OCAxLjMwOC40NDMgMi44OTUgMS4yMTIgMy44MyAyLjM3NS4xNzguMjItLjA2LjU2Ni0uMzQyLjU3Ny0uODgyLjAzMi0yLjQ4Mi40MDItNC41OTMgMi4xOTUtLjI0NC4yMDctLjA0MS41OTIuMjczLjUzIDQuNTM2LS44OTcgOS4xNy0uNDU1IDExLjkgMi4wMjcuMTc3LjE2LjA4NC40NS0uMTQ3LjUxMi0yMy42OTQgNi40NC0xOS4wMDMgMjcuMDUtMTIuNjk2IDUyLjM0NCA1LjYxOSAyMi41MyA3LjczMyAyOS43OTIgOC40IDMyLjAwNGEuNzE4LjcxOCAwIDAgMCAuNDIzLjQ2N2M4LjE1NiAzLjI0OCAyNS45MjggMy4zOTIgMjUuOTI4LTIuMTMyeiIgZmlsbD0iI2RkZCIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBkPSJtNzYuMjUgMTE2LjVjLTIuODc1IDEuMTI1LTguNSAxLjYyNS0xMS43NSAxLjYyNS00Ljc2NCAwLTExLjYyNS0uNzUtMTQuMTI1LTEuODc1LTEuNTQ0LTQuNzUxLTYuMTY0LTE5LjQ4LTEwLjcyNy0zOC4xODVsLS40NDctMS44MjctLjAwNC0uMDE1Yy01LjQyNC0yMi4xNTctOS44NTUtNDAuMjUzIDE0LjQyNy00NS45MzguMjIyLS4wNTIuMzMtLjMxNy4xODQtLjQ5Mi0yLjc4Ni0zLjMwNS04LjAwNS00LjM4OC0xNC42MDUtMi4xMTEtLjI3LjA5My0uNTA2LS4xOC0uMzM3LS40MTIgMS4yOTQtMS43ODMgMy44MjMtMy4xNTUgNS4wNzEtMy43NTYuMjU4LS4xMjQuMjQyLS41MDItLjAzLS41ODhhMjcuODc3IDI3Ljg3NyAwIDAgMCAtMy43NzItLjljLS4zNy0uMDU5LS40MDMtLjY5My0uMDMyLS43NDMgOS4zNTYtMS4yNTkgMTkuMTI1IDEuNTUgMjQuMDI4IDcuNzI2YS4zMjYuMzI2IDAgMCAwIC4xODYuMTE0YzE3Ljk1MiAzLjg1NiAxOS4yMzggMzIuMjM1IDE3LjE3IDMzLjUyOC0uNDA4LjI1NS0xLjcxNS4xMDgtMy40MzgtLjA4NS02Ljk4Ni0uNzgxLTIwLjgxOC0yLjMyOS05LjQwMiAxOC45NDguMTEzLjIxLS4wMzYuNDg4LS4yNzIuNTI1LTYuNDM4IDEgMS44MTIgMjEuMTczIDcuODc1IDM0LjQ2MXoiIGZpbGw9IiNmZmYiLz4KICAgIDxwYXRoIGQ9Im04NC4yOCA5MC42OThjLTEuMzY3LS42MzMtNi42MjEgMy4xMzUtMTAuMTEgNi4wMjgtLjcyOC0xLjAzMS0yLjEwMy0xLjc4LTUuMjAzLTEuMjQyLTIuNzEzLjQ3Mi00LjIxMSAxLjEyNi00Ljg4IDIuMjU0LTQuMjgzLTEuNjIzLTExLjQ4OC00LjEzLTEzLjIyOS0xLjcxLTEuOTAyIDIuNjQ2LjQ3NiAxNS4xNjEgMy4wMDMgMTYuNzg2IDEuMzIuODQ5IDcuNjMtMy4yMDggMTAuOTI2LTYuMDA1LjUzMi43NDkgMS4zODggMS4xNzggMy4xNDggMS4xMzcgMi42NjItLjA2MiA2Ljk3OS0uNjgxIDcuNjQ5LTEuOTIxLjA0LS4wNzUuMDc1LS4xNjQuMTA1LS4yNjYgMy4zODggMS4yNjYgOS4zNSAyLjYwNiAxMC42ODIgMi40MDYgMy40Ny0uNTIxLS40ODQtMTYuNzIzLTIuMDktMTcuNDY3eiIgZmlsbD0iIzNjYTgyYiIvPgogICAgPHBhdGggZD0ibTc0LjQ5IDk3LjA5N2MuMTQ0LjI1Ni4yNi41MjYuMzU4LjguNDgzIDEuMzUyIDEuMjcgNS42NDguNjc0IDYuNzA5LS41OTUgMS4wNjItNC40NTkgMS41NzQtNi44NDMgMS42MTVzLTIuOTItLjgzMS0zLjQwMy0yLjE4MWMtLjM4Ny0xLjA4MS0uNTc3LTMuNjIxLS41NzItNS4wNzUtLjA5OC0yLjE1OC42OS0yLjkxNiA0LjMzNC0zLjUwNiAyLjY5Ni0uNDM2IDQuMTIxLjA3MSA0Ljk0NC45NCAzLjgyOC0yLjg1NyAxMC4yMTUtNi44ODkgMTAuODM4LTYuMTUyIDMuMTA2IDMuNjc0IDMuNDk5IDEyLjQyIDIuODI2IDE1LjkzOS0uMjIgMS4xNTEtMTAuNTA1LTEuMTM5LTEwLjUwNS0yLjM4IDAtNS4xNTItMS4zMzctNi41NjUtMi42NS02Ljcxem0tMjIuNTMtMS42MDljLjg0My0xLjMzMyA3LjY3NC4zMjUgMTEuNDI0IDEuOTkzIDAgMC0uNzcgMy40OTEuNDU2IDcuNjA0LjM1OSAxLjIwMy04LjYyNyA2LjU1OC05LjggNS42MzctMS4zNTUtMS4wNjUtMy44NS0xMi40MzItMi4wOC0xNS4yMzR6IiBmaWxsPSIjNGNiYTNjIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im01NS4yNjkgNjguNDA2Yy41NTMtMi40MDMgMy4xMjctNi45MzIgMTIuMzIxLTYuODIyIDQuNjQ4LS4wMTkgMTAuNDIyLS4wMDIgMTQuMjUtLjQzNmE1MS4zMTIgNTEuMzEyIDAgMCAwIDEyLjcyNi0zLjA5NWMzLjk4LTEuNTE5IDUuMzkyLTEuMTggNS44ODctLjI3Mi41NDQuOTk5LS4wOTcgMi43MjItMS40ODggNC4zMDktMi42NTYgMy4wMy03LjQzMSA1LjM4LTE1Ljg2NSA2LjA3Ni04LjQzMy42OTgtMTQuMDItMS41NjUtMTYuNDI1IDIuMTE4LTEuMDM4IDEuNTg5LS4yMzYgNS4zMzMgNy45MiA2LjUxMiAxMS4wMiAxLjU5IDIwLjA3Mi0xLjkxNyAyMS4xOS4yMDEgMS4xMTkgMi4xMTgtNS4zMjMgNi40MjgtMTYuMzYyIDYuNTE4cy0xNy45MzQtMy44NjUtMjAuMzc5LTUuODNjLTMuMTAyLTIuNDk1LTQuNDktNi4xMzMtMy43NzUtOS4yNzl6IiBmaWxsPSIjZmMzIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICAgIDxnIGZpbGw9IiMxNDMwN2UiIG9wYWNpdHk9Ii44Ij4KICAgICAgPHBhdGggZD0ibTY5LjMyNyA0Mi4xMjdjLjYxNi0xLjAwOCAxLjk4MS0xLjc4NiA0LjIxNi0xLjc4NiAyLjIzNCAwIDMuMjg1Ljg4OSA0LjAxMyAxLjg4LjE0OC4yMDItLjA3Ni40NC0uMzA2LjM0YTU5Ljg2OSA1OS44NjkgMCAwIDEgLS4xNjgtLjA3M2MtLjgxNy0uMzU3LTEuODItLjc5NS0zLjU0LS44Mi0xLjgzOC0uMDI2LTIuOTk3LjQzNS0zLjcyNy44MzEtLjI0Ni4xMzQtLjYzNC0uMTMzLS40ODgtLjM3MnptLTI1LjE1NyAxLjI5YzIuMTctLjkwNyAzLjg3Ni0uNzkgNS4wODEtLjUwNC4yNTQuMDYuNDMtLjIxMy4yMjctLjM3Ny0uOTM1LS43NTUtMy4wMy0xLjY5Mi01Ljc2LS42NzQtMi40MzcuOTA5LTMuNTg1IDIuNzk2LTMuNTkyIDQuMDM4LS4wMDIuMjkyLjYuMzE3Ljc1Ni4wNy40Mi0uNjcgMS4xMi0xLjY0NiAzLjI4OS0yLjU1M3oiLz4KICAgICAgPHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNzUuNDQgNTUuOTJhMy40NyAzLjQ3IDAgMCAxIC0zLjQ3NC0zLjQ2MiAzLjQ3IDMuNDcgMCAwIDEgMy40NzUtMy40NiAzLjQ3IDMuNDcgMCAwIDEgMy40NzQgMy40NiAzLjQ3IDMuNDcgMCAwIDEgLTMuNDc1IDMuNDYyem0yLjQ0Ny00LjYwOGEuODk5Ljg5OSAwIDAgMCAtMS43OTkgMGMwIC40OTQuNDA1Ljg5NS45Ljg5NS40OTkgMCAuOS0uNC45LS44OTV6bS0yNS40NjQgMy41NDJhNC4wNDIgNC4wNDIgMCAwIDEgLTQuMDQ5IDQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIC00LjA1LTQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIDQuMDUtNC4wMzcgNC4wNDUgNC4wNDUgMCAwIDEgNC4wNSA0LjAzN3ptLTEuMTkzLTEuMzM4YTEuMDUgMS4wNSAwIDAgMCAtMi4wOTcgMCAxLjA0OCAxLjA0OCAwIDAgMCAyLjA5NyAweiIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8L2c+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im02NCAxMTcuNzVjMjkuNjg1IDAgNTMuNzUtMjQuMDY1IDUzLjc1LTUzLjc1cy0yNC4wNjUtNTMuNzUtNTMuNzUtNTMuNzUtNTMuNzUgMjQuMDY1LTUzLjc1IDUzLjc1IDI0LjA2NSA1My43NSA1My43NSA1My43NXptMCA1YzMyLjQ0NyAwIDU4Ljc1LTI2LjMwMyA1OC43NS01OC43NXMtMjYuMzAzLTU4Ljc1LTU4Ljc1LTU4Ljc1LTU4Ljc1IDI2LjMwMy01OC43NSA1OC43NSAyNi4zMDMgNTguNzUgNTguNzUgNTguNzV6IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+');\n}\n\n/* Email tooltip specific */\n.tooltip__button--email {\n flex-direction: column;\n justify-content: center;\n align-items: flex-start;\n font-size: 14px;\n padding: 4px 8px;\n}\n.tooltip__button--email__primary-text {\n font-weight: bold;\n}\n.tooltip__button--email__secondary-text {\n font-size: 12px;\n}\n\n/* Email Protection signup notice */\n:not(.top-autofill) .tooltip--email-signup {\n text-align: left;\n color: #222222;\n padding: 16px 20px;\n width: 380px;\n}\n\n.tooltip--email-signup h1 {\n font-weight: 700;\n font-size: 16px;\n line-height: 1.5;\n margin: 0;\n}\n\n.tooltip--email-signup p {\n font-weight: 400;\n font-size: 14px;\n line-height: 1.4;\n}\n\n.notice-controls {\n display: flex;\n}\n\n.tooltip--email-signup .notice-controls > * {\n border-radius: 8px;\n border: 0;\n cursor: pointer;\n display: inline-block;\n font-family: inherit;\n font-style: normal;\n font-weight: bold;\n padding: 8px 12px;\n text-decoration: none;\n}\n\n.notice-controls .ghost {\n margin-left: 1rem;\n}\n\n.tooltip--email-signup a.primary {\n background: #3969EF;\n color: #fff;\n}\n\n.tooltip--email-signup a.primary:hover,\n.tooltip--email-signup a.primary:focus {\n background: #2b55ca;\n}\n\n.tooltip--email-signup a.primary:active {\n background: #1e42a4;\n}\n\n.tooltip--email-signup button.ghost {\n background: transparent;\n color: #3969EF;\n}\n\n.tooltip--email-signup button.ghost:hover,\n.tooltip--email-signup button.ghost:focus {\n background-color: rgba(0, 0, 0, 0.06);\n color: #2b55ca;\n}\n\n.tooltip--email-signup button.ghost:active {\n background-color: rgba(0, 0, 0, 0.12);\n color: #1e42a4;\n}\n\n.tooltip--email-signup button.close-tooltip {\n background-color: transparent;\n background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMiAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5NCAwLjY1NjkwN0MwLjY4MzQxOCAwLjI2NjM4MyAxLjMxNjU4IDAuMjY2MzgzIDEuNzA3MTEgMC42NTY5MDdMNiA0Ljk0OThMMTAuMjkyOSAwLjY1NjkwN0MxMC42ODM0IDAuMjY2MzgzIDExLjMxNjYgMC4yNjYzODMgMTEuNzA3MSAwLjY1NjkwN0MxMi4wOTc2IDEuMDQ3NDMgMTIuMDk3NiAxLjY4MDYgMTEuNzA3MSAyLjA3MTEyTDcuNDE0MjEgNi4zNjQwMUwxMS43MDcxIDEwLjY1NjlDMTIuMDk3NiAxMS4wNDc0IDEyLjA5NzYgMTEuNjgwNiAxMS43MDcxIDEyLjA3MTFDMTEuMzE2NiAxMi40NjE2IDEwLjY4MzQgMTIuNDYxNiAxMC4yOTI5IDEyLjA3MTFMNiA3Ljc3ODIzTDEuNzA3MTEgMTIuMDcxMUMxLjMxNjU4IDEyLjQ2MTYgMC42ODM0MTcgMTIuNDYxNiAwLjI5Mjg5MyAxMi4wNzExQy0wLjA5NzYzMTEgMTEuNjgwNiAtMC4wOTc2MzExIDExLjA0NzQgMC4yOTI4OTMgMTAuNjU2OUw0LjU4NTc5IDYuMzY0MDFMMC4yOTI4OTQgMi4wNzExMkMtMC4wOTc2MzA2IDEuNjgwNiAtMC4wOTc2MzA2IDEuMDQ3NDMgMC4yOTI4OTQgMC42NTY5MDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg0Ii8+Cjwvc3ZnPgo=);\n background-position: center center;\n background-repeat: no-repeat;\n border: 0;\n cursor: pointer;\n padding: 16px;\n position: absolute;\n right: 12px;\n top: 12px;\n}\n\n/* Import promotion prompt icon style */\n\n.tooltip__button--credentials-import::before {\n content: \"\";\n background-image: url(data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03Ljk3MjY5IDkuMDEzN0M3Ljk3MjY5IDUuMTQwMTQgMTEuMTEyOCAyIDE0Ljk4NjQgMkMxOC44NTk5IDIgMjIuMDAwMSA1LjE0MDE0IDIyLjAwMDEgOS4wMTM3QzIyLjAwMDEgOS44MDM5NyAyMS44Njk0IDEwLjU2MzcgMjEuNjI4NCAxMS4yNzI1QzIxLjQ2MDEgMTEuNzY3NiAyMC44NjU2IDExLjkxMzUgMjAuMzk3NyAxMS42OEMxOS45MjIxIDExLjQ0MjggMTkuNjc3OSAxMC44NjQ0IDE5LjgxOTUgMTAuMzUyQzE5LjkzNzIgOS45MjU5OSAyMC4wMDAxIDkuNDc3MTkgMjAuMDAwMSA5LjAxMzdDMjAuMDAwMSA2LjI0NDcxIDE3Ljc1NTQgNCAxNC45ODY0IDRDMTIuMjE3NCA0IDkuOTcyNjkgNi4yNDQ3MSA5Ljk3MjY5IDkuMDEzN0M5Ljk3MjY5IDkuNTU1NCAxMC4wNTgyIDEwLjA3NTIgMTAuMjE1OCAxMC41NjEzQzEwLjMzMTcgMTAuOTE4OCAxMC4yMzc0IDExLjMxMTEgOS45NzE2NSAxMS41NzY4TDMuOTk5OTQgMTcuNTQ4NVYyMC4wMDAyTDYuNDk5OTggMjAuMDAwMlYxOC4zMDAyQzYuNDk5OTggMTcuNzQ3OSA2Ljk0NzcgMTcuMzAwMiA3LjQ5OTk4IDE3LjMwMDJIOS4yMDAwMVYxNS42MDAxQzkuMjAwMDEgMTUuMDQ3OCA5LjY0NzczIDE0LjYwMDEgMTAuMiAxNC42MDAxSDEwLjVDMTEuMjEwNiAxNC41ODcgMTEuNDI1MiAxNS4zMzY5IDExLjMxNTYgMTUuNzMzMUMxMS4xNTY1IDE2LjMwODUgMTEgMTcuMDAxIDExIDE3LjVDMTEgMTcuNzI4NCAxMS4wMTE4IDE3Ljk1NCAxMS4wMzQ4IDE4LjE3NjNDMTAuNzM3OSAxOC44Mzg3IDEwLjA3MjkgMTkuMzAwMiA5LjMgMTkuMzAwMkg4LjQ5OTk4VjIwLjEwMDJDOC40OTk5OCAyMS4xNDk1IDcuNjQ5MzMgMjIuMDAwMiA2LjU5OTk5IDIyLjAwMDJMMi45OTk5NSAyMi4wMDAyQzIuNzM0NzMgMjIuMDAwMiAyLjQ4MDM3IDIxLjg5NDkgMi4yOTI4NCAyMS43MDczQzIuMTA1MyAyMS41MTk4IDEuOTk5OTQgMjEuMjY1NCAxLjk5OTk0IDIxLjAwMDJWMTcuMTM0M0MxLjk5OTk0IDE2Ljg2OTEgMi4xMDUzIDE2LjYxNDcgMi4yOTI4MyAxNi40MjcyTDguMTQ2ODkgMTAuNTczMUM4LjAzMjc5IDEwLjA3MDkgNy45NzI2OSA5LjU0ODgxIDcuOTcyNjkgOS4wMTM3WiIgZmlsbD0iYmxhY2siIC8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMgOC40OTk4OUMxMyA3LjExOTE4IDE0LjExOTMgNS45OTk4OSAxNS41IDUuOTk5ODlDMTYuODgwNyA1Ljk5OTg5IDE4IDcuMTE5MTggMTggOC40OTk4OUMxOCA5Ljg4MDYgMTYuODgwNyAxMC45OTk5IDE1LjUgMTAuOTk5OUMxNC4xMTkzIDEwLjk5OTkgMTMgOS44ODA2IDEzIDguNDk5ODlaTTE1LjUgNy45OTk4OUMxNS4yMjM5IDcuOTk5ODkgMTUgOC4yMjM3NSAxNSA4LjQ5OTg5QzE1IDguNzc2MDQgMTUuMjIzOSA4Ljk5OTg5IDE1LjUgOC45OTk4OUMxNS43NzYyIDguOTk5ODkgMTYgOC43NzYwNCAxNiA4LjQ5OTg5QzE2IDguMjIzNzUgMTUuNzc2MiA3Ljk5OTg5IDE1LjUgNy45OTk4OVoiIGZpbGw9ImJsYWNrIiAvPgo8cGF0aCBkPSJNMTcgMTVMMTQuNSAxNy41TDE3IDIwIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMjEuNSAxNy41TDE2IDE3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);\n background-repeat: no-repeat;\n}\n"; -},{}],64:[function(require,module,exports){ +},{}],65:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17492,7 +17626,7 @@ function findEnclosedElements(root, selector) { return shadowElements; } -},{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ +},{"./Form/matching.js":45,"./constants.js":68,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],66:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -17523,7 +17657,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":23,"./autofill-utils.js":64,"./requestIdleCallback.js":104}],66:[function(require,module,exports){ +},{"./DeviceInterface.js":24,"./autofill-utils.js":65,"./requestIdleCallback.js":105}],67:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17540,6 +17674,10 @@ const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a- * @returns {GlobalConfig} */ function createGlobalConfig(overrides) { + /** + * Defines whether it's one of our desktop apps + * @type {boolean} + */ let isApp = false; let isTopFrame = false; let supportsTopFrame = false; @@ -17609,7 +17747,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],67:[function(require,module,exports){ +},{}],68:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17626,13 +17764,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],68:[function(require,module,exports){ +},{}],69:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAliasCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -17794,9 +17932,19 @@ class StartCredentialsImportFlowCall extends _deviceApi.DeviceApiCall { method = "startCredentialsImportFlow"; } /** - * @extends {DeviceApiCall} + * @extends {DeviceApiCall} */ exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; +class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { + method = "emailProtectionGetAlias"; + id = "emailProtectionGetAliasResponse"; + paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; + resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; +} +/** + * @extends {DeviceApiCall} + */ +exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -17879,13 +18027,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[function(require,module,exports){ +},{"../../../packages/device-api":12,"./validators.zod.js":70}],70:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.userPreferencesSchema = exports.triggerContextSchema = exports.storeFormDataSchema = exports.showInContextEmailProtectionSignupPromptSchema = exports.setSizeParamsSchema = exports.setIncontextSignupPermanentlyDismissedAtSchema = exports.sendJSPixelParamsSchema = exports.selectedDetailParamsSchema = exports.runtimeConfigurationSchema = exports.providerStatusUpdatedSchema = exports.outgoingCredentialsSchema = exports.getRuntimeConfigurationResponseSchema = exports.getIncontextSignupDismissedAtSchema = exports.getAvailableInputTypesResultSchema = exports.getAutofillInitDataResponseSchema = exports.getAutofillDataResponseSchema = exports.getAutofillDataRequestSchema = exports.getAutofillCredentialsResultSchema = exports.getAutofillCredentialsParamsSchema = exports.getAliasResultSchema = exports.getAliasParamsSchema = exports.genericErrorSchema = exports.generatedPasswordSchema = exports.emailProtectionStoreUserDataParamsSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionGetUserDataResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetAddressesResultSchema = exports.credentialsSchema = exports.contentScopeSchema = exports.checkCredentialsProviderStatusResultSchema = exports.availableInputTypesSchema = exports.availableInputTypes1Schema = exports.autofillSettingsSchema = exports.autofillFeatureTogglesSchema = exports.askToUnlockProviderResultSchema = exports.apiSchema = exports.addDebugFlagParamsSchema = void 0; +exports.userPreferencesSchema = exports.triggerContextSchema = exports.storeFormDataSchema = exports.showInContextEmailProtectionSignupPromptSchema = exports.setSizeParamsSchema = exports.setIncontextSignupPermanentlyDismissedAtSchema = exports.sendJSPixelParamsSchema = exports.selectedDetailParamsSchema = exports.runtimeConfigurationSchema = exports.providerStatusUpdatedSchema = exports.outgoingCredentialsSchema = exports.getRuntimeConfigurationResponseSchema = exports.getIncontextSignupDismissedAtSchema = exports.getAvailableInputTypesResultSchema = exports.getAutofillInitDataResponseSchema = exports.getAutofillDataResponseSchema = exports.getAutofillDataRequestSchema = exports.getAutofillCredentialsResultSchema = exports.getAutofillCredentialsParamsSchema = exports.getAliasResultSchema = exports.getAliasParamsSchema = exports.genericErrorSchema = exports.generatedPasswordSchema = exports.emailProtectionStoreUserDataParamsSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionGetUserDataResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetAliasResultSchema = exports.emailProtectionGetAliasParamsSchema = exports.emailProtectionGetAddressesResultSchema = exports.credentialsSchema = exports.contentScopeSchema = exports.checkCredentialsProviderStatusResultSchema = exports.availableInputTypesSchema = exports.availableInputTypes1Schema = exports.autofillSettingsSchema = exports.autofillFeatureTogglesSchema = exports.askToUnlockProviderResultSchema = exports.apiSchema = exports.addDebugFlagParamsSchema = void 0; var _zod = require("zod"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ // Generated by ts-to-zod @@ -17945,6 +18093,11 @@ const getAliasResultSchema = exports.getAliasResultSchema = _zod.z.object({ alias: _zod.z.string().optional() }) }); +const emailProtectionGetAliasParamsSchema = exports.emailProtectionGetAliasParamsSchema = _zod.z.object({ + requiresUserPermission: _zod.z.boolean(), + shouldConsumeAliasIfProvided: _zod.z.boolean(), + isIncontextSignupAvailable: _zod.z.boolean().optional() +}); const emailProtectionStoreUserDataParamsSchema = exports.emailProtectionStoreUserDataParamsSchema = _zod.z.object({ token: _zod.z.string(), userName: _zod.z.string(), @@ -17999,10 +18152,6 @@ const userPreferencesSchema = exports.userPreferencesSchema = _zod.z.object({ settings: _zod.z.record(_zod.z.unknown()) })) }); -const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = _zod.z.object({ - username: _zod.z.string().optional(), - password: _zod.z.string().optional() -}); const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18035,6 +18184,10 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = _zod.z.object({ + username: _zod.z.string().optional(), + password: _zod.z.string().optional() +}); const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18067,6 +18220,11 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ + status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), + credentials: _zod.z.array(credentialsSchema), + availableInputTypes: availableInputTypesSchema +}); const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({ inputType_credentials: _zod.z.boolean().optional(), inputType_identities: _zod.z.boolean().optional(), @@ -18102,7 +18260,7 @@ const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ }); const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ type: _zod.z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypesSchema, + success: availableInputTypes1Schema, error: genericErrorSchema.optional() }); const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ @@ -18125,9 +18283,25 @@ const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultS }).optional(), error: genericErrorSchema.optional() }); +const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({ + type: _zod.z.literal("askToUnlockProviderResponse").optional(), + success: providerStatusUpdatedSchema, + error: genericErrorSchema.optional() +}); +const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = _zod.z.object({ + type: _zod.z.literal("checkCredentialsProviderStatusResponse").optional(), + success: providerStatusUpdatedSchema, + error: genericErrorSchema.optional() +}); const autofillSettingsSchema = exports.autofillSettingsSchema = _zod.z.object({ featureToggles: autofillFeatureTogglesSchema }); +const emailProtectionGetAliasResultSchema = exports.emailProtectionGetAliasResultSchema = _zod.z.object({ + success: _zod.z.object({ + alias: _zod.z.string() + }).optional(), + error: genericErrorSchema.optional() +}); const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = _zod.z.object({ success: _zod.z.boolean().optional(), error: genericErrorSchema.optional() @@ -18165,28 +18339,14 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ contentScope: contentScopeSchema, userUnprotectedDomains: _zod.z.array(_zod.z.string()), - userPreferences: userPreferencesSchema -}); -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ - status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), - credentials: _zod.z.array(credentialsSchema), - availableInputTypes: availableInputTypes1Schema + userPreferences: userPreferencesSchema, + availableInputTypes: availableInputTypesSchema.optional() }); const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = _zod.z.object({ type: _zod.z.literal("getRuntimeConfigurationResponse").optional(), success: runtimeConfigurationSchema.optional(), error: genericErrorSchema.optional() }); -const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = _zod.z.object({ - type: _zod.z.literal("askToUnlockProviderResponse").optional(), - success: providerStatusUpdatedSchema, - error: genericErrorSchema.optional() -}); -const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = _zod.z.object({ - type: _zod.z.literal("checkCredentialsProviderStatusResponse").optional(), - success: providerStatusUpdatedSchema, - error: genericErrorSchema.optional() -}); const apiSchema = exports.apiSchema = _zod.z.object({ addDebugFlag: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ paramsValidator: addDebugFlagParamsSchema.optional() @@ -18254,6 +18414,11 @@ const apiSchema = exports.apiSchema = _zod.z.object({ openManageCreditCards: _zod.z.record(_zod.z.unknown()).optional(), openManageIdentities: _zod.z.record(_zod.z.unknown()).optional(), startCredentialsImportFlow: _zod.z.record(_zod.z.unknown()).optional(), + emailProtectionGetAlias: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ + id: _zod.z.literal("emailProtectionGetAliasResponse").optional(), + paramsValidator: emailProtectionGetAliasParamsSchema.optional(), + resultValidator: emailProtectionGetAliasResultSchema.optional() + })).optional(), emailProtectionStoreUserData: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("emailProtectionStoreUserDataResponse").optional(), paramsValidator: emailProtectionStoreUserDataParamsSchema.optional() @@ -18287,7 +18452,7 @@ const apiSchema = exports.apiSchema = _zod.z.object({ })).optional() }); -},{"zod":9}],70:[function(require,module,exports){ +},{"zod":9}],71:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18313,7 +18478,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":69}],71:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":70}],72:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18321,7 +18486,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.AndroidTransport = void 0; var _index = require("../../../packages/device-api/index.js"); -var _deviceApiCalls = require("../__generated__/deviceApiCalls.js"); +var _messaging = require("../../../packages/messaging/messaging.js"); +var _android = require("../../../packages/messaging/android.js"); class AndroidTransport extends _index.DeviceApiTransport { /** @type {GlobalConfig} */ config; @@ -18330,133 +18496,39 @@ class AndroidTransport extends _index.DeviceApiTransport { constructor(globalConfig) { super(); this.config = globalConfig; - if (this.config.isDDGTestMode) { - if (typeof window.BrowserAutofill?.getAutofillData !== 'function') { - console.warn('window.BrowserAutofill.getAutofillData missing'); - } - if (typeof window.BrowserAutofill?.storeFormData !== 'function') { - console.warn('window.BrowserAutofill.storeFormData missing'); - } - } + const messageHandlerNames = ['EmailProtectionStoreUserData', 'EmailProtectionRemoveUserData', 'EmailProtectionGetUserData', 'EmailProtectionGetCapabilities', 'EmailProtectionGetAlias', 'SetIncontextSignupPermanentlyDismissedAt', 'StartEmailProtectionSignup', 'CloseEmailProtectionTab', 'ShowInContextEmailProtectionSignupPrompt', 'StoreFormData', 'GetIncontextSignupDismissedAt', 'GetRuntimeConfiguration', 'GetAutofillData']; + const androidMessagingConfig = new _android.AndroidMessagingConfig({ + messageHandlerNames + }); + this.messaging = new _messaging.Messaging(androidMessagingConfig); } /** * @param {import("../../../packages/device-api").DeviceApiCall} deviceApiCall * @returns {Promise} */ async send(deviceApiCall) { - if (deviceApiCall instanceof _deviceApiCalls.GetRuntimeConfigurationCall) { - return androidSpecificRuntimeConfiguration(this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetAvailableInputTypesCall) { - return androidSpecificAvailableInputTypes(this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetIncontextSignupDismissedAtCall) { - window.BrowserAutofill.getIncontextSignupDismissedAt(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.SetIncontextSignupPermanentlyDismissedAtCall) { - return window.BrowserAutofill.setIncontextSignupPermanentlyDismissedAt(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.StartEmailProtectionSignupCall) { - return window.BrowserAutofill.startEmailProtectionSignup(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.CloseEmailProtectionTabCall) { - return window.BrowserAutofill.closeEmailProtectionTab(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall) { - window.BrowserAutofill.showInContextEmailProtectionSignupPrompt(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetAutofillDataCall) { - window.BrowserAutofill.getAutofillData(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.StoreFormDataCall) { - return window.BrowserAutofill.storeFormData(JSON.stringify(deviceApiCall.params)); - } - throw new Error('android: not implemented: ' + deviceApiCall.method); - } -} - -/** - * @param {string} expectedResponse - the name/id of the response - * @param {GlobalConfig} config - * @returns {Promise<*>} - */ -exports.AndroidTransport = AndroidTransport; -function waitForResponse(expectedResponse, config) { - return new Promise(resolve => { - const handler = e => { - if (!config.isDDGTestMode) { - if (e.origin !== '') { - return; - } - } - if (!e.data) { - return; - } - if (typeof e.data !== 'string') { - if (config.isDDGTestMode) { - console.log('❌ event.data was not a string. Expected a string so that it can be JSON parsed'); - } - return; + try { + // if the call has an `id`, it means that it expects a response + if (deviceApiCall.id) { + return await this.messaging.request(deviceApiCall.method, deviceApiCall.params || undefined); + } else { + return this.messaging.notify(deviceApiCall.method, deviceApiCall.params || undefined); } - try { - let data = JSON.parse(e.data); - if (data.type === expectedResponse) { - window.removeEventListener('message', handler); - return resolve(data); - } - if (config.isDDGTestMode) { - console.log(`❌ event.data.type was '${data.type}', which didnt match '${expectedResponse}'`, JSON.stringify(data)); - } - } catch (e) { - window.removeEventListener('message', handler); - if (config.isDDGTestMode) { - console.log('❌ Could not JSON.parse the response'); + } catch (e) { + if (e instanceof _messaging.MissingHandler) { + if (this.config.isDDGTestMode) { + console.log('MissingAndroidHandler error for:', deviceApiCall.method); } + throw new Error('unimplemented handler: ' + deviceApiCall.method); + } else { + throw e; } - }; - window.addEventListener('message', handler); - }); -} - -/** - * @param {GlobalConfig} globalConfig - * @returns {{success: import('../__generated__/validators-ts').RuntimeConfiguration}} - */ -function androidSpecificRuntimeConfiguration(globalConfig) { - if (!globalConfig.userPreferences) { - throw new Error('globalConfig.userPreferences not supported yet on Android'); - } - return { - success: { - // @ts-ignore - contentScope: globalConfig.contentScope, - // @ts-ignore - userPreferences: globalConfig.userPreferences, - // @ts-ignore - userUnprotectedDomains: globalConfig.userUnprotectedDomains, - // @ts-ignore - availableInputTypes: globalConfig.availableInputTypes } - }; -} - -/** - * @param {GlobalConfig} globalConfig - * @returns {{success: import('../__generated__/validators-ts').AvailableInputTypes}} - */ -function androidSpecificAvailableInputTypes(globalConfig) { - if (!globalConfig.availableInputTypes) { - throw new Error('globalConfig.availableInputTypes not supported yet on Android'); } - return { - success: globalConfig.availableInputTypes - }; } +exports.AndroidTransport = AndroidTransport; -},{"../../../packages/device-api/index.js":12,"../__generated__/deviceApiCalls.js":68}],72:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/android.js":15,"../../../packages/messaging/messaging.js":16}],73:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18499,7 +18571,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":15}],73:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":16}],74:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18653,7 +18725,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":12,"../../Settings.js":51,"../../autofill-utils.js":64,"../__generated__/deviceApiCalls.js":68}],74:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../Settings.js":52,"../../autofill-utils.js":65,"../__generated__/deviceApiCalls.js":69}],75:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18697,7 +18769,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":71,"./apple.transport.js":72,"./extension.transport.js":73,"./windows.transport.js":75}],75:[function(require,module,exports){ +},{"./android.transport.js":72,"./apple.transport.js":73,"./extension.transport.js":74,"./windows.transport.js":76}],76:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18782,7 +18854,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":12}],76:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12}],77:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -18883,7 +18955,7 @@ module.exports={ } } -},{}],77:[function(require,module,exports){ +},{}],78:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -18984,7 +19056,7 @@ module.exports={ } } -},{}],78:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19085,7 +19157,7 @@ module.exports={ } } -},{}],79:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19186,7 +19258,7 @@ module.exports={ } } -},{}],80:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19287,7 +19359,7 @@ module.exports={ } } -},{}],81:[function(require,module,exports){ +},{}],82:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -19389,7 +19461,7 @@ module.exports={ } } -},{}],82:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19490,7 +19562,7 @@ module.exports={ } } -},{}],83:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19591,7 +19663,7 @@ module.exports={ } } -},{}],84:[function(require,module,exports){ +},{}],85:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19692,7 +19764,7 @@ module.exports={ } } -},{}],85:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19793,7 +19865,7 @@ module.exports={ } } -},{}],86:[function(require,module,exports){ +},{}],87:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19894,7 +19966,7 @@ module.exports={ } } -},{}],87:[function(require,module,exports){ +},{}],88:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19995,7 +20067,7 @@ module.exports={ } } -},{}],88:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20096,7 +20168,7 @@ module.exports={ } } -},{}],89:[function(require,module,exports){ +},{}],90:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20197,7 +20269,7 @@ module.exports={ } } -},{}],90:[function(require,module,exports){ +},{}],91:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20298,7 +20370,7 @@ module.exports={ } } -},{}],91:[function(require,module,exports){ +},{}],92:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20399,7 +20471,7 @@ module.exports={ } } -},{}],92:[function(require,module,exports){ +},{}],93:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20500,7 +20572,7 @@ module.exports={ } } -},{}],93:[function(require,module,exports){ +},{}],94:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20601,7 +20673,7 @@ module.exports={ } } -},{}],94:[function(require,module,exports){ +},{}],95:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20702,7 +20774,7 @@ module.exports={ } } -},{}],95:[function(require,module,exports){ +},{}],96:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20803,7 +20875,7 @@ module.exports={ } } -},{}],96:[function(require,module,exports){ +},{}],97:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20904,7 +20976,7 @@ module.exports={ } } -},{}],97:[function(require,module,exports){ +},{}],98:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21005,7 +21077,7 @@ module.exports={ } } -},{}],98:[function(require,module,exports){ +},{}],99:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21106,7 +21178,7 @@ module.exports={ } } -},{}],99:[function(require,module,exports){ +},{}],100:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21194,7 +21266,7 @@ function translateImpl(library, namespacedId, opts) { return out; } -},{"./translations.js":102}],100:[function(require,module,exports){ +},{"./translations.js":103}],101:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21295,7 +21367,7 @@ module.exports={ } } -},{}],101:[function(require,module,exports){ +},{}],102:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21396,7 +21468,7 @@ module.exports={ } } -},{}],102:[function(require,module,exports){ +},{}],103:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21515,7 +21587,7 @@ var _default = exports.default = { } }; -},{"./bg/autofill.json":76,"./cs/autofill.json":77,"./da/autofill.json":78,"./de/autofill.json":79,"./el/autofill.json":80,"./en/autofill.json":81,"./es/autofill.json":82,"./et/autofill.json":83,"./fi/autofill.json":84,"./fr/autofill.json":85,"./hr/autofill.json":86,"./hu/autofill.json":87,"./it/autofill.json":88,"./lt/autofill.json":89,"./lv/autofill.json":90,"./nb/autofill.json":91,"./nl/autofill.json":92,"./pl/autofill.json":93,"./pt/autofill.json":94,"./ro/autofill.json":95,"./ru/autofill.json":96,"./sk/autofill.json":97,"./sl/autofill.json":98,"./sv/autofill.json":100,"./tr/autofill.json":101,"./xa/autofill.json":103}],103:[function(require,module,exports){ +},{"./bg/autofill.json":77,"./cs/autofill.json":78,"./da/autofill.json":79,"./de/autofill.json":80,"./el/autofill.json":81,"./en/autofill.json":82,"./es/autofill.json":83,"./et/autofill.json":84,"./fi/autofill.json":85,"./fr/autofill.json":86,"./hr/autofill.json":87,"./hu/autofill.json":88,"./it/autofill.json":89,"./lt/autofill.json":90,"./lv/autofill.json":91,"./nb/autofill.json":92,"./nl/autofill.json":93,"./pl/autofill.json":94,"./pt/autofill.json":95,"./ro/autofill.json":96,"./ru/autofill.json":97,"./sk/autofill.json":98,"./sl/autofill.json":99,"./sv/autofill.json":101,"./tr/autofill.json":102,"./xa/autofill.json":104}],104:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -21608,7 +21680,7 @@ module.exports={ "note": "Button that prevents the DuckDuckGo email protection signup prompt from appearing again." } } -},{}],104:[function(require,module,exports){ +},{}],105:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21651,4 +21723,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[65]); +},{}]},{},[66]); diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js index 2552b407a2e9..e07247a53195 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill.js @@ -425,6 +425,130 @@ exports.DeviceApi = DeviceApi; },{}],5:[function(require,module,exports){ "use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AndroidMessagingTransport = exports.AndroidMessagingConfig = void 0; +var _messaging = require("./messaging.js"); +/** + * @module Android Messaging + * + * @description A wrapper for messaging on Android. See example usage in android.transport.js + */ + +/** + * @typedef {import("./messaging").MessagingTransport} MessagingTransport + */ + +/** + * On Android, handlers are added to the window object and are prefixed with `ddg`. The object looks like this: + * + * ```typescript + * { + * onMessage: undefined, + * postMessage: (message) => void, + * addEventListener: (eventType: string, Function) => void, + * removeEventListener: (eventType: string, Function) => void + * } + * ``` + * + * You send messages to `postMessage` and listen with `addEventListener`. Once the event is received, + * we also remove the listener with `removeEventListener`. + * + * @link https://developer.android.com/reference/androidx/webkit/WebViewCompat#addWebMessageListener(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E,androidx.webkit.WebViewCompat.WebMessageListener) + * @implements {MessagingTransport} + */ +class AndroidMessagingTransport { + /** @type {AndroidMessagingConfig} */ + config; + globals = { + capturedHandlers: {} + }; + /** + * @param {AndroidMessagingConfig} config + */ + constructor(config) { + this.config = config; + } + + /** + * Given the method name, returns the related Android handler + * @param {string} methodName + * @returns {AndroidHandler} + * @private + */ + _getHandler(methodName) { + const androidSpecificName = this._getHandlerName(methodName); + if (!(androidSpecificName in window)) { + throw new _messaging.MissingHandler(`Missing android handler: '${methodName}'`, methodName); + } + return window[androidSpecificName]; + } + + /** + * Given the autofill method name, it returns the Android-specific handler name + * @param {string} internalName + * @returns {string} + * @private + */ + _getHandlerName(internalName) { + return 'ddg' + internalName[0].toUpperCase() + internalName.slice(1); + } + + /** + * @param {string} name + * @param {Record} [data] + */ + notify(name) { + let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const handler = this._getHandler(name); + const message = data ? JSON.stringify(data) : ''; + handler.postMessage(message); + } + + /** + * @param {string} name + * @param {Record} [data] + */ + async request(name) { + let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + // Set up the listener first + const handler = this._getHandler(name); + const responseOnce = new Promise(resolve => { + const responseHandler = e => { + handler.removeEventListener('message', responseHandler); + resolve(e.data); + }; + handler.addEventListener('message', responseHandler); + }); + + // Then send the message + this.notify(name, data); + + // And return once the promise resolves + const responseJSON = await responseOnce; + return JSON.parse(responseJSON); + } +} + +/** + * Use this configuration to create an instance of {@link Messaging} for Android + */ +exports.AndroidMessagingTransport = AndroidMessagingTransport; +class AndroidMessagingConfig { + /** + * All the expected Android handler names + * @param {{messageHandlerNames: string[]}} config + */ + constructor(config) { + this.messageHandlerNames = config.messageHandlerNames; + } +} +exports.AndroidMessagingConfig = AndroidMessagingConfig; + +},{"./messaging.js":6}],6:[function(require,module,exports){ +"use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); @@ -436,6 +560,7 @@ Object.defineProperty(exports, "WebkitMessagingConfig", { } }); var _webkit = require("./webkit.js"); +var _android = require("./android.js"); /** * @module Messaging * @@ -494,7 +619,7 @@ var _webkit = require("./webkit.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig} config + * @param {WebkitMessagingConfig | AndroidMessagingConfig} config */ constructor(config) { this.transport = getTransport(config); @@ -566,7 +691,7 @@ class MessagingTransport { } /** - * @param {WebkitMessagingConfig} config + * @param {WebkitMessagingConfig | AndroidMessagingConfig} config * @returns {MessagingTransport} */ exports.MessagingTransport = MessagingTransport; @@ -574,6 +699,9 @@ function getTransport(config) { if (config instanceof _webkit.WebkitMessagingConfig) { return new _webkit.WebkitMessagingTransport(config); } + if (config instanceof _android.AndroidMessagingConfig) { + return new _android.AndroidMessagingTransport(config); + } throw new Error('unreachable'); } @@ -596,7 +724,7 @@ class MissingHandler extends Error { */ exports.MissingHandler = MissingHandler; -},{"./webkit.js":6}],6:[function(require,module,exports){ +},{"./android.js":5,"./webkit.js":7}],7:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -991,7 +1119,7 @@ function captureGlobals() { }; } -},{"./messaging.js":5}],7:[function(require,module,exports){ +},{"./messaging.js":6}],8:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1122,7 +1250,7 @@ function _safeHostname(inputHostname) { } } -},{"./lib/apple.password.js":8,"./lib/constants.js":9,"./lib/rules-parser.js":10}],8:[function(require,module,exports){ +},{"./lib/apple.password.js":9,"./lib/constants.js":10,"./lib/rules-parser.js":11}],9:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1651,7 +1779,7 @@ class Password { } exports.Password = Password; -},{"./constants.js":9,"./rules-parser.js":10}],9:[function(require,module,exports){ +},{"./constants.js":10,"./rules-parser.js":11}],10:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1671,7 +1799,7 @@ const constants = exports.constants = { DEFAULT_UNAMBIGUOUS_CHARS }; -},{}],10:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -2267,7 +2395,7 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return newPasswordRules; } -},{}],11:[function(require,module,exports){ +},{}],12:[function(require,module,exports){ module.exports={ "163.com": { "password-rules": "minlength: 6; maxlength: 16;" @@ -3299,7 +3427,7 @@ module.exports={ "password-rules": "minlength: 8; maxlength: 32; max-consecutive: 6; required: lower; required: upper; required: digit;" } } -},{}],12:[function(require,module,exports){ +},{}],13:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3363,7 +3491,7 @@ class CredentialsImport { } exports.CredentialsImport = CredentialsImport; -},{"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],13:[function(require,module,exports){ +},{"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],14:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3419,7 +3547,7 @@ function createDevice() { return new _ExtensionInterface.ExtensionInterface(globalConfig, deviceApi, settings); } -},{"../packages/device-api/index.js":2,"./DeviceInterface/AndroidInterface.js":14,"./DeviceInterface/AppleDeviceInterface.js":15,"./DeviceInterface/AppleOverlayDeviceInterface.js":16,"./DeviceInterface/ExtensionInterface.js":17,"./DeviceInterface/WindowsInterface.js":19,"./DeviceInterface/WindowsOverlayDeviceInterface.js":20,"./Settings.js":41,"./config.js":56,"./deviceApiCalls/transports/transports.js":64}],14:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./DeviceInterface/AndroidInterface.js":15,"./DeviceInterface/AppleDeviceInterface.js":16,"./DeviceInterface/AppleOverlayDeviceInterface.js":17,"./DeviceInterface/ExtensionInterface.js":18,"./DeviceInterface/WindowsInterface.js":20,"./DeviceInterface/WindowsOverlayDeviceInterface.js":21,"./Settings.js":42,"./config.js":57,"./deviceApiCalls/transports/transports.js":65}],15:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3439,25 +3567,35 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {Promise} */ async getAlias() { - const { - alias - } = await (0, _autofillUtils.sendAndWaitForAnswer)(async () => { - if (this.inContextSignup.isAvailable()) { - const { - isSignedIn - } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); + // If in-context signup is available, do that first + if (this.inContextSignup.isAvailable()) { + const { + isSignedIn + } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); + if (isSignedIn) { // On Android we can't get the input type data again without // refreshing the page, so instead we can mutate it now that we // know the user has Email Protection available. - if (this.globalConfig.availableInputTypes) { - this.globalConfig.availableInputTypes.email = isSignedIn; + if (this.settings.availableInputTypes) { + this.settings.setAvailableInputTypes({ + email: isSignedIn + }); } this.updateForStateChange(); this.onFinishedAutofill(); } - return window.EmailInterface.showTooltip(); - }, 'getAliasResponse'); - return alias; + } + // Then, if successful actually prompt to fill + if (this.settings.availableInputTypes.email) { + const { + alias + } = await this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetAliasCall({ + requiresUserPermission: !this.globalConfig.isApp, + shouldConsumeAliasIfProvided: !this.globalConfig.isApp, + isIncontextSignupAvailable: this.inContextSignup.isAvailable() + })); + return alias ? (0, _autofillUtils.formatDuckAddress)(alias) : undefined; + } } /** @@ -3472,14 +3610,9 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {boolean} */ isDeviceSignedIn() { - // on DDG domains, always check via `window.EmailInterface.isSignedIn()` - if (this.globalConfig.isDDGDomain) { - return window.EmailInterface.isSignedIn() === 'true'; - } - // on non-DDG domains, where `availableInputTypes.email` is present, use it - if (typeof this.globalConfig.availableInputTypes?.email === 'boolean') { - return this.globalConfig.availableInputTypes.email; + if (typeof this.settings.availableInputTypes?.email === 'boolean') { + return this.settings.availableInputTypes.email; } // ...on other domains we assume true because the script wouldn't exist otherwise @@ -3494,15 +3627,7 @@ class AndroidInterface extends _InterfacePrototype.default { * Settings page displays data of the logged in user data */ getUserData() { - let userData = null; - try { - userData = JSON.parse(window.EmailInterface.getUserData()); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } - return Promise.resolve(userData); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetUserDataCall({})); } /** @@ -3510,25 +3635,13 @@ class AndroidInterface extends _InterfacePrototype.default { * Device capabilities determine which functionality is available to the user */ getEmailProtectionCapabilities() { - let deviceCapabilities = null; - try { - deviceCapabilities = JSON.parse(window.EmailInterface.getDeviceCapabilities()); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } - return Promise.resolve(deviceCapabilities); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetCapabilitiesCall({})); } storeUserData(_ref) { let { - addUserData: { - token, - userName, - cohort - } + addUserData } = _ref; - return window.EmailInterface.storeCredentials(token, userName, cohort); + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionStoreUserDataCall(addUserData)); } /** @@ -3536,13 +3649,7 @@ class AndroidInterface extends _InterfacePrototype.default { * Provides functionality to log the user out */ removeUserData() { - try { - return window.EmailInterface.removeCredentials(); - } catch (e) { - if (this.globalConfig.isDDGTestMode) { - console.error(e); - } - } + return this.deviceApi.request(new _deviceApiCalls.EmailProtectionRemoveUserDataCall({})); } /** @@ -3567,7 +3674,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":35,"../UI/controllers/NativeUIController.js":49,"../autofill-utils.js":54,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18}],15:[function(require,module,exports){ +},{"../InContextSignup.js":36,"../UI/controllers/NativeUIController.js":50,"../autofill-utils.js":55,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3923,7 +4030,7 @@ class AppleDeviceInterface extends _InterfacePrototype.default { } exports.AppleDeviceInterface = AppleDeviceInterface; -},{"../../packages/device-api/index.js":2,"../Form/matching.js":34,"../InContextSignup.js":35,"../ThirdPartyProvider.js":42,"../UI/HTMLTooltip.js":47,"../UI/controllers/HTMLTooltipUIController.js":48,"../UI/controllers/NativeUIController.js":49,"../UI/controllers/OverlayUIController.js":50,"../autofill-utils.js":54,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"../deviceApiCalls/additionalDeviceApiCalls.js":60,"./InterfacePrototype.js":18}],16:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../Form/matching.js":35,"../InContextSignup.js":36,"../ThirdPartyProvider.js":43,"../UI/HTMLTooltip.js":48,"../UI/controllers/HTMLTooltipUIController.js":49,"../UI/controllers/NativeUIController.js":50,"../UI/controllers/OverlayUIController.js":51,"../autofill-utils.js":55,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"../deviceApiCalls/additionalDeviceApiCalls.js":61,"./InterfacePrototype.js":19}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4042,7 +4149,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":48,"./AppleDeviceInterface.js":15,"./overlayApi.js":22}],17:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":49,"./AppleDeviceInterface.js":16,"./overlayApi.js":23}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4261,7 +4368,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":34,"../InContextSignup.js":35,"../UI/HTMLTooltip.js":47,"../UI/controllers/HTMLTooltipUIController.js":48,"../autofill-utils.js":54,"./InterfacePrototype.js":18}],18:[function(require,module,exports){ +},{"../Form/matching.js":35,"../InContextSignup.js":36,"../UI/HTMLTooltip.js":48,"../UI/controllers/HTMLTooltipUIController.js":49,"../autofill-utils.js":55,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4530,8 +4637,7 @@ class InterfacePrototype { } async startInit() { if (this.isInitializationStarted) return; - this.alreadyInitialized = true; - await this.settings.refresh(); + this.isInitializationStarted = true; this.addDeviceListeners(); await this.setupAutofill(); this.uiController = this.createUIController(); @@ -4865,11 +4971,19 @@ class InterfacePrototype { let userData; try { userData = await this.getUserData(); - } catch (e) {} + } catch (e) { + if (this.isTestMode()) { + console.log('getUserData failed with', e); + } + } let capabilities; try { capabilities = await this.getEmailProtectionCapabilities(); - } catch (e) {} + } catch (e) { + if (this.isTestMode()) { + console.log('capabilities fetching failed with', e); + } + } // Set up listener for web app actions if (this.globalConfig.isDDGDomain) { @@ -4925,6 +5039,13 @@ class InterfacePrototype { const data = await (0, _autofillUtils.sendAndWaitForAnswer)(_autofillUtils.SIGN_IN_MSG, 'addUserData'); // This call doesn't send a response, so we can't know if it succeeded this.storeUserData(data); + + // Assuming the previous call succeeded, let's update availableInputTypes + if (this.settings.availableInputTypes) { + this.settings.setAvailableInputTypes({ + email: true + }); + } await this.setupAutofill(); await this.settings.refresh(); await this.setupSettingsPage({ @@ -5093,7 +5214,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":2,"../CredentialsImport.js":12,"../EmailProtection.js":23,"../Form/formatters.js":27,"../Form/matching.js":34,"../InputTypes/Credentials.js":36,"../PasswordGenerator.js":39,"../Scanner.js":40,"../Settings.js":41,"../UI/controllers/NativeUIController.js":49,"../autofill-utils.js":54,"../config.js":56,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"../deviceApiCalls/transports/transports.js":64,"../locales/strings.js":89,"./initFormSubmissionsApi.js":21}],19:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../CredentialsImport.js":13,"../EmailProtection.js":24,"../Form/formatters.js":28,"../Form/matching.js":35,"../InputTypes/Credentials.js":37,"../PasswordGenerator.js":40,"../Scanner.js":41,"../Settings.js":42,"../UI/controllers/NativeUIController.js":50,"../autofill-utils.js":55,"../config.js":57,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"../deviceApiCalls/transports/transports.js":65,"../locales/strings.js":90,"./initFormSubmissionsApi.js":22}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5178,13 +5299,13 @@ class WindowsInterface extends _InterfacePrototype.default { return await this.credentialsImport.refresh(); } default: - if (this.globalConfig.isDDGTestMode) { + if (this.isTestMode()) { console.warn('unhandled response', resp); } return this._closeAutofillParent(); } } catch (e) { - if (this.globalConfig.isDDGTestMode) { + if (this.isTestMode()) { if (e instanceof DOMException && e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -5259,7 +5380,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":50,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18}],20:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":51,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19}],21:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5438,7 +5559,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":48,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18,"./overlayApi.js":22}],21:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":49,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19,"./overlayApi.js":23}],22:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5537,7 +5658,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":30,"../autofill-utils.js":54}],22:[function(require,module,exports){ +},{"../Form/label-util.js":31,"../autofill-utils.js":55}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5595,7 +5716,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":58}],23:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":59}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5630,7 +5751,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],24:[function(require,module,exports){ +},{}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5770,7 +5891,7 @@ class Form { } submitHandler() { let via = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown'; - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('Form.submitHandler via:', via, this); } if (this.submitHandlerExecuted) return; @@ -6537,7 +6658,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":54,"../constants.js":57,"./FormAnalyzer.js":25,"./formatters.js":27,"./inputStyles.js":28,"./inputTypeConfig.js":29,"./matching.js":34}],25:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":37,"../autofill-utils.js":55,"../constants.js":58,"./FormAnalyzer.js":26,"./formatters.js":28,"./inputStyles.js":29,"./inputTypeConfig.js":30,"./matching.js":35}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -6907,7 +7028,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":54,"../constants.js":57,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching.js":34}],26:[function(require,module,exports){ +},{"../autofill-utils.js":55,"../constants.js":58,"./matching-config/__generated__/compiled-matching-config.js":33,"./matching.js":35}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7472,7 +7593,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],27:[function(require,module,exports){ +},{}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7777,7 +7898,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"./countryNames.js":27,"./matching.js":35}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7868,7 +7989,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":29}],29:[function(require,module,exports){ +},{"./inputTypeConfig.js":30}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8122,7 +8243,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":36,"../InputTypes/CreditCard.js":37,"../InputTypes/Identity.js":38,"../UI/img/ddgPasswordIcon.js":52,"../constants.js":57,"./logo-svg.js":31,"./matching.js":34}],30:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":37,"../InputTypes/CreditCard.js":38,"../InputTypes/Identity.js":39,"../UI/img/ddgPasswordIcon.js":53,"../constants.js":58,"./logo-svg.js":32,"./matching.js":35}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8170,7 +8291,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":34}],31:[function(require,module,exports){ +},{"./matching.js":35}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8203,7 +8324,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],32:[function(require,module,exports){ +},{}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8656,7 +8777,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],33:[function(require,module,exports){ +},{}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8731,7 +8852,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":54,"./matching.js":34}],34:[function(require,module,exports){ +},{"../autofill-utils.js":55,"./matching.js":35}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9723,7 +9844,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":54,"../constants.js":57,"./label-util.js":30,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching-utils.js":33}],35:[function(require,module,exports){ +},{"../autofill-utils.js":55,"../constants.js":58,"./label-util.js":31,"./matching-config/__generated__/compiled-matching-config.js":33,"./matching-utils.js":34}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9855,7 +9976,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":54,"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],36:[function(require,module,exports){ +},{"./autofill-utils.js":55,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10011,7 +10132,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":54}],37:[function(require,module,exports){ +},{"../autofill-utils.js":55}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10036,7 +10157,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],38:[function(require,module,exports){ +},{}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10082,7 +10203,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":27}],39:[function(require,module,exports){ +},{"../Form/formatters.js":28}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10124,7 +10245,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":7,"../packages/password/rules.json":11}],40:[function(require,module,exports){ +},{"../packages/password/index.js":8,"../packages/password/rules.json":12}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10585,7 +10706,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":24,"./Form/matching.js":34,"./autofill-utils.js":54,"./constants.js":57,"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],41:[function(require,module,exports){ +},{"./Form/Form.js":25,"./Form/matching.js":35,"./autofill-utils.js":55,"./constants.js":58,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10731,6 +10852,11 @@ class Settings { if (this._runtimeConfiguration) return this._runtimeConfiguration; const runtimeConfig = await this.deviceApi.request(new _deviceApiCalls.GetRuntimeConfigurationCall(null)); this._runtimeConfiguration = runtimeConfig; + + // If the platform sends availableInputTypes here, store them + if (runtimeConfig.availableInputTypes) { + this.setAvailableInputTypes(runtimeConfig.availableInputTypes); + } return this._runtimeConfiguration; } @@ -10746,6 +10872,9 @@ class Settings { if (this.globalConfig.isTopFrame) { return Settings.defaults.availableInputTypes; } + if (this._availableInputTypes) { + return this.availableInputTypes; + } return await this.deviceApi.request(new _deviceApiCalls.GetAvailableInputTypesCall(null)); } catch (e) { if (this.globalConfig.isDDGTestMode) { @@ -10998,7 +11127,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":2,"./autofill-utils.js":54,"./deviceApiCalls/__generated__/deviceApiCalls.js":58,"./deviceApiCalls/__generated__/validators.zod.js":59}],42:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./autofill-utils.js":55,"./deviceApiCalls/__generated__/deviceApiCalls.js":59,"./deviceApiCalls/__generated__/validators.zod.js":60}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11065,7 +11194,7 @@ class ThirdPartyProvider { this.device.scanner.forms.forEach(form => form.recategorizeAllInputs()); } } catch (e) { - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('isDDGTestMode: providerStatusUpdated error: ❌', e); } } @@ -11080,7 +11209,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.globalConfig.isDDGTestMode) { + if (this.device.isTestMode()) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -11088,7 +11217,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":2,"./Form/matching.js":34,"./deviceApiCalls/__generated__/deviceApiCalls.js":58,"./deviceApiCalls/__generated__/validators.zod.js":59}],43:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./Form/matching.js":35,"./deviceApiCalls/__generated__/deviceApiCalls.js":59,"./deviceApiCalls/__generated__/validators.zod.js":60}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11129,7 +11258,7 @@ ${this.options.css} } var _default = exports.default = CredentialsImportTooltip; -},{"./HTMLTooltip.js":47}],44:[function(require,module,exports){ +},{"./HTMLTooltip.js":48}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11280,7 +11409,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":54,"./HTMLTooltip.js":47}],45:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":37,"../autofill-utils.js":55,"./HTMLTooltip.js":48}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11362,7 +11491,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":54,"./HTMLTooltip.js":47}],46:[function(require,module,exports){ +},{"../autofill-utils.js":55,"./HTMLTooltip.js":48}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11415,7 +11544,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":47}],47:[function(require,module,exports){ +},{"./HTMLTooltip.js":48}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11803,7 +11932,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":34,"../autofill-utils.js":54,"./styles/styles.js":53}],48:[function(require,module,exports){ +},{"../Form/matching.js":35,"../autofill-utils.js":55,"./styles/styles.js":54}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12171,7 +12300,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":29,"../../Form/matching.js":34,"../../autofill-utils.js":54,"../CredentialsImportTooltip.js":43,"../DataHTMLTooltip.js":44,"../EmailHTMLTooltip.js":45,"../EmailSignupHTMLTooltip.js":46,"../HTMLTooltip.js":47,"./UIController.js":51}],49:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":30,"../../Form/matching.js":35,"../../autofill-utils.js":55,"../CredentialsImportTooltip.js":44,"../DataHTMLTooltip.js":45,"../EmailHTMLTooltip.js":46,"../EmailSignupHTMLTooltip.js":47,"../HTMLTooltip.js":48,"./UIController.js":52}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12273,6 +12402,11 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } + case 'none': + { + // do nothing + break; + } default: { if (args.device.isTestMode()) { @@ -12333,7 +12467,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":34,"../../InputTypes/Credentials.js":36,"../../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./UIController.js":51}],50:[function(require,module,exports){ +},{"../../Form/matching.js":35,"../../InputTypes/Credentials.js":37,"../../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./UIController.js":52}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12570,7 +12704,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":34,"./UIController.js":51}],51:[function(require,module,exports){ +},{"../../Form/matching.js":35,"./UIController.js":52}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12654,7 +12788,7 @@ class UIController { } exports.UIController = UIController; -},{}],52:[function(require,module,exports){ +},{}],53:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12671,7 +12805,7 @@ const ddgCcIconBase = exports.ddgCcIconBase = 'data:image/svg+xml;base64,PD94bWw const ddgCcIconFilled = exports.ddgCcIconFilled = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzc2NDMxMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjNzY0MzEwIi8+Cjwvc3ZnPgo='; const ddgIdentityIconBase = exports.ddgIdentityIconBase = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4KPHBhdGggeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSJub25lIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMiAyMWMyLjE0MyAwIDQuMTExLS43NSA1LjY1Ny0yLS42MjYtLjUwNi0xLjMxOC0uOTI3LTIuMDYtMS4yNS0xLjEtLjQ4LTIuMjg1LS43MzUtMy40ODYtLjc1LTEuMi0uMDE0LTIuMzkyLjIxMS0zLjUwNC42NjQtLjgxNy4zMzMtMS41OC43ODMtMi4yNjQgMS4zMzYgMS41NDYgMS4yNSAzLjUxNCAyIDUuNjU3IDJ6bTQuMzk3LTUuMDgzYy45NjcuNDIyIDEuODY2Ljk4IDIuNjcyIDEuNjU1QzIwLjI3OSAxNi4wMzkgMjEgMTQuMTA0IDIxIDEyYzAtNC45Ny00LjAzLTktOS05cy05IDQuMDMtOSA5YzAgMi4xMDQuNzIyIDQuMDQgMS45MzIgNS41NzIuODc0LS43MzQgMS44Ni0xLjMyOCAyLjkyMS0xLjc2IDEuMzYtLjU1NCAyLjgxNi0uODMgNC4yODMtLjgxMSAxLjQ2Ny4wMTggMi45MTYuMzMgNC4yNi45MTZ6TTEyIDIzYzYuMDc1IDAgMTEtNC45MjUgMTEtMTFTMTguMDc1IDEgMTIgMSAxIDUuOTI1IDEgMTJzNC45MjUgMTEgMTEgMTF6bTMtMTNjMCAxLjY1Ny0xLjM0MyAzLTMgM3MtMy0xLjM0My0zLTMgMS4zNDMtMyAzLTMgMyAxLjM0MyAzIDN6bTIgMGMwIDIuNzYxLTIuMjM5IDUtNSA1cy01LTIuMjM5LTUtNSAyLjIzOS01IDUtNSA1IDIuMjM5IDUgNXoiIGZpbGw9IiMwMDAiLz4KPC9zdmc+Cg==`; -},{}],53:[function(require,module,exports){ +},{}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12680,7 +12814,7 @@ Object.defineProperty(exports, "__esModule", { exports.CSS_STYLES = void 0; const CSS_STYLES = exports.CSS_STYLES = ":root {\n color-scheme: light dark;\n}\n\n.wrapper *, .wrapper *::before, .wrapper *::after {\n box-sizing: border-box;\n}\n.wrapper {\n position: fixed;\n top: 0;\n left: 0;\n padding: 0;\n font-family: 'DDG_ProximaNova', 'Proxima Nova', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n -webkit-font-smoothing: antialiased;\n z-index: 2147483647;\n}\n.wrapper--data {\n font-family: 'SF Pro Text', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n}\n.wrapper:not(.top-autofill) .tooltip {\n position: absolute;\n width: 300px;\n max-width: calc(100vw - 25px);\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--data, #topAutofill {\n background-color: rgba(242, 240, 240, 1);\n -webkit-backdrop-filter: blur(40px);\n backdrop-filter: blur(40px);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data, #topAutofill {\n background: rgb(100, 98, 102, .9);\n }\n}\n.tooltip--data {\n padding: 6px;\n font-size: 13px;\n line-height: 14px;\n width: 315px;\n max-height: 290px;\n overflow-y: auto;\n}\n.top-autofill .tooltip--data {\n min-height: 100vh;\n}\n.tooltip--data.tooltip--incontext-signup {\n width: 360px;\n}\n.wrapper:not(.top-autofill) .tooltip--data {\n top: 100%;\n left: 100%;\n border: 0.5px solid rgba(255, 255, 255, 0.2);\n border-radius: 6px;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.32);\n}\n@media (prefers-color-scheme: dark) {\n .wrapper:not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n.wrapper:not(.top-autofill) .tooltip--email {\n top: calc(100% + 6px);\n right: calc(100% - 48px);\n padding: 8px;\n border: 1px solid #D0D0D0;\n border-radius: 10px;\n background-color: #FFFFFF;\n font-size: 14px;\n line-height: 1.3;\n color: #333333;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);\n}\n.tooltip--email__caret {\n position: absolute;\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--email__caret::before,\n.tooltip--email__caret::after {\n content: \"\";\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n display: block;\n border-bottom: 8px solid #D0D0D0;\n position: absolute;\n right: -28px;\n}\n.tooltip--email__caret::before {\n border-bottom-color: #D0D0D0;\n top: -1px;\n}\n.tooltip--email__caret::after {\n border-bottom-color: #FFFFFF;\n top: 0px;\n}\n\n/* Buttons */\n.tooltip__button {\n display: flex;\n width: 100%;\n padding: 8px 8px 8px 0px;\n font-family: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: 6px;\n text-align: left;\n}\n.tooltip__button.currentFocus,\n.wrapper:not(.top-autofill) .tooltip__button:hover {\n background-color: #3969EF;\n color: #FFFFFF;\n}\n\n/* Data autofill tooltip specific */\n.tooltip__button--data {\n position: relative;\n min-height: 48px;\n flex-direction: row;\n justify-content: flex-start;\n font-size: inherit;\n font-weight: 500;\n line-height: 16px;\n text-align: left;\n border-radius: 3px;\n}\n.tooltip--data__item-container {\n max-height: 220px;\n overflow: auto;\n}\n.tooltip__button--data:first-child {\n margin-top: 0;\n}\n.tooltip__button--data:last-child {\n margin-bottom: 0;\n}\n.tooltip__button--data::before {\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 20px 20px;\n background-repeat: no-repeat;\n background-position: center center;\n}\n#provider_locked::after {\n position: absolute;\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 11px 13px;\n background-repeat: no-repeat;\n background-position: right bottom;\n}\n.tooltip__button--data.currentFocus:not(.tooltip__button--data--bitwarden)::before,\n.wrapper:not(.top-autofill) .tooltip__button--data:not(.tooltip__button--data--bitwarden):hover::before {\n filter: invert(100%);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before,\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before {\n filter: invert(100%);\n opacity: .9;\n }\n}\n.tooltip__button__text-container {\n margin: auto 0;\n}\n.label {\n display: block;\n font-weight: 400;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.8);\n font-size: 13px;\n line-height: 1;\n}\n.label + .label {\n margin-top: 2px;\n}\n.label.label--medium {\n font-weight: 500;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.9);\n}\n.label.label--small {\n font-size: 11px;\n font-weight: 400;\n letter-spacing: 0.06px;\n color: rgba(0,0,0,0.6);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data .label {\n color: #ffffff;\n }\n .tooltip--data .label--medium {\n color: #ffffff;\n }\n .tooltip--data .label--small {\n color: #cdcdcd;\n }\n}\n.tooltip__button.currentFocus .label,\n.wrapper:not(.top-autofill) .tooltip__button:hover .label {\n color: #FFFFFF;\n}\n\n.tooltip__button--manage {\n font-size: 13px;\n padding: 5px 9px;\n border-radius: 3px;\n margin: 0;\n}\n\n/* Icons */\n.tooltip__button--data--credentials::before,\n.tooltip__button--data--credentials__current::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNS4zMzQgNi42NjdhMiAyIDAgMSAwIDAgNCAyIDIgMCAwIDAgMC00Wm0tLjY2NyAyYS42NjcuNjY3IDAgMSAxIDEuMzMzIDAgLjY2Ny42NjcgMCAwIDEtMS4zMzMgMFoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNjY3IDRhNS4zMzMgNS4zMzMgMCAwIDAtNS4xODggNi41NzhsLTUuMjg0IDUuMjg0YS42NjcuNjY3IDAgMCAwLS4xOTUuNDcxdjNjMCAuMzY5LjI5OC42NjcuNjY3LjY2N2gyLjY2NmMuNzM3IDAgMS4zMzQtLjU5NyAxLjMzNC0xLjMzM1YxOGguNjY2Yy43MzcgMCAxLjMzNC0uNTk3IDEuMzM0LTEuMzMzdi0xLjMzNEgxMmMuMTc3IDAgLjM0Ni0uMDcuNDcxLS4xOTVsLjY4OC0uNjg4QTUuMzMzIDUuMzMzIDAgMSAwIDE0LjY2NyA0Wm0tNCA1LjMzM2E0IDQgMCAxIDEgMi41NTUgMy43MzIuNjY3LjY2NyAwIDAgMC0uNzEzLjE1bC0uNzg1Ljc4NUgxMGEuNjY3LjY2NyAwIDAgMC0uNjY3LjY2N3YySDhhLjY2Ny42NjcgMCAwIDAtLjY2Ny42NjZ2MS4zMzRoLTJ2LTIuMDU4bDUuMzY1LTUuMzY0YS42NjcuNjY3IDAgMCAwIC4xNjMtLjY3NyAzLjk5NiAzLjk5NiAwIDAgMS0uMTk0LTEuMjM1WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--credentials__new::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNOC4wNDcgNC42MjVDNy45MzcgNC4xMjUgNy44NjIgNCA3LjUgNGMtLjM2MiAwLS40MzguMTI1LS41NDcuNjI1LS4wNjguMzEtLjE3NyAxLjMzOC0uMjUxIDIuMDc3LS43MzguMDc0LTEuNzY3LjE4My0yLjA3Ny4yNTEtLjUuMTEtLjYyNS4xODQtLjYyNS41NDcgMCAuMzYyLjEyNS40MzcuNjI1LjU0Ny4zMS4wNjcgMS4zMzYuMTc3IDIuMDc0LjI1LjA3My43NjcuMTg1IDEuODQyLjI1NCAyLjA3OC4xMS4zNzUuMTg1LjYyNS41NDcuNjI1LjM2MiAwIC40MzgtLjEyNS41NDctLjYyNS4wNjgtLjMxLjE3Ny0xLjMzNi4yNS0yLjA3NC43NjctLjA3MyAxLjg0Mi0uMTg1IDIuMDc4LS4yNTQuMzc1LS4xMS42MjUtLjE4NS42MjUtLjU0NyAwLS4zNjMtLjEyNS0uNDM4LS42MjUtLjU0Ny0uMzEtLjA2OC0xLjMzOS0uMTc3LTIuMDc3LS4yNTEtLjA3NC0uNzM5LS4xODMtMS43NjctLjI1MS0yLjA3N1oiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0xNC42ODEgNS4xOTljLS43NjYgMC0xLjQ4Mi4yMS0yLjA5My41NzhhLjYzNi42MzYgMCAwIDEtLjY1NS0xLjA5IDUuMzQgNS4zNCAwIDEgMSAxLjMwMiA5LjcyMmwtLjc3NS43NzZhLjYzNi42MzYgMCAwIDEtLjQ1LjE4NmgtMS4zOTh2MS42NWMwIC40OTMtLjQuODkzLS44OTMuODkzSDguNTc4djEuMTQxYzAgLjQ5NC0uNC44OTMtLjg5NC44OTNINC42MzZBLjYzNi42MzYgMCAwIDEgNCAxOS4zMTNWMTYuMjZjMC0uMTY5LjA2Ny0uMzMuMTg2LS40NWw1LjU2Mi01LjU2MmEuNjM2LjYzNiAwIDEgMSAuOS45bC01LjM3NiA1LjM3NXYyLjE1M2gyLjAzNHYtMS4zOTljMC0uMzUuMjg1LS42MzYuNjM2LS42MzZIOS4zNHYtMS45MDdjMC0uMzUxLjI4NC0uNjM2LjYzNS0uNjM2aDEuNzcxbC44NjQtLjg2M2EuNjM2LjYzNiAwIDAgMSAuNjY4LS4xNDcgNC4wNjkgNC4wNjkgMCAxIDAgMS40MDItNy44OVoiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTEzLjYyNSA4LjQ5OWExLjg3NSAxLjg3NSAwIDEgMSAzLjc1IDAgMS44NzUgMS44NzUgMCAwIDEtMy43NSAwWm0xLjg3NS0uNjI1YS42MjUuNjI1IDAgMSAwIDAgMS4yNS42MjUuNjI1IDAgMCAwIDAtMS4yNVoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNjI1IDEyLjEyNWEuNjI1LjYyNSAwIDEgMCAwLTEuMjUuNjI1LjYyNSAwIDAgMCAwIDEuMjVaIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--creditCards::before {\n background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzAwMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjMDAwIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--identities::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4=');\n}\n.tooltip__button--data--credentials.tooltip__button--data--bitwarden::before,\n.tooltip__button--data--credentials__current.tooltip__button--data--bitwarden::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iOCIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE4LjU2OTYgNS40MzM1NUMxOC41MDg0IDUuMzc0NDIgMTguNDM0NyA1LjMyNzYzIDE4LjM1MzEgNS4yOTYxMUMxOC4yNzE1IDUuMjY0NiAxOC4xODM3IDUuMjQ5MDQgMTguMDk1MyA1LjI1MDQxSDUuOTIxOTFDNS44MzMyNiA1LjI0NzI5IDUuNzQ0OTMgNS4yNjIwNSA1LjY2MzA0IDUuMjkzNjdDNS41ODExNSA1LjMyNTI5IDUuNTA3NjUgNS4zNzMwMiA1LjQ0NzYyIDUuNDMzNTVDNS4zMjE3IDUuNTUwMTMgNS4yNTA2NSA1LjcwODE1IDUuMjUgNS44NzMxVjEzLjM4MjFDNS4yNTMzNiAxMy45NTM1IDUuMzc0MDggMTQuNTE5MSA1LjYwNTcyIDE1LjA0ODdDNS44MTkzMSAxNS41NzI4IDYuMTEyMDcgMTYuMDY2MSA2LjQ3NTI0IDE2LjUxMzlDNi44NDIgMTYuOTY4MyA3LjI1OTI5IDE3LjM4NTcgNy43MjAyNSAxNy43NTkzQzguMTQwNTMgMTguMTI1NiA4LjU4OTcxIDE4LjQ2MjMgOS4wNjQwNyAxOC43NjY2QzkuNDU5MzEgMTkuMDIzIDkuOTEzODMgMTkuMjc5NCAxMC4zNDg2IDE5LjUxNzVDMTAuNzgzNCAxOS43NTU2IDExLjA5OTYgMTkuOTIwNCAxMS4yNzc0IDE5Ljk5MzdDMTEuNDU1MyAyMC4wNjY5IDExLjYxMzQgMjAuMTQwMiAxMS43MTIyIDIwLjE5NTFDMTEuNzk5MiAyMC4yMzEzIDExLjg5MzUgMjAuMjUgMTEuOTg4OCAyMC4yNUMxMi4wODQyIDIwLjI1IDEyLjE3ODUgMjAuMjMxMyAxMi4yNjU1IDIwLjE5NTFDMTIuNDIxMiAyMC4xMzYzIDEyLjU3MjkgMjAuMDY5IDEyLjcyIDE5Ljk5MzdDMTIuNzcxMSAxOS45Njc0IDEyLjgzMzUgMTkuOTM2NiAxMi45MDY5IDE5LjkwMDRDMTMuMDg5MSAxOS44MTA1IDEzLjMzODggMTkuNjg3MiAxMy42NDg5IDE5LjUxNzVDMTQuMDgzNiAxOS4yNzk0IDE0LjUxODQgMTkuMDIzIDE0LjkzMzQgMTguNzY2NkMxNS40MDQgMTguNDU3NyAxNS44NTI4IDE4LjEyMTIgMTYuMjc3MiAxNy43NTkzQzE2LjczMzEgMTcuMzgwOSAxNy4xNDk5IDE2Ljk2NCAxNy41MjIyIDE2LjUxMzlDMTcuODc4IDE2LjA2MTcgMTguMTcwMiAxNS41NjkzIDE4LjM5MTcgMTUuMDQ4N0MxOC42MjM0IDE0LjUxOTEgMTguNzQ0MSAxMy45NTM1IDE4Ljc0NzQgMTMuMzgyMVY1Ljg3MzFDMTguNzU1NyA1Ljc5MjE0IDE4Ljc0MzkgNS43MTA1IDE4LjcxMzEgNS42MzQzNUMxOC42ODIzIDUuNTU4MiAxOC42MzMyIDUuNDg5NTQgMTguNTY5NiA1LjQzMzU1Wk0xNy4wMDg0IDEzLjQ1NTNDMTcuMDA4NCAxNi4xODQyIDEyLjAwODYgMTguNTI4NSAxMi4wMDg2IDE4LjUyODVWNi44NjIwOUgxNy4wMDg0VjEzLjQ1NTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');\n}\n#provider_locked:after {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMSAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgNy42MDA1N1Y3LjYwMjVWOS41MjI1QzEgMTAuMDgwMSAxLjIyMTUxIDEwLjYxNDkgMS42MTU4MSAxMS4wMDkyQzIuMDEwMSAxMS40MDM1IDIuNTQ0ODggMTEuNjI1IDMuMTAyNSAxMS42MjVINy4yNzI1QzcuNTQ4NjEgMTEuNjI1IDcuODIyMDEgMTEuNTcwNiA4LjA3NzA5IDExLjQ2NUM4LjMzMjE4IDExLjM1OTMgOC41NjM5NiAxMS4yMDQ0IDguNzU5MTkgMTEuMDA5MkM4Ljk1NDQzIDEwLjgxNCA5LjEwOTMgMTAuNTgyMiA5LjIxNDk2IDEwLjMyNzFDOS4zMjA2MiAxMC4wNzIgOS4zNzUgOS43OTg2MSA5LjM3NSA5LjUyMjVMOS4zNzUgNy42MDI1TDkuMzc1IDcuNjAwNTdDOS4zNzQxNSA3LjE2MTMxIDkuMjM1NzQgNi43MzMzNSA4Ljk3OTIyIDYuMzc2NzhDOC44NzY4MyA2LjIzNDQ2IDguNzU3NjggNi4xMDYzNyA4LjYyNSA1Ljk5NDg5VjUuMTg3NUM4LjYyNSA0LjI3NTgyIDguMjYyODQgMy40MDE0OCA3LjYxODE4IDIuNzU2ODJDNi45NzM1MiAyLjExMjE2IDYuMDk5MTggMS43NSA1LjE4NzUgMS43NUM0LjI3NTgyIDEuNzUgMy40MDE0OCAyLjExMjE2IDIuNzU2ODIgMi43NTY4MkMyLjExMjE2IDMuNDAxNDggMS43NSA0LjI3NTgyIDEuNzUgNS4xODc1VjUuOTk0ODlDMS42MTczMiA2LjEwNjM3IDEuNDk4MTcgNi4yMzQ0NiAxLjM5NTc4IDYuMzc2NzhDMS4xMzkyNiA2LjczMzM1IDEuMDAwODUgNy4xNjEzMSAxIDcuNjAwNTdaTTQuOTY4NyA0Ljk2ODdDNS4wMjY5NCA0LjkxMDQ3IDUuMTA1MzIgNC44NzY5OSA1LjE4NzUgNC44NzUwN0M1LjI2OTY4IDQuODc2OTkgNS4zNDgwNiA0LjkxMDQ3IDUuNDA2MyA0Ljk2ODdDNS40NjU0MiA1LjAyNzgzIDUuNDk5MDQgNS4xMDc3NCA1LjUgNS4xOTEzVjUuNUg0Ljg3NVY1LjE5MTNDNC44NzU5NiA1LjEwNzc0IDQuOTA5NTggNS4wMjc4MyA0Ljk2ODcgNC45Njg3WiIgZmlsbD0iIzIyMjIyMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');\n}\n\nhr {\n display: block;\n margin: 5px 9px;\n border: none; /* reset the border */\n border-top: 1px solid rgba(0,0,0,.1);\n}\n\nhr:first-child {\n display: none;\n}\n\n@media (prefers-color-scheme: dark) {\n hr {\n border-top: 1px solid rgba(255,255,255,.2);\n }\n}\n\n#privateAddress {\n align-items: flex-start;\n}\n#personalAddress::before,\n#privateAddress::before,\n#incontextSignup::before,\n#personalAddress.currentFocus::before,\n#personalAddress:hover::before,\n#privateAddress.currentFocus::before,\n#privateAddress:hover::before {\n filter: none;\n /* This is the same icon as `daxBase64` in `src/Form/logo-svg.js` */\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTY0IDEyOGMzNS4zNDYgMCA2NC0yOC42NTQgNjQtNjRzLTI4LjY1NC02NC02NC02NC02NCAyOC42NTQtNjQgNjQgMjguNjU0IDY0IDY0IDY0eiIgZmlsbD0iI2RlNTgzMyIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im03MyAxMTEuNzVjMC0uNS4xMjMtLjYxNC0xLjQ2Ni0zLjc4Mi00LjIyNC04LjQ1OS04LjQ3LTIwLjM4NC02LjU0LTI4LjA3NS4zNTMtMS4zOTctMy45NzgtNTEuNzQ0LTcuMDQtNTMuMzY1LTMuNDAyLTEuODEzLTcuNTg4LTQuNjktMTEuNDE4LTUuMzMtMS45NDMtLjMxLTQuNDktLjE2NC02LjQ4Mi4xMDUtLjM1My4wNDctLjM2OC42ODMtLjAzLjc5OCAxLjMwOC40NDMgMi44OTUgMS4yMTIgMy44MyAyLjM3NS4xNzguMjItLjA2LjU2Ni0uMzQyLjU3Ny0uODgyLjAzMi0yLjQ4Mi40MDItNC41OTMgMi4xOTUtLjI0NC4yMDctLjA0MS41OTIuMjczLjUzIDQuNTM2LS44OTcgOS4xNy0uNDU1IDExLjkgMi4wMjcuMTc3LjE2LjA4NC40NS0uMTQ3LjUxMi0yMy42OTQgNi40NC0xOS4wMDMgMjcuMDUtMTIuNjk2IDUyLjM0NCA1LjYxOSAyMi41MyA3LjczMyAyOS43OTIgOC40IDMyLjAwNGEuNzE4LjcxOCAwIDAgMCAuNDIzLjQ2N2M4LjE1NiAzLjI0OCAyNS45MjggMy4zOTIgMjUuOTI4LTIuMTMyeiIgZmlsbD0iI2RkZCIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBkPSJtNzYuMjUgMTE2LjVjLTIuODc1IDEuMTI1LTguNSAxLjYyNS0xMS43NSAxLjYyNS00Ljc2NCAwLTExLjYyNS0uNzUtMTQuMTI1LTEuODc1LTEuNTQ0LTQuNzUxLTYuMTY0LTE5LjQ4LTEwLjcyNy0zOC4xODVsLS40NDctMS44MjctLjAwNC0uMDE1Yy01LjQyNC0yMi4xNTctOS44NTUtNDAuMjUzIDE0LjQyNy00NS45MzguMjIyLS4wNTIuMzMtLjMxNy4xODQtLjQ5Mi0yLjc4Ni0zLjMwNS04LjAwNS00LjM4OC0xNC42MDUtMi4xMTEtLjI3LjA5My0uNTA2LS4xOC0uMzM3LS40MTIgMS4yOTQtMS43ODMgMy44MjMtMy4xNTUgNS4wNzEtMy43NTYuMjU4LS4xMjQuMjQyLS41MDItLjAzLS41ODhhMjcuODc3IDI3Ljg3NyAwIDAgMCAtMy43NzItLjljLS4zNy0uMDU5LS40MDMtLjY5My0uMDMyLS43NDMgOS4zNTYtMS4yNTkgMTkuMTI1IDEuNTUgMjQuMDI4IDcuNzI2YS4zMjYuMzI2IDAgMCAwIC4xODYuMTE0YzE3Ljk1MiAzLjg1NiAxOS4yMzggMzIuMjM1IDE3LjE3IDMzLjUyOC0uNDA4LjI1NS0xLjcxNS4xMDgtMy40MzgtLjA4NS02Ljk4Ni0uNzgxLTIwLjgxOC0yLjMyOS05LjQwMiAxOC45NDguMTEzLjIxLS4wMzYuNDg4LS4yNzIuNTI1LTYuNDM4IDEgMS44MTIgMjEuMTczIDcuODc1IDM0LjQ2MXoiIGZpbGw9IiNmZmYiLz4KICAgIDxwYXRoIGQ9Im04NC4yOCA5MC42OThjLTEuMzY3LS42MzMtNi42MjEgMy4xMzUtMTAuMTEgNi4wMjgtLjcyOC0xLjAzMS0yLjEwMy0xLjc4LTUuMjAzLTEuMjQyLTIuNzEzLjQ3Mi00LjIxMSAxLjEyNi00Ljg4IDIuMjU0LTQuMjgzLTEuNjIzLTExLjQ4OC00LjEzLTEzLjIyOS0xLjcxLTEuOTAyIDIuNjQ2LjQ3NiAxNS4xNjEgMy4wMDMgMTYuNzg2IDEuMzIuODQ5IDcuNjMtMy4yMDggMTAuOTI2LTYuMDA1LjUzMi43NDkgMS4zODggMS4xNzggMy4xNDggMS4xMzcgMi42NjItLjA2MiA2Ljk3OS0uNjgxIDcuNjQ5LTEuOTIxLjA0LS4wNzUuMDc1LS4xNjQuMTA1LS4yNjYgMy4zODggMS4yNjYgOS4zNSAyLjYwNiAxMC42ODIgMi40MDYgMy40Ny0uNTIxLS40ODQtMTYuNzIzLTIuMDktMTcuNDY3eiIgZmlsbD0iIzNjYTgyYiIvPgogICAgPHBhdGggZD0ibTc0LjQ5IDk3LjA5N2MuMTQ0LjI1Ni4yNi41MjYuMzU4LjguNDgzIDEuMzUyIDEuMjcgNS42NDguNjc0IDYuNzA5LS41OTUgMS4wNjItNC40NTkgMS41NzQtNi44NDMgMS42MTVzLTIuOTItLjgzMS0zLjQwMy0yLjE4MWMtLjM4Ny0xLjA4MS0uNTc3LTMuNjIxLS41NzItNS4wNzUtLjA5OC0yLjE1OC42OS0yLjkxNiA0LjMzNC0zLjUwNiAyLjY5Ni0uNDM2IDQuMTIxLjA3MSA0Ljk0NC45NCAzLjgyOC0yLjg1NyAxMC4yMTUtNi44ODkgMTAuODM4LTYuMTUyIDMuMTA2IDMuNjc0IDMuNDk5IDEyLjQyIDIuODI2IDE1LjkzOS0uMjIgMS4xNTEtMTAuNTA1LTEuMTM5LTEwLjUwNS0yLjM4IDAtNS4xNTItMS4zMzctNi41NjUtMi42NS02Ljcxem0tMjIuNTMtMS42MDljLjg0My0xLjMzMyA3LjY3NC4zMjUgMTEuNDI0IDEuOTkzIDAgMC0uNzcgMy40OTEuNDU2IDcuNjA0LjM1OSAxLjIwMy04LjYyNyA2LjU1OC05LjggNS42MzctMS4zNTUtMS4wNjUtMy44NS0xMi40MzItMi4wOC0xNS4yMzR6IiBmaWxsPSIjNGNiYTNjIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im01NS4yNjkgNjguNDA2Yy41NTMtMi40MDMgMy4xMjctNi45MzIgMTIuMzIxLTYuODIyIDQuNjQ4LS4wMTkgMTAuNDIyLS4wMDIgMTQuMjUtLjQzNmE1MS4zMTIgNTEuMzEyIDAgMCAwIDEyLjcyNi0zLjA5NWMzLjk4LTEuNTE5IDUuMzkyLTEuMTggNS44ODctLjI3Mi41NDQuOTk5LS4wOTcgMi43MjItMS40ODggNC4zMDktMi42NTYgMy4wMy03LjQzMSA1LjM4LTE1Ljg2NSA2LjA3Ni04LjQzMy42OTgtMTQuMDItMS41NjUtMTYuNDI1IDIuMTE4LTEuMDM4IDEuNTg5LS4yMzYgNS4zMzMgNy45MiA2LjUxMiAxMS4wMiAxLjU5IDIwLjA3Mi0xLjkxNyAyMS4xOS4yMDEgMS4xMTkgMi4xMTgtNS4zMjMgNi40MjgtMTYuMzYyIDYuNTE4cy0xNy45MzQtMy44NjUtMjAuMzc5LTUuODNjLTMuMTAyLTIuNDk1LTQuNDktNi4xMzMtMy43NzUtOS4yNzl6IiBmaWxsPSIjZmMzIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICAgIDxnIGZpbGw9IiMxNDMwN2UiIG9wYWNpdHk9Ii44Ij4KICAgICAgPHBhdGggZD0ibTY5LjMyNyA0Mi4xMjdjLjYxNi0xLjAwOCAxLjk4MS0xLjc4NiA0LjIxNi0xLjc4NiAyLjIzNCAwIDMuMjg1Ljg4OSA0LjAxMyAxLjg4LjE0OC4yMDItLjA3Ni40NC0uMzA2LjM0YTU5Ljg2OSA1OS44NjkgMCAwIDEgLS4xNjgtLjA3M2MtLjgxNy0uMzU3LTEuODItLjc5NS0zLjU0LS44Mi0xLjgzOC0uMDI2LTIuOTk3LjQzNS0zLjcyNy44MzEtLjI0Ni4xMzQtLjYzNC0uMTMzLS40ODgtLjM3MnptLTI1LjE1NyAxLjI5YzIuMTctLjkwNyAzLjg3Ni0uNzkgNS4wODEtLjUwNC4yNTQuMDYuNDMtLjIxMy4yMjctLjM3Ny0uOTM1LS43NTUtMy4wMy0xLjY5Mi01Ljc2LS42NzQtMi40MzcuOTA5LTMuNTg1IDIuNzk2LTMuNTkyIDQuMDM4LS4wMDIuMjkyLjYuMzE3Ljc1Ni4wNy40Mi0uNjcgMS4xMi0xLjY0NiAzLjI4OS0yLjU1M3oiLz4KICAgICAgPHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNzUuNDQgNTUuOTJhMy40NyAzLjQ3IDAgMCAxIC0zLjQ3NC0zLjQ2MiAzLjQ3IDMuNDcgMCAwIDEgMy40NzUtMy40NiAzLjQ3IDMuNDcgMCAwIDEgMy40NzQgMy40NiAzLjQ3IDMuNDcgMCAwIDEgLTMuNDc1IDMuNDYyem0yLjQ0Ny00LjYwOGEuODk5Ljg5OSAwIDAgMCAtMS43OTkgMGMwIC40OTQuNDA1Ljg5NS45Ljg5NS40OTkgMCAuOS0uNC45LS44OTV6bS0yNS40NjQgMy41NDJhNC4wNDIgNC4wNDIgMCAwIDEgLTQuMDQ5IDQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIC00LjA1LTQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIDQuMDUtNC4wMzcgNC4wNDUgNC4wNDUgMCAwIDEgNC4wNSA0LjAzN3ptLTEuMTkzLTEuMzM4YTEuMDUgMS4wNSAwIDAgMCAtMi4wOTcgMCAxLjA0OCAxLjA0OCAwIDAgMCAyLjA5NyAweiIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8L2c+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im02NCAxMTcuNzVjMjkuNjg1IDAgNTMuNzUtMjQuMDY1IDUzLjc1LTUzLjc1cy0yNC4wNjUtNTMuNzUtNTMuNzUtNTMuNzUtNTMuNzUgMjQuMDY1LTUzLjc1IDUzLjc1IDI0LjA2NSA1My43NSA1My43NSA1My43NXptMCA1YzMyLjQ0NyAwIDU4Ljc1LTI2LjMwMyA1OC43NS01OC43NXMtMjYuMzAzLTU4Ljc1LTU4Ljc1LTU4Ljc1LTU4Ljc1IDI2LjMwMy01OC43NSA1OC43NSAyNi4zMDMgNTguNzUgNTguNzUgNTguNzV6IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+');\n}\n\n/* Email tooltip specific */\n.tooltip__button--email {\n flex-direction: column;\n justify-content: center;\n align-items: flex-start;\n font-size: 14px;\n padding: 4px 8px;\n}\n.tooltip__button--email__primary-text {\n font-weight: bold;\n}\n.tooltip__button--email__secondary-text {\n font-size: 12px;\n}\n\n/* Email Protection signup notice */\n:not(.top-autofill) .tooltip--email-signup {\n text-align: left;\n color: #222222;\n padding: 16px 20px;\n width: 380px;\n}\n\n.tooltip--email-signup h1 {\n font-weight: 700;\n font-size: 16px;\n line-height: 1.5;\n margin: 0;\n}\n\n.tooltip--email-signup p {\n font-weight: 400;\n font-size: 14px;\n line-height: 1.4;\n}\n\n.notice-controls {\n display: flex;\n}\n\n.tooltip--email-signup .notice-controls > * {\n border-radius: 8px;\n border: 0;\n cursor: pointer;\n display: inline-block;\n font-family: inherit;\n font-style: normal;\n font-weight: bold;\n padding: 8px 12px;\n text-decoration: none;\n}\n\n.notice-controls .ghost {\n margin-left: 1rem;\n}\n\n.tooltip--email-signup a.primary {\n background: #3969EF;\n color: #fff;\n}\n\n.tooltip--email-signup a.primary:hover,\n.tooltip--email-signup a.primary:focus {\n background: #2b55ca;\n}\n\n.tooltip--email-signup a.primary:active {\n background: #1e42a4;\n}\n\n.tooltip--email-signup button.ghost {\n background: transparent;\n color: #3969EF;\n}\n\n.tooltip--email-signup button.ghost:hover,\n.tooltip--email-signup button.ghost:focus {\n background-color: rgba(0, 0, 0, 0.06);\n color: #2b55ca;\n}\n\n.tooltip--email-signup button.ghost:active {\n background-color: rgba(0, 0, 0, 0.12);\n color: #1e42a4;\n}\n\n.tooltip--email-signup button.close-tooltip {\n background-color: transparent;\n background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMiAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5NCAwLjY1NjkwN0MwLjY4MzQxOCAwLjI2NjM4MyAxLjMxNjU4IDAuMjY2MzgzIDEuNzA3MTEgMC42NTY5MDdMNiA0Ljk0OThMMTAuMjkyOSAwLjY1NjkwN0MxMC42ODM0IDAuMjY2MzgzIDExLjMxNjYgMC4yNjYzODMgMTEuNzA3MSAwLjY1NjkwN0MxMi4wOTc2IDEuMDQ3NDMgMTIuMDk3NiAxLjY4MDYgMTEuNzA3MSAyLjA3MTEyTDcuNDE0MjEgNi4zNjQwMUwxMS43MDcxIDEwLjY1NjlDMTIuMDk3NiAxMS4wNDc0IDEyLjA5NzYgMTEuNjgwNiAxMS43MDcxIDEyLjA3MTFDMTEuMzE2NiAxMi40NjE2IDEwLjY4MzQgMTIuNDYxNiAxMC4yOTI5IDEyLjA3MTFMNiA3Ljc3ODIzTDEuNzA3MTEgMTIuMDcxMUMxLjMxNjU4IDEyLjQ2MTYgMC42ODM0MTcgMTIuNDYxNiAwLjI5Mjg5MyAxMi4wNzExQy0wLjA5NzYzMTEgMTEuNjgwNiAtMC4wOTc2MzExIDExLjA0NzQgMC4yOTI4OTMgMTAuNjU2OUw0LjU4NTc5IDYuMzY0MDFMMC4yOTI4OTQgMi4wNzExMkMtMC4wOTc2MzA2IDEuNjgwNiAtMC4wOTc2MzA2IDEuMDQ3NDMgMC4yOTI4OTQgMC42NTY5MDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg0Ii8+Cjwvc3ZnPgo=);\n background-position: center center;\n background-repeat: no-repeat;\n border: 0;\n cursor: pointer;\n padding: 16px;\n position: absolute;\n right: 12px;\n top: 12px;\n}\n\n/* Import promotion prompt icon style */\n\n.tooltip__button--credentials-import::before {\n content: \"\";\n background-image: url(data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03Ljk3MjY5IDkuMDEzN0M3Ljk3MjY5IDUuMTQwMTQgMTEuMTEyOCAyIDE0Ljk4NjQgMkMxOC44NTk5IDIgMjIuMDAwMSA1LjE0MDE0IDIyLjAwMDEgOS4wMTM3QzIyLjAwMDEgOS44MDM5NyAyMS44Njk0IDEwLjU2MzcgMjEuNjI4NCAxMS4yNzI1QzIxLjQ2MDEgMTEuNzY3NiAyMC44NjU2IDExLjkxMzUgMjAuMzk3NyAxMS42OEMxOS45MjIxIDExLjQ0MjggMTkuNjc3OSAxMC44NjQ0IDE5LjgxOTUgMTAuMzUyQzE5LjkzNzIgOS45MjU5OSAyMC4wMDAxIDkuNDc3MTkgMjAuMDAwMSA5LjAxMzdDMjAuMDAwMSA2LjI0NDcxIDE3Ljc1NTQgNCAxNC45ODY0IDRDMTIuMjE3NCA0IDkuOTcyNjkgNi4yNDQ3MSA5Ljk3MjY5IDkuMDEzN0M5Ljk3MjY5IDkuNTU1NCAxMC4wNTgyIDEwLjA3NTIgMTAuMjE1OCAxMC41NjEzQzEwLjMzMTcgMTAuOTE4OCAxMC4yMzc0IDExLjMxMTEgOS45NzE2NSAxMS41NzY4TDMuOTk5OTQgMTcuNTQ4NVYyMC4wMDAyTDYuNDk5OTggMjAuMDAwMlYxOC4zMDAyQzYuNDk5OTggMTcuNzQ3OSA2Ljk0NzcgMTcuMzAwMiA3LjQ5OTk4IDE3LjMwMDJIOS4yMDAwMVYxNS42MDAxQzkuMjAwMDEgMTUuMDQ3OCA5LjY0NzczIDE0LjYwMDEgMTAuMiAxNC42MDAxSDEwLjVDMTEuMjEwNiAxNC41ODcgMTEuNDI1MiAxNS4zMzY5IDExLjMxNTYgMTUuNzMzMUMxMS4xNTY1IDE2LjMwODUgMTEgMTcuMDAxIDExIDE3LjVDMTEgMTcuNzI4NCAxMS4wMTE4IDE3Ljk1NCAxMS4wMzQ4IDE4LjE3NjNDMTAuNzM3OSAxOC44Mzg3IDEwLjA3MjkgMTkuMzAwMiA5LjMgMTkuMzAwMkg4LjQ5OTk4VjIwLjEwMDJDOC40OTk5OCAyMS4xNDk1IDcuNjQ5MzMgMjIuMDAwMiA2LjU5OTk5IDIyLjAwMDJMMi45OTk5NSAyMi4wMDAyQzIuNzM0NzMgMjIuMDAwMiAyLjQ4MDM3IDIxLjg5NDkgMi4yOTI4NCAyMS43MDczQzIuMTA1MyAyMS41MTk4IDEuOTk5OTQgMjEuMjY1NCAxLjk5OTk0IDIxLjAwMDJWMTcuMTM0M0MxLjk5OTk0IDE2Ljg2OTEgMi4xMDUzIDE2LjYxNDcgMi4yOTI4MyAxNi40MjcyTDguMTQ2ODkgMTAuNTczMUM4LjAzMjc5IDEwLjA3MDkgNy45NzI2OSA5LjU0ODgxIDcuOTcyNjkgOS4wMTM3WiIgZmlsbD0iYmxhY2siIC8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMgOC40OTk4OUMxMyA3LjExOTE4IDE0LjExOTMgNS45OTk4OSAxNS41IDUuOTk5ODlDMTYuODgwNyA1Ljk5OTg5IDE4IDcuMTE5MTggMTggOC40OTk4OUMxOCA5Ljg4MDYgMTYuODgwNyAxMC45OTk5IDE1LjUgMTAuOTk5OUMxNC4xMTkzIDEwLjk5OTkgMTMgOS44ODA2IDEzIDguNDk5ODlaTTE1LjUgNy45OTk4OUMxNS4yMjM5IDcuOTk5ODkgMTUgOC4yMjM3NSAxNSA4LjQ5OTg5QzE1IDguNzc2MDQgMTUuMjIzOSA4Ljk5OTg5IDE1LjUgOC45OTk4OUMxNS43NzYyIDguOTk5ODkgMTYgOC43NzYwNCAxNiA4LjQ5OTg5QzE2IDguMjIzNzUgMTUuNzc2MiA3Ljk5OTg5IDE1LjUgNy45OTk4OVoiIGZpbGw9ImJsYWNrIiAvPgo8cGF0aCBkPSJNMTcgMTVMMTQuNSAxNy41TDE3IDIwIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMjEuNSAxNy41TDE2IDE3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);\n background-repeat: no-repeat;\n}\n"; -},{}],54:[function(require,module,exports){ +},{}],55:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13326,7 +13460,7 @@ function findEnclosedElements(root, selector) { return shadowElements; } -},{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ +},{"./Form/matching.js":35,"./constants.js":58,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],56:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -13357,7 +13491,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":13,"./autofill-utils.js":54,"./requestIdleCallback.js":94}],56:[function(require,module,exports){ +},{"./DeviceInterface.js":14,"./autofill-utils.js":55,"./requestIdleCallback.js":95}],57:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13374,6 +13508,10 @@ const DDG_DOMAIN_REGEX = exports.DDG_DOMAIN_REGEX = new RegExp(/^https:\/\/(([a- * @returns {GlobalConfig} */ function createGlobalConfig(overrides) { + /** + * Defines whether it's one of our desktop apps + * @type {boolean} + */ let isApp = false; let isTopFrame = false; let supportsTopFrame = false; @@ -13443,7 +13581,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],57:[function(require,module,exports){ +},{}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13460,13 +13598,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],58:[function(require,module,exports){ +},{}],59:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAliasCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; var _validatorsZod = require("./validators.zod.js"); var _deviceApi = require("../../../packages/device-api"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ @@ -13628,9 +13766,19 @@ class StartCredentialsImportFlowCall extends _deviceApi.DeviceApiCall { method = "startCredentialsImportFlow"; } /** - * @extends {DeviceApiCall} + * @extends {DeviceApiCall} */ exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; +class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { + method = "emailProtectionGetAlias"; + id = "emailProtectionGetAliasResponse"; + paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; + resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; +} +/** + * @extends {DeviceApiCall} + */ +exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -13713,13 +13861,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[function(require,module,exports){ +},{"../../../packages/device-api":2,"./validators.zod.js":60}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.userPreferencesSchema = exports.triggerContextSchema = exports.storeFormDataSchema = exports.showInContextEmailProtectionSignupPromptSchema = exports.setSizeParamsSchema = exports.setIncontextSignupPermanentlyDismissedAtSchema = exports.sendJSPixelParamsSchema = exports.selectedDetailParamsSchema = exports.runtimeConfigurationSchema = exports.providerStatusUpdatedSchema = exports.outgoingCredentialsSchema = exports.getRuntimeConfigurationResponseSchema = exports.getIncontextSignupDismissedAtSchema = exports.getAvailableInputTypesResultSchema = exports.getAutofillInitDataResponseSchema = exports.getAutofillDataResponseSchema = exports.getAutofillDataRequestSchema = exports.getAutofillCredentialsResultSchema = exports.getAutofillCredentialsParamsSchema = exports.getAliasResultSchema = exports.getAliasParamsSchema = exports.genericErrorSchema = exports.generatedPasswordSchema = exports.emailProtectionStoreUserDataParamsSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionGetUserDataResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetAddressesResultSchema = exports.credentialsSchema = exports.contentScopeSchema = exports.checkCredentialsProviderStatusResultSchema = exports.availableInputTypesSchema = exports.availableInputTypes1Schema = exports.autofillSettingsSchema = exports.autofillFeatureTogglesSchema = exports.askToUnlockProviderResultSchema = exports.apiSchema = exports.addDebugFlagParamsSchema = void 0; +exports.userPreferencesSchema = exports.triggerContextSchema = exports.storeFormDataSchema = exports.showInContextEmailProtectionSignupPromptSchema = exports.setSizeParamsSchema = exports.setIncontextSignupPermanentlyDismissedAtSchema = exports.sendJSPixelParamsSchema = exports.selectedDetailParamsSchema = exports.runtimeConfigurationSchema = exports.providerStatusUpdatedSchema = exports.outgoingCredentialsSchema = exports.getRuntimeConfigurationResponseSchema = exports.getIncontextSignupDismissedAtSchema = exports.getAvailableInputTypesResultSchema = exports.getAutofillInitDataResponseSchema = exports.getAutofillDataResponseSchema = exports.getAutofillDataRequestSchema = exports.getAutofillCredentialsResultSchema = exports.getAutofillCredentialsParamsSchema = exports.getAliasResultSchema = exports.getAliasParamsSchema = exports.genericErrorSchema = exports.generatedPasswordSchema = exports.emailProtectionStoreUserDataParamsSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionGetUserDataResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetAliasResultSchema = exports.emailProtectionGetAliasParamsSchema = exports.emailProtectionGetAddressesResultSchema = exports.credentialsSchema = exports.contentScopeSchema = exports.checkCredentialsProviderStatusResultSchema = exports.availableInputTypesSchema = exports.availableInputTypes1Schema = exports.autofillSettingsSchema = exports.autofillFeatureTogglesSchema = exports.askToUnlockProviderResultSchema = exports.apiSchema = exports.addDebugFlagParamsSchema = void 0; const sendJSPixelParamsSchema = exports.sendJSPixelParamsSchema = null; const addDebugFlagParamsSchema = exports.addDebugFlagParamsSchema = null; const getAutofillCredentialsParamsSchema = exports.getAutofillCredentialsParamsSchema = null; @@ -13729,6 +13877,7 @@ const setIncontextSignupPermanentlyDismissedAtSchema = exports.setIncontextSignu const getIncontextSignupDismissedAtSchema = exports.getIncontextSignupDismissedAtSchema = null; const getAliasParamsSchema = exports.getAliasParamsSchema = null; const getAliasResultSchema = exports.getAliasResultSchema = null; +const emailProtectionGetAliasParamsSchema = exports.emailProtectionGetAliasParamsSchema = null; const emailProtectionStoreUserDataParamsSchema = exports.emailProtectionStoreUserDataParamsSchema = null; const showInContextEmailProtectionSignupPromptSchema = exports.showInContextEmailProtectionSignupPromptSchema = null; const generatedPasswordSchema = exports.generatedPasswordSchema = null; @@ -13737,9 +13886,10 @@ const credentialsSchema = exports.credentialsSchema = null; const genericErrorSchema = exports.genericErrorSchema = null; const contentScopeSchema = exports.contentScopeSchema = null; const userPreferencesSchema = exports.userPreferencesSchema = null; -const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; const availableInputTypesSchema = exports.availableInputTypesSchema = null; +const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; const availableInputTypes1Schema = exports.availableInputTypes1Schema = null; +const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = null; const getAutofillDataRequestSchema = exports.getAutofillDataRequestSchema = null; const getAutofillDataResponseSchema = exports.getAutofillDataResponseSchema = null; @@ -13747,20 +13897,20 @@ const storeFormDataSchema = exports.storeFormDataSchema = null; const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = null; const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = null; const getAutofillCredentialsResultSchema = exports.getAutofillCredentialsResultSchema = null; +const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null; +const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null; const autofillSettingsSchema = exports.autofillSettingsSchema = null; +const emailProtectionGetAliasResultSchema = exports.emailProtectionGetAliasResultSchema = null; const emailProtectionGetIsLoggedInResultSchema = exports.emailProtectionGetIsLoggedInResultSchema = null; const emailProtectionGetUserDataResultSchema = exports.emailProtectionGetUserDataResultSchema = null; const emailProtectionGetCapabilitiesResultSchema = exports.emailProtectionGetCapabilitiesResultSchema = null; const emailProtectionGetAddressesResultSchema = exports.emailProtectionGetAddressesResultSchema = null; const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtectionRefreshPrivateAddressResultSchema = null; const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = null; -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = null; const getRuntimeConfigurationResponseSchema = exports.getRuntimeConfigurationResponseSchema = null; -const askToUnlockProviderResultSchema = exports.askToUnlockProviderResultSchema = null; -const checkCredentialsProviderStatusResultSchema = exports.checkCredentialsProviderStatusResultSchema = null; const apiSchema = exports.apiSchema = null; -},{}],60:[function(require,module,exports){ +},{}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13786,7 +13936,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":59}],61:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":60}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13794,7 +13944,8 @@ Object.defineProperty(exports, "__esModule", { }); exports.AndroidTransport = void 0; var _index = require("../../../packages/device-api/index.js"); -var _deviceApiCalls = require("../__generated__/deviceApiCalls.js"); +var _messaging = require("../../../packages/messaging/messaging.js"); +var _android = require("../../../packages/messaging/android.js"); class AndroidTransport extends _index.DeviceApiTransport { /** @type {GlobalConfig} */ config; @@ -13803,133 +13954,39 @@ class AndroidTransport extends _index.DeviceApiTransport { constructor(globalConfig) { super(); this.config = globalConfig; - if (this.config.isDDGTestMode) { - if (typeof window.BrowserAutofill?.getAutofillData !== 'function') { - console.warn('window.BrowserAutofill.getAutofillData missing'); - } - if (typeof window.BrowserAutofill?.storeFormData !== 'function') { - console.warn('window.BrowserAutofill.storeFormData missing'); - } - } + const messageHandlerNames = ['EmailProtectionStoreUserData', 'EmailProtectionRemoveUserData', 'EmailProtectionGetUserData', 'EmailProtectionGetCapabilities', 'EmailProtectionGetAlias', 'SetIncontextSignupPermanentlyDismissedAt', 'StartEmailProtectionSignup', 'CloseEmailProtectionTab', 'ShowInContextEmailProtectionSignupPrompt', 'StoreFormData', 'GetIncontextSignupDismissedAt', 'GetRuntimeConfiguration', 'GetAutofillData']; + const androidMessagingConfig = new _android.AndroidMessagingConfig({ + messageHandlerNames + }); + this.messaging = new _messaging.Messaging(androidMessagingConfig); } /** * @param {import("../../../packages/device-api").DeviceApiCall} deviceApiCall * @returns {Promise} */ async send(deviceApiCall) { - if (deviceApiCall instanceof _deviceApiCalls.GetRuntimeConfigurationCall) { - return androidSpecificRuntimeConfiguration(this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetAvailableInputTypesCall) { - return androidSpecificAvailableInputTypes(this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetIncontextSignupDismissedAtCall) { - window.BrowserAutofill.getIncontextSignupDismissedAt(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.SetIncontextSignupPermanentlyDismissedAtCall) { - return window.BrowserAutofill.setIncontextSignupPermanentlyDismissedAt(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.StartEmailProtectionSignupCall) { - return window.BrowserAutofill.startEmailProtectionSignup(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.CloseEmailProtectionTabCall) { - return window.BrowserAutofill.closeEmailProtectionTab(JSON.stringify(deviceApiCall.params)); - } - if (deviceApiCall instanceof _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall) { - window.BrowserAutofill.showInContextEmailProtectionSignupPrompt(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.GetAutofillDataCall) { - window.BrowserAutofill.getAutofillData(JSON.stringify(deviceApiCall.params)); - return waitForResponse(deviceApiCall.id, this.config); - } - if (deviceApiCall instanceof _deviceApiCalls.StoreFormDataCall) { - return window.BrowserAutofill.storeFormData(JSON.stringify(deviceApiCall.params)); - } - throw new Error('android: not implemented: ' + deviceApiCall.method); - } -} - -/** - * @param {string} expectedResponse - the name/id of the response - * @param {GlobalConfig} config - * @returns {Promise<*>} - */ -exports.AndroidTransport = AndroidTransport; -function waitForResponse(expectedResponse, config) { - return new Promise(resolve => { - const handler = e => { - if (!config.isDDGTestMode) { - if (e.origin !== '') { - return; - } - } - if (!e.data) { - return; - } - if (typeof e.data !== 'string') { - if (config.isDDGTestMode) { - console.log('❌ event.data was not a string. Expected a string so that it can be JSON parsed'); - } - return; + try { + // if the call has an `id`, it means that it expects a response + if (deviceApiCall.id) { + return await this.messaging.request(deviceApiCall.method, deviceApiCall.params || undefined); + } else { + return this.messaging.notify(deviceApiCall.method, deviceApiCall.params || undefined); } - try { - let data = JSON.parse(e.data); - if (data.type === expectedResponse) { - window.removeEventListener('message', handler); - return resolve(data); - } - if (config.isDDGTestMode) { - console.log(`❌ event.data.type was '${data.type}', which didnt match '${expectedResponse}'`, JSON.stringify(data)); - } - } catch (e) { - window.removeEventListener('message', handler); - if (config.isDDGTestMode) { - console.log('❌ Could not JSON.parse the response'); + } catch (e) { + if (e instanceof _messaging.MissingHandler) { + if (this.config.isDDGTestMode) { + console.log('MissingAndroidHandler error for:', deviceApiCall.method); } + throw new Error('unimplemented handler: ' + deviceApiCall.method); + } else { + throw e; } - }; - window.addEventListener('message', handler); - }); -} - -/** - * @param {GlobalConfig} globalConfig - * @returns {{success: import('../__generated__/validators-ts').RuntimeConfiguration}} - */ -function androidSpecificRuntimeConfiguration(globalConfig) { - if (!globalConfig.userPreferences) { - throw new Error('globalConfig.userPreferences not supported yet on Android'); - } - return { - success: { - // @ts-ignore - contentScope: globalConfig.contentScope, - // @ts-ignore - userPreferences: globalConfig.userPreferences, - // @ts-ignore - userUnprotectedDomains: globalConfig.userUnprotectedDomains, - // @ts-ignore - availableInputTypes: globalConfig.availableInputTypes } - }; -} - -/** - * @param {GlobalConfig} globalConfig - * @returns {{success: import('../__generated__/validators-ts').AvailableInputTypes}} - */ -function androidSpecificAvailableInputTypes(globalConfig) { - if (!globalConfig.availableInputTypes) { - throw new Error('globalConfig.availableInputTypes not supported yet on Android'); } - return { - success: globalConfig.availableInputTypes - }; } +exports.AndroidTransport = AndroidTransport; -},{"../../../packages/device-api/index.js":2,"../__generated__/deviceApiCalls.js":58}],62:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/android.js":5,"../../../packages/messaging/messaging.js":6}],63:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13972,7 +14029,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":5}],63:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":6}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14126,7 +14183,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":2,"../../Settings.js":41,"../../autofill-utils.js":54,"../__generated__/deviceApiCalls.js":58}],64:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../Settings.js":42,"../../autofill-utils.js":55,"../__generated__/deviceApiCalls.js":59}],65:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14170,7 +14227,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":61,"./apple.transport.js":62,"./extension.transport.js":63,"./windows.transport.js":65}],65:[function(require,module,exports){ +},{"./android.transport.js":62,"./apple.transport.js":63,"./extension.transport.js":64,"./windows.transport.js":66}],66:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14255,7 +14312,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":2}],66:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2}],67:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14356,7 +14413,7 @@ module.exports={ } } -},{}],67:[function(require,module,exports){ +},{}],68:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14457,7 +14514,7 @@ module.exports={ } } -},{}],68:[function(require,module,exports){ +},{}],69:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14558,7 +14615,7 @@ module.exports={ } } -},{}],69:[function(require,module,exports){ +},{}],70:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14659,7 +14716,7 @@ module.exports={ } } -},{}],70:[function(require,module,exports){ +},{}],71:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14760,7 +14817,7 @@ module.exports={ } } -},{}],71:[function(require,module,exports){ +},{}],72:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -14862,7 +14919,7 @@ module.exports={ } } -},{}],72:[function(require,module,exports){ +},{}],73:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14963,7 +15020,7 @@ module.exports={ } } -},{}],73:[function(require,module,exports){ +},{}],74:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15064,7 +15121,7 @@ module.exports={ } } -},{}],74:[function(require,module,exports){ +},{}],75:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15165,7 +15222,7 @@ module.exports={ } } -},{}],75:[function(require,module,exports){ +},{}],76:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15266,7 +15323,7 @@ module.exports={ } } -},{}],76:[function(require,module,exports){ +},{}],77:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15367,7 +15424,7 @@ module.exports={ } } -},{}],77:[function(require,module,exports){ +},{}],78:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15468,7 +15525,7 @@ module.exports={ } } -},{}],78:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15569,7 +15626,7 @@ module.exports={ } } -},{}],79:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15670,7 +15727,7 @@ module.exports={ } } -},{}],80:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15771,7 +15828,7 @@ module.exports={ } } -},{}],81:[function(require,module,exports){ +},{}],82:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15872,7 +15929,7 @@ module.exports={ } } -},{}],82:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15973,7 +16030,7 @@ module.exports={ } } -},{}],83:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16074,7 +16131,7 @@ module.exports={ } } -},{}],84:[function(require,module,exports){ +},{}],85:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16175,7 +16232,7 @@ module.exports={ } } -},{}],85:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16276,7 +16333,7 @@ module.exports={ } } -},{}],86:[function(require,module,exports){ +},{}],87:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16377,7 +16434,7 @@ module.exports={ } } -},{}],87:[function(require,module,exports){ +},{}],88:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16478,7 +16535,7 @@ module.exports={ } } -},{}],88:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16579,7 +16636,7 @@ module.exports={ } } -},{}],89:[function(require,module,exports){ +},{}],90:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16667,7 +16724,7 @@ function translateImpl(library, namespacedId, opts) { return out; } -},{"./translations.js":92}],90:[function(require,module,exports){ +},{"./translations.js":93}],91:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16768,7 +16825,7 @@ module.exports={ } } -},{}],91:[function(require,module,exports){ +},{}],92:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16869,7 +16926,7 @@ module.exports={ } } -},{}],92:[function(require,module,exports){ +},{}],93:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16988,7 +17045,7 @@ var _default = exports.default = { } }; -},{"./bg/autofill.json":66,"./cs/autofill.json":67,"./da/autofill.json":68,"./de/autofill.json":69,"./el/autofill.json":70,"./en/autofill.json":71,"./es/autofill.json":72,"./et/autofill.json":73,"./fi/autofill.json":74,"./fr/autofill.json":75,"./hr/autofill.json":76,"./hu/autofill.json":77,"./it/autofill.json":78,"./lt/autofill.json":79,"./lv/autofill.json":80,"./nb/autofill.json":81,"./nl/autofill.json":82,"./pl/autofill.json":83,"./pt/autofill.json":84,"./ro/autofill.json":85,"./ru/autofill.json":86,"./sk/autofill.json":87,"./sl/autofill.json":88,"./sv/autofill.json":90,"./tr/autofill.json":91,"./xa/autofill.json":93}],93:[function(require,module,exports){ +},{"./bg/autofill.json":67,"./cs/autofill.json":68,"./da/autofill.json":69,"./de/autofill.json":70,"./el/autofill.json":71,"./en/autofill.json":72,"./es/autofill.json":73,"./et/autofill.json":74,"./fi/autofill.json":75,"./fr/autofill.json":76,"./hr/autofill.json":77,"./hu/autofill.json":78,"./it/autofill.json":79,"./lt/autofill.json":80,"./lv/autofill.json":81,"./nb/autofill.json":82,"./nl/autofill.json":83,"./pl/autofill.json":84,"./pt/autofill.json":85,"./ro/autofill.json":86,"./ru/autofill.json":87,"./sk/autofill.json":88,"./sl/autofill.json":89,"./sv/autofill.json":91,"./tr/autofill.json":92,"./xa/autofill.json":94}],94:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -17081,7 +17138,7 @@ module.exports={ "note": "Button that prevents the DuckDuckGo email protection signup prompt from appearing again." } } -},{}],94:[function(require,module,exports){ +},{}],95:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17124,4 +17181,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[55]); +},{}]},{},[56]); diff --git a/package-lock.json b/package-lock.json index 3ddd13f28f93..79c5f4f3f88a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@duckduckgo/autoconsent": "^10.15.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#13.1.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#14.0.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.19.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#5.1.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523" @@ -67,12 +67,14 @@ } }, "node_modules/@duckduckgo/autofill": { - "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#1fee787458d13f8ed07f9fe81aecd6e59609339e", - "hasInstallScript": true + "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#8462e3b2d03015246e06ca5b6cfe9e381f626095", + "hasInstallScript": true, + "license": "Apache-2.0" }, "node_modules/@duckduckgo/content-scope-scripts": { "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#1ed569676555d493c9c5575eaed22aa02569aac9", "hasInstallScript": true, + "license": "Apache-2.0", "workspaces": [ "packages/special-pages", "packages/messaging" @@ -92,7 +94,8 @@ } }, "node_modules/@duckduckgo/privacy-reference-tests": { - "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#6133e7d9d9cd5f1b925cab1971b4d785dc639df7" + "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#6133e7d9d9cd5f1b925cab1971b4d785dc639df7", + "license": "Apache-2.0" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", diff --git a/package.json b/package.json index bda620878dc1..e5107fbbc91f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@duckduckgo/autoconsent": "^10.15.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#13.1.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#14.0.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.19.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#5.1.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523"