From d1397dd0d607862ae69a109bea8e41d9e1df0138 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 14:50:58 +0200 Subject: [PATCH 1/9] Updated release notes and version number for new release - 5.216.2 --- 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 2ff217abe70e..684df20b0433 100644 --- a/app/version/version.properties +++ b/app/version/version.properties @@ -1 +1 @@ -VERSION=5.216.1 \ No newline at end of file +VERSION=5.216.2 \ No newline at end of file From 7d1ae8dd3a2ba9bb27fa9237a6ad0a16ce41edbe Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 14:51:37 +0200 Subject: [PATCH 2/9] Revert "Add new feature flag canIntegrateWebMessageBasedAutofillInWebView (#5111)" This reverts commit e2031f5d37842b3801e1cf66e5db37febfe4b4af. --- .../java/com/duckduckgo/autofill/api/AutofillFeature.kt | 4 ++-- .../autofill/impl/AutofillGlobalCapabilityChecker.kt | 2 +- .../com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt | 2 ++ .../AutofillGlobalCapabilityCheckerImplGlobalFeatureTest.kt | 2 +- .../duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt | 6 +++--- .../autofill/internal/AutofillInternalSettingsActivity.kt | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFeature.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFeature.kt index 2ac72749c2f5..c37e4a264cb0 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFeature.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFeature.kt @@ -33,11 +33,11 @@ interface AutofillFeature { /** * Kill switch for if we should inject Autofill javascript into the browser. * - * @return `true` when the remote config has the global "canIntegrateWebMessageBasedAutofillInWebView" autofill sub-feature flag enabled + * @return `true` when the remote config has the global "canIntegrateAutofillInWebView" autofill sub-feature flag enabled * If the remote feature is not present defaults to `true` */ @Toggle.DefaultValue(true) - fun canIntegrateWebMessageBasedAutofillInWebView(): Toggle + fun canIntegrateAutofillInWebView(): Toggle /** * @return `true` when the remote config has the global "canInjectCredentials" autofill sub-feature flag enabled diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityChecker.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityChecker.kt index 7cc0d1e15b43..644e10b2d405 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityChecker.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityChecker.kt @@ -55,7 +55,7 @@ class AutofillGlobalCapabilityCheckerImpl @Inject constructor( override suspend fun isAutofillEnabledByConfiguration(url: String): Boolean { return withContext(dispatcherProvider.io()) { val enabledAtTopLevel = isInternalTester() || isGlobalFeatureEnabled() - val canIntegrateAutofill = autofillFeature.canIntegrateWebMessageBasedAutofillInWebView().isEnabled() + val canIntegrateAutofill = autofillFeature.canIntegrateAutofillInWebView().isEnabled() enabledAtTopLevel && canIntegrateAutofill && !isAnException(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 e6a797b7aa79..6fbbaa955ac3 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 @@ -118,6 +118,8 @@ class AutofillWebMessageAttacherImpl @Inject constructor( private val safeWebMessageHandler: SafeWebMessageHandler, ) : AutofillWebMessageAttacher { + @SuppressLint("AddWebMessageListenerUsage") + // suppress AddWebMessageListenerUsage, we don't have access to DuckDuckGoWebView here. override suspend fun addListener( webView: WebView, listener: AutofillWebMessageListener, diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityCheckerImplGlobalFeatureTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityCheckerImplGlobalFeatureTest.kt index b3686be55213..20a4024798b6 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityCheckerImplGlobalFeatureTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillGlobalCapabilityCheckerImplGlobalFeatureTest.kt @@ -230,7 +230,7 @@ class AutofillGlobalCapabilityCheckerImplGlobalFeatureTest( private fun configureCanIntegrateAutofillSubfeature(isEnabled: Boolean) { val toggle: Toggle = mock() whenever(toggle.isEnabled()).thenReturn(isEnabled) - whenever(autofillFeature.canIntegrateWebMessageBasedAutofillInWebView()).thenReturn(toggle) + whenever(autofillFeature.canIntegrateAutofillInWebView()).thenReturn(toggle) } private fun configureIfUrlIsException(isException: Boolean) { 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 1b82496fa079..4881053b0588 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 @@ -95,7 +95,7 @@ class InlineBrowserAutofillTest { canSaveCredentials: Boolean = true, canGeneratePassword: Boolean = true, canAccessCredentialManagement: Boolean = true, - canIntegrateWebMessageBasedAutofillInWebView: Boolean = true, + canIntegrateAutofillInWebView: Boolean = true, deviceWebViewSupportsAutofill: Boolean = true, ): InlineBrowserAutofill { val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) @@ -104,10 +104,10 @@ class InlineBrowserAutofillTest { autofillFeature.canSaveCredentials().setRawStoredState(State(enable = canSaveCredentials)) autofillFeature.canGeneratePasswords().setRawStoredState(State(enable = canGeneratePassword)) autofillFeature.canAccessCredentialManagement().setRawStoredState(State(enable = canAccessCredentialManagement)) - autofillFeature.canIntegrateWebMessageBasedAutofillInWebView().setRawStoredState(State(enable = canIntegrateWebMessageBasedAutofillInWebView)) + autofillFeature.canIntegrateAutofillInWebView().setRawStoredState(State(enable = canIntegrateAutofillInWebView)) whenever(capabilityChecker.webViewSupportsAutofill()).thenReturn(deviceWebViewSupportsAutofill) - whenever(capabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(canInjectCredentials) + whenever(capabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(canIntegrateAutofillInWebView) return InlineBrowserAutofill( autofillCapabilityChecker = capabilityChecker, diff --git a/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt b/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt index 2047dbff7c27..308f6531e0a5 100644 --- a/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt +++ b/autofill/autofill-internal/src/main/java/com/duckduckgo/autofill/internal/AutofillInternalSettingsActivity.kt @@ -117,7 +117,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { val autofillEnabled = autofillFeature.self() val onByDefault = autofillFeature.onByDefault() val onForExistingUsers = autofillFeature.onForExistingUsers() - val canIntegrateAutofill = autofillFeature.canIntegrateWebMessageBasedAutofillInWebView() + val canIntegrateAutofill = autofillFeature.canIntegrateAutofillInWebView() val canSaveCredentials = autofillFeature.canSaveCredentials() val canInjectCredentials = autofillFeature.canInjectCredentials() val canGeneratePasswords = autofillFeature.canGeneratePasswords() From ce5b52bc4433ef68aaf4edfcde3759d956f3acbc Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 14:52:05 +0200 Subject: [PATCH 3/9] Revert "Use safeWebMessageHandler code for autofill integration" This reverts commit 676e4c1c986348d573d6e4ff01fd5bc54a284008. --- .../app/browser/DuckDuckGoWebView.kt | 2 +- .../browser/RealWebViewCapabilityChecker.kt | 4 +- .../app/browser/SafeWebMessageHandlerImpl.kt | 79 ------------------- .../autofill/impl/InlineBrowserAutofill.kt | 14 ++-- browser-api/build.gradle | 2 - .../app/browser/api/SafeWebMessageHandler.kt | 38 --------- 6 files changed, 9 insertions(+), 130 deletions(-) delete mode 100644 app/src/main/java/com/duckduckgo/app/browser/SafeWebMessageHandlerImpl.kt delete mode 100644 browser-api/src/main/java/com/duckduckgo/app/browser/api/SafeWebMessageHandler.kt diff --git a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt index 1c23c71ed22f..dc1fe3011316 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DuckDuckGoWebView.kt @@ -62,7 +62,7 @@ class DuckDuckGoWebView : WebView, NestedScrollingChild3 { private var nestedScrollHelper: NestedScrollingChildHelper = NestedScrollingChildHelper(this) private val helper = CoordinatorLayoutHelper() - var isDestroyed: Boolean = false + private var isDestroyed: Boolean = false var isSafeWebViewEnabled: Boolean = false constructor(context: Context) : this(context, null) diff --git a/app/src/main/java/com/duckduckgo/app/browser/RealWebViewCapabilityChecker.kt b/app/src/main/java/com/duckduckgo/app/browser/RealWebViewCapabilityChecker.kt index 4d2314a377fd..0e4e74e89521 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/RealWebViewCapabilityChecker.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/RealWebViewCapabilityChecker.kt @@ -24,12 +24,12 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.browser.api.WebViewVersionProvider import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.extensions.compareSemanticVersion -import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.FragmentScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject import kotlinx.coroutines.withContext -@ContributesBinding(AppScope::class) +@ContributesBinding(FragmentScope::class) class RealWebViewCapabilityChecker @Inject constructor( private val dispatchers: DispatcherProvider, private val webViewVersionProvider: WebViewVersionProvider, diff --git a/app/src/main/java/com/duckduckgo/app/browser/SafeWebMessageHandlerImpl.kt b/app/src/main/java/com/duckduckgo/app/browser/SafeWebMessageHandlerImpl.kt deleted file mode 100644 index 8189b1ae5b16..000000000000 --- a/app/src/main/java/com/duckduckgo/app/browser/SafeWebMessageHandlerImpl.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.app.browser - -import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.webkit.WebViewCompat -import androidx.webkit.WebViewCompat.WebMessageListener -import com.duckduckgo.app.browser.api.SafeWebMessageHandler -import com.duckduckgo.app.browser.api.WebViewCapabilityChecker -import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import javax.inject.Inject -import timber.log.Timber - -@SuppressLint("RequiresFeature", "AddWebMessageListenerUsage", "RemoveWebMessageListenerUsage") -@ContributesBinding(AppScope::class) -class SafeWebMessageHandlerImpl @Inject constructor( - private val webViewCapabilityChecker: WebViewCapabilityChecker, -) : SafeWebMessageHandler { - - override suspend fun addWebMessageListener( - webView: WebView, - jsObjectName: String, - allowedOriginRules: Set, - listener: WebMessageListener, - ): Boolean = runCatching { - if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed(webView)) { - WebViewCompat.addWebMessageListener(webView, jsObjectName, allowedOriginRules, listener) - true - } else { - false - } - }.getOrElse { exception -> - Timber.e(exception, "Error adding WebMessageListener: $jsObjectName") - false - } - - override suspend fun removeWebMessageListener( - webView: WebView, - jsObjectName: String, - ): Boolean = runCatching { - if (webViewCapabilityChecker.isSupported(WebViewCapability.WebMessageListener) && !isDestroyed(webView)) { - WebViewCompat.removeWebMessageListener(webView, jsObjectName) - true - } else { - false - } - }.getOrElse { exception -> - Timber.e(exception, "Error removing WebMessageListener: $jsObjectName") - false - } - - /** - * Can only check destroyed flag for DuckDuckGoWebView for now. If a normal WebView, assume not destroyed. - */ - private fun isDestroyed(webView: WebView): Boolean { - return if (webView is DuckDuckGoWebView) { - webView.isDestroyed - } else { - false - } - } -} 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 6fbbaa955ac3..58a5b65ab501 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt @@ -18,7 +18,7 @@ package com.duckduckgo.autofill.impl import android.annotation.SuppressLint import android.webkit.WebView -import com.duckduckgo.app.browser.api.SafeWebMessageHandler +import androidx.webkit.WebViewCompat import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.Callback @@ -90,7 +90,7 @@ class InlineBrowserAutofill @Inject constructor( } } - private suspend fun WebView.addWebMessageListener( + private fun WebView.addWebMessageListener( messageListener: AutofillWebMessageListener, autofillCallback: Callback, tabId: String, @@ -106,7 +106,7 @@ class InlineBrowserAutofill @Inject constructor( } interface AutofillWebMessageAttacher { - suspend fun addListener( + fun addListener( webView: WebView, listener: AutofillWebMessageListener, ) @@ -114,16 +114,14 @@ interface AutofillWebMessageAttacher { @SuppressLint("RequiresFeature") @ContributesBinding(FragmentScope::class) -class AutofillWebMessageAttacherImpl @Inject constructor( - private val safeWebMessageHandler: SafeWebMessageHandler, -) : AutofillWebMessageAttacher { +class AutofillWebMessageAttacherImpl @Inject constructor() : AutofillWebMessageAttacher { @SuppressLint("AddWebMessageListenerUsage") // suppress AddWebMessageListenerUsage, we don't have access to DuckDuckGoWebView here. - override suspend fun addListener( + override fun addListener( webView: WebView, listener: AutofillWebMessageListener, ) { - safeWebMessageHandler.addWebMessageListener(webView, listener.key, listener.origins, listener) + WebViewCompat.addWebMessageListener(webView, listener.key, listener.origins, listener) } } diff --git a/browser-api/build.gradle b/browser-api/build.gradle index 51bff42a8a35..dcb21130e7f7 100644 --- a/browser-api/build.gradle +++ b/browser-api/build.gradle @@ -35,8 +35,6 @@ dependencies { // LiveData implementation AndroidX.lifecycle.liveDataKtx - - implementation AndroidX.webkit } android { namespace 'com.duckduckgo.browser.api' diff --git a/browser-api/src/main/java/com/duckduckgo/app/browser/api/SafeWebMessageHandler.kt b/browser-api/src/main/java/com/duckduckgo/app/browser/api/SafeWebMessageHandler.kt deleted file mode 100644 index afed1da44986..000000000000 --- a/browser-api/src/main/java/com/duckduckgo/app/browser/api/SafeWebMessageHandler.kt +++ /dev/null @@ -1,38 +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.app.browser.api - -import android.webkit.WebView -import androidx.webkit.WebViewCompat.WebMessageListener - -/** - * Add and remove web message listeners to a WebView, guarded by extra checks to ensure WebView compatibility - */ -interface SafeWebMessageHandler { - - suspend fun addWebMessageListener( - webView: WebView, - jsObjectName: String, - allowedOriginRules: Set, - listener: WebMessageListener, - ): Boolean - - suspend fun removeWebMessageListener( - webView: WebView, - jsObjectName: String, - ): Boolean -} From 0e13eb2bd88b36f6d77ba6e988c02eb13a137afe Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 14:56:48 +0200 Subject: [PATCH 4/9] Revert "Re-enable autofill in iFrames (#5007)" This reverts commit a04393107dda0fc56e8dabb927b66b518a1bdfae. --- .github/workflows/e2e-nightly-autofill.yml | 2 +- .../app/browser/BrowserTabViewModelTest.kt | 62 +- .../app/browser/BrowserWebViewClientTest.kt | 10 + .../app/email/EmailInjectorJsTest.kt | 178 +++++ ...efaultEmailProtectionJavascriptInjector.kt | 50 ++ .../duckduckgo/app/browser/BrowserActivity.kt | 17 +- .../app/browser/BrowserTabFragment.kt | 307 ++++++--- .../app/browser/BrowserTabViewModel.kt | 33 +- .../app/browser/BrowserWebViewClient.kt | 3 + .../app/browser/commands/Command.kt | 10 +- .../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 ++ .../PasswordsScreenPromotionPlugin.kt | 2 - ...er.kt => AutofillCapabilityCheckerImpl.kt} | 49 +- .../impl/AutofillDisabledByConfigWarningUI.kt | 64 -- .../impl/AutofillJavascriptInjector.kt | 54 -- .../impl/AutofillJavascriptInterface.kt | 436 ++++++++++++ .../autofill/impl/AutofillSupportWarningUI.kt | 66 -- .../autofill/impl/InlineBrowserAutofill.kt | 110 +-- .../impl/RealDuckAddressLoginCreator.kt | 3 +- .../AutofillRuntimeConfigProvider.kt | 31 +- .../InlineBrowserAutofillConfigurator.kt | 67 ++ .../RuntimeConfigurationWriter.kt | 12 +- .../JavascriptCommunicationSupportImpl.kt | 73 -- .../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 | 349 ---------- ...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 +- .../management/AutofillSettingsViewModel.kt | 9 +- .../viewing/AutofillManagementListMode.kt | 6 +- ...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 - .../view_autofill_config_disabled_warning.xml | 28 - .../layout/view_autofill_warning_support.xml | 28 - .../src/main/res/values/donottranslate.xml | 2 - .../main/res/values/strings-autofill-impl.xml | 2 +- ...t => AutofillCapabilityCheckerImplTest.kt} | 19 - ...tofillStoredBackJavascriptInterfaceTest.kt | 440 ++++++++++++ .../impl/InlineBrowserAutofillTest.kt | 220 +++--- .../impl/RealDuckAddressLoginCreatorTest.kt | 6 +- .../InlineBrowserAutofillConfiguratorTest.kt | 81 +++ .../RealAutofillRuntimeConfigProviderTest.kt | 38 +- .../RealRuntimeConfigurationWriterTest.kt | 14 +- .../AutofillWebMessageListenerTest.kt | 74 -- .../TestWebMessageListenerCallback.kt | 75 -- .../WebMessageListenerGetAutofillDataTest.kt | 266 ------- .../WebMessageListenerStoreFormDataTest.kt | 158 ----- ...ltHandlerEmailProtectionChooseEmailTest.kt | 29 +- .../AutofillSettingsViewModelTest.kt | 38 +- .../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 | 650 ++++++++---------- .../@duckduckgo/autofill/dist/autofill.js | 601 ++++++++-------- package-lock.json | 11 +- package.json | 2 +- 98 files changed, 3606 insertions(+), 4172 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/AutofillDisabledByConfigWarningUI.kt 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 delete mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.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 delete mode 100644 autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml delete mode 100644 autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.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 63ac9b12d3d5..ba67fc39b604 100644 --- a/.github/workflows/e2e-nightly-autofill.yml +++ b/.github/workflows/e2e-nightly-autofill.yml @@ -60,7 +60,7 @@ jobs: api-key: ${{ secrets.MOBILE_DEV_API_KEY }} name: ${{ github.sha }} app-file: apk/release.apk - android-api-level: 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 a52af4ecfc46..7813af53cd22 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -174,7 +174,6 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.widget.ui.WidgetCapabilities import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillCapabilityChecker -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor @@ -3790,14 +3789,51 @@ class BrowserTabViewModelTest { assertTrue(browserViewState().isEmailSignedIn) } + @Test + fun whenEmailSignOutEventThenEmailSignEventCommandSent() = runTest { + emailStateFlow.emit(true) + emailStateFlow.emit(false) + + assertCommandIssuedTimes(2) + } + + @Test + fun whenEmailIsSignedInThenEmailSignEventCommandSent() = runTest { + emailStateFlow.emit(true) + + assertCommandIssued() + } + + @Test + fun whenConsumeAliasThenInjectAddressCommandSent() { + whenever(mockEmailManager.getAlias()).thenReturn("alias") + + testee.usePrivateDuckAddress("", "alias") + + assertCommandIssued { + assertEquals("alias", this.duckAddress) + } + } + + @Test + fun whenUseAddressThenInjectAddressCommandSent() { + whenever(mockEmailManager.getEmailAddress()).thenReturn("address") + + testee.usePersonalDuckAddress("", "address") + + assertCommandIssued { + assertEquals("address", this.duckAddress) + } + } + @Test fun whenShowEmailTooltipIfAddressExistsThenShowEmailTooltipCommandSent() { whenever(mockEmailManager.getEmailAddress()).thenReturn("address") - testee.showEmailProtectionChooseEmailPrompt(urlRequest()) + testee.showEmailProtectionChooseEmailPrompt() assertCommandIssued { - assertEquals("address", this.duckAddress) + assertEquals("address", this.address) } } @@ -3805,7 +3841,7 @@ class BrowserTabViewModelTest { fun whenShowEmailTooltipIfAddressDoesNotExistThenCommandNotSent() { whenever(mockEmailManager.getEmailAddress()).thenReturn(null) - testee.showEmailProtectionChooseEmailPrompt(urlRequest()) + testee.showEmailProtectionChooseEmailPrompt() assertCommandNotIssued() } @@ -4406,6 +4442,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) @@ -5992,8 +6038,6 @@ class BrowserTabViewModelTest { } } - private fun urlRequest() = AutofillWebMessageRequest("", "", "") - private fun givenLoginDetected(domain: String) = LoginDetected(authLoginDomain = "", forwardedToDomain = domain) private fun givenCurrentSite(domain: String): Site { @@ -6146,6 +6190,10 @@ class BrowserTabViewModelTest { fun anyUri(): Uri = any() 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 d4747c0d2f06..25fbe9a65401 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserWebViewClientTest.kt @@ -61,6 +61,7 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT import com.duckduckgo.autoconsent.api.Autoconsent +import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.browser.api.WebViewVersionProvider @@ -117,6 +118,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() @@ -154,6 +156,7 @@ class BrowserWebViewClientTest { thirdPartyCookieManager, TestScope(), coroutinesTestRule.testDispatcherProvider, + browserAutofillConfigurator, ampLinks, printInjector, internalTestUserChecker, @@ -367,6 +370,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..2a2cc540789b --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.email + +import android.webkit.WebView +import androidx.test.annotation.UiThreadTest +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.autofill.DefaultEmailProtectionJavascriptInjector +import com.duckduckgo.app.autofill.EmailProtectionJavascriptInjector +import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl +import com.duckduckgo.app.browser.R +import com.duckduckgo.autofill.api.Autofill +import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle +import java.io.BufferedReader +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.* + +class EmailInjectorJsTest { + + private val mockEmailManager: EmailManager = mock() + private val mockDispatcherProvider: DispatcherProvider = mock() + private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) + private val mockAutofill: Autofill = mock() + private val javascriptInjector: EmailProtectionJavascriptInjector = DefaultEmailProtectionJavascriptInjector() + + lateinit var testee: EmailInjectorJs + + @Before + fun setup() { + testee = + EmailInjectorJs( + mockEmailManager, + DuckDuckGoUrlDetectorImpl(), + mockDispatcherProvider, + autofillFeature, + javascriptInjector, + mockAutofill, + ) + whenever(mockAutofill.isAnException(any())).thenReturn(false) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenInjectAddressThenInjectJsCodeReplacingTheAlias() { + val address = "address" + val jsToEvaluate = getAliasJsToEvaluate().replace("%s", address) + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + autofillFeature.self().setEnabled(Toggle.State(enable = true)) + + testee.injectAddressInEmailField(webView, address, "https://example.com") + + verify(webView).evaluateJavascript(jsToEvaluate, null) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenInjectAddressAndFeatureIsDisabledThenJsCodeNotInjected() { + autofillFeature.self().setEnabled(Toggle.State(enable = true)) + + val address = "address" + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.injectAddressInEmailField(webView, address, "https://example.com") + + verify(webView, never()).evaluateJavascript(any(), any()) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenInjectAddressAndUrlIsAnExceptionThenJsCodeNotInjected() { + whenever(mockAutofill.isAnException(any())).thenReturn(true) + + val address = "address" + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.injectAddressInEmailField(webView, address, "https://example.com") + + verify(webView, never()).evaluateJavascript(any(), any()) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsSignedInThenDoNotEvaluateJsCode() { + whenever(mockEmailManager.isSignedIn()).thenReturn(true) + val jsToEvaluate = getNotifySignOutJsToEvaluate() + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.notifyWebAppSignEvent(webView, "https://example.com") + + verify(webView, never()).evaluateJavascript(jsToEvaluate, null) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { + whenever(mockEmailManager.isSignedIn()).thenReturn(false) + val jsToEvaluate = getNotifySignOutJsToEvaluate() + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.notifyWebAppSignEvent(webView, "https://example.com") + + verify(webView, never()).evaluateJavascript(jsToEvaluate, null) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsDisabledAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { + whenever(mockEmailManager.isSignedIn()).thenReturn(false) + autofillFeature.self().setEnabled(Toggle.State(enable = false)) + + val jsToEvaluate = getNotifySignOutJsToEvaluate() + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.notifyWebAppSignEvent(webView, "https://duckduckgo.com/email") + + verify(webView, never()).evaluateJavascript(jsToEvaluate, null) + } + + @UiThreadTest + @Test + @SdkSuppress(minSdkVersion = 24) + fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsEnabledAndEmailIsNotSignedInThenEvaluateJsCode() { + whenever(mockEmailManager.isSignedIn()).thenReturn(false) + autofillFeature.self().setEnabled(Toggle.State(enable = true)) + + val jsToEvaluate = getNotifySignOutJsToEvaluate() + val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) + + testee.notifyWebAppSignEvent(webView, "https://duckduckgo.com/email") + + verify(webView).evaluateJavascript(jsToEvaluate, null) + } + + private fun getAliasJsToEvaluate(): String { + val js = InstrumentationRegistry.getInstrumentation().targetContext.resources.openRawResource(R.raw.inject_alias) + .bufferedReader() + .use { it.readText() } + return "javascript:$js" + } + + private fun getNotifySignOutJsToEvaluate(): String { + val js = + InstrumentationRegistry.getInstrumentation().targetContext.resources.openRawResource(R.raw.signout_autofill) + .bufferedReader() + .use { it.readText() } + return "javascript:$js" + } + + private fun readResource(resourceName: String): BufferedReader? { + return javaClass.classLoader?.getResource(resourceName)?.openStream()?.bufferedReader() + } +} diff --git a/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt b/app/src/main/java/com/duckduckgo/app/autofill/DefaultEmailProtectionJavascriptInjector.kt 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 3981de8bba3f..334cd0e784b1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -333,17 +333,14 @@ open class BrowserActivity : DuckDuckGoActivity() { Toast.makeText(applicationContext, R.string.fireDataCleared, Toast.LENGTH_LONG).show() } - 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") @@ -743,9 +740,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 6c80e0fd6232..793f2b9b9883 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -215,12 +215,12 @@ import com.duckduckgo.app.widget.AddWidgetLauncher import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autoconsent.api.Autoconsent import com.duckduckgo.autoconsent.api.AutoconsentCallback +import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenDirectlyViewCredentialsParams import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreenShowSuggestionsForSiteParams import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.Callback import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory @@ -230,8 +230,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 @@ -239,9 +240,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.WebViewVersionProvider import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.common.ui.DuckDuckGoActivity @@ -348,7 +350,8 @@ class BrowserTabFragment : TrackersAnimatorListener, DownloadConfirmationDialogListener, SitePermissionsGrantedListener, - AutofillEventListener { + AutofillEventListener, + EmailProtectionUserPromptListener { private val supervisorJob = SupervisorJob() @@ -418,6 +421,9 @@ class BrowserTabFragment : @Inject lateinit var thirdPartyCookieManager: ThirdPartyCookieManager + @Inject + lateinit var emailInjector: EmailInjector + @Inject lateinit var browserAutofill: BrowserAutofill @@ -467,6 +473,9 @@ class BrowserTabFragment : @Inject lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory + @Inject + lateinit var duckAddressInjectedResultHandler: DuckAddressLoginCreator + @Inject lateinit var existingCredentialMatchDetector: ExistingCredentialMatchDetector @@ -479,6 +488,9 @@ class BrowserTabFragment : @Inject lateinit var autoconsent: Autoconsent + @Inject + lateinit var autofillCapabilityChecker: AutofillCapabilityChecker + @Inject lateinit var sitePermissionsDialogLauncher: SitePermissionsDialogLauncher @@ -514,9 +526,6 @@ class BrowserTabFragment : @Inject lateinit var clientBrandHintProvider: ClientBrandHintProvider - @Inject - lateinit var autofillMessagePoster: AutofillMessagePoster - @Inject lateinit var subscriptions: Subscriptions @@ -555,7 +564,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 @@ -644,13 +653,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 -> { @@ -660,12 +669,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()) @@ -735,17 +738,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, ) { @@ -753,32 +756,20 @@ class BrowserTabFragment : delay(KEYBOARD_DELAY) 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 @@ -789,8 +780,6 @@ class BrowserTabFragment : return } - val currentUrl = autofillWebMessageRequest.requestOrigin - val matchType = existingCredentialMatchDetector.determine(currentUrl, username, password) Timber.v("MatchType is %s", matchType.javaClass.simpleName) @@ -800,30 +789,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, ) { @@ -832,12 +821,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) } } @@ -1353,8 +1342,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) { @@ -1365,6 +1382,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() @@ -1561,8 +1589,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(it.privacyProtectionEnabled) is Command.EditWithSelectedQuery -> { omnibar.omnibarTextInput.setText(it.query) @@ -1571,6 +1607,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) @@ -1704,11 +1743,6 @@ class BrowserTabFragment : } } - private fun onPageChanged() { - browserAutofill.notifyPageChanged() - hideDialogWithTag(CredentialAutofillPickerDialog.TAG) - } - private fun extractUrlFromAmpLink(initialUrl: String) { context?.let { val client = urlExtractingWebViewClient.get() @@ -1729,6 +1763,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) @@ -2389,6 +2452,11 @@ class BrowserTabFragment : it.setFindListener(this) loginDetector.addLoginDetection(it) { viewModel.loginDetected() } + emailInjector.addJsInterface( + it, + onSignedInEmailProtectionPromptShown = { viewModel.showEmailProtectionChooseEmailPrompt() }, + onInContextEmailProtectionSignupPromptShown = { showNativeInContextEmailProtectionSignupPrompt() }, + ) configureWebViewForBlobDownload(it) configureWebViewForAutofill(it) printInjector.addJsInterface(it) { viewModel.printFromWebView() } @@ -2552,50 +2620,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) } @@ -3100,6 +3209,7 @@ class BrowserTabFragment : if (::popupMenu.isInitialized) popupMenu.dismiss() loginDetectionDialog?.dismiss() automaticFireproofDialog?.dismiss() + browserAutofill.removeJsInterface() destroyWebView() super.onDestroy() } @@ -3334,15 +3444,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, ) @@ -3350,28 +3457,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 6b4267d10cc0..fbf303ba0edb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -82,6 +82,7 @@ import com.duckduckgo.app.browser.commands.Command.AskToDisableLoginDetection import com.duckduckgo.app.browser.commands.Command.AskToFireproofWebsite import com.duckduckgo.app.browser.commands.Command.AutocompleteItemRemoved import com.duckduckgo.app.browser.commands.Command.BrokenSiteFeedback +import com.duckduckgo.app.browser.commands.Command.CancelIncomingAutofillRequest import com.duckduckgo.app.browser.commands.Command.CheckSystemLocationPermission import com.duckduckgo.app.browser.commands.Command.ChildTabClosed import com.duckduckgo.app.browser.commands.Command.ConvertBlobToDataUri @@ -94,6 +95,7 @@ import com.duckduckgo.app.browser.commands.Command.DialNumber import com.duckduckgo.app.browser.commands.Command.DismissFindInPage import com.duckduckgo.app.browser.commands.Command.DownloadImage import com.duckduckgo.app.browser.commands.Command.EditWithSelectedQuery +import com.duckduckgo.app.browser.commands.Command.EmailSignEvent import com.duckduckgo.app.browser.commands.Command.ExtractUrlFromCloakedAmpLink import com.duckduckgo.app.browser.commands.Command.FindInPageCommand import com.duckduckgo.app.browser.commands.Command.GenerateWebViewPreviewImage @@ -102,6 +104,7 @@ import com.duckduckgo.app.browser.commands.Command.HideKeyboard import com.duckduckgo.app.browser.commands.Command.HideOnboardingDaxDialog import com.duckduckgo.app.browser.commands.Command.HideSSLError import com.duckduckgo.app.browser.commands.Command.HideWebContent +import com.duckduckgo.app.browser.commands.Command.InjectEmailAddress import com.duckduckgo.app.browser.commands.Command.LaunchAddWidget import com.duckduckgo.app.browser.commands.Command.LaunchAutofillSettings import com.duckduckgo.app.browser.commands.Command.LaunchNewTab @@ -112,7 +115,6 @@ import com.duckduckgo.app.browser.commands.Command.OpenAppLink import com.duckduckgo.app.browser.commands.Command.OpenInNewBackgroundTab import com.duckduckgo.app.browser.commands.Command.OpenInNewTab import com.duckduckgo.app.browser.commands.Command.OpenMessageInNewTab -import com.duckduckgo.app.browser.commands.Command.PageChanged import com.duckduckgo.app.browser.commands.Command.PrintLink import com.duckduckgo.app.browser.commands.Command.RefreshUserAgent import com.duckduckgo.app.browser.commands.Command.RequestFileDownload @@ -241,7 +243,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 @@ -592,6 +593,7 @@ class BrowserTabViewModel @Inject constructor( emailManager.signedInFlow().onEach { isSignedIn -> browserViewState.value = currentBrowserViewState().copy(isEmailSignedIn = isSignedIn) + command.value = EmailSignEvent }.launchIn(viewModelScope) observeAccessibilitySettings() @@ -1456,7 +1458,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) @@ -3021,9 +3023,9 @@ class BrowserTabViewModel @Inject constructor( }.getOrNull() ?: return false } - fun showEmailProtectionChooseEmailPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { + fun showEmailProtectionChooseEmailPrompt() { emailManager.getEmailAddress()?.let { - command.postValue(ShowEmailProtectionChooseEmailPrompt(it, autofillWebMessageRequest)) + command.postValue(ShowEmailProtectionChooseEmailPrompt(it)) } } @@ -3041,6 +3043,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) } @@ -3164,6 +3183,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 572a606c7280..f6a4f14b482d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -63,6 +63,7 @@ import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LOADING_BAR_EXPERIMENT import com.duckduckgo.autoconsent.api.Autoconsent +import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.browser.api.JsInjectorPlugin import com.duckduckgo.common.utils.CurrentTimeProvider @@ -96,6 +97,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, @@ -360,6 +362,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 a07a513151ef..451e6a4f7ef8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/commands/Command.kt @@ -37,7 +37,6 @@ import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.browser.api.brokensite.BrokenSiteData import com.duckduckgo.js.messaging.api.JsCallbackData @@ -193,16 +192,19 @@ 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() class CancelIncomingAutofillRequest(val url: String) : Command() data class LaunchAutofillSettings(val privacyProtectionEnabled: Boolean) : Command() class EditWithSelectedQuery(val query: String) : Command() class ShowBackNavigationHistory(val history: List) : Command() + object EmailSignEvent : Command() class ShowSitePermissionsDialog( val permissionsToRequest: SitePermissions, val request: PermissionRequest, diff --git a/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt b/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt 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-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt index 7b59482c1424..f004da904785 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/promotion/PasswordsScreenPromotionPlugin.kt @@ -41,8 +41,6 @@ interface PasswordsScreenPromotionPlugin { } companion object { - const val PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING = 50 - const val PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING = 60 const val PRIORITY_KEY_SURVEY = 100 const val PRIORITY_KEY_SYNC_PROMO = 200 } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/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 6ac440fa2639..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. - */ - suspend fun webViewSupportsAutofill(): Boolean - - /** - * Whether autofill can inject credentials into a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canInjectCredentialsToWebView(url: String): Boolean - - /** - * Whether autofill can save credentials from a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canSaveCredentialsFromWebView(url: String): Boolean - - /** - * Whether autofill can generate a password into a WebView for the given page. - * @param url The URL of the webpage to check. - */ - suspend fun canGeneratePasswordFromWebView(url: String): Boolean - - /** - * Whether autofill is configured to be enabled. This is a configuration value, not a user preference. - */ - suspend fun isAutofillEnabledByConfiguration(url: String): Boolean -} - @ContributesBinding(AppScope::class) class AutofillCapabilityCheckerImpl @Inject constructor( private val autofillFeature: AutofillFeature, private val internalTestUserChecker: InternalTestUserChecker, private val autofillGlobalCapabilityChecker: AutofillGlobalCapabilityChecker, - private val javascriptCommunicationSupport: JavascriptCommunicationSupport, private val dispatcherProvider: DispatcherProvider, -) : 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 suspend 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/AutofillDisabledByConfigWarningUI.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillDisabledByConfigWarningUI.kt deleted file mode 100644 index 90c90256373f..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillDisabledByConfigWarningUI.kt +++ /dev/null @@ -1,64 +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.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import androidx.core.view.isVisible -import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.anvil.annotations.PriorityKey -import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin -import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin.Companion.PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING -import com.duckduckgo.autofill.impl.databinding.ViewAutofillConfigDisabledWarningBinding -import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.di.scopes.ViewScope -import com.squareup.anvil.annotations.ContributesMultibinding -import dagger.android.support.AndroidSupportInjection -import javax.inject.Inject - -@ContributesMultibinding(scope = AppScope::class) -@PriorityKey(PRIORITY_KEY_AUTOFILL_DISABLED_CONFIG_WARNING) -class AutofillDisabledByConfigWarningUI @Inject constructor( - private val internalAutofillCapabilityChecker: InternalAutofillCapabilityChecker, -) : PasswordsScreenPromotionPlugin { - - override suspend fun getView(context: Context, numberSavedPasswords: Int): View? { - val autofillConfigEnabled = internalAutofillCapabilityChecker.isAutofillEnabledByConfiguration("") - if (autofillConfigEnabled) return null - - return AutofillDisabledByConfigWarningView(context) - } -} - -@InjectWith(ViewScope::class) -class AutofillDisabledByConfigWarningView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : FrameLayout(context, attrs, defStyle) { - - private val binding: ViewAutofillConfigDisabledWarningBinding by viewBinding() - - override fun onAttachedToWindow() { - AndroidSupportInjection.inject(this) - super.onAttachedToWindow() - binding.webViewUnsupportedWarningPanel.isVisible = true - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillJavascriptInjector.kt 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/AutofillSupportWarningUI.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.kt deleted file mode 100644 index 44e7ea373467..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/AutofillSupportWarningUI.kt +++ /dev/null @@ -1,66 +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.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.FrameLayout -import androidx.core.view.isVisible -import com.duckduckgo.anvil.annotations.InjectWith -import com.duckduckgo.anvil.annotations.PriorityKey -import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin -import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin.Companion.PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING -import com.duckduckgo.autofill.impl.databinding.ViewAutofillWarningSupportBinding -import com.duckduckgo.autofill.impl.ui.credential.management.survey.AutofillSurvey -import com.duckduckgo.common.ui.viewbinding.viewBinding -import com.duckduckgo.di.scopes.AppScope -import com.duckduckgo.di.scopes.ViewScope -import com.squareup.anvil.annotations.ContributesMultibinding -import dagger.android.support.AndroidSupportInjection -import javax.inject.Inject - -@ContributesMultibinding(scope = AppScope::class) -@PriorityKey(PRIORITY_KEY_AUTOFILL_SUPPORT_WARNING) -class AutofillSupportWarningUI @Inject constructor( - private val internalAutofillCapabilityChecker: InternalAutofillCapabilityChecker, - private val autofillSurvey: AutofillSurvey, -) : PasswordsScreenPromotionPlugin { - - override suspend fun getView(context: Context, numberSavedPasswords: Int): View? { - val autofillSupported = internalAutofillCapabilityChecker.webViewSupportsAutofill() - if (autofillSupported) return null - - return AutofillSupportWarningView(context) - } -} - -@InjectWith(ViewScope::class) -class AutofillSupportWarningView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0, -) : FrameLayout(context, attrs, defStyle) { - - private val binding: ViewAutofillWarningSupportBinding by viewBinding() - - override fun onAttachedToWindow() { - AndroidSupportInjection.inject(this) - super.onAttachedToWindow() - binding.webViewUnsupportedWarningPanel.isVisible = true - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/InlineBrowserAutofill.kt index 58a5b65ab501..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,112 +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 - } - - if (!autofillCapabilityChecker.canInjectCredentialsToWebView("")) { - Timber.w("Autofill injection on WebView is not supported; 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 { - - @SuppressLint("AddWebMessageListenerUsage") - // suppress AddWebMessageListenerUsage, we don't have access to DuckDuckGoWebView here. - override fun addListener( - webView: WebView, - listener: AutofillWebMessageListener, - ) { - WebViewCompat.addWebMessageListener(webView, listener.key, listener.origins, listener) + 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 72d7d00ac5cc..75fa4a587438 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,10 +16,10 @@ package com.duckduckgo.autofill.impl.configuration +import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials @@ -31,7 +31,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) @@ -39,14 +42,14 @@ 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 autofillFeature: AutofillFeature, private val shareableCredentials: ShareableCredentials, private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules, private val neverSavedSiteRepository: NeverSavedSiteRepository, ) : AutofillRuntimeConfigProvider { - override suspend fun getRuntimeConfiguration( + rawJs: String, url: String?, ): String { Timber.v("BrowserAutofill: getRuntimeConfiguration called") @@ -63,17 +66,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 { @@ -83,7 +80,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 23277813b7e6..14a4d073c304 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/RuntimeConfigurationWriter.kt @@ -60,7 +60,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateContentScope(): String { return """ - "contentScope" : { + contentScope = { "features": { "autofill": { "state": "enabled", @@ -68,7 +68,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run } }, "unprotectedTemporary": [] - } + }; """.trimIndent() } @@ -77,7 +77,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run */ override fun generateUserUnprotectedDomains(): String { return """ - "userUnprotectedDomains" : [] + userUnprotectedDomains = []; """.trimIndent() } @@ -90,7 +90,7 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run unknownUsernameCategorization: Boolean, ): String { return """ - "userPreferences" : { + userPreferences = { "debug": false, "platform": { "name": "android" @@ -107,12 +107,12 @@ class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : Run "credentials_saving": $credentialSaving, "inlineIcon_credentials": $showInlineKeyIcon, "emailProtection_incontext_signup": $showInContextEmailProtectionSignup, - "unknown_username_categorization": $unknownUsernameCategorization + "unknown_username_categorization": $unknownUsernameCategorization, } } } } - } + }; """.trimIndent() } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.kt deleted file mode 100644 index c1f686243ad2..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/JavascriptCommunicationSupportImpl.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 - -import androidx.webkit.WebViewFeature -import com.duckduckgo.browser.api.WebViewVersionProvider -import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.common.utils.extensions.compareSemanticVersion -import com.duckduckgo.di.scopes.AppScope -import com.squareup.anvil.annotations.ContributesBinding -import dagger.SingleInstanceIn -import javax.inject.Inject -import kotlinx.coroutines.withContext -import timber.log.Timber - -interface JavascriptCommunicationSupport { - suspend fun supportsModernIntegration(): Boolean -} - -@ContributesBinding(AppScope::class) -@SingleInstanceIn(AppScope::class) -class JavascriptCommunicationSupportImpl @Inject constructor( - private val webViewVersionProvider: WebViewVersionProvider, - private val dispatcherProvider: DispatcherProvider, -) : JavascriptCommunicationSupport { - - override suspend fun supportsModernIntegration(): Boolean = isWebMessageListenerSupported() && isModernSupportAvailable - - private val isModernSupportAvailable by lazy { - autofillRequiredFeatures.forEach { requiredFeature -> - if (!WebViewFeature.isFeatureSupported(requiredFeature)) { - Timber.i("Modern integration is not supported because feature %s is not supported", requiredFeature) - return@lazy false - } - } - - return@lazy true - } - - private suspend fun isWebMessageListenerSupported(): Boolean { - return withContext(dispatcherProvider.io()) { - webViewVersionProvider.getFullVersion() - .compareSemanticVersion(WEB_MESSAGE_LISTENER_WEBVIEW_VERSION)?.let { it >= 0 } ?: false - } && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) - } - - companion object { - private const val WEB_MESSAGE_LISTENER_WEBVIEW_VERSION = "126.0.6478.40" - - /** - * We need all of these to be supported in order to use autofill - */ - private val autofillRequiredFeatures = listOf( - WebViewFeature.DOCUMENT_START_SCRIPT, - WebViewFeature.WEB_MESSAGE_LISTENER, - WebViewFeature.POST_WEB_MESSAGE, - ) - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListener.kt 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 3bedfd1b7bae..de1f320f8e45 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/di/AutofillModule.kt @@ -25,7 +25,6 @@ import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin import com.duckduckgo.autofill.api.InternalTestUserChecker import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin -import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizer import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher import com.duckduckgo.autofill.impl.urlmatcher.AutofillUrlMatcher @@ -160,6 +159,3 @@ interface UnusedAutofillResultPlugin @ContributesPluginPoint(scope = ActivityScope::class, boundType = PasswordsScreenPromotionPlugin::class) private interface PasswordsScreenPromotionPluginPoint - -@ContributesPluginPoint(scope = AppScope::class, boundType = AutofillWebMessageListener::class) -interface UnusedAutofillWebMessageListener diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/EmailProtectionChooseEmailFragment.kt index 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 177ad636e030..6b9e1e58cf8b 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,35 +16,30 @@ 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.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -52,13 +47,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, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { @@ -72,72 +66,52 @@ 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) + onSelectedToUsePersonalAddress(originalUrl, autofillCallback) notifyAutofillListenersDuckAddressFilled() } UsePrivateAliasAddress -> { - onSelectedToUsePrivateAlias(autofillWebMessageRequest, tabId) + onSelectedToUsePrivateAlias(originalUrl, autofillCallback) notifyAutofillListenersDuckAddressFilled() } - DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection(autofillWebMessageRequest) + 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) } @@ -156,6 +130,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 5076f88c9cf6..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupFragment.kt +++ /dev/null @@ -1,349 +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", "AddWebMessageListenerUsage") - // suppress AddWebMessageListenerUsage, we are not using DuckDuckGo WebView here. - private fun configureWebView() { - binding.webView.let { - it.webChromeClient = EmailProtectionInContextSignUpWebChromeClient(this) - - it.settings.apply { - userAgentString = userAgentProvider.userAgent() - javaScriptEnabled = true - domStorageEnabled = true - loadWithOverviewMode = true - useWideViewPort = true - builtInZoomControls = true - displayZoomControls = false - mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE - setSupportMultipleWindows(true) - databaseEnabled = false - setSupportZoom(true) - } - - autofillConfigurationJob += lifecycleScope.launch(dispatchers.main()) { - if (!autofillCapabilityChecker.webViewSupportsAutofill()) { - activity?.finish() - return@launch - } - - webMessageListener.callback = this@EmailProtectionInContextSignupFragment - WebViewCompat.addWebMessageListener(it, webMessageListener.key, webMessageListener.origins, webMessageListener) - - browserAutofill.addJsInterface( - webView = it, - tabId = "", - autofillCallback = noOpCallback, - ) - } - } - } - - companion object { - private const val STARTING_URL = "https://duckduckgo.com/email/start-incontext" - } - - override fun onPageFinished(url: String) { - viewModel.onPageFinished(url) - } - - private fun getPreviousWebPageUrl(): String? { - val webHistory = binding.webView.copyBackForwardList() - val currentIndex = webHistory.currentIndex - if (currentIndex < 0) return null - val previousIndex = currentIndex - 1 - if (previousIndex < 0) return null - return webHistory.getItemAtIndex(previousIndex)?.url - } - - private fun getToolbar() = (activity as EmailProtectionInContextSignupActivity).binding.includeToolbar.toolbar - - override suspend fun closeNativeInContextEmailProtectionSignup() { - closeActivityAsSuccessfulSignup() - } - - private val noOpCallback = object : Callback { - override suspend fun onCredentialsAvailableToInject( - autofillWebMessageRequest: AutofillWebMessageRequest, - credentials: List, - triggerType: LoginTriggerType, - ) { - } - - override suspend fun onCredentialsAvailableToSave( - autofillWebMessageRequest: AutofillWebMessageRequest, - credentials: LoginCredentials, - ) { - } - - override suspend fun onGeneratedPasswordAvailableToUse( - autofillWebMessageRequest: AutofillWebMessageRequest, - username: String?, - generatedPassword: String, - ) { - } - - override fun showNativeChooseEmailAddressPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { - } - - override fun showNativeInContextEmailProtectionSignupPrompt(autofillWebMessageRequest: AutofillWebMessageRequest) { - } - - override fun onCredentialsSaved(savedCredentials: LoginCredentials) { - } - } -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/EmailProtectionInContextSignupViewModel.kt index 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/management/AutofillSettingsViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt index f7b3c3869786..151aed68cee7 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt @@ -33,7 +33,6 @@ import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Sync import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Unknown import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration @@ -134,7 +133,6 @@ class AutofillSettingsViewModel @Inject constructor( private val autofillBreakageReportSender: AutofillBreakageReportSender, private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore, private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules, - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -412,11 +410,7 @@ class AutofillSettingsViewModel @Inject constructor( fun onViewCreated() { if (combineJob != null) return combineJob = viewModelScope.launch(dispatchers.io()) { - _viewState.value = _viewState.value.copy( - autofillEnabled = autofillStore.autofillEnabled, - isAutofillSupported = autofillCapabilityChecker.webViewSupportsAutofill() && - autofillCapabilityChecker.isAutofillEnabledByConfiguration(""), - ) + _viewState.value = _viewState.value.copy(autofillEnabled = autofillStore.autofillEnabled) val allCredentials = autofillStore.getAllCredentials().distinctUntilChanged() val combined = allCredentials.combine(searchQueryFilter) { credentials, filter -> @@ -785,7 +779,6 @@ class AutofillSettingsViewModel @Inject constructor( val credentialSearchQuery: String = "", val reportBreakageState: ReportBreakageState = ReportBreakageState(), val canShowPromo: Boolean = false, - val isAutofillSupported: Boolean = true, ) data class ReportBreakageState( diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt index a811f5b0b076..45a54b9a7c6e 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListMode.kt @@ -332,11 +332,7 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.viewState.collect { state -> - if (state.isAutofillSupported) { - binding.enabledToggle.quietlySetIsChecked(state.autofillEnabled, globalAutofillToggleListener) - } else { - binding.enabledToggle.isEnabled = false - } + binding.enabledToggle.quietlySetIsChecked(state.autofillEnabled, globalAutofillToggleListener) state.logins?.let { credentialsListUpdated(it, state.credentialSearchQuery, state.reportBreakageState.allowBreakageReporting) parentActivity()?.invalidateOptionsMenu() diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/AutofillUseGeneratedPasswordDialogFragment.kt index 4522dbe5733f..ba9d28a3c7b3 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 @@ -106,8 +100,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) configureSubtitleText(binding) } @@ -151,15 +146,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) @@ -185,7 +183,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) @@ -210,15 +208,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, @@ -226,10 +224,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 230ecbe2d40c..353e4bf75244 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,25 +18,20 @@ package com.duckduckgo.autofill.impl.ui.credential.passwordgeneration import android.content.Context import android.os.Bundle -import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog -import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_URL import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -44,14 +39,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, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { @@ -65,43 +58,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, @@ -109,18 +87,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) } autofilledListeners.getPlugins().forEach { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/AutofillSavingCredentialsDialogFragment.kt index a0de787a15a5..79424fac69a6 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.core.view.isVisible import androidx.fragment.app.setFragmentResult import androidx.lifecycle.Lifecycle @@ -33,11 +32,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 @@ -185,12 +180,12 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre pixelNameDialogEvent(Accepted, binding.keyFeaturesContainer.isVisible)?.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) @@ -217,7 +212,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()) @@ -229,7 +224,7 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre private fun onUserChoseNeverSaveThisSite() { pixelNameDialogEvent(Exclude, isOnboardingMode())?.let { pixel.fire(it) } - viewModel.addSiteToNeverSaveList(getWebMessageRequest().requestOrigin) + viewModel.addSiteToNeverSaveList(getOriginalUrl()) // this is another way to refuse saving credentials, so ensure that normal logic still runs onUserRejectedToSaveCredentials() @@ -284,23 +279,23 @@ class AutofillSavingCredentialsDialogFragment : BottomSheetDialogFragment(), Cre object Exclude : 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 20ab22b45d93..1ef8f2569f5e 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,29 +16,26 @@ 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.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesMultibinding import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -46,22 +43,17 @@ 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, private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { - override fun resultKey(tabId: String): String { - return CredentialAutofillPickerDialog.resultKey(tabId) - } - override fun processResult( result: Bundle, context: Context, @@ -71,11 +63,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 } @@ -83,37 +75,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() @@ -128,19 +103,19 @@ class ResultHandlerCredentialSelection @Inject constructor( Timber.v("Autofill: user selected credential to use, and successfully authenticated") pixel.fire(AutofillPixelNames.AUTOFILL_AUTHENTICATION_TO_AUTOFILL_AUTH_SUCCESSFUL) notifyAutofilledListeners() - 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) } } } @@ -160,10 +135,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/main/res/layout/view_autofill_config_disabled_warning.xml b/autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml deleted file mode 100644 index fffc2217ec91..000000000000 --- a/autofill/autofill-impl/src/main/res/layout/view_autofill_config_disabled_warning.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml b/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml deleted file mode 100644 index b0822b6372b3..000000000000 --- a/autofill/autofill-impl/src/main/res/layout/view_autofill_warning_support.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index 97df55f79047..633f147275b8 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -17,6 +17,4 @@ Passwords - - Autofill for passwords is currently unavailable. We’re working to restore this in a future app update. \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml index 703928b6e4ab..b5ed7705518c 100644 --- a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml @@ -56,7 +56,7 @@ Suggested - Autofill for passwords is unavailable because your version of Android WebView is outdated or incompatible. + Autofill for passwords is unavailable because your version of Android WebView is too old. Clear search input No results for \'%1$s\' 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 4fa036b21280..352a830e6eeb 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 @@ -16,11 +16,9 @@ package com.duckduckgo.autofill.impl -import android.annotation.SuppressLint import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.InternalTestUserChecker -import com.duckduckgo.autofill.impl.configuration.integration.JavascriptCommunicationSupport import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State @@ -42,7 +40,6 @@ class AutofillCapabilityCheckerImplTest { private val internalTestUserChecker: InternalTestUserChecker = mock() private val autofillGlobalCapabilityChecker: AutofillGlobalCapabilityChecker = mock() - private val javascriptCommunicationSupport: JavascriptCommunicationSupport = mock() private lateinit var testee: AutofillCapabilityCheckerImpl @@ -126,20 +123,6 @@ class AutofillCapabilityCheckerImplTest { assertFalse(testee.canGeneratePasswordFromWebView(URL)) } - @Test - fun whenModernJavascriptIntegrationIsSupportedThenSupportsAutofillIsTrue() = runTest { - setupConfig(topLevelFeatureEnabled = true, autofillEnabledByUser = true) - whenever(javascriptCommunicationSupport.supportsModernIntegration()).thenReturn(true) - assertTrue(testee.webViewSupportsAutofill()) - } - - @Test - fun whenModernJavascriptIntegrationIsSupportedThenSupportsAutofillIsFalse() = runTest { - setupConfig(topLevelFeatureEnabled = true, autofillEnabledByUser = true) - whenever(javascriptCommunicationSupport.supportsModernIntegration()).thenReturn(false) - assertFalse(testee.webViewSupportsAutofill()) - } - private suspend fun assertAllSubFeaturesDisabled() { assertFalse(testee.canAccessCredentialManagementScreen()) assertFalse(testee.canGeneratePasswordFromWebView(URL)) @@ -147,7 +130,6 @@ class AutofillCapabilityCheckerImplTest { assertFalse(testee.canSaveCredentialsFromWebView(URL)) } - @SuppressLint("DenyListedApi") private suspend fun setupConfig( topLevelFeatureEnabled: Boolean = false, autofillEnabledByUser: Boolean = false, @@ -173,7 +155,6 @@ class AutofillCapabilityCheckerImplTest { 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 4881053b0588..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,121 +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.annotation.SuppressLint import android.webkit.WebView +import androidx.test.core.app.ApplicationProvider.getApplicationContext import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.Callback -import com.duckduckgo.autofill.impl.configuration.integration.modern.listener.AutofillWebMessageListener -import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.common.utils.plugins.PluginPoint -import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory -import com.duckduckgo.feature.toggles.api.Toggle.State -import kotlinx.coroutines.test.runTest -import org.junit.Rule +import 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.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 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 - @Test - fun whenAutofillFeatureFlagDisabledThenDoNotAddJsInterface() = runTest { - val testee = setupConfig(topLevelFeatureEnabled = false) - testee.addJsInterface() - verifyJavascriptNotAdded() + private lateinit var testWebView: WebView + + private val emailProtectionInContextCallback: EmailProtectionUserPromptListener = mock() + private val emailProtectionInContextSignupFlowCallback: EmailProtectionInContextSignupFlowListener = mock() + + private val testCallback = object : Callback { + override suspend fun onCredentialsAvailableToInject( + originalUrl: String, + credentials: List, + triggerType: LoginTriggerType, + ) { + } + + override suspend fun onCredentialsAvailableToSave( + currentUrl: String, + credentials: LoginCredentials, + ) { + } + + override suspend fun onGeneratedPasswordAvailableToUse( + originalUrl: String, + username: String?, + generatedPassword: String, + ) { + } + + override fun noCredentialsAvailable(originalUrl: String) { + } + + override fun onCredentialsSaved(savedCredentials: LoginCredentials) { + } } - @Test - fun whenWebViewDoesNotSupportIntegrationThenDoNotAddJsInterface() = runTest { - val testee = setupConfig(deviceWebViewSupportsAutofill = 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 { - val testee = setupConfig() - testee.addJsInterface() - verifyJavascriptIsAdded() + fun whenRemoveJsInterfaceThenRemoveReferenceToWebview() { + testee.addJsInterface(testWebView, testCallback, emailProtectionInContextCallback, emailProtectionInContextSignupFlowCallback, "tabId") + + assertNotNull(autofillJavascriptInterface.webView) + + testee.removeJsInterface() + + assertNull(autofillJavascriptInterface.webView) } @Test - fun whenPluginsIsEmptyThenJsInterfaceIsAdded() = runTest { - val testee = setupConfig() - webMessageListeners.clear() - testee.addJsInterface() - verifyJavascriptIsAdded() + fun whenInjectCredentialsNullThenInterfaceInjectNoCredentials() { + testee.injectCredentials(null) + + assertEquals(NoCredentialsInjected, autofillJavascriptInterface.lastAction) } @Test - fun whenPluginsIsNotEmptyThenIsRegisteredWithWebView() = runTest { - val testee = setupConfig() - 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() + } - private suspend fun InlineBrowserAutofill.addJsInterface() { - addJsInterface(mockWebView, autofillCallback, "tab-id-123") - } + var lastAction: Actions? = null - @SuppressLint("DenyListedApi") - private suspend fun setupConfig( - topLevelFeatureEnabled: Boolean = true, - autofillEnabledByUser: Boolean = true, - canInjectCredentials: Boolean = true, - canSaveCredentials: Boolean = true, - canGeneratePassword: Boolean = true, - canAccessCredentialManagement: Boolean = true, - canIntegrateAutofillInWebView: Boolean = true, - deviceWebViewSupportsAutofill: Boolean = true, - ): InlineBrowserAutofill { - val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) - autofillFeature.self().setRawStoredState(State(enable = topLevelFeatureEnabled)) - autofillFeature.canInjectCredentials().setRawStoredState(State(enable = canInjectCredentials)) - autofillFeature.canSaveCredentials().setRawStoredState(State(enable = canSaveCredentials)) - autofillFeature.canGeneratePasswords().setRawStoredState(State(enable = canGeneratePassword)) - autofillFeature.canAccessCredentialManagement().setRawStoredState(State(enable = canAccessCredentialManagement)) - autofillFeature.canIntegrateAutofillInWebView().setRawStoredState(State(enable = canIntegrateAutofillInWebView)) - - whenever(capabilityChecker.webViewSupportsAutofill()).thenReturn(deviceWebViewSupportsAutofill) - whenever(capabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(canIntegrateAutofillInWebView) - - return InlineBrowserAutofill( - autofillCapabilityChecker = capabilityChecker, - dispatchers = coroutineTestRule.testDispatcherProvider, - autofillJavascriptInjector = autofillJavascriptInjector, - webMessageListeners = webMessageListenersPlugin, - autofillFeature = autofillFeature, - webMessageAttacher = webMessageAttacher, - ) + 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() { + } + + 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 016784f52800..075448d64f22 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. @@ -17,10 +17,10 @@ package com.duckduckgo.autofill.impl.configuration import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.api.AutofillCapabilityChecker import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials @@ -49,7 +49,7 @@ class RealAutofillRuntimeConfigProviderTest { private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) private val runtimeConfigurationWriter: RuntimeConfigurationWriter = mock() private val shareableCredentials: ShareableCredentials = mock() - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() + private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() @@ -91,7 +91,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenAutofillNotEnabledThenConfigurationUserPrefsCredentialsIsFalse() = runTest { configureAutofillCapabilities(enabled = false) - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(false) } @@ -99,7 +99,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenAutofillEnabledThenConfigurationUserPrefsCredentialsIsTrue() = runTest { configureAutofillCapabilities(enabled = true) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(true) } @@ -108,14 +108,14 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillCapabilities(enabled = true) configureAutofillAvailableForSite(EXAMPLE_URL) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyKeyIconRequestedToShow() } @Test fun whenNoCredentialsForUrlThenConfigurationInputTypeCredentialsIsFalse() = runTest { configureAutofillEnabledWithNoSavedCredentials(EXAMPLE_URL) - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -139,7 +139,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -163,7 +163,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", EXAMPLE_URL) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -188,7 +188,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -213,7 +213,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -238,7 +238,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(EXAMPLE_URL) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = true) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -263,7 +263,7 @@ class RealAutofillRuntimeConfigProviderTest { ) configureNoShareableLogins() - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -287,7 +287,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -311,7 +311,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( @@ -326,7 +326,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(true) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -340,7 +340,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(emailManager.isSignedIn()).thenReturn(false) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( credentialsAvailable = any(), @@ -352,7 +352,7 @@ class RealAutofillRuntimeConfigProviderTest { fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { val url = "example.com" configureAutofillEnabledWithNoSavedCredentials(url) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verifyCanSaveCredentialsReturnedAs(true) } @@ -362,7 +362,7 @@ class RealAutofillRuntimeConfigProviderTest { configureAutofillEnabledWithNoSavedCredentials(url) whenever(neverSavedSiteRepository.isInNeverSaveList(url)).thenReturn(true) - testee.getRuntimeConfiguration(url) + testee.getRuntimeConfiguration("", url) verifyCanSaveCredentialsReturnedAs(true) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealRuntimeConfigurationWriterTest.kt index 7b3560a0574c..2f7b044dea5f 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" @@ -95,12 +93,12 @@ class RealRuntimeConfigurationWriterTest { "credentials_saving": true, "inlineIcon_credentials": true, "emailProtection_incontext_signup": true, - "unknown_username_categorization": false + "unknown_username_categorization": false, } } } } - } + }; """.trimIndent() assertEquals( expectedJson, diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/integration/modern/listener/AutofillWebMessageListenerTest.kt 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 a5d1bce72fda..0215dda1f3db 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,16 +23,14 @@ import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.COHORT import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter.LAST_USED_DAY +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.DoNotUseEmailProtection import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePersonalEmailAddress import com.duckduckgo.autofill.api.EmailProtectionChooseEmailDialog.UseEmailResultType.UsePrivateAliasAddress -import com.duckduckgo.autofill.api.credential.saving.DuckAddressLoginCreator import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS @@ -43,12 +41,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 { @@ -58,18 +51,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, autofilledListeners = FakePluginPoint(), ) @@ -82,17 +73,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 @@ -155,9 +146,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/management/AutofillSettingsViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt index 80f3e91f8826..3c7b7accf969 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt @@ -31,7 +31,6 @@ import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.SettingsActivity import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.Sync import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager -import com.duckduckgo.autofill.impl.InternalAutofillCapabilityChecker import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizerImpl import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames @@ -122,7 +121,6 @@ class AutofillSettingsViewModelTest { private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules = mock() private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore = mock() private val urlMatcher = AutofillDomainNameUrlMatcher(UrlUnicodeNormalizerImpl()) - private val autofillCapabilityChecker: InternalAutofillCapabilityChecker = mock() private val testee = AutofillSettingsViewModel( autofillStore = mockStore, @@ -142,18 +140,18 @@ class AutofillSettingsViewModelTest { autofillBreakageReportSender = autofillBreakageReportSender, autofillBreakageReportDataStore = autofillBreakageReportDataStore, autofillBreakageReportCanShowRules = autofillBreakageReportCanShowRules, - autofillCapabilityChecker = autofillCapabilityChecker, ) @Before - fun setup() = runTest { + fun setup() { whenever(webUrlIdentifier.isLikelyAUrl(anyOrNull())).thenReturn(true) - whenever(autofillCapabilityChecker.webViewSupportsAutofill()).thenReturn(true) - whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(true) - whenever(mockStore.getAllCredentials()).thenReturn(emptyFlow()) - whenever(mockStore.getCredentialCount()).thenReturn(flowOf(0)) - whenever(neverSavedSiteRepository.neverSaveListCount()).thenReturn(emptyFlow()) - whenever(deviceAuthenticator.isAuthenticationRequiredForAutofill()).thenReturn(true) + + runTest { + whenever(mockStore.getAllCredentials()).thenReturn(emptyFlow()) + whenever(mockStore.getCredentialCount()).thenReturn(flowOf(0)) + whenever(neverSavedSiteRepository.neverSaveListCount()).thenReturn(emptyFlow()) + whenever(deviceAuthenticator.isAuthenticationRequiredForAutofill()).thenReturn(true) + } } @Test @@ -924,26 +922,6 @@ class AutofillSettingsViewModelTest { verify(pixel).fire(AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISMISSED) } - @Test - fun whenWebViewDoesNotSupportAutofillThenShowDisabledMode() = runTest { - whenever(autofillCapabilityChecker.webViewSupportsAutofill()).thenReturn(false) - testee.onViewCreated() - testee.viewState.test { - assertEquals(false, this.awaitItem().isAutofillSupported) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun whenAutofillConfigDisabledThenShowDisabledMode() = runTest { - whenever(autofillCapabilityChecker.isAutofillEnabledByConfiguration(any())).thenReturn(false) - testee.onViewCreated() - testee.viewState.test { - assertEquals(false, this.awaitItem().autofillEnabled) - cancelAndIgnoreRemainingEvents() - } - } - private fun List.verifyHasCommandToShowDeleteAllConfirmation(expectedNumberOfCredentialsToDelete: Int) { val confirmationCommand = this.firstOrNull { it is LaunchDeleteAllPasswordsConfirmation } assertNotNull(confirmationCommand) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/passwordgeneration/ResultHandlerUseGeneratedPasswordTest.kt index 45ed9e3d3040..02cdbe160e3d 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 @@ -31,8 +30,6 @@ import com.duckduckgo.autofill.api.UseGeneratedPasswordDialog.Companion.KEY_USER import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.passwordgeneration.AutomaticSavedLoginsMonitor import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.plugins.PluginPoint @@ -54,17 +51,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, autofilledListeners = FakePluginPoint(), ) @@ -80,20 +73,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 @@ -176,12 +167,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 @@ -198,9 +187,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 6cfc63c5f72a..18f29ff7b9f4 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,16 +21,14 @@ import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.autofill.api.AutofillEventListener -import com.duckduckgo.autofill.api.AutofillWebMessageRequest import com.duckduckgo.autofill.api.CredentialAutofillPickerDialog import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector import com.duckduckgo.autofill.api.ExistingCredentialMatchDetector.ContainsCredentialsResult.NoMatch import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.deviceauth.FakeAuthenticator import com.duckduckgo.autofill.impl.engagement.DataAutofilledListener -import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster -import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.plugins.PluginPoint @@ -50,11 +48,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 { @@ -68,43 +65,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 @@ -112,7 +101,7 @@ class ResultHandlerCredentialSelectionTest { configureSuccessfulAuth() val bundle = bundleMissingCredentials("example.com") testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) - verifyNoInteractions(messagePoster) + verifyNoInteractions(callback) } @Test @@ -123,25 +112,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) } } @@ -170,17 +159,12 @@ class ResultHandlerCredentialSelectionTest { appCoroutineScope = coroutineTestRule.testScope, pixel = pixel, deviceAuthenticator = deviceAuthenticator, + appBuildConfig = appBuildConfig, autofillStore = autofillStore, - messagePoster = messagePoster, - autofillResponseWriter = responseWriter, autofilledListeners = FakePluginPoint(), ) } - private fun String.asUrlRequest(): AutofillWebMessageRequest { - return AutofillWebMessageRequest(this, this, "request-id-123") - } - private class FakePluginPoint : PluginPoint { override fun getPlugins(): Collection { return emptyList() diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt index 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 0734be99a67b..4ea43dc6470e 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;" @@ -7593,7 +7465,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", { @@ -7657,7 +7529,7 @@ class CredentialsImport { } exports.CredentialsImport = CredentialsImport; -},{"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],24:[function(require,module,exports){ +},{"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7713,7 +7585,7 @@ function createDevice() { return new _ExtensionInterface.ExtensionInterface(globalConfig, deviceApi, settings); } -},{"../packages/device-api/index.js":12,"./DeviceInterface/AndroidInterface.js":25,"./DeviceInterface/AppleDeviceInterface.js":26,"./DeviceInterface/AppleOverlayDeviceInterface.js":27,"./DeviceInterface/ExtensionInterface.js":28,"./DeviceInterface/WindowsInterface.js":30,"./DeviceInterface/WindowsOverlayDeviceInterface.js":31,"./Settings.js":52,"./config.js":67,"./deviceApiCalls/transports/transports.js":75}],25:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./DeviceInterface/AndroidInterface.js":24,"./DeviceInterface/AppleDeviceInterface.js":25,"./DeviceInterface/AppleOverlayDeviceInterface.js":26,"./DeviceInterface/ExtensionInterface.js":27,"./DeviceInterface/WindowsInterface.js":29,"./DeviceInterface/WindowsOverlayDeviceInterface.js":30,"./Settings.js":51,"./config.js":66,"./deviceApiCalls/transports/transports.js":74}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7733,35 +7605,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; } /** @@ -7776,9 +7638,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 @@ -7793,7 +7660,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); } /** @@ -7801,13 +7676,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); } /** @@ -7815,7 +7702,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); + } + } } /** @@ -7840,7 +7733,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":46,"../UI/controllers/NativeUIController.js":60,"../autofill-utils.js":65,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29}],26:[function(require,module,exports){ +},{"../InContextSignup.js":45,"../UI/controllers/NativeUIController.js":59,"../autofill-utils.js":64,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8196,7 +8089,7 @@ class AppleDeviceInterface extends _InterfacePrototype.default { } exports.AppleDeviceInterface = AppleDeviceInterface; -},{"../../packages/device-api/index.js":12,"../Form/matching.js":45,"../InContextSignup.js":46,"../ThirdPartyProvider.js":53,"../UI/HTMLTooltip.js":58,"../UI/controllers/HTMLTooltipUIController.js":59,"../UI/controllers/NativeUIController.js":60,"../UI/controllers/OverlayUIController.js":61,"../autofill-utils.js":65,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"../deviceApiCalls/additionalDeviceApiCalls.js":71,"./InterfacePrototype.js":29}],27:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../Form/matching.js":44,"../InContextSignup.js":45,"../ThirdPartyProvider.js":52,"../UI/HTMLTooltip.js":57,"../UI/controllers/HTMLTooltipUIController.js":58,"../UI/controllers/NativeUIController.js":59,"../UI/controllers/OverlayUIController.js":60,"../autofill-utils.js":64,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"../deviceApiCalls/additionalDeviceApiCalls.js":70,"./InterfacePrototype.js":28}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8315,7 +8208,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":59,"./AppleDeviceInterface.js":26,"./overlayApi.js":33}],28:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../UI/controllers/HTMLTooltipUIController.js":58,"./AppleDeviceInterface.js":25,"./overlayApi.js":32}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8534,7 +8427,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":45,"../InContextSignup.js":46,"../UI/HTMLTooltip.js":58,"../UI/controllers/HTMLTooltipUIController.js":59,"../autofill-utils.js":65,"./InterfacePrototype.js":29}],29:[function(require,module,exports){ +},{"../Form/matching.js":44,"../InContextSignup.js":45,"../UI/HTMLTooltip.js":57,"../UI/controllers/HTMLTooltipUIController.js":58,"../autofill-utils.js":64,"./InterfacePrototype.js":28}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8803,7 +8696,8 @@ class InterfacePrototype { } async startInit() { if (this.isInitializationStarted) return; - this.isInitializationStarted = true; + this.alreadyInitialized = true; + await this.settings.refresh(); this.addDeviceListeners(); await this.setupAutofill(); this.uiController = this.createUIController(); @@ -9137,19 +9031,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) { @@ -9205,13 +9091,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({ @@ -9380,7 +9259,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":12,"../CredentialsImport.js":23,"../EmailProtection.js":34,"../Form/formatters.js":38,"../Form/matching.js":45,"../InputTypes/Credentials.js":47,"../PasswordGenerator.js":50,"../Scanner.js":51,"../Settings.js":52,"../UI/controllers/NativeUIController.js":60,"../autofill-utils.js":65,"../config.js":67,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"../deviceApiCalls/transports/transports.js":75,"../locales/strings.js":100,"./initFormSubmissionsApi.js":32}],30:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"../CredentialsImport.js":22,"../EmailProtection.js":33,"../Form/formatters.js":37,"../Form/matching.js":44,"../InputTypes/Credentials.js":46,"../PasswordGenerator.js":49,"../Scanner.js":50,"../Settings.js":51,"../UI/controllers/NativeUIController.js":59,"../autofill-utils.js":64,"../config.js":66,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"../deviceApiCalls/transports/transports.js":74,"../locales/strings.js":99,"./initFormSubmissionsApi.js":31}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9465,13 +9344,13 @@ class WindowsInterface extends _InterfacePrototype.default { return await this.credentialsImport.refresh(); } 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 instanceof DOMException && e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -9546,7 +9425,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":61,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29}],31:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":60,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9725,7 +9604,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":59,"../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./InterfacePrototype.js":29,"./overlayApi.js":33}],32:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":58,"../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./InterfacePrototype.js":28,"./overlayApi.js":32}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9824,7 +9703,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":41,"../autofill-utils.js":65}],33:[function(require,module,exports){ +},{"../Form/label-util.js":40,"../autofill-utils.js":64}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9882,7 +9761,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":69}],34:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":68}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9917,7 +9796,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],35:[function(require,module,exports){ +},{}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10057,7 +9936,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; @@ -10824,7 +10703,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":47,"../autofill-utils.js":65,"../constants.js":68,"./FormAnalyzer.js":36,"./formatters.js":38,"./inputStyles.js":39,"./inputTypeConfig.js":40,"./matching.js":45}],36:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":64,"../constants.js":67,"./FormAnalyzer.js":35,"./formatters.js":37,"./inputStyles.js":38,"./inputTypeConfig.js":39,"./matching.js":44}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11194,7 +11073,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":65,"../constants.js":68,"./matching-config/__generated__/compiled-matching-config.js":43,"./matching.js":45}],37:[function(require,module,exports){ +},{"../autofill-utils.js":64,"../constants.js":67,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching.js":44}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11759,7 +11638,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],38:[function(require,module,exports){ +},{}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12064,7 +11943,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":37,"./matching.js":45}],39:[function(require,module,exports){ +},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12155,7 +12034,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":40}],40:[function(require,module,exports){ +},{"./inputTypeConfig.js":39}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12409,7 +12288,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":47,"../InputTypes/CreditCard.js":48,"../InputTypes/Identity.js":49,"../UI/img/ddgPasswordIcon.js":63,"../constants.js":68,"./logo-svg.js":42,"./matching.js":45}],41:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":46,"../InputTypes/CreditCard.js":47,"../InputTypes/Identity.js":48,"../UI/img/ddgPasswordIcon.js":62,"../constants.js":67,"./logo-svg.js":41,"./matching.js":44}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12457,7 +12336,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":45}],42:[function(require,module,exports){ +},{"./matching.js":44}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12490,7 +12369,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],43:[function(require,module,exports){ +},{}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12943,7 +12822,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],44:[function(require,module,exports){ +},{}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13018,7 +12897,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":65,"./matching.js":45}],45:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./matching.js":44}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14010,7 +13889,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":65,"../constants.js":68,"./label-util.js":41,"./matching-config/__generated__/compiled-matching-config.js":43,"./matching-utils.js":44}],46:[function(require,module,exports){ +},{"../autofill-utils.js":64,"../constants.js":67,"./label-util.js":40,"./matching-config/__generated__/compiled-matching-config.js":42,"./matching-utils.js":43}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14142,7 +14021,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":65,"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],47:[function(require,module,exports){ +},{"./autofill-utils.js":64,"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14298,7 +14177,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":65}],48:[function(require,module,exports){ +},{"../autofill-utils.js":64}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14323,7 +14202,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],49:[function(require,module,exports){ +},{}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14369,7 +14248,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":38}],50:[function(require,module,exports){ +},{"../Form/formatters.js":37}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14411,7 +14290,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":18,"../packages/password/rules.json":22}],51:[function(require,module,exports){ +},{"../packages/password/index.js":17,"../packages/password/rules.json":21}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14872,7 +14751,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":35,"./Form/matching.js":45,"./autofill-utils.js":65,"./constants.js":68,"./deviceApiCalls/__generated__/deviceApiCalls.js":69}],52:[function(require,module,exports){ +},{"./Form/Form.js":34,"./Form/matching.js":44,"./autofill-utils.js":64,"./constants.js":67,"./deviceApiCalls/__generated__/deviceApiCalls.js":68}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15018,11 +14897,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; } @@ -15038,9 +14912,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) { @@ -15293,7 +15164,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":12,"./autofill-utils.js":65,"./deviceApiCalls/__generated__/deviceApiCalls.js":69,"./deviceApiCalls/__generated__/validators.zod.js":70}],53:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./autofill-utils.js":64,"./deviceApiCalls/__generated__/deviceApiCalls.js":68,"./deviceApiCalls/__generated__/validators.zod.js":69}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15360,7 +15231,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); } } @@ -15375,7 +15246,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -15383,7 +15254,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":12,"./Form/matching.js":45,"./deviceApiCalls/__generated__/deviceApiCalls.js":69,"./deviceApiCalls/__generated__/validators.zod.js":70}],54:[function(require,module,exports){ +},{"../packages/device-api/index.js":12,"./Form/matching.js":44,"./deviceApiCalls/__generated__/deviceApiCalls.js":68,"./deviceApiCalls/__generated__/validators.zod.js":69}],53:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15424,7 +15295,7 @@ ${this.options.css} } var _default = exports.default = CredentialsImportTooltip; -},{"./HTMLTooltip.js":58}],55:[function(require,module,exports){ +},{"./HTMLTooltip.js":57}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15575,7 +15446,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":47,"../autofill-utils.js":65,"./HTMLTooltip.js":58}],56:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":46,"../autofill-utils.js":64,"./HTMLTooltip.js":57}],55:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15657,7 +15528,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":65,"./HTMLTooltip.js":58}],57:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./HTMLTooltip.js":57}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -15710,7 +15581,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":58}],58:[function(require,module,exports){ +},{"./HTMLTooltip.js":57}],57:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16098,7 +15969,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":45,"../autofill-utils.js":65,"./styles/styles.js":64}],59:[function(require,module,exports){ +},{"../Form/matching.js":44,"../autofill-utils.js":64,"./styles/styles.js":63}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16466,7 +16337,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":40,"../../Form/matching.js":45,"../../autofill-utils.js":65,"../CredentialsImportTooltip.js":54,"../DataHTMLTooltip.js":55,"../EmailHTMLTooltip.js":56,"../EmailSignupHTMLTooltip.js":57,"../HTMLTooltip.js":58,"./UIController.js":62}],60:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":39,"../../Form/matching.js":44,"../../autofill-utils.js":64,"../CredentialsImportTooltip.js":53,"../DataHTMLTooltip.js":54,"../EmailHTMLTooltip.js":55,"../EmailSignupHTMLTooltip.js":56,"../HTMLTooltip.js":57,"./UIController.js":61}],59:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16568,11 +16439,6 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } - case 'none': - { - // do nothing - break; - } default: { if (args.device.isTestMode()) { @@ -16633,7 +16499,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":45,"../../InputTypes/Credentials.js":47,"../../deviceApiCalls/__generated__/deviceApiCalls.js":69,"./UIController.js":62}],61:[function(require,module,exports){ +},{"../../Form/matching.js":44,"../../InputTypes/Credentials.js":46,"../../deviceApiCalls/__generated__/deviceApiCalls.js":68,"./UIController.js":61}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16870,7 +16736,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":45,"./UIController.js":62}],62:[function(require,module,exports){ +},{"../../Form/matching.js":44,"./UIController.js":61}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16954,7 +16820,7 @@ class UIController { } exports.UIController = UIController; -},{}],63:[function(require,module,exports){ +},{}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16971,7 +16837,7 @@ const ddgCcIconBase = exports.ddgCcIconBase = 'data:image/svg+xml;base64,PD94bWw const ddgCcIconFilled = exports.ddgCcIconFilled = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzc2NDMxMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjNzY0MzEwIi8+Cjwvc3ZnPgo='; const ddgIdentityIconBase = exports.ddgIdentityIconBase = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4KPHBhdGggeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSJub25lIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMiAyMWMyLjE0MyAwIDQuMTExLS43NSA1LjY1Ny0yLS42MjYtLjUwNi0xLjMxOC0uOTI3LTIuMDYtMS4yNS0xLjEtLjQ4LTIuMjg1LS43MzUtMy40ODYtLjc1LTEuMi0uMDE0LTIuMzkyLjIxMS0zLjUwNC42NjQtLjgxNy4zMzMtMS41OC43ODMtMi4yNjQgMS4zMzYgMS41NDYgMS4yNSAzLjUxNCAyIDUuNjU3IDJ6bTQuMzk3LTUuMDgzYy45NjcuNDIyIDEuODY2Ljk4IDIuNjcyIDEuNjU1QzIwLjI3OSAxNi4wMzkgMjEgMTQuMTA0IDIxIDEyYzAtNC45Ny00LjAzLTktOS05cy05IDQuMDMtOSA5YzAgMi4xMDQuNzIyIDQuMDQgMS45MzIgNS41NzIuODc0LS43MzQgMS44Ni0xLjMyOCAyLjkyMS0xLjc2IDEuMzYtLjU1NCAyLjgxNi0uODMgNC4yODMtLjgxMSAxLjQ2Ny4wMTggMi45MTYuMzMgNC4yNi45MTZ6TTEyIDIzYzYuMDc1IDAgMTEtNC45MjUgMTEtMTFTMTguMDc1IDEgMTIgMSAxIDUuOTI1IDEgMTJzNC45MjUgMTEgMTEgMTF6bTMtMTNjMCAxLjY1Ny0xLjM0MyAzLTMgM3MtMy0xLjM0My0zLTMgMS4zNDMtMyAzLTMgMyAxLjM0MyAzIDN6bTIgMGMwIDIuNzYxLTIuMjM5IDUtNSA1cy01LTIuMjM5LTUtNSAyLjIzOS01IDUtNSA1IDIuMjM5IDUgNXoiIGZpbGw9IiMwMDAiLz4KPC9zdmc+Cg==`; -},{}],64:[function(require,module,exports){ +},{}],63:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16980,7 +16846,7 @@ Object.defineProperty(exports, "__esModule", { exports.CSS_STYLES = void 0; const CSS_STYLES = exports.CSS_STYLES = ":root {\n color-scheme: light dark;\n}\n\n.wrapper *, .wrapper *::before, .wrapper *::after {\n box-sizing: border-box;\n}\n.wrapper {\n position: fixed;\n top: 0;\n left: 0;\n padding: 0;\n font-family: 'DDG_ProximaNova', 'Proxima Nova', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n -webkit-font-smoothing: antialiased;\n z-index: 2147483647;\n}\n.wrapper--data {\n font-family: 'SF Pro Text', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n}\n.wrapper:not(.top-autofill) .tooltip {\n position: absolute;\n width: 300px;\n max-width: calc(100vw - 25px);\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--data, #topAutofill {\n background-color: rgba(242, 240, 240, 1);\n -webkit-backdrop-filter: blur(40px);\n backdrop-filter: blur(40px);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data, #topAutofill {\n background: rgb(100, 98, 102, .9);\n }\n}\n.tooltip--data {\n padding: 6px;\n font-size: 13px;\n line-height: 14px;\n width: 315px;\n max-height: 290px;\n overflow-y: auto;\n}\n.top-autofill .tooltip--data {\n min-height: 100vh;\n}\n.tooltip--data.tooltip--incontext-signup {\n width: 360px;\n}\n.wrapper:not(.top-autofill) .tooltip--data {\n top: 100%;\n left: 100%;\n border: 0.5px solid rgba(255, 255, 255, 0.2);\n border-radius: 6px;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.32);\n}\n@media (prefers-color-scheme: dark) {\n .wrapper:not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n.wrapper:not(.top-autofill) .tooltip--email {\n top: calc(100% + 6px);\n right: calc(100% - 48px);\n padding: 8px;\n border: 1px solid #D0D0D0;\n border-radius: 10px;\n background-color: #FFFFFF;\n font-size: 14px;\n line-height: 1.3;\n color: #333333;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);\n}\n.tooltip--email__caret {\n position: absolute;\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--email__caret::before,\n.tooltip--email__caret::after {\n content: \"\";\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n display: block;\n border-bottom: 8px solid #D0D0D0;\n position: absolute;\n right: -28px;\n}\n.tooltip--email__caret::before {\n border-bottom-color: #D0D0D0;\n top: -1px;\n}\n.tooltip--email__caret::after {\n border-bottom-color: #FFFFFF;\n top: 0px;\n}\n\n/* Buttons */\n.tooltip__button {\n display: flex;\n width: 100%;\n padding: 8px 8px 8px 0px;\n font-family: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: 6px;\n text-align: left;\n}\n.tooltip__button.currentFocus,\n.wrapper:not(.top-autofill) .tooltip__button:hover {\n background-color: #3969EF;\n color: #FFFFFF;\n}\n\n/* Data autofill tooltip specific */\n.tooltip__button--data {\n position: relative;\n min-height: 48px;\n flex-direction: row;\n justify-content: flex-start;\n font-size: inherit;\n font-weight: 500;\n line-height: 16px;\n text-align: left;\n border-radius: 3px;\n}\n.tooltip--data__item-container {\n max-height: 220px;\n overflow: auto;\n}\n.tooltip__button--data:first-child {\n margin-top: 0;\n}\n.tooltip__button--data:last-child {\n margin-bottom: 0;\n}\n.tooltip__button--data::before {\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 20px 20px;\n background-repeat: no-repeat;\n background-position: center center;\n}\n#provider_locked::after {\n position: absolute;\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 11px 13px;\n background-repeat: no-repeat;\n background-position: right bottom;\n}\n.tooltip__button--data.currentFocus:not(.tooltip__button--data--bitwarden)::before,\n.wrapper:not(.top-autofill) .tooltip__button--data:not(.tooltip__button--data--bitwarden):hover::before {\n filter: invert(100%);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before,\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before {\n filter: invert(100%);\n opacity: .9;\n }\n}\n.tooltip__button__text-container {\n margin: auto 0;\n}\n.label {\n display: block;\n font-weight: 400;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.8);\n font-size: 13px;\n line-height: 1;\n}\n.label + .label {\n margin-top: 2px;\n}\n.label.label--medium {\n font-weight: 500;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.9);\n}\n.label.label--small {\n font-size: 11px;\n font-weight: 400;\n letter-spacing: 0.06px;\n color: rgba(0,0,0,0.6);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data .label {\n color: #ffffff;\n }\n .tooltip--data .label--medium {\n color: #ffffff;\n }\n .tooltip--data .label--small {\n color: #cdcdcd;\n }\n}\n.tooltip__button.currentFocus .label,\n.wrapper:not(.top-autofill) .tooltip__button:hover .label {\n color: #FFFFFF;\n}\n\n.tooltip__button--manage {\n font-size: 13px;\n padding: 5px 9px;\n border-radius: 3px;\n margin: 0;\n}\n\n/* Icons */\n.tooltip__button--data--credentials::before,\n.tooltip__button--data--credentials__current::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNS4zMzQgNi42NjdhMiAyIDAgMSAwIDAgNCAyIDIgMCAwIDAgMC00Wm0tLjY2NyAyYS42NjcuNjY3IDAgMSAxIDEuMzMzIDAgLjY2Ny42NjcgMCAwIDEtMS4zMzMgMFoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNjY3IDRhNS4zMzMgNS4zMzMgMCAwIDAtNS4xODggNi41NzhsLTUuMjg0IDUuMjg0YS42NjcuNjY3IDAgMCAwLS4xOTUuNDcxdjNjMCAuMzY5LjI5OC42NjcuNjY3LjY2N2gyLjY2NmMuNzM3IDAgMS4zMzQtLjU5NyAxLjMzNC0xLjMzM1YxOGguNjY2Yy43MzcgMCAxLjMzNC0uNTk3IDEuMzM0LTEuMzMzdi0xLjMzNEgxMmMuMTc3IDAgLjM0Ni0uMDcuNDcxLS4xOTVsLjY4OC0uNjg4QTUuMzMzIDUuMzMzIDAgMSAwIDE0LjY2NyA0Wm0tNCA1LjMzM2E0IDQgMCAxIDEgMi41NTUgMy43MzIuNjY3LjY2NyAwIDAgMC0uNzEzLjE1bC0uNzg1Ljc4NUgxMGEuNjY3LjY2NyAwIDAgMC0uNjY3LjY2N3YySDhhLjY2Ny42NjcgMCAwIDAtLjY2Ny42NjZ2MS4zMzRoLTJ2LTIuMDU4bDUuMzY1LTUuMzY0YS42NjcuNjY3IDAgMCAwIC4xNjMtLjY3NyAzLjk5NiAzLjk5NiAwIDAgMS0uMTk0LTEuMjM1WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--credentials__new::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNOC4wNDcgNC42MjVDNy45MzcgNC4xMjUgNy44NjIgNCA3LjUgNGMtLjM2MiAwLS40MzguMTI1LS41NDcuNjI1LS4wNjguMzEtLjE3NyAxLjMzOC0uMjUxIDIuMDc3LS43MzguMDc0LTEuNzY3LjE4My0yLjA3Ny4yNTEtLjUuMTEtLjYyNS4xODQtLjYyNS41NDcgMCAuMzYyLjEyNS40MzcuNjI1LjU0Ny4zMS4wNjcgMS4zMzYuMTc3IDIuMDc0LjI1LjA3My43NjcuMTg1IDEuODQyLjI1NCAyLjA3OC4xMS4zNzUuMTg1LjYyNS41NDcuNjI1LjM2MiAwIC40MzgtLjEyNS41NDctLjYyNS4wNjgtLjMxLjE3Ny0xLjMzNi4yNS0yLjA3NC43NjctLjA3MyAxLjg0Mi0uMTg1IDIuMDc4LS4yNTQuMzc1LS4xMS42MjUtLjE4NS42MjUtLjU0NyAwLS4zNjMtLjEyNS0uNDM4LS42MjUtLjU0Ny0uMzEtLjA2OC0xLjMzOS0uMTc3LTIuMDc3LS4yNTEtLjA3NC0uNzM5LS4xODMtMS43NjctLjI1MS0yLjA3N1oiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0xNC42ODEgNS4xOTljLS43NjYgMC0xLjQ4Mi4yMS0yLjA5My41NzhhLjYzNi42MzYgMCAwIDEtLjY1NS0xLjA5IDUuMzQgNS4zNCAwIDEgMSAxLjMwMiA5LjcyMmwtLjc3NS43NzZhLjYzNi42MzYgMCAwIDEtLjQ1LjE4NmgtMS4zOTh2MS42NWMwIC40OTMtLjQuODkzLS44OTMuODkzSDguNTc4djEuMTQxYzAgLjQ5NC0uNC44OTMtLjg5NC44OTNINC42MzZBLjYzNi42MzYgMCAwIDEgNCAxOS4zMTNWMTYuMjZjMC0uMTY5LjA2Ny0uMzMuMTg2LS40NWw1LjU2Mi01LjU2MmEuNjM2LjYzNiAwIDEgMSAuOS45bC01LjM3NiA1LjM3NXYyLjE1M2gyLjAzNHYtMS4zOTljMC0uMzUuMjg1LS42MzYuNjM2LS42MzZIOS4zNHYtMS45MDdjMC0uMzUxLjI4NC0uNjM2LjYzNS0uNjM2aDEuNzcxbC44NjQtLjg2M2EuNjM2LjYzNiAwIDAgMSAuNjY4LS4xNDcgNC4wNjkgNC4wNjkgMCAxIDAgMS40MDItNy44OVoiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTEzLjYyNSA4LjQ5OWExLjg3NSAxLjg3NSAwIDEgMSAzLjc1IDAgMS44NzUgMS44NzUgMCAwIDEtMy43NSAwWm0xLjg3NS0uNjI1YS42MjUuNjI1IDAgMSAwIDAgMS4yNS42MjUuNjI1IDAgMCAwIDAtMS4yNVoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNjI1IDEyLjEyNWEuNjI1LjYyNSAwIDEgMCAwLTEuMjUuNjI1LjYyNSAwIDAgMCAwIDEuMjVaIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--creditCards::before {\n background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzAwMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjMDAwIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--identities::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4=');\n}\n.tooltip__button--data--credentials.tooltip__button--data--bitwarden::before,\n.tooltip__button--data--credentials__current.tooltip__button--data--bitwarden::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iOCIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE4LjU2OTYgNS40MzM1NUMxOC41MDg0IDUuMzc0NDIgMTguNDM0NyA1LjMyNzYzIDE4LjM1MzEgNS4yOTYxMUMxOC4yNzE1IDUuMjY0NiAxOC4xODM3IDUuMjQ5MDQgMTguMDk1MyA1LjI1MDQxSDUuOTIxOTFDNS44MzMyNiA1LjI0NzI5IDUuNzQ0OTMgNS4yNjIwNSA1LjY2MzA0IDUuMjkzNjdDNS41ODExNSA1LjMyNTI5IDUuNTA3NjUgNS4zNzMwMiA1LjQ0NzYyIDUuNDMzNTVDNS4zMjE3IDUuNTUwMTMgNS4yNTA2NSA1LjcwODE1IDUuMjUgNS44NzMxVjEzLjM4MjFDNS4yNTMzNiAxMy45NTM1IDUuMzc0MDggMTQuNTE5MSA1LjYwNTcyIDE1LjA0ODdDNS44MTkzMSAxNS41NzI4IDYuMTEyMDcgMTYuMDY2MSA2LjQ3NTI0IDE2LjUxMzlDNi44NDIgMTYuOTY4MyA3LjI1OTI5IDE3LjM4NTcgNy43MjAyNSAxNy43NTkzQzguMTQwNTMgMTguMTI1NiA4LjU4OTcxIDE4LjQ2MjMgOS4wNjQwNyAxOC43NjY2QzkuNDU5MzEgMTkuMDIzIDkuOTEzODMgMTkuMjc5NCAxMC4zNDg2IDE5LjUxNzVDMTAuNzgzNCAxOS43NTU2IDExLjA5OTYgMTkuOTIwNCAxMS4yNzc0IDE5Ljk5MzdDMTEuNDU1MyAyMC4wNjY5IDExLjYxMzQgMjAuMTQwMiAxMS43MTIyIDIwLjE5NTFDMTEuNzk5MiAyMC4yMzEzIDExLjg5MzUgMjAuMjUgMTEuOTg4OCAyMC4yNUMxMi4wODQyIDIwLjI1IDEyLjE3ODUgMjAuMjMxMyAxMi4yNjU1IDIwLjE5NTFDMTIuNDIxMiAyMC4xMzYzIDEyLjU3MjkgMjAuMDY5IDEyLjcyIDE5Ljk5MzdDMTIuNzcxMSAxOS45Njc0IDEyLjgzMzUgMTkuOTM2NiAxMi45MDY5IDE5LjkwMDRDMTMuMDg5MSAxOS44MTA1IDEzLjMzODggMTkuNjg3MiAxMy42NDg5IDE5LjUxNzVDMTQuMDgzNiAxOS4yNzk0IDE0LjUxODQgMTkuMDIzIDE0LjkzMzQgMTguNzY2NkMxNS40MDQgMTguNDU3NyAxNS44NTI4IDE4LjEyMTIgMTYuMjc3MiAxNy43NTkzQzE2LjczMzEgMTcuMzgwOSAxNy4xNDk5IDE2Ljk2NCAxNy41MjIyIDE2LjUxMzlDMTcuODc4IDE2LjA2MTcgMTguMTcwMiAxNS41NjkzIDE4LjM5MTcgMTUuMDQ4N0MxOC42MjM0IDE0LjUxOTEgMTguNzQ0MSAxMy45NTM1IDE4Ljc0NzQgMTMuMzgyMVY1Ljg3MzFDMTguNzU1NyA1Ljc5MjE0IDE4Ljc0MzkgNS43MTA1IDE4LjcxMzEgNS42MzQzNUMxOC42ODIzIDUuNTU4MiAxOC42MzMyIDUuNDg5NTQgMTguNTY5NiA1LjQzMzU1Wk0xNy4wMDg0IDEzLjQ1NTNDMTcuMDA4NCAxNi4xODQyIDEyLjAwODYgMTguNTI4NSAxMi4wMDg2IDE4LjUyODVWNi44NjIwOUgxNy4wMDg0VjEzLjQ1NTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');\n}\n#provider_locked:after {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMSAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgNy42MDA1N1Y3LjYwMjVWOS41MjI1QzEgMTAuMDgwMSAxLjIyMTUxIDEwLjYxNDkgMS42MTU4MSAxMS4wMDkyQzIuMDEwMSAxMS40MDM1IDIuNTQ0ODggMTEuNjI1IDMuMTAyNSAxMS42MjVINy4yNzI1QzcuNTQ4NjEgMTEuNjI1IDcuODIyMDEgMTEuNTcwNiA4LjA3NzA5IDExLjQ2NUM4LjMzMjE4IDExLjM1OTMgOC41NjM5NiAxMS4yMDQ0IDguNzU5MTkgMTEuMDA5MkM4Ljk1NDQzIDEwLjgxNCA5LjEwOTMgMTAuNTgyMiA5LjIxNDk2IDEwLjMyNzFDOS4zMjA2MiAxMC4wNzIgOS4zNzUgOS43OTg2MSA5LjM3NSA5LjUyMjVMOS4zNzUgNy42MDI1TDkuMzc1IDcuNjAwNTdDOS4zNzQxNSA3LjE2MTMxIDkuMjM1NzQgNi43MzMzNSA4Ljk3OTIyIDYuMzc2NzhDOC44NzY4MyA2LjIzNDQ2IDguNzU3NjggNi4xMDYzNyA4LjYyNSA1Ljk5NDg5VjUuMTg3NUM4LjYyNSA0LjI3NTgyIDguMjYyODQgMy40MDE0OCA3LjYxODE4IDIuNzU2ODJDNi45NzM1MiAyLjExMjE2IDYuMDk5MTggMS43NSA1LjE4NzUgMS43NUM0LjI3NTgyIDEuNzUgMy40MDE0OCAyLjExMjE2IDIuNzU2ODIgMi43NTY4MkMyLjExMjE2IDMuNDAxNDggMS43NSA0LjI3NTgyIDEuNzUgNS4xODc1VjUuOTk0ODlDMS42MTczMiA2LjEwNjM3IDEuNDk4MTcgNi4yMzQ0NiAxLjM5NTc4IDYuMzc2NzhDMS4xMzkyNiA2LjczMzM1IDEuMDAwODUgNy4xNjEzMSAxIDcuNjAwNTdaTTQuOTY4NyA0Ljk2ODdDNS4wMjY5NCA0LjkxMDQ3IDUuMTA1MzIgNC44NzY5OSA1LjE4NzUgNC44NzUwN0M1LjI2OTY4IDQuODc2OTkgNS4zNDgwNiA0LjkxMDQ3IDUuNDA2MyA0Ljk2ODdDNS40NjU0MiA1LjAyNzgzIDUuNDk5MDQgNS4xMDc3NCA1LjUgNS4xOTEzVjUuNUg0Ljg3NVY1LjE5MTNDNC44NzU5NiA1LjEwNzc0IDQuOTA5NTggNS4wMjc4MyA0Ljk2ODcgNC45Njg3WiIgZmlsbD0iIzIyMjIyMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');\n}\n\nhr {\n display: block;\n margin: 5px 9px;\n border: none; /* reset the border */\n border-top: 1px solid rgba(0,0,0,.1);\n}\n\nhr:first-child {\n display: none;\n}\n\n@media (prefers-color-scheme: dark) {\n hr {\n border-top: 1px solid rgba(255,255,255,.2);\n }\n}\n\n#privateAddress {\n align-items: flex-start;\n}\n#personalAddress::before,\n#privateAddress::before,\n#incontextSignup::before,\n#personalAddress.currentFocus::before,\n#personalAddress:hover::before,\n#privateAddress.currentFocus::before,\n#privateAddress:hover::before {\n filter: none;\n /* This is the same icon as `daxBase64` in `src/Form/logo-svg.js` */\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTY0IDEyOGMzNS4zNDYgMCA2NC0yOC42NTQgNjQtNjRzLTI4LjY1NC02NC02NC02NC02NCAyOC42NTQtNjQgNjQgMjguNjU0IDY0IDY0IDY0eiIgZmlsbD0iI2RlNTgzMyIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im03MyAxMTEuNzVjMC0uNS4xMjMtLjYxNC0xLjQ2Ni0zLjc4Mi00LjIyNC04LjQ1OS04LjQ3LTIwLjM4NC02LjU0LTI4LjA3NS4zNTMtMS4zOTctMy45NzgtNTEuNzQ0LTcuMDQtNTMuMzY1LTMuNDAyLTEuODEzLTcuNTg4LTQuNjktMTEuNDE4LTUuMzMtMS45NDMtLjMxLTQuNDktLjE2NC02LjQ4Mi4xMDUtLjM1My4wNDctLjM2OC42ODMtLjAzLjc5OCAxLjMwOC40NDMgMi44OTUgMS4yMTIgMy44MyAyLjM3NS4xNzguMjItLjA2LjU2Ni0uMzQyLjU3Ny0uODgyLjAzMi0yLjQ4Mi40MDItNC41OTMgMi4xOTUtLjI0NC4yMDctLjA0MS41OTIuMjczLjUzIDQuNTM2LS44OTcgOS4xNy0uNDU1IDExLjkgMi4wMjcuMTc3LjE2LjA4NC40NS0uMTQ3LjUxMi0yMy42OTQgNi40NC0xOS4wMDMgMjcuMDUtMTIuNjk2IDUyLjM0NCA1LjYxOSAyMi41MyA3LjczMyAyOS43OTIgOC40IDMyLjAwNGEuNzE4LjcxOCAwIDAgMCAuNDIzLjQ2N2M4LjE1NiAzLjI0OCAyNS45MjggMy4zOTIgMjUuOTI4LTIuMTMyeiIgZmlsbD0iI2RkZCIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBkPSJtNzYuMjUgMTE2LjVjLTIuODc1IDEuMTI1LTguNSAxLjYyNS0xMS43NSAxLjYyNS00Ljc2NCAwLTExLjYyNS0uNzUtMTQuMTI1LTEuODc1LTEuNTQ0LTQuNzUxLTYuMTY0LTE5LjQ4LTEwLjcyNy0zOC4xODVsLS40NDctMS44MjctLjAwNC0uMDE1Yy01LjQyNC0yMi4xNTctOS44NTUtNDAuMjUzIDE0LjQyNy00NS45MzguMjIyLS4wNTIuMzMtLjMxNy4xODQtLjQ5Mi0yLjc4Ni0zLjMwNS04LjAwNS00LjM4OC0xNC42MDUtMi4xMTEtLjI3LjA5My0uNTA2LS4xOC0uMzM3LS40MTIgMS4yOTQtMS43ODMgMy44MjMtMy4xNTUgNS4wNzEtMy43NTYuMjU4LS4xMjQuMjQyLS41MDItLjAzLS41ODhhMjcuODc3IDI3Ljg3NyAwIDAgMCAtMy43NzItLjljLS4zNy0uMDU5LS40MDMtLjY5My0uMDMyLS43NDMgOS4zNTYtMS4yNTkgMTkuMTI1IDEuNTUgMjQuMDI4IDcuNzI2YS4zMjYuMzI2IDAgMCAwIC4xODYuMTE0YzE3Ljk1MiAzLjg1NiAxOS4yMzggMzIuMjM1IDE3LjE3IDMzLjUyOC0uNDA4LjI1NS0xLjcxNS4xMDgtMy40MzgtLjA4NS02Ljk4Ni0uNzgxLTIwLjgxOC0yLjMyOS05LjQwMiAxOC45NDguMTEzLjIxLS4wMzYuNDg4LS4yNzIuNTI1LTYuNDM4IDEgMS44MTIgMjEuMTczIDcuODc1IDM0LjQ2MXoiIGZpbGw9IiNmZmYiLz4KICAgIDxwYXRoIGQ9Im04NC4yOCA5MC42OThjLTEuMzY3LS42MzMtNi42MjEgMy4xMzUtMTAuMTEgNi4wMjgtLjcyOC0xLjAzMS0yLjEwMy0xLjc4LTUuMjAzLTEuMjQyLTIuNzEzLjQ3Mi00LjIxMSAxLjEyNi00Ljg4IDIuMjU0LTQuMjgzLTEuNjIzLTExLjQ4OC00LjEzLTEzLjIyOS0xLjcxLTEuOTAyIDIuNjQ2LjQ3NiAxNS4xNjEgMy4wMDMgMTYuNzg2IDEuMzIuODQ5IDcuNjMtMy4yMDggMTAuOTI2LTYuMDA1LjUzMi43NDkgMS4zODggMS4xNzggMy4xNDggMS4xMzcgMi42NjItLjA2MiA2Ljk3OS0uNjgxIDcuNjQ5LTEuOTIxLjA0LS4wNzUuMDc1LS4xNjQuMTA1LS4yNjYgMy4zODggMS4yNjYgOS4zNSAyLjYwNiAxMC42ODIgMi40MDYgMy40Ny0uNTIxLS40ODQtMTYuNzIzLTIuMDktMTcuNDY3eiIgZmlsbD0iIzNjYTgyYiIvPgogICAgPHBhdGggZD0ibTc0LjQ5IDk3LjA5N2MuMTQ0LjI1Ni4yNi41MjYuMzU4LjguNDgzIDEuMzUyIDEuMjcgNS42NDguNjc0IDYuNzA5LS41OTUgMS4wNjItNC40NTkgMS41NzQtNi44NDMgMS42MTVzLTIuOTItLjgzMS0zLjQwMy0yLjE4MWMtLjM4Ny0xLjA4MS0uNTc3LTMuNjIxLS41NzItNS4wNzUtLjA5OC0yLjE1OC42OS0yLjkxNiA0LjMzNC0zLjUwNiAyLjY5Ni0uNDM2IDQuMTIxLjA3MSA0Ljk0NC45NCAzLjgyOC0yLjg1NyAxMC4yMTUtNi44ODkgMTAuODM4LTYuMTUyIDMuMTA2IDMuNjc0IDMuNDk5IDEyLjQyIDIuODI2IDE1LjkzOS0uMjIgMS4xNTEtMTAuNTA1LTEuMTM5LTEwLjUwNS0yLjM4IDAtNS4xNTItMS4zMzctNi41NjUtMi42NS02Ljcxem0tMjIuNTMtMS42MDljLjg0My0xLjMzMyA3LjY3NC4zMjUgMTEuNDI0IDEuOTkzIDAgMC0uNzcgMy40OTEuNDU2IDcuNjA0LjM1OSAxLjIwMy04LjYyNyA2LjU1OC05LjggNS42MzctMS4zNTUtMS4wNjUtMy44NS0xMi40MzItMi4wOC0xNS4yMzR6IiBmaWxsPSIjNGNiYTNjIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im01NS4yNjkgNjguNDA2Yy41NTMtMi40MDMgMy4xMjctNi45MzIgMTIuMzIxLTYuODIyIDQuNjQ4LS4wMTkgMTAuNDIyLS4wMDIgMTQuMjUtLjQzNmE1MS4zMTIgNTEuMzEyIDAgMCAwIDEyLjcyNi0zLjA5NWMzLjk4LTEuNTE5IDUuMzkyLTEuMTggNS44ODctLjI3Mi41NDQuOTk5LS4wOTcgMi43MjItMS40ODggNC4zMDktMi42NTYgMy4wMy03LjQzMSA1LjM4LTE1Ljg2NSA2LjA3Ni04LjQzMy42OTgtMTQuMDItMS41NjUtMTYuNDI1IDIuMTE4LTEuMDM4IDEuNTg5LS4yMzYgNS4zMzMgNy45MiA2LjUxMiAxMS4wMiAxLjU5IDIwLjA3Mi0xLjkxNyAyMS4xOS4yMDEgMS4xMTkgMi4xMTgtNS4zMjMgNi40MjgtMTYuMzYyIDYuNTE4cy0xNy45MzQtMy44NjUtMjAuMzc5LTUuODNjLTMuMTAyLTIuNDk1LTQuNDktNi4xMzMtMy43NzUtOS4yNzl6IiBmaWxsPSIjZmMzIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICAgIDxnIGZpbGw9IiMxNDMwN2UiIG9wYWNpdHk9Ii44Ij4KICAgICAgPHBhdGggZD0ibTY5LjMyNyA0Mi4xMjdjLjYxNi0xLjAwOCAxLjk4MS0xLjc4NiA0LjIxNi0xLjc4NiAyLjIzNCAwIDMuMjg1Ljg4OSA0LjAxMyAxLjg4LjE0OC4yMDItLjA3Ni40NC0uMzA2LjM0YTU5Ljg2OSA1OS44NjkgMCAwIDEgLS4xNjgtLjA3M2MtLjgxNy0uMzU3LTEuODItLjc5NS0zLjU0LS44Mi0xLjgzOC0uMDI2LTIuOTk3LjQzNS0zLjcyNy44MzEtLjI0Ni4xMzQtLjYzNC0uMTMzLS40ODgtLjM3MnptLTI1LjE1NyAxLjI5YzIuMTctLjkwNyAzLjg3Ni0uNzkgNS4wODEtLjUwNC4yNTQuMDYuNDMtLjIxMy4yMjctLjM3Ny0uOTM1LS43NTUtMy4wMy0xLjY5Mi01Ljc2LS42NzQtMi40MzcuOTA5LTMuNTg1IDIuNzk2LTMuNTkyIDQuMDM4LS4wMDIuMjkyLjYuMzE3Ljc1Ni4wNy40Mi0uNjcgMS4xMi0xLjY0NiAzLjI4OS0yLjU1M3oiLz4KICAgICAgPHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNzUuNDQgNTUuOTJhMy40NyAzLjQ3IDAgMCAxIC0zLjQ3NC0zLjQ2MiAzLjQ3IDMuNDcgMCAwIDEgMy40NzUtMy40NiAzLjQ3IDMuNDcgMCAwIDEgMy40NzQgMy40NiAzLjQ3IDMuNDcgMCAwIDEgLTMuNDc1IDMuNDYyem0yLjQ0Ny00LjYwOGEuODk5Ljg5OSAwIDAgMCAtMS43OTkgMGMwIC40OTQuNDA1Ljg5NS45Ljg5NS40OTkgMCAuOS0uNC45LS44OTV6bS0yNS40NjQgMy41NDJhNC4wNDIgNC4wNDIgMCAwIDEgLTQuMDQ5IDQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIC00LjA1LTQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIDQuMDUtNC4wMzcgNC4wNDUgNC4wNDUgMCAwIDEgNC4wNSA0LjAzN3ptLTEuMTkzLTEuMzM4YTEuMDUgMS4wNSAwIDAgMCAtMi4wOTcgMCAxLjA0OCAxLjA0OCAwIDAgMCAyLjA5NyAweiIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8L2c+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im02NCAxMTcuNzVjMjkuNjg1IDAgNTMuNzUtMjQuMDY1IDUzLjc1LTUzLjc1cy0yNC4wNjUtNTMuNzUtNTMuNzUtNTMuNzUtNTMuNzUgMjQuMDY1LTUzLjc1IDUzLjc1IDI0LjA2NSA1My43NSA1My43NSA1My43NXptMCA1YzMyLjQ0NyAwIDU4Ljc1LTI2LjMwMyA1OC43NS01OC43NXMtMjYuMzAzLTU4Ljc1LTU4Ljc1LTU4Ljc1LTU4Ljc1IDI2LjMwMy01OC43NSA1OC43NSAyNi4zMDMgNTguNzUgNTguNzUgNTguNzV6IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+');\n}\n\n/* Email tooltip specific */\n.tooltip__button--email {\n flex-direction: column;\n justify-content: center;\n align-items: flex-start;\n font-size: 14px;\n padding: 4px 8px;\n}\n.tooltip__button--email__primary-text {\n font-weight: bold;\n}\n.tooltip__button--email__secondary-text {\n font-size: 12px;\n}\n\n/* Email Protection signup notice */\n:not(.top-autofill) .tooltip--email-signup {\n text-align: left;\n color: #222222;\n padding: 16px 20px;\n width: 380px;\n}\n\n.tooltip--email-signup h1 {\n font-weight: 700;\n font-size: 16px;\n line-height: 1.5;\n margin: 0;\n}\n\n.tooltip--email-signup p {\n font-weight: 400;\n font-size: 14px;\n line-height: 1.4;\n}\n\n.notice-controls {\n display: flex;\n}\n\n.tooltip--email-signup .notice-controls > * {\n border-radius: 8px;\n border: 0;\n cursor: pointer;\n display: inline-block;\n font-family: inherit;\n font-style: normal;\n font-weight: bold;\n padding: 8px 12px;\n text-decoration: none;\n}\n\n.notice-controls .ghost {\n margin-left: 1rem;\n}\n\n.tooltip--email-signup a.primary {\n background: #3969EF;\n color: #fff;\n}\n\n.tooltip--email-signup a.primary:hover,\n.tooltip--email-signup a.primary:focus {\n background: #2b55ca;\n}\n\n.tooltip--email-signup a.primary:active {\n background: #1e42a4;\n}\n\n.tooltip--email-signup button.ghost {\n background: transparent;\n color: #3969EF;\n}\n\n.tooltip--email-signup button.ghost:hover,\n.tooltip--email-signup button.ghost:focus {\n background-color: rgba(0, 0, 0, 0.06);\n color: #2b55ca;\n}\n\n.tooltip--email-signup button.ghost:active {\n background-color: rgba(0, 0, 0, 0.12);\n color: #1e42a4;\n}\n\n.tooltip--email-signup button.close-tooltip {\n background-color: transparent;\n background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMiAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5NCAwLjY1NjkwN0MwLjY4MzQxOCAwLjI2NjM4MyAxLjMxNjU4IDAuMjY2MzgzIDEuNzA3MTEgMC42NTY5MDdMNiA0Ljk0OThMMTAuMjkyOSAwLjY1NjkwN0MxMC42ODM0IDAuMjY2MzgzIDExLjMxNjYgMC4yNjYzODMgMTEuNzA3MSAwLjY1NjkwN0MxMi4wOTc2IDEuMDQ3NDMgMTIuMDk3NiAxLjY4MDYgMTEuNzA3MSAyLjA3MTEyTDcuNDE0MjEgNi4zNjQwMUwxMS43MDcxIDEwLjY1NjlDMTIuMDk3NiAxMS4wNDc0IDEyLjA5NzYgMTEuNjgwNiAxMS43MDcxIDEyLjA3MTFDMTEuMzE2NiAxMi40NjE2IDEwLjY4MzQgMTIuNDYxNiAxMC4yOTI5IDEyLjA3MTFMNiA3Ljc3ODIzTDEuNzA3MTEgMTIuMDcxMUMxLjMxNjU4IDEyLjQ2MTYgMC42ODM0MTcgMTIuNDYxNiAwLjI5Mjg5MyAxMi4wNzExQy0wLjA5NzYzMTEgMTEuNjgwNiAtMC4wOTc2MzExIDExLjA0NzQgMC4yOTI4OTMgMTAuNjU2OUw0LjU4NTc5IDYuMzY0MDFMMC4yOTI4OTQgMi4wNzExMkMtMC4wOTc2MzA2IDEuNjgwNiAtMC4wOTc2MzA2IDEuMDQ3NDMgMC4yOTI4OTQgMC42NTY5MDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg0Ii8+Cjwvc3ZnPgo=);\n background-position: center center;\n background-repeat: no-repeat;\n border: 0;\n cursor: pointer;\n padding: 16px;\n position: absolute;\n right: 12px;\n top: 12px;\n}\n\n/* Import promotion prompt icon style */\n\n.tooltip__button--credentials-import::before {\n content: \"\";\n background-image: url(data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03Ljk3MjY5IDkuMDEzN0M3Ljk3MjY5IDUuMTQwMTQgMTEuMTEyOCAyIDE0Ljk4NjQgMkMxOC44NTk5IDIgMjIuMDAwMSA1LjE0MDE0IDIyLjAwMDEgOS4wMTM3QzIyLjAwMDEgOS44MDM5NyAyMS44Njk0IDEwLjU2MzcgMjEuNjI4NCAxMS4yNzI1QzIxLjQ2MDEgMTEuNzY3NiAyMC44NjU2IDExLjkxMzUgMjAuMzk3NyAxMS42OEMxOS45MjIxIDExLjQ0MjggMTkuNjc3OSAxMC44NjQ0IDE5LjgxOTUgMTAuMzUyQzE5LjkzNzIgOS45MjU5OSAyMC4wMDAxIDkuNDc3MTkgMjAuMDAwMSA5LjAxMzdDMjAuMDAwMSA2LjI0NDcxIDE3Ljc1NTQgNCAxNC45ODY0IDRDMTIuMjE3NCA0IDkuOTcyNjkgNi4yNDQ3MSA5Ljk3MjY5IDkuMDEzN0M5Ljk3MjY5IDkuNTU1NCAxMC4wNTgyIDEwLjA3NTIgMTAuMjE1OCAxMC41NjEzQzEwLjMzMTcgMTAuOTE4OCAxMC4yMzc0IDExLjMxMTEgOS45NzE2NSAxMS41NzY4TDMuOTk5OTQgMTcuNTQ4NVYyMC4wMDAyTDYuNDk5OTggMjAuMDAwMlYxOC4zMDAyQzYuNDk5OTggMTcuNzQ3OSA2Ljk0NzcgMTcuMzAwMiA3LjQ5OTk4IDE3LjMwMDJIOS4yMDAwMVYxNS42MDAxQzkuMjAwMDEgMTUuMDQ3OCA5LjY0NzczIDE0LjYwMDEgMTAuMiAxNC42MDAxSDEwLjVDMTEuMjEwNiAxNC41ODcgMTEuNDI1MiAxNS4zMzY5IDExLjMxNTYgMTUuNzMzMUMxMS4xNTY1IDE2LjMwODUgMTEgMTcuMDAxIDExIDE3LjVDMTEgMTcuNzI4NCAxMS4wMTE4IDE3Ljk1NCAxMS4wMzQ4IDE4LjE3NjNDMTAuNzM3OSAxOC44Mzg3IDEwLjA3MjkgMTkuMzAwMiA5LjMgMTkuMzAwMkg4LjQ5OTk4VjIwLjEwMDJDOC40OTk5OCAyMS4xNDk1IDcuNjQ5MzMgMjIuMDAwMiA2LjU5OTk5IDIyLjAwMDJMMi45OTk5NSAyMi4wMDAyQzIuNzM0NzMgMjIuMDAwMiAyLjQ4MDM3IDIxLjg5NDkgMi4yOTI4NCAyMS43MDczQzIuMTA1MyAyMS41MTk4IDEuOTk5OTQgMjEuMjY1NCAxLjk5OTk0IDIxLjAwMDJWMTcuMTM0M0MxLjk5OTk0IDE2Ljg2OTEgMi4xMDUzIDE2LjYxNDcgMi4yOTI4MyAxNi40MjcyTDguMTQ2ODkgMTAuNTczMUM4LjAzMjc5IDEwLjA3MDkgNy45NzI2OSA5LjU0ODgxIDcuOTcyNjkgOS4wMTM3WiIgZmlsbD0iYmxhY2siIC8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMgOC40OTk4OUMxMyA3LjExOTE4IDE0LjExOTMgNS45OTk4OSAxNS41IDUuOTk5ODlDMTYuODgwNyA1Ljk5OTg5IDE4IDcuMTE5MTggMTggOC40OTk4OUMxOCA5Ljg4MDYgMTYuODgwNyAxMC45OTk5IDE1LjUgMTAuOTk5OUMxNC4xMTkzIDEwLjk5OTkgMTMgOS44ODA2IDEzIDguNDk5ODlaTTE1LjUgNy45OTk4OUMxNS4yMjM5IDcuOTk5ODkgMTUgOC4yMjM3NSAxNSA4LjQ5OTg5QzE1IDguNzc2MDQgMTUuMjIzOSA4Ljk5OTg5IDE1LjUgOC45OTk4OUMxNS43NzYyIDguOTk5ODkgMTYgOC43NzYwNCAxNiA4LjQ5OTg5QzE2IDguMjIzNzUgMTUuNzc2MiA3Ljk5OTg5IDE1LjUgNy45OTk4OVoiIGZpbGw9ImJsYWNrIiAvPgo8cGF0aCBkPSJNMTcgMTVMMTQuNSAxNy41TDE3IDIwIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMjEuNSAxNy41TDE2IDE3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);\n background-repeat: no-repeat;\n}\n"; -},{}],65:[function(require,module,exports){ +},{}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17626,7 +17492,7 @@ function findEnclosedElements(root, selector) { return shadowElements; } -},{"./Form/matching.js":45,"./constants.js":68,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],66:[function(require,module,exports){ +},{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -17657,7 +17523,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":24,"./autofill-utils.js":65,"./requestIdleCallback.js":105}],67:[function(require,module,exports){ +},{"./DeviceInterface.js":23,"./autofill-utils.js":64,"./requestIdleCallback.js":104}],66:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17674,10 +17540,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; @@ -17747,7 +17609,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],68:[function(require,module,exports){ +},{}],67:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17764,13 +17626,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],69:[function(require,module,exports){ +},{}],68:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAliasCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.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 */ @@ -17931,20 +17793,10 @@ exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; class StartCredentialsImportFlowCall extends _deviceApi.DeviceApiCall { method = "startCredentialsImportFlow"; } -/** - * @extends {DeviceApiCall} - */ -exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; -class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { - method = "emailProtectionGetAlias"; - id = "emailProtectionGetAliasResponse"; - paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; - resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; -} /** * @extends {DeviceApiCall} */ -exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; +exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -18027,13 +17879,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":12,"./validators.zod.js":70}],70:[function(require,module,exports){ +},{"../../../packages/device-api":12,"./validators.zod.js":69}],69:[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 @@ -18093,11 +17945,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(), @@ -18152,6 +17999,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(), @@ -18184,10 +18035,6 @@ const availableInputTypesSchema = exports.availableInputTypesSchema = _zod.z.obj credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); -const outgoingCredentialsSchema = exports.outgoingCredentialsSchema = _zod.z.object({ - username: _zod.z.string().optional(), - password: _zod.z.string().optional() -}); const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.object({ credentials: _zod.z.object({ username: _zod.z.boolean().optional(), @@ -18220,11 +18067,6 @@ const availableInputTypes1Schema = exports.availableInputTypes1Schema = _zod.z.o credentialsProviderStatus: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]).optional(), credentialsImport: _zod.z.boolean().optional() }); -const providerStatusUpdatedSchema = exports.providerStatusUpdatedSchema = _zod.z.object({ - status: _zod.z.union([_zod.z.literal("locked"), _zod.z.literal("unlocked")]), - credentials: _zod.z.array(credentialsSchema), - availableInputTypes: availableInputTypesSchema -}); const autofillFeatureTogglesSchema = exports.autofillFeatureTogglesSchema = _zod.z.object({ inputType_credentials: _zod.z.boolean().optional(), inputType_identities: _zod.z.boolean().optional(), @@ -18260,7 +18102,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({ @@ -18283,25 +18125,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() @@ -18339,14 +18165,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() @@ -18414,11 +18254,6 @@ const apiSchema = exports.apiSchema = _zod.z.object({ openManageCreditCards: _zod.z.record(_zod.z.unknown()).optional(), openManageIdentities: _zod.z.record(_zod.z.unknown()).optional(), startCredentialsImportFlow: _zod.z.record(_zod.z.unknown()).optional(), - emailProtectionGetAlias: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ - id: _zod.z.literal("emailProtectionGetAliasResponse").optional(), - paramsValidator: emailProtectionGetAliasParamsSchema.optional(), - resultValidator: emailProtectionGetAliasResultSchema.optional() - })).optional(), emailProtectionStoreUserData: _zod.z.record(_zod.z.unknown()).and(_zod.z.object({ id: _zod.z.literal("emailProtectionStoreUserDataResponse").optional(), paramsValidator: emailProtectionStoreUserDataParamsSchema.optional() @@ -18452,7 +18287,7 @@ const apiSchema = exports.apiSchema = _zod.z.object({ })).optional() }); -},{"zod":9}],71:[function(require,module,exports){ +},{"zod":9}],70:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18478,7 +18313,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":70}],72:[function(require,module,exports){ +},{"../../packages/device-api/index.js":12,"./__generated__/validators.zod.js":69}],71:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18486,8 +18321,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; @@ -18496,39 +18330,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}],73:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../__generated__/deviceApiCalls.js":68}],72:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18571,7 +18499,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":16}],74:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../../packages/messaging/messaging.js":15}],73:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18725,7 +18653,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":12,"../../Settings.js":52,"../../autofill-utils.js":65,"../__generated__/deviceApiCalls.js":69}],75:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12,"../../Settings.js":51,"../../autofill-utils.js":64,"../__generated__/deviceApiCalls.js":68}],74:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18769,7 +18697,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":72,"./apple.transport.js":73,"./extension.transport.js":74,"./windows.transport.js":76}],76:[function(require,module,exports){ +},{"./android.transport.js":71,"./apple.transport.js":72,"./extension.transport.js":73,"./windows.transport.js":75}],75:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -18854,7 +18782,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":12}],77:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":12}],76:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -18955,7 +18883,7 @@ module.exports={ } } -},{}],78:[function(require,module,exports){ +},{}],77:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19056,7 +18984,7 @@ module.exports={ } } -},{}],79:[function(require,module,exports){ +},{}],78:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19157,7 +19085,7 @@ module.exports={ } } -},{}],80:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19258,7 +19186,7 @@ module.exports={ } } -},{}],81:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19359,7 +19287,7 @@ module.exports={ } } -},{}],82:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -19461,7 +19389,7 @@ module.exports={ } } -},{}],83:[function(require,module,exports){ +},{}],82:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19562,7 +19490,7 @@ module.exports={ } } -},{}],84:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19663,7 +19591,7 @@ module.exports={ } } -},{}],85:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19764,7 +19692,7 @@ module.exports={ } } -},{}],86:[function(require,module,exports){ +},{}],85:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19865,7 +19793,7 @@ module.exports={ } } -},{}],87:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -19966,7 +19894,7 @@ module.exports={ } } -},{}],88:[function(require,module,exports){ +},{}],87:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20067,7 +19995,7 @@ module.exports={ } } -},{}],89:[function(require,module,exports){ +},{}],88:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20168,7 +20096,7 @@ module.exports={ } } -},{}],90:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20269,7 +20197,7 @@ module.exports={ } } -},{}],91:[function(require,module,exports){ +},{}],90:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20370,7 +20298,7 @@ module.exports={ } } -},{}],92:[function(require,module,exports){ +},{}],91:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20471,7 +20399,7 @@ module.exports={ } } -},{}],93:[function(require,module,exports){ +},{}],92:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20572,7 +20500,7 @@ module.exports={ } } -},{}],94:[function(require,module,exports){ +},{}],93:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20673,7 +20601,7 @@ module.exports={ } } -},{}],95:[function(require,module,exports){ +},{}],94:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20774,7 +20702,7 @@ module.exports={ } } -},{}],96:[function(require,module,exports){ +},{}],95:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20875,7 +20803,7 @@ module.exports={ } } -},{}],97:[function(require,module,exports){ +},{}],96:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -20976,7 +20904,7 @@ module.exports={ } } -},{}],98:[function(require,module,exports){ +},{}],97:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21077,7 +21005,7 @@ module.exports={ } } -},{}],99:[function(require,module,exports){ +},{}],98:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21178,7 +21106,7 @@ module.exports={ } } -},{}],100:[function(require,module,exports){ +},{}],99:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21266,7 +21194,7 @@ function translateImpl(library, namespacedId, opts) { return out; } -},{"./translations.js":103}],101:[function(require,module,exports){ +},{"./translations.js":102}],100:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21367,7 +21295,7 @@ module.exports={ } } -},{}],102:[function(require,module,exports){ +},{}],101:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -21468,7 +21396,7 @@ module.exports={ } } -},{}],103:[function(require,module,exports){ +},{}],102:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21587,7 +21515,7 @@ var _default = exports.default = { } }; -},{"./bg/autofill.json":77,"./cs/autofill.json":78,"./da/autofill.json":79,"./de/autofill.json":80,"./el/autofill.json":81,"./en/autofill.json":82,"./es/autofill.json":83,"./et/autofill.json":84,"./fi/autofill.json":85,"./fr/autofill.json":86,"./hr/autofill.json":87,"./hu/autofill.json":88,"./it/autofill.json":89,"./lt/autofill.json":90,"./lv/autofill.json":91,"./nb/autofill.json":92,"./nl/autofill.json":93,"./pl/autofill.json":94,"./pt/autofill.json":95,"./ro/autofill.json":96,"./ru/autofill.json":97,"./sk/autofill.json":98,"./sl/autofill.json":99,"./sv/autofill.json":101,"./tr/autofill.json":102,"./xa/autofill.json":104}],104:[function(require,module,exports){ +},{"./bg/autofill.json":76,"./cs/autofill.json":77,"./da/autofill.json":78,"./de/autofill.json":79,"./el/autofill.json":80,"./en/autofill.json":81,"./es/autofill.json":82,"./et/autofill.json":83,"./fi/autofill.json":84,"./fr/autofill.json":85,"./hr/autofill.json":86,"./hu/autofill.json":87,"./it/autofill.json":88,"./lt/autofill.json":89,"./lv/autofill.json":90,"./nb/autofill.json":91,"./nl/autofill.json":92,"./pl/autofill.json":93,"./pt/autofill.json":94,"./ro/autofill.json":95,"./ru/autofill.json":96,"./sk/autofill.json":97,"./sl/autofill.json":98,"./sv/autofill.json":100,"./tr/autofill.json":101,"./xa/autofill.json":103}],103:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -21680,7 +21608,7 @@ module.exports={ "note": "Button that prevents the DuckDuckGo email protection signup prompt from appearing again." } } -},{}],105:[function(require,module,exports){ +},{}],104:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -21723,4 +21651,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[66]); +},{}]},{},[65]); diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js index e07247a53195..2552b407a2e9 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;" @@ -3427,7 +3299,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", { @@ -3491,7 +3363,7 @@ class CredentialsImport { } exports.CredentialsImport = CredentialsImport; -},{"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],14:[function(require,module,exports){ +},{"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],13:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3547,7 +3419,7 @@ function createDevice() { return new _ExtensionInterface.ExtensionInterface(globalConfig, deviceApi, settings); } -},{"../packages/device-api/index.js":2,"./DeviceInterface/AndroidInterface.js":15,"./DeviceInterface/AppleDeviceInterface.js":16,"./DeviceInterface/AppleOverlayDeviceInterface.js":17,"./DeviceInterface/ExtensionInterface.js":18,"./DeviceInterface/WindowsInterface.js":20,"./DeviceInterface/WindowsOverlayDeviceInterface.js":21,"./Settings.js":42,"./config.js":57,"./deviceApiCalls/transports/transports.js":65}],15:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./DeviceInterface/AndroidInterface.js":14,"./DeviceInterface/AppleDeviceInterface.js":15,"./DeviceInterface/AppleOverlayDeviceInterface.js":16,"./DeviceInterface/ExtensionInterface.js":17,"./DeviceInterface/WindowsInterface.js":19,"./DeviceInterface/WindowsOverlayDeviceInterface.js":20,"./Settings.js":41,"./config.js":56,"./deviceApiCalls/transports/transports.js":64}],14:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -3567,35 +3439,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; } /** @@ -3610,9 +3472,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 @@ -3627,7 +3494,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); } /** @@ -3635,13 +3510,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); } /** @@ -3649,7 +3536,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); + } + } } /** @@ -3674,7 +3567,7 @@ class AndroidInterface extends _InterfacePrototype.default { } exports.AndroidInterface = AndroidInterface; -},{"../InContextSignup.js":36,"../UI/controllers/NativeUIController.js":50,"../autofill-utils.js":55,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19}],16:[function(require,module,exports){ +},{"../InContextSignup.js":35,"../UI/controllers/NativeUIController.js":49,"../autofill-utils.js":54,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18}],15:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4030,7 +3923,7 @@ class AppleDeviceInterface extends _InterfacePrototype.default { } exports.AppleDeviceInterface = AppleDeviceInterface; -},{"../../packages/device-api/index.js":2,"../Form/matching.js":35,"../InContextSignup.js":36,"../ThirdPartyProvider.js":43,"../UI/HTMLTooltip.js":48,"../UI/controllers/HTMLTooltipUIController.js":49,"../UI/controllers/NativeUIController.js":50,"../UI/controllers/OverlayUIController.js":51,"../autofill-utils.js":55,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"../deviceApiCalls/additionalDeviceApiCalls.js":61,"./InterfacePrototype.js":19}],17:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../Form/matching.js":34,"../InContextSignup.js":35,"../ThirdPartyProvider.js":42,"../UI/HTMLTooltip.js":47,"../UI/controllers/HTMLTooltipUIController.js":48,"../UI/controllers/NativeUIController.js":49,"../UI/controllers/OverlayUIController.js":50,"../autofill-utils.js":54,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"../deviceApiCalls/additionalDeviceApiCalls.js":60,"./InterfacePrototype.js":18}],16:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4149,7 +4042,7 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; -},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":49,"./AppleDeviceInterface.js":16,"./overlayApi.js":23}],18:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../UI/controllers/HTMLTooltipUIController.js":48,"./AppleDeviceInterface.js":15,"./overlayApi.js":22}],17:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4368,7 +4261,7 @@ class ExtensionInterface extends _InterfacePrototype.default { } exports.ExtensionInterface = ExtensionInterface; -},{"../Form/matching.js":35,"../InContextSignup.js":36,"../UI/HTMLTooltip.js":48,"../UI/controllers/HTMLTooltipUIController.js":49,"../autofill-utils.js":55,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ +},{"../Form/matching.js":34,"../InContextSignup.js":35,"../UI/HTMLTooltip.js":47,"../UI/controllers/HTMLTooltipUIController.js":48,"../autofill-utils.js":54,"./InterfacePrototype.js":18}],18:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -4637,7 +4530,8 @@ class InterfacePrototype { } async startInit() { if (this.isInitializationStarted) return; - this.isInitializationStarted = true; + this.alreadyInitialized = true; + await this.settings.refresh(); this.addDeviceListeners(); await this.setupAutofill(); this.uiController = this.createUIController(); @@ -4971,19 +4865,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) { @@ -5039,13 +4925,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({ @@ -5214,7 +5093,7 @@ class InterfacePrototype { } var _default = exports.default = InterfacePrototype; -},{"../../packages/device-api/index.js":2,"../CredentialsImport.js":13,"../EmailProtection.js":24,"../Form/formatters.js":28,"../Form/matching.js":35,"../InputTypes/Credentials.js":37,"../PasswordGenerator.js":40,"../Scanner.js":41,"../Settings.js":42,"../UI/controllers/NativeUIController.js":50,"../autofill-utils.js":55,"../config.js":57,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"../deviceApiCalls/transports/transports.js":65,"../locales/strings.js":90,"./initFormSubmissionsApi.js":22}],20:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"../CredentialsImport.js":12,"../EmailProtection.js":23,"../Form/formatters.js":27,"../Form/matching.js":34,"../InputTypes/Credentials.js":36,"../PasswordGenerator.js":39,"../Scanner.js":40,"../Settings.js":41,"../UI/controllers/NativeUIController.js":49,"../autofill-utils.js":54,"../config.js":56,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"../deviceApiCalls/transports/transports.js":64,"../locales/strings.js":89,"./initFormSubmissionsApi.js":21}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5299,13 +5178,13 @@ class WindowsInterface extends _InterfacePrototype.default { return await this.credentialsImport.refresh(); } 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 instanceof DOMException && e.name === 'AbortError') { console.log('Promise Aborted'); } else { @@ -5380,7 +5259,7 @@ class WindowsInterface extends _InterfacePrototype.default { } exports.WindowsInterface = WindowsInterface; -},{"../UI/controllers/OverlayUIController.js":51,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19}],21:[function(require,module,exports){ +},{"../UI/controllers/OverlayUIController.js":50,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18}],20:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5559,7 +5438,7 @@ class WindowsOverlayDeviceInterface extends _InterfacePrototype.default { } exports.WindowsOverlayDeviceInterface = WindowsOverlayDeviceInterface; -},{"../UI/controllers/HTMLTooltipUIController.js":49,"../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./InterfacePrototype.js":19,"./overlayApi.js":23}],22:[function(require,module,exports){ +},{"../UI/controllers/HTMLTooltipUIController.js":48,"../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./InterfacePrototype.js":18,"./overlayApi.js":22}],21:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5658,7 +5537,7 @@ function initFormSubmissionsApi(forms, matching) { }); } -},{"../Form/label-util.js":31,"../autofill-utils.js":55}],23:[function(require,module,exports){ +},{"../Form/label-util.js":30,"../autofill-utils.js":54}],22:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5716,7 +5595,7 @@ function overlayApi(device) { }; } -},{"../deviceApiCalls/__generated__/deviceApiCalls.js":59}],24:[function(require,module,exports){ +},{"../deviceApiCalls/__generated__/deviceApiCalls.js":58}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5751,7 +5630,7 @@ class EmailProtection { } exports.EmailProtection = EmailProtection; -},{}],25:[function(require,module,exports){ +},{}],24:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5891,7 +5770,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; @@ -6658,7 +6537,7 @@ class Form { } exports.Form = Form; -},{"../InputTypes/Credentials.js":37,"../autofill-utils.js":55,"../constants.js":58,"./FormAnalyzer.js":26,"./formatters.js":28,"./inputStyles.js":29,"./inputTypeConfig.js":30,"./matching.js":35}],26:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":54,"../constants.js":57,"./FormAnalyzer.js":25,"./formatters.js":27,"./inputStyles.js":28,"./inputTypeConfig.js":29,"./matching.js":34}],25:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7028,7 +6907,7 @@ class FormAnalyzer { } var _default = exports.default = FormAnalyzer; -},{"../autofill-utils.js":55,"../constants.js":58,"./matching-config/__generated__/compiled-matching-config.js":33,"./matching.js":35}],27:[function(require,module,exports){ +},{"../autofill-utils.js":54,"../constants.js":57,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching.js":34}],26:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7593,7 +7472,7 @@ const COUNTRY_NAMES_TO_CODES = exports.COUNTRY_NAMES_TO_CODES = { 'Unknown Region': 'ZZ' }; -},{}],28:[function(require,module,exports){ +},{}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7898,7 +7777,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":27,"./matching.js":35}],29:[function(require,module,exports){ +},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -7989,7 +7868,7 @@ const getIconStylesAutofilled = (input, form) => { }; exports.getIconStylesAutofilled = getIconStylesAutofilled; -},{"./inputTypeConfig.js":30}],30:[function(require,module,exports){ +},{"./inputTypeConfig.js":29}],29:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8243,7 +8122,7 @@ const isFieldDecorated = input => { }; exports.isFieldDecorated = isFieldDecorated; -},{"../InputTypes/Credentials.js":37,"../InputTypes/CreditCard.js":38,"../InputTypes/Identity.js":39,"../UI/img/ddgPasswordIcon.js":53,"../constants.js":58,"./logo-svg.js":32,"./matching.js":35}],31:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":36,"../InputTypes/CreditCard.js":37,"../InputTypes/Identity.js":38,"../UI/img/ddgPasswordIcon.js":52,"../constants.js":57,"./logo-svg.js":31,"./matching.js":34}],30:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8291,7 +8170,7 @@ const extractElementStrings = element => { }; exports.extractElementStrings = extractElementStrings; -},{"./matching.js":35}],32:[function(require,module,exports){ +},{"./matching.js":34}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8324,7 +8203,7 @@ const daxGrayscaleSvg = ` `.trim(); const daxGrayscaleBase64 = exports.daxGrayscaleBase64 = `data:image/svg+xml;base64,${window.btoa(daxGrayscaleSvg)}`; -},{}],33:[function(require,module,exports){ +},{}],32:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8777,7 +8656,7 @@ const matchingConfiguration = exports.matchingConfiguration = { } }; -},{}],34:[function(require,module,exports){ +},{}],33:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8852,7 +8731,7 @@ function logUnmatched(el, allStrings) { console.groupEnd(); } -},{"../autofill-utils.js":55,"./matching.js":35}],35:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./matching.js":34}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9844,7 +9723,7 @@ function createMatching() { return new Matching(_compiledMatchingConfig.matchingConfiguration); } -},{"../autofill-utils.js":55,"../constants.js":58,"./label-util.js":31,"./matching-config/__generated__/compiled-matching-config.js":33,"./matching-utils.js":34}],36:[function(require,module,exports){ +},{"../autofill-utils.js":54,"../constants.js":57,"./label-util.js":30,"./matching-config/__generated__/compiled-matching-config.js":32,"./matching-utils.js":33}],35:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -9976,7 +9855,7 @@ class InContextSignup { } exports.InContextSignup = InContextSignup; -},{"./autofill-utils.js":55,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],37:[function(require,module,exports){ +},{"./autofill-utils.js":54,"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10132,7 +10011,7 @@ function createCredentialsTooltipItem(data) { return new CredentialsTooltipItem(data); } -},{"../autofill-utils.js":55}],38:[function(require,module,exports){ +},{"../autofill-utils.js":54}],37:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10157,7 +10036,7 @@ class CreditCardTooltipItem { } exports.CreditCardTooltipItem = CreditCardTooltipItem; -},{}],39:[function(require,module,exports){ +},{}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10203,7 +10082,7 @@ class IdentityTooltipItem { } exports.IdentityTooltipItem = IdentityTooltipItem; -},{"../Form/formatters.js":28}],40:[function(require,module,exports){ +},{"../Form/formatters.js":27}],39:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10245,7 +10124,7 @@ class PasswordGenerator { } exports.PasswordGenerator = PasswordGenerator; -},{"../packages/password/index.js":8,"../packages/password/rules.json":12}],41:[function(require,module,exports){ +},{"../packages/password/index.js":7,"../packages/password/rules.json":11}],40:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10706,7 +10585,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":25,"./Form/matching.js":35,"./autofill-utils.js":55,"./constants.js":58,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],42:[function(require,module,exports){ +},{"./Form/Form.js":24,"./Form/matching.js":34,"./autofill-utils.js":54,"./constants.js":57,"./deviceApiCalls/__generated__/deviceApiCalls.js":58}],41:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10852,11 +10731,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; } @@ -10872,9 +10746,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) { @@ -11127,7 +10998,7 @@ class Settings { } exports.Settings = Settings; -},{"../packages/device-api/index.js":2,"./autofill-utils.js":55,"./deviceApiCalls/__generated__/deviceApiCalls.js":59,"./deviceApiCalls/__generated__/validators.zod.js":60}],43:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./autofill-utils.js":54,"./deviceApiCalls/__generated__/deviceApiCalls.js":58,"./deviceApiCalls/__generated__/validators.zod.js":59}],42:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11194,7 +11065,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); } } @@ -11209,7 +11080,7 @@ class ThirdPartyProvider { } setTimeout(() => this._pollForUpdatesToCredentialsProvider(), 2000); } catch (e) { - if (this.device.isTestMode()) { + if (this.device.globalConfig.isDDGTestMode) { console.log('isDDGTestMode: _pollForUpdatesToCredentialsProvider: ❌', e); } } @@ -11217,7 +11088,7 @@ class ThirdPartyProvider { } exports.ThirdPartyProvider = ThirdPartyProvider; -},{"../packages/device-api/index.js":2,"./Form/matching.js":35,"./deviceApiCalls/__generated__/deviceApiCalls.js":59,"./deviceApiCalls/__generated__/validators.zod.js":60}],44:[function(require,module,exports){ +},{"../packages/device-api/index.js":2,"./Form/matching.js":34,"./deviceApiCalls/__generated__/deviceApiCalls.js":58,"./deviceApiCalls/__generated__/validators.zod.js":59}],43:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11258,7 +11129,7 @@ ${this.options.css} } var _default = exports.default = CredentialsImportTooltip; -},{"./HTMLTooltip.js":48}],45:[function(require,module,exports){ +},{"./HTMLTooltip.js":47}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11409,7 +11280,7 @@ ${css} } var _default = exports.default = DataHTMLTooltip; -},{"../InputTypes/Credentials.js":37,"../autofill-utils.js":55,"./HTMLTooltip.js":48}],46:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":36,"../autofill-utils.js":54,"./HTMLTooltip.js":47}],45:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11491,7 +11362,7 @@ ${this.options.css} } var _default = exports.default = EmailHTMLTooltip; -},{"../autofill-utils.js":55,"./HTMLTooltip.js":48}],47:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./HTMLTooltip.js":47}],46:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11544,7 +11415,7 @@ ${this.options.css} } var _default = exports.default = EmailSignupHTMLTooltip; -},{"./HTMLTooltip.js":48}],48:[function(require,module,exports){ +},{"./HTMLTooltip.js":47}],47:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -11932,7 +11803,7 @@ class HTMLTooltip { exports.HTMLTooltip = HTMLTooltip; var _default = exports.default = HTMLTooltip; -},{"../Form/matching.js":35,"../autofill-utils.js":55,"./styles/styles.js":54}],49:[function(require,module,exports){ +},{"../Form/matching.js":34,"../autofill-utils.js":54,"./styles/styles.js":53}],48:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12300,7 +12171,7 @@ class HTMLTooltipUIController extends _UIController.UIController { } exports.HTMLTooltipUIController = HTMLTooltipUIController; -},{"../../Form/inputTypeConfig.js":30,"../../Form/matching.js":35,"../../autofill-utils.js":55,"../CredentialsImportTooltip.js":44,"../DataHTMLTooltip.js":45,"../EmailHTMLTooltip.js":46,"../EmailSignupHTMLTooltip.js":47,"../HTMLTooltip.js":48,"./UIController.js":52}],50:[function(require,module,exports){ +},{"../../Form/inputTypeConfig.js":29,"../../Form/matching.js":34,"../../autofill-utils.js":54,"../CredentialsImportTooltip.js":43,"../DataHTMLTooltip.js":44,"../EmailHTMLTooltip.js":45,"../EmailSignupHTMLTooltip.js":46,"../HTMLTooltip.js":47,"./UIController.js":51}],49:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12402,11 +12273,6 @@ class NativeUIController extends _UIController.UIController { form.activeInput?.focus(); break; } - case 'none': - { - // do nothing - break; - } default: { if (args.device.isTestMode()) { @@ -12467,7 +12333,7 @@ class NativeUIController extends _UIController.UIController { } exports.NativeUIController = NativeUIController; -},{"../../Form/matching.js":35,"../../InputTypes/Credentials.js":37,"../../deviceApiCalls/__generated__/deviceApiCalls.js":59,"./UIController.js":52}],51:[function(require,module,exports){ +},{"../../Form/matching.js":34,"../../InputTypes/Credentials.js":36,"../../deviceApiCalls/__generated__/deviceApiCalls.js":58,"./UIController.js":51}],50:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12704,7 +12570,7 @@ class OverlayUIController extends _UIController.UIController { } exports.OverlayUIController = OverlayUIController; -},{"../../Form/matching.js":35,"./UIController.js":52}],52:[function(require,module,exports){ +},{"../../Form/matching.js":34,"./UIController.js":51}],51:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12788,7 +12654,7 @@ class UIController { } exports.UIController = UIController; -},{}],53:[function(require,module,exports){ +},{}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12805,7 +12671,7 @@ const ddgCcIconBase = exports.ddgCcIconBase = 'data:image/svg+xml;base64,PD94bWw const ddgCcIconFilled = exports.ddgCcIconFilled = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzc2NDMxMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjNzY0MzEwIi8+Cjwvc3ZnPgo='; const ddgIdentityIconBase = exports.ddgIdentityIconBase = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4KPHBhdGggeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSJub25lIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMiAyMWMyLjE0MyAwIDQuMTExLS43NSA1LjY1Ny0yLS42MjYtLjUwNi0xLjMxOC0uOTI3LTIuMDYtMS4yNS0xLjEtLjQ4LTIuMjg1LS43MzUtMy40ODYtLjc1LTEuMi0uMDE0LTIuMzkyLjIxMS0zLjUwNC42NjQtLjgxNy4zMzMtMS41OC43ODMtMi4yNjQgMS4zMzYgMS41NDYgMS4yNSAzLjUxNCAyIDUuNjU3IDJ6bTQuMzk3LTUuMDgzYy45NjcuNDIyIDEuODY2Ljk4IDIuNjcyIDEuNjU1QzIwLjI3OSAxNi4wMzkgMjEgMTQuMTA0IDIxIDEyYzAtNC45Ny00LjAzLTktOS05cy05IDQuMDMtOSA5YzAgMi4xMDQuNzIyIDQuMDQgMS45MzIgNS41NzIuODc0LS43MzQgMS44Ni0xLjMyOCAyLjkyMS0xLjc2IDEuMzYtLjU1NCAyLjgxNi0uODMgNC4yODMtLjgxMSAxLjQ2Ny4wMTggMi45MTYuMzMgNC4yNi45MTZ6TTEyIDIzYzYuMDc1IDAgMTEtNC45MjUgMTEtMTFTMTguMDc1IDEgMTIgMSAxIDUuOTI1IDEgMTJzNC45MjUgMTEgMTEgMTF6bTMtMTNjMCAxLjY1Ny0xLjM0MyAzLTMgM3MtMy0xLjM0My0zLTMgMS4zNDMtMyAzLTMgMyAxLjM0MyAzIDN6bTIgMGMwIDIuNzYxLTIuMjM5IDUtNSA1cy01LTIuMjM5LTUtNSAyLjIzOS01IDUtNSA1IDIuMjM5IDUgNXoiIGZpbGw9IiMwMDAiLz4KPC9zdmc+Cg==`; -},{}],54:[function(require,module,exports){ +},{}],53:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12814,7 +12680,7 @@ Object.defineProperty(exports, "__esModule", { exports.CSS_STYLES = void 0; const CSS_STYLES = exports.CSS_STYLES = ":root {\n color-scheme: light dark;\n}\n\n.wrapper *, .wrapper *::before, .wrapper *::after {\n box-sizing: border-box;\n}\n.wrapper {\n position: fixed;\n top: 0;\n left: 0;\n padding: 0;\n font-family: 'DDG_ProximaNova', 'Proxima Nova', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n -webkit-font-smoothing: antialiased;\n z-index: 2147483647;\n}\n.wrapper--data {\n font-family: 'SF Pro Text', system-ui, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n}\n.wrapper:not(.top-autofill) .tooltip {\n position: absolute;\n width: 300px;\n max-width: calc(100vw - 25px);\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--data, #topAutofill {\n background-color: rgba(242, 240, 240, 1);\n -webkit-backdrop-filter: blur(40px);\n backdrop-filter: blur(40px);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data, #topAutofill {\n background: rgb(100, 98, 102, .9);\n }\n}\n.tooltip--data {\n padding: 6px;\n font-size: 13px;\n line-height: 14px;\n width: 315px;\n max-height: 290px;\n overflow-y: auto;\n}\n.top-autofill .tooltip--data {\n min-height: 100vh;\n}\n.tooltip--data.tooltip--incontext-signup {\n width: 360px;\n}\n.wrapper:not(.top-autofill) .tooltip--data {\n top: 100%;\n left: 100%;\n border: 0.5px solid rgba(255, 255, 255, 0.2);\n border-radius: 6px;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.32);\n}\n@media (prefers-color-scheme: dark) {\n .wrapper:not(.top-autofill) .tooltip--data {\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n}\n.wrapper:not(.top-autofill) .tooltip--email {\n top: calc(100% + 6px);\n right: calc(100% - 48px);\n padding: 8px;\n border: 1px solid #D0D0D0;\n border-radius: 10px;\n background-color: #FFFFFF;\n font-size: 14px;\n line-height: 1.3;\n color: #333333;\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);\n}\n.tooltip--email__caret {\n position: absolute;\n transform: translate(-1000px, -1000px);\n z-index: 2147483647;\n}\n.tooltip--email__caret::before,\n.tooltip--email__caret::after {\n content: \"\";\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n display: block;\n border-bottom: 8px solid #D0D0D0;\n position: absolute;\n right: -28px;\n}\n.tooltip--email__caret::before {\n border-bottom-color: #D0D0D0;\n top: -1px;\n}\n.tooltip--email__caret::after {\n border-bottom-color: #FFFFFF;\n top: 0px;\n}\n\n/* Buttons */\n.tooltip__button {\n display: flex;\n width: 100%;\n padding: 8px 8px 8px 0px;\n font-family: inherit;\n color: inherit;\n background: transparent;\n border: none;\n border-radius: 6px;\n text-align: left;\n}\n.tooltip__button.currentFocus,\n.wrapper:not(.top-autofill) .tooltip__button:hover {\n background-color: #3969EF;\n color: #FFFFFF;\n}\n\n/* Data autofill tooltip specific */\n.tooltip__button--data {\n position: relative;\n min-height: 48px;\n flex-direction: row;\n justify-content: flex-start;\n font-size: inherit;\n font-weight: 500;\n line-height: 16px;\n text-align: left;\n border-radius: 3px;\n}\n.tooltip--data__item-container {\n max-height: 220px;\n overflow: auto;\n}\n.tooltip__button--data:first-child {\n margin-top: 0;\n}\n.tooltip__button--data:last-child {\n margin-bottom: 0;\n}\n.tooltip__button--data::before {\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 20px 20px;\n background-repeat: no-repeat;\n background-position: center center;\n}\n#provider_locked::after {\n position: absolute;\n content: '';\n flex-shrink: 0;\n display: block;\n width: 32px;\n height: 32px;\n margin: 0 8px;\n background-size: 11px 13px;\n background-repeat: no-repeat;\n background-position: right bottom;\n}\n.tooltip__button--data.currentFocus:not(.tooltip__button--data--bitwarden)::before,\n.wrapper:not(.top-autofill) .tooltip__button--data:not(.tooltip__button--data--bitwarden):hover::before {\n filter: invert(100%);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before,\n .tooltip__button--data:not(.tooltip__button--data--bitwarden)::before {\n filter: invert(100%);\n opacity: .9;\n }\n}\n.tooltip__button__text-container {\n margin: auto 0;\n}\n.label {\n display: block;\n font-weight: 400;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.8);\n font-size: 13px;\n line-height: 1;\n}\n.label + .label {\n margin-top: 2px;\n}\n.label.label--medium {\n font-weight: 500;\n letter-spacing: -0.25px;\n color: rgba(0,0,0,.9);\n}\n.label.label--small {\n font-size: 11px;\n font-weight: 400;\n letter-spacing: 0.06px;\n color: rgba(0,0,0,0.6);\n}\n@media (prefers-color-scheme: dark) {\n .tooltip--data .label {\n color: #ffffff;\n }\n .tooltip--data .label--medium {\n color: #ffffff;\n }\n .tooltip--data .label--small {\n color: #cdcdcd;\n }\n}\n.tooltip__button.currentFocus .label,\n.wrapper:not(.top-autofill) .tooltip__button:hover .label {\n color: #FFFFFF;\n}\n\n.tooltip__button--manage {\n font-size: 13px;\n padding: 5px 9px;\n border-radius: 3px;\n margin: 0;\n}\n\n/* Icons */\n.tooltip__button--data--credentials::before,\n.tooltip__button--data--credentials__current::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNS4zMzQgNi42NjdhMiAyIDAgMSAwIDAgNCAyIDIgMCAwIDAgMC00Wm0tLjY2NyAyYS42NjcuNjY3IDAgMSAxIDEuMzMzIDAgLjY2Ny42NjcgMCAwIDEtMS4zMzMgMFoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNjY3IDRhNS4zMzMgNS4zMzMgMCAwIDAtNS4xODggNi41NzhsLTUuMjg0IDUuMjg0YS42NjcuNjY3IDAgMCAwLS4xOTUuNDcxdjNjMCAuMzY5LjI5OC42NjcuNjY3LjY2N2gyLjY2NmMuNzM3IDAgMS4zMzQtLjU5NyAxLjMzNC0xLjMzM1YxOGguNjY2Yy43MzcgMCAxLjMzNC0uNTk3IDEuMzM0LTEuMzMzdi0xLjMzNEgxMmMuMTc3IDAgLjM0Ni0uMDcuNDcxLS4xOTVsLjY4OC0uNjg4QTUuMzMzIDUuMzMzIDAgMSAwIDE0LjY2NyA0Wm0tNCA1LjMzM2E0IDQgMCAxIDEgMi41NTUgMy43MzIuNjY3LjY2NyAwIDAgMC0uNzEzLjE1bC0uNzg1Ljc4NUgxMGEuNjY3LjY2NyAwIDAgMC0uNjY3LjY2N3YySDhhLjY2Ny42NjcgMCAwIDAtLjY2Ny42NjZ2MS4zMzRoLTJ2LTIuMDU4bDUuMzY1LTUuMzY0YS42NjcuNjY3IDAgMCAwIC4xNjMtLjY3NyAzLjk5NiAzLjk5NiAwIDAgMS0uMTk0LTEuMjM1WiIgY2xpcC1ydWxlPSJldmVub2RkIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--credentials__new::before {\n background-size: 28px 28px;\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNOC4wNDcgNC42MjVDNy45MzcgNC4xMjUgNy44NjIgNCA3LjUgNGMtLjM2MiAwLS40MzguMTI1LS41NDcuNjI1LS4wNjguMzEtLjE3NyAxLjMzOC0uMjUxIDIuMDc3LS43MzguMDc0LTEuNzY3LjE4My0yLjA3Ny4yNTEtLjUuMTEtLjYyNS4xODQtLjYyNS41NDcgMCAuMzYyLjEyNS40MzcuNjI1LjU0Ny4zMS4wNjcgMS4zMzYuMTc3IDIuMDc0LjI1LjA3My43NjcuMTg1IDEuODQyLjI1NCAyLjA3OC4xMS4zNzUuMTg1LjYyNS41NDcuNjI1LjM2MiAwIC40MzgtLjEyNS41NDctLjYyNS4wNjgtLjMxLjE3Ny0xLjMzNi4yNS0yLjA3NC43NjctLjA3MyAxLjg0Mi0uMTg1IDIuMDc4LS4yNTQuMzc1LS4xMS42MjUtLjE4NS42MjUtLjU0NyAwLS4zNjMtLjEyNS0uNDM4LS42MjUtLjU0Ny0uMzEtLjA2OC0xLjMzOS0uMTc3LTIuMDc3LS4yNTEtLjA3NC0uNzM5LS4xODMtMS43NjctLjI1MS0yLjA3N1oiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGQ9Ik0xNC42ODEgNS4xOTljLS43NjYgMC0xLjQ4Mi4yMS0yLjA5My41NzhhLjYzNi42MzYgMCAwIDEtLjY1NS0xLjA5IDUuMzQgNS4zNCAwIDEgMSAxLjMwMiA5LjcyMmwtLjc3NS43NzZhLjYzNi42MzYgMCAwIDEtLjQ1LjE4NmgtMS4zOTh2MS42NWMwIC40OTMtLjQuODkzLS44OTMuODkzSDguNTc4djEuMTQxYzAgLjQ5NC0uNC44OTMtLjg5NC44OTNINC42MzZBLjYzNi42MzYgMCAwIDEgNCAxOS4zMTNWMTYuMjZjMC0uMTY5LjA2Ny0uMzMuMTg2LS40NWw1LjU2Mi01LjU2MmEuNjM2LjYzNiAwIDEgMSAuOS45bC01LjM3NiA1LjM3NXYyLjE1M2gyLjAzNHYtMS4zOTljMC0uMzUuMjg1LS42MzYuNjM2LS42MzZIOS4zNHYtMS45MDdjMC0uMzUxLjI4NC0uNjM2LjYzNS0uNjM2aDEuNzcxbC44NjQtLjg2M2EuNjM2LjYzNiAwIDAgMSAuNjY4LS4xNDcgNC4wNjkgNC4wNjkgMCAxIDAgMS40MDItNy44OVoiLz4KICAgIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTEzLjYyNSA4LjQ5OWExLjg3NSAxLjg3NSAwIDEgMSAzLjc1IDAgMS44NzUgMS44NzUgMCAwIDEtMy43NSAwWm0xLjg3NS0uNjI1YS42MjUuNjI1IDAgMSAwIDAgMS4yNS42MjUuNjI1IDAgMCAwIDAtMS4yNVoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTQuNjI1IDEyLjEyNWEuNjI1LjYyNSAwIDEgMCAwLTEuMjUuNjI1LjYyNSAwIDAgMCAwIDEuMjVaIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--creditCards::before {\n background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBkPSJNNSA5Yy0uNTUyIDAtMSAuNDQ4LTEgMXYyYzAgLjU1Mi40NDggMSAxIDFoM2MuNTUyIDAgMS0uNDQ4IDEtMXYtMmMwLS41NTItLjQ0OC0xLTEtMUg1eiIgZmlsbD0iIzAwMCIvPgogICAgPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xIDZjMC0yLjIxIDEuNzktNCA0LTRoMTRjMi4yMSAwIDQgMS43OSA0IDR2MTJjMCAyLjIxLTEuNzkgNC00IDRINWMtMi4yMSAwLTQtMS43OS00LTRWNnptNC0yYy0xLjEwNSAwLTIgLjg5NS0yIDJ2OWgxOFY2YzAtMS4xMDUtLjg5NS0yLTItMkg1em0wIDE2Yy0xLjEwNSAwLTItLjg5NS0yLTJoMThjMCAxLjEwNS0uODk1IDItMiAySDV6IiBmaWxsPSIjMDAwIi8+Cjwvc3ZnPgo=');\n}\n.tooltip__button--data--identities::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgZmlsbD0ibm9uZSI+CiAgICA8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTEyIDIxYzIuMTQzIDAgNC4xMTEtLjc1IDUuNjU3LTItLjYyNi0uNTA2LTEuMzE4LS45MjctMi4wNi0xLjI1LTEuMS0uNDgtMi4yODUtLjczNS0zLjQ4Ni0uNzUtMS4yLS4wMTQtMi4zOTIuMjExLTMuNTA0LjY2NC0uODE3LjMzMy0xLjU4Ljc4My0yLjI2NCAxLjMzNiAxLjU0NiAxLjI1IDMuNTE0IDIgNS42NTcgMnptNC4zOTctNS4wODNjLjk2Ny40MjIgMS44NjYuOTggMi42NzIgMS42NTVDMjAuMjc5IDE2LjAzOSAyMSAxNC4xMDQgMjEgMTJjMC00Ljk3LTQuMDMtOS05LTlzLTkgNC4wMy05IDljMCAyLjEwNC43MjIgNC4wNCAxLjkzMiA1LjU3Mi44NzQtLjczNCAxLjg2LTEuMzI4IDIuOTIxLTEuNzYgMS4zNi0uNTU0IDIuODE2LS44MyA0LjI4My0uODExIDEuNDY3LjAxOCAyLjkxNi4zMyA0LjI2LjkxNnpNMTIgMjNjNi4wNzUgMCAxMS00LjkyNSAxMS0xMVMxOC4wNzUgMSAxMiAxIDEgNS45MjUgMSAxMnM0LjkyNSAxMSAxMSAxMXptMy0xM2MwIDEuNjU3LTEuMzQzIDMtMyAzcy0zLTEuMzQzLTMtMyAxLjM0My0zIDMtMyAzIDEuMzQzIDMgM3ptMiAwYzAgMi43NjEtMi4yMzkgNS01IDVzLTUtMi4yMzktNS01IDIuMjM5LTUgNS01IDUgMi4yMzkgNSA1eiIgZmlsbD0iIzAwMCIvPgo8L3N2Zz4=');\n}\n.tooltip__button--data--credentials.tooltip__button--data--bitwarden::before,\n.tooltip__button--data--credentials__current.tooltip__button--data--bitwarden::before {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iOCIgZmlsbD0iIzE3NUREQyIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE4LjU2OTYgNS40MzM1NUMxOC41MDg0IDUuMzc0NDIgMTguNDM0NyA1LjMyNzYzIDE4LjM1MzEgNS4yOTYxMUMxOC4yNzE1IDUuMjY0NiAxOC4xODM3IDUuMjQ5MDQgMTguMDk1MyA1LjI1MDQxSDUuOTIxOTFDNS44MzMyNiA1LjI0NzI5IDUuNzQ0OTMgNS4yNjIwNSA1LjY2MzA0IDUuMjkzNjdDNS41ODExNSA1LjMyNTI5IDUuNTA3NjUgNS4zNzMwMiA1LjQ0NzYyIDUuNDMzNTVDNS4zMjE3IDUuNTUwMTMgNS4yNTA2NSA1LjcwODE1IDUuMjUgNS44NzMxVjEzLjM4MjFDNS4yNTMzNiAxMy45NTM1IDUuMzc0MDggMTQuNTE5MSA1LjYwNTcyIDE1LjA0ODdDNS44MTkzMSAxNS41NzI4IDYuMTEyMDcgMTYuMDY2MSA2LjQ3NTI0IDE2LjUxMzlDNi44NDIgMTYuOTY4MyA3LjI1OTI5IDE3LjM4NTcgNy43MjAyNSAxNy43NTkzQzguMTQwNTMgMTguMTI1NiA4LjU4OTcxIDE4LjQ2MjMgOS4wNjQwNyAxOC43NjY2QzkuNDU5MzEgMTkuMDIzIDkuOTEzODMgMTkuMjc5NCAxMC4zNDg2IDE5LjUxNzVDMTAuNzgzNCAxOS43NTU2IDExLjA5OTYgMTkuOTIwNCAxMS4yNzc0IDE5Ljk5MzdDMTEuNDU1MyAyMC4wNjY5IDExLjYxMzQgMjAuMTQwMiAxMS43MTIyIDIwLjE5NTFDMTEuNzk5MiAyMC4yMzEzIDExLjg5MzUgMjAuMjUgMTEuOTg4OCAyMC4yNUMxMi4wODQyIDIwLjI1IDEyLjE3ODUgMjAuMjMxMyAxMi4yNjU1IDIwLjE5NTFDMTIuNDIxMiAyMC4xMzYzIDEyLjU3MjkgMjAuMDY5IDEyLjcyIDE5Ljk5MzdDMTIuNzcxMSAxOS45Njc0IDEyLjgzMzUgMTkuOTM2NiAxMi45MDY5IDE5LjkwMDRDMTMuMDg5MSAxOS44MTA1IDEzLjMzODggMTkuNjg3MiAxMy42NDg5IDE5LjUxNzVDMTQuMDgzNiAxOS4yNzk0IDE0LjUxODQgMTkuMDIzIDE0LjkzMzQgMTguNzY2NkMxNS40MDQgMTguNDU3NyAxNS44NTI4IDE4LjEyMTIgMTYuMjc3MiAxNy43NTkzQzE2LjczMzEgMTcuMzgwOSAxNy4xNDk5IDE2Ljk2NCAxNy41MjIyIDE2LjUxMzlDMTcuODc4IDE2LjA2MTcgMTguMTcwMiAxNS41NjkzIDE4LjM5MTcgMTUuMDQ4N0MxOC42MjM0IDE0LjUxOTEgMTguNzQ0MSAxMy45NTM1IDE4Ljc0NzQgMTMuMzgyMVY1Ljg3MzFDMTguNzU1NyA1Ljc5MjE0IDE4Ljc0MzkgNS43MTA1IDE4LjcxMzEgNS42MzQzNUMxOC42ODIzIDUuNTU4MiAxOC42MzMyIDUuNDg5NTQgMTguNTY5NiA1LjQzMzU1Wk0xNy4wMDg0IDEzLjQ1NTNDMTcuMDA4NCAxNi4xODQyIDEyLjAwODYgMTguNTI4NSAxMi4wMDg2IDE4LjUyODVWNi44NjIwOUgxNy4wMDg0VjEzLjQ1NTNaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K');\n}\n#provider_locked:after {\n background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMSAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgNy42MDA1N1Y3LjYwMjVWOS41MjI1QzEgMTAuMDgwMSAxLjIyMTUxIDEwLjYxNDkgMS42MTU4MSAxMS4wMDkyQzIuMDEwMSAxMS40MDM1IDIuNTQ0ODggMTEuNjI1IDMuMTAyNSAxMS42MjVINy4yNzI1QzcuNTQ4NjEgMTEuNjI1IDcuODIyMDEgMTEuNTcwNiA4LjA3NzA5IDExLjQ2NUM4LjMzMjE4IDExLjM1OTMgOC41NjM5NiAxMS4yMDQ0IDguNzU5MTkgMTEuMDA5MkM4Ljk1NDQzIDEwLjgxNCA5LjEwOTMgMTAuNTgyMiA5LjIxNDk2IDEwLjMyNzFDOS4zMjA2MiAxMC4wNzIgOS4zNzUgOS43OTg2MSA5LjM3NSA5LjUyMjVMOS4zNzUgNy42MDI1TDkuMzc1IDcuNjAwNTdDOS4zNzQxNSA3LjE2MTMxIDkuMjM1NzQgNi43MzMzNSA4Ljk3OTIyIDYuMzc2NzhDOC44NzY4MyA2LjIzNDQ2IDguNzU3NjggNi4xMDYzNyA4LjYyNSA1Ljk5NDg5VjUuMTg3NUM4LjYyNSA0LjI3NTgyIDguMjYyODQgMy40MDE0OCA3LjYxODE4IDIuNzU2ODJDNi45NzM1MiAyLjExMjE2IDYuMDk5MTggMS43NSA1LjE4NzUgMS43NUM0LjI3NTgyIDEuNzUgMy40MDE0OCAyLjExMjE2IDIuNzU2ODIgMi43NTY4MkMyLjExMjE2IDMuNDAxNDggMS43NSA0LjI3NTgyIDEuNzUgNS4xODc1VjUuOTk0ODlDMS42MTczMiA2LjEwNjM3IDEuNDk4MTcgNi4yMzQ0NiAxLjM5NTc4IDYuMzc2NzhDMS4xMzkyNiA2LjczMzM1IDEuMDAwODUgNy4xNjEzMSAxIDcuNjAwNTdaTTQuOTY4NyA0Ljk2ODdDNS4wMjY5NCA0LjkxMDQ3IDUuMTA1MzIgNC44NzY5OSA1LjE4NzUgNC44NzUwN0M1LjI2OTY4IDQuODc2OTkgNS4zNDgwNiA0LjkxMDQ3IDUuNDA2MyA0Ljk2ODdDNS40NjU0MiA1LjAyNzgzIDUuNDk5MDQgNS4xMDc3NCA1LjUgNS4xOTEzVjUuNUg0Ljg3NVY1LjE5MTNDNC44NzU5NiA1LjEwNzc0IDQuOTA5NTggNS4wMjc4MyA0Ljk2ODcgNC45Njg3WiIgZmlsbD0iIzIyMjIyMiIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=');\n}\n\nhr {\n display: block;\n margin: 5px 9px;\n border: none; /* reset the border */\n border-top: 1px solid rgba(0,0,0,.1);\n}\n\nhr:first-child {\n display: none;\n}\n\n@media (prefers-color-scheme: dark) {\n hr {\n border-top: 1px solid rgba(255,255,255,.2);\n }\n}\n\n#privateAddress {\n align-items: flex-start;\n}\n#personalAddress::before,\n#privateAddress::before,\n#incontextSignup::before,\n#personalAddress.currentFocus::before,\n#personalAddress:hover::before,\n#privateAddress.currentFocus::before,\n#privateAddress:hover::before {\n filter: none;\n /* This is the same icon as `daxBase64` in `src/Form/logo-svg.js` */\n background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwYXRoIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0ibTY0IDEyOGMzNS4zNDYgMCA2NC0yOC42NTQgNjQtNjRzLTI4LjY1NC02NC02NC02NC02NCAyOC42NTQtNjQgNjQgMjguNjU0IDY0IDY0IDY0eiIgZmlsbD0iI2RlNTgzMyIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im03MyAxMTEuNzVjMC0uNS4xMjMtLjYxNC0xLjQ2Ni0zLjc4Mi00LjIyNC04LjQ1OS04LjQ3LTIwLjM4NC02LjU0LTI4LjA3NS4zNTMtMS4zOTctMy45NzgtNTEuNzQ0LTcuMDQtNTMuMzY1LTMuNDAyLTEuODEzLTcuNTg4LTQuNjktMTEuNDE4LTUuMzMtMS45NDMtLjMxLTQuNDktLjE2NC02LjQ4Mi4xMDUtLjM1My4wNDctLjM2OC42ODMtLjAzLjc5OCAxLjMwOC40NDMgMi44OTUgMS4yMTIgMy44MyAyLjM3NS4xNzguMjItLjA2LjU2Ni0uMzQyLjU3Ny0uODgyLjAzMi0yLjQ4Mi40MDItNC41OTMgMi4xOTUtLjI0NC4yMDctLjA0MS41OTIuMjczLjUzIDQuNTM2LS44OTcgOS4xNy0uNDU1IDExLjkgMi4wMjcuMTc3LjE2LjA4NC40NS0uMTQ3LjUxMi0yMy42OTQgNi40NC0xOS4wMDMgMjcuMDUtMTIuNjk2IDUyLjM0NCA1LjYxOSAyMi41MyA3LjczMyAyOS43OTIgOC40IDMyLjAwNGEuNzE4LjcxOCAwIDAgMCAuNDIzLjQ2N2M4LjE1NiAzLjI0OCAyNS45MjggMy4zOTIgMjUuOTI4LTIuMTMyeiIgZmlsbD0iI2RkZCIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8cGF0aCBkPSJtNzYuMjUgMTE2LjVjLTIuODc1IDEuMTI1LTguNSAxLjYyNS0xMS43NSAxLjYyNS00Ljc2NCAwLTExLjYyNS0uNzUtMTQuMTI1LTEuODc1LTEuNTQ0LTQuNzUxLTYuMTY0LTE5LjQ4LTEwLjcyNy0zOC4xODVsLS40NDctMS44MjctLjAwNC0uMDE1Yy01LjQyNC0yMi4xNTctOS44NTUtNDAuMjUzIDE0LjQyNy00NS45MzguMjIyLS4wNTIuMzMtLjMxNy4xODQtLjQ5Mi0yLjc4Ni0zLjMwNS04LjAwNS00LjM4OC0xNC42MDUtMi4xMTEtLjI3LjA5My0uNTA2LS4xOC0uMzM3LS40MTIgMS4yOTQtMS43ODMgMy44MjMtMy4xNTUgNS4wNzEtMy43NTYuMjU4LS4xMjQuMjQyLS41MDItLjAzLS41ODhhMjcuODc3IDI3Ljg3NyAwIDAgMCAtMy43NzItLjljLS4zNy0uMDU5LS40MDMtLjY5My0uMDMyLS43NDMgOS4zNTYtMS4yNTkgMTkuMTI1IDEuNTUgMjQuMDI4IDcuNzI2YS4zMjYuMzI2IDAgMCAwIC4xODYuMTE0YzE3Ljk1MiAzLjg1NiAxOS4yMzggMzIuMjM1IDE3LjE3IDMzLjUyOC0uNDA4LjI1NS0xLjcxNS4xMDgtMy40MzgtLjA4NS02Ljk4Ni0uNzgxLTIwLjgxOC0yLjMyOS05LjQwMiAxOC45NDguMTEzLjIxLS4wMzYuNDg4LS4yNzIuNTI1LTYuNDM4IDEgMS44MTIgMjEuMTczIDcuODc1IDM0LjQ2MXoiIGZpbGw9IiNmZmYiLz4KICAgIDxwYXRoIGQ9Im04NC4yOCA5MC42OThjLTEuMzY3LS42MzMtNi42MjEgMy4xMzUtMTAuMTEgNi4wMjgtLjcyOC0xLjAzMS0yLjEwMy0xLjc4LTUuMjAzLTEuMjQyLTIuNzEzLjQ3Mi00LjIxMSAxLjEyNi00Ljg4IDIuMjU0LTQuMjgzLTEuNjIzLTExLjQ4OC00LjEzLTEzLjIyOS0xLjcxLTEuOTAyIDIuNjQ2LjQ3NiAxNS4xNjEgMy4wMDMgMTYuNzg2IDEuMzIuODQ5IDcuNjMtMy4yMDggMTAuOTI2LTYuMDA1LjUzMi43NDkgMS4zODggMS4xNzggMy4xNDggMS4xMzcgMi42NjItLjA2MiA2Ljk3OS0uNjgxIDcuNjQ5LTEuOTIxLjA0LS4wNzUuMDc1LS4xNjQuMTA1LS4yNjYgMy4zODggMS4yNjYgOS4zNSAyLjYwNiAxMC42ODIgMi40MDYgMy40Ny0uNTIxLS40ODQtMTYuNzIzLTIuMDktMTcuNDY3eiIgZmlsbD0iIzNjYTgyYiIvPgogICAgPHBhdGggZD0ibTc0LjQ5IDk3LjA5N2MuMTQ0LjI1Ni4yNi41MjYuMzU4LjguNDgzIDEuMzUyIDEuMjcgNS42NDguNjc0IDYuNzA5LS41OTUgMS4wNjItNC40NTkgMS41NzQtNi44NDMgMS42MTVzLTIuOTItLjgzMS0zLjQwMy0yLjE4MWMtLjM4Ny0xLjA4MS0uNTc3LTMuNjIxLS41NzItNS4wNzUtLjA5OC0yLjE1OC42OS0yLjkxNiA0LjMzNC0zLjUwNiAyLjY5Ni0uNDM2IDQuMTIxLjA3MSA0Ljk0NC45NCAzLjgyOC0yLjg1NyAxMC4yMTUtNi44ODkgMTAuODM4LTYuMTUyIDMuMTA2IDMuNjc0IDMuNDk5IDEyLjQyIDIuODI2IDE1LjkzOS0uMjIgMS4xNTEtMTAuNTA1LTEuMTM5LTEwLjUwNS0yLjM4IDAtNS4xNTItMS4zMzctNi41NjUtMi42NS02Ljcxem0tMjIuNTMtMS42MDljLjg0My0xLjMzMyA3LjY3NC4zMjUgMTEuNDI0IDEuOTkzIDAgMC0uNzcgMy40OTEuNDU2IDcuNjA0LjM1OSAxLjIwMy04LjYyNyA2LjU1OC05LjggNS42MzctMS4zNTUtMS4wNjUtMy44NS0xMi40MzItMi4wOC0xNS4yMzR6IiBmaWxsPSIjNGNiYTNjIi8+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im01NS4yNjkgNjguNDA2Yy41NTMtMi40MDMgMy4xMjctNi45MzIgMTIuMzIxLTYuODIyIDQuNjQ4LS4wMTkgMTAuNDIyLS4wMDIgMTQuMjUtLjQzNmE1MS4zMTIgNTEuMzEyIDAgMCAwIDEyLjcyNi0zLjA5NWMzLjk4LTEuNTE5IDUuMzkyLTEuMTggNS44ODctLjI3Mi41NDQuOTk5LS4wOTcgMi43MjItMS40ODggNC4zMDktMi42NTYgMy4wMy03LjQzMSA1LjM4LTE1Ljg2NSA2LjA3Ni04LjQzMy42OTgtMTQuMDItMS41NjUtMTYuNDI1IDIuMTE4LTEuMDM4IDEuNTg5LS4yMzYgNS4zMzMgNy45MiA2LjUxMiAxMS4wMiAxLjU5IDIwLjA3Mi0xLjkxNyAyMS4xOS4yMDEgMS4xMTkgMi4xMTgtNS4zMjMgNi40MjgtMTYuMzYyIDYuNTE4cy0xNy45MzQtMy44NjUtMjAuMzc5LTUuODNjLTMuMTAyLTIuNDk1LTQuNDktNi4xMzMtMy43NzUtOS4yNzl6IiBmaWxsPSIjZmMzIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICAgIDxnIGZpbGw9IiMxNDMwN2UiIG9wYWNpdHk9Ii44Ij4KICAgICAgPHBhdGggZD0ibTY5LjMyNyA0Mi4xMjdjLjYxNi0xLjAwOCAxLjk4MS0xLjc4NiA0LjIxNi0xLjc4NiAyLjIzNCAwIDMuMjg1Ljg4OSA0LjAxMyAxLjg4LjE0OC4yMDItLjA3Ni40NC0uMzA2LjM0YTU5Ljg2OSA1OS44NjkgMCAwIDEgLS4xNjgtLjA3M2MtLjgxNy0uMzU3LTEuODItLjc5NS0zLjU0LS44Mi0xLjgzOC0uMDI2LTIuOTk3LjQzNS0zLjcyNy44MzEtLjI0Ni4xMzQtLjYzNC0uMTMzLS40ODgtLjM3MnptLTI1LjE1NyAxLjI5YzIuMTctLjkwNyAzLjg3Ni0uNzkgNS4wODEtLjUwNC4yNTQuMDYuNDMtLjIxMy4yMjctLjM3Ny0uOTM1LS43NTUtMy4wMy0xLjY5Mi01Ljc2LS42NzQtMi40MzcuOTA5LTMuNTg1IDIuNzk2LTMuNTkyIDQuMDM4LS4wMDIuMjkyLjYuMzE3Ljc1Ni4wNy40Mi0uNjcgMS4xMi0xLjY0NiAzLjI4OS0yLjU1M3oiLz4KICAgICAgPHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNzUuNDQgNTUuOTJhMy40NyAzLjQ3IDAgMCAxIC0zLjQ3NC0zLjQ2MiAzLjQ3IDMuNDcgMCAwIDEgMy40NzUtMy40NiAzLjQ3IDMuNDcgMCAwIDEgMy40NzQgMy40NiAzLjQ3IDMuNDcgMCAwIDEgLTMuNDc1IDMuNDYyem0yLjQ0Ny00LjYwOGEuODk5Ljg5OSAwIDAgMCAtMS43OTkgMGMwIC40OTQuNDA1Ljg5NS45Ljg5NS40OTkgMCAuOS0uNC45LS44OTV6bS0yNS40NjQgMy41NDJhNC4wNDIgNC4wNDIgMCAwIDEgLTQuMDQ5IDQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIC00LjA1LTQuMDM3IDQuMDQ1IDQuMDQ1IDAgMCAxIDQuMDUtNC4wMzcgNC4wNDUgNC4wNDUgMCAwIDEgNC4wNSA0LjAzN3ptLTEuMTkzLTEuMzM4YTEuMDUgMS4wNSAwIDAgMCAtMi4wOTcgMCAxLjA0OCAxLjA0OCAwIDAgMCAyLjA5NyAweiIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgICA8L2c+CiAgICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Im02NCAxMTcuNzVjMjkuNjg1IDAgNTMuNzUtMjQuMDY1IDUzLjc1LTUzLjc1cy0yNC4wNjUtNTMuNzUtNTMuNzUtNTMuNzUtNTMuNzUgMjQuMDY1LTUzLjc1IDUzLjc1IDI0LjA2NSA1My43NSA1My43NSA1My43NXptMCA1YzMyLjQ0NyAwIDU4Ljc1LTI2LjMwMyA1OC43NS01OC43NXMtMjYuMzAzLTU4Ljc1LTU4Ljc1LTU4Ljc1LTU4Ljc1IDI2LjMwMy01OC43NSA1OC43NSAyNi4zMDMgNTguNzUgNTguNzUgNTguNzV6IiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KPC9zdmc+');\n}\n\n/* Email tooltip specific */\n.tooltip__button--email {\n flex-direction: column;\n justify-content: center;\n align-items: flex-start;\n font-size: 14px;\n padding: 4px 8px;\n}\n.tooltip__button--email__primary-text {\n font-weight: bold;\n}\n.tooltip__button--email__secondary-text {\n font-size: 12px;\n}\n\n/* Email Protection signup notice */\n:not(.top-autofill) .tooltip--email-signup {\n text-align: left;\n color: #222222;\n padding: 16px 20px;\n width: 380px;\n}\n\n.tooltip--email-signup h1 {\n font-weight: 700;\n font-size: 16px;\n line-height: 1.5;\n margin: 0;\n}\n\n.tooltip--email-signup p {\n font-weight: 400;\n font-size: 14px;\n line-height: 1.4;\n}\n\n.notice-controls {\n display: flex;\n}\n\n.tooltip--email-signup .notice-controls > * {\n border-radius: 8px;\n border: 0;\n cursor: pointer;\n display: inline-block;\n font-family: inherit;\n font-style: normal;\n font-weight: bold;\n padding: 8px 12px;\n text-decoration: none;\n}\n\n.notice-controls .ghost {\n margin-left: 1rem;\n}\n\n.tooltip--email-signup a.primary {\n background: #3969EF;\n color: #fff;\n}\n\n.tooltip--email-signup a.primary:hover,\n.tooltip--email-signup a.primary:focus {\n background: #2b55ca;\n}\n\n.tooltip--email-signup a.primary:active {\n background: #1e42a4;\n}\n\n.tooltip--email-signup button.ghost {\n background: transparent;\n color: #3969EF;\n}\n\n.tooltip--email-signup button.ghost:hover,\n.tooltip--email-signup button.ghost:focus {\n background-color: rgba(0, 0, 0, 0.06);\n color: #2b55ca;\n}\n\n.tooltip--email-signup button.ghost:active {\n background-color: rgba(0, 0, 0, 0.12);\n color: #1e42a4;\n}\n\n.tooltip--email-signup button.close-tooltip {\n background-color: transparent;\n background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTMiIHZpZXdCb3g9IjAgMCAxMiAxMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wLjI5Mjg5NCAwLjY1NjkwN0MwLjY4MzQxOCAwLjI2NjM4MyAxLjMxNjU4IDAuMjY2MzgzIDEuNzA3MTEgMC42NTY5MDdMNiA0Ljk0OThMMTAuMjkyOSAwLjY1NjkwN0MxMC42ODM0IDAuMjY2MzgzIDExLjMxNjYgMC4yNjYzODMgMTEuNzA3MSAwLjY1NjkwN0MxMi4wOTc2IDEuMDQ3NDMgMTIuMDk3NiAxLjY4MDYgMTEuNzA3MSAyLjA3MTEyTDcuNDE0MjEgNi4zNjQwMUwxMS43MDcxIDEwLjY1NjlDMTIuMDk3NiAxMS4wNDc0IDEyLjA5NzYgMTEuNjgwNiAxMS43MDcxIDEyLjA3MTFDMTEuMzE2NiAxMi40NjE2IDEwLjY4MzQgMTIuNDYxNiAxMC4yOTI5IDEyLjA3MTFMNiA3Ljc3ODIzTDEuNzA3MTEgMTIuMDcxMUMxLjMxNjU4IDEyLjQ2MTYgMC42ODM0MTcgMTIuNDYxNiAwLjI5Mjg5MyAxMi4wNzExQy0wLjA5NzYzMTEgMTEuNjgwNiAtMC4wOTc2MzExIDExLjA0NzQgMC4yOTI4OTMgMTAuNjU2OUw0LjU4NTc5IDYuMzY0MDFMMC4yOTI4OTQgMi4wNzExMkMtMC4wOTc2MzA2IDEuNjgwNiAtMC4wOTc2MzA2IDEuMDQ3NDMgMC4yOTI4OTQgMC42NTY5MDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg0Ii8+Cjwvc3ZnPgo=);\n background-position: center center;\n background-repeat: no-repeat;\n border: 0;\n cursor: pointer;\n padding: 16px;\n position: absolute;\n right: 12px;\n top: 12px;\n}\n\n/* Import promotion prompt icon style */\n\n.tooltip__button--credentials-import::before {\n content: \"\";\n background-image: url(data:image/svg+xml;base64,Cjxzdmcgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03Ljk3MjY5IDkuMDEzN0M3Ljk3MjY5IDUuMTQwMTQgMTEuMTEyOCAyIDE0Ljk4NjQgMkMxOC44NTk5IDIgMjIuMDAwMSA1LjE0MDE0IDIyLjAwMDEgOS4wMTM3QzIyLjAwMDEgOS44MDM5NyAyMS44Njk0IDEwLjU2MzcgMjEuNjI4NCAxMS4yNzI1QzIxLjQ2MDEgMTEuNzY3NiAyMC44NjU2IDExLjkxMzUgMjAuMzk3NyAxMS42OEMxOS45MjIxIDExLjQ0MjggMTkuNjc3OSAxMC44NjQ0IDE5LjgxOTUgMTAuMzUyQzE5LjkzNzIgOS45MjU5OSAyMC4wMDAxIDkuNDc3MTkgMjAuMDAwMSA5LjAxMzdDMjAuMDAwMSA2LjI0NDcxIDE3Ljc1NTQgNCAxNC45ODY0IDRDMTIuMjE3NCA0IDkuOTcyNjkgNi4yNDQ3MSA5Ljk3MjY5IDkuMDEzN0M5Ljk3MjY5IDkuNTU1NCAxMC4wNTgyIDEwLjA3NTIgMTAuMjE1OCAxMC41NjEzQzEwLjMzMTcgMTAuOTE4OCAxMC4yMzc0IDExLjMxMTEgOS45NzE2NSAxMS41NzY4TDMuOTk5OTQgMTcuNTQ4NVYyMC4wMDAyTDYuNDk5OTggMjAuMDAwMlYxOC4zMDAyQzYuNDk5OTggMTcuNzQ3OSA2Ljk0NzcgMTcuMzAwMiA3LjQ5OTk4IDE3LjMwMDJIOS4yMDAwMVYxNS42MDAxQzkuMjAwMDEgMTUuMDQ3OCA5LjY0NzczIDE0LjYwMDEgMTAuMiAxNC42MDAxSDEwLjVDMTEuMjEwNiAxNC41ODcgMTEuNDI1MiAxNS4zMzY5IDExLjMxNTYgMTUuNzMzMUMxMS4xNTY1IDE2LjMwODUgMTEgMTcuMDAxIDExIDE3LjVDMTEgMTcuNzI4NCAxMS4wMTE4IDE3Ljk1NCAxMS4wMzQ4IDE4LjE3NjNDMTAuNzM3OSAxOC44Mzg3IDEwLjA3MjkgMTkuMzAwMiA5LjMgMTkuMzAwMkg4LjQ5OTk4VjIwLjEwMDJDOC40OTk5OCAyMS4xNDk1IDcuNjQ5MzMgMjIuMDAwMiA2LjU5OTk5IDIyLjAwMDJMMi45OTk5NSAyMi4wMDAyQzIuNzM0NzMgMjIuMDAwMiAyLjQ4MDM3IDIxLjg5NDkgMi4yOTI4NCAyMS43MDczQzIuMTA1MyAyMS41MTk4IDEuOTk5OTQgMjEuMjY1NCAxLjk5OTk0IDIxLjAwMDJWMTcuMTM0M0MxLjk5OTk0IDE2Ljg2OTEgMi4xMDUzIDE2LjYxNDcgMi4yOTI4MyAxNi40MjcyTDguMTQ2ODkgMTAuNTczMUM4LjAzMjc5IDEwLjA3MDkgNy45NzI2OSA5LjU0ODgxIDcuOTcyNjkgOS4wMTM3WiIgZmlsbD0iYmxhY2siIC8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTMgOC40OTk4OUMxMyA3LjExOTE4IDE0LjExOTMgNS45OTk4OSAxNS41IDUuOTk5ODlDMTYuODgwNyA1Ljk5OTg5IDE4IDcuMTE5MTggMTggOC40OTk4OUMxOCA5Ljg4MDYgMTYuODgwNyAxMC45OTk5IDE1LjUgMTAuOTk5OUMxNC4xMTkzIDEwLjk5OTkgMTMgOS44ODA2IDEzIDguNDk5ODlaTTE1LjUgNy45OTk4OUMxNS4yMjM5IDcuOTk5ODkgMTUgOC4yMjM3NSAxNSA4LjQ5OTg5QzE1IDguNzc2MDQgMTUuMjIzOSA4Ljk5OTg5IDE1LjUgOC45OTk4OUMxNS43NzYyIDguOTk5ODkgMTYgOC43NzYwNCAxNiA4LjQ5OTg5QzE2IDguMjIzNzUgMTUuNzc2MiA3Ljk5OTg5IDE1LjUgNy45OTk4OVoiIGZpbGw9ImJsYWNrIiAvPgo8cGF0aCBkPSJNMTcgMTVMMTQuNSAxNy41TDE3IDIwIiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMjEuNSAxNy41TDE2IDE3LjUiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);\n background-repeat: no-repeat;\n}\n"; -},{}],55:[function(require,module,exports){ +},{}],54:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13460,7 +13326,7 @@ function findEnclosedElements(root, selector) { return shadowElements; } -},{"./Form/matching.js":35,"./constants.js":58,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],56:[function(require,module,exports){ +},{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict"; require("./requestIdleCallback.js"); @@ -13491,7 +13357,7 @@ var _autofillUtils = require("./autofill-utils.js"); } })(); -},{"./DeviceInterface.js":14,"./autofill-utils.js":55,"./requestIdleCallback.js":95}],57:[function(require,module,exports){ +},{"./DeviceInterface.js":13,"./autofill-utils.js":54,"./requestIdleCallback.js":94}],56:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13508,10 +13374,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; @@ -13581,7 +13443,7 @@ function createGlobalConfig(overrides) { return config; } -},{}],58:[function(require,module,exports){ +},{}],57:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13598,13 +13460,13 @@ const constants = exports.constants = { MAX_FORM_RESCANS: 50 }; -},{}],59:[function(require,module,exports){ +},{}],58:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.EmailProtectionGetAliasCall = exports.EmailProtectionGetAddressesCall = exports.CloseEmailProtectionTabCall = exports.CloseAutofillParentCall = exports.CheckCredentialsProviderStatusCall = exports.AskToUnlockProviderCall = exports.AddDebugFlagCall = void 0; +exports.StoreFormDataCall = exports.StartEmailProtectionSignupCall = exports.StartCredentialsImportFlowCall = exports.ShowInContextEmailProtectionSignupPromptCall = exports.SetSizeCall = exports.SetIncontextSignupPermanentlyDismissedAtCall = exports.SendJSPixelCall = exports.SelectedDetailCall = exports.OpenManagePasswordsCall = exports.OpenManageIdentitiesCall = exports.OpenManageCreditCardsCall = exports.GetRuntimeConfigurationCall = exports.GetIncontextSignupDismissedAtCall = exports.GetAvailableInputTypesCall = exports.GetAutofillInitDataCall = exports.GetAutofillDataCall = exports.GetAutofillCredentialsCall = exports.EmailProtectionStoreUserDataCall = exports.EmailProtectionRemoveUserDataCall = exports.EmailProtectionRefreshPrivateAddressCall = exports.EmailProtectionGetUserDataCall = exports.EmailProtectionGetIsLoggedInCall = exports.EmailProtectionGetCapabilitiesCall = exports.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 */ @@ -13765,20 +13627,10 @@ exports.OpenManageIdentitiesCall = OpenManageIdentitiesCall; class StartCredentialsImportFlowCall extends _deviceApi.DeviceApiCall { method = "startCredentialsImportFlow"; } -/** - * @extends {DeviceApiCall} - */ -exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; -class EmailProtectionGetAliasCall extends _deviceApi.DeviceApiCall { - method = "emailProtectionGetAlias"; - id = "emailProtectionGetAliasResponse"; - paramsValidator = _validatorsZod.emailProtectionGetAliasParamsSchema; - resultValidator = _validatorsZod.emailProtectionGetAliasResultSchema; -} /** * @extends {DeviceApiCall} */ -exports.EmailProtectionGetAliasCall = EmailProtectionGetAliasCall; +exports.StartCredentialsImportFlowCall = StartCredentialsImportFlowCall; class EmailProtectionStoreUserDataCall extends _deviceApi.DeviceApiCall { method = "emailProtectionStoreUserData"; id = "emailProtectionStoreUserDataResponse"; @@ -13861,13 +13713,13 @@ class ShowInContextEmailProtectionSignupPromptCall extends _deviceApi.DeviceApiC } exports.ShowInContextEmailProtectionSignupPromptCall = ShowInContextEmailProtectionSignupPromptCall; -},{"../../../packages/device-api":2,"./validators.zod.js":60}],60:[function(require,module,exports){ +},{"../../../packages/device-api":2,"./validators.zod.js":59}],59:[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; @@ -13877,7 +13729,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; @@ -13886,10 +13737,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; @@ -13897,20 +13747,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; -},{}],61:[function(require,module,exports){ +},{}],60:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13936,7 +13786,7 @@ class GetAlias extends _index.DeviceApiCall { } exports.GetAlias = GetAlias; -},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":60}],62:[function(require,module,exports){ +},{"../../packages/device-api/index.js":2,"./__generated__/validators.zod.js":59}],61:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -13944,8 +13794,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; @@ -13954,39 +13803,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'); } - throw new Error('unimplemented handler: ' + deviceApiCall.method); - } else { - throw e; + 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'); + } + } + }; + 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}],63:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../__generated__/deviceApiCalls.js":58}],62:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14029,7 +13972,7 @@ class AppleTransport extends _index.DeviceApiTransport { } exports.AppleTransport = AppleTransport; -},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":6}],64:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../../packages/messaging/messaging.js":5}],63:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14183,7 +14126,7 @@ async function extensionSpecificSetIncontextSignupPermanentlyDismissedAtCall(par }); } -},{"../../../packages/device-api/index.js":2,"../../Settings.js":42,"../../autofill-utils.js":55,"../__generated__/deviceApiCalls.js":59}],65:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2,"../../Settings.js":41,"../../autofill-utils.js":54,"../__generated__/deviceApiCalls.js":58}],64:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14227,7 +14170,7 @@ function createTransport(globalConfig) { return new _extensionTransport.ExtensionTransport(globalConfig); } -},{"./android.transport.js":62,"./apple.transport.js":63,"./extension.transport.js":64,"./windows.transport.js":66}],66:[function(require,module,exports){ +},{"./android.transport.js":61,"./apple.transport.js":62,"./extension.transport.js":63,"./windows.transport.js":65}],65:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14312,7 +14255,7 @@ function waitForWindowsResponse(responseId, options) { }); } -},{"../../../packages/device-api/index.js":2}],67:[function(require,module,exports){ +},{"../../../packages/device-api/index.js":2}],66:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14413,7 +14356,7 @@ module.exports={ } } -},{}],68:[function(require,module,exports){ +},{}],67:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14514,7 +14457,7 @@ module.exports={ } } -},{}],69:[function(require,module,exports){ +},{}],68:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14615,7 +14558,7 @@ module.exports={ } } -},{}],70:[function(require,module,exports){ +},{}],69:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14716,7 +14659,7 @@ module.exports={ } } -},{}],71:[function(require,module,exports){ +},{}],70:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -14817,7 +14760,7 @@ module.exports={ } } -},{}],72:[function(require,module,exports){ +},{}],71:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -14919,7 +14862,7 @@ module.exports={ } } -},{}],73:[function(require,module,exports){ +},{}],72:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15020,7 +14963,7 @@ module.exports={ } } -},{}],74:[function(require,module,exports){ +},{}],73:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15121,7 +15064,7 @@ module.exports={ } } -},{}],75:[function(require,module,exports){ +},{}],74:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15222,7 +15165,7 @@ module.exports={ } } -},{}],76:[function(require,module,exports){ +},{}],75:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15323,7 +15266,7 @@ module.exports={ } } -},{}],77:[function(require,module,exports){ +},{}],76:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15424,7 +15367,7 @@ module.exports={ } } -},{}],78:[function(require,module,exports){ +},{}],77:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15525,7 +15468,7 @@ module.exports={ } } -},{}],79:[function(require,module,exports){ +},{}],78:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15626,7 +15569,7 @@ module.exports={ } } -},{}],80:[function(require,module,exports){ +},{}],79:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15727,7 +15670,7 @@ module.exports={ } } -},{}],81:[function(require,module,exports){ +},{}],80:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15828,7 +15771,7 @@ module.exports={ } } -},{}],82:[function(require,module,exports){ +},{}],81:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -15929,7 +15872,7 @@ module.exports={ } } -},{}],83:[function(require,module,exports){ +},{}],82:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16030,7 +15973,7 @@ module.exports={ } } -},{}],84:[function(require,module,exports){ +},{}],83:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16131,7 +16074,7 @@ module.exports={ } } -},{}],85:[function(require,module,exports){ +},{}],84:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16232,7 +16175,7 @@ module.exports={ } } -},{}],86:[function(require,module,exports){ +},{}],85:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16333,7 +16276,7 @@ module.exports={ } } -},{}],87:[function(require,module,exports){ +},{}],86:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16434,7 +16377,7 @@ module.exports={ } } -},{}],88:[function(require,module,exports){ +},{}],87:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16535,7 +16478,7 @@ module.exports={ } } -},{}],89:[function(require,module,exports){ +},{}],88:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16636,7 +16579,7 @@ module.exports={ } } -},{}],90:[function(require,module,exports){ +},{}],89:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -16724,7 +16667,7 @@ function translateImpl(library, namespacedId, opts) { return out; } -},{"./translations.js":93}],91:[function(require,module,exports){ +},{"./translations.js":92}],90:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16825,7 +16768,7 @@ module.exports={ } } -},{}],92:[function(require,module,exports){ +},{}],91:[function(require,module,exports){ module.exports={ "smartling" : { "string_format" : "icu", @@ -16926,7 +16869,7 @@ module.exports={ } } -},{}],93:[function(require,module,exports){ +},{}],92:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17045,7 +16988,7 @@ var _default = exports.default = { } }; -},{"./bg/autofill.json":67,"./cs/autofill.json":68,"./da/autofill.json":69,"./de/autofill.json":70,"./el/autofill.json":71,"./en/autofill.json":72,"./es/autofill.json":73,"./et/autofill.json":74,"./fi/autofill.json":75,"./fr/autofill.json":76,"./hr/autofill.json":77,"./hu/autofill.json":78,"./it/autofill.json":79,"./lt/autofill.json":80,"./lv/autofill.json":81,"./nb/autofill.json":82,"./nl/autofill.json":83,"./pl/autofill.json":84,"./pt/autofill.json":85,"./ro/autofill.json":86,"./ru/autofill.json":87,"./sk/autofill.json":88,"./sl/autofill.json":89,"./sv/autofill.json":91,"./tr/autofill.json":92,"./xa/autofill.json":94}],94:[function(require,module,exports){ +},{"./bg/autofill.json":66,"./cs/autofill.json":67,"./da/autofill.json":68,"./de/autofill.json":69,"./el/autofill.json":70,"./en/autofill.json":71,"./es/autofill.json":72,"./et/autofill.json":73,"./fi/autofill.json":74,"./fr/autofill.json":75,"./hr/autofill.json":76,"./hu/autofill.json":77,"./it/autofill.json":78,"./lt/autofill.json":79,"./lv/autofill.json":80,"./nb/autofill.json":81,"./nl/autofill.json":82,"./pl/autofill.json":83,"./pt/autofill.json":84,"./ro/autofill.json":85,"./ru/autofill.json":86,"./sk/autofill.json":87,"./sl/autofill.json":88,"./sv/autofill.json":90,"./tr/autofill.json":91,"./xa/autofill.json":93}],93:[function(require,module,exports){ module.exports={ "smartling": { "string_format": "icu", @@ -17138,7 +17081,7 @@ module.exports={ "note": "Button that prevents the DuckDuckGo email protection signup prompt from appearing again." } } -},{}],95:[function(require,module,exports){ +},{}],94:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17181,4 +17124,4 @@ window.cancelIdleCallback = window.cancelIdleCallback || function (id) { }; var _default = exports.default = {}; -},{}]},{},[56]); +},{}]},{},[55]); diff --git a/package-lock.json b/package-lock.json index 0e79a4e4e737..9fa934cb6065 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@duckduckgo/autoconsent": "^10.15.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#14.0.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#13.1.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.19.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#7.0.2", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523" @@ -71,14 +71,12 @@ } }, "node_modules/@duckduckgo/autofill": { - "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#8462e3b2d03015246e06ca5b6cfe9e381f626095", - "hasInstallScript": true, - "license": "Apache-2.0" + "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#1fee787458d13f8ed07f9fe81aecd6e59609339e", + "hasInstallScript": true }, "node_modules/@duckduckgo/content-scope-scripts": { "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#1ed569676555d493c9c5575eaed22aa02569aac9", "hasInstallScript": true, - "license": "Apache-2.0", "workspaces": [ "packages/special-pages", "packages/messaging" @@ -98,8 +96,7 @@ } }, "node_modules/@duckduckgo/privacy-reference-tests": { - "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#6133e7d9d9cd5f1b925cab1971b4d785dc639df7", - "license": "Apache-2.0" + "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#6133e7d9d9cd5f1b925cab1971b4d785dc639df7" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", diff --git a/package.json b/package.json index b1275ff6b8b5..fe356813134b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@duckduckgo/autoconsent": "^10.15.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#14.0.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#13.1.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.19.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#7.0.2", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1724449523" From 2d21a5bc6bc0f87136a30c39ef82c3e432f3bc9b Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 15:19:39 +0200 Subject: [PATCH 5/9] fixing unit test --- .../com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt index d5ca5b1ea56c..5b15839b311f 100644 --- a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt +++ b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt @@ -61,7 +61,7 @@ class EmailJavascriptInterfaceTest { mockAutofill, ) { counter++ } - autofillFeature.self().setEnabled(Toggle.State(enable = true)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) whenever(mockAutofill.isAnException(any())).thenReturn(false) } @@ -131,7 +131,7 @@ class EmailJavascriptInterfaceTest { @Test fun whenShowTooltipAndFeatureDisabledThenLambdaNotCalled() { whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) - autofillFeature.self().setEnabled(Toggle.State(enable = false)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = false)) testee.showTooltip() From 7dca4801b32e72d26300dce03e374c72593cc4bc Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 15:30:40 +0200 Subject: [PATCH 6/9] suppress lint on Test file --- .../autofill/impl/AutofillCapabilityCheckerImplTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt index 352a830e6eeb..2dd7b85368a6 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/AutofillCapabilityCheckerImplTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.autofill.impl +import android.annotation.SuppressLint import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.InternalTestUserChecker @@ -130,6 +131,7 @@ class AutofillCapabilityCheckerImplTest { assertFalse(testee.canSaveCredentialsFromWebView(URL)) } + @SuppressLint("DenyListedApi") private suspend fun setupConfig( topLevelFeatureEnabled: Boolean = false, autofillEnabledByUser: Boolean = false, From 4ee2cb9788fb40857ce502be04ea3b7664cef900 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 15:54:26 +0200 Subject: [PATCH 7/9] fixing lint issues --- .../com/duckduckgo/app/email/EmailInjectorJsTest.kt | 13 +++++++++---- .../ResultHandlerEmailProtectionChooseEmail.kt | 2 +- .../ResultHandlerInContextEmailProtectionPrompt.kt | 2 +- .../saving/ResultHandlerSaveLoginCredentials.kt | 2 +- .../selecting/ResultHandlerCredentialSelection.kt | 2 +- .../updating/ResultHandlerUpdateLoginCredentials.kt | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt index 2a2cc540789b..a613d52eb799 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.email +import android.annotation.SuppressLint import android.webkit.WebView import androidx.test.annotation.UiThreadTest import androidx.test.filters.SdkSuppress @@ -59,6 +60,7 @@ class EmailInjectorJsTest { whenever(mockAutofill.isAnException(any())).thenReturn(false) } + @SuppressLint("DenyListedApi") @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) @@ -66,18 +68,19 @@ class EmailInjectorJsTest { val address = "address" val jsToEvaluate = getAliasJsToEvaluate().replace("%s", address) val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) - autofillFeature.self().setEnabled(Toggle.State(enable = true)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) testee.injectAddressInEmailField(webView, address, "https://example.com") verify(webView).evaluateJavascript(jsToEvaluate, null) } + @SuppressLint("DenyListedApi") @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) fun whenInjectAddressAndFeatureIsDisabledThenJsCodeNotInjected() { - autofillFeature.self().setEnabled(Toggle.State(enable = true)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) val address = "address" val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) @@ -127,12 +130,13 @@ class EmailInjectorJsTest { verify(webView, never()).evaluateJavascript(jsToEvaluate, null) } + @SuppressLint("DenyListedApi") @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsDisabledAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { whenever(mockEmailManager.isSignedIn()).thenReturn(false) - autofillFeature.self().setEnabled(Toggle.State(enable = false)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = false)) val jsToEvaluate = getNotifySignOutJsToEvaluate() val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) @@ -142,12 +146,13 @@ class EmailInjectorJsTest { verify(webView, never()).evaluateJavascript(jsToEvaluate, null) } + @SuppressLint("DenyListedApi") @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsEnabledAndEmailIsNotSignedInThenEvaluateJsCode() { whenever(mockEmailManager.isSignedIn()).thenReturn(false) - autofillFeature.self().setEnabled(Toggle.State(enable = true)) + autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) val jsToEvaluate = getNotifySignOutJsToEvaluate() val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt index 6b9e1e58cf8b..10272eb798b3 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 @@ -133,7 +133,7 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { + if (appBuildConfig.sdkInt >= 33) { getParcelable(key, T::class.java) } else { getParcelable(key) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/incontext/ResultHandlerInContextEmailProtectionPrompt.kt index cf078914ed8c..bca04333ecf9 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 @@ -90,7 +90,7 @@ class ResultHandlerInContextEmailProtectionPrompt @Inject constructor( @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { + if (appBuildConfig.sdkInt >= 33) { getParcelable(key, T::class.java) } else { getParcelable(key) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentials.kt index 25441bf51617..fee51f2829f4 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 @@ -80,7 +80,7 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { + if (appBuildConfig.sdkInt >= 33) { getParcelable(key, T::class.java) } else { getParcelable(key) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelection.kt index 1ef8f2569f5e..ca06560a94ad 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 @@ -138,7 +138,7 @@ class ResultHandlerCredentialSelection @Inject constructor( @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { + if (appBuildConfig.sdkInt >= 33) { getParcelable(key, T::class.java) } else { getParcelable(key) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentials.kt index a1b7019fb820..b0cc1d9a1afe 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 @@ -78,7 +78,7 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( @Suppress("DEPRECATION") @SuppressLint("NewApi") private inline fun Bundle.safeGetParcelable(key: String) = - if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU) { + if (appBuildConfig.sdkInt >= 33) { getParcelable(key, T::class.java) } else { getParcelable(key) From d892d1347e856f3aff17982917dc71b103e0c005 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 15:58:06 +0200 Subject: [PATCH 8/9] fix format --- .../impl/email/ResultHandlerEmailProtectionChooseEmail.kt | 1 - .../incontext/ResultHandlerInContextEmailProtectionPrompt.kt | 1 - .../ui/credential/saving/ResultHandlerSaveLoginCredentials.kt | 1 - .../ui/credential/selecting/ResultHandlerCredentialSelection.kt | 1 - .../credential/updating/ResultHandlerUpdateLoginCredentials.kt | 1 - 5 files changed, 5 deletions(-) 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 10272eb798b3..a815300baecb 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 @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.email import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.Fragment 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 bca04333ecf9..d31295cd3551 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 @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.email.incontext import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.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 fee51f2829f4..141d04e73b2e 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 @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.ui.credential.saving import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.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 ca06560a94ad..4a3d7095283c 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 @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.ui.credential.selecting import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.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 b0cc1d9a1afe..d77541072157 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 @@ -18,7 +18,6 @@ package com.duckduckgo.autofill.impl.ui.credential.updating import android.annotation.SuppressLint import android.content.Context -import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.fragment.app.Fragment From e4d8ffeeed4bff4d4f3f66cefc17ddb919aacc26 Mon Sep 17 00:00:00 2001 From: Cristian Monforte Date: Wed, 9 Oct 2024 16:12:56 +0200 Subject: [PATCH 9/9] lint failure --- .../com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt index 5b15839b311f..fa1ccb33707c 100644 --- a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt +++ b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.email +import android.annotation.SuppressLint import android.webkit.WebView import androidx.test.ext.junit.runners.AndroidJUnit4 import com.duckduckgo.app.browser.DuckDuckGoUrlDetectorImpl @@ -48,6 +49,7 @@ class EmailJavascriptInterfaceTest { lateinit var testee: EmailJavascriptInterface private var counter = 0 + @SuppressLint("DenyListedApi") @Before fun setup() { autofillFeature = com.duckduckgo.autofill.api.FakeAutofillFeature.create() @@ -128,6 +130,7 @@ class EmailJavascriptInterfaceTest { assertEquals(1, counter) } + @SuppressLint("DenyListedApi") @Test fun whenShowTooltipAndFeatureDisabledThenLambdaNotCalled() { whenever(mockWebView.url).thenReturn(NON_EMAIL_URL)