diff --git a/autofill/autofill-impl/build.gradle b/autofill/autofill-impl/build.gradle index 8ccbc43a69fc..bad937238f70 100644 --- a/autofill/autofill-impl/build.gradle +++ b/autofill/autofill-impl/build.gradle @@ -72,6 +72,7 @@ dependencies { implementation AndroidX.biometric implementation "net.zetetic:android-database-sqlcipher:_" + implementation "com.facebook.shimmer:shimmer:_" // Testing dependencies testImplementation "org.mockito.kotlin:mockito-kotlin:_" diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt index 90eed5c0f602..0ab5e328398c 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/deviceauth/AutofillAuthorizationGracePeriod.kt @@ -40,6 +40,16 @@ interface AutofillAuthorizationGracePeriod { */ fun recordSuccessfulAuthorization() + /** + * Requests an extended grace period. This may extend the grace period to a longer duration. + */ + fun requestExtendedGracePeriod() + + /** + * Removes the request for an extended grace period + */ + fun removeRequestForExtendedGracePeriod() + /** * Invalidates the grace period, so that the next call to [isAuthRequired] will return true */ @@ -53,12 +63,21 @@ class AutofillTimeBasedAuthorizationGracePeriod @Inject constructor( ) : AutofillAuthorizationGracePeriod { private var lastSuccessfulAuthTime: Long? = null + private var extendedGraceTimeRequested: Long? = null override fun recordSuccessfulAuthorization() { lastSuccessfulAuthTime = timeProvider.currentTimeMillis() Timber.v("Recording timestamp of successful auth") } + override fun requestExtendedGracePeriod() { + extendedGraceTimeRequested = timeProvider.currentTimeMillis() + } + + override fun removeRequestForExtendedGracePeriod() { + extendedGraceTimeRequested = null + } + override fun isAuthRequired(): Boolean { lastSuccessfulAuthTime?.let { lastAuthTime -> val timeSinceLastAuth = timeProvider.currentTimeMillis() - lastAuthTime @@ -67,17 +86,35 @@ class AutofillTimeBasedAuthorizationGracePeriod @Inject constructor( Timber.v("Within grace period; auth not required") return false } + + if (inExtendedGracePeriod()) { + Timber.v("Within extended grace period; auth not required") + return false + } } + Timber.v("No last auth time recorded or outside grace period; auth required") return true } + private fun inExtendedGracePeriod(): Boolean { + val extendedRequest = extendedGraceTimeRequested + if (extendedRequest == null) { + return false + } else { + val timeSinceExtendedGrace = timeProvider.currentTimeMillis() - extendedRequest + return timeSinceExtendedGrace <= AUTH_GRACE_EXTENDED_PERIOD_MS + } + } + override fun invalidate() { lastSuccessfulAuthTime = null + removeRequestForExtendedGracePeriod() } companion object { private const val AUTH_GRACE_PERIOD_MS = 15_000 + private const val AUTH_GRACE_EXTENDED_PERIOD_MS = 180_000 } } 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 1567f2898930..49641f9e124e 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 @@ -44,7 +44,13 @@ import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.FragmentImportGooglePasswordsWebflowBinding import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordResult.Companion.RESULT_KEY_DETAILS -import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.* +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.Initializing +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.LoadStartPage +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.NavigatingBack +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserCancelledImportFlow +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserFinishedCannotImport +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserFinishedImportFlow +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.WebContentShowing import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowWebViewClient.NewPageCallback import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpAutofillCallback import com.duckduckgo.autofill.impl.importing.gpm.webflow.autofill.NoOpAutofillEventListener diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt index 9b46fa6d9b86..bdc925101e0a 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/pixel/AutofillPixelNames.kt @@ -24,9 +24,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT_ONBOARDED_USER import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_ENGAGEMENT_STACKED_LOGINS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_COPIED_DESKTOP_LINK -import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER -import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SHARED_DESKTOP_LINK import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_USER_JOURNEY_RESTARTED @@ -43,6 +41,8 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAK import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_CONFIRMED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISPLAYED +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_TOOLTIP_DISMISSED import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ADDRESS import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.EMAIL_USE_ALIAS @@ -142,8 +142,8 @@ enum class AutofillPixelNames(override val pixelName: String) : Pixel.PixelName AUTOFILL_TOGGLED_ON_SEARCH("m_autofill_toggled_on"), AUTOFILL_TOGGLED_OFF_SEARCH("m_autofill_toggled_off"), - AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON("m_autofill_logins_import_no_passwords"), - AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU("m_autofill_logins_import"), + AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON("m_autofill_logins_import_no_passwords"), + AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU("m_autofill_logins_import"), AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER("m_autofill_logins_import_get_desktop"), AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP("m_autofill_logins_import_sync"), AUTOFILL_IMPORT_PASSWORDS_USER_TOOK_NO_ACTION("m_autofill_logins_import_no-action"), @@ -177,8 +177,8 @@ object AutofillPixelsRequiringDataCleaning : PixelParamRemovalPlugin { AUTOFILL_ENGAGEMENT_ONBOARDED_USER.pixelName to PixelParameter.removeAtb(), AUTOFILL_ENGAGEMENT_STACKED_LOGINS.pixelName to PixelParameter.removeAtb(), - AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON.pixelName to PixelParameter.removeAtb(), - AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU.pixelName to PixelParameter.removeAtb(), + AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON.pixelName to PixelParameter.removeAtb(), + AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU.pixelName to PixelParameter.removeAtb(), AUTOFILL_IMPORT_PASSWORDS_GET_DESKTOP_BROWSER.pixelName to PixelParameter.removeAtb(), AUTOFILL_IMPORT_PASSWORDS_SYNC_WITH_DESKTOP.pixelName to PixelParameter.removeAtb(), AUTOFILL_IMPORT_PASSWORDS_USER_TOOK_NO_ACTION.pixelName to PixelParameter.removeAtb(), diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt index 151aed68cee7..5fb06faec9e0 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModel.kt @@ -20,8 +20,12 @@ import android.util.Patterns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel +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.app.browser.favicon.FaviconManager import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserOverflow import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserSnackbar @@ -89,6 +93,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.LaunchResetNeverSaveListConfirmation import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.PromptUserToAuthenticateMassDeletion import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.ReevalutePromotions +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsManagementViewState import com.duckduckgo.autofill.impl.ui.credential.management.neversaved.NeverSavedSitesViewState import com.duckduckgo.autofill.impl.ui.credential.management.searching.CredentialListFilter import com.duckduckgo.autofill.impl.ui.credential.management.viewing.duckaddress.DuckAddressIdentifier @@ -133,6 +138,8 @@ class AutofillSettingsViewModel @Inject constructor( private val autofillBreakageReportSender: AutofillBreakageReportSender, private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore, private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules, + private val autofillFeature: AutofillFeature, + private val webViewCapabilityChecker: WebViewCapabilityChecker, ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState()) @@ -141,6 +148,9 @@ class AutofillSettingsViewModel @Inject constructor( private val _neverSavedSitesViewState = MutableStateFlow(NeverSavedSitesViewState()) val neverSavedSitesViewState: StateFlow = _neverSavedSitesViewState + private val _importPasswordsViewState = MutableStateFlow(ImportPasswordsManagementViewState()) + val importPasswordsViewState: StateFlow = _importPasswordsViewState + private val _commands = MutableStateFlow>(emptyList()) val commands: StateFlow> = _commands @@ -431,6 +441,14 @@ class AutofillSettingsViewModel @Inject constructor( _neverSavedSitesViewState.value = NeverSavedSitesViewState(showOptionToReset = count > 0) } } + + viewModelScope.launch(dispatchers.io()) { + val gpmImport = autofillFeature.self().isEnabled() && autofillFeature.canImportFromGooglePasswordManager().isEnabled() + val webViewWebMessageSupport = webViewCapabilityChecker.isSupported(WebMessageListener) + val webViewDocumentStartJavascript = webViewCapabilityChecker.isSupported(DocumentStartJavaScript) + val canImport = gpmImport && webViewWebMessageSupport && webViewDocumentStartJavascript + _importPasswordsViewState.value = ImportPasswordsManagementViewState(canImport) + } } private suspend fun isBreakageReportingAllowed(): Boolean { @@ -690,7 +708,9 @@ class AutofillSettingsViewModel @Inject constructor( } fun onImportPasswords() { - addCommand(LaunchImportPasswords) + viewModelScope.launch(dispatchers.io()) { + addCommand(LaunchImportPasswords) + } } fun onReportBreakageClicked() { @@ -702,7 +722,10 @@ class AutofillSettingsViewModel @Inject constructor( } } - fun updateCurrentSite(currentUrl: String?, privacyProtectionEnabled: Boolean?) { + fun updateCurrentSite( + currentUrl: String?, + privacyProtectionEnabled: Boolean?, + ) { val updatedReportBreakageState = _viewState.value.reportBreakageState.copy( currentUrl = currentUrl, privacyProtectionEnabled = privacyProtectionEnabled, diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsManagementViewState.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsManagementViewState.kt new file mode 100644 index 000000000000..5e2c2435b248 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/importpassword/ImportPasswordsManagementViewState.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.autofill.impl.ui.credential.management.importpassword + +data class ImportPasswordsManagementViewState( + val canImportFromGooglePasswords: Boolean = false, +) 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 620118de9b5e..ff11c06029df 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 @@ -47,8 +47,8 @@ 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.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON -import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON +import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU 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.ContextMenuAction.CopyPassword @@ -64,6 +64,7 @@ import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsVie import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.ReevalutePromotions import com.duckduckgo.autofill.impl.ui.credential.management.AutofillSettingsViewModel.ListModeCommand.ShowUserReportSentMessage import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordActivityParams +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsManagementViewState import com.duckduckgo.autofill.impl.ui.credential.management.sorting.CredentialGrouper import com.duckduckgo.autofill.impl.ui.credential.management.sorting.InitialExtractor import com.duckduckgo.autofill.impl.ui.credential.management.suggestion.SuggestionListBuilder @@ -143,7 +144,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill private var searchMenuItem: MenuItem? = null private var resetNeverSavedSitesMenuItem: MenuItem? = null private var deleteAllPasswordsMenuItem: MenuItem? = null - private var importPasswordsMenuItem: MenuItem? = null + private var syncDesktopPasswordsMenuItem: MenuItem? = null + private var importGooglePasswordsMenuItem: MenuItem? = null private val globalAutofillToggleListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> if (!lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) return@OnCheckedChangeListener @@ -241,9 +243,13 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill } private fun configureImportPasswordsButton() { - binding.emptyStateLayout.importPasswordsButton.setOnClickListener { + binding.emptyStateLayout.importPasswordsFromGoogleButton.setOnClickListener { viewModel.onImportPasswords() - pixel.fire(AUTOFILL_IMPORT_PASSWORDS_CTA_BUTTON) + } + + binding.emptyStateLayout.importPasswordsViaDesktopSyncButton.setOnClickListener { + launchImportPasswordsFromDesktopSyncScreen() + pixel.fire(AUTOFILL_SYNC_DESKTOP_PASSWORDS_CTA_BUTTON) } } @@ -258,7 +264,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill searchMenuItem = menu.findItem(R.id.searchLogins) resetNeverSavedSitesMenuItem = menu.findItem(R.id.resetNeverSavedSites) deleteAllPasswordsMenuItem = menu.findItem(R.id.deleteAllPasswords) - importPasswordsMenuItem = menu.findItem(R.id.importPasswords) + syncDesktopPasswordsMenuItem = menu.findItem(R.id.syncDesktopPasswords) + importGooglePasswordsMenuItem = menu.findItem(R.id.importGooglePasswords) initializeSearchBar() } @@ -268,7 +275,8 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill searchMenuItem?.isVisible = loginsSaved deleteAllPasswordsMenuItem?.isVisible = loginsSaved resetNeverSavedSitesMenuItem?.isVisible = viewModel.neverSavedSitesViewState.value.showOptionToReset - importPasswordsMenuItem?.isVisible = loginsSaved + syncDesktopPasswordsMenuItem?.isVisible = loginsSaved + importGooglePasswordsMenuItem?.isVisible = loginsSaved && viewModel.importPasswordsViewState.value.canImportFromGooglePasswords } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { @@ -288,9 +296,14 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill true } - R.id.importPasswords -> { + R.id.importGooglePasswords -> { viewModel.onImportPasswords() - pixel.fire(AUTOFILL_IMPORT_PASSWORDS_OVERFLOW_MENU) + true + } + + R.id.syncDesktopPasswords -> { + launchImportPasswordsFromDesktopSyncScreen() + pixel.fire(AUTOFILL_SYNC_DESKTOP_PASSWORDS_OVERFLOW_MENU) true } @@ -364,9 +377,28 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill } } + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.importPasswordsViewState.collect { + // we can just invalidate the menu as [onPrepareMenu] will handle the new visibility for importing passwords menu item + parentActivity()?.invalidateOptionsMenu() + + configureImportPasswordsButtonVisibility(it) + } + } + } + viewModel.onViewCreated() } + private fun configureImportPasswordsButtonVisibility(state: ImportPasswordsManagementViewState) { + if (state.canImportFromGooglePasswords) { + binding.emptyStateLayout.importPasswordsFromGoogleButton.show() + } else { + binding.emptyStateLayout.importPasswordsFromGoogleButton.gone() + } + } + private fun observeListModeViewModelCommands() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(State.STARTED) { @@ -395,6 +427,13 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill } private fun launchImportPasswordsScreen() { + context?.let { + val dialog = ImportFromGooglePasswordsDialog.instance() + dialog.show(parentFragmentManager, "SelectImportPasswordMethodDialog") + } + } + + private fun launchImportPasswordsFromDesktopSyncScreen() { context?.let { globalActivityStarter.start(it, ImportPasswordActivityParams) } @@ -602,7 +641,11 @@ class AutofillManagementListMode : DuckDuckGoFragment(R.layout.fragment_autofill } companion object { - fun instance(currentUrl: String? = null, privacyProtectionEnabled: Boolean?, source: AutofillSettingsLaunchSource? = null) = + fun instance( + currentUrl: String? = null, + privacyProtectionEnabled: Boolean?, + source: AutofillSettingsLaunchSource? = null, + ) = AutofillManagementListMode().apply { arguments = Bundle().apply { putString(ARG_CURRENT_URL, currentUrl) diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/ImportFromGooglePasswordsDialog.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/ImportFromGooglePasswordsDialog.kt new file mode 100644 index 000000000000..02897375bc79 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/ui/credential/management/viewing/ImportFromGooglePasswordsDialog.kt @@ -0,0 +1,275 @@ +/* + * 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.ui.credential.management.viewing + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.core.content.IntentCompat +import androidx.fragment.app.setFragmentResult +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.statistics.pixels.Pixel +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.CredentialImporter +import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult +import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePassword.AutofillImportViaGooglePasswordManagerScreen +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.viewing.ImportFromGooglePasswordsDialog.Companion.Result.UserChoseGcmImport +import com.duckduckgo.common.utils.extensions.html +import com.duckduckgo.di.scopes.FragmentScope +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import timber.log.Timber + +@InjectWith(FragmentScope::class) +class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { + + @Inject + lateinit var pixel: Pixel + + @Inject + lateinit var credentialImporter: CredentialImporter + + /** + * To capture all the ways the BottomSheet can be dismissed, we might end up with onCancel being called when we don't want it + * This flag is set to true when taking an action which dismisses the dialog, but should not be treated as a cancellation. + */ + private var ignoreCancellationEvents = false + + override fun getTheme(): Int = R.style.AutofillBottomSheetDialogTheme + + @Inject + lateinit var faviconManager: FaviconManager + + @Inject + lateinit var globalActivityStarter: GlobalActivityStarter + + @Inject + lateinit var authorizationGracePeriod: AutofillAuthorizationGracePeriod + + private var _binding: ContentImportFromGooglePasswordDialogBinding? = null + + private val binding get() = _binding!! + + private val importGooglePasswordsFlowLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + Timber.i("cdr onActivityResult for Google Password Manager import flow. resultCode=${activityResult.resultCode}") + + if (activityResult.resultCode == Activity.RESULT_OK) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + activityResult.data?.let { data -> + val resultDetails = IntentCompat.getParcelableExtra( + data, + ImportGooglePasswordResult.RESULT_KEY_DETAILS, + ImportGooglePasswordResult::class.java, + ) + when (resultDetails) { + is ImportGooglePasswordResult.Success -> { + binding.prePostViewSwitcher.displayedChild = 1 + observeImportJob() + } + is ImportGooglePasswordResult.Error -> processErrorResult() + is ImportGooglePasswordResult.UserCancelled, null -> { + Timber.i("cdr User cancelled Google Password Manager import flow") + } + } + } + } + } + } + } + + private suspend fun observeImportJob() { + credentialImporter.getImportStatus().collect { + when (it) { + is ImportResult.InProgress -> { + Timber.d("Import in progress") + binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 0 + } + + is Finished -> { + Timber.d("Import finished: ${it.savedCredentials} imported. ${it.numberSkipped} skipped.") + processSuccessResult(it) + } + } + } + } + + private fun processSuccessResult(result: Finished) { + binding.postflow.importFinished.errorNotImported.visibility = GONE + binding.postflow.appIcon.setImageDrawable(ContextCompat.getDrawable(binding.root.context, R.drawable.ic_success_128)) + binding.postflow.dialogTitle.text = getString(R.string.importPasswordsProcessingResultDialogTitleUponSuccess) + + with(binding.postflow.importFinished.resultsImported) { + val output = getString(R.string.importPasswordsProcessingResultDialogResultPasswordsImported, result.savedCredentials) + setPrimaryText(output.html(binding.root.context)) + } + + with(binding.postflow.importFinished.duplicatesNotImported) { + val output = getString(R.string.importPasswordsProcessingResultDialogResultDuplicatesSkipped, result.numberSkipped) + setPrimaryText(output.html(binding.root.context)) + visibility = if (result.numberSkipped > 0) VISIBLE else GONE + } + + with(binding.postflow.importFinished.primaryCtaButton) { + setOnClickListener { + setResult(UserChoseGcmImport(result)) + dismiss() + } + setText(R.string.importPasswordsProcessingResultDialogDoneButtonText) + } + + binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 1 + } + + private fun processErrorResult() { + binding.postflow.importFinished.resultsImported.visibility = GONE + binding.postflow.importFinished.duplicatesNotImported.visibility = GONE + binding.postflow.importFinished.errorNotImported.visibility = VISIBLE + + binding.postflow.dialogTitle.text = getString(R.string.importPasswordsProcessingResultDialogTitleBeforeSuccess) + binding.postflow.appIcon.setImageDrawable(ContextCompat.getDrawable(binding.root.context, R.drawable.ic_passwords_import_128)) + + with(binding.postflow.importFinished.primaryCtaButton) { + setOnClickListener { + launchImportGcmFlow() + binding.prePostViewSwitcher.displayedChild = 0 + binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 0 + } + text = getString(R.string.importPasswordsProcessingResultDialogRetryButtonText) + } + + binding.prePostViewSwitcher.displayedChild = 1 + binding.postflow.inProgressFinishedViewSwitcher.displayedChild = 1 + } + + override fun onAttach(context: Context) { + AndroidSupportInjection.inject(this) + super.onAttach(context) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState != null) { + // If being created after a configuration change, dismiss the dialog as the WebView will be re-created too + dismiss() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = ContentImportFromGooglePasswordDialogBinding.inflate(inflater, container, false) + configureViews(binding) + return binding.root + } + + override fun onDestroyView() { + _binding = null + authorizationGracePeriod.removeRequestForExtendedGracePeriod() + super.onDestroyView() + } + + private fun configureViews(binding: ContentImportFromGooglePasswordDialogBinding) { + (dialog as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED + configureCloseButton(binding) + + with(binding.preflow.importGcmButton) { + setOnClickListener { onImportGcmButtonClicked() } + } + } + + private fun onImportGcmButtonClicked() { + launchImportGcmFlow() + } + + private fun launchImportGcmFlow() { + authorizationGracePeriod.requestExtendedGracePeriod() + + val intent = globalActivityStarter.startIntent(requireContext(), AutofillImportViaGooglePasswordManagerScreen) + importGooglePasswordsFlowLauncher.launch(intent) + } + + private fun setResult(result: Result?) { + val resultBundle = Bundle().apply { + putParcelable(RESULT_KEY_DETAILS, result) + } + setFragmentResult(RESULT_KEY, resultBundle) + dismiss() + } + + override fun onCancel(dialog: DialogInterface) { + if (ignoreCancellationEvents) { + Timber.v("onCancel: Ignoring cancellation event") + return + } + setResult(Result.UserCancelled) + } + + private fun configureCloseButton(binding: ContentImportFromGooglePasswordDialogBinding) { + binding.closeButton.setOnClickListener { (dialog as BottomSheetDialog).animateClosed() } + } + + companion object { + + fun instance(): ImportFromGooglePasswordsDialog { + val fragment = ImportFromGooglePasswordsDialog() + return fragment + } + + const val RESULT_KEY = "SelectImportPasswordMethodDialogResult" + const val RESULT_KEY_DETAILS = "SelectImportPasswordMethodDialogResultDetails" + + sealed interface Result : Parcelable { + + @Parcelize + data class UserChoseGcmImport(val importResult: ImportResult) : Result + + @Parcelize + data object UserCancelled : Result + + @Parcelize + data object ErrorDuringImport : Result + } + } +} diff --git a/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml b/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml new file mode 100644 index 000000000000..a23100c2710f --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/autofill_gpm_export_instruction.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml b/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml new file mode 100644 index 000000000000..b7a9c542829f --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/autofill_rounded_border_import_background.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml b/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml new file mode 100644 index 000000000000..3e79b3d20fbe --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_check_recolorable_24.xml @@ -0,0 +1,14 @@ + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml b/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml new file mode 100644 index 000000000000..432454a57240 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_cross_recolorable_red_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml new file mode 100644 index 000000000000..bcc09c00bf5e --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_passwords_import_128.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_success_128.xml b/autofill/autofill-impl/src/main/res/drawable/ic_success_128.xml new file mode 100644 index 000000000000..722ef58f645f --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_success_128.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml index c234aed93668..e9dc191c4343 100644 --- a/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml +++ b/autofill/autofill-impl/src/main/res/layout/autofill_management_credential_list_empty_state.xml @@ -66,20 +66,32 @@ android:layout_marginTop="@dimen/keyline_2" android:layout_marginStart="@dimen/keyline_6" android:layout_marginEnd="@dimen/keyline_6" + android:paddingBottom="@dimen/keyline_5" app:layout_constraintWidth_max="300dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/emptyPlaceholderTitle" android:text="@string/credentialManagementNoLoginsSavedSubtitle" /> + + + android:text="@string/autofillSyncDesktopPasswordEmptyStateButtonTitle" /> diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_from_google_password_dialog.xml b/autofill/autofill-impl/src/main/res/layout/content_import_from_google_password_dialog.xml new file mode 100644 index 000000000000..17e894b84d25 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/content_import_from_google_password_dialog.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml new file mode 100644 index 000000000000..94e5109a2fa4 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml new file mode 100644 index 000000000000..c2307128fc73 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_in_progress.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml new file mode 100644 index 000000000000..2e48cbafdf4a --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_post_flow_result.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 000000000000..b5cc4d9e9de9 --- /dev/null +++ b/autofill/autofill-impl/src/main/res/layout/content_import_google_password_pre_flow.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml index d8b20f9b45df..ca4aecf0ee4d 100644 --- a/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml +++ b/autofill/autofill-impl/src/main/res/menu/autofill_list_mode_menu.xml @@ -40,15 +40,21 @@ app:showAsAction="never" /> + + \ 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 fa2047cdba76..0dc3cfeee517 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -20,4 +20,26 @@ Import Google Passwords %1$d passwords imported from Google + %1$d passwords imported from CSV + + Import Passwords From Google + Import Passwords From Google + + Sync DuckDuckGo Passwords + Sync DuckDuckGo Passwords + + Import Your Google Passwords + Google may ask you to sign in or enter your password to confirm. + Open Google Passwords + Choose a CSV file + Import from Desktop Browser + + Import to DuckDuckGo + Import Complete + Got It + Retry + Password import failed + Skipped (duplicate or invalid): %d]]> + Passwords imported: %d]]> + \ No newline at end of file diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt index 3c7b7accf969..bd4399ca86a4 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/management/AutofillSettingsViewModelTest.kt @@ -18,9 +18,13 @@ package com.duckduckgo.autofill.impl.ui.credential.management import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test +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.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.AutofillSettingsLaunchSource import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserOverflow import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource.BrowserSnackbar @@ -78,6 +82,8 @@ import com.duckduckgo.autofill.impl.ui.credential.management.viewing.duckaddress import com.duckduckgo.autofill.impl.ui.credential.repository.DuckAddressStatusRepository import com.duckduckgo.autofill.impl.urlmatcher.AutofillDomainNameUrlMatcher import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.Toggle.State import kotlin.reflect.KClass import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -121,6 +127,8 @@ class AutofillSettingsViewModelTest { private val autofillBreakageReportCanShowRules: AutofillBreakageReportCanShowRules = mock() private val autofillBreakageReportDataStore: AutofillSiteBreakageReportingDataStore = mock() private val urlMatcher = AutofillDomainNameUrlMatcher(UrlUnicodeNormalizerImpl()) + private val webViewCapabilityChecker: WebViewCapabilityChecker = mock() + private val autofillFeature = FakeFeatureToggleFactory.create(AutofillFeature::class.java) private val testee = AutofillSettingsViewModel( autofillStore = mockStore, @@ -140,6 +148,8 @@ class AutofillSettingsViewModelTest { autofillBreakageReportSender = autofillBreakageReportSender, autofillBreakageReportDataStore = autofillBreakageReportDataStore, autofillBreakageReportCanShowRules = autofillBreakageReportCanShowRules, + webViewCapabilityChecker = webViewCapabilityChecker, + autofillFeature = autofillFeature, ) @Before @@ -151,6 +161,10 @@ class AutofillSettingsViewModelTest { whenever(mockStore.getCredentialCount()).thenReturn(flowOf(0)) whenever(neverSavedSiteRepository.neverSaveListCount()).thenReturn(emptyFlow()) whenever(deviceAuthenticator.isAuthenticationRequiredForAutofill()).thenReturn(true) + whenever(webViewCapabilityChecker.isSupported(WebMessageListener)).thenReturn(true) + whenever(webViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(true) + autofillFeature.self().setRawStoredState(State(enable = true)) + autofillFeature.canImportFromGooglePasswordManager().setRawStoredState(State(enable = true)) } } @@ -922,6 +936,45 @@ class AutofillSettingsViewModelTest { verify(pixel).fire(AUTOFILL_SITE_BREAKAGE_REPORT_CONFIRMATION_DISMISSED) } + @Test + fun whenImportGooglePasswordsIsEnabledThenViewStateReflectsThat() = runTest { + testee.onViewCreated() + testee.importPasswordsViewState.test { + assertTrue(awaitItem().canImportFromGooglePasswords) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenImportGooglePasswordsFeatureFlagDisabledThenViewStateReflectsThat() = runTest { + autofillFeature.canImportFromGooglePasswordManager().setRawStoredState(State(enable = false)) + testee.onViewCreated() + testee.importPasswordsViewState.test { + assertFalse(awaitItem().canImportFromGooglePasswords) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenImportGooglePasswordsFeatureDisabledDueToWebMessageListenerNotSupportedThenViewStateReflectsThat() = runTest { + whenever(webViewCapabilityChecker.isSupported(WebMessageListener)).thenReturn(false) + testee.onViewCreated() + testee.importPasswordsViewState.test { + assertFalse(awaitItem().canImportFromGooglePasswords) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun whenImportGooglePasswordsFeatureDisabledDueToDocumentStartJavascriptNotSupportedThenViewStateReflectsThat() = runTest { + whenever(webViewCapabilityChecker.isSupported(DocumentStartJavaScript)).thenReturn(false) + testee.onViewCreated() + testee.importPasswordsViewState.test { + assertFalse(awaitItem().canImportFromGooglePasswords) + cancelAndIgnoreRemainingEvents() + } + } + private fun List.verifyHasCommandToShowDeleteAllConfirmation(expectedNumberOfCredentialsToDelete: Int) { val confirmationCommand = this.firstOrNull { it is LaunchDeleteAllPasswordsConfirmation } assertNotNull(confirmationCommand) 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 77564c4fe5cb..5577f5fe4f22 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 @@ -159,7 +159,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { } is CsvCredentialImportResult.Error -> { - "Failed to import passwords due to an error".showSnackbar() + FAILED_IMPORT_GENERIC_ERROR.showSnackbar() } } } @@ -177,7 +177,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { observePasswordInputUpdates() } Error -> { - "Failed to import passwords due to an error".showSnackbar() + FAILED_IMPORT_GENERIC_ERROR.showSnackbar() } is UserCancelled, null -> { } @@ -600,6 +600,8 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { return Intent(context, AutofillInternalSettingsActivity::class.java) } + private const val FAILED_IMPORT_GENERIC_ERROR = "Failed to import passwords due to an error" + private val sampleUrlList = listOf( "fill.dev", "duckduckgo.com",