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 9ed01201b647..6092d0014a3f 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -210,6 +210,7 @@ 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.AutofillImportLaunchSource.InBrowserPromo import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreenWithSuggestions import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementViewCredential @@ -761,6 +762,20 @@ class BrowserTabFragment : viewModel.onShowUserCredentialsSaved(savedCredentials) } + override suspend fun promptUserToImportPassword(originalUrl: String) { + withContext(dispatchers.main()) { + showDialogHidingPrevious( + credentialAutofillDialogFactory.autofillImportPasswordsPromoDialog( + importSource = InBrowserPromo, + tabId = tabId, + url = originalUrl, + ), + tabId, + requiredUrl = originalUrl, + ) + } + } + override suspend fun onCredentialsAvailableToSave( currentUrl: String, credentials: LoginCredentials, @@ -3178,13 +3193,16 @@ class 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, - ) + lifecycleScope.launch { + plugin.processResult( + result = result, + context = it, + tabId = tabId, + fragment = this@BrowserTabFragment, + autofillCallback = this@BrowserTabFragment, + webView = webView, + ) + } } } } 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 3c180fa88838..4bf2e3b3fe5a 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 @@ -249,6 +249,11 @@ interface CredentialAutofillDialogFactory { * Creates a dialog which prompts the user to sign up for Email Protection */ fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment + + /** + * Creates a dialog which prompts the user to import passwords from Google Passwords + */ + fun autofillImportPasswordsPromoDialog(importSource: AutofillImportLaunchSource, tabId: String, url: String): DialogFragment } private fun prefix( @@ -257,3 +262,13 @@ private fun prefix( ): String { return "$tabId/$tag" } + +@Parcelize +enum class AutofillImportLaunchSource(val value: String) : Parcelable { + PasswordManagementPromo("password_management_promo"), + PasswordManagementEmptyState("password_management_empty_state"), + PasswordManagementOverflow("password_management_overflow"), + AutofillSettings("autofill_settings_button"), + InBrowserPromo("in_browser_promo"), + Unknown("unknown"), +} 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 e411a2cdde82..ed48dccd3fbd 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 @@ -154,4 +154,7 @@ interface AutofillFeature { @Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE) fun canPromoteImportPasswordsInPasswordManagement(): Toggle + + @Toggle.DefaultValue(defaultValue = DefaultFeatureValue.TRUE) + fun canPromoteImportGooglePasswordsInBrowser(): Toggle } diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFragmentResultsPlugin.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFragmentResultsPlugin.kt index 29017581d455..16ee89eebbb5 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFragmentResultsPlugin.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillFragmentResultsPlugin.kt @@ -18,6 +18,7 @@ package com.duckduckgo.autofill.api import android.content.Context import android.os.Bundle +import android.webkit.WebView import androidx.annotation.MainThread import androidx.fragment.app.Fragment @@ -34,12 +35,13 @@ interface AutofillFragmentResultsPlugin { * Will be invoked on the main thread. */ @MainThread - fun processResult( + suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) /** diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/BrowserAutofill.kt index 941a1ef02fe1..01c21e01d95c 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 @@ -153,4 +153,9 @@ interface Callback { * Called when credentials have been saved, and we want to show the user some visual confirmation. */ fun onCredentialsSaved(savedCredentials: LoginCredentials) + + /** + * Called when the user should be prompted to import passwords from Google. + */ + suspend fun promptUserToImportPassword(originalUrl: String) } 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 index 1c07b74ac059..cf156ce6440c 100644 --- 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 @@ -33,6 +33,7 @@ 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.importing.InBrowserImportPromo import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser @@ -46,6 +47,7 @@ import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillInputSubTy 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.CREDENTIALS_IMPORT import com.duckduckgo.autofill.impl.jsbridge.request.SupportedAutofillTriggerType.USER_INITIATED import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter import com.duckduckgo.autofill.impl.partialsave.PartialCredentialSaveStore @@ -133,6 +135,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( private val partialCredentialSaveStore: PartialCredentialSaveStore, private val usernameBackFiller: UsernameBackFiller, private val existingCredentialMatchDetector: ExistingCredentialMatchDetector, + private val inBrowserImportPromo: InBrowserImportPromo, ) : AutofillJavascriptInterface { override var callback: Callback? = null @@ -186,6 +189,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( } } + private fun handlePromoteImport(url: String) { + coroutineScope.launch(dispatcherProvider.io()) { + callback?.promptUserToImportPassword(url) + } + } + @JavascriptInterface override fun getIncontextSignupDismissedAt(data: String) { emailProtectionInContextSignupJob += coroutineScope.launch(dispatcherProvider.io()) { @@ -246,7 +255,12 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( val finalCredentialList = ensureUsernamesNotNull(dedupedCredentials) if (finalCredentialList.isEmpty()) { - callback?.noCredentialsAvailable(url) + val canShowImport = inBrowserImportPromo.canShowPromo(credentialsAvailableForCurrentPage = false, url = url) + if (canShowImport) { + handlePromoteImport(url) + } else { + callback?.noCredentialsAvailable(url) + } } else { callback?.onCredentialsAvailableToInject(url, finalCredentialList, triggerType) } @@ -265,6 +279,7 @@ class AutofillStoredBackJavascriptInterface @Inject constructor( return when (trigger) { USER_INITIATED -> LoginTriggerType.USER_INITIATED AUTOPROMPT -> LoginTriggerType.AUTOPROMPT + CREDENTIALS_IMPORT -> LoginTriggerType.AUTOPROMPT } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt index 3aa4a57f501b..ca48b48273a9 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStore.kt @@ -94,11 +94,24 @@ class SecureStoreBackedAutofillStore @Inject constructor( return autofillPrefsStore.hasEverImportedPasswordsFlow() } - override var hasDismissedImportedPasswordsPromo: Boolean - get() = autofillPrefsStore.hasDismissedImportedPasswordsPromo + override var hasDeclinedPasswordManagementImportPromo: Boolean + get() = autofillPrefsStore.hasDeclinedPasswordManagementImportPromo set(value) { - autofillPrefsStore.hasDismissedImportedPasswordsPromo = value + autofillPrefsStore.hasDeclinedPasswordManagementImportPromo = value } + + override var hasDeclinedInBrowserPasswordImportPromo: Boolean + get() = autofillPrefsStore.hasDeclinedInBrowserPasswordImportPromo + set(value) { + autofillPrefsStore.hasDeclinedInBrowserPasswordImportPromo = value + } + + override var inBrowserImportPromoShownCount: Int + get() = autofillPrefsStore.inBrowserImportPromoShownCount + set(value) { + autofillPrefsStore.inBrowserImportPromoShownCount = value + } + override var autofillDeclineCount: Int get() = autofillPrefsStore.autofillDeclineCount set(value) { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillAvailableInputTypesProvider.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillAvailableInputTypesProvider.kt new file mode 100644 index 000000000000..458c1bd8b261 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillAvailableInputTypesProvider.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 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 com.duckduckgo.autofill.api.AutofillCapabilityChecker +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider.AvailableInputTypes +import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo +import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials +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.AppScope +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import kotlinx.coroutines.withContext + +interface AutofillAvailableInputTypesProvider { + suspend fun getTypes(url: String?): AvailableInputTypes + + data class AvailableInputTypes( + val username: Boolean = false, + val password: Boolean = false, + val email: Boolean = false, + val credentialsImport: Boolean = false, + ) +} + +@ContributesBinding(AppScope::class) +class RealAutofillAvailableInputTypesProvider @Inject constructor( + private val emailManager: EmailManager, + private val autofillStore: InternalAutofillStore, + private val shareableCredentials: ShareableCredentials, + private val autofillCapabilityChecker: AutofillCapabilityChecker, + private val inBrowserPromo: InBrowserImportPromo, + private val dispatchers: DispatcherProvider, +) : AutofillAvailableInputTypesProvider { + + override suspend fun getTypes(url: String?): AvailableInputTypes { + return withContext(dispatchers.io()) { + val availableInputTypeCredentials = determineIfCredentialsAvailable(url) + val credentialsAvailableOnThisPage = availableInputTypeCredentials.username || availableInputTypeCredentials.password + val emailAvailable = determineIfEmailAvailable() + val importPromoAvailable = inBrowserPromo.canShowPromo(credentialsAvailableOnThisPage, url) + + AvailableInputTypes( + username = availableInputTypeCredentials.username, + password = availableInputTypeCredentials.password, + email = emailAvailable, + credentialsImport = importPromoAvailable, + ) + } + } + + private suspend fun determineIfCredentialsAvailable(url: String?): AvailableInputTypeCredentials { + return if (url == null || !autofillCapabilityChecker.canInjectCredentialsToWebView(url)) { + AvailableInputTypeCredentials(username = false, password = false) + } else { + val matches = mutableListOf() + val directMatches = autofillStore.getCredentials(url) + val shareableMatches = shareableCredentials.shareableCredentials(url) + matches.addAll(directMatches) + matches.addAll(shareableMatches) + + val usernameSearch = matches.find { !it.username.isNullOrEmpty() } + val passwordSearch = matches.find { !it.password.isNullOrEmpty() } + + AvailableInputTypeCredentials(username = usernameSearch != null, password = passwordSearch != null) + } + } + private fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn() +} 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 3f731664fae2..93ef78a5f8b5 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 @@ -18,12 +18,7 @@ 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.email.incontext.availability.EmailProtectionInContextAvailabilityRules -import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials -import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials -import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding @@ -40,15 +35,13 @@ interface AutofillRuntimeConfigProvider { @ContributesBinding(AppScope::class) class RealAutofillRuntimeConfigProvider @Inject constructor( - private val emailManager: EmailManager, - private val autofillStore: InternalAutofillStore, private val runtimeConfigurationWriter: RuntimeConfigurationWriter, private val autofillCapabilityChecker: AutofillCapabilityChecker, private val autofillFeature: AutofillFeature, - private val shareableCredentials: ShareableCredentials, private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules, private val neverSavedSiteRepository: NeverSavedSiteRepository, private val siteSpecificFixesStore: AutofillSiteSpecificFixesStore, + private val autofillAvailableInputTypesProvider: AutofillAvailableInputTypesProvider, ) : AutofillRuntimeConfigProvider { override suspend fun getRuntimeConfiguration( rawJs: String, @@ -88,32 +81,14 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( } private suspend fun generateAvailableInputTypes(url: String?): String { - val credentialsAvailable = determineIfCredentialsAvailable(url) - val emailAvailable = determineIfEmailAvailable() + val inputTypes = autofillAvailableInputTypesProvider.getTypes(url) - val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(credentialsAvailable, emailAvailable).also { + val json = runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(inputTypes).also { logcat(VERBOSE) { "autofill-config: availableInputTypes for $url: \n$it" } } return "availableInputTypes = $json" } - private suspend fun determineIfCredentialsAvailable(url: String?): AvailableInputTypeCredentials { - return if (url == null || !autofillCapabilityChecker.canInjectCredentialsToWebView(url)) { - AvailableInputTypeCredentials(username = false, password = false) - } else { - val matches = mutableListOf() - val directMatches = autofillStore.getCredentials(url) - val shareableMatches = shareableCredentials.shareableCredentials(url) - matches.addAll(directMatches) - matches.addAll(shareableMatches) - - val usernameSearch = matches.find { !it.username.isNullOrEmpty() } - val passwordSearch = matches.find { !it.password.isNullOrEmpty() } - - AvailableInputTypeCredentials(username = usernameSearch != null, password = passwordSearch != null) - } - } - private suspend fun canInjectCredentials(url: String?): Boolean { if (url == null) return false return autofillCapabilityChecker.canInjectCredentialsToWebView(url) @@ -160,8 +135,6 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( return emailProtectionInContextAvailabilityRules.permittedToShow(url) } - private fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn() - companion object { private const val TAG_INJECT_CONTENT_SCOPE = "// INJECT contentScope HERE" private const val TAG_INJECT_USER_UNPROTECTED_DOMAINS = "// INJECT userUnprotectedDomains HERE" 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 ba3514e22675..0a4fd883cdff 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 @@ -16,6 +16,7 @@ package com.duckduckgo.autofill.impl.configuration +import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider.AvailableInputTypes import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputSuccessResponse import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.duckduckgo.di.scopes.AppScope @@ -25,11 +26,7 @@ import dagger.SingleInstanceIn import javax.inject.Inject interface RuntimeConfigurationWriter { - fun generateResponseGetAvailableInputTypes( - credentialsAvailable: AvailableInputTypeCredentials, - emailAvailable: Boolean, - ): String - + fun generateResponseGetAvailableInputTypes(availableInputTypes: AvailableInputTypes): String fun generateContentScope(settingsJson: AutofillSiteSpecificFixesSettings): String fun generateUserUnprotectedDomains(): String fun generateUserPreferences( @@ -49,12 +46,10 @@ interface RuntimeConfigurationWriter { class RealRuntimeConfigurationWriter @Inject constructor(val moshi: Moshi) : RuntimeConfigurationWriter { private val availableInputTypesAdapter = moshi.adapter(AvailableInputSuccessResponse::class.java).indent(" ") - override fun generateResponseGetAvailableInputTypes( - credentialsAvailable: AvailableInputTypeCredentials, - emailAvailable: Boolean, - ): String { - val availableInputTypes = AvailableInputSuccessResponse(credentialsAvailable, emailAvailable) - return availableInputTypesAdapter.toJson(availableInputTypes) + override fun generateResponseGetAvailableInputTypes(availableInputTypes: AvailableInputTypes): String { + val credentialTypes = AvailableInputTypeCredentials(username = availableInputTypes.username, password = availableInputTypes.password) + val response = AvailableInputSuccessResponse(credentialTypes, availableInputTypes.email, availableInputTypes.credentialsImport) + return availableInputTypesAdapter.toJson(response) } private fun generateSiteSpecificFixesJson(settingsJson: AutofillSiteSpecificFixesSettings): String { 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 97f4e304afef..fb7a374278b7 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 @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel @@ -58,12 +59,13 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( private val partialCredentialSaveStore: PartialCredentialSaveStore, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } 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 46a42ca964ff..ccafb3d39cf1 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 @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.appbuildconfig.api.AppBuildConfig @@ -46,7 +47,14 @@ class ResultHandlerInContextEmailProtectionPrompt @Inject constructor( private val dataStore: EmailProtectionInContextDataStore, private val appBuildConfig: AppBuildConfig, ) : AutofillFragmentResultsPlugin { - override fun processResult(result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener) { + override suspend fun processResult( + result: Bundle, + context: Context, + tabId: String, + fragment: Fragment, + autofillCallback: AutofillEventListener, + webView: WebView?, + ) { logcat { "${this::class.java.simpleName}: processing result" } val userSelection = result.safeGetParcelable(EmailProtectionInContextSignUpDialog.KEY_RESULT) ?: return diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/AutofillImportLaunchSource.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/AutofillImportLaunchSource.kt deleted file mode 100644 index 3114d45fbce6..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/AutofillImportLaunchSource.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2025 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.importing - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class AutofillImportLaunchSource(val value: String) : Parcelable { - PasswordManagementPromo("password_management_promo"), - PasswordManagementEmptyState("password_management_empty_state"), - PasswordManagementOverflow("password_management_overflow"), - AutofillSettings("autofill_settings_button"), - Unknown("unknown"), -} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/InBrowserImportPromo.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/InBrowserImportPromo.kt new file mode 100644 index 000000000000..5adba9649aef --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/InBrowserImportPromo.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 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.importing + +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.WebMessageListener +import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository +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.flow.firstOrNull +import kotlinx.coroutines.withContext + +interface InBrowserImportPromo { + suspend fun canShowPromo( + credentialsAvailableForCurrentPage: Boolean, + url: String?, + ): Boolean +} + +@ContributesBinding(AppScope::class) +class RealInBrowserImportPromo @Inject constructor( + private val autofillStore: InternalAutofillStore, + private val dispatchers: DispatcherProvider, + private val neverSavedSiteRepository: NeverSavedSiteRepository, + private val autofillFeature: AutofillFeature, + private val webViewCapabilityChecker: WebViewCapabilityChecker, +) : InBrowserImportPromo { + + override suspend fun canShowPromo( + credentialsAvailableForCurrentPage: Boolean, + url: String?, + ): Boolean { + return withContext(dispatchers.io()) { + if (credentialsAvailableForCurrentPage) { + return@withContext false + } + + if (featureEnabled().not()) { + return@withContext false + } + + if (autofillStore.hasEverImportedPasswords) { + return@withContext false + } + + if (autofillStore.hasDeclinedInBrowserPasswordImportPromo) { + return@withContext false + } + + if ((autofillStore.getCredentialCount().firstOrNull() ?: 0) >= MAX_CREDENTIALS_FOR_PROMO) { + return@withContext false + } + + if (autofillStore.inBrowserImportPromoShownCount >= MAX_PROMO_SHOWN_COUNT) { + return@withContext false + } + + if (url != null && neverSavedSiteRepository.isInNeverSaveList(url)) { + return@withContext false + } + + if (webViewCapableOfImporting().not()) { + return@withContext false + } + + return@withContext true + } + } + + private suspend fun webViewCapableOfImporting(): Boolean { + val webViewWebMessageSupport = webViewCapabilityChecker.isSupported(WebMessageListener) + val webViewDocumentStartJavascript = webViewCapabilityChecker.isSupported(DocumentStartJavaScript) + return webViewWebMessageSupport && webViewDocumentStartJavascript + } + + private fun featureEnabled(): Boolean { + if (autofillFeature.self().isEnabled().not()) return false + if (autofillFeature.canPromoteImportGooglePasswordsInBrowser().isEnabled().not()) return false + return true + } + + companion object { + const val MAX_PROMO_SHOWN_COUNT = 5 + const val MAX_CREDENTIALS_FOR_PROMO = 25 + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ResultHandlerImportPasswords.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ResultHandlerImportPasswords.kt new file mode 100644 index 000000000000..efae3ca47ae0 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ResultHandlerImportPasswords.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 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.importing + +import android.content.Context +import android.os.Bundle +import android.webkit.WebView +import androidx.fragment.app.Fragment +import com.duckduckgo.autofill.api.AutofillEventListener +import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider +import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster +import com.duckduckgo.autofill.impl.jsbridge.response.AutofillResponseWriter +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog.ImportPasswordsDialog +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject +import kotlinx.coroutines.withContext +import logcat.logcat + +@ContributesMultibinding(AppScope::class) +class ResultHandlerImportPasswords @Inject constructor( + private val autofillResponseWriter: AutofillResponseWriter, + private val autofillAvailableInputTypesProvider: AutofillAvailableInputTypesProvider, + private val dispatchers: DispatcherProvider, + private val autofillMessagePoster: AutofillMessagePoster, +) : AutofillFragmentResultsPlugin { + + override suspend fun processResult( + result: Bundle, + context: Context, + tabId: String, + fragment: Fragment, + autofillCallback: AutofillEventListener, + webView: WebView?, + ) { + logcat { "Autofill: processing import passwords result for tab $tabId" } + if (result.getBoolean(ImportPasswordsDialog.KEY_IMPORT_SUCCESS)) { + logcat { "Autofill: refresh after import passwords success" } + val originalUrl = result.getString(ImportPasswordsDialog.KEY_URL) + if (originalUrl != null && webView != null) { + refreshAvailableInputTypes(webView, originalUrl) + } else { + logcat { "Autofill: cannot refresh available input types for url=$originalUrl (webView is null: ${webView == null})" } + } + } else { + logcat { "Autofill: import didn't succeed; returning a 'no credential' response" } + val originalUrl = result.getString(ImportPasswordsDialog.KEY_URL) ?: return + autofillCallback.onNoCredentialsChosenForAutofill(originalUrl) + } + } + + private suspend fun refreshAvailableInputTypes( + webView: WebView, + originalUrl: String, + ) { + withContext(dispatchers.io()) { + val availableInputTypes = autofillAvailableInputTypesProvider.getTypes(originalUrl) + val json = autofillResponseWriter.generateResponseNewAutofillDataAvailable(availableInputTypes) + logcat { "Autofill: import completed; refresh request: $json" } + autofillMessagePoster.postMessage(webView, json) + } + } + + override fun resultKey(tabId: String): String { + return ImportPasswordsDialog.resultKey(tabId) + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt index af7829d47625..a4595bda7bc5 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowFragment.kt @@ -264,13 +264,16 @@ class ImportGooglePasswordsWebFlowFragment : autofillFragmentResultListeners.getPlugins().forEach { plugin -> setFragmentResultListener(plugin.resultKey(CUSTOM_FLOW_TAB_ID)) { _, result -> context?.let { ctx -> - plugin.processResult( - result = result, - context = ctx, - tabId = CUSTOM_FLOW_TAB_ID, - fragment = this@ImportGooglePasswordsWebFlowFragment, - autofillCallback = this@ImportGooglePasswordsWebFlowFragment, - ) + lifecycleScope.launch { + plugin.processResult( + result = result, + context = ctx, + tabId = CUSTOM_FLOW_TAB_ID, + fragment = this@ImportGooglePasswordsWebFlowFragment, + autofillCallback = this@ImportGooglePasswordsWebFlowFragment, + webView = binding?.webView, + ) + } } } } @@ -324,6 +327,10 @@ class ImportGooglePasswordsWebFlowFragment : } } + override suspend fun promptUserToImportPassword(originalUrl: String) { + // no-op, we don't prompt the user for anything in this flow + } + override suspend fun onCsvAvailable(csv: String) { viewModel.onCsvAvailable(csv) } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsPromotionViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsPromotionViewModel.kt index 2dddde067d94..1d4dd2c59f74 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsPromotionViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsPromotionViewModel.kt @@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementPromo import com.duckduckgo.autofill.impl.importing.promo.ImportInPasswordsPromotionViewModel.Command.DismissImport import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsVisibility.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsVisibility.kt index 6d6df4a7f3a2..93364681b037 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsVisibility.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/promo/ImportInPasswordsVisibility.kt @@ -76,14 +76,14 @@ class RealImportInPasswordsVisibility @Inject constructor( } override fun onPromoDismissed() { - internalAutofillStore.hasDismissedImportedPasswordsPromo = true + internalAutofillStore.hasDeclinedPasswordManagementImportPromo = true canShowImportPasswords = false } private suspend fun evaluateIfUserCanShowImportPromo(): Boolean { if (autofillFeature.canPromoteImportPasswordsInPasswordManagement().isEnabled().not()) return false - if (internalAutofillStore.hasEverImportedPasswords || internalAutofillStore.hasDismissedImportedPasswordsPromo) return false + if (internalAutofillStore.hasEverImportedPasswords || internalAutofillStore.hasDeclinedPasswordManagementImportPromo) return false val gpmImport = autofillFeature.self().isEnabled() && autofillFeature.canImportFromGooglePasswordManager().isEnabled() val webViewWebMessageSupport = webViewCapabilityChecker.isSupported(WebMessageListener) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt index 11672e3ff211..51bb8d764f10 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillDataRequest.kt @@ -77,6 +77,9 @@ enum class SupportedAutofillTriggerType { @Json(name = "autoprompt") AUTOPROMPT, + + @Json(name = "credentialsImport") + CREDENTIALS_IMPORT, } enum class FormSubmissionTriggerType { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt index 02ec5ff01536..a6f95046c5a6 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/request/AutofillRequestParser.kt @@ -55,7 +55,9 @@ class AutofillJsonRequestParser @Inject constructor( return withContext(dispatchers.io()) { val result = kotlin.runCatching { autofillDataRequestParser.fromJson(request) - }.getOrNull() + } + .onFailure { logcat(WARN) { "Failed to parse autofill JSON for AutofillDataRequest ${it.asLog()}\n$request" } } + .getOrNull() return@withContext if (result == null) { Result.failure(IllegalArgumentException("Failed to parse autofill JSON for AutofillDataRequest")) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponse.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponse.kt deleted file mode 100644 index b6be2798b5c4..000000000000 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponse.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2022 DuckDuckGo - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.duckduckgo.autofill.impl.jsbridge.response - -data class AutofillDataResponse( - val type: String = AUTOFILL_DATA_RESPONSE, - val success: CredentialSuccessResponse, -) { - - data class CredentialSuccessResponse( - val username: String? = "", - val password: String? = null, - ) -} - -data class AutofillAvailableInputTypesResponse( - val type: String = AVAILABLE_INPUT_TYPES_RESPONSE, - val success: AvailableInputSuccessResponse, -) { - - data class AvailableInputSuccessResponse( - val credentials: Boolean, - ) -} - -private const val AUTOFILL_DATA_RESPONSE = "getAutofillDataResponse" -private const val AVAILABLE_INPUT_TYPES_RESPONSE = "getAvailableInputTypesResponse" diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt index 4d3e20152b03..8d7e20c3cd9d 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillDataResponses.kt @@ -44,6 +44,16 @@ data class RejectGeneratedPasswordResponse( data class RejectGeneratedPassword(val action: String = "rejectGeneratedPassword") } +data class NewAutofillDataAvailableResponse( + val type: String = "getAutofillDataResponse", + val success: NewAutofillDataAvailable, +) { + data class NewAutofillDataAvailable( + val action: String = "refreshAvailableInputTypes", + val availableInputTypes: AvailableInputSuccessResponse, + ) +} + data class EmptyResponse( val type: String = "getAutofillDataResponse", val success: EmptyCredentialResponse, @@ -57,6 +67,7 @@ data class EmptyResponse( data class AvailableInputSuccessResponse( val credentials: AvailableInputTypeCredentials, val email: Boolean, + val credentialsImport: Boolean, ) data class AvailableInputTypeCredentials( diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/jsbridge/response/AutofillResponseWriter.kt index 6f9fd01ee623..666d4035c4fa 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 @@ -16,8 +16,10 @@ package com.duckduckgo.autofill.impl.jsbridge.response +import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider.AvailableInputTypes import com.duckduckgo.autofill.impl.domain.javascript.JavascriptCredentials import com.duckduckgo.autofill.impl.jsbridge.response.EmailProtectionInContextSignupDismissedAtResponse.DismissedAt +import com.duckduckgo.autofill.impl.jsbridge.response.NewAutofillDataAvailableResponse.NewAutofillDataAvailable import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import com.squareup.moshi.Moshi @@ -30,6 +32,7 @@ interface AutofillResponseWriter { fun generateResponseForRejectingGeneratedPassword(): String fun generateResponseForEmailProtectionInContextSignup(installedRecently: Boolean, permanentlyDismissedAtTimestamp: Long?): String fun generateResponseForEmailProtectionEndOfFlow(isSignedIn: Boolean): String + fun generateResponseNewAutofillDataAvailable(inputTypes: AvailableInputTypes): String } @ContributesBinding(AppScope::class) @@ -37,6 +40,7 @@ class AutofillJsonResponseWriter @Inject constructor(val moshi: Moshi) : Autofil private val autofillDataAdapterCredentialsAvailable = moshi.adapter(ContainingCredentials::class.java).indent(" ") private val autofillDataAdapterCredentialsUnavailable = moshi.adapter(EmptyResponse::class.java).indent(" ") + private val autofillDataAdapterRefreshData = moshi.adapter(NewAutofillDataAvailableResponse::class.java).indent(" ") private val autofillDataAdapterAcceptGeneratedPassword = moshi.adapter(AcceptGeneratedPasswordResponse::class.java).indent(" ") private val autofillDataAdapterRejectGeneratedPassword = moshi.adapter(RejectGeneratedPasswordResponse::class.java).indent(" ") private val emailProtectionDataAdapterInContextSignup = moshi.adapter(EmailProtectionInContextSignupDismissedAtResponse::class.java).indent(" ") @@ -77,4 +81,12 @@ class AutofillJsonResponseWriter @Inject constructor(val moshi: Moshi) : Autofil val topLevelResponse = ShowInContextEmailProtectionSignupPromptResponse(success = response) return emailDataAdapterInContextEndOfFlow.toJson(topLevelResponse) } + + override fun generateResponseNewAutofillDataAvailable(inputTypes: AvailableInputTypes): String { + val credentialTypes = AvailableInputTypeCredentials(username = inputTypes.username, password = inputTypes.password) + val inputTypesResponse = AvailableInputSuccessResponse(credentialTypes, inputTypes.email, inputTypes.credentialsImport) + val response = NewAutofillDataAvailable(availableInputTypes = inputTypesResponse) + val topLevelResponse = NewAutofillDataAvailableResponse(success = response) + return autofillDataAdapterRefreshData.toJson(topLevelResponse) + } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/AutofillEffectDispatcher.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/AutofillEffectDispatcher.kt index c17cff0a07e7..ab8f190abf29 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/AutofillEffectDispatcher.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/AutofillEffectDispatcher.kt @@ -16,7 +16,7 @@ package com.duckduckgo.autofill.impl.store -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import dagger.SingleInstanceIn diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt index b71a98d9605d..4964cf329464 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/store/InternalAutofillStore.kt @@ -42,9 +42,12 @@ interface InternalAutofillStore : AutofillStore { var hasEverImportedPasswords: Boolean + var hasDeclinedInBrowserPasswordImportPromo: Boolean + var hasDeclinedPasswordManagementImportPromo: Boolean + fun hasEverImportedPasswordsFlow(): Flow - var hasDismissedImportedPasswordsPromo: Boolean + var inBrowserImportPromoShownCount: Int /** * Find saved credential for the given id diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/CredentialAutofillDialogAndroidFactory.kt index badc6a00573c..099f144be784 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,12 +17,14 @@ package com.duckduckgo.autofill.impl.ui import androidx.fragment.app.DialogFragment +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory import com.duckduckgo.autofill.api.CredentialUpdateExistingCredentialsDialog.CredentialUpdateType import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType import com.duckduckgo.autofill.impl.email.EmailProtectionChooseEmailFragment import com.duckduckgo.autofill.impl.email.incontext.prompt.EmailProtectionInContextSignUpPromptFragment +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog import com.duckduckgo.autofill.impl.ui.credential.passwordgeneration.AutofillUseGeneratedPasswordDialogFragment import com.duckduckgo.autofill.impl.ui.credential.saving.AutofillSavingCredentialsDialogFragment import com.duckduckgo.autofill.impl.ui.credential.selecting.AutofillSelectCredentialsDialogFragment @@ -101,4 +103,8 @@ class CredentialAutofillDialogAndroidFactory @Inject constructor() : CredentialA override fun emailProtectionInContextSignUpDialog(tabId: String): DialogFragment { return EmailProtectionInContextSignUpPromptFragment.instance(tabId) } + + override fun autofillImportPasswordsPromoDialog(importSource: AutofillImportLaunchSource, tabId: String, url: String): DialogFragment { + return ImportFromGooglePasswordsDialog.instance(importSource = importSource, tabId = tabId, originalUrl = url) + } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillPasswordsManagementViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillPasswordsManagementViewModel.kt index ba86ef3ed82b..389afb385e00 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillPasswordsManagementViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillPasswordsManagementViewModel.kt @@ -26,6 +26,8 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementPromo import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager @@ -33,8 +35,6 @@ import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.asString import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_DELETE_LOGIN import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsPixelSender.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsPixelSender.kt index e357ba65a159..f58d07b129a4 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsPixelSender.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsPixelSender.kt @@ -17,8 +17,8 @@ package com.duckduckgo.autofill.impl.ui.credential.management.importpassword import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.impl.engagement.store.AutofillEngagementBucketing -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason.ErrorParsingCsv import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_TAPPED diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialog.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialog.kt index 3faf1a445a31..1865e5b2040b 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialog.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialog.kt @@ -28,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.core.content.IntentCompat import androidx.core.os.BundleCompat +import androidx.fragment.app.setFragmentResult import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -35,17 +36,21 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementPromo +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.Unknown import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ContentImportFromGooglePasswordDialogBinding import com.duckduckgo.autofill.impl.deviceauth.AutofillAuthorizationGracePeriod -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementPromo -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.Unknown import com.duckduckgo.autofill.impl.importing.CredentialImporter import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePassword import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult import com.duckduckgo.autofill.impl.ui.credential.dialog.animateClosed import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog.ImportPasswordsDialog.Companion.KEY_IMPORT_SUCCESS +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog.ImportPasswordsDialog.Companion.KEY_TAB_ID +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog.ImportPasswordsDialog.Companion.KEY_URL +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.BrowserPromoPreImport import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.DeterminingFirstView import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.FlowTerminated import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.ImportError @@ -54,6 +59,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goog import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.PreImport import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewState import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.prependIconToText import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.utils.FragmentViewModelFactory import com.duckduckgo.common.utils.extensions.html @@ -102,6 +108,10 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { private val viewModel by bindViewModel() + private var successImport = false + + private fun getTabId(): String? = arguments?.getString(KEY_TAB_ID) + private val importGooglePasswordsFlowLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> if (activityResult.resultCode == Activity.RESULT_OK) { lifecycleScope.launch { @@ -144,11 +154,33 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { } private fun switchDialogShowPreImportView() { + with(binding.preflow) { + dialogTitle.text = getString(R.string.importPasswordsChooseMethodDialogTitle) + onboardingSubtitle.text = getString(R.string.importPasswordsChooseMethodDialogSubtitle) + declineButton.gone() + topIllustrationAnimated.gone() + appIcon.show() + } + showDialogContent() + binding.prePostViewSwitcher.displayedChild = 0 + } + + private fun switchDialogShowInBrowserPromoPreImportView() { + with(binding.preflow) { + dialogTitle.text = getString(R.string.passwords_import_promo_in_browser_title) + onboardingSubtitle.text = binding.root.context.prependIconToText(R.string.passwords_import_promo_subtitle, R.drawable.ic_lock_solid_12) + declineButton.show() + topIllustrationAnimated.setAnimation(R.raw.anim_password_keys) + topIllustrationAnimated.show() + topIllustrationAnimated.playAnimation() + appIcon.gone() + } showDialogContent() binding.prePostViewSwitcher.displayedChild = 0 } private fun processSuccessResult(result: CredentialImporter.ImportResult.Finished) { + successImport = true showDialogContent() binding.postflow.importFinished.errorNotImported.visibility = View.GONE @@ -214,7 +246,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { return } - // check if we should show the initial instructional prompt + // check if we should show the initial instructional prompt, and if so, which variant of it. if not, start the import flow directly val launchSource = getLaunchSource() if (canShowPreImportDialog(launchSource)) { viewModel.shouldShowInitialInstructionalPrompt(launchSource) @@ -238,6 +270,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { _binding = ContentImportFromGooglePasswordDialogBinding.inflate(inflater, container, false) configureViews(binding) observeViewModel() + logcat { "Creating ImportFromGooglePasswordsDialog with launch source: ${getLaunchSource()}" } return binding.root } @@ -250,6 +283,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { private fun renderViewState(viewState: ViewState) { when (viewState.viewMode) { is PreImport -> switchDialogShowPreImportView() + is BrowserPromoPreImport -> switchDialogShowInBrowserPromoPreImportView() is ImportError -> processErrorResult() is ImportSuccess -> processSuccessResult(viewState.viewMode.importResult) is Importing -> switchDialogShowImportInProgressView() @@ -258,6 +292,24 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + logcat { "Dismissing ImportFromGooglePasswordsDialog, successful import? $successImport" } + setResult(successImport) + } + + private fun setResult(successImport: Boolean) { + logcat { "Setting result for ImportFromGooglePasswordsDialog, successful import? $successImport" } + + getTabId()?.let { tabId -> + val result = Bundle().also { + it.putBoolean(KEY_IMPORT_SUCCESS, successImport) + it.putString(KEY_URL, getOriginalUrl()) + } + parentFragment?.setFragmentResult(ImportPasswordsDialog.resultKey(tabId), result) + } + } + override fun onDestroyView() { _binding = null authorizationGracePeriod.removeRequestForExtendedGracePeriod() @@ -277,6 +329,10 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { dismiss() } } + + binding.preflow.declineButton.setOnClickListener { + viewModel.onInBrowserPromoDismissed() + } } private fun onImportGcmButtonClicked() { @@ -298,32 +354,57 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { } override fun onCancel(dialog: DialogInterface) { + logcat { + "Cancelling ImportFromGooglePasswordsDialog, " + + "successful import? $successImport. " + + "ignore cancellation events: $ignoreCancellationEvents" + } + if (ignoreCancellationEvents) { logcat(VERBOSE) { "onCancel: Ignoring cancellation event" } return } importPasswordsPixelSender.onUserCancelledImportPasswordsDialog(getLaunchSource()) - - dismiss() } private fun configureCloseButton(binding: ContentImportFromGooglePasswordDialogBinding) { binding.closeButton.setOnClickListener { (dialog as BottomSheetDialog).animateClosed() } } + private fun getOriginalUrl() = arguments?.getString(KEY_URL)!! + private inline fun bindViewModel() = lazy { ViewModelProvider(this, viewModelFactory)[V::class.java] } companion object { private const val KEY_LAUNCH_SOURCE = "launchSource" - fun instance(importSource: AutofillImportLaunchSource): ImportFromGooglePasswordsDialog { + fun instance(importSource: AutofillImportLaunchSource, tabId: String? = null, originalUrl: String? = null): ImportFromGooglePasswordsDialog { val fragment = ImportFromGooglePasswordsDialog() fragment.arguments = Bundle().apply { putParcelable(KEY_LAUNCH_SOURCE, importSource) + putString(KEY_TAB_ID, tabId) + putString(KEY_URL, originalUrl) } return fragment } } + + interface ImportPasswordsDialog { + + companion object { + + fun resultKey(tabId: String) = "${prefix(tabId, TAG)}/Result" + + const val TAG = "ImportPasswordsDialog" + const val KEY_URL = "url" + const val KEY_IMPORT_SUCCESS = "importSuccess" + const val KEY_TAB_ID = "tabId" + + private fun prefix(tabId: String, tag: String): String { + return "$tabId/$tag" + } + } + } } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModel.kt index d106a2f32e54..fcb4ff643206 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModel.kt @@ -19,11 +19,14 @@ package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goo import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.InBrowserPromo import com.duckduckgo.autofill.impl.importing.CredentialImporter import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason +import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.BrowserPromoPreImport import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.DeterminingFirstView import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.Importing import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.PreImport @@ -40,6 +43,7 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor( private val credentialImporter: CredentialImporter, private val dispatchers: DispatcherProvider, private val importPasswordsPixelSender: ImportPasswordsPixelSender, + private val autofillStore: InternalAutofillStore, ) : ViewModel() { fun onImportFlowFinishedSuccessfully(importSource: AutofillImportLaunchSource) { @@ -91,8 +95,27 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor( } fun shouldShowInitialInstructionalPrompt(importSource: AutofillImportLaunchSource) { + val viewMode = if (importSource == AutofillImportLaunchSource.InBrowserPromo) { + logcat { "ImportFromGooglePasswordsDialogViewModel: InBrowserPromo scenario" } + BrowserPromoPreImport + } else { + logcat { "ImportFromGooglePasswordsDialogViewModel: PreImport scenario" } + PreImport + } + + viewModelScope.launch(dispatchers.io()) { + autofillStore.inBrowserImportPromoShownCount += 1 + } importPasswordsPixelSender.onImportPasswordsDialogDisplayed(importSource) - _viewState.value = viewState.value.copy(viewMode = PreImport) + _viewState.value = viewState.value.copy(viewMode = viewMode) + } + + fun onInBrowserPromoDismissed() { + viewModelScope.launch(dispatchers.io()) { + autofillStore.hasDeclinedInBrowserPasswordImportPromo = true + importPasswordsPixelSender.onUserCancelledImportPasswordsDialog(InBrowserPromo) + } + _viewState.value = viewState.value.copy(viewMode = ViewMode.FlowTerminated) } private val _viewState = MutableStateFlow(ViewState()) @@ -103,6 +126,7 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor( sealed interface ViewMode { data object DeterminingFirstView : ViewMode data object PreImport : ViewMode + data object BrowserPromoPreImport : ViewMode data object Importing : ViewMode data class ImportSuccess(val importResult: ImportResult.Finished) : ViewMode data object ImportError : ViewMode 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 6c2db8ea9f1a..b84c34624053 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 @@ -29,6 +29,9 @@ import androidx.lifecycle.repeatOnLifecycle import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementEmptyState +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementOverflow import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin @@ -37,9 +40,6 @@ import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementListMo import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthResult.Success -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementEmptyState -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementOverflow import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementActivity import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapter.AutofillToggleState diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListModeLegacy.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListModeLegacy.kt index fb4745c2f3a0..522ccd9e3085 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListModeLegacy.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/AutofillManagementListModeLegacy.kt @@ -38,6 +38,9 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementEmptyState +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementOverflow import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.promotion.PasswordsScreenPromotionPlugin @@ -46,9 +49,6 @@ import com.duckduckgo.autofill.impl.databinding.FragmentAutofillManagementListMo import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthConfiguration import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator.AuthResult.Success -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementEmptyState -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementOverflow import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementActivity import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapterLegacy import com.duckduckgo.autofill.impl.ui.credential.management.AutofillManagementRecyclerAdapterLegacy.ContextMenuAction.CopyPassword 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 4392028ac645..f27c1d0d748a 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,6 +18,7 @@ package com.duckduckgo.autofill.impl.ui.credential.passwordgeneration import android.content.Context import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.autofill.api.AutofillEventListener @@ -57,12 +58,13 @@ class ResultHandlerUseGeneratedPassword @Inject constructor( private val usernameBackFiller: UsernameBackFiller, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSaving.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSaving.kt index e6fa30828d47..607a428f9b01 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSaving.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSaving.kt @@ -19,6 +19,7 @@ package com.duckduckgo.autofill.impl.ui.credential.saving import android.content.Context import android.os.Bundle import android.view.View +import android.webkit.WebView import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog.Builder import androidx.fragment.app.Fragment @@ -60,12 +61,13 @@ class ResultHandlerPromptToDisableCredentialSaving @Inject constructor( private val dispatchers: DispatcherProvider, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } 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 2209a174812c..6d626e33abd8 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 @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.appbuildconfig.api.AppBuildConfig @@ -49,12 +50,13 @@ class ResultHandlerSaveLoginCredentials @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } 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 6ba15979bd5c..adf3fb436305 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 @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel @@ -55,12 +56,13 @@ class ResultHandlerCredentialSelection @Inject constructor( private val autofilledListeners: PluginPoint, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } 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 b4815633275c..2f214b24211c 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 @@ -20,6 +20,7 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.os.Parcelable +import android.webkit.WebView import androidx.fragment.app.Fragment import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.appbuildconfig.api.AppBuildConfig @@ -50,12 +51,13 @@ class ResultHandlerUpdateLoginCredentials @Inject constructor( @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : AutofillFragmentResultsPlugin { - override fun processResult( + override suspend fun processResult( result: Bundle, context: Context, tabId: String, fragment: Fragment, autofillCallback: AutofillEventListener, + webView: WebView?, ) { logcat { "${this::class.java.simpleName}: processing result" } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsActivity.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsActivity.kt index b83a959ae384..1b8edaa19059 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsActivity.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsActivity.kt @@ -27,13 +27,13 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.AutofillScreenLaunchSource.AutofillSettings import com.duckduckgo.autofill.api.AutofillScreens.AutofillPasswordsManagementScreen import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ActivityAutofillSettingsBinding -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordActivityParams import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialog import com.duckduckgo.autofill.impl.ui.credential.management.viewing.AutofillManagementDeviceUnsupportedMode diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModel.kt index e98d920b2191..aeeafb954ab0 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModel.kt @@ -24,10 +24,10 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.WebMessageListener import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.impl.asString import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/store/AutofillPrefsStore.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/store/AutofillPrefsStore.kt index def71666b4b0..fff36c58dcb2 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/store/AutofillPrefsStore.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/store/AutofillPrefsStore.kt @@ -35,10 +35,12 @@ interface AutofillPrefsStore { var hasEverBeenPromptedToSaveLogin: Boolean var hasEverImportedPasswords: Boolean fun hasEverImportedPasswordsFlow(): Flow - var hasDismissedImportedPasswordsPromo: Boolean + var hasDeclinedPasswordManagementImportPromo: Boolean + var hasDeclinedInBrowserPasswordImportPromo: Boolean val autofillStateSetByUser: Boolean var timestampUserLastPromptedToDisableAutofill: Long? var domainTargetDatasetVersion: Long + var inBrowserImportPromoShownCount: Int /** * Returns if Autofill was enabled by default. @@ -105,11 +107,18 @@ class RealAutofillPrefsStore( .distinctUntilChanged() } - override var hasDismissedImportedPasswordsPromo: Boolean - get() = prefs.getBoolean(HAS_DISMISSED_IMPORT_PASSWORDS_PROMO, false) + override var hasDeclinedPasswordManagementImportPromo: Boolean + get() = prefs.getBoolean(HAS_DECLINED_PASSWORD_MANAGEMENT_IMPORT_PASSWORDS_PROMO, false) set(value) { - prefs.edit { putBoolean(HAS_DISMISSED_IMPORT_PASSWORDS_PROMO, value) } + prefs.edit { putBoolean(HAS_DECLINED_PASSWORD_MANAGEMENT_IMPORT_PASSWORDS_PROMO, value) } } + + override var hasDeclinedInBrowserPasswordImportPromo: Boolean + get() = prefs.getBoolean(HAS_DECLINED_IN_BROWSER_IMPORT_PASSWORDS_PROMO, false) + set(value) { + prefs.edit { putBoolean(HAS_DECLINED_IN_BROWSER_IMPORT_PASSWORDS_PROMO, value) } + } + override val autofillStateSetByUser: Boolean get() = autofillStateSetByUser() @@ -130,6 +139,13 @@ class RealAutofillPrefsStore( putLong(DOMAIN_TARGET_DATASET_VERSION, value) } } + override var inBrowserImportPromoShownCount: Int + get() = prefs.getInt(BROWSER_IMPORT_PROMO_SHOWN_COUNT, 0) + set(value) { + prefs.edit { + putInt(BROWSER_IMPORT_PROMO_SHOWN_COUNT, value) + } + } /** * Returns if Autofill was enabled by default. Note, this is not necessarily the same as the current state of Autofill. @@ -180,7 +196,9 @@ class RealAutofillPrefsStore( const val AUTOFILL_ENABLED = "autofill_enabled" const val HAS_EVER_BEEN_PROMPTED_TO_SAVE_LOGIN = "autofill_has_ever_been_prompted_to_save_login" const val HAS_EVER_IMPORT_PASSWORDS = "autofill_has_ever_import_passwords" - const val HAS_DISMISSED_IMPORT_PASSWORDS_PROMO = "autofill_dismissed_import_passwords_promo" + const val HAS_DECLINED_IN_BROWSER_IMPORT_PASSWORDS_PROMO = "autofill_declined_in_browser_import_passwords_promo" + const val HAS_DECLINED_PASSWORD_MANAGEMENT_IMPORT_PASSWORDS_PROMO = "autofill_dismissed_import_passwords_promo" + const val BROWSER_IMPORT_PROMO_SHOWN_COUNT = "autofill_in_browser_import_promo_shown_count" const val TIMESTAMP_WHEN_USER_LAST_PROMPTED_TO_DISABLE_AUTOFILL = "timestamp_when_user_last_prompted_to_disable_autofill" const val AUTOFILL_DECLINE_COUNT = "autofill_decline_count" const val MONITOR_AUTOFILL_DECLINES = "monitor_autofill_declines" diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml index 8c4463eea6b2..eaf92fd47463 100644 --- a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml +++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml @@ -41,6 +41,20 @@ app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/autofill_gpm_export_instruction" /> + + + + \ 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 def6aa352279..8769ff257403 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -15,5 +15,6 @@ --> - + Bring your passwords from Google to DuckDuckGo + Set Up Later in Settings \ 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 86d32817c1aa..83041b3c62c3 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 @@ -248,7 +248,7 @@ Import Your Google Passwords Google may ask you to sign in or enter your password to confirm. - Open Google Passwords + Import From Google Choose a CSV file Import from Desktop Browser 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 index 389ecb80b2ad..bbb7712d3bc0 100644 --- 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 @@ -29,9 +29,11 @@ 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.configuration.AutofillAvailableInputTypesProvider 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.importing.InBrowserImportPromo import com.duckduckgo.autofill.impl.jsbridge.AutofillMessagePoster import com.duckduckgo.autofill.impl.jsbridge.request.AutofillDataRequest import com.duckduckgo.autofill.impl.jsbridge.request.AutofillRequestParser @@ -105,6 +107,8 @@ class AutofillStoredBackJavascriptInterfaceTest { private val partialCredentialSaveStore: PartialCredentialSaveStore = mock() private val usernameBackFiller: UsernameBackFiller = mock() private val existingCredentialMatchDetector: ExistingCredentialMatchDetector = mock() + private val inBrowserImportPromo: InBrowserImportPromo = mock() + private val autofillAvailableInputTypesProvider: AutofillAvailableInputTypesProvider = mock() private lateinit var testee: AutofillStoredBackJavascriptInterface private val testCallback = TestCallback() @@ -136,6 +140,7 @@ class AutofillStoredBackJavascriptInterfaceTest { partialCredentialSaveStore = partialCredentialSaveStore, usernameBackFiller = usernameBackFiller, existingCredentialMatchDetector = existingCredentialMatchDetector, + inBrowserImportPromo = inBrowserImportPromo, ) testee.callback = testCallback testee.webView = testWebView @@ -150,6 +155,8 @@ class AutofillStoredBackJavascriptInterfaceTest { ) whenever(autofillResponseWriter.generateEmptyResponseGetAutofillData()).thenReturn("") whenever(autofillResponseWriter.generateResponseGetAutofillData(any())).thenReturn("") + whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(false) + whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) } @Test @@ -480,6 +487,29 @@ class AutofillStoredBackJavascriptInterfaceTest { assertFalse(testCallback.onCredentialsSavedCalled) } + @Test + fun whenUserShouldBePromptedToImportPasswordsCallbackIsInvoked() = runTest { + whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(true) + initiateGetAutofillDataRequest() + assertImportPasswordPrompted() + } + + @Test + fun whenUserShouldNotBePromptedToImportPasswordsBecauseImportRulesExcludeItThenCallbackIsNotInvoked() = runTest { + whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(false) + initiateGetAutofillDataRequest() + assertImportPasswordNotPrompted() + } + + @Test + fun whenUserShouldBeNotPromptedToImportPasswordsBecauseThereArePageCredentialsCallbackIsNotInvoked() = runTest { + whenever(inBrowserImportPromo.canShowPromo(any(), anyOrNull())).thenReturn(true) + whenever(autofillStore.getCredentials(any())).thenReturn(listOf(LoginCredentials(0, "example.com", "username", "password"))) + initiateGetAutofillDataRequest() + assertImportPasswordNotPrompted() + assertCredentialsAvailable() + } + private suspend fun configureActionsToBeProcessed(actions: List) { whenever( passwordEventResolver.decideActions( @@ -552,6 +582,14 @@ class AutofillStoredBackJavascriptInterfaceTest { assertTrue(testCallback.credentialsAvailableToInject!!) } + private fun assertImportPasswordPrompted() { + assertTrue(testCallback.onPromptedToImportPassword) + } + + private fun assertImportPasswordNotPrompted() { + assertFalse(testCallback.onPromptedToImportPassword) + } + private fun initiateGetAutofillDataRequest() { testee.getAutofillData("") } @@ -574,6 +612,8 @@ class AutofillStoredBackJavascriptInterfaceTest { var onCredentialsSavedCalled: Boolean = false + var onPromptedToImportPassword: Boolean = false + override suspend fun onCredentialsAvailableToInject( originalUrl: String, credentials: List, @@ -605,6 +645,10 @@ class AutofillStoredBackJavascriptInterfaceTest { override fun onCredentialsSaved(savedCredentials: LoginCredentials) { onCredentialsSavedCalled = true } + + override suspend fun promptUserToImportPassword(originalUrl: String) { + onPromptedToImportPassword = true + } } private companion object { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/InlineBrowserAutofillTest.kt index 0f28994e95a1..683105fbac3e 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 @@ -71,6 +71,9 @@ class InlineBrowserAutofillTest { override fun onCredentialsSaved(savedCredentials: LoginCredentials) { } + + override suspend fun promptUserToImportPassword(originalUrl: String) { + } } @Before diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt index 1e9dbb51888a..f5a3872edbad 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/SecureStoreBackedAutofillStoreTest.kt @@ -719,15 +719,15 @@ class SecureStoreBackedAutofillStoreTest { @Test fun whenUserHasNeverDismissedImportPasswordPromoThenCallsThroughToStoreCorrectly() = runTest { setupTesteeWithAutofillAvailable() - testee.hasDismissedImportedPasswordsPromo - verify(autofillPrefsStore).hasDismissedImportedPasswordsPromo + testee.hasDeclinedPasswordManagementImportPromo + verify(autofillPrefsStore).hasDeclinedPasswordManagementImportPromo } @Test fun whenUserHasDismissedImportPasswordPromoThenCallsThroughToStoreCorrectly() = runTest { setupTesteeWithAutofillAvailable() - testee.hasDismissedImportedPasswordsPromo = true - verify(autofillPrefsStore).hasDismissedImportedPasswordsPromo = true + testee.hasDeclinedPasswordManagementImportPromo = true + verify(autofillPrefsStore).hasDeclinedPasswordManagementImportPromo = true } private fun enableDeepDomainCheckFeatureFlag() { diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillAvailableInputTypesProviderTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillAvailableInputTypesProviderTest.kt new file mode 100644 index 000000000000..ce4c419c0de1 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillAvailableInputTypesProviderTest.kt @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2025 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.autofill.api.AutofillCapabilityChecker +import com.duckduckgo.autofill.api.domain.app.LoginCredentials +import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.autofill.impl.importing.InBrowserImportPromo +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.assertFalse +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.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class RealAutofillAvailableInputTypesProviderTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private lateinit var testee: RealAutofillAvailableInputTypesProvider + + private val emailManager: EmailManager = mock() + private val autofillStore: InternalAutofillStore = mock() + private val shareableCredentials: ShareableCredentials = mock() + private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() + private val inBrowserPromo: InBrowserImportPromo = mock() + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + testee = RealAutofillAvailableInputTypesProvider( + emailManager = emailManager, + autofillStore = autofillStore, + shareableCredentials = shareableCredentials, + autofillCapabilityChecker = autofillCapabilityChecker, + inBrowserPromo = inBrowserPromo, + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + // Default setup + runTest { + whenever(autofillStore.getCredentials(any())).thenReturn(emptyList()) + whenever(shareableCredentials.shareableCredentials(any())).thenReturn(emptyList()) + whenever(emailManager.isSignedIn()).thenReturn(false) + whenever(inBrowserPromo.canShowPromo(any(), anyOrNull())).thenReturn(false) + whenever(autofillCapabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(true) + } + } + + @Test + fun whenNoCredentialsForUrlThenUsernameAndPasswordAreFalse() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn(emptyList()) + whenever(shareableCredentials.shareableCredentials(EXAMPLE_URL)).thenReturn(emptyList()) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.username) + assertFalse(result.password) + } + + @Test + fun whenWithCredentialsForUrlThenUsernameAndPasswordAreTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertTrue(result.password) + } + + @Test + fun whenWithShareableCredentialsForUrlThenUsernameAndPasswordAreTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn(emptyList()) + whenever(shareableCredentials.shareableCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertTrue(result.password) + } + + @Test + fun whenWithUsernameOnlyForUrlThenOnlyUsernameIsTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = null, + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertFalse(result.password) + } + + @Test + fun whenWithEmptyUsernameForUrlThenUsernameIsFalse() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "", + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.username) + assertTrue(result.password) + } + + @Test + fun whenWithPasswordOnlyForUrlThenOnlyPasswordIsTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = null, + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.username) + assertTrue(result.password) + } + + @Test + fun whenWithEmptyPasswordForUrlThenPasswordIsFalse() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = "", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertFalse(result.password) + } + + @Test + fun whenWithCredentialsButAutofillDisabledThenUsernameAndPasswordAreFalse() = runTest { + configureAutofillCapabilities(enabled = false) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.username) + assertFalse(result.password) + } + + @Test + fun whenUrlIsNullThenUsernameAndPasswordAreFalse() = runTest { + // Even if we have credentials, null URL should return false + whenever(autofillStore.getCredentials(any())).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = "example.com", + username = "username", + password = "password", + ), + ), + ) + + val result = testee.getTypes(null) + + assertFalse(result.username) + assertFalse(result.password) + } + + @Test + fun whenEmailIsSignedInThenEmailIsTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(emailManager.isSignedIn()).thenReturn(true) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.email) + } + + @Test + fun whenEmailIsSignedOutThenEmailIsFalse() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(emailManager.isSignedIn()).thenReturn(false) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.email) + } + + @Test + fun whenImportPromoCanShowThenCredentialsImportIsTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(inBrowserPromo.canShowPromo(any(), anyOrNull())).thenReturn(true) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.credentialsImport) + } + + @Test + fun whenImportPromoCannotShowThenCredentialsImportIsFalse() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(inBrowserPromo.canShowPromo(any(), anyOrNull())).thenReturn(false) + + val result = testee.getTypes(EXAMPLE_URL) + + assertFalse(result.credentialsImport) + } + + @Test + fun whenMultipleCredentialsWithDifferentFieldsThenCombinesCorrectly() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = null, + ), + LoginCredentials( + id = 2, + domain = EXAMPLE_URL, + username = null, + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertTrue(result.password) + } + + @Test + fun whenCombiningDirectAndShareableCredentialsThenBothAreConsidered() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = null, + ), + ), + ) + whenever(shareableCredentials.shareableCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 2, + domain = EXAMPLE_URL, + username = null, + password = "password", + ), + ), + ) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertTrue(result.password) + } + + @Test + fun whenAllFeaturesEnabledThenAllTypesAreTrue() = runTest { + configureAutofillCapabilities(enabled = true) + whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( + listOf( + LoginCredentials( + id = 1, + domain = EXAMPLE_URL, + username = "username", + password = "password", + ), + ), + ) + whenever(emailManager.isSignedIn()).thenReturn(true) + whenever(inBrowserPromo.canShowPromo(any(), anyOrNull())).thenReturn(true) + + val result = testee.getTypes(EXAMPLE_URL) + + assertTrue(result.username) + assertTrue(result.password) + assertTrue(result.email) + assertTrue(result.credentialsImport) + } + + private suspend fun configureAutofillCapabilities(enabled: Boolean) { + whenever(autofillCapabilityChecker.canInjectCredentialsToWebView(any())).thenReturn(enabled) + } + + companion object { + private const val EXAMPLE_URL = "example.com" + } +} 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 27197b1b4346..89ce0ad63e59 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 @@ -20,12 +20,8 @@ import android.annotation.SuppressLint 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.configuration.AutofillAvailableInputTypesProvider.AvailableInputTypes import com.duckduckgo.autofill.impl.email.incontext.availability.EmailProtectionInContextAvailabilityRules -import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials -import com.duckduckgo.autofill.impl.sharedcreds.ShareableCredentials -import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle.State @@ -46,34 +42,37 @@ class RealAutofillRuntimeConfigProviderTest { private lateinit var testee: RealAutofillRuntimeConfigProvider - private val emailManager: EmailManager = mock() - private val autofillStore: InternalAutofillStore = mock() private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) private val runtimeConfigurationWriter: RuntimeConfigurationWriter = mock() - private val shareableCredentials: ShareableCredentials = mock() private val autofillCapabilityChecker: AutofillCapabilityChecker = mock() private val siteSpecificFixesStore: AutofillSiteSpecificFixesStore = mock() private val emailProtectionInContextAvailabilityRules: EmailProtectionInContextAvailabilityRules = mock() private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() + private val availableInputTypesProvider: AutofillAvailableInputTypesProvider = mock() @Before fun setUp() { MockitoAnnotations.openMocks(this) testee = RealAutofillRuntimeConfigProvider( - emailManager, - autofillStore, - runtimeConfigurationWriter, + runtimeConfigurationWriter = runtimeConfigurationWriter, autofillCapabilityChecker = autofillCapabilityChecker, autofillFeature = autofillFeature, - shareableCredentials = shareableCredentials, emailProtectionInContextAvailabilityRules = emailProtectionInContextAvailabilityRules, neverSavedSiteRepository = neverSavedSiteRepository, siteSpecificFixesStore = siteSpecificFixesStore, + autofillAvailableInputTypesProvider = availableInputTypesProvider, ) runTest { - whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn(emptyList()) whenever(neverSavedSiteRepository.isInNeverSaveList(any())).thenReturn(false) + whenever(availableInputTypesProvider.getTypes(any())).thenReturn( + AvailableInputTypes( + username = false, + password = false, + email = false, + credentialsImport = false, + ), + ) } autofillFeature.canCategorizeUnknownUsername().setRawStoredState(State(enable = true)) @@ -85,7 +84,7 @@ class RealAutofillRuntimeConfigProviderTest { ), ), ).thenReturn("") - whenever(runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(any(), any())).thenReturn("") + whenever(runtimeConfigurationWriter.generateResponseGetAvailableInputTypes(any())).thenReturn("") whenever(runtimeConfigurationWriter.generateUserUnprotectedDomains()).thenReturn("") whenever( runtimeConfigurationWriter.generateUserPreferences( @@ -111,7 +110,6 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenAutofillEnabledThenConfigurationUserPrefsCredentialsIsTrue() = runTest { configureAutofillCapabilities(enabled = true) - configureNoShareableLogins() testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyAutofillCredentialsReturnedAs(true) } @@ -119,8 +117,14 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenCanAutofillThenConfigSpecifiesShowingKeyIcon() = runTest { configureAutofillCapabilities(enabled = true) - configureAutofillAvailableForSite(EXAMPLE_URL) - configureNoShareableLogins() + whenever(availableInputTypesProvider.getTypes(EXAMPLE_URL)).thenReturn( + AvailableInputTypes( + username = true, + password = true, + email = false, + credentialsImport = false, + ), + ) testee.getRuntimeConfiguration("", EXAMPLE_URL) verifyKeyIconRequestedToShow() } @@ -142,269 +146,36 @@ class RealAutofillRuntimeConfigProviderTest { } @Test - fun whenNoCredentialsForUrlThenConfigurationInputTypeCredentialsIsFalse() = runTest { - configureAutofillEnabledWithNoSavedCredentials(EXAMPLE_URL) - testee.getRuntimeConfiguration("", EXAMPLE_URL) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithCredentialsForUrlThenConfigurationInputTypeCredentialsIsTrue() = runTest { + fun whenAvailableInputTypesProviderCalledThenResultPassedToWriter() = runTest { configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = EXAMPLE_URL, - username = "username", - password = "password", - ), - ), + val expectedInputTypes = AvailableInputTypes( + username = true, + password = true, + email = true, + credentialsImport = true, ) - configureNoShareableLogins() + whenever(availableInputTypesProvider.getTypes(EXAMPLE_URL)).thenReturn(expectedInputTypes) testee.getRuntimeConfiguration("", EXAMPLE_URL) - val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) + verify(availableInputTypesProvider).getTypes(EXAMPLE_URL) + verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes(eq(expectedInputTypes)) } @Test - fun whenWithShareableCredentialsForUrlThenConfigurationInputTypeCredentialsIsTrue() = runTest { + fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn(emptyList()) - whenever(shareableCredentials.shareableCredentials(any())).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = EXAMPLE_URL, - username = "username", - password = "password", - ), - ), - ) - testee.getRuntimeConfiguration("", EXAMPLE_URL) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = true) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithUsernameOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = "username", - password = null, - ), - ), - ) - configureNoShareableLogins() - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = true, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithEmptyUsernameOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsFalse() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = "", - password = null, - ), - ), - ) - configureNoShareableLogins() - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithPasswordOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = null, - password = "password", - ), - ), - ) - configureNoShareableLogins() - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = true) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithEmptyPasswordOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = null, - password = "", - ), - ), - ) - configureNoShareableLogins() - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithCredentialsForUrlButAutofillDisabledThenConfigurationInputTypeCredentialsIsFalse() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = false) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = "username", - password = "password", - ), - ), - ) - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenWithCredentialsForUrlButAutofillUnavailableThenConfigurationInputTypeCredentialsIsFalse() = runTest { - val url = "example.com" - configureAutofillCapabilities(enabled = false) - whenever(autofillStore.getCredentials(url)).thenReturn( - listOf( - LoginCredentials( - id = 1, - domain = url, - username = "username", - password = "password", - ), - ), - ) - - testee.getRuntimeConfiguration("", url) - - val expectedCredentialResponse = AvailableInputTypeCredentials(username = false, password = false) - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = eq(expectedCredentialResponse), - emailAvailable = any(), - ) - } - - @Test - fun whenEmailIsSignedInThenConfigurationInputTypeEmailIsTrue() = runTest { - val url = "example.com" - configureAutofillEnabledWithNoSavedCredentials(url) - whenever(emailManager.isSignedIn()).thenReturn(true) - - testee.getRuntimeConfiguration("", url) - - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = any(), - emailAvailable = eq(true), - ) - } - - @Test - fun whenEmailIsSignedOutThenConfigurationInputTypeEmailIsFalse() = runTest { - val url = "example.com" - configureAutofillEnabledWithNoSavedCredentials(url) - whenever(emailManager.isSignedIn()).thenReturn(false) - - testee.getRuntimeConfiguration("", url) - - verify(runtimeConfigurationWriter).generateResponseGetAvailableInputTypes( - credentialsAvailable = any(), - emailAvailable = eq(false), - ) - } - - @Test - fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { - val url = "example.com" - configureAutofillEnabledWithNoSavedCredentials(url) - testee.getRuntimeConfiguration("", url) verifyCanSaveCredentialsReturnedAs(true) } @Test fun whenSiteInNeverSaveListThenStillTellJsWeCanSaveCredentials() = runTest { - val url = "example.com" - configureAutofillEnabledWithNoSavedCredentials(url) - whenever(neverSavedSiteRepository.isInNeverSaveList(url)).thenReturn(true) - - testee.getRuntimeConfiguration("", url) - verifyCanSaveCredentialsReturnedAs(true) - } - - private suspend fun RealAutofillRuntimeConfigProviderTest.configureAutofillEnabledWithNoSavedCredentials(url: String) { configureAutofillCapabilities(enabled = true) - whenever(autofillStore.getCredentials(url)).thenReturn(emptyList()) - configureNoShareableLogins() - } + whenever(neverSavedSiteRepository.isInNeverSaveList(EXAMPLE_URL)).thenReturn(true) - private suspend fun configureAutofillAvailableForSite(url: String) { - whenever(autofillStore.getCredentials(url)).thenReturn(emptyList()) - whenever(autofillStore.autofillEnabled).thenReturn(true) - whenever(autofillStore.autofillAvailable()).thenReturn(true) + testee.getRuntimeConfiguration("", EXAMPLE_URL) + verifyCanSaveCredentialsReturnedAs(true) } private suspend fun configureAutofillCapabilities(enabled: Boolean) { @@ -454,10 +225,6 @@ class RealAutofillRuntimeConfigProviderTest { ) } - private suspend fun configureNoShareableLogins() { - whenever(shareableCredentials.shareableCredentials(any())).thenReturn(emptyList()) - } - private fun verifyKeyIconRequestedToShow() { verify(runtimeConfigurationWriter).generateUserPreferences( autofillCredentials = any(), 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 1be9e0bd9f22..df12e37c8b45 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 @@ -16,8 +16,8 @@ package com.duckduckgo.autofill.impl.configuration +import com.duckduckgo.autofill.impl.configuration.AutofillAvailableInputTypesProvider.AvailableInputTypes import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputSuccessResponse -import com.duckduckgo.autofill.impl.jsbridge.response.AvailableInputTypeCredentials import com.squareup.moshi.Json import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -40,14 +40,14 @@ class RealRuntimeConfigurationWriterTest { "password": false, "username": false }, - "email": true + "email": true, + "credentialsImport": true } """, ) val actualInputTypes = testee.generateResponseGetAvailableInputTypes( - credentialsAvailable = AvailableInputTypeCredentials(username = false, password = false), - emailAvailable = true, + availableInputTypes = AvailableInputTypes(username = false, password = false, email = true, credentialsImport = true), ) assertAvailableInputTypesJsonCorrect(expectedAvailableInputTypes, actualInputTypes) 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 9f6ce7f64139..a35db9417aa0 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 @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.email import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -57,6 +58,7 @@ class ResultHandlerEmailProtectionChooseEmailTest { private val emailManager: EmailManager = mock() private val pixel: Pixel = mock() private val partialCredentialSaveStore: PartialCredentialSaveStore = mock() + private val webView: WebView = mock() private val testee = ResultHandlerEmailProtectionChooseEmail( appBuildConfig = appBuildConfig, @@ -79,70 +81,70 @@ class ResultHandlerEmailProtectionChooseEmailTest { @Test fun whenUserSelectedToUsePersonalAddressThenCorrectCallbackInvoked() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onUseEmailProtectionPersonalAddress(any(), any()) } @Test fun whenUserSelectedToUsePersonalAddressThenPartialUsernameSaveMade() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(partialCredentialSaveStore).saveUsername(url = any(), username = eq("personal-example@duck.com")) } @Test fun whenUserSelectedToUsePrivateAliasAddressThenCorrectCallbackInvoked() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onUseEmailProtectionPrivateAlias(any(), any()) } @Test fun whenUserSelectedToUsePrivateAliasAddressThenPartialUsernameSaveMade() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(partialCredentialSaveStore).saveUsername(url = any(), username = eq("private-example@duck.com")) } @Test fun whenUrlMissingFromBundleThenExceptionThrown() = runTest { val bundle = bundle(url = null, result = UsePersonalEmailAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyNoInteractions(callback) } @Test fun whenResultTypeMissingFromBundleThenExceptionThrown() = runTest { val bundle = bundle(result = null) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyNoInteractions(callback) } @Test fun whenUserSelectedToUsePrivateAliasAddressThenSetNewLastUsedDateCalled() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(emailManager).setNewLastUsedDate() } @Test fun whenUserSelectedToUsePersonalDuckAddressThenSetNewLastUsedDateCalled() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(emailManager).setNewLastUsedDate() } @Test fun whenUserSelectedNotToUseEmailProtectionThenPixelSent() = runTest { val bundle = bundle(result = DoNotUseEmailProtection) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(pixel).enqueueFire(EMAIL_TOOLTIP_DISMISSED, mapOf(COHORT to "cohort")) } @Test fun whenUserSelectedToUsePersonalDuckAddressThenPixelSent() = runTest { val bundle = bundle(result = UsePersonalEmailAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(pixel).enqueueFire( EMAIL_USE_ADDRESS, mapOf(COHORT to "cohort", LAST_USED_DAY to "2021-01-01"), @@ -152,7 +154,7 @@ class ResultHandlerEmailProtectionChooseEmailTest { @Test fun whenUserSelectedToUsePrivateAliasThenPixelSent() = runTest { val bundle = bundle(result = UsePrivateAliasAddress) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(pixel).enqueueFire( EMAIL_USE_ALIAS, mapOf(COHORT to "cohort", LAST_USED_DAY to "2021-01-01"), diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/RealInBrowserImportPromoTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/RealInBrowserImportPromoTest.kt new file mode 100644 index 000000000000..950350c1b119 --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/RealInBrowserImportPromoTest.kt @@ -0,0 +1,308 @@ +package com.duckduckgo.autofill.impl.importing + +import android.annotation.SuppressLint +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.DocumentStartJavaScript +import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.WebMessageListener +import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.impl.importing.RealInBrowserImportPromo.Companion.MAX_PROMO_SHOWN_COUNT +import com.duckduckgo.autofill.impl.store.InternalAutofillStore +import com.duckduckgo.autofill.impl.store.NeverSavedSiteRepository +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle.State +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Mockito.mock +import org.mockito.kotlin.whenever + +@SuppressLint("DenyListedApi") +@RunWith(Parameterized::class) +class RealInBrowserImportPromoParameterizedTest( + private val testCase: CanShowPromoTestCase, +) { + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) + + private val autofillStore: InternalAutofillStore = mock() + private val neverSavedSiteRepository: NeverSavedSiteRepository = mock() + private val webViewCapabilityChecker: WebViewCapabilityChecker = mock() + + private val testee = RealInBrowserImportPromo( + autofillStore = autofillStore, + dispatchers = coroutineTestRule.testDispatcherProvider, + neverSavedSiteRepository = neverSavedSiteRepository, + autofillFeature = autofillFeature, + webViewCapabilityChecker = webViewCapabilityChecker, + ) + + @Before + fun setup() = runTest { + whenever(autofillStore.hasEverImportedPasswords).thenReturn(testCase.hasEverImportedPasswords) + whenever(autofillStore.hasDeclinedInBrowserPasswordImportPromo).thenReturn(testCase.hasDeclinedPromo) + whenever(autofillStore.getCredentialCount()).thenReturn(flowOf(testCase.credentialCount)) + whenever(autofillStore.inBrowserImportPromoShownCount).thenReturn(testCase.promoShownCount) + whenever(neverSavedSiteRepository.isInNeverSaveList(EXAMPLE_URL)).thenReturn(false) + whenever(neverSavedSiteRepository.isInNeverSaveList(NEVER_SAVE_URL)).thenReturn(true) + autofillFeature.canPromoteImportGooglePasswordsInBrowser().setRawStoredState(State(enable = testCase.inBrowserPromoFeatureEnabled)) + autofillFeature.self().setRawStoredState(State(enable = testCase.autofillFeatureEnabled)) + whenever(webViewCapabilityChecker.isSupported(WebMessageListener)).thenReturn(testCase.webViewWebMessageSupport) + whenever(webViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(testCase.webViewDocumentStartJavascript) + } + + @Test + fun inBrowserPromoRules() = runTest { + val result = testee.canShowPromo(testCase.credentialsAvailableForCurrentPage, testCase.url) + assertEquals(testCase.description, testCase.expected, result) + } + + companion object { + + @JvmStatic + @Parameterized.Parameters(name = "{index}: {0}") + fun data(): List { + return listOf( + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = true, + description = "eligible: all conditions met", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = true, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: credentials available for current page", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = false, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: can import password feature disabled", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = false, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: autofill feature disabled", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = true, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: has ever imported passwords", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = true, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: user has declined promo", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = RealInBrowserImportPromo.MAX_CREDENTIALS_FOR_PROMO - 1, + promoShownCount = 0, + expected = true, + description = "eligible: credentialCount just below max", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = RealInBrowserImportPromo.MAX_CREDENTIALS_FOR_PROMO, + promoShownCount = 0, + expected = false, + description = "ineligible: credential count at max limit", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = RealInBrowserImportPromo.MAX_CREDENTIALS_FOR_PROMO + 1, + promoShownCount = 0, + expected = false, + description = "ineligible: credentialCount just above max", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = MAX_PROMO_SHOWN_COUNT - 1, + expected = true, + description = "eligible: promoShownCount just below max", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = MAX_PROMO_SHOWN_COUNT, + expected = false, + description = "ineligible: promo shown count at max limit", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = MAX_PROMO_SHOWN_COUNT + 1, + expected = false, + description = "ineligible: promoShownCount just above max", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = NEVER_SAVE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = false, + description = "ineligible: url in never save list", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = null, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + expected = true, + description = "eligible: url is null, all other conditions met", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = true, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = false, + autofillFeatureEnabled = true, + hasEverImportedPasswords = true, + hasDeclinedPromo = true, + credentialCount = RealInBrowserImportPromo.MAX_CREDENTIALS_FOR_PROMO, + promoShownCount = MAX_PROMO_SHOWN_COUNT, + expected = false, + description = "ineligible: multiple disqualifying conditions", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + webViewWebMessageSupport = false, + webViewDocumentStartJavascript = true, + expected = false, + description = "ineligible: webview does not support WebMessageListener", + ), + CanShowPromoTestCase( + credentialsAvailableForCurrentPage = false, + url = EXAMPLE_URL, + inBrowserPromoFeatureEnabled = true, + autofillFeatureEnabled = true, + hasEverImportedPasswords = false, + hasDeclinedPromo = false, + credentialCount = 0, + promoShownCount = 0, + webViewWebMessageSupport = true, + webViewDocumentStartJavascript = false, + expected = false, + description = "ineligible: webview does not support DocumentStartJavaScript", + ), + ) + } + + private const val EXAMPLE_URL = "https://example.com" + private const val NEVER_SAVE_URL = "https://neversave.example.com" + } + + override fun toString(): String { + return testCase.description + } +} + +data class CanShowPromoTestCase( + val credentialsAvailableForCurrentPage: Boolean, + val url: String?, + val inBrowserPromoFeatureEnabled: Boolean, + val autofillFeatureEnabled: Boolean, + val hasEverImportedPasswords: Boolean, + val hasDeclinedPromo: Boolean, + val credentialCount: Int, + val promoShownCount: Int, + val webViewWebMessageSupport: Boolean = true, + val webViewDocumentStartJavascript: Boolean = true, + val expected: Boolean, + val description: String, +) { + override fun toString() = description +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/promo/RealImportInPasswordsVisibilityTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/promo/RealImportInPasswordsVisibilityTest.kt index dbb2360c7ef4..37e513e76fe3 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/promo/RealImportInPasswordsVisibilityTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/promo/RealImportInPasswordsVisibilityTest.kt @@ -164,7 +164,7 @@ class RealImportInPasswordsVisibilityTest { testee.onPromoDismissed() assertFalse(testee.canShowImportInPasswords(5)) - verify(internalAutofillStore).hasDismissedImportedPasswordsPromo = true + verify(internalAutofillStore).hasDeclinedPasswordManagementImportPromo = true } @Test @@ -244,6 +244,6 @@ class RealImportInPasswordsVisibilityTest { hasDismissedImportedPasswordsPromo: Boolean = false, ) { whenever(internalAutofillStore.hasEverImportedPasswords).thenReturn(hasEverImportedPasswords) - whenever(internalAutofillStore.hasDismissedImportedPasswordsPromo).thenReturn(hasDismissedImportedPasswordsPromo) + whenever(internalAutofillStore.hasDeclinedPasswordManagementImportPromo).thenReturn(hasDismissedImportedPasswordsPromo) } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsActivityScreenViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsActivityScreenViewModelTest.kt index f079fa54e16e..dac98964ea4d 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsActivityScreenViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsActivityScreenViewModelTest.kt @@ -26,6 +26,7 @@ import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Count import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementEmptyState import com.duckduckgo.autofill.api.AutofillScreenLaunchSource import com.duckduckgo.autofill.api.AutofillScreenLaunchSource.BrowserOverflow import com.duckduckgo.autofill.api.AutofillScreenLaunchSource.BrowserSnackbar @@ -38,7 +39,6 @@ import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator import com.duckduckgo.autofill.impl.encoding.UrlUnicodeNormalizerImpl -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.PasswordManagementEmptyState import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_COPY_PASSWORD import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_COPY_USERNAME diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModelTest.kt index 9001d918c636..80b50636cf57 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/google/ImportFromGooglePasswordsDialogViewModelTest.kt @@ -2,11 +2,13 @@ package com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goo import app.cash.turbine.TurbineTestContext import app.cash.turbine.test -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.InBrowserPromo import com.duckduckgo.autofill.impl.importing.CredentialImporter import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.InProgress import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason.ErrorParsingCsv +import com.duckduckgo.autofill.impl.store.InternalAutofillStore import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.DeterminingFirstView @@ -15,17 +17,21 @@ import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.goog import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewMode.PreImport import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.google.ImportFromGooglePasswordsDialogViewModel.ViewState import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@OptIn(ExperimentalCoroutinesApi::class) class ImportFromGooglePasswordsDialogViewModelTest { @get:Rule @@ -34,10 +40,12 @@ class ImportFromGooglePasswordsDialogViewModelTest { private val importPasswordsPixelSender: ImportPasswordsPixelSender = mock() private val credentialImporter: CredentialImporter = mock() + private val autofillStore: InternalAutofillStore = mock() private val testee = ImportFromGooglePasswordsDialogViewModel( credentialImporter = credentialImporter, dispatchers = coroutineTestRule.testDispatcherProvider, importPasswordsPixelSender = importPasswordsPixelSender, + autofillStore = autofillStore, ) @Before @@ -121,6 +129,20 @@ class ImportFromGooglePasswordsDialogViewModelTest { } } + @Test + fun whenInBrowserPromoDismissedThenPixelSent() = runTest { + testee.onInBrowserPromoDismissed() + advanceUntilIdle() + verify(importPasswordsPixelSender).onUserCancelledImportPasswordsDialog(InBrowserPromo) + } + + @Test + fun whenInBrowserPromoDismissedThenDeclineRecorded() = runTest { + testee.onInBrowserPromoDismissed() + advanceUntilIdle() + verify(autofillStore).hasDeclinedInBrowserPasswordImportPromo = true + } + private fun configureImportInProgress() { whenever(credentialImporter.getImportStatus()).thenReturn(listOf(InProgress).asFlow()) } 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 abd14bf6296a..48b1b2960839 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 @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.ui.credential.passwordgeneration import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -56,6 +57,7 @@ class ResultHandlerUseGeneratedPasswordTest { private val existingCredentialMatchDetector: ExistingCredentialMatchDetector = mock() private val callback: AutofillEventListener = mock() private val usernameBackFiller: UsernameBackFiller = mock() + private val webView: WebView = mock() private val testee = ResultHandlerUseGeneratedPassword( dispatchers = coroutineTestRule.testDispatcherProvider, @@ -81,9 +83,9 @@ class ResultHandlerUseGeneratedPasswordTest { } @Test - fun whenUserRejectedToUsePasswordThenCorrectCallbackInvoked() { + fun whenUserRejectedToUsePasswordThenCorrectCallbackInvoked() = runTest { val bundle = bundle("example.com", acceptedGeneratedPassword = false) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onRejectGeneratedPassword("example.com") } @@ -91,7 +93,7 @@ class ResultHandlerUseGeneratedPasswordTest { 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) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onAcceptGeneratedPassword("example.com") } @@ -99,7 +101,7 @@ class ResultHandlerUseGeneratedPasswordTest { fun whenUserAcceptedToUsePasswordNoAutoLoginInThenCredentialIsSaved() = 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) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(any(), any()) } @@ -109,7 +111,7 @@ class ResultHandlerUseGeneratedPasswordTest { whenever(autofillStore.saveCredentials(any(), any())).thenReturn(aLogin(1)) val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = "pw") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autoSavedLoginsMonitor).setAutoSavedLoginId(any(), any()) } @@ -118,7 +120,7 @@ class ResultHandlerUseGeneratedPasswordTest { whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) "from-backfill".useBackFilledUsername() val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = "pw") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(any(), eq(LoginCredentials(domain = "example.com", username = "from-backfill", password = "pw"))) } @@ -126,14 +128,14 @@ class ResultHandlerUseGeneratedPasswordTest { fun whenUserAcceptedToUsePasswordAutoLoginButNothingToUpdateThenCredentialIsSaved() = runTest { whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) val bundle = bundle("example.com", acceptedGeneratedPassword = true, username = "from-js", password = "pw") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(any(), eq(LoginCredentials(domain = "example.com", username = "from-js", password = "pw"))) } fun whenUserAcceptedToUsePasswordNoAutoLoginUsernameProvidedThenBackFilledUsernameNotUsed() = runTest { whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) val bundle = bundle("example.com", acceptedGeneratedPassword = true, username = "from-js", password = "pw") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(any(), eq(LoginCredentials(domain = "example.com", username = "from-js", password = "pw"))) } @@ -143,7 +145,7 @@ class ResultHandlerUseGeneratedPasswordTest { whenever(autofillStore.getCredentialsWithId(1)).thenReturn(null) val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = "pw") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(any(), any()) } @@ -159,7 +161,7 @@ class ResultHandlerUseGeneratedPasswordTest { username = testLogin.username, password = testLogin.password, ) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore, never()).saveCredentials(any(), any()) verify(autofillStore, never()).updateCredentials(any(), any()) } @@ -176,7 +178,7 @@ class ResultHandlerUseGeneratedPasswordTest { username = "different-username", password = testLogin.password, ) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore, never()).saveCredentials(any(), any()) verify(autofillStore).updateCredentials(any(), any()) } @@ -193,7 +195,7 @@ class ResultHandlerUseGeneratedPasswordTest { username = testLogin.username, password = "different-password", ) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore, never()).saveCredentials(any(), any()) verify(autofillStore).updateCredentials(any(), any()) } @@ -201,7 +203,7 @@ class ResultHandlerUseGeneratedPasswordTest { @Test fun whenUserAcceptedToUsePasswordButPasswordIsNullThenCorrectCallbackNotInvoked() = runTest { val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = null) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback, never()).onAcceptGeneratedPassword("example.com") } @@ -210,7 +212,7 @@ class ResultHandlerUseGeneratedPasswordTest { val bundle = bundle("example.com", acceptedGeneratedPassword = true, password = "pw") whenever(autoSavedLoginsMonitor.getAutoSavedLoginId(any())).thenReturn(null) whenever(existingCredentialMatchDetector.determine(anyOrNull(), anyOrNull(), anyOrNull())).thenReturn(UsernameMatchDifferentPassword) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore, never()).saveCredentials(any(), any()) verify(autofillStore, never()).updateCredentials(any(), any()) } @@ -218,7 +220,7 @@ class ResultHandlerUseGeneratedPasswordTest { @Test fun whenBundleMissingUrlThenCallbackNotInvoked() = runTest { val bundle = bundle(url = null, acceptedGeneratedPassword = true) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyNoInteractions(callback) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSavingTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSavingTest.kt index 56de185fc696..d0b8cacabf07 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSavingTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerPromptToDisableCredentialSavingTest.kt @@ -1,6 +1,7 @@ package com.duckduckgo.autofill.impl.ui.credential.saving import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation @@ -8,6 +9,7 @@ import com.duckduckgo.autofill.api.AutofillEventListener import com.duckduckgo.autofill.impl.AutofillFireproofDialogSuppressor import com.duckduckgo.autofill.store.AutofillPrefsStore import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -29,6 +31,7 @@ class ResultHandlerPromptToDisableCredentialSavingTest { whenever(it.createBehavior(any(), any(), any())).thenReturn(disableAutofillPromptBehavior) } private val callback: AutofillEventListener = mock() + private val webView: WebView = mock() private val testee = ResultHandlerPromptToDisableCredentialSaving( autofillFireproofDialogSuppressor = autofillFireproofDialogSuppressor, @@ -39,17 +42,17 @@ class ResultHandlerPromptToDisableCredentialSavingTest { ) @Test - fun whenResultProcessedThenFireproofNotifiedDialogNotVisible() { + fun whenResultProcessedThenFireproofNotifiedDialogNotVisible() = runTest { val result = bundleForAutofillDisablePrompt() - testee.processResult(result, context, "tab-id-123", Fragment(), callback) + testee.processResult(result, context, "tab-id-123", Fragment(), callback, webView) verify(autofillFireproofDialogSuppressor).autofillSaveOrUpdateDialogVisibilityChanged(false) } @Test - fun whenResultProcessedThenShowPrompt() { + fun whenResultProcessedThenShowPrompt() = runTest { val fragment = Fragment() val result = bundleForAutofillDisablePrompt() - testee.processResult(result, context, "tab-id-123", fragment, callback) + testee.processResult(result, context, "tab-id-123", fragment, callback, webView) verify(disablePromptBehaviorFactory).createBehavior(context, fragment, callback) verify(disableAutofillPromptBehavior).showPrompt() verify(autofillPrefsStore).timestampUserLastPromptedToDisableAutofill = any() diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/saving/ResultHandlerSaveLoginCredentialsTest.kt index ac353e319d1b..9b5da6b35770 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 @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.ui.credential.saving import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -46,6 +47,7 @@ class ResultHandlerSaveLoginCredentialsTest { private val declineCounter: AutofillDeclineCounter = mock() private val autofillStore: InternalAutofillStore = mock() private val appBuildConfig: AppBuildConfig = mock() + private val webView: WebView = mock() private val testee = ResultHandlerSaveLoginCredentials( autofillFireproofDialogSuppressor = autofillFireproofDialogSuppressor, @@ -59,7 +61,7 @@ class ResultHandlerSaveLoginCredentialsTest { @Test fun whenSaveBundleMissingUrlThenNoAttemptToSaveMade() = runTest { val bundle = bundle(url = null, credentials = someLoginCredentials()) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifySaveNeverCalled() verifyNoInteractions(callback) } @@ -67,7 +69,7 @@ class ResultHandlerSaveLoginCredentialsTest { @Test fun whenSaveBundleMissingCredentialsThenNoAttemptToSaveMade() = runTest { val bundle = bundle(url = "example.com", credentials = null) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifySaveNeverCalled() verifyNoInteractions(callback) } @@ -77,7 +79,7 @@ class ResultHandlerSaveLoginCredentialsTest { val loginCredentials = LoginCredentials(domain = "example.com", username = "foo", password = "bar") val bundle = bundle("example.com", loginCredentials) whenever(autofillStore.saveCredentials(any(), any())).thenReturn(loginCredentials) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).saveCredentials(eq("example.com"), eq(loginCredentials)) verify(callback).onSavedCredentials(loginCredentials) } @@ -87,7 +89,7 @@ class ResultHandlerSaveLoginCredentialsTest { val loginCredentials = LoginCredentials(domain = "example.com", username = "foo", password = "bar") val bundle = bundle("example.com", loginCredentials) whenever(autofillStore.saveCredentials(any(), any())).thenReturn(loginCredentials) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(declineCounter).disableDeclineCounter() } @@ -95,7 +97,7 @@ class ResultHandlerSaveLoginCredentialsTest { fun whenSaveCredentialsUnsuccessfulThenDoesNotDisableDeclineCountMonitoringFlag() = runTest { val bundle = bundle("example.com", someLoginCredentials()) whenever(autofillStore.saveCredentials(any(), any())).thenReturn(null) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(declineCounter, never()).disableDeclineCounter() } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/selecting/ResultHandlerCredentialSelectionTest.kt index 18f29ff7b9f4..7581b9fa356a 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 @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.ui.credential.selecting import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -52,6 +53,7 @@ class ResultHandlerCredentialSelectionTest { private lateinit var deviceAuthenticator: FakeAuthenticator private lateinit var testee: ResultHandlerCredentialSelection private val autofillStore: InternalAutofillStore = mock() + private val webView: WebView = mock() @Before fun setup() = runTest { @@ -68,7 +70,7 @@ class ResultHandlerCredentialSelectionTest { fun whenUserRejectedToUseCredentialThenCorrectCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleForUserCancelling("example.com") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onNoCredentialsChosenForAutofill("example.com") } @@ -76,7 +78,7 @@ class ResultHandlerCredentialSelectionTest { fun whenUserAcceptedToUseCredentialsAndSuccessfullyAuthenticatedThenCorrectCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onShareCredentialsForAutofill("example.com", aLogin()) } @@ -84,7 +86,7 @@ class ResultHandlerCredentialSelectionTest { fun whenUserAcceptedToUseCredentialsAndCancelsAuthenticationThenCorrectCallbackInvoked() = runTest { configureCancelledAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onNoCredentialsChosenForAutofill("example.com") } @@ -92,7 +94,7 @@ class ResultHandlerCredentialSelectionTest { fun whenUserAcceptedToUseCredentialsAndAuthenticationFailsThenCorrectCallbackInvoked() = runTest { configureFailedAuth() val bundle = bundleForUserAcceptingToAutofill("example.com") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(callback).onNoCredentialsChosenForAutofill("example.com") } @@ -100,7 +102,7 @@ class ResultHandlerCredentialSelectionTest { fun whenUserAcceptedToUseCredentialsButMissingInBundleThenNoCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleMissingCredentials("example.com") - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyNoInteractions(callback) } @@ -108,7 +110,7 @@ class ResultHandlerCredentialSelectionTest { fun whenMissingUrlThenNoCallbackInvoked() = runTest { configureSuccessfulAuth() val bundle = bundleMissingUrl() - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyNoInteractions(callback) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/updating/ResultHandlerUpdateLoginCredentialsTest.kt index 8d9b3c36c206..f39c7d74a75c 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 @@ -17,6 +17,7 @@ package com.duckduckgo.autofill.impl.ui.credential.updating import android.os.Bundle +import android.webkit.WebView import androidx.fragment.app.Fragment import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry @@ -46,6 +47,7 @@ class ResultHandlerUpdateLoginCredentialsTest { private val autofillDialogSuppressor: AutofillFireproofDialogSuppressor = mock() private val callback: AutofillEventListener = mock() private val appBuildConfig: AppBuildConfig = mock() + private val webView: WebView = mock() private val testee = ResultHandlerUpdateLoginCredentials( autofillFireproofDialogSuppressor = autofillDialogSuppressor, @@ -62,7 +64,7 @@ class ResultHandlerUpdateLoginCredentialsTest { credentials = someLoginCredentials(), Password, ) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyUpdateNeverCalled() } @@ -70,7 +72,7 @@ class ResultHandlerUpdateLoginCredentialsTest { fun whenUpdateBundleMissingCredentialsThenNoAttemptToSaveMade() = runTest { val bundle = bundleForUpdateDialog(url = "example.com", credentials = null, Password) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verifyUpdateNeverCalled() } @@ -78,7 +80,7 @@ class ResultHandlerUpdateLoginCredentialsTest { fun whenUpdateBundleWellFormedThenCredentialsAreUpdated() = runTest { val loginCredentials = LoginCredentials(domain = "example.com", username = "foo", password = "bar") val bundle = bundleForUpdateDialog("example.com", loginCredentials, Password) - testee.processResult(bundle, context, "tab-id-123", Fragment(), callback) + testee.processResult(bundle, context, "tab-id-123", Fragment(), callback, webView) verify(autofillStore).updateCredentials( eq("example.com"), eq(loginCredentials), diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModelTest.kt index 17dca6832f51..aa3556899bfc 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/settings/AutofillSettingsViewModelTest.kt @@ -8,10 +8,9 @@ import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.WebMessageListener import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.AutofillSettings import com.duckduckgo.autofill.api.AutofillScreenLaunchSource -import com.duckduckgo.autofill.api.AutofillScreenLaunchSource.SettingsActivity import com.duckduckgo.autofill.impl.deviceauth.DeviceAuthenticator -import com.duckduckgo.autofill.impl.importing.AutofillImportLaunchSource.AutofillSettings import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_DISABLED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENABLE_AUTOFILL_TOGGLE_MANUALLY_ENABLED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_EMPTY_STATE_CTA_BUTTON_SHOWN 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 ccee07dd825e..900f03b1358e 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 @@ -320,6 +320,9 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { binding.importPasswordsResetImportedFlagButton.setClickListener { lifecycleScope.launch(dispatchers.io()) { autofillStore.hasEverImportedPasswords = false + autofillStore.hasDeclinedPasswordManagementImportPromo = false + autofillStore.hasDeclinedInBrowserPasswordImportPromo = false + autofillStore.inBrowserImportPromoShownCount = 0 } Toast.makeText( this@AutofillInternalSettingsActivity, diff --git a/autofill/autofill-internal/src/main/res/values/donottranslate.xml b/autofill/autofill-internal/src/main/res/values/donottranslate.xml index c729ef01af3f..92a77b90dddb 100644 --- a/autofill/autofill-internal/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-internal/src/main/res/values/donottranslate.xml @@ -45,7 +45,7 @@ Import CSV %1$d passwords imported from Google Previous Google Imports - Tap to forget previous imports + Tap to forget previous imports, and import prompt choices Eligible to see Google Import promos again Maximum number of days since install