From 4a80dbdfba395e1be1a22cd6695e510a81b0bb0f Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Fri, 10 May 2024 15:50:17 +0100 Subject: [PATCH 1/3] Updated release notes and version number for new release - 5.199.5 --- app/version/version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/version/version.properties b/app/version/version.properties index bdb8a8cd009d..6a5e9e10bd66 100644 --- a/app/version/version.properties +++ b/app/version/version.properties @@ -1 +1 @@ -VERSION=5.199.4 \ No newline at end of file +VERSION=5.199.5 \ No newline at end of file From de3498c393dfccd09465408d994c012c25a36d44 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Fri, 10 May 2024 16:03:34 +0100 Subject: [PATCH 2/3] Revert "Migrate Autofill to modern javascript communication mechanism (#4360)" This reverts commit df1f9644d53ab5e0faa450eecc75770af647b2fe. --- .github/workflows/e2e-nightly-autofill.yml | 2 +- .../app/browser/BrowserTabViewModelTest.kt | 61 +- .../app/browser/BrowserWebViewClientTest.kt | 10 + .../app/email/EmailInjectorJsTest.kt | 192 ++++++ ...efaultEmailProtectionJavascriptInjector.kt | 50 ++ .../duckduckgo/app/browser/BrowserActivity.kt | 16 +- .../app/browser/BrowserTabFragment.kt | 306 ++++++--- .../app/browser/BrowserTabViewModel.kt | 29 +- .../app/browser/BrowserWebViewClient.kt | 3 + .../app/browser/commands/Command.kt | 12 +- .../duckduckgo/app/email/EmailInjectorJs.kt | 91 +++ .../app/email/EmailJavascriptInterface.kt | 113 ++++ app/src/main/res/raw/inject_alias.js | 21 + app/src/main/res/raw/signout_autofill.js | 21 + .../app/email/EmailJavascriptInterfaceTest.kt | 169 +++++ .../listener/email/EmailProtectionUrlTest.kt | 32 - .../autofill/api/AutofillCapabilityChecker.kt | 33 +- .../autofill/api/AutofillCredentialDialogs.kt | 17 +- .../autofill/api/AutofillEventListener.kt | 46 +- .../autofill/api/BrowserAutofill.kt | 109 +-- .../EmailProtectionInContextSignUpScreens.kt | 12 +- .../api/emailprotection/EmailInjector.kt | 39 ++ ...er.kt => AutofillCapabilityCheckerImpl.kt} | 49 +- .../impl/AutofillJavascriptInjector.kt | 54 -- .../impl/AutofillJavascriptInterface.kt | 436 ++++++++++++ .../autofill/impl/InlineBrowserAutofill.kt | 103 +-- .../impl/RealDuckAddressLoginCreator.kt | 3 +- .../AutofillRuntimeConfigProvider.kt | 31 +- .../InlineBrowserAutofillConfigurator.kt | 67 ++ .../RuntimeConfigurationWriter.kt | 10 +- .../JavascriptCommunicationSupportImpl.kt | 58 -- .../listener/AutofillWebMessageListener.kt | 117 ---- .../WebMessageListenerGetAutofillConfig.kt | 63 -- .../WebMessageListenerGetAutofillData.kt | 199 ------ .../email/WebMessageListenerEmailGetAlias.kt | 83 --- .../WebMessageListenerEmailGetCapabilities.kt | 79 --- .../WebMessageListenerEmailGetUserData.kt | 77 --- ...ebMessageListenerEmailRemoveCredentials.kt | 68 -- ...WebMessageListenerEmailStoreCredentials.kt | 88 --- ...bMessageListenerCloseEmailProtectionTab.kt | 69 -- ...geListenerGetIncontextSignupDismissedAt.kt | 73 -- ...howInContextEmailProtectionSignupPrompt.kt | 89 --- .../WebMessageListenerStoreFormData.kt | 197 ------ .../autofill/impl/di/AutofillModule.kt | 4 - .../EmailProtectionChooseEmailFragment.kt | 11 +- ...ResultHandlerEmailProtectionChooseEmail.kt | 79 +-- ...lProtectionInContextSignUpWebViewClient.kt | 39 ++ .../EmailProtectionInContextSignupActivity.kt | 244 ++++++- .../EmailProtectionInContextSignupFragment.kt | 348 ---------- ...EmailProtectionInContextSignupViewModel.kt | 34 +- ...ltHandlerInContextEmailProtectionPrompt.kt | 73 +- ...ProtectionInContextSignUpPromptFragment.kt | 7 - .../impl/jsbridge/AutofillMessagePoster.kt | 44 +- .../response/AutofillDataResponses.kt | 4 - .../response/AutofillResponseWriter.kt | 7 - .../CredentialAutofillDialogAndroidFactory.kt | 29 +- ...ofillUseGeneratedPasswordDialogFragment.kt | 40 +- .../ResultHandlerUseGeneratedPassword.kt | 44 +- ...AutofillSavingCredentialsDialogFragment.kt | 29 +- .../ResultHandlerSaveLoginCredentials.kt | 25 +- ...AutofillSelectCredentialsDialogFragment.kt | 41 +- .../ResultHandlerCredentialSelection.kt | 77 +-- ...datingExistingCredentialsDialogFragment.kt | 37 +- .../ResultHandlerUpdateLoginCredentials.kt | 25 +- ...ity_email_protection_in_context_signup.xml | 6 +- ...ent_email_protection_in_context_signup.xml | 28 - ...t => AutofillCapabilityCheckerImplTest.kt} | 19 +- ...tofillStoredBackJavascriptInterfaceTest.kt | 440 +++++++++++++ .../impl/InlineBrowserAutofillTest.kt | 200 ++++-- .../impl/RealDuckAddressLoginCreatorTest.kt | 6 +- .../InlineBrowserAutofillConfiguratorTest.kt | 81 +++ .../RealAutofillRuntimeConfigProviderTest.kt | 38 +- .../RealRuntimeConfigurationWriterTest.kt | 12 +- .../AutofillWebMessageListenerTest.kt | 74 --- .../TestWebMessageListenerCallback.kt | 75 --- .../WebMessageListenerGetAutofillDataTest.kt | 266 -------- .../WebMessageListenerStoreFormDataTest.kt | 158 ----- ...ltHandlerEmailProtectionChooseEmailTest.kt | 29 +- .../ResultHandlerUseGeneratedPasswordTest.kt | 29 +- .../ResultHandlerSaveLoginCredentialsTest.kt | 8 +- .../ResultHandlerCredentialSelectionTest.kt | 52 +- ...ResultHandlerUpdateLoginCredentialsTest.kt | 6 +- .../autofill/api/FakeAutofillFeature.kt | 45 ++ .../EmailProtectionJavascriptInjector.kt | 21 +- .../autofill/dist/autofill-debug.js | 622 ++++++++---------- .../@duckduckgo/autofill/dist/autofill.css | 8 +- .../@duckduckgo/autofill/dist/autofill.js | 573 +++++++--------- package-lock.json | 4 +- package.json | 2 +- 89 files changed, 3549 insertions(+), 3891 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt create mode 100644 app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt create mode 100644 app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt create mode 100644 app/src/main/res/raw/inject_alias.js create mode 100644 app/src/main/res/raw/signout_autofill.js create mode 100644 app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt delete mode 100644 app/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionUrlTest.kt create mode 100644 autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/emailprotection/EmailInjector.kt rename autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/{InternalAutofillCapabilityChecker.kt => AutofillCapabilityCheckerImpl.kt} (69%) delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillConfig.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillData.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetAlias.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetCapabilities.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetUserData.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailRemoveCredentials.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailStoreCredentials.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerCloseEmailProtectionTab.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerGetIncontextSignupDismissedAt.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerShowInContextEmailProtectionSignupPrompt.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormData.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignUpWebViewClient.kt delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt delete mode 100644 autofill/autofill-impl/src/main/res/layout/fragment_email_protection_in_context_signup.xml rename autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/{InternalAutofillCapabilityCheckerImplTest.kt => AutofillCapabilityCheckerImplTest.kt} (87%) create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt create mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfiguratorTest.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/TestWebMessageListenerCallback.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillDataTest.kt delete mode 100644 autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormDataTest.kt create mode 100644 autofill/autofill-test/src/main/java/com/duckduckgo/autofill/api/FakeAutofillFeature.kt rename autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt => browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt (58%) diff --git a/.github/workflows/e2e-nightly-autofill.yml b/.github/workflows/e2e-nightly-autofill.yml index e070d89fc945..a4805f53b9bf 100644 --- a/.github/workflows/e2e-nightly-autofill.yml +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -59,7 +59,7 @@ jobs: api-key: ${{ secrets.MOBILE_DEV_API_KEY }} name: ${{ github.sha }} app-file: apk/release.apk - android-api-level: 33 + android-api-level: 30 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 f000aaecfd4b..ee67334009a2 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -151,7 +151,6 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.widget.ui.WidgetCapabilities 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 @@ -3688,14 +3687,50 @@ class BrowserTabViewModelTest { assertTrue(browserViewState().isEmailSignedIn) } + @Test + fun whenEmailSignOutEventThenEmailSignEventCommandSent() = runTest { + emailStateFlow.emit(false) + + assertCommandIssued() + } + + @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(urlRequest()) + testee.showEmailProtectionChooseEmailPrompt() assertCommandIssued { - assertEquals("address", this.duckAddress) + assertEquals("address", this.address) } } @@ -3703,7 +3738,7 @@ class BrowserTabViewModelTest { fun whenShowEmailTooltipIfAddressDoesNotExistThenCommandNotSent() { whenever(mockEmailManager.getEmailAddress()).thenReturn(null) - testee.showEmailProtectionChooseEmailPrompt(urlRequest()) + testee.showEmailProtectionChooseEmailPrompt() assertCommandNotIssued() } @@ -4367,6 +4402,16 @@ 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) @@ -5470,8 +5515,6 @@ class BrowserTabViewModelTest { } } - private fun urlRequest() = AutofillWebMessageRequest("", "", "") - private fun givenLoginDetected(domain: String) = LoginDetected(authLoginDomain = "", forwardedToDomain = domain) private fun givenCurrentSite(domain: String): Site { @@ -5622,6 +5665,10 @@ class BrowserTabViewModelTest { private fun accessibilityViewState() = testee.accessibilityViewState.value!! class FakeCapabilityChecker(var enabled: Boolean) : AutofillCapabilityChecker { - override suspend fun canAccessCredentialManagementScreen(): Boolean = enabled + 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 } } 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 5a13887af234..cfd1e2d11ff0 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -59,6 +59,7 @@ import com.duckduckgo.app.browser.print.PrintInjector import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.statistics.pixels.Pixel 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 @@ -111,6 +112,7 @@ 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() @@ -145,6 +147,7 @@ class BrowserWebViewClientTest { thirdPartyCookieManager, TestScope(), coroutinesTestRule.testDispatcherProvider, + browserAutofillConfigurator, ampLinks, printInjector, internalTestUserChecker, @@ -355,6 +358,13 @@ 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 new file mode 100644 index 000000000000..582fd57f4f93 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt @@ -0,0 +1,192 @@ +/* + * 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.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.State +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 mockAutofillFeature: AutofillFeature = mock() + private val mockAutofill: Autofill = mock() + private val javascriptInjector: EmailProtectionJavascriptInjector = DefaultEmailProtectionJavascriptInjector() + + lateinit var testee: EmailInjectorJs + + @Before + fun setup() { + testee = + EmailInjectorJs( + mockEmailManager, + DuckDuckGoUrlDetectorImpl(), + mockDispatcherProvider, + mockAutofillFeature, + javascriptInjector, + mockAutofill, + ) + + whenever(mockAutofillFeature.self()).thenReturn( + object : Toggle { + var state: Toggle.State? = null + + override fun isEnabled(): Boolean = state?.enable ?: false + + override fun setEnabled(state: Toggle.State) { + this.state = state + } + + override fun getRawStoredState(): State? = this.state + }, + ) + 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)) + mockAutofillFeature.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() { + mockAutofillFeature.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) + mockAutofillFeature.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) + mockAutofillFeature.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 new file mode 100644 index 000000000000..d402b019a509 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt @@ -0,0 +1,50 @@ +/* + * 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 c49f59366578..18494e367606 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -330,18 +330,14 @@ open class BrowserActivity : DuckDuckGoActivity() { return } - val inContextSignupState = currentTab?.inContextEmailProtectionSignupState - if (emailProtectionLinkVerifier.shouldDelegateToInContextView(intent.intentText, inContextSignupState?.showing)) { - currentTab?.resumeEmailProtectionInContextWebFlow( - verificationUrl = intent.intentText, - messageRequestId = inContextSignupState?.requestId!!, - ) + if (emailProtectionLinkVerifier.shouldDelegateToInContextView(intent.intentText, currentTab?.inContextEmailProtectionShowing)) { + currentTab?.showEmailProtectionInContextWebFlow(intent.intentText) 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?.inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) + currentTab?.inContextEmailProtectionShowing = false if (launchNewSearch(intent)) { Timber.w("new tab requested") @@ -715,9 +711,3 @@ 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 6e9d9e272184..e6afec97a8b5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -194,11 +194,11 @@ 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.AutofillWebMessageRequest import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.Callback import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory @@ -208,8 +208,9 @@ 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.EmailProtectionInContextSignUpStartScreen +import com.duckduckgo.autofill.api.EmailProtectionUserPromptListener import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.ExactMatch import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch @@ -217,9 +218,10 @@ 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.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.api.emailprotection.EmailInjector import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.common.ui.DuckDuckGoActivity import com.duckduckgo.common.ui.DuckDuckGoFragment @@ -304,7 +306,8 @@ class BrowserTabFragment : TrackersAnimatorListener, DownloadConfirmationDialogListener, SitePermissionsGrantedListener, - AutofillEventListener { + AutofillEventListener, + EmailProtectionUserPromptListener { private val supervisorJob = SupervisorJob() @@ -372,6 +375,9 @@ class BrowserTabFragment : @Inject lateinit var thirdPartyCookieManager: ThirdPartyCookieManager + @Inject + lateinit var emailInjector: EmailInjector + @Inject lateinit var browserAutofill: BrowserAutofill @@ -421,6 +427,9 @@ class BrowserTabFragment : @Inject lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory + @Inject + lateinit var duckAddressInjectedResultHandler: DuckAddressLoginCreator + @Inject lateinit var existingCredentialMatchDetector: ExistingCredentialMatchDetector @@ -433,6 +442,9 @@ class BrowserTabFragment : @Inject lateinit var autoconsent: Autoconsent + @Inject + lateinit var autofillCapabilityChecker: AutofillCapabilityChecker + @Inject lateinit var sitePermissionsDialogLauncher: SitePermissionsDialogLauncher @@ -467,9 +479,6 @@ class BrowserTabFragment : @Inject lateinit var clientBrandHintProvider: ClientBrandHintProvider - @Inject - lateinit var autofillMessagePoster: AutofillMessagePoster - @Inject lateinit var subscriptions: Subscriptions @@ -478,7 +487,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 inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState() + var inContextEmailProtectionShowing: Boolean = false private var urlExtractingWebView: UrlExtractingWebView? = null @@ -576,13 +585,13 @@ class BrowserTabFragment : private val activityResultHandlerEmailProtectionInContextSignup = registerForActivityResult(StartActivityForResult()) { result: ActivityResult -> when (result.resultCode) { EmailProtectionInContextSignUpScreenResult.SUCCESS -> { - postEmailProtectionFlowFinishedResult(result.data) - inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) + browserAutofill.inContextEmailProtectionFlowFinished() + inContextEmailProtectionShowing = false } EmailProtectionInContextSignUpScreenResult.CANCELLED -> { - postEmailProtectionFlowFinishedResult(result.data) - inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = false) + browserAutofill.inContextEmailProtectionFlowFinished() + inContextEmailProtectionShowing = false } else -> { @@ -592,12 +601,6 @@ 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()) @@ -655,17 +658,17 @@ class BrowserTabFragment : private val autofillCallback = object : Callback { override suspend fun onCredentialsAvailableToInject( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, credentials: List, triggerType: LoginTriggerType, ) { withContext(dispatchers.main()) { - showAutofillDialogChooseCredentials(autofillWebMessageRequest, credentials, triggerType) + showAutofillDialogChooseCredentials(originalUrl, credentials, triggerType) } } override suspend fun onGeneratedPasswordAvailableToUse( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, username: String?, generatedPassword: String, ) { @@ -673,32 +676,20 @@ class BrowserTabFragment : delay(100) withContext(dispatchers.main()) { - showUserAutoGeneratedPasswordDialog(autofillWebMessageRequest, username, generatedPassword) + showUserAutoGeneratedPasswordDialog(originalUrl, username, generatedPassword) } } - override fun onCredentialsSaved(savedCredentials: LoginCredentials) { - viewModel.onShowUserCredentialsSaved(savedCredentials) - } - - override fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { - viewModel.showEmailProtectionChooseEmailPrompt(autofillWebMessageRequest) + override fun noCredentialsAvailable(originalUrl: String) { + viewModel.returnNoCredentialsWithPage(originalUrl) } - 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 fun onCredentialsSaved(savedCredentials: LoginCredentials) { + viewModel.onShowUserCredentialsSaved(savedCredentials) } override suspend fun onCredentialsAvailableToSave( - autofillWebMessageRequest: AutofillWebMessageRequest, + currentUrl: String, credentials: LoginCredentials, ) { val username = credentials.username @@ -709,8 +700,6 @@ class BrowserTabFragment : return } - val currentUrl = autofillWebMessageRequest.requestOrigin - val matchType = existingCredentialMatchDetector.determine(currentUrl, username, password) Timber.v("MatchType is %s", matchType.javaClass.simpleName) @@ -720,30 +709,30 @@ class BrowserTabFragment : withContext(dispatchers.main()) { when (matchType) { ExactMatch -> Timber.w("Credentials already exist for %s", currentUrl) - UsernameMatch -> showAutofillDialogUpdatePassword(autofillWebMessageRequest, credentials) - UsernameMissing -> showAutofillDialogUpdateUsername(autofillWebMessageRequest, credentials) - NoMatch -> showAutofillDialogSaveCredentials(autofillWebMessageRequest, credentials) - UrlOnlyMatch -> showAutofillDialogSaveCredentials(autofillWebMessageRequest, credentials) + UsernameMatch -> showAutofillDialogUpdatePassword(currentUrl, credentials) + UsernameMissing -> showAutofillDialogUpdateUsername(currentUrl, credentials) + NoMatch -> showAutofillDialogSaveCredentials(currentUrl, credentials) + UrlOnlyMatch -> showAutofillDialogSaveCredentials(currentUrl, credentials) } } } private fun showUserAutoGeneratedPasswordDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, username: String?, generatedPassword: String, ) { val url = webView?.url ?: return - if (url != autofillWebMessageRequest.originalPageUrl) { + if (url != originalUrl) { Timber.w("WebView url has changed since autofill request; bailing") return } - val dialog = credentialAutofillDialogFactory.autofillGeneratePasswordDialog(autofillWebMessageRequest, username, generatedPassword, tabId) - showDialogHidingPrevious(dialog, UseGeneratedPasswordDialog.TAG, autofillWebMessageRequest.originalPageUrl) + val dialog = credentialAutofillDialogFactory.autofillGeneratePasswordDialog(url, username, generatedPassword, tabId) + showDialogHidingPrevious(dialog, UseGeneratedPasswordDialog.TAG, originalUrl) } private fun showAutofillDialogChooseCredentials( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, credentials: List, triggerType: LoginTriggerType, ) { @@ -752,12 +741,12 @@ class BrowserTabFragment : return } val url = webView?.url ?: return - if (url != autofillWebMessageRequest.originalPageUrl) { + if (url != originalUrl) { Timber.w("WebView url has changed since autofill request; bailing") return } - val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(autofillWebMessageRequest, credentials, triggerType, tabId) - showDialogHidingPrevious(dialog, CredentialAutofillPickerDialog.TAG, autofillWebMessageRequest.originalPageUrl) + val dialog = credentialAutofillDialogFactory.autofillSelectCredentialsDialog(url, credentials, triggerType, tabId) + showDialogHidingPrevious(dialog, CredentialAutofillPickerDialog.TAG, originalUrl) } } @@ -1269,8 +1258,36 @@ class BrowserTabFragment : viewModel.onRefreshRequested(triggeredByUser = false) } - override fun onSelectedToSignUpForInContextEmailProtection(autofillWebMessageRequest: AutofillWebMessageRequest) { - showEmailProtectionInContextWebFlow(autofillWebMessageRequest = autofillWebMessageRequest) + 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 onSavedCredentials(credentials: LoginCredentials) { @@ -1281,6 +1298,17 @@ 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() @@ -1475,8 +1503,16 @@ 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.ShowEmailProtectionChooseEmailPrompt -> showEmailProtectionChooseEmailDialog(it.duckAddress, it.autofillWebMessageRequest) - is Command.PageChanged -> onPageChanged() + 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.LaunchAutofillSettings -> launchAutofillManagementScreen() is Command.EditWithSelectedQuery -> { omnibar.omnibarTextInput.setText(it.query) @@ -1485,6 +1521,9 @@ 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) @@ -1536,11 +1575,6 @@ class BrowserTabFragment : } } - private fun onPageChanged() { - browserAutofill.notifyPageChanged() - hideDialogWithTag(CredentialAutofillPickerDialog.TAG) - } - private fun extractUrlFromAmpLink(initialUrl: String) { context?.let { val client = urlExtractingWebViewClient.get() @@ -1561,6 +1595,35 @@ 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) @@ -2260,6 +2323,11 @@ class BrowserTabFragment : it.setFindListener(this) loginDetector.addLoginDetection(it) { viewModel.loginDetected() } blobConverterInjector.addJsInterface(it) { url, mimeType -> viewModel.requestFileDownload(url, null, mimeType, true) } + emailInjector.addJsInterface( + it, + onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, + onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, + ) configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } autoconsent.addJsInterface(it, autoconsentCallback) @@ -2311,51 +2379,91 @@ class BrowserTabFragment : } private fun configureWebViewForAutofill(it: DuckDuckGoWebView) { - launch(dispatchers.main()) { - browserAutofill.addJsInterface(it, autofillCallback, tabId) + browserAutofill.addJsInterface(it, autofillCallback, this, null, 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( - autofillWebMessageRequest: AutofillWebMessageRequest, + currentUrl: String, credentials: LoginCredentials, ) { - val dialog = credentialAutofillDialogFactory.autofillSavingCredentialsDialog(autofillWebMessageRequest, credentials, tabId) + val url = webView?.url ?: return + if (url != currentUrl) return + + val dialog = credentialAutofillDialogFactory.autofillSavingCredentialsDialog(url, credentials, tabId) showDialogHidingPrevious(dialog, CredentialSavePickerDialog.TAG) } private fun showAutofillDialogUpdatePassword( - autofillWebMessageRequest: AutofillWebMessageRequest, + currentUrl: String, credentials: LoginCredentials, ) { - val dialog = credentialAutofillDialogFactory.autofillSavingUpdatePasswordDialog(autofillWebMessageRequest, credentials, tabId) + val url = webView?.url ?: return + if (url != currentUrl) return + + val dialog = credentialAutofillDialogFactory.autofillSavingUpdatePasswordDialog(url, credentials, tabId) showDialogHidingPrevious(dialog, CredentialUpdateExistingCredentialsDialog.TAG) } private fun showAutofillDialogUpdateUsername( - autofillWebMessageRequest: AutofillWebMessageRequest, + currentUrl: String, credentials: LoginCredentials, ) { - val dialog = credentialAutofillDialogFactory.autofillSavingUpdateUsernameDialog(autofillWebMessageRequest, credentials, tabId) + val url = webView?.url ?: return + if (url != currentUrl) return + + val dialog = credentialAutofillDialogFactory.autofillSavingUpdateUsernameDialog(url, credentials, tabId) showDialogHidingPrevious(dialog, CredentialUpdateExistingCredentialsDialog.TAG) } @@ -2804,6 +2912,7 @@ class BrowserTabFragment : popupMenu.dismiss() loginDetectionDialog?.dismiss() automaticFireproofDialog?.dismiss() + browserAutofill.removeJsInterface() destroyWebView() super.onDestroy() } @@ -3064,15 +3173,12 @@ class BrowserTabFragment : // NO OP } - private fun showEmailProtectionChooseEmailDialog( - address: String, - autofillWebMessageRequest: AutofillWebMessageRequest, - ) { + private fun showEmailProtectionChooseEmailDialog(address: String) { context?.let { val url = webView?.url ?: return val dialog = credentialAutofillDialogFactory.autofillEmailProtectionEmailChooserDialog( - autofillWebMessageRequest = autofillWebMessageRequest, + url = url, personalDuckAddress = address, tabId = tabId, ) @@ -3080,28 +3186,34 @@ class BrowserTabFragment : } } - private fun showEmailProtectionInContextWebFlow(autofillWebMessageRequest: AutofillWebMessageRequest) { + override fun showNativeInContextEmailProtectionSignupPrompt() { context?.let { - val params = EmailProtectionInContextSignUpStartScreen(messageRequestId = autofillWebMessageRequest.requestId) - val intent = globalActivityStarter.startIntent(it, params) - activityResultHandlerEmailProtectionInContextSignup.launch(intent) - inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState( - showing = true, - requestId = autofillWebMessageRequest.requestId, + val url = webView?.url ?: return + + val dialog = credentialAutofillDialogFactory.emailProtectionInContextSignUpDialog( + tabId = tabId, ) + showDialogHidingPrevious(dialog, EmailProtectionInContextSignUpDialog.TAG, url) } } - fun resumeEmailProtectionInContextWebFlow(verificationUrl: String?, messageRequestId: String) { - if (verificationUrl == null) return + fun showEmailProtectionInContextWebFlow(verificationUrl: String? = null) { context?.let { - val params = EmailProtectionInContextSignUpHandleVerificationLink(url = verificationUrl, messageRequestId = messageRequestId) + val params = if (verificationUrl == null) { + EmailProtectionInContextSignUpScreenNoParams + } else { + EmailProtectionInContextSignUpHandleVerificationLink(verificationUrl) + } val intent = globalActivityStarter.startIntent(it, params) activityResultHandlerEmailProtectionInContextSignup.launch(intent) - inContextEmailProtectionSignupState = InProgressEmailProtectionSignupState(showing = true, requestId = messageRequestId) + inContextEmailProtectionShowing = true } } + 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 76dbfa4808b0..d7d723dea226 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -142,7 +142,6 @@ 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 @@ -449,6 +448,7 @@ class BrowserTabViewModel @Inject constructor( emailManager.signedInFlow().onEach { isSignedIn -> browserViewState.value = currentBrowserViewState().copy(isEmailSignedIn = isSignedIn) + command.value = EmailSignEvent }.launchIn(viewModelScope) observeAccessibilitySettings() @@ -1175,7 +1175,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) @@ -2722,9 +2722,9 @@ class BrowserTabViewModel @Inject constructor( command.postValue(RequestFileDownload(url, contentDisposition, mimeType, requestUserConfirmation)) } - fun showEmailProtectionChooseEmailPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + fun showEmailProtectionChooseEmailPrompt() { emailManager.getEmailAddress()?.let { - command.postValue(ShowEmailProtectionChooseEmailPrompt(it, autofillWebMessageRequest)) + command.postValue(ShowEmailProtectionChooseEmailPrompt(it)) } } @@ -2742,6 +2742,23 @@ 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) } @@ -2864,6 +2881,10 @@ 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 861690a2dafe..a1071100c806 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -61,6 +61,7 @@ import com.duckduckgo.app.browser.print.PrintInjector import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel 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 @@ -89,6 +90,7 @@ 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, @@ -323,6 +325,7 @@ 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 e87a9a57c666..ac012161e03e 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 @@ -38,7 +38,6 @@ import com.duckduckgo.app.cta.ui.Cta import com.duckduckgo.app.cta.ui.ExperimentOnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.survey.model.Survey -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 @@ -193,19 +192,24 @@ sealed class Command { object ChildTabClosed : Command() class CopyAliasToClipboard(val alias: String) : Command() - class ShowEmailProtectionChooseEmailPrompt( + class InjectEmailAddress( val duckAddress: String, - val autofillWebMessageRequest: AutofillWebMessageRequest, + val originalUrl: String, + val autoSaveLogin: Boolean, ) : Command() - object PageChanged : Command() + class ShowEmailProtectionChooseEmailPrompt(val address: String) : Command() + object ShowEmailProtectionInContextSignUpPrompt : Command() sealed class DaxCommand : Command() { object FinishPartialTrackerAnimation : DaxCommand() class HideDaxDialog(val cta: Cta) : DaxCommand() } + + class CancelIncomingAutofillRequest(val url: String) : Command() object LaunchAutofillSettings : 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 new file mode 100644 index 000000000000..1999021a1fdf --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt @@ -0,0 +1,91 @@ +/* + * 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 new file mode 100644 index 000000000000..b17215186201 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt @@ -0,0 +1,113 @@ +/* + * 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 new file mode 100644 index 000000000000..4b938bc50230 --- /dev/null +++ b/app/src/main/res/raw/inject_alias.js @@ -0,0 +1,21 @@ +// +// 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 new file mode 100644 index 000000000000..635651815639 --- /dev/null +++ b/app/src/main/res/raw/signout_autofill.js @@ -0,0 +1,21 @@ +// +// 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 new file mode 100644 index 000000000000..d5ca5b1ea56c --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt @@ -0,0 +1,169 @@ +/* + * 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 deleted file mode 100644 index 1a87ac4777f7..000000000000 --- a/app/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionUrlTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -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 678d1c829359..12109b062c24 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,12 +17,37 @@ package com.duckduckgo.autofill.api /** - * 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 + * 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 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 390e2f3aadee..7e58d3eb9036 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,7 +181,6 @@ interface EmailProtectionInContextSignUpDialog { const val TAG = "EmailProtectionInContextSignUpDialog" const val KEY_RESULT = "result" - const val KEY_URL = "url" } } @@ -194,7 +193,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose which saved credential to autofill */ fun autofillSelectCredentialsDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: List, triggerType: LoginTriggerType, tabId: String, @@ -204,7 +203,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to save credentials or not */ fun autofillSavingCredentialsDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -213,7 +212,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to update an existing credential's password */ fun autofillSavingUpdatePasswordDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -222,7 +221,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to update an existing credential's username */ fun autofillSavingUpdateUsernameDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment @@ -231,7 +230,7 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to choose whether to use generated password or not */ fun autofillGeneratePasswordDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, username: String?, generatedPassword: String, tabId: String, @@ -241,7 +240,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( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, personalDuckAddress: String, tabId: String, ): DialogFragment @@ -249,7 +248,7 @@ interface CredentialAutofillDialogFactory { /** * Creates a dialog which prompts the user to sign up for Email Protection */ - fun emailProtectionInContextSignUpDialog(tabId: String, autofillWebMessageRequest: AutofillWebMessageRequest): DialogFragment + fun emailProtectionInContextSignUpDialog(tabId: String): 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 08170b770e80..247c9a520573 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,10 +25,54 @@ 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(autofillWebMessageRequest: AutofillWebMessageRequest) + 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) /** * 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 3d97714c1236..941a1ef02fe1 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,37 +16,98 @@ 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 */ - suspend fun addJsInterface( + fun addJsInterface( webView: WebView, autofillCallback: Callback, + emailProtectionInContextCallback: EmailProtectionUserPromptListener? = null, + emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener? = null, tabId: String, ) /** - * Notifies that there has been a change in web page, and the autofill state should be re-evaluated + * Removes the JS interface as a clean-up. Recommended to call from onDestroy() of Fragment/Activity containing the WebView */ - fun notifyPageChanged() + 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?) /** * 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() } /** @@ -59,7 +120,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( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, credentials: List, triggerType: LoginTriggerType, ) @@ -69,7 +130,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( - autofillWebMessageRequest: AutofillWebMessageRequest, + currentUrl: String, credentials: LoginCredentials, ) @@ -78,46 +139,18 @@ 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( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, username: String?, generatedPassword: String, ) /** - * 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 + * Called when we've been asked which credentials we have available to autofill, but the answer is none. */ - fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) + fun noCredentialsAvailable(originalUrl: String) /** * 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 563afc6f93c4..df39b2538ce3 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,16 +20,15 @@ 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 */ -data class EmailProtectionInContextSignUpStartScreen(val messageRequestId: String) : GlobalActivityStarter.ActivityParams +object EmailProtectionInContextSignUpScreenNoParams : GlobalActivityStarter.ActivityParams { + private fun readResolve(): Any = EmailProtectionInContextSignUpScreenNoParams +} /** * 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, val messageRequestId: String) : GlobalActivityStarter.ActivityParams +data class EmailProtectionInContextSignUpHandleVerificationLink(val url: String) : GlobalActivityStarter.ActivityParams /** * Activity result codes @@ -37,7 +36,4 @@ 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 new file mode 100644 index 000000000000..aafe79802ab9 --- /dev/null +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/emailprotection/EmailInjector.kt @@ -0,0 +1,39 @@ +/* + * 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-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt similarity index 69% rename from autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt rename to autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt index 9cbb7e25c13d..6b067e1224c1 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityChecker.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImpl.kt @@ -19,57 +19,19 @@ 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. - */ - 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, -) : InternalAutofillCapabilityChecker { +) : AutofillCapabilityChecker { override suspend fun canInjectCredentialsToWebView(url: String): Boolean = withContext(dispatcherProvider.io()) { if (!isSecureAutofillAvailable()) return@withContext false @@ -113,10 +75,6 @@ class AutofillCapabilityCheckerImpl @Inject constructor( return@withContext autofillFeature.canAccessCredentialManagement().isEnabled() } - override fun webViewSupportsAutofill(): Boolean { - return javascriptCommunicationSupport.supportsModernIntegration() - } - private suspend fun isInternalTester(): Boolean { return withContext(dispatcherProvider.io()) { internalTestUserChecker.isInternalTestUser @@ -135,8 +93,3 @@ 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/AutofillJavascriptInjector.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt deleted file mode 100644 index 8a9c327224a5..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 new file mode 100644 index 000000000000..c5ef7fdb189e --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInterface.kt @@ -0,0 +1,436 @@ +/* + * 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/InlineBrowserAutofill.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt index 8671ba3d06bf..b29580d8bb3b 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,105 +16,66 @@ 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.impl.configuration.integration.modern.listener.AutofillWebMessageListener -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.common.utils.plugins.PluginPoint +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.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 autofillCapabilityChecker: InternalAutofillCapabilityChecker, - private val dispatchers: DispatcherProvider, - private val autofillJavascriptInjector: AutofillJavascriptInjector, - private val webMessageListeners: PluginPoint, - private val autofillFeature: AutofillFeature, - private val webMessageAttacher: AutofillWebMessageAttacher, + private val autofillInterface: AutofillJavascriptInterface, + private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, ) : BrowserAutofill { - override suspend fun addJsInterface( + override fun addJsInterface( webView: WebView, autofillCallback: Callback, + emailProtectionInContextCallback: EmailProtectionUserPromptListener?, + emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener?, tabId: String, ) { - 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 - } - - configureModernIntegration(webView, autofillCallback, tabId) - } + 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 } - 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() - } + override fun removeJsInterface() { + autofillInterface.webView = null + } - autofillJavascriptInjector.addDocumentStartJavascript(webView) + override fun injectCredentials(credentials: LoginCredentials?) { + if (credentials == null) { + autofillInterface.injectNoCredentials() + } else { + autofillInterface.injectCredentials(credentials) } } override fun cancelPendingAutofillRequestToChooseCredentials() { - webMessageListeners.getPlugins().forEach { - it.cancelOutstandingRequests() - } + autofillInterface.cancelRetrievingStoredLogins() } - private fun WebView.addWebMessageListener( - messageListener: AutofillWebMessageListener, - autofillCallback: Callback, - tabId: String, - ) { - webMessageAttacher.addListener(this, messageListener) - messageListener.callback = autofillCallback - messageListener.tabId = tabId + override fun acceptGeneratedPassword() { + autofillInterface.acceptGeneratedPassword() } - override fun notifyPageChanged() { - webMessageListeners.getPlugins().forEach { it.cancelOutstandingRequests() } + override fun rejectGeneratedPassword() { + autofillInterface.rejectGeneratedPassword() } -} - -interface AutofillWebMessageAttacher { - fun addListener( - webView: WebView, - listener: AutofillWebMessageListener, - ) -} - -@SuppressLint("RequiresFeature") -@ContributesBinding(FragmentScope::class) -class AutofillWebMessageAttacherImpl @Inject constructor() : AutofillWebMessageAttacher { - override fun addListener( - webView: WebView, - listener: AutofillWebMessageListener, - ) { - WebViewCompat.addWebMessageListener(webView, listener.key, listener.origins, listener) + override fun inContextEmailProtectionFlowFinished() { + autofillInterface.inContextEmailProtectionFlowFinished() } } 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 d89645591737..ef5ea4187e8f 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,6 +17,7 @@ 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 @@ -34,7 +35,7 @@ import timber.log.Timber class RealDuckAddressLoginCreator @Inject constructor( private val autofillStore: InternalAutofillStore, private val autoSavedLoginsMonitor: AutomaticSavedLoginsMonitor, - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val autofillCapabilityChecker: AutofillCapabilityChecker, @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 67f1174016fd..36446c0b0394 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) 2024 DuckDuckGo + * 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. @@ -16,9 +16,9 @@ package com.duckduckgo.autofill.impl.configuration +import com.duckduckgo.autofill.api.AutofillCapabilityChecker 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 @@ -30,7 +30,10 @@ import javax.inject.Inject import timber.log.Timber interface AutofillRuntimeConfigProvider { - suspend fun getRuntimeConfiguration(url: String?): String + suspend fun getRuntimeConfiguration( + rawJs: String, + url: String?, + ): String } @ContributesBinding(AppScope::class) @@ -38,13 +41,13 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( private val emailManager: EmailManager, private val autofillStore: InternalAutofillStore, private val runtimeConfigurationWriter: RuntimeConfigurationWriter, - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, + private val autofillCapabilityChecker: AutofillCapabilityChecker, 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") @@ -60,17 +63,11 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( ) val availableInputTypes = generateAvailableInputTypes(url) - return """ - { - "type": "getRuntimeConfigurationResponse", - "success": { - $contentScope, - $userPreferences, - $availableInputTypes, - $userUnprotectedDomains - } - } - """.trimIndent() + return rawJs + .replace("// INJECT contentScope HERE", contentScope) + .replace("// INJECT userUnprotectedDomains HERE", userUnprotectedDomains) + .replace("// INJECT userPreferences HERE", userPreferences) + .replace("// INJECT availableInputTypes HERE", availableInputTypes) } private suspend fun generateAvailableInputTypes(url: String?): String { @@ -80,7 +77,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 new file mode 100644 index 000000000000..7d37957ca38d --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfigurator.kt @@ -0,0 +1,67 @@ +/* + * 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 760ca108c15c..c3afea4263e1 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 @@ -59,7 +59,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateContentScope(): String { return """ - "contentScope" : { + contentScope = { "features": { "autofill": { "state": "enabled", @@ -67,7 +67,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run } }, "unprotectedTemporary": [] - } + }; """.trimIndent() } @@ -76,7 +76,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateUserUnprotectedDomains(): String { return """ - "userUnprotectedDomains" : [] + userUnprotectedDomains = []; """.trimIndent() } @@ -88,7 +88,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run showInContextEmailProtectionSignup: Boolean, ): String { return """ - "userPreferences" : { + userPreferences = { "debug": false, "platform": { "name": "android" @@ -109,7 +109,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run } } } - } + }; """.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 deleted file mode 100644 index 13e0eb0cc6eb..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import javax.inject.Inject -import timber.log.Timber - -interface JavascriptCommunicationSupport { - fun supportsModernIntegration(): Boolean -} - -@ContributesBinding(AppScope::class) -@SingleInstanceIn(AppScope::class) -class JavascriptCommunicationSupportImpl @Inject constructor() : JavascriptCommunicationSupport { - - 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 - } - - override fun supportsModernIntegration(): Boolean = isModernSupportAvailable - - companion object { - - /** - * 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 deleted file mode 100644 index 3d0d49c4ee17..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 deleted file mode 100644 index 123bee39b5de..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillConfig.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 deleted file mode 100644 index 2fb87998cf04..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillData.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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/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 deleted file mode 100644 index cd2f58759ff4..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetAlias.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 deleted file mode 100644 index b6a0029c02c4..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetCapabilities.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 deleted file mode 100644 index 47017dd2dac9..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailGetUserData.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 deleted file mode 100644 index e33fe7d5d13b..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailRemoveCredentials.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 deleted file mode 100644 index 61feb0c08662..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/WebMessageListenerEmailStoreCredentials.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6f6adedc7058..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerCloseEmailProtectionTab.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 deleted file mode 100644 index d792698d09f9..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerGetIncontextSignupDismissedAt.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 deleted file mode 100644 index e004ff35d021..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/incontext/WebMessageListenerShowInContextEmailProtectionSignupPrompt.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 deleted file mode 100644 index 73bd03268060..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormData.kt +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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 839f8a3f5591..21736e5b18e8 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 @@ -24,7 +24,6 @@ import com.duckduckgo.app.di.IsMainProcess import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.InternalTestUserChecker -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 @@ -144,6 +143,3 @@ class AutofillModule { */ @ContributesPluginPoint(scope = AppScope::class, boundType = AutofillFragmentResultsPlugin::class) interface UnusedAutofillResultPlugin - -@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 528641767312..4e446f1c0863 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,12 +22,9 @@ 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 @@ -92,7 +89,7 @@ class EmailProtectionChooseEmailFragment : BottomSheetDialogFragment(), EmailPro Timber.v("User action: %s", resultType::class.java.simpleName) val result = Bundle().also { - it.putParcelable(KEY_URL, getWebMessageRequest()) + it.putString(EmailProtectionChooseEmailDialog.KEY_URL, getOriginalUrl()) it.putParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT, resultType) } @@ -112,19 +109,19 @@ class EmailProtectionChooseEmailFragment : BottomSheetDialogFragment(), EmailPro } private fun getPersonalAddress() = arguments?.getString(KEY_ADDRESS)!! - private fun getWebMessageRequest() = BundleCompat.getParcelable(requireArguments(), KEY_URL, AutofillWebMessageRequest::class.java)!! + private fun getOriginalUrl() = arguments?.getString(EmailProtectionChooseEmailDialog.KEY_URL)!! private fun getTabId() = arguments?.getString(KEY_TAB_ID)!! companion object { fun instance( personalDuckAddress: String, - url: AutofillWebMessageRequest, + url: String, tabId: String, ): EmailProtectionChooseEmailFragment { val fragment = EmailProtectionChooseEmailFragment() fragment.arguments = Bundle().also { it.putString(KEY_ADDRESS, personalDuckAddress) - it.putParcelable(KEY_URL, url) + it.putString(EmailProtectionChooseEmailDialog.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 b837cfed47bf..146bdfa96c6e 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,33 +16,28 @@ package com.duckduckgo.autofill.impl.email +import android.annotation.SuppressLint import android.content.Context +import android.os.Build import android.os.Bundle -import androidx.core.os.BundleCompat +import android.os.Parcelable 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.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.EmailProtectionChooseEmailDialog.UseEmailResultType.* import com.duckduckgo.autofill.api.email.EmailManager -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.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -50,13 +45,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(FragmentScope::class) +@ContributesMultibinding(AppScope::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, ) : AutofillFragmentResultsPlugin { @@ -69,66 +63,46 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val userSelection = BundleCompat.getParcelable(result, KEY_RESULT, UseEmailResultType::class.java) ?: return - val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return + val userSelection: EmailProtectionChooseEmailDialog.UseEmailResultType = + result.safeGetParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT) ?: return + val originalUrl = result.getString(EmailProtectionChooseEmailDialog.KEY_URL) ?: return when (userSelection) { - UsePersonalEmailAddress -> onSelectedToUsePersonalAddress(autofillWebMessageRequest) - UsePrivateAliasAddress -> onSelectedToUsePrivateAlias(autofillWebMessageRequest, tabId) - DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection(autofillWebMessageRequest) + UsePersonalEmailAddress -> onSelectedToUsePersonalAddress(originalUrl, autofillCallback) + UsePrivateAliasAddress -> onSelectedToUsePrivateAlias(originalUrl, autofillCallback) + DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection() } } - private fun onSelectedToUsePersonalAddress(autofillWebMessageRequest: AutofillWebMessageRequest) { + private fun onSelectedToUsePersonalAddress(originalUrl: String, autofillCallback: AutofillEventListener) { appCoroutineScope.launch(dispatchers.io()) { val duckAddress = emailManager.getEmailAddress() ?: return@launch enqueueEmailProtectionPixel(EMAIL_USE_ADDRESS, includeLastUsedDay = true) - withContext(dispatchers.io()) { - val message = buildResponseMessage(duckAddress) - messagePoster.postMessage(message, autofillWebMessageRequest.requestId) + withContext(dispatchers.main()) { + autofillCallback.onUseEmailProtectionPersonalAddress(originalUrl, duckAddress) } emailManager.setNewLastUsedDate() } } - private fun onSelectedToUsePrivateAlias( - autofillWebMessageRequest: AutofillWebMessageRequest, - tabId: String, - ) { + private fun onSelectedToUsePrivateAlias(originalUrl: String, autofillCallback: AutofillEventListener) { appCoroutineScope.launch(dispatchers.io()) { val privateAlias = emailManager.getAlias() ?: return@launch enqueueEmailProtectionPixel(EMAIL_USE_ALIAS, includeLastUsedDay = true) - val message = buildResponseMessage(privateAlias) - messagePoster.postMessage(message, autofillWebMessageRequest.requestId) - - loginCreator.createLoginForPrivateDuckAddress( - duckAddress = privateAlias, - tabId = tabId, - originalUrl = autofillWebMessageRequest.requestOrigin, - ) + withContext(dispatchers.main()) { + autofillCallback.onUseEmailProtectionPrivateAlias(originalUrl, privateAlias) + } emailManager.setNewLastUsedDate() } } - 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) + private fun onSelectedNotToUseEmailProtection() { enqueueEmailProtectionPixel(EMAIL_TOOLTIP_DISMISSED, includeLastUsedDay = false) } @@ -147,6 +121,15 @@ 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 new file mode 100644 index 000000000000..fac79f80a763 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignUpWebViewClient.kt @@ -0,0 +1,39 @@ +/* + * 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 57bae3b15dc1..1fc81290114e 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,30 +16,262 @@ package com.duckduckgo.autofill.impl.email.incontext +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.os.Bundle -import androidx.fragment.app.commit +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 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.EmailProtectionInContextSignUpStartScreen +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.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(EmailProtectionInContextSignUpStartScreen::class) +@ContributeToActivityStarter(EmailProtectionInContextSignUpScreenNoParams::class) @ContributeToActivityStarter(EmailProtectionInContextSignUpHandleVerificationLink::class) -class EmailProtectionInContextSignupActivity : DuckDuckGoActivity() { +class EmailProtectionInContextSignupActivity : + DuckDuckGoActivity(), + EmailProtectionInContextSignUpWebChromeClient.ProgressListener, + EmailProtectionInContextSignUpWebViewClient.NewPageCallback { 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) - supportFragmentManager.commit { - replace(R.id.fragment_container, EmailProtectionInContextSignupFragment()) + 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() } } } + + 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 deleted file mode 100644 index b8a4330bd9d1..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt +++ /dev/null @@ -1,348 +0,0 @@ -/* - * 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") - 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 501725fc10d2..41fc608d3b66 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,17 +16,13 @@ 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 @@ -37,18 +33,15 @@ 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) @@ -56,15 +49,6 @@ 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 @@ -125,27 +109,16 @@ 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 || url == null) return + if (!signedIn) return - if (url.contains(EMAIL_VERIFICATION_LINK_URL) || url.contains(EMAIL_SETTINGS_URL)) { - Timber.d("Detected email verification link or signed in state") + if (url?.contains(EMAIL_VERIFICATION_LINK_URL) == true) { + Timber.d("Detected email verification link") _viewState.value = ExitingAsSuccess } } @@ -195,7 +168,6 @@ 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 4a0b07f2ff57..cf078914ed8c 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,25 +16,22 @@ package com.duckduckgo.autofill.impl.email.incontext +import android.annotation.SuppressLint import android.content.Context +import android.os.Build import android.os.Bundle -import androidx.core.os.BundleCompat +import android.os.Parcelable 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.Cancel -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.DoNotShowAgain -import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.SignUp +import com.duckduckgo.autofill.api.EmailProtectionInContextSignUpDialog.EmailProtectionInContextSignUpResult.* 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.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -42,66 +39,60 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(FragmentScope::class) +@ContributesMultibinding(AppScope::class) class ResultHandlerInContextEmailProtectionPrompt @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val dispatchers: DispatcherProvider, private val dataStore: EmailProtectionInContextDataStore, - private val messagePoster: AutofillMessagePoster, + private val appBuildConfig: AppBuildConfig, ) : 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 = BundleCompat.getParcelable(result, KEY_RESULT, EmailProtectionInContextSignUpResult::class.java) ?: return - val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return + val userSelection = result.safeGetParcelable(EmailProtectionInContextSignUpDialog.KEY_RESULT) ?: return appCoroutineScope.launch(dispatchers.io()) { when (userSelection) { - SignUp -> signUpSelected(autofillCallback, autofillWebMessageRequest) - Cancel -> cancelled(autofillWebMessageRequest) - DoNotShowAgain -> doNotAskAgain(autofillWebMessageRequest) + SignUp -> signUpSelected(autofillCallback) + Cancel -> cancelled(autofillCallback) + DoNotShowAgain -> doNotAskAgain(autofillCallback) } } } - private suspend fun signUpSelected( - autofillCallback: AutofillEventListener, - autofillWebMessageRequest: AutofillWebMessageRequest, - ) { + private suspend fun signUpSelected(autofillCallback: AutofillEventListener) { withContext(dispatchers.main()) { - autofillCallback.onSelectedToSignUpForInContextEmailProtection(autofillWebMessageRequest) + autofillCallback.onSelectedToSignUpForInContextEmailProtection() } } - private suspend fun doNotAskAgain(autofillWebMessageRequest: AutofillWebMessageRequest) { + private suspend fun doNotAskAgain(autofillCallback: AutofillEventListener) { Timber.i("User selected to not show sign up for email protection again") dataStore.onUserChoseNeverAskAgain() - notifyEndOfFlow(autofillWebMessageRequest) + notifyEndOfFlow(autofillCallback) } - private suspend fun cancelled(autofillWebMessageRequest: AutofillWebMessageRequest) { + private suspend fun cancelled(autofillCallback: AutofillEventListener) { Timber.i("User cancelled sign up for email protection") - notifyEndOfFlow(autofillWebMessageRequest) + notifyEndOfFlow(autofillCallback) } - private fun notifyEndOfFlow(autofillWebMessageRequest: AutofillWebMessageRequest) { - val message = """ - { - "success": { - "isSignedIn": false - } - } - """.trimIndent() - messagePoster.postMessage(message, autofillWebMessageRequest.requestId) + private suspend fun notifyEndOfFlow(autofillCallback: AutofillEventListener) { + withContext(dispatchers.main()) { + autofillCallback.onEndOfEmailProtectionInContextSignupFlow() + } } 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 17cb8d708cc9..d44b635b2fd2 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,7 +22,6 @@ 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 @@ -30,7 +29,6 @@ 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 @@ -123,7 +121,6 @@ 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) @@ -136,22 +133,18 @@ 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 94544d2a0d1f..092781fc2475 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,28 +17,48 @@ package com.duckduckgo.autofill.impl.jsbridge import android.annotation.SuppressLint -import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener -import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.FragmentScope +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.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn import javax.inject.Inject +import kotlinx.coroutines.withContext import timber.log.Timber interface AutofillMessagePoster { - fun postMessage(message: String, requestId: String) + suspend fun postMessage( + webView: WebView?, + message: String, + ) } -@SuppressLint("RequiresFeature") -@SingleInstanceIn(FragmentScope::class) -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) class AutofillWebViewMessagePoster @Inject constructor( - private val webMessageListeners: PluginPoint, + private val dispatchers: DispatcherProvider, ) : AutofillMessagePoster { - override fun postMessage(message: String, requestId: String) { - webMessageListeners.getPlugins().firstOrNull { it.onResponse(message, requestId) } ?: { - Timber.w("No listener found for requestId: %s", requestId) + @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) + } } } + + 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 50d47d0e9569..4d3e20152b03 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,10 +44,6 @@ 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 fa742b14aa14..6f9fd01ee623 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,7 +28,6 @@ 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 } @@ -40,7 +39,6 @@ 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(" ") @@ -68,11 +66,6 @@ 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 3873de74840d..badc6a00573c 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,7 +17,6 @@ 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 @@ -36,29 +35,29 @@ import javax.inject.Inject class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialAutofillDialogFactory { override fun autofillSelectCredentialsDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: List, triggerType: LoginTriggerType, tabId: String, ): DialogFragment { - return AutofillSelectCredentialsDialogFragment.instance(autofillWebMessageRequest, credentials, triggerType, tabId) + return AutofillSelectCredentialsDialogFragment.instance(url, credentials, triggerType, tabId) } override fun autofillSavingCredentialsDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment { - return AutofillSavingCredentialsDialogFragment.instance(autofillWebMessageRequest, credentials, tabId) + return AutofillSavingCredentialsDialogFragment.instance(url, credentials, tabId) } override fun autofillSavingUpdatePasswordDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment { return AutofillUpdatingExistingCredentialsDialogFragment.instance( - autofillWebMessageRequest, + url, credentials, tabId, CredentialUpdateType.Password, @@ -66,12 +65,12 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA } override fun autofillSavingUpdateUsernameDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): DialogFragment { return AutofillUpdatingExistingCredentialsDialogFragment.instance( - autofillWebMessageRequest, + url, credentials, tabId, CredentialUpdateType.Username, @@ -79,27 +78,27 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA } override fun autofillGeneratePasswordDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, username: String?, generatedPassword: String, tabId: String, ): DialogFragment { - return AutofillUseGeneratedPasswordDialogFragment.instance(autofillWebMessageRequest, username, generatedPassword, tabId) + return AutofillUseGeneratedPasswordDialogFragment.instance(url, username, generatedPassword, tabId) } override fun autofillEmailProtectionEmailChooserDialog( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, personalDuckAddress: String, tabId: String, ): DialogFragment { return EmailProtectionChooseEmailFragment.instance( personalDuckAddress = personalDuckAddress, - url = autofillWebMessageRequest, + url = url, tabId = tabId, ) } - override fun emailProtectionInContextSignUpDialog(tabId: String, autofillWebMessageRequest: AutofillWebMessageRequest): DialogFragment { - return EmailProtectionInContextSignUpPromptFragment.instance(tabId, autofillWebMessageRequest) + override fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment { + return EmailProtectionInContextSignUpPromptFragment.instance(tabId) } } 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 c9db014544fa..3476af5319b8 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,17 +26,11 @@ 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 @@ -105,8 +99,9 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), private fun configureViews(binding: ContentAutofillGeneratePasswordDialogBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED + val originalUrl = getOriginalUrl() configureCloseButton(binding) - configureGeneratePasswordButton(binding) + configureGeneratePasswordButton(binding, originalUrl) configurePasswordField(binding) } @@ -145,15 +140,18 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), return appBuildConfig.sdkInt <= VERSION_CODES.S_V2 } - private fun configureGeneratePasswordButton(binding: ContentAutofillGeneratePasswordDialogBinding) { + private fun configureGeneratePasswordButton( + binding: ContentAutofillGeneratePasswordDialogBinding, + originalUrl: String, + ) { binding.useSecurePasswordButton.setOnClickListener { pixelNameDialogEvent(GeneratedPasswordAccepted)?.let { pixel.fire(it) } val result = Bundle().also { - it.putParcelable(KEY_URL, getAutofillWebMessageRequest()) + it.putString(UseGeneratedPasswordDialog.KEY_URL, originalUrl) it.putBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED, true) - it.putString(KEY_USERNAME, getUsername()) - it.putString(KEY_PASSWORD, getGeneratedPassword()) + it.putString(UseGeneratedPasswordDialog.KEY_USERNAME, getUsername()) + it.putString(UseGeneratedPasswordDialog.KEY_PASSWORD, getGeneratedPassword()) } parentFragment?.setFragmentResult(UseGeneratedPasswordDialog.resultKey(getTabId()), result) @@ -179,7 +177,7 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), val result = Bundle().also { it.putBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED, false) - it.putParcelable(KEY_URL, getAutofillWebMessageRequest()) + it.putString(UseGeneratedPasswordDialog.KEY_URL, getOriginalUrl()) } parentFragment?.setFragmentResult(UseGeneratedPasswordDialog.resultKey(getTabId()), result) @@ -204,15 +202,15 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), object GeneratedPasswordAccepted : DialogEvent } - 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)!! + 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)!! companion object { fun instance( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, username: String?, generatedPassword: String, tabId: String, @@ -220,10 +218,10 @@ class AutofillUseGeneratedPasswordDialogFragment : BottomSheetDialogFragment(), val fragment = AutofillUseGeneratedPasswordDialogFragment() fragment.arguments = Bundle().also { - it.putParcelable(KEY_URL, autofillWebMessageRequest) - it.putString(KEY_USERNAME, username) - it.putString(KEY_PASSWORD, generatedPassword) - it.putString(KEY_TAB_ID, tabId) + 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) } 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 11497db12e78..cae6c4f70bf1 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,23 +18,18 @@ 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.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.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -42,14 +37,12 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(FragmentScope::class) +@ContributesMultibinding(AppScope::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, ) : AutofillFragmentResultsPlugin { @@ -62,43 +55,28 @@ class ResultHandlerUseGeneratedPassword @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return + val originalUrl = result.getString(UseGeneratedPasswordDialog.KEY_URL) ?: return if (result.getBoolean(UseGeneratedPasswordDialog.KEY_ACCEPTED)) { appCoroutineScope.launch(dispatchers.io()) { - onUserAcceptedToUseGeneratedPassword(result, tabId, autofillWebMessageRequest) + onUserAcceptedToUseGeneratedPassword(result, tabId, originalUrl, autofillCallback) } } else { appCoroutineScope.launch(dispatchers.main()) { - rejectGeneratedPassword(autofillWebMessageRequest) + autofillCallback.onRejectGeneratedPassword(originalUrl) } } } - 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, - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, + callback: AutofillEventListener, ) { val username = result.getString(UseGeneratedPasswordDialog.KEY_USERNAME) val password = result.getString(UseGeneratedPasswordDialog.KEY_PASSWORD) ?: return val autologinId = autoSavedLoginsMonitor.getAutoSavedLoginId(tabId) - val matchType = existingCredentialMatchDetector.determine(autofillWebMessageRequest.requestOrigin, username, password) + val matchType = existingCredentialMatchDetector.determine(originalUrl, username, password) Timber.v( "autoSavedLoginId: %s. Match type against existing entries: %s", autologinId, @@ -106,18 +84,18 @@ class ResultHandlerUseGeneratedPassword @Inject constructor( ) if (autologinId == null) { - saveLoginIfNotAlreadySaved(matchType, autofillWebMessageRequest.requestOrigin, username, password, tabId) + saveLoginIfNotAlreadySaved(matchType, originalUrl, username, password, tabId) } else { val existingAutoSavedLogin = autofillStore.getCredentialsWithId(autologinId) if (existingAutoSavedLogin == null) { Timber.w("Can't find saved login with autosavedLoginId: $autologinId") - saveLoginIfNotAlreadySaved(matchType, autofillWebMessageRequest.requestOrigin, username, password, tabId) + saveLoginIfNotAlreadySaved(matchType, originalUrl, username, password, tabId) } else { updateLoginIfDifferent(existingAutoSavedLogin, username, password) } } withContext(dispatchers.main()) { - acceptGeneratedPassword(autofillWebMessageRequest) + callback.onAcceptGeneratedPassword(originalUrl) } } 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 2660c842dd30..ca80ff3fe924 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,7 +22,6 @@ 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 androidx.lifecycle.lifecycleScope @@ -30,11 +29,7 @@ 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 @@ -149,12 +144,12 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre pixelNameDialogEvent(Accepted)?.let { pixel.fire(it) } lifecycleScope.launch(dispatcherProvider.io()) { - faviconManager.persistCachedFavicon(getTabId(), getWebMessageRequest().requestOrigin) + faviconManager.persistCachedFavicon(getTabId(), getOriginalUrl()) } val result = Bundle().also { - it.putParcelable(KEY_URL, getWebMessageRequest()) - it.putParcelable(KEY_CREDENTIALS, getCredentialsToSave()) + it.putString(CredentialSavePickerDialog.KEY_URL, getOriginalUrl()) + it.putParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS, getCredentialsToSave()) } parentFragment?.setFragmentResult(CredentialSavePickerDialog.resultKeyUserChoseToSaveCredentials(getTabId()), result) @@ -181,7 +176,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val parentFragmentForResult = parentFragment appCoroutineScope.launch(dispatcherProvider.io()) { - autofillDeclineCounter.userDeclinedToSaveCredentials(getWebMessageRequest().requestOrigin.extractDomain()) + autofillDeclineCounter.userDeclinedToSaveCredentials(getOriginalUrl().extractDomain()) if (autofillDeclineCounter.shouldPromptToDisableAutofill()) { parentFragmentForResult?.setFragmentResult(CredentialSavePickerDialog.resultKeyShouldPromptToDisableAutofill(getTabId()), Bundle()) @@ -192,7 +187,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre } private fun onUserChoseNeverSaveThisSite() { - viewModel.addSiteToNeverSaveList(getWebMessageRequest().requestOrigin) + viewModel.addSiteToNeverSaveList(getOriginalUrl()) // this is another way to refuse saving credentials, so ensure that normal logic still runs onUserRejectedToSaveCredentials() @@ -239,23 +234,23 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre object Accepted : DialogEvent } - 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)!! + 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)!! companion object { fun instance( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, ): AutofillSavingCredentialsDialogFragment { val fragment = AutofillSavingCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putParcelable(KEY_URL, autofillWebMessageRequest) - it.putParcelable(KEY_CREDENTIALS, credentials) - it.putString(KEY_TAB_ID, tabId) + it.putString(CredentialSavePickerDialog.KEY_URL, url) + it.putParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS, credentials) + it.putString(CredentialSavePickerDialog.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 0bdb84d9b312..25441bf51617 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 androidx.core.os.BundleCompat +import android.os.Parcelable 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,6 +46,7 @@ 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 { @@ -60,11 +61,12 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( autofillFireproofDialogSuppressor.autofillSaveOrUpdateDialogVisibilityChanged(visible = false) - val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL, AutofillWebMessageRequest::class.java) ?: return - val selectedCredentials = BundleCompat.getParcelable(result, KEY_CREDENTIALS, LoginCredentials::class.java) ?: return + val originalUrl = result.getString(CredentialSavePickerDialog.KEY_URL) ?: return + val selectedCredentials = + result.safeGetParcelable(CredentialSavePickerDialog.KEY_CREDENTIALS) ?: return appCoroutineScope.launch(dispatchers.io()) { - val savedCredentials = autofillStore.saveCredentials(autofillWebMessageRequest.requestOrigin, selectedCredentials) + val savedCredentials = autofillStore.saveCredentials(originalUrl, selectedCredentials) if (savedCredentials != null) { declineCounter.disableDeclineCounter() @@ -75,6 +77,15 @@ 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 049320fe7256..02eda26dab3a 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,17 +22,11 @@ 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 @@ -109,7 +103,8 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre private fun configureViews(binding: ContentAutofillSelectCredentialsTooltipBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED - configureRecyclerView(getUrlRequest(), binding) + val originalUrl = getOriginalUrl() + configureRecyclerView(originalUrl, binding) configureCloseButton(binding) } @@ -118,10 +113,10 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre } private fun configureRecyclerView( - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, binding: ContentAutofillSelectCredentialsTooltipBinding, ) { - binding.availableCredentialsRecycler.adapter = configureAdapter(getAvailableCredentials(autofillWebMessageRequest)) + binding.availableCredentialsRecycler.adapter = configureAdapter(getAvailableCredentials(originalUrl)) } private fun configureAdapter(credentials: List): CredentialsPickerRecyclerAdapter { @@ -136,8 +131,8 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val result = Bundle().also { it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, false) - it.putParcelable(KEY_URL_REQUEST, getUrlRequest()) - it.putParcelable(KEY_CREDENTIALS, selectedCredentials) + it.putString(CredentialAutofillPickerDialog.KEY_URL, getOriginalUrl()) + it.putParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS, selectedCredentials) } parentFragment?.setFragmentResult(CredentialAutofillPickerDialog.resultKey(getTabId()), result) @@ -158,7 +153,7 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val result = Bundle().also { it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, true) - it.putParcelable(KEY_URL_REQUEST, getUrlRequest()) + it.putString(CredentialAutofillPickerDialog.KEY_URL, getOriginalUrl()) } parentFragment?.setFragmentResult(CredentialAutofillPickerDialog.resultKey(getTabId()), result) @@ -181,20 +176,20 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre object Selected : DialogEvent } - private fun getAvailableCredentials(autofillWebMessageRequest: AutofillWebMessageRequest): List { - val unsortedCredentials = BundleCompat.getParcelableArrayList(requireArguments(), KEY_CREDENTIALS, LoginCredentials::class.java)!! - val grouped = autofillSelectCredentialsGrouper.group(autofillWebMessageRequest.requestOrigin, unsortedCredentials) + private fun getAvailableCredentials(originalUrl: String): List { + val unsortedCredentials = arguments?.getParcelableArrayList(CredentialAutofillPickerDialog.KEY_CREDENTIALS)!! + val grouped = autofillSelectCredentialsGrouper.group(originalUrl, unsortedCredentials) return autofillSelectCredentialsListBuilder.buildFlatList(grouped) } - 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)!! + 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)!! companion object { fun instance( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: List, triggerType: LoginTriggerType, tabId: String, @@ -204,10 +199,10 @@ class AutofillSelectCredentialsDialogFragment : BottomSheetDialogFragment(), Cre val fragment = AutofillSelectCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putParcelable(KEY_URL_REQUEST, autofillWebMessageRequest) - it.putParcelableArrayList(KEY_CREDENTIALS, cr) - it.putSerializable(KEY_TRIGGER_TYPE, triggerType) - it.putString(KEY_TAB_ID, tabId) + 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) } 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 c77ec9ddc49c..38e6c4a5e3c0 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,27 +16,24 @@ 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 androidx.core.os.BundleCompat +import android.os.Parcelable 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.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.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -44,21 +41,16 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@ContributesMultibinding(FragmentScope::class) +@ContributesMultibinding(AppScope::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, ) : AutofillFragmentResultsPlugin { - override fun resultKey(tabId: String): String { - return CredentialAutofillPickerDialog.resultKey(tabId) - } - override fun processResult( result: Bundle, context: Context, @@ -68,11 +60,11 @@ class ResultHandlerCredentialSelection @Inject constructor( ) { Timber.d("${this::class.java.simpleName}: processing result") - val autofillWebMessageRequest = BundleCompat.getParcelable(result, KEY_URL_REQUEST, AutofillWebMessageRequest::class.java) ?: return + val originalUrl = result.getString(CredentialAutofillPickerDialog.KEY_URL) ?: return if (result.getBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED)) { Timber.v("Autofill: User cancelled credential selection") - injectNoCredentials(autofillWebMessageRequest) + autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) return } @@ -80,37 +72,20 @@ class ResultHandlerCredentialSelection @Inject constructor( processAutofillCredentialSelectionResult( result = result, browserTabFragment = fragment, - autofillWebMessageRequest = autofillWebMessageRequest, + autofillCallback = autofillCallback, + originalUrl = originalUrl, ) } } - 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, - autofillWebMessageRequest: AutofillWebMessageRequest, + autofillCallback: AutofillEventListener, + originalUrl: String, ) { - val selectedCredentials = BundleCompat.getParcelable(result, KEY_CREDENTIALS, LoginCredentials::class.java) ?: return + val selectedCredentials: LoginCredentials = + result.safeGetParcelable(CredentialAutofillPickerDialog.KEY_CREDENTIALS) ?: return selectedCredentials.updateLastUsedTimestamp() @@ -124,19 +99,19 @@ class ResultHandlerCredentialSelection @Inject constructor( DeviceAuthenticator.AuthResult.Success -> { Timber.v("Autofill: user selected credential to use, and successfully authenticated") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_SUCCESSFUL) - injectCredentials(selectedCredentials, autofillWebMessageRequest) + autofillCallback.onShareCredentialsForAutofill(originalUrl, selectedCredentials) } DeviceAuthenticator.AuthResult.UserCancelled -> { Timber.d("Autofill: user selected credential to use, but cancelled without authenticating") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_CANCELLED) - injectNoCredentials(autofillWebMessageRequest) + autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) } 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) - injectNoCredentials(autofillWebMessageRequest) + autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) } } } @@ -150,10 +125,16 @@ class ResultHandlerCredentialSelection @Inject constructor( } } - private fun LoginCredentials.asJsCredentials(): JavascriptCredentials { - return JavascriptCredentials( - username = username, - password = password, - ) + @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) } } 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 f12c0f8700d8..ee6fd6d155df 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,17 +22,11 @@ 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 @@ -110,14 +104,14 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm private fun configureViews(binding: ContentAutofillUpdateExistingCredentialsBinding) { (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED val credentials = getCredentialsToSave() - val webMessageRequest = getWebMessageRequest() + val originalUrl = getOriginalUrl() val updateType = getUpdateType() Timber.v("Update type is $updateType") configureDialogTitle(binding, updateType) configureCloseButtons(binding) configureUpdatedFieldPreview(binding, credentials, updateType) - configureUpdateButton(binding, webMessageRequest, credentials, updateType) + configureUpdateButton(binding, originalUrl, credentials, updateType) } private fun configureDialogTitle( @@ -136,7 +130,7 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm private fun configureUpdateButton( binding: ContentAutofillUpdateExistingCredentialsBinding, - autofillWebMessageRequest: AutofillWebMessageRequest, + originalUrl: String, credentials: LoginCredentials, updateType: CredentialUpdateType, ) { @@ -149,9 +143,9 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm pixelNameDialogEvent(Updated)?.let { pixel.fire(it) } val result = Bundle().also { - it.putParcelable(KEY_URL, autofillWebMessageRequest) - it.putParcelable(KEY_CREDENTIALS, credentials) - it.putParcelable(KEY_CREDENTIAL_UPDATE_TYPE, getUpdateType()) + it.putString(CredentialUpdateExistingCredentialsDialog.KEY_URL, originalUrl) + it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIALS, credentials) + it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_CREDENTIAL_UPDATE_TYPE, getUpdateType()) } parentFragment?.setFragmentResult(CredentialUpdateExistingCredentialsDialog.resultKeyCredentialUpdated(getTabId()), result) @@ -206,15 +200,16 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm object Updated : DialogEvent } - 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)!! + 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)!! companion object { fun instance( - autofillWebMessageRequest: AutofillWebMessageRequest, + url: String, credentials: LoginCredentials, tabId: String, credentialUpdateType: CredentialUpdateType, @@ -222,10 +217,10 @@ class AutofillUpdatingExistingCredentialsDialogFragment : BottomSheetDialogFragm val fragment = AutofillUpdatingExistingCredentialsDialogFragment() fragment.arguments = Bundle().also { - it.putParcelable(KEY_URL, autofillWebMessageRequest) - it.putParcelable(KEY_CREDENTIALS, credentials) - it.putString(KEY_TAB_ID, tabId) - it.putParcelable(KEY_CREDENTIAL_UPDATE_TYPE, credentialUpdateType) + 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) } 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 5bf017bf188c..a1b7019fb820 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,18 +16,19 @@ 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 androidx.core.os.BundleCompat +import android.os.Parcelable 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 @@ -46,6 +47,7 @@ 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 { @@ -60,12 +62,12 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( autofillFireproofDialogSuppressor.autofillSaveOrUpdateDialogVisibilityChanged(visible = false) - 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 + val selectedCredentials = result.safeGetParcelable(KEY_CREDENTIALS) ?: return + val originalUrl = result.getString(CredentialUpdateExistingCredentialsDialog.KEY_URL) ?: return + val updateType = result.safeGetParcelable(KEY_CREDENTIAL_UPDATE_TYPE) ?: return appCoroutineScope.launch(dispatchers.io()) { - autofillStore.updateCredentials(autofillWebMessageRequest.requestOrigin, selectedCredentials, updateType)?.let { + autofillStore.updateCredentials(originalUrl, selectedCredentials, updateType)?.let { withContext(dispatchers.main()) { autofillCallback.onUpdatedCredentials(it) } @@ -73,6 +75,15 @@ 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 7fe7ceb413b9..0494b569d3db 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 deleted file mode 100644 index 59bf313e7578..000000000000 --- a/autofill/autofill-impl/src/main/res/layout/fragment_email_protection_in_context_signup.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt similarity index 87% rename from autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt rename to autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt index 3b8e39b083f4..16354c3f2a8f 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InternalAutofillCapabilityCheckerImplTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt @@ -17,7 +17,6 @@ package com.duckduckgo.autofill.impl 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.toggle.AutofillTestFeature import kotlinx.coroutines.test.runTest @@ -29,14 +28,13 @@ import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -class InternalAutofillCapabilityCheckerImplTest { +class AutofillCapabilityCheckerImplTest { @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() private val internalTestUserChecker: InternalTestUserChecker = mock() private val autofillGlobalCapabilityChecker: AutofillGlobalCapabilityChecker = mock() - private val javascriptCommunicationSupport: JavascriptCommunicationSupport = mock() private lateinit var testee: AutofillCapabilityCheckerImpl @@ -120,20 +118,6 @@ class InternalAutofillCapabilityCheckerImplTest { 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)) @@ -167,7 +151,6 @@ class InternalAutofillCapabilityCheckerImplTest { internalTestUserChecker = internalTestUserChecker, autofillGlobalCapabilityChecker = autofillGlobalCapabilityChecker, dispatcherProvider = coroutineTestRule.testDispatcherProvider, - javascriptCommunicationSupport = javascriptCommunicationSupport, ) } 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 new file mode 100644 index 000000000000..172112766201 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillStoredBackJavascriptInterfaceTest.kt @@ -0,0 +1,440 @@ +/* + * 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 80174fe3a77f..0f28994e95a1 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,107 +1,161 @@ +/* + * 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.Callback -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.toggle.AutofillTestFeature -import kotlinx.coroutines.test.runTest +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 org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.eq +import org.mockito.MockitoAnnotations 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 { - @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 autofillFeature = AutofillTestFeature() - private val webMessageAttacher: AutofillWebMessageAttacher = mock() - private val webMessageListeners = mutableListOf() - private val webMessageListenersPlugin: PluginPoint = object : PluginPoint { - override fun getPlugins(): Collection = webMessageListeners - } + private lateinit var testee: InlineBrowserAutofill + private val automaticSavedLoginsMonitor: AutomaticSavedLoginsMonitor = mock() + private lateinit var autofillJavascriptInterface: FakeAutofillJavascriptInterface - private val testee = InlineBrowserAutofill( - autofillCapabilityChecker = capabilityChecker, - dispatchers = coroutineTestRule.testDispatcherProvider, - autofillJavascriptInjector = autofillJavascriptInjector, - webMessageListeners = webMessageListenersPlugin, - autofillFeature = autofillFeature, - webMessageAttacher = webMessageAttacher, - ) + private lateinit var testWebView: WebView - @Before - fun setup() { - whenever(capabilityChecker.webViewSupportsAutofill()).thenReturn(true) - with(autofillFeature) { - topLevelFeatureEnabled = true - canInjectCredentials = true - canSaveCredentials = true - canGeneratePassword = true - canAccessCredentialManagement = true - onByDefault = true + 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, + ) { } - } - @Test - fun whenAutofillFeatureFlagDisabledThenDoNotAddJsInterface() = runTest { - autofillFeature.topLevelFeatureEnabled = false - testee.addJsInterface() - verifyJavascriptNotAdded() + 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) { + } } - @Test - fun whenWebViewDoesNotSupportIntegrationThenDoNotAddJsInterface() = runTest { - whenever(capabilityChecker.webViewSupportsAutofill()).thenReturn(false) - testee.addJsInterface() - verifyJavascriptNotAdded() + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + autofillJavascriptInterface = FakeAutofillJavascriptInterface() + testWebView = WebView(getApplicationContext()) + testee = InlineBrowserAutofill(autofillInterface = autofillJavascriptInterface, autoSavedLoginsMonitor = automaticSavedLoginsMonitor) } @Test - fun whenWebViewSupportsIntegrationAndFeatureEnabledThenJsInterfaceIsAdded() = runTest { - testee.addJsInterface() - verifyJavascriptIsAdded() + fun whenRemoveJsInterfaceThenRemoveReferenceToWebview() { + testee.addJsInterface(testWebView, testCallback, emailProtectionInContextCallback, emailProtectionInContextSignupFlowCallback, "tabId") + + assertNotNull(autofillJavascriptInterface.webView) + + testee.removeJsInterface() + + assertNull(autofillJavascriptInterface.webView) } @Test - fun whenPluginsIsEmptyThenJsInterfaceIsAdded() = runTest { - webMessageListeners.clear() - testee.addJsInterface() - verifyJavascriptIsAdded() + fun whenInjectCredentialsNullThenInterfaceInjectNoCredentials() { + testee.injectCredentials(null) + + assertEquals(NoCredentialsInjected, autofillJavascriptInterface.lastAction) } @Test - fun whenPluginsIsNotEmptyThenIsRegisteredWithWebView() = runTest { - val mockMessageListener: AutofillWebMessageListener = mock() - webMessageListeners.add(mockMessageListener) - testee.addJsInterface() - verify(webMessageAttacher).addListener(any(), eq(mockMessageListener)) - } + fun whenInjectCredentialsThenInterfaceCredentialsInjected() { + val toInject = LoginCredentials( + id = 1, + domain = "hello.com", + username = "test", + password = "test123", + ) + testee.injectCredentials(toInject) - private suspend fun verifyJavascriptNotAdded() { - verify(autofillJavascriptInjector, never()).addDocumentStartJavascript(any()) + assertEquals(CredentialsInjected(toInject), autofillJavascriptInterface.lastAction) } - private suspend fun verifyJavascriptIsAdded() { - verify(autofillJavascriptInjector).addDocumentStartJavascript(any()) - } + 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() { + } + + override fun acceptGeneratedPassword() { + } + + override fun rejectGeneratedPassword() { + } + + 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 } } 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 f384a2e4f3ab..f16fc89e9679 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.AutofillWebMessageRequest +import com.duckduckgo.autofill.api.AutofillCapabilityChecker 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: InternalAutofillCapabilityChecker = mock() + private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() private val testee = RealDuckAddressLoginCreator( @@ -126,8 +126,6 @@ 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 new file mode 100644 index 000000000000..9ac08289d8e8 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/InlineBrowserAutofillConfiguratorTest.kt @@ -0,0 +1,81 @@ +/* + * 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 db0734aa01c7..757a04770905 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) 2024 DuckDuckGo + * 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. @@ -16,9 +16,9 @@ package com.duckduckgo.autofill.impl.configuration +import com.duckduckgo.autofill.api.AutofillCapabilityChecker 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 @@ -42,7 +42,7 @@ class RealAutofillRuntimeConfigProviderTest { private val autofillStore: InternalAutofillStore = mock() private val runtimeConfigurationWriter: RuntimeConfigurationWriter = mock() private val shareableCredentials: ShareableCredentials = mock() - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() + private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() @@ -81,7 +81,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenAutofillNotEnabledThenConfigurationUserPrefsCredentialsIsFalse() = runTest { configureAutofillCapabilities(enabled = false) - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(false) } @@ -89,7 +89,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenAutofillEnabledThenConfigurationUserPrefsCredentialsIsTrue() = runTest { configureAutofillCapabilities(enabled = true) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(true) } @@ -98,14 +98,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( @@ -129,7 +129,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -153,7 +153,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -178,7 +178,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -203,7 +203,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -228,7 +228,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -253,7 +253,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -277,7 +277,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -301,7 +301,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -316,7 +316,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(true) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -330,7 +330,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(false) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -342,7 +342,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { val url = "example.com" configureAutofillEnabledWithNoSavedCredentials(url) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verifyCanSaveCredentialsReturnedAs(true) } @@ -352,7 +352,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 c9877196ff66..d228b54919fe 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,9 +66,7 @@ class RealRuntimeConfigurationWriterTest { @Test fun whenGenerateUserUnprotectedDomainsThenReturnUserUnprotectedDomainsString() { - val expectedJson = """ - "userUnprotectedDomains" : [] - """.trimIndent() + val expectedJson = "userUnprotectedDomains = [];" assertEquals( expectedJson, testee.generateUserUnprotectedDomains(), @@ -78,7 +76,7 @@ class RealRuntimeConfigurationWriterTest { @Test fun whenGenerateUserPreferencesThenReturnUserPreferencesString() { val expectedJson = """ - "userPreferences" : { + userPreferences = { "debug": false, "platform": { "name": "android" @@ -99,7 +97,7 @@ class RealRuntimeConfigurationWriterTest { } } } - } + }; """.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 deleted file mode 100644 index 4958df8013d3..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 5b44cc1adb05..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/TestWebMessageListenerCallback.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 deleted file mode 100644 index af6e1ab17985..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/WebMessageListenerGetAutofillDataTest.kt +++ /dev/null @@ -1,266 +0,0 @@ -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 deleted file mode 100644 index 96d44afe8e0a..000000000000 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/password/WebMessageListenerStoreFormDataTest.kt +++ /dev/null @@ -1,158 +0,0 @@ -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 fbdf891d6a76..37dff6d86a6e 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,15 +23,13 @@ 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.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,12 +39,7 @@ 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.argWhere -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions -import org.mockito.kotlin.whenever +import org.mockito.kotlin.* @RunWith(AndroidJUnit4::class) class ResultHandlerEmailProtectionChooseEmailTest { @@ -56,18 +49,16 @@ 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, ) @Before @@ -79,17 +70,17 @@ class ResultHandlerEmailProtectionChooseEmailTest { } @Test - fun whenUserSelectedToUsePersonalAddressThenCorrectResponsePosted() = runTest { + fun whenUserSelectedToUsePersonalAddressThenCorrectCallbackInvoked() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(messagePoster).postMessage(argWhere { it.contains(""""alias": "personal-example""") }, any()) + verify(callback).onUseEmailProtectionPersonalAddress(any(), any()) } @Test - fun whenUserSelectedToUsePrivateAliasAddressThenCorrectResponsePosted() = runTest { + fun whenUserSelectedToUsePrivateAliasAddressThenCorrectCallbackInvoked() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(messagePoster).postMessage(argWhere { it.contains(""""alias": "private-example""") }, any()) + verify(callback).onUseEmailProtectionPrivateAlias(any(), any()) } @Test @@ -152,9 +143,7 @@ class ResultHandlerEmailProtectionChooseEmailTest { result: EmailProtectionChooseEmailDialog.UseEmailResultType?, ): Bundle { return Bundle().also { - if (url != null) { - it.putParcelable(EmailProtectionChooseEmailDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) - } + it.putString(EmailProtectionChooseEmailDialog.KEY_URL, url) it.putParcelable(EmailProtectionChooseEmailDialog.KEY_RESULT, result) } } 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 6d41447d5acc..2f4f9a072cf4 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,7 +21,6 @@ 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,8 +29,6 @@ import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_USERNAME import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor -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 kotlinx.coroutines.test.runTest @@ -52,17 +49,13 @@ 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, ) @Before @@ -77,20 +70,18 @@ class ResultHandlerUseGeneratedPasswordTest { } @Test - fun whenUserRejectedToUsePasswordThenCorrectResponsePosted() = runTest { + fun whenUserRejectedToUsePasswordThenCorrectCallbackInvoked() { val bundle = bundle("example.com", acceptedGeneratedPassword = false) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verify(responseWriter).generateResponseForRejectingGeneratedPassword() - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onRejectGeneratedPassword("example.com") } @Test - fun whenUserAcceptedToUsePasswordNoAutoLoginInThenCorrectResponsePosted() = runTest { + fun whenUserAcceptedToUsePasswordNoAutoLoginInThenCorrectCallbackInvoked() = 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(responseWriter).generateResponseForAcceptingGeneratedPassword() - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onAcceptGeneratedPassword("example.com") } @Test @@ -173,12 +164,10 @@ class ResultHandlerUseGeneratedPasswordTest { } @Test - fun whenUserAcceptedToUsePasswordButPasswordIsNullThen() = runTest { + fun whenUserAcceptedToUsePasswordButPasswordIsNullThenCorrectCallbackNotInvoked() = runTest { val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = null) testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - - verify(responseWriter, never()).generateResponseForAcceptingGeneratedPassword() - verify(messagePoster, never()).postMessage(any(), any()) + verify(callback, never()).onAcceptGeneratedPassword("example.com") } @Test @@ -195,9 +184,7 @@ class ResultHandlerUseGeneratedPasswordTest { password: String? = null, ): Bundle { return Bundle().also { - if (url != null) { - it.putParcelable(KEY_URL, AutofillWebMessageRequest(url, url, "abc-123")) - } + it.putString(KEY_URL, url) 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 3bf61138190d..ac353e319d1b 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,12 +45,14 @@ 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, ) @@ -106,9 +108,7 @@ class ResultHandlerSaveLoginCredentialsTest { credentials: LoginCredentials?, ): Bundle { return Bundle().also { - if (url != null) { - it.putParcelable(CredentialSavePickerDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) - } + it.putString(CredentialSavePickerDialog.KEY_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 1c1153a8fc40..af92579d5953 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,15 +21,13 @@ 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.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import kotlinx.coroutines.test.runTest @@ -48,11 +46,10 @@ 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 { @@ -66,43 +63,35 @@ class ResultHandlerCredentialSelectionTest { } @Test - fun whenUserRejectedToUseCredentialThenCorrectResponsePosted() = runTest { + fun whenUserRejectedToUseCredentialThenCorrectCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleForUserCancelling("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - - verify(responseWriter).generateEmptyResponseGetAutofillData() - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onNoCredentialsChosenForAutofill("example.com") } @Test - fun whenUserAcceptedToUseCredentialsAndSuccessfullyAuthenticatedThenCorrectResponsePosted() = runTest { + fun whenUserAcceptedToUseCredentialsAndSuccessfullyAuthenticatedThenCorrectCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - - verify(responseWriter).generateResponseGetAutofillData(any()) - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onShareCredentialsForAutofill("example.com", aLogin()) } @Test - fun whenUserAcceptedToUseCredentialsAndCancelsAuthenticationThenCorrectResponsePosted() = runTest { + fun whenUserAcceptedToUseCredentialsAndCancelsAuthenticationThenCorrectCallbackInvoked() = runTest { configureCancelledAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - - verify(responseWriter).generateEmptyResponseGetAutofillData() - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onNoCredentialsChosenForAutofill("example.com") } @Test - fun whenUserAcceptedToUseCredentialsAndAuthenticationFailsThenCorrectResponsePosted() = runTest { + fun whenUserAcceptedToUseCredentialsAndAuthenticationFailsThenCorrectCallbackInvoked() = runTest { configureFailedAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - - verify(responseWriter).generateEmptyResponseGetAutofillData() - verify(messagePoster).postMessage(anyOrNull(), any()) + verify(callback).onNoCredentialsChosenForAutofill("example.com") } @Test @@ -110,7 +99,7 @@ class ResultHandlerCredentialSelectionTest { configureSuccessfulAuth() val bundle = bundleMissingCredentials("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verifyNoInteractions(messagePoster) + verifyNoInteractions(callback) } @Test @@ -121,25 +110,25 @@ class ResultHandlerCredentialSelectionTest { verifyNoInteractions(callback) } - private fun bundleForUserCancelling(url: String): Bundle { + private fun bundleForUserCancelling(url: String?): Bundle { return Bundle().also { - it.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) + it.putString(CredentialAutofillPickerDialog.KEY_URL, url) it.putBoolean(CredentialAutofillPickerDialog.KEY_CANCELLED, true) } } - private fun bundleForUserAcceptingToAutofill(url: String): Bundle { + private fun bundleForUserAcceptingToAutofill(url: String?): Bundle { return Bundle().also { - it.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) + it.putString(CredentialAutofillPickerDialog.KEY_URL, url) 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.putParcelable(CredentialAutofillPickerDialog.KEY_URL_REQUEST, url.asUrlRequest()) + it.putString(CredentialAutofillPickerDialog.KEY_URL, url) } } @@ -168,13 +157,8 @@ class ResultHandlerCredentialSelectionTest { appCoroutineScope = coroutineTestRule.testScope, pixel = pixel, deviceAuthenticator = deviceAuthenticator, + appBuildConfig = appBuildConfig, autofillStore = autofillStore, - messagePoster = messagePoster, - autofillResponseWriter = responseWriter, ) } - - private fun String.asUrlRequest(): AutofillWebMessageRequest { - return AutofillWebMessageRequest(this, this, "request-id-123") - } } 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 46e7bb93b88b..8d9b3c36c206 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,11 +45,13 @@ 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, ) @@ -101,7 +103,7 @@ class ResultHandlerUpdateLoginCredentialsTest { updateType: CredentialUpdateType, ): Bundle { return Bundle().also { - if (url != null) it.putParcelable(CredentialUpdateExistingCredentialsDialog.KEY_URL, AutofillWebMessageRequest(url, url, "")) + if (url != null) it.putString(CredentialUpdateExistingCredentialsDialog.KEY_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 new file mode 100644 index 000000000000..303db459c592 --- /dev/null +++ b/autofill/autofill-test/src/main/java/com/duckduckgo/autofill/api/FakeAutofillFeature.kt @@ -0,0 +1,45 @@ +/* + * 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/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt b/browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt similarity index 58% rename from autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt rename to browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt index 026bfa470306..fdefaa4d20ab 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/email/EmailProtectionSettingsUrl.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/autofill/EmailProtectionJavascriptInjector.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 DuckDuckGo + * 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. @@ -14,15 +14,18 @@ * limitations under the License. */ -package com.duckduckgo.autofill.impl.configuration.integration.modern.listener.email +package com.duckduckgo.app.autofill -class EmailProtectionUrl { +import android.content.Context - companion object { - fun isEmailProtectionUrl(url: String?): Boolean { - return url?.startsWith(EMAIL_PROTECTION_SETTINGS_URL) == true - } +interface EmailProtectionJavascriptInjector { - private const val EMAIL_PROTECTION_SETTINGS_URL = "https://duckduckgo.com/email" - } + fun getAliasFunctions( + context: Context, + alias: String?, + ): String + + fun getSignOutFunctions( + context: Context, + ): String } diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js index be56d3928cf7..de63872a0617 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js @@ -4591,130 +4591,6 @@ 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 }); @@ -4726,7 +4602,6 @@ Object.defineProperty(exports, "WebkitMessagingConfig", { } }); var _webkit = require("./webkit.js"); -var _android = require("./android.js"); /** * @module Messaging * @@ -4785,7 +4660,7 @@ var _android = require("./android.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig | AndroidMessagingConfig} config + * @param {WebkitMessagingConfig} config */ constructor(config) { this.transport = getTransport(config); @@ -4857,7 +4732,7 @@ class MessagingTransport { } /** - * @param {WebkitMessagingConfig | AndroidMessagingConfig} config + * @param {WebkitMessagingConfig} config * @returns {MessagingTransport} */ exports.MessagingTransport = MessagingTransport; @@ -4865,9 +4740,6 @@ 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'); } @@ -4890,7 +4762,7 @@ class MissingHandler extends Error { */ exports.MissingHandler = MissingHandler; -},{"./android.js":15,"./webkit.js":17}],17:[function(require,module,exports){ +},{"./webkit.js":16}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5285,7 +5157,7 @@ function captureGlobals() { }; } -},{"./messaging.js":16}],18:[function(require,module,exports){ +},{"./messaging.js":15}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5416,7 +5288,7 @@ function _safeHostname(inputHostname) { } } -},{"./lib/apple.password.js":19,"./lib/constants.js":20,"./lib/rules-parser.js":21}],19:[function(require,module,exports){ +},{"./lib/apple.password.js":18,"./lib/constants.js":19,"./lib/rules-parser.js":20}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5945,7 +5817,7 @@ class Password { } exports.Password = Password; -},{"./constants.js":20,"./rules-parser.js":21}],20:[function(require,module,exports){ +},{"./constants.js":19,"./rules-parser.js":20}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5965,7 +5837,7 @@ const constants = exports.constants = { DEFAULT_UNAMBIGUOUS_CHARS }; -},{}],21:[function(require,module,exports){ +},{}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -6561,7 +6433,7 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return newPasswordRules; } -},{}],22:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ module.exports={ "163.com": { "password-rules": "minlength: 6; maxlength: 16;" @@ -7569,7 +7441,7 @@ module.exports={ "password-rules": "minlength: 8; maxlength: 32; max-consecutive: 6; required: lower; required: upper; required: digit;" } } -},{}],23:[function(require,module,exports){ +},{}],22:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7625,7 +7497,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":65,"./deviceApiCalls/transports/transports.js":73}],24:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./DeviceInterface/AndroidInterface.js":23,"./DeviceInterface/AppleDeviceInterface.js":24,"./DeviceInterface/AppleOverlayDeviceInterface.js":25,"./DeviceInterface/ExtensionInterface.js":26,"./DeviceInterface/WindowsInterface.js":28,"./DeviceInterface/WindowsOverlayDeviceInterface.js":29,"./Settings.js":50,"./config.js":64,"./deviceApiCalls/transports/transports.js":72}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7645,35 +7517,25 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {Promise} */ async getAlias() { - // 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) { + const { + alias + } = await (0, _autofillUtils.sendAndWaitForAnswer)(async () => { + if (this.inContextSignup.isAvailable()) { + const { + isSignedIn + } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); // 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.settings.availableInputTypes) { - this.settings.setAvailableInputTypes({ - email: isSignedIn - }); + if (this.globalConfig.availableInputTypes) { + this.globalConfig.availableInputTypes.email = isSignedIn; } this.updateForStateChange(); this.onFinishedAutofill(); } - } - // 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; - } + return window.EmailInterface.showTooltip(); + }, 'getAliasResponse'); + return alias; } /** @@ -7688,9 +7550,14 @@ 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.settings.availableInputTypes?.email === 'boolean') { - return this.settings.availableInputTypes.email; + if (typeof this.globalConfig.availableInputTypes?.email === 'boolean') { + return this.globalConfig.availableInputTypes.email; } // ...on other domains we assume true because the script wouldn't exist otherwise @@ -7705,7 +7572,15 @@ class AndroidInterface extends _InterfacePrototype.default { * Settings page displays data of the logged in user data */ getUserData() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetUserDataCall({})); + let userData = null; + try { + userData = JSON.parse(window.EmailInterface.getUserData()); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } + return Promise.resolve(userData); } /** @@ -7713,13 +7588,25 @@ class AndroidInterface extends _InterfacePrototype.default { * Device capabilities determine which functionality is available to the user */ getEmailProtectionCapabilities() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetCapabilitiesCall({})); + let deviceCapabilities = null; + try { + deviceCapabilities = JSON.parse(window.EmailInterface.getDeviceCapabilities()); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } + return Promise.resolve(deviceCapabilities); } storeUserData(_ref) { let { - addUserData + addUserData: { + token, + userName, + cohort + } } = _ref; - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionStoreUserDataCall(addUserData)); + return window.EmailInterface.storeCredentials(token, userName, cohort); } /** @@ -7727,7 +7614,13 @@ class AndroidInterface extends _InterfacePrototype.default { * Provides functionality to log the user out */ removeUserData() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionRemoveUserDataCall({})); + try { + return window.EmailInterface.removeCredentials(); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } } /** @@ -7752,7 +7645,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":45,"../UI/controllers/NativeUIController.js":58,"../autofill-utils.js":63,"../deviceApiCalls/__generated__/deviceApiCalls.js":67,"./InterfacePrototype.js":28}],25:[function(require,module,exports){ +},{"../InContextSignup.js":44,"../UI/controllers/NativeUIController.js":57,"../autofill-utils.js":62,"../deviceApiCalls/__generated__/deviceApiCalls.js":66,"./InterfacePrototype.js":27}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7984,20 +7877,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManagePasswords')); } - /** - * Opens the native UI for managing identities - */ - openManageIdentities() { - return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManageIdentities')); - } - - /** - * Opens the native UI for managing credit cards - */ - openManageCreditCards() { - return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManageCreditCards')); - } - /** * Gets a single identity obj once the user requests it * @param {IdentityObject['id']} id @@ -8107,7 +7986,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":56,"../UI/controllers/HTMLTooltipUIController.js":57,"../UI/controllers/NativeUIController.js":58,"../UI/controllers/OverlayUIController.js":59,"../autofill-utils.js":63,"../deviceApiCalls/__generated__/deviceApiCalls.js":67,"../deviceApiCalls/additionalDeviceApiCalls.js":69,"./InterfacePrototype.js":28}],26:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../Form/matching.js":43,"../InContextSignup.js":44,"../ThirdPartyProvider.js":51,"../UI/HTMLTooltip.js":55,"../UI/controllers/HTMLTooltipUIController.js":56,"../UI/controllers/NativeUIController.js":57,"../UI/controllers/OverlayUIController.js":58,"../autofill-utils.js":62,"../deviceApiCalls/__generated__/deviceApiCalls.js":66,"../deviceApiCalls/additionalDeviceApiCalls.js":68,"./InterfacePrototype.js":27}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8222,7 +8101,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":57,"./AppleDeviceInterface.js":25,"./overlayApi.js":32}],27:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":56,"./AppleDeviceInterface.js":24,"./overlayApi.js":31}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8441,7 +8320,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":44,"../InContextSignup.js":45,"../UI/HTMLTooltip.js":56,"../UI/controllers/HTMLTooltipUIController.js":57,"../autofill-utils.js":63,"./InterfacePrototype.js":28}],28:[function(require,module,exports){ +},{"../Form/matching.js":43,"../InContextSignup.js":44,"../UI/HTMLTooltip.js":55,"../UI/controllers/HTMLTooltipUIController.js":56,"../autofill-utils.js":62,"./InterfacePrototype.js":27}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9033,19 +8912,11 @@ class InterfacePrototype { let userData; try { userData = await this.getUserData(); - } catch (e) { - if (this.isTestMode()) { - console.log('getUserData failed with', e); - } - } + } catch (e) {} let capabilities; try { capabilities = await this.getEmailProtectionCapabilities(); - } catch (e) { - if (this.isTestMode()) { - console.log('capabilities fetching failed with', e); - } - } + } catch (e) {} // Set up listener for web app actions if (this.globalConfig.isDDGDomain) { @@ -9101,13 +8972,6 @@ 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({ @@ -9275,7 +9139,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":12,"../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":58,"../autofill-utils.js":63,"../config.js":65,"../deviceApiCalls/__generated__/deviceApiCalls.js":67,"../deviceApiCalls/transports/transports.js":73,"./initFormSubmissionsApi.js":31}],29:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../EmailProtection.js":32,"../Form/formatters.js":36,"../Form/matching.js":43,"../InputTypes/Credentials.js":45,"../PasswordGenerator.js":48,"../Scanner.js":49,"../Settings.js":50,"../UI/controllers/NativeUIController.js":57,"../autofill-utils.js":62,"../config.js":64,"../deviceApiCalls/__generated__/deviceApiCalls.js":66,"../deviceApiCalls/transports/transports.js":72,"./initFormSubmissionsApi.js":30}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9355,14 +9219,14 @@ class WindowsInterface extends _InterfacePrototype.default { } default: { - if (this.isTestMode()) { + if (this.globalConfig.isDDGTestMode) { console.warn('unhandled response', resp); } } } return this._closeAutofillParent(); }).catch(e => { - if (this.isTestMode()) { + if (this.globalConfig.isDDGTestMode) { if (e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -9437,7 +9301,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":59,"../deviceApiCalls/__generated__/deviceApiCalls.js":67,"./InterfacePrototype.js":28}],30:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":58,"../deviceApiCalls/__generated__/deviceApiCalls.js":66,"./InterfacePrototype.js":27}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9616,7 +9480,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":57,"../deviceApiCalls/__generated__/deviceApiCalls.js":67,"./InterfacePrototype.js":28,"./overlayApi.js":32}],31:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":56,"../deviceApiCalls/__generated__/deviceApiCalls.js":66,"./InterfacePrototype.js":27,"./overlayApi.js":31}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9715,7 +9579,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":40,"../autofill-utils.js":63}],32:[function(require,module,exports){ +},{"../Form/label-util.js":39,"../autofill-utils.js":62}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9773,7 +9637,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":67}],33:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":66}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9808,7 +9672,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],34:[function(require,module,exports){ +},{}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9945,7 +9809,7 @@ class Form { } submitHandler() { let via = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown'; - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('Form.submitHandler via:', via, this); } if (this.submitHandlerExecuted) return; @@ -10652,7 +10516,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":63,"../constants.js":66,"./FormAnalyzer.js":35,"./formatters.js":37,"./inputStyles.js":38,"./inputTypeConfig.js":39,"./matching.js":44}],35:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":45,"../autofill-utils.js":62,"../constants.js":65,"./FormAnalyzer.js":34,"./formatters.js":36,"./inputStyles.js":37,"./inputTypeConfig.js":38,"./matching.js":43}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11017,7 +10881,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":63,"../constants.js":66,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching.js":44}],36:[function(require,module,exports){ +},{"../autofill-utils.js":62,"../constants.js":65,"./matching-config/__generated__/compiled-matching-config.js":41,"./matching.js":43}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11582,7 +11446,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],37:[function(require,module,exports){ +},{}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11887,7 +11751,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"./countryNames.js":35,"./matching.js":43}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11978,7 +11842,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":39}],39:[function(require,module,exports){ +},{"./inputTypeConfig.js":38}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12230,7 +12094,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":46,"../InputTypes/CreditCard.js":47,"../InputTypes/Identity.js":48,"../UI/img/ddgPasswordIcon.js":61,"../constants.js":66,"./logo-svg.js":41,"./matching.js":44}],40:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":45,"../InputTypes/CreditCard.js":46,"../InputTypes/Identity.js":47,"../UI/img/ddgPasswordIcon.js":60,"../constants.js":65,"./logo-svg.js":40,"./matching.js":43}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12278,7 +12142,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":44}],41:[function(require,module,exports){ +},{"./matching.js":43}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12311,7 +12175,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],42:[function(require,module,exports){ +},{}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12764,7 +12628,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],43:[function(require,module,exports){ +},{}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12839,7 +12703,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":63,"./matching.js":44}],44:[function(require,module,exports){ +},{"../autofill-utils.js":62,"./matching.js":43}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13824,7 +13688,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":63,"../constants.js":66,"./label-util.js":40,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching-utils.js":43}],45:[function(require,module,exports){ +},{"../autofill-utils.js":62,"../constants.js":65,"./label-util.js":39,"./matching-config/__generated__/compiled-matching-config.js":41,"./matching-utils.js":42}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13956,7 +13820,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":63,"./deviceApiCalls/__generated__/deviceApiCalls.js":67}],46:[function(require,module,exports){ +},{"./autofill-utils.js":62,"./deviceApiCalls/__generated__/deviceApiCalls.js":66}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14103,7 +13967,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":63}],47:[function(require,module,exports){ +},{"../autofill-utils.js":62}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14126,7 +13990,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],48:[function(require,module,exports){ +},{}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14166,7 +14030,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":37}],49:[function(require,module,exports){ +},{"../Form/formatters.js":36}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14208,7 +14072,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":18,"../packages/password/rules.json":22}],50:[function(require,module,exports){ +},{"../packages/password/index.js":17,"../packages/password/rules.json":21}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14623,7 +14487,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":34,"./Form/matching.js":44,"./autofill-utils.js":63,"./constants.js":66,"./deviceApiCalls/__generated__/deviceApiCalls.js":67}],51:[function(require,module,exports){ +},{"./Form/Form.js":33,"./Form/matching.js":43,"./autofill-utils.js":62,"./constants.js":65,"./deviceApiCalls/__generated__/deviceApiCalls.js":66}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14738,11 +14602,6 @@ 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; } @@ -14758,9 +14617,6 @@ 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) { @@ -14803,22 +14659,15 @@ class Settings { * @param {{ * mainType: SupportedMainTypes * subtype: import('./Form/matching.js').SupportedSubTypes | "unknown" - * variant?: import('./Form/matching.js').SupportedVariants | "" * }} types * @returns {boolean} */ isTypeUnavailable(_ref) { let { mainType, - subtype, - variant + subtype } = _ref; if (mainType === 'unknown') return true; - - // Ensure password generation feature flag is respected - if (subtype === 'password' && variant === 'new') { - return !this.featureToggles.password_generation; - } if (!this.featureToggles[`inputType_${mainType}`] && subtype !== 'emailAddress') { return true; } @@ -14839,20 +14688,17 @@ class Settings { * @param {{ * mainType: SupportedMainTypes * subtype: import('./Form/matching.js').SupportedSubTypes | "unknown" - * variant?: import('./Form/matching.js').SupportedVariants | "" * }} types * @returns {Promise} */ async populateDataIfNeeded(_ref2) { let { mainType, - subtype, - variant + subtype } = _ref2; if (this.isTypeUnavailable({ mainType, - subtype, - variant + subtype })) return false; if (this.availableInputTypes?.[mainType] === undefined) { await this.populateData(); @@ -14880,8 +14726,7 @@ class Settings { } = _ref3; if (this.isTypeUnavailable({ mainType, - subtype, - variant + subtype })) return false; // If it's an email field and Email Protection is enabled, return true regardless of other options @@ -14997,7 +14842,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":12,"./autofill-utils.js":63,"./deviceApiCalls/__generated__/deviceApiCalls.js":67,"./deviceApiCalls/__generated__/validators.zod.js":68}],52:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./autofill-utils.js":62,"./deviceApiCalls/__generated__/deviceApiCalls.js":66,"./deviceApiCalls/__generated__/validators.zod.js":67}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15064,7 +14909,7 @@ class ThirdPartyProvider { this.device.scanner.forms.forEach(form => form.recategorizeAllInputs()); } } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: providerStatusUpdated error: ❌', e); } } @@ -15079,7 +14924,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -15087,7 +14932,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":12,"./Form/matching.js":44,"./deviceApiCalls/__generated__/deviceApiCalls.js":67,"./deviceApiCalls/__generated__/validators.zod.js":68}],53:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./Form/matching.js":43,"./deviceApiCalls/__generated__/deviceApiCalls.js":66,"./deviceApiCalls/__generated__/validators.zod.js":67}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15215,7 +15060,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":63,"./HTMLTooltip.js":56}],54:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":45,"../autofill-utils.js":62,"./HTMLTooltip.js":55}],53:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15293,7 +15138,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":63,"./HTMLTooltip.js":56}],55:[function(require,module,exports){ +},{"../autofill-utils.js":62,"./HTMLTooltip.js":55}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15349,7 +15194,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":56}],56:[function(require,module,exports){ +},{"./HTMLTooltip.js":55}],55:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15737,7 +15582,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":44,"../autofill-utils.js":63,"./styles/styles.js":62}],57:[function(require,module,exports){ +},{"../Form/matching.js":43,"../autofill-utils.js":62,"./styles/styles.js":61}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16094,7 +15939,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":39,"../../Form/matching.js":44,"../../autofill-utils.js":63,"../DataHTMLTooltip.js":53,"../EmailHTMLTooltip.js":54,"../EmailSignupHTMLTooltip.js":55,"../HTMLTooltip.js":56,"./UIController.js":60}],58:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":38,"../../Form/matching.js":43,"../../autofill-utils.js":62,"../DataHTMLTooltip.js":52,"../EmailHTMLTooltip.js":53,"../EmailSignupHTMLTooltip.js":54,"../HTMLTooltip.js":55,"./UIController.js":59}],57:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16196,11 +16041,6 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } - case 'none': - { - // do nothing - break; - } default: { if (args.device.isTestMode()) { @@ -16261,7 +16101,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":44,"../../InputTypes/Credentials.js":46,"../../deviceApiCalls/__generated__/deviceApiCalls.js":67,"./UIController.js":60}],59:[function(require,module,exports){ +},{"../../Form/matching.js":43,"../../InputTypes/Credentials.js":45,"../../deviceApiCalls/__generated__/deviceApiCalls.js":66,"./UIController.js":59}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16498,7 +16338,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":44,"./UIController.js":60}],60:[function(require,module,exports){ +},{"../../Form/matching.js":43,"./UIController.js":59}],59:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16582,7 +16422,7 @@ class UIController { } exports.UIController = UIController; -},{}],61:[function(require,module,exports){ +},{}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16599,16 +16439,16 @@ 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==`; -},{}],62:[function(require,module,exports){ +},{}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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}\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"; +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: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: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 :not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n: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}\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"; -},{}],63:[function(require,module,exports){ +},{}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17227,7 +17067,7 @@ function getActiveElement() { return innerActiveElement; } -},{"./Form/matching.js":44,"./constants.js":66,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],64:[function(require,module,exports){ +},{"./Form/matching.js":43,"./constants.js":65,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],63:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -17258,7 +17098,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":23,"./autofill-utils.js":63,"./requestIdleCallback.js":75}],65:[function(require,module,exports){ +},{"./DeviceInterface.js":22,"./autofill-utils.js":62,"./requestIdleCallback.js":74}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17275,10 +17115,6 @@ 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; @@ -17348,7 +17184,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],66:[function(require,module,exports){ +},{}],65:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17365,13 +17201,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],67:[function(require,module,exports){ +},{}],66:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = 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; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = 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; 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 */ @@ -17525,20 +17361,10 @@ exports.OpenManageCreditCardsCall = OpenManageCreditCardsCall; class OpenManageIdentitiesCall extends _deviceApi.DeviceApiCall { method = "openManageIdentities"; } -/** - * @extends {DeviceApiCall} - */ -exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; -class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { - method = "emailProtectionGetAlias"; - id = "emailProtectionGetAliasResponse"; - paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; - resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; -} /** * @extends {DeviceApiCall} */ -exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; +exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -17621,13 +17447,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":12,"./validators.zod.js":68}],68:[function(require,module,exports){ +},{"../../../packages/device-api":12,"./validators.zod.js":67}],67:[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.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; +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; var _zod = require("zod"); /* DO NOT EDIT, this file was generated by scripts/api-call-generator.js */ // Generated by ts-to-zod @@ -17685,11 +17511,6 @@ 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(), @@ -17743,6 +17564,10 @@ 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(), @@ -17774,10 +17599,6 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj email: _zod.z.boolean().optional(), credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).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(), @@ -17809,11 +17630,6 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o email: _zod.z.boolean().optional(), credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).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(), @@ -17848,7 +17664,7 @@ const storeFormDataSchema = exports.storeFormDataSchema = _zod.z.object({ }); const getAvailableInputTypesResultSchema = exports.getAvailableInputTypesResultSchema = _zod.z.object({ type: _zod.z.literal("getAvailableInputTypesResponse").optional(), - success: availableInputTypes1Schema, + success: availableInputTypesSchema, error: genericErrorSchema.optional() }); const getAutofillInitDataResponseSchema = exports.getAutofillInitDataResponseSchema = _zod.z.object({ @@ -17871,25 +17687,9 @@ 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() @@ -17927,14 +17727,28 @@ const emailProtectionRefreshPrivateAddressResultSchema = exports.emailProtection const runtimeConfigurationSchema = exports.runtimeConfigurationSchema = _zod.z.object({ contentScope: contentScopeSchema, userUnprotectedDomains: _zod.z.array(_zod.z.string()), - userPreferences: userPreferencesSchema, - availableInputTypes: availableInputTypesSchema.optional() + 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 }); 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() @@ -18001,11 +17815,6 @@ const apiSchema = exports.apiSchema = _zod.z.object({ openManagePasswords: _zod.z.record(_zod.z.unknown()).optional(), openManageCreditCards: _zod.z.record(_zod.z.unknown()).optional(), openManageIdentities: _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() @@ -18039,7 +17848,7 @@ const apiSchema = exports.apiSchema = _zod.z.object({ })).optional() }); -},{"zod":9}],69:[function(require,module,exports){ +},{"zod":9}],68:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18065,7 +17874,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":68}],70:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":67}],69:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18073,8 +17882,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.AndroidTransport = void 0; var _index = require("../../../packages/device-api/index.js"); -var _messaging = require("../../../packages/messaging/messaging.js"); -var _android = require("../../../packages/messaging/android.js"); +var _deviceApiCalls = require("../__generated__/deviceApiCalls.js"); class AndroidTransport extends _index.DeviceApiTransport { /** @type {GlobalConfig} */ config; @@ -18083,39 +17891,133 @@ class AndroidTransport extends _index.DeviceApiTransport { constructor(globalConfig) { super(); this.config = globalConfig; - 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); + 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'); + } + } } /** * @param {import("../../../packages/device-api").DeviceApiCall} deviceApiCall * @returns {Promise} */ async send(deviceApiCall) { - 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); + 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; + } } - } catch (e) { - if (e instanceof _messaging.MissingHandler) { - if (this.config.isDDGTestMode) { - console.log('MissingAndroidHandler error for:', deviceApiCall.method); + 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 { + 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'); } - 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,"../../../packages/messaging/android.js":15,"../../../packages/messaging/messaging.js":16}],71:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../__generated__/deviceApiCalls.js":66}],70:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18158,7 +18060,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":16}],72:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":15}],71:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18310,7 +18212,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":12,"../../Settings.js":51,"../../autofill-utils.js":63,"../__generated__/deviceApiCalls.js":67}],73:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../Settings.js":50,"../../autofill-utils.js":62,"../__generated__/deviceApiCalls.js":66}],72:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18354,7 +18256,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":70,"./apple.transport.js":71,"./extension.transport.js":72,"./windows.transport.js":74}],74:[function(require,module,exports){ +},{"./android.transport.js":69,"./apple.transport.js":70,"./extension.transport.js":71,"./windows.transport.js":73}],73:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18439,7 +18341,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":12}],75:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12}],74:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18482,4 +18384,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[64]); +},{}]},{},[63]); diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.css b/node_modules/@duckduckgo/autofill/dist/autofill.css index 35b838d04485..e712368f55c7 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill.css +++ b/node_modules/@duckduckgo/autofill/dist/autofill.css @@ -19,7 +19,7 @@ font-family: 'SF Pro Text', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; } -.wrapper:not(.top-autofill) .tooltip { +:not(.top-autofill) .tooltip { position: absolute; width: 300px; max-width: calc(100vw - 25px); @@ -50,7 +50,7 @@ .tooltip--data.tooltip--incontext-signup { width: 360px; } -.wrapper:not(.top-autofill) .tooltip--data { +:not(.top-autofill) .tooltip--data { top: 100%; left: 100%; border: 0.5px solid rgba(255, 255, 255, 0.2); @@ -58,11 +58,11 @@ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.32); } @media (prefers-color-scheme: dark) { - .wrapper:not(.top-autofill) .tooltip--data { + :not(.top-autofill) .tooltip--data { border: 1px solid rgba(255, 255, 255, 0.2); } } -.wrapper:not(.top-autofill) .tooltip--email { +:not(.top-autofill) .tooltip--email { top: calc(100% + 6px); right: calc(100% - 48px); padding: 8px; diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js index c3eb2099f700..637b93e6949d 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill.js @@ -425,130 +425,6 @@ 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 }); @@ -560,7 +436,6 @@ Object.defineProperty(exports, "WebkitMessagingConfig", { } }); var _webkit = require("./webkit.js"); -var _android = require("./android.js"); /** * @module Messaging * @@ -619,7 +494,7 @@ var _android = require("./android.js"); */ class Messaging { /** - * @param {WebkitMessagingConfig | AndroidMessagingConfig} config + * @param {WebkitMessagingConfig} config */ constructor(config) { this.transport = getTransport(config); @@ -691,7 +566,7 @@ class MessagingTransport { } /** - * @param {WebkitMessagingConfig | AndroidMessagingConfig} config + * @param {WebkitMessagingConfig} config * @returns {MessagingTransport} */ exports.MessagingTransport = MessagingTransport; @@ -699,9 +574,6 @@ 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'); } @@ -724,7 +596,7 @@ class MissingHandler extends Error { */ exports.MissingHandler = MissingHandler; -},{"./android.js":5,"./webkit.js":7}],7:[function(require,module,exports){ +},{"./webkit.js":6}],6:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1119,7 +991,7 @@ function captureGlobals() { }; } -},{"./messaging.js":6}],8:[function(require,module,exports){ +},{"./messaging.js":5}],7:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1250,7 +1122,7 @@ function _safeHostname(inputHostname) { } } -},{"./lib/apple.password.js":9,"./lib/constants.js":10,"./lib/rules-parser.js":11}],9:[function(require,module,exports){ +},{"./lib/apple.password.js":8,"./lib/constants.js":9,"./lib/rules-parser.js":10}],8:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1779,7 +1651,7 @@ class Password { } exports.Password = Password; -},{"./constants.js":10,"./rules-parser.js":11}],10:[function(require,module,exports){ +},{"./constants.js":9,"./rules-parser.js":10}],9:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -1799,7 +1671,7 @@ const constants = exports.constants = { DEFAULT_UNAMBIGUOUS_CHARS }; -},{}],11:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -2395,7 +2267,7 @@ function parsePasswordRules(input, formatRulesForMinifiedVersion) { return newPasswordRules; } -},{}],12:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ module.exports={ "163.com": { "password-rules": "minlength: 6; maxlength: 16;" @@ -3403,7 +3275,7 @@ module.exports={ "password-rules": "minlength: 8; maxlength: 32; max-consecutive: 6; required: lower; required: upper; required: digit;" } } -},{}],13:[function(require,module,exports){ +},{}],12:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3459,7 +3331,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":55,"./deviceApiCalls/transports/transports.js":63}],14:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./DeviceInterface/AndroidInterface.js":13,"./DeviceInterface/AppleDeviceInterface.js":14,"./DeviceInterface/AppleOverlayDeviceInterface.js":15,"./DeviceInterface/ExtensionInterface.js":16,"./DeviceInterface/WindowsInterface.js":18,"./DeviceInterface/WindowsOverlayDeviceInterface.js":19,"./Settings.js":40,"./config.js":54,"./deviceApiCalls/transports/transports.js":62}],13:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3479,35 +3351,25 @@ class AndroidInterface extends _InterfacePrototype.default { * @returns {Promise} */ async getAlias() { - // 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) { + const { + alias + } = await (0, _autofillUtils.sendAndWaitForAnswer)(async () => { + if (this.inContextSignup.isAvailable()) { + const { + isSignedIn + } = await this.deviceApi.request(new _deviceApiCalls.ShowInContextEmailProtectionSignupPromptCall(null)); // 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.settings.availableInputTypes) { - this.settings.setAvailableInputTypes({ - email: isSignedIn - }); + if (this.globalConfig.availableInputTypes) { + this.globalConfig.availableInputTypes.email = isSignedIn; } this.updateForStateChange(); this.onFinishedAutofill(); } - } - // 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; - } + return window.EmailInterface.showTooltip(); + }, 'getAliasResponse'); + return alias; } /** @@ -3522,9 +3384,14 @@ 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.settings.availableInputTypes?.email === 'boolean') { - return this.settings.availableInputTypes.email; + if (typeof this.globalConfig.availableInputTypes?.email === 'boolean') { + return this.globalConfig.availableInputTypes.email; } // ...on other domains we assume true because the script wouldn't exist otherwise @@ -3539,7 +3406,15 @@ class AndroidInterface extends _InterfacePrototype.default { * Settings page displays data of the logged in user data */ getUserData() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetUserDataCall({})); + let userData = null; + try { + userData = JSON.parse(window.EmailInterface.getUserData()); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } + return Promise.resolve(userData); } /** @@ -3547,13 +3422,25 @@ class AndroidInterface extends _InterfacePrototype.default { * Device capabilities determine which functionality is available to the user */ getEmailProtectionCapabilities() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionGetCapabilitiesCall({})); + let deviceCapabilities = null; + try { + deviceCapabilities = JSON.parse(window.EmailInterface.getDeviceCapabilities()); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } + return Promise.resolve(deviceCapabilities); } storeUserData(_ref) { let { - addUserData + addUserData: { + token, + userName, + cohort + } } = _ref; - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionStoreUserDataCall(addUserData)); + return window.EmailInterface.storeCredentials(token, userName, cohort); } /** @@ -3561,7 +3448,13 @@ class AndroidInterface extends _InterfacePrototype.default { * Provides functionality to log the user out */ removeUserData() { - return this.deviceApi.request(new _deviceApiCalls.EmailProtectionRemoveUserDataCall({})); + try { + return window.EmailInterface.removeCredentials(); + } catch (e) { + if (this.globalConfig.isDDGTestMode) { + console.error(e); + } + } } /** @@ -3586,7 +3479,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":35,"../UI/controllers/NativeUIController.js":48,"../autofill-utils.js":53,"../deviceApiCalls/__generated__/deviceApiCalls.js":57,"./InterfacePrototype.js":18}],15:[function(require,module,exports){ +},{"../InContextSignup.js":34,"../UI/controllers/NativeUIController.js":47,"../autofill-utils.js":52,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./InterfacePrototype.js":17}],14:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3818,20 +3711,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManagePasswords')); } - /** - * Opens the native UI for managing identities - */ - openManageIdentities() { - return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManageIdentities')); - } - - /** - * Opens the native UI for managing credit cards - */ - openManageCreditCards() { - return this.deviceApi.notify((0, _index.createNotification)('pmHandlerOpenManageCreditCards')); - } - /** * Gets a single identity obj once the user requests it * @param {IdentityObject['id']} id @@ -3941,7 +3820,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":46,"../UI/controllers/HTMLTooltipUIController.js":47,"../UI/controllers/NativeUIController.js":48,"../UI/controllers/OverlayUIController.js":49,"../autofill-utils.js":53,"../deviceApiCalls/__generated__/deviceApiCalls.js":57,"../deviceApiCalls/additionalDeviceApiCalls.js":59,"./InterfacePrototype.js":18}],16:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../Form/matching.js":33,"../InContextSignup.js":34,"../ThirdPartyProvider.js":41,"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../UI/controllers/NativeUIController.js":47,"../UI/controllers/OverlayUIController.js":48,"../autofill-utils.js":52,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"../deviceApiCalls/additionalDeviceApiCalls.js":58,"./InterfacePrototype.js":17}],15:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4056,7 +3935,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":47,"./AppleDeviceInterface.js":15,"./overlayApi.js":22}],17:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":46,"./AppleDeviceInterface.js":14,"./overlayApi.js":21}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4275,7 +4154,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":34,"../InContextSignup.js":35,"../UI/HTMLTooltip.js":46,"../UI/controllers/HTMLTooltipUIController.js":47,"../autofill-utils.js":53,"./InterfacePrototype.js":18}],18:[function(require,module,exports){ +},{"../Form/matching.js":33,"../InContextSignup.js":34,"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../autofill-utils.js":52,"./InterfacePrototype.js":17}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4867,19 +4746,11 @@ class InterfacePrototype { let userData; try { userData = await this.getUserData(); - } catch (e) { - if (this.isTestMode()) { - console.log('getUserData failed with', e); - } - } + } catch (e) {} let capabilities; try { capabilities = await this.getEmailProtectionCapabilities(); - } catch (e) { - if (this.isTestMode()) { - console.log('capabilities fetching failed with', e); - } - } + } catch (e) {} // Set up listener for web app actions if (this.globalConfig.isDDGDomain) { @@ -4935,13 +4806,6 @@ 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({ @@ -5109,7 +4973,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":2,"../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":48,"../autofill-utils.js":53,"../config.js":55,"../deviceApiCalls/__generated__/deviceApiCalls.js":57,"../deviceApiCalls/transports/transports.js":63,"./initFormSubmissionsApi.js":21}],19:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../EmailProtection.js":22,"../Form/formatters.js":26,"../Form/matching.js":33,"../InputTypes/Credentials.js":35,"../PasswordGenerator.js":38,"../Scanner.js":39,"../Settings.js":40,"../UI/controllers/NativeUIController.js":47,"../autofill-utils.js":52,"../config.js":54,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"../deviceApiCalls/transports/transports.js":62,"./initFormSubmissionsApi.js":20}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5189,14 +5053,14 @@ class WindowsInterface extends _InterfacePrototype.default { } default: { - if (this.isTestMode()) { + if (this.globalConfig.isDDGTestMode) { console.warn('unhandled response', resp); } } } return this._closeAutofillParent(); }).catch(e => { - if (this.isTestMode()) { + if (this.globalConfig.isDDGTestMode) { if (e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -5271,7 +5135,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":49,"../deviceApiCalls/__generated__/deviceApiCalls.js":57,"./InterfacePrototype.js":18}],20:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":48,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./InterfacePrototype.js":17}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5450,7 +5314,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":47,"../deviceApiCalls/__generated__/deviceApiCalls.js":57,"./InterfacePrototype.js":18,"./overlayApi.js":22}],21:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":46,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./InterfacePrototype.js":17,"./overlayApi.js":21}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5549,7 +5413,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":30,"../autofill-utils.js":53}],22:[function(require,module,exports){ +},{"../Form/label-util.js":29,"../autofill-utils.js":52}],21:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5607,7 +5471,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":57}],23:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":56}],22:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5642,7 +5506,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],24:[function(require,module,exports){ +},{}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5779,7 +5643,7 @@ class Form { } submitHandler() { let via = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown'; - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('Form.submitHandler via:', via, this); } if (this.submitHandlerExecuted) return; @@ -6486,7 +6350,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":53,"../constants.js":56,"./FormAnalyzer.js":25,"./formatters.js":27,"./inputStyles.js":28,"./inputTypeConfig.js":29,"./matching.js":34}],25:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":35,"../autofill-utils.js":52,"../constants.js":55,"./FormAnalyzer.js":24,"./formatters.js":26,"./inputStyles.js":27,"./inputTypeConfig.js":28,"./matching.js":33}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -6851,7 +6715,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":53,"../constants.js":56,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching.js":34}],26:[function(require,module,exports){ +},{"../autofill-utils.js":52,"../constants.js":55,"./matching-config/__generated__/compiled-matching-config.js":31,"./matching.js":33}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7416,7 +7280,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],27:[function(require,module,exports){ +},{}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7721,7 +7585,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"./countryNames.js":25,"./matching.js":33}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7812,7 +7676,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":29}],29:[function(require,module,exports){ +},{"./inputTypeConfig.js":28}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8064,7 +7928,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":36,"../InputTypes/CreditCard.js":37,"../InputTypes/Identity.js":38,"../UI/img/ddgPasswordIcon.js":51,"../constants.js":56,"./logo-svg.js":31,"./matching.js":34}],30:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":35,"../InputTypes/CreditCard.js":36,"../InputTypes/Identity.js":37,"../UI/img/ddgPasswordIcon.js":50,"../constants.js":55,"./logo-svg.js":30,"./matching.js":33}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8112,7 +7976,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":34}],31:[function(require,module,exports){ +},{"./matching.js":33}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8145,7 +8009,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],32:[function(require,module,exports){ +},{}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8598,7 +8462,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],33:[function(require,module,exports){ +},{}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8673,7 +8537,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":53,"./matching.js":34}],34:[function(require,module,exports){ +},{"../autofill-utils.js":52,"./matching.js":33}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9658,7 +9522,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":53,"../constants.js":56,"./label-util.js":30,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching-utils.js":33}],35:[function(require,module,exports){ +},{"../autofill-utils.js":52,"../constants.js":55,"./label-util.js":29,"./matching-config/__generated__/compiled-matching-config.js":31,"./matching-utils.js":32}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9790,7 +9654,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":53,"./deviceApiCalls/__generated__/deviceApiCalls.js":57}],36:[function(require,module,exports){ +},{"./autofill-utils.js":52,"./deviceApiCalls/__generated__/deviceApiCalls.js":56}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9937,7 +9801,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":53}],37:[function(require,module,exports){ +},{"../autofill-utils.js":52}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9960,7 +9824,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],38:[function(require,module,exports){ +},{}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10000,7 +9864,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":27}],39:[function(require,module,exports){ +},{"../Form/formatters.js":26}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10042,7 +9906,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":8,"../packages/password/rules.json":12}],40:[function(require,module,exports){ +},{"../packages/password/index.js":7,"../packages/password/rules.json":11}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10457,7 +10321,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":24,"./Form/matching.js":34,"./autofill-utils.js":53,"./constants.js":56,"./deviceApiCalls/__generated__/deviceApiCalls.js":57}],41:[function(require,module,exports){ +},{"./Form/Form.js":23,"./Form/matching.js":33,"./autofill-utils.js":52,"./constants.js":55,"./deviceApiCalls/__generated__/deviceApiCalls.js":56}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10572,11 +10436,6 @@ 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; } @@ -10592,9 +10451,6 @@ 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) { @@ -10637,22 +10493,15 @@ class Settings { * @param {{ * mainType: SupportedMainTypes * subtype: import('./Form/matching.js').SupportedSubTypes | "unknown" - * variant?: import('./Form/matching.js').SupportedVariants | "" * }} types * @returns {boolean} */ isTypeUnavailable(_ref) { let { mainType, - subtype, - variant + subtype } = _ref; if (mainType === 'unknown') return true; - - // Ensure password generation feature flag is respected - if (subtype === 'password' && variant === 'new') { - return !this.featureToggles.password_generation; - } if (!this.featureToggles[`inputType_${mainType}`] && subtype !== 'emailAddress') { return true; } @@ -10673,20 +10522,17 @@ class Settings { * @param {{ * mainType: SupportedMainTypes * subtype: import('./Form/matching.js').SupportedSubTypes | "unknown" - * variant?: import('./Form/matching.js').SupportedVariants | "" * }} types * @returns {Promise} */ async populateDataIfNeeded(_ref2) { let { mainType, - subtype, - variant + subtype } = _ref2; if (this.isTypeUnavailable({ mainType, - subtype, - variant + subtype })) return false; if (this.availableInputTypes?.[mainType] === undefined) { await this.populateData(); @@ -10714,8 +10560,7 @@ class Settings { } = _ref3; if (this.isTypeUnavailable({ mainType, - subtype, - variant + subtype })) return false; // If it's an email field and Email Protection is enabled, return true regardless of other options @@ -10831,7 +10676,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":2,"./autofill-utils.js":53,"./deviceApiCalls/__generated__/deviceApiCalls.js":57,"./deviceApiCalls/__generated__/validators.zod.js":58}],42:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./autofill-utils.js":52,"./deviceApiCalls/__generated__/deviceApiCalls.js":56,"./deviceApiCalls/__generated__/validators.zod.js":57}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10898,7 +10743,7 @@ class ThirdPartyProvider { this.device.scanner.forms.forEach(form => form.recategorizeAllInputs()); } } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: providerStatusUpdated error: ❌', e); } } @@ -10913,7 +10758,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -10921,7 +10766,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":2,"./Form/matching.js":34,"./deviceApiCalls/__generated__/deviceApiCalls.js":57,"./deviceApiCalls/__generated__/validators.zod.js":58}],43:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./Form/matching.js":33,"./deviceApiCalls/__generated__/deviceApiCalls.js":56,"./deviceApiCalls/__generated__/validators.zod.js":57}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11049,7 +10894,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":53,"./HTMLTooltip.js":46}],44:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":35,"../autofill-utils.js":52,"./HTMLTooltip.js":45}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11127,7 +10972,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":53,"./HTMLTooltip.js":46}],45:[function(require,module,exports){ +},{"../autofill-utils.js":52,"./HTMLTooltip.js":45}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11183,7 +11028,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":46}],46:[function(require,module,exports){ +},{"./HTMLTooltip.js":45}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11571,7 +11416,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":34,"../autofill-utils.js":53,"./styles/styles.js":52}],47:[function(require,module,exports){ +},{"../Form/matching.js":33,"../autofill-utils.js":52,"./styles/styles.js":51}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11928,7 +11773,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":29,"../../Form/matching.js":34,"../../autofill-utils.js":53,"../DataHTMLTooltip.js":43,"../EmailHTMLTooltip.js":44,"../EmailSignupHTMLTooltip.js":45,"../HTMLTooltip.js":46,"./UIController.js":50}],48:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":28,"../../Form/matching.js":33,"../../autofill-utils.js":52,"../DataHTMLTooltip.js":42,"../EmailHTMLTooltip.js":43,"../EmailSignupHTMLTooltip.js":44,"../HTMLTooltip.js":45,"./UIController.js":49}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12030,11 +11875,6 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } - case 'none': - { - // do nothing - break; - } default: { if (args.device.isTestMode()) { @@ -12095,7 +11935,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":34,"../../InputTypes/Credentials.js":36,"../../deviceApiCalls/__generated__/deviceApiCalls.js":57,"./UIController.js":50}],49:[function(require,module,exports){ +},{"../../Form/matching.js":33,"../../InputTypes/Credentials.js":35,"../../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./UIController.js":49}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12332,7 +12172,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":34,"./UIController.js":50}],50:[function(require,module,exports){ +},{"../../Form/matching.js":33,"./UIController.js":49}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12416,7 +12256,7 @@ class UIController { } exports.UIController = UIController; -},{}],51:[function(require,module,exports){ +},{}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12433,16 +12273,16 @@ 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==`; -},{}],52:[function(require,module,exports){ +},{}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 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}\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"; +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: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: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 :not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n: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}\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"; -},{}],53:[function(require,module,exports){ +},{}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13061,7 +12901,7 @@ function getActiveElement() { return innerActiveElement; } -},{"./Form/matching.js":34,"./constants.js":56,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],54:[function(require,module,exports){ +},{"./Form/matching.js":33,"./constants.js":55,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],53:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -13092,7 +12932,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":13,"./autofill-utils.js":53,"./requestIdleCallback.js":65}],55:[function(require,module,exports){ +},{"./DeviceInterface.js":12,"./autofill-utils.js":52,"./requestIdleCallback.js":64}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13109,10 +12949,6 @@ 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; @@ -13182,7 +13018,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],56:[function(require,module,exports){ +},{}],55:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13199,13 +13035,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],57:[function(require,module,exports){ +},{}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = 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; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = 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; 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 */ @@ -13359,20 +13195,10 @@ exports.OpenManageCreditCardsCall = OpenManageCreditCardsCall; class OpenManageIdentitiesCall extends _deviceApi.DeviceApiCall { method = "openManageIdentities"; } -/** - * @extends {DeviceApiCall} - */ -exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; -class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { - method = "emailProtectionGetAlias"; - id = "emailProtectionGetAliasResponse"; - paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; - resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; -} /** * @extends {DeviceApiCall} */ -exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; +exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -13455,13 +13281,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":2,"./validators.zod.js":58}],58:[function(require,module,exports){ +},{"../../../packages/device-api":2,"./validators.zod.js":57}],57:[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.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; +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; const sendJSPixelParamsSchema = exports.sendJSPixelParamsSchema = null; const addDebugFlagParamsSchema = exports.addDebugFlagParamsSchema = null; const getAutofillCredentialsParamsSchema = exports.getAutofillCredentialsParamsSchema = null; @@ -13471,7 +13297,6 @@ 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; @@ -13480,10 +13305,9 @@ const credentialsSchema = exports.credentialsSchema = null; const genericErrorSchema = exports.genericErrorSchema = null; const contentScopeSchema = exports.contentScopeSchema = null; const userPreferencesSchema = exports.userPreferencesSchema = null; -const availableInputTypesSchema = exports.availableInputTypesSchema = null; const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = null; +const availableInputTypesSchema = exports.availableInputTypesSchema = 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; @@ -13491,20 +13315,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; -},{}],59:[function(require,module,exports){ +},{}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13530,7 +13354,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":58}],60:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":57}],59:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13538,8 +13362,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.AndroidTransport = void 0; var _index = require("../../../packages/device-api/index.js"); -var _messaging = require("../../../packages/messaging/messaging.js"); -var _android = require("../../../packages/messaging/android.js"); +var _deviceApiCalls = require("../__generated__/deviceApiCalls.js"); class AndroidTransport extends _index.DeviceApiTransport { /** @type {GlobalConfig} */ config; @@ -13548,39 +13371,133 @@ class AndroidTransport extends _index.DeviceApiTransport { constructor(globalConfig) { super(); this.config = globalConfig; - 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); + 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'); + } + } } /** * @param {import("../../../packages/device-api").DeviceApiCall} deviceApiCall * @returns {Promise} */ async send(deviceApiCall) { - 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); + 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; + } } - } catch (e) { - if (e instanceof _messaging.MissingHandler) { - if (this.config.isDDGTestMode) { - console.log('MissingAndroidHandler error for:', deviceApiCall.method); + 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 { + 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'); } - 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,"../../../packages/messaging/android.js":5,"../../../packages/messaging/messaging.js":6}],61:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../__generated__/deviceApiCalls.js":56}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13623,7 +13540,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":6}],62:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":5}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13775,7 +13692,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":2,"../../Settings.js":41,"../../autofill-utils.js":53,"../__generated__/deviceApiCalls.js":57}],63:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../Settings.js":40,"../../autofill-utils.js":52,"../__generated__/deviceApiCalls.js":56}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13819,7 +13736,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":60,"./apple.transport.js":61,"./extension.transport.js":62,"./windows.transport.js":64}],64:[function(require,module,exports){ +},{"./android.transport.js":59,"./apple.transport.js":60,"./extension.transport.js":61,"./windows.transport.js":63}],63:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13904,7 +13821,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":2}],65:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13947,4 +13864,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[54]); +},{}]},{},[53]); diff --git a/package-lock.json b/package-lock.json index 4fb6813f69e1..02d819d0a504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@duckduckgo/autoconsent": "^10.6.1", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#11.0.1", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#10.2.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.13.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1708702034" @@ -65,7 +65,7 @@ "integrity": "sha512-d1AZePYIiheZt+RI9OV3s4LW2woNl4XSrQNqY0TYHfSessK1Y/a0aFpTXYdvPIH2JWw48jVS0XGRk1P1tSK+4w==" }, "node_modules/@duckduckgo/autofill": { - "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#6053999d6af384a716ab0ce7205dbab5d70ed1b3", + "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#6493e296934bf09277c03df45f11f4619711cb24", "hasInstallScript": true }, "node_modules/@duckduckgo/content-scope-scripts": { diff --git a/package.json b/package.json index d5fc1f6f0f65..e385f89508f2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@duckduckgo/autoconsent": "^10.6.1", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#11.0.1", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#10.2.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#5.13.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1708702034" From 6325573b6421facd9b3bae77ed3db3a474924e5d Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Fri, 10 May 2024 16:07:27 +0100 Subject: [PATCH 3/3] Revert "Add warning to password management screen for incompatible WebViews (#4406)" This reverts commit 8aad2b5f4812d5987b063cf0e8d5a9875536a118. --- .../management/AutofillSettingsViewModel.kt | 9 +------- .../viewing/AutofillManagementListMode.kt | 6 ----- ...fragment_autofill_management_list_mode.xml | 14 +---------- .../AutofillSettingsViewModelTest.kt | 23 ------------------- 4 files changed, 2 insertions(+), 50 deletions(-) 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 bf2b51c61093..1ddc47dbffe7 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 @@ -24,7 +24,6 @@ import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.statistics.pixels.Pixel 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 @@ -108,7 +107,6 @@ class AutofillSettingsViewModel @Inject constructor( private val duckAddressIdentifier: DuckAddressIdentifier, private val syncEngine: SyncEngine, private val neverSavedSiteRepository: NeverSavedSiteRepository, - private val capabilityChecker: InternalAutofillCapabilityChecker, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -362,11 +360,7 @@ class AutofillSettingsViewModel @Inject constructor( fun onViewCreated() { if (combineJob != null) return combineJob = viewModelScope.launch(dispatchers.io()) { - _viewState.value = _viewState.value.copy( - autofillEnabled = autofillStore.autofillEnabled, - webViewCompatible = capabilityChecker.webViewSupportsAutofill(), - ) - + _viewState.value = _viewState.value.copy(autofillEnabled = autofillStore.autofillEnabled) val allCredentials = autofillStore.getAllCredentials().distinctUntilChanged() val combined = allCredentials.combine(searchQueryFilter) { credentials, filter -> credentialListFilter.filter(credentials, filter) @@ -646,7 +640,6 @@ class AutofillSettingsViewModel @Inject constructor( val logins: List? = null, val credentialMode: CredentialMode? = null, val credentialSearchQuery: String = "", - val webViewCompatible: Boolean = true, ) /** 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 6bfff6264571..5701961a661d 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 @@ -220,12 +220,6 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill binding.credentialToggleGroup.gone() binding.logins.updateTopMargin(resources.getDimensionPixelSize(CommonR.dimen.keyline_4)) } - - if (state.webViewCompatible) { - binding.webViewUnsupportedWarningPanel.gone() - } else { - binding.webViewUnsupportedWarningPanel.show() - } } } } diff --git a/autofill/autofill-impl/src/main/res/layout/fragment_autofill_management_list_mode.xml b/autofill/autofill-impl/src/main/res/layout/fragment_autofill_management_list_mode.xml index cc60c01db729..f0de78b0d7bb 100644 --- a/autofill/autofill-impl/src/main/res/layout/fragment_autofill_management_list_mode.xml +++ b/autofill/autofill-impl/src/main/res/layout/fragment_autofill_management_list_mode.xml @@ -26,18 +26,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - - + app:layout_constraintTop_toTopOf="parent" />