diff --git a/PixelDefinitions/pixels/definitions/autofill.json5 b/PixelDefinitions/pixels/definitions/autofill.json5 index 6b4943d6f23c..584f73916bc9 100644 --- a/PixelDefinitions/pixels/definitions/autofill.json5 +++ b/PixelDefinitions/pixels/definitions/autofill.json5 @@ -515,5 +515,167 @@ "triggers": ["exception"], "suffixes": ["form_factor"], "parameters": ["appVersion", "error"] + }, + "autofill_import_google_passwords_preimport_prompt_displayed": { + "description": "The prompt explaining the Google Password Manager import was displayed to the user.", + "owners": ["catalinradoiu"], + "triggers": ["other"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] + }, + "autofill_import_google_passwords_preimport_prompt_confirmed": { + "description": "The user accepted the pre-import prompt, starting the Google Password Manager import web flow.", + "owners": ["catalinradoiu"], + "triggers": ["other"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] + }, + "autofill_import_google_passwords_result_user_cancelled": { + "description": "The user backed out of the Google Password Manager import. `stage` says where they were when they left: the pre-import prompt, or a stage of the web flow itself.", + "owners": ["catalinradoiu"], + "triggers": ["other"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "stage", + "type": "string", + "description": "Where the user left the import. `pre-import-dialog` for the prompt before the web flow; otherwise the web flow stage, mapped from the current URL against the URL mappings in the privacy config, or `webflow-unknown` when no mapping matches.", + "examples": ["pre-import-dialog", "webflow-unknown"] + }, + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] + }, + "autofill_import_google_passwords_result_success": { + "description": "The Google Password Manager import completed and the credentials were written to the password store.", + "owners": ["catalinradoiu"], + "triggers": ["other"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "saved_credentials", + "type": "string", + "description": "Number of credentials saved, bucketed.", + "enum": ["none", "few", "some", "many", "lots"] + }, + { + "key": "skipped_credentials", + "type": "string", + "description": "Number of credentials skipped as duplicates, bucketed.", + "enum": ["none", "few", "some", "many", "lots"] + }, + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] + }, + "autofill_import_google_passwords_result_parsing": { + "description": "The Google Password Manager import failed because the exported CSV of credentials could not be parsed.", + "owners": ["catalinradoiu"], + "triggers": ["exception"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] + }, + "autofill_import_google_passwords_webview_crash": { + "description": "The Google Password Manager import failed because the WebView hosting the import flow crashed.", + "owners": ["catalinradoiu"], + "triggers": ["exception"], + "suffixes": ["form_factor"], + "parameters": [ + "appVersion", + { + "key": "source", + "type": "string", + "description": "Where the import was launched from.", + "enum": [ + "password_management_promo", + "password_management_empty_state", + "password_management_overflow", + "autofill_settings_button", + "in_browser_promo", + "settings", + "onboarding", + "unknown" + ] + } + ] } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModel.kt index d1e4ef0861ce..aaac4640d36f 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModel.kt @@ -214,7 +214,7 @@ class ConfigDrivenOnboardingPageViewModel @Inject constructor( } } - fun onContentBound( + fun onBeforeContentBound( stepId: LinearOnboardingStepId, content: ContentConfig, ) { diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenWelcomePageFragment.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenWelcomePageFragment.kt index bc2f9cbfb295..1f45322bf166 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenWelcomePageFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenWelcomePageFragment.kt @@ -189,7 +189,7 @@ class ConfigDrivenWelcomePageFragment : OnboardingPageFragment(R.layout.content_ content = ContentControllerImpl( binding = binding.daxDialogCta, contentValues = viewModel.contentValues, - onContentBound = viewModel::onContentBound, + onBeforeContentBound = viewModel::onBeforeContentBound, isLightMode = { appTheme.isLightModeEnabled() }, isAddressBarRebrandEnabled = { appBrandDesignUpdateToggles.addressBar().isEnabled() }, ), diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/engine/ContentController.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/engine/ContentController.kt index b775cc64d9a7..e1d08058a098 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/engine/ContentController.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/configdriven/engine/ContentController.kt @@ -56,7 +56,7 @@ interface ContentController { class ContentControllerImpl( private val binding: PreOnboardingDaxDialogCtaBrandDesignUpdateBinding, private val contentValues: ContentValueStore, - private val onContentBound: (LinearOnboardingStepId, ContentConfig) -> Unit, + private val onBeforeContentBound: (LinearOnboardingStepId, ContentConfig) -> Unit, isLightMode: () -> Boolean, isAddressBarRebrandEnabled: () -> Boolean, ) : ContentController { @@ -95,7 +95,7 @@ class ContentControllerImpl( content: ContentConfig, scope: BindScope, ): ContentHandle { - onContentBound(stepId, content) + onBeforeContentBound(stepId, content) val handle = when (content) { is ContentConfig.Welcome -> { boundView = welcome.view diff --git a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModelTest.kt index cb2c97688ae2..51eb26fffd00 100644 --- a/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/onboarding/ui/page/configdriven/ConfigDrivenOnboardingPageViewModelTest.kt @@ -182,7 +182,7 @@ class ConfigDrivenOnboardingPageViewModelTest { private fun bindContent(testee: ConfigDrivenOnboardingPageViewModel) { val dialog = testee.viewState.value.screen as Screen.Dialog - testee.onContentBound(dialog.stepId, dialog.config.content) + testee.onBeforeContentBound(dialog.stepId, dialog.config.content) } private fun importCompleteState(testee: ConfigDrivenOnboardingPageViewModel): MutableStateFlow { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt index 636ebd481cf2..acb079d6edeb 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/CredentialImporter.kt @@ -18,6 +18,7 @@ package com.duckduckgo.autofill.impl.importing import android.os.Parcelable import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished @@ -38,6 +39,7 @@ interface CredentialImporter { suspend fun import( importList: List, originalImportListSize: Int, + source: AutofillImportLaunchSource, ) fun getImportStatus(): Flow @@ -51,6 +53,7 @@ interface CredentialImporter { data class Finished( val savedCredentials: Int, val numberSkipped: Int, + val source: AutofillImportLaunchSource, ) : ImportResult } } @@ -68,15 +71,17 @@ class CredentialImporterImpl @Inject constructor( override suspend fun import( importList: List, originalImportListSize: Int, + source: AutofillImportLaunchSource, ) { appCoroutineScope.launch(dispatchers.io()) { - doImportCredentials(importList, originalImportListSize) + doImportCredentials(importList, originalImportListSize, source) } } private suspend fun doImportCredentials( importList: List, originalImportListSize: Int, + source: AutofillImportLaunchSource, ) { var skippedCredentials = originalImportListSize - importList.size @@ -89,7 +94,7 @@ class CredentialImporterImpl @Inject constructor( // mark that the user has imported passwords at least once, regardless of the number of credentials imported autofillStore.hasEverImportedPasswords = true - _importStatus.emit(Finished(savedCredentials = insertedIds.size, numberSkipped = skippedCredentials)) + _importStatus.emit(Finished(savedCredentials = insertedIds.size, numberSkipped = skippedCredentials, source = source)) } override fun getImportStatus(): Flow = _importStatus diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserver.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserver.kt new file mode 100644 index 000000000000..7ca0ad6b306e --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserver.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 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 androidx.lifecycle.LifecycleOwner +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver +import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Reports the outcome of a successful password import. + * + * The web flow returns as soon as the credentials are handed over, but they are still being written and + * counted after that, so the counts outlive the screen that started the import. Observing app-wide from + * process start keeps this independent of whichever caller launched the flow, and means the replay cache + * is always empty when collection begins — so every import is reported exactly once. + */ +@ContributesMultibinding( + scope = AppScope::class, + boundType = MainProcessLifecycleObserver::class, +) +@SingleInstanceIn(AppScope::class) +class ImportPasswordsResultPixelObserver @Inject constructor( + private val credentialImporter: CredentialImporter, + private val importPasswordsPixelSender: ImportPasswordsPixelSender, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, + private val dispatchers: DispatcherProvider, +) : MainProcessLifecycleObserver { + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + + appCoroutineScope.launch(dispatchers.io()) { + credentialImporter.getImportStatus() + .filterIsInstance() + .collect { + importPasswordsPixelSender.onImportSuccessful( + savedCredentials = it.savedCredentials, + numberSkipped = it.numberSkipped, + source = it.source, + ) + } + } + } +} diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowActivity.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowActivity.kt index d03e698a729c..6885222ba775 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowActivity.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowActivity.kt @@ -21,6 +21,8 @@ import android.os.Bundle import androidx.fragment.app.commit import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.Unknown import com.duckduckgo.autofill.api.AutofillScreens.AutofillImportPasswordsScreen import com.duckduckgo.autofill.impl.R import com.duckduckgo.autofill.impl.databinding.ActivityImportGooglePasswordsWebflowBinding @@ -33,6 +35,7 @@ import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeBucket import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeHandler import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeProvider import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.navigation.api.getActivityParams import javax.inject.Inject @InjectWith(ActivityScope::class) @@ -47,6 +50,9 @@ class ImportGooglePasswordsWebFlowActivity : DuckDuckGoActivity() { val binding: ActivityImportGooglePasswordsWebflowBinding by viewBinding() + private val launchSource: AutofillImportLaunchSource + get() = intent.getActivityParams(AutofillImportPasswordsScreen::class.java)?.source ?: Unknown + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val edgeToEdgeEnabled = edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.WEBVIEW) @@ -69,7 +75,7 @@ class ImportGooglePasswordsWebFlowActivity : DuckDuckGoActivity() { private fun launchImportFragment() { supportFragmentManager.commit { - replace(R.id.fragment_container, ImportGooglePasswordsWebFlowFragment()) + replace(R.id.fragment_container, ImportGooglePasswordsWebFlowFragment.newInstance(launchSource)) } } 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 34ea7418cd8f..eff00701bf14 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 @@ -25,6 +25,7 @@ import android.webkit.WebSettings import android.webkit.WebView import androidx.activity.OnBackPressedCallback import androidx.appcompat.widget.Toolbar +import androidx.core.os.BundleCompat import androidx.fragment.app.setFragmentResult import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.Lifecycle @@ -34,6 +35,8 @@ import androidx.lifecycle.lifecycleScope import androidx.webkit.WebViewCompat import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.autofill.api.AutofillFragmentResultsPlugin +import com.duckduckgo.autofill.api.AutofillImportLaunchSource +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.Unknown import com.duckduckgo.autofill.api.BrowserAutofill import com.duckduckgo.autofill.api.CredentialAutofillDialogFactory import com.duckduckgo.autofill.api.domain.app.LoginCredentials @@ -50,6 +53,7 @@ import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordRe import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.InjectCredentialsFromReauth import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.NoCredentialsAvailable import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.PromptUserToSelectFromStoredCredentials +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Factory import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.Initializing import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.LoadStartPage @@ -68,7 +72,6 @@ import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails import com.duckduckgo.browsermode.api.BrowserMode import com.duckduckgo.common.ui.DuckDuckGoFragment import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.common.utils.FragmentViewModelFactory import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.di.scopes.FragmentScope import com.duckduckgo.user.agent.api.UserAgentProvider @@ -96,7 +99,7 @@ class ImportGooglePasswordsWebFlowFragment : lateinit var dispatchers: DispatcherProvider @Inject - lateinit var viewModelFactory: FragmentViewModelFactory + lateinit var viewModelFactory: Factory @Inject lateinit var credentialAutofillDialogFactory: CredentialAutofillDialogFactory @@ -121,8 +124,14 @@ class ImportGooglePasswordsWebFlowFragment : private var binding: FragmentImportGooglePasswordsWebflowBinding? = null + private val launchSource: AutofillImportLaunchSource + get() = BundleCompat.getParcelable(arguments ?: Bundle(), KEY_LAUNCH_SOURCE, AutofillImportLaunchSource::class.java) ?: Unknown + private val viewModel by lazy { - ViewModelProvider(requireActivity(), viewModelFactory)[ImportGooglePasswordsWebFlowViewModel::class.java] + ViewModelProvider( + requireActivity(), + Factory.Provider(viewModelFactory, launchSource), + )[ImportGooglePasswordsWebFlowViewModel::class.java] } override fun onCreateView( @@ -462,6 +471,13 @@ class ImportGooglePasswordsWebFlowFragment : } companion object { + fun newInstance(source: AutofillImportLaunchSource): ImportGooglePasswordsWebFlowFragment { + return ImportGooglePasswordsWebFlowFragment().apply { + arguments = Bundle().apply { putParcelable(KEY_LAUNCH_SOURCE, source) } + } + } + + private const val KEY_LAUNCH_SOURCE = "launchSource" private const val CUSTOM_FLOW_TAB_ID = "import-passwords-webflow" private const val SELECT_CREDENTIALS_FRAGMENT_TAG = "autofillSelectCredentialsDialog" } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModel.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModel.kt index 9f7a7a2a3db7..e6ff8bc84273 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModel.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModel.kt @@ -18,9 +18,10 @@ package com.duckduckgo.autofill.impl.importing.gpm.webflow import android.os.Parcelable import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType import com.duckduckgo.autofill.impl.importing.CredentialImporter @@ -36,8 +37,11 @@ import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsW import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserCancelledImportFlow import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails import com.duckduckgo.autofill.impl.store.ReauthenticationHandler +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender import com.duckduckgo.common.utils.DispatcherProvider -import com.duckduckgo.di.scopes.FragmentScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -48,10 +52,9 @@ import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import logcat.LogPriority.WARN import logcat.logcat -import javax.inject.Inject -@ContributesViewModel(FragmentScope::class) -class ImportGooglePasswordsWebFlowViewModel @Inject constructor( +class ImportGooglePasswordsWebFlowViewModel @AssistedInject constructor( + @Assisted private val launchSource: AutofillImportLaunchSource, private val dispatchers: DispatcherProvider, private val credentialImporter: CredentialImporter, private val csvCredentialConverter: CsvCredentialConverter, @@ -59,6 +62,7 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor( private val urlToStageMapper: ImportGooglePasswordUrlToStageMapper, private val reauthenticationHandler: ReauthenticationHandler, private val autofillFeature: AutofillFeature, + private val importPasswordsPixelSender: ImportPasswordsPixelSender, ) : ViewModel() { private val _viewState = MutableStateFlow(Initializing) @@ -81,17 +85,19 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor( } private suspend fun onCsvParsed(parseResult: CsvCredentialImportResult.Success) { - credentialImporter.import(parseResult.loginCredentialsToImport, parseResult.numberCredentialsInSource) + credentialImporter.import(parseResult.loginCredentialsToImport, parseResult.numberCredentialsInSource, launchSource) _viewState.value = ViewState.UserFinishedImportFlow } fun onCsvError() { logcat(WARN) { "Error decoding CSV" } + importPasswordsPixelSender.onImportFailed(ErrorParsingCsv, launchSource) _viewState.value = ViewState.UserFinishedCannotImport(ErrorParsingCsv) } fun onWebViewCrash() { logcat(WARN) { "WebView has crashed during password import flow" } + importPasswordsPixelSender.onImportFailed(WebViewCrash, launchSource) _viewState.value = ViewState.UserFinishedCannotImport(WebViewCrash) } @@ -114,7 +120,9 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor( private fun terminateFlowAsCancellation(url: String) { viewModelScope.launch { - _viewState.value = UserCancelledImportFlow(urlToStageMapper.getStage(url)) + val stage = urlToStageMapper.getStage(url) + importPasswordsPixelSender.onUserCancelledImportWebFlow(stage, launchSource) + _viewState.value = UserCancelledImportFlow(stage) } } @@ -251,4 +259,20 @@ class ImportGooglePasswordsWebFlowViewModel @Inject constructor( sealed interface BackButtonAction { data object NavigateBack : BackButtonAction } + + @AssistedFactory + interface Factory { + fun create(launchSource: AutofillImportLaunchSource): ImportGooglePasswordsWebFlowViewModel + + class Provider( + private val assistedFactory: Factory, + private val launchSource: AutofillImportLaunchSource, + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return assistedFactory.create(launchSource) as T + } + } + } } 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 60da9eb62ab9..e9b6e83309b8 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 @@ -32,7 +32,7 @@ import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOO import com.duckduckgo.autofill.impl.pixel.AutofillPixelNames.AUTOFILL_IMPORT_GOOGLE_PASSWORDS_RESULT_SUCCESS 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.di.scopes.FragmentScope +import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -49,7 +49,7 @@ interface ImportPasswordsPixelSender { fun onImportPasswordsViaDesktopSyncOverflowMenuTapped() } -@ContributesBinding(FragmentScope::class) +@ContributesBinding(AppScope::class) class ImportPasswordsPixelSenderImpl @Inject constructor( private val pixel: Pixel, private val engagementBucketing: AutofillEngagementBucketing, 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 44d8477d654e..150127b5b076 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 @@ -133,8 +133,7 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { if (activityResult.resultCode == Activity.RESULT_OK) { lifecycleScope.launch { activityResult.data?.let { data -> - val launchSource = getLaunchSource() - processImportFlowResult(data, launchSource) + processImportFlowResult(data) } } } @@ -143,16 +142,12 @@ class ImportFromGooglePasswordsDialog : BottomSheetDialogFragment() { private fun getLaunchSource() = BundleCompat.getParcelable(arguments ?: Bundle(), KEY_LAUNCH_SOURCE, AutofillImportLaunchSource::class.java) ?: Unknown - private fun ImportFromGooglePasswordsDialog.processImportFlowResult(data: Intent, launchSource: AutofillImportLaunchSource) { + private fun ImportFromGooglePasswordsDialog.processImportFlowResult(data: Intent) { (IntentCompat.getParcelableExtra(data, ImportGooglePasswordResult.RESULT_KEY_DETAILS, ImportGooglePasswordResult::class.java)).let { when (it) { - is ImportGooglePasswordResult.Success -> viewModel.onImportFlowFinishedSuccessfully(launchSource) - is ImportGooglePasswordResult.Error -> viewModel.onImportFlowFinishedWithError(it.reason, launchSource) - is ImportGooglePasswordResult.UserCancelled -> viewModel.onImportFlowCancelledByUser( - it.stage, - canShowPreImportDialog(launchSource), - launchSource, - ) + is ImportGooglePasswordResult.Success -> viewModel.onImportFlowFinishedSuccessfully() + is ImportGooglePasswordResult.Error -> viewModel.onImportFlowFinishedWithError() + is ImportGooglePasswordResult.UserCancelled -> viewModel.onImportFlowCancelledByUser(canShowPreImportDialog(getLaunchSource())) else -> {} } } 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 0408c4ad9ea8..b69ed89a76a5 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 @@ -23,7 +23,6 @@ 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 @@ -46,13 +45,13 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor( private val autofillStore: InternalAutofillStore, ) : ViewModel() { - fun onImportFlowFinishedSuccessfully(importSource: AutofillImportLaunchSource) { + fun onImportFlowFinishedSuccessfully() { viewModelScope.launch(dispatchers.main()) { - observeImportJob(importSource) + observeImportJob() } } - private suspend fun observeImportJob(importSource: AutofillImportLaunchSource) { + private suspend fun observeImportJob() { credentialImporter.getImportStatus().collect { when (it) { is ImportResult.InProgress -> { @@ -62,38 +61,22 @@ class ImportFromGooglePasswordsDialogViewModel @Inject constructor( is ImportResult.Finished -> { logcat { "Import finished: ${it.savedCredentials} imported. ${it.numberSkipped} skipped." } - fireImportSuccessPixel(savedCredentials = it.savedCredentials, numberSkipped = it.numberSkipped, importSource = importSource) _viewState.value = ViewState(viewMode = ViewMode.ImportSuccess(it)) } } } } - fun onImportFlowFinishedWithError(reason: UserCannotImportReason, importSource: AutofillImportLaunchSource) { - fireImportFailedPixel(reason, importSource) + fun onImportFlowFinishedWithError() { _viewState.value = ViewState(viewMode = ViewMode.ImportError) } - fun onImportFlowCancelledByUser(stage: String, canShowPreImportDialog: Boolean, importSource: AutofillImportLaunchSource) { - importPasswordsPixelSender.onUserCancelledImportWebFlow(stage, importSource) - + fun onImportFlowCancelledByUser(canShowPreImportDialog: Boolean) { if (!canShowPreImportDialog) { _viewState.value = ViewState(viewMode = ViewMode.FlowTerminated) } } - private fun fireImportSuccessPixel(savedCredentials: Int, numberSkipped: Int, importSource: AutofillImportLaunchSource) { - importPasswordsPixelSender.onImportSuccessful( - savedCredentials = savedCredentials, - numberSkipped = numberSkipped, - source = importSource, - ) - } - - private fun fireImportFailedPixel(reason: UserCannotImportReason, importSource: AutofillImportLaunchSource) { - importPasswordsPixelSender.onImportFailed(reason, importSource) - } - fun shouldShowInitialInstructionalPrompt(importSource: AutofillImportLaunchSource) { val viewMode = if (importSource == AutofillImportLaunchSource.InBrowserPromo) { logcat { "ImportFromGooglePasswordsDialogViewModel: InBrowserPromo scenario" } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/CredentialImporterImplTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/CredentialImporterImplTest.kt index 2ec0cd47c4c1..9061840a1702 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/CredentialImporterImplTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/CredentialImporterImplTest.kt @@ -1,6 +1,7 @@ package com.duckduckgo.autofill.impl.importing import app.cash.turbine.test +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.PasswordManagementEmptyState import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult import com.duckduckgo.autofill.impl.store.InternalAutofillStore @@ -132,7 +133,7 @@ class CredentialImporterImplTest { } private suspend fun List.import(originalListSize: Int = this.size) { - testee.import(this, originalListSize) + testee.import(this, originalListSize, PasswordManagementEmptyState) } private suspend fun assertResult( @@ -143,6 +144,7 @@ class CredentialImporterImplTest { with(awaitItem() as ImportResult.Finished) { assertEquals("Wrong number of duplicates in result", numberSkippedExpected, numberSkipped) assertEquals("Wrong import size in result", importListSizeExpected, savedCredentials) + assertEquals("Wrong launch source in result", PasswordManagementEmptyState, source) } } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserverTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserverTest.kt new file mode 100644 index 000000000000..a7d2b152193c --- /dev/null +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/ImportPasswordsResultPixelObserverTest.kt @@ -0,0 +1,59 @@ +package com.duckduckgo.autofill.impl.importing + +import androidx.lifecycle.LifecycleOwner +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.Onboarding +import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.Finished +import com.duckduckgo.autofill.impl.importing.CredentialImporter.ImportResult.InProgress +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +class ImportPasswordsResultPixelObserverTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + + private val credentialImporter: CredentialImporter = mock() + private val importPasswordsPixelSender: ImportPasswordsPixelSender = mock() + private val lifecycleOwner: LifecycleOwner = mock() + + private val testee = ImportPasswordsResultPixelObserver( + credentialImporter = credentialImporter, + importPasswordsPixelSender = importPasswordsPixelSender, + appCoroutineScope = coroutineTestRule.testScope, + dispatchers = coroutineTestRule.testDispatcherProvider, + ) + + @Test + fun whenNoImportStatusThenNoPixelSent() = runTest { + whenever(credentialImporter.getImportStatus()).thenReturn(emptyFlow()) + testee.onCreate(lifecycleOwner) + verifyNoInteractions(importPasswordsPixelSender) + } + + @Test + fun whenImportStillInProgressThenNoPixelSent() = runTest { + whenever(credentialImporter.getImportStatus()).thenReturn(listOf(InProgress).asFlow()) + testee.onCreate(lifecycleOwner) + verifyNoInteractions(importPasswordsPixelSender) + } + + @Test + fun whenImportFinishedThenSuccessPixelSentWithCountsAndLaunchSource() = runTest { + whenever(credentialImporter.getImportStatus()).thenReturn( + listOf(InProgress, Finished(savedCredentials = 10, numberSkipped = 2, source = Onboarding)).asFlow(), + ) + + testee.onCreate(lifecycleOwner) + + verify(importPasswordsPixelSender).onImportSuccessful(savedCredentials = 10, numberSkipped = 2, source = Onboarding) + } +} diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModelTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModelTest.kt index 81e323930e34..fcc0dcb93e26 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModelTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/importing/gpm/webflow/ImportGooglePasswordsWebFlowViewModelTest.kt @@ -3,6 +3,7 @@ package com.duckduckgo.autofill.impl.importing.gpm.webflow import androidx.test.ext.junit.runners.AndroidJUnit4 import app.cash.turbine.test import com.duckduckgo.autofill.api.AutofillFeature +import com.duckduckgo.autofill.api.AutofillImportLaunchSource.Onboarding import com.duckduckgo.autofill.api.domain.app.LoginCredentials import com.duckduckgo.autofill.api.domain.app.LoginTriggerType.AUTOPROMPT import com.duckduckgo.autofill.api.domain.app.LoginTriggerType.USER_INITIATED @@ -15,6 +16,7 @@ import com.duckduckgo.autofill.impl.importing.gpm.feature.AutofillImportPassword import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.InjectCredentialsFromReauth import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.NoCredentialsAvailable import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.Command.PromptUserToSelectFromStoredCredentials +import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason.ErrorParsingCsv import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.UserCannotImportReason.WebViewCrash import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.LoadStartPage import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.NavigatingBack @@ -23,6 +25,7 @@ import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsW import com.duckduckgo.autofill.impl.importing.gpm.webflow.ImportGooglePasswordsWebFlowViewModel.ViewState.UserFinishedImportFlow import com.duckduckgo.autofill.impl.store.ReAuthenticationDetails import com.duckduckgo.autofill.impl.store.ReauthenticationHandler +import com.duckduckgo.autofill.impl.ui.credential.management.importpassword.ImportPasswordsPixelSender import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.Toggle import kotlinx.coroutines.test.runTest @@ -49,8 +52,10 @@ class ImportGooglePasswordsWebFlowViewModelTest { private val urlToStageMapper: ImportGooglePasswordUrlToStageMapper = mock() private val reauthenticationHandler: ReauthenticationHandler = mock() private val autofillFeature: AutofillFeature = mock() + private val importPasswordsPixelSender: ImportPasswordsPixelSender = mock() private val testee = ImportGooglePasswordsWebFlowViewModel( + launchSource = Onboarding, dispatchers = coroutineTestRule.testDispatcherProvider, credentialImporter = credentialImporter, csvCredentialConverter = csvCredentialConverter, @@ -58,6 +63,7 @@ class ImportGooglePasswordsWebFlowViewModelTest { urlToStageMapper = urlToStageMapper, reauthenticationHandler = reauthenticationHandler, autofillFeature = autofillFeature, + importPasswordsPixelSender = importPasswordsPixelSender, ) @Test @@ -329,6 +335,54 @@ class ImportGooglePasswordsWebFlowViewModelTest { } } + @Test + fun whenCloseButtonPressedThenUserCancelledPixelSentWithStageAndLaunchSource() = runTest { + val expectedStage = "stage" + whenever(urlToStageMapper.getStage(any())).thenReturn(expectedStage) + testee.onCloseButtonPressed("https://example.com") + + verify(importPasswordsPixelSender).onUserCancelledImportWebFlow(expectedStage, Onboarding) + } + + @Test + fun whenBackButtonPressedAndCannotGoBackThenUserCancelledPixelSent() = runTest { + val expectedStage = "stage" + whenever(urlToStageMapper.getStage(any())).thenReturn(expectedStage) + testee.onBackButtonPressed(url = "https://example.com", canGoBack = false) + + verify(importPasswordsPixelSender).onUserCancelledImportWebFlow(expectedStage, Onboarding) + } + + @Test + fun whenBackButtonPressedAndCanGoBackThenNoUserCancelledPixelSent() = runTest { + testee.onBackButtonPressed(url = "https://example.com", canGoBack = true) + + verify(importPasswordsPixelSender, never()).onUserCancelledImportWebFlow(any(), any()) + } + + @Test + fun whenCsvParseErrorThenImportFailedPixelSent() = runTest { + configureCsvParseError() + + verify(importPasswordsPixelSender).onImportFailed(ErrorParsingCsv, Onboarding) + } + + @Test + fun whenWebViewCrashesThenImportFailedPixelSent() = runTest { + testee.onWebViewCrash() + + verify(importPasswordsPixelSender).onImportFailed(WebViewCrash, Onboarding) + } + + @Test + fun whenCsvParsedThenImportStartedWithLaunchSource() = runTest { + val credentials = listOf(creds()) + + configureCsvSuccess(loginCredentialsToImport = credentials) + + verify(credentialImporter).import(credentials, credentials.size, Onboarding) + } + private fun configureReAuthenticationFeatureFlagEnabled() { val mockToggle: Toggle = mock() whenever(mockToggle.isEnabled()).thenReturn(true) 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 80b50636cf57..44f15c513b9a 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 @@ -7,7 +7,6 @@ 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 @@ -55,7 +54,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { @Test fun whenParsingErrorOnImportThenViewModeUpdatedToError() = runTest { - testee.onImportFlowFinishedWithError(reason = ErrorParsingCsv, importSource = TEST_SOURCE) + testee.onImportFlowFinishedWithError() testee.viewState.test { assertTrue(awaitItem().viewMode is ViewMode.ImportError) } @@ -65,7 +64,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { fun whenSuccessfulImportThenViewModeUpdatedToInProgress() = runTest { configureImportInProgress() testee.shouldShowInitialInstructionalPrompt(importSource = TEST_SOURCE) - testee.onImportFlowFinishedSuccessfully(importSource = TEST_SOURCE) + testee.onImportFlowFinishedSuccessfully() testee.viewState.test { awaitImportInProgress() } @@ -75,7 +74,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { fun whenSuccessfulImportFlowThenImportFinishesNothingImportedThenViewModeUpdatedToResults() = runTest { configureImportFinished(savedCredentials = 0, numberSkipped = 0) testee.shouldShowInitialInstructionalPrompt(importSource = TEST_SOURCE) - testee.onImportFlowFinishedSuccessfully(importSource = TEST_SOURCE) + testee.onImportFlowFinishedSuccessfully() testee.viewState.test { awaitImportSuccess() } @@ -85,7 +84,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { fun whenSuccessfulImportFlowThenImportFinishesCredentialsImportedNoDuplicatesThenViewModeUpdatedToResults() = runTest { configureImportFinished(savedCredentials = 10, numberSkipped = 0) testee.shouldShowInitialInstructionalPrompt(importSource = TEST_SOURCE) - testee.onImportFlowFinishedSuccessfully(importSource = TEST_SOURCE) + testee.onImportFlowFinishedSuccessfully() testee.viewState.test { val result = awaitImportSuccess() assertEquals(10, result.importResult.savedCredentials) @@ -97,7 +96,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { fun whenSuccessfulImportFlowThenImportFinishesOnlyDuplicatesThenViewModeUpdatedToResults() = runTest { configureImportFinished(savedCredentials = 0, numberSkipped = 2) testee.shouldShowInitialInstructionalPrompt(importSource = TEST_SOURCE) - testee.onImportFlowFinishedSuccessfully(importSource = TEST_SOURCE) + testee.onImportFlowFinishedSuccessfully() testee.viewState.test { val result = awaitImportSuccess() assertEquals(0, result.importResult.savedCredentials) @@ -108,7 +107,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { @Test fun whenSuccessfulImportNoUpdatesThenThenViewModeFirstInitialisedToPreImport() = runTest { testee.shouldShowInitialInstructionalPrompt(importSource = TEST_SOURCE) - testee.onImportFlowFinishedSuccessfully(importSource = TEST_SOURCE) + testee.onImportFlowFinishedSuccessfully() testee.viewState.test { awaitItem().assertIsPreImport() } @@ -154,7 +153,7 @@ class ImportFromGooglePasswordsDialogViewModelTest { whenever(credentialImporter.getImportStatus()).thenReturn( listOf( InProgress, - Finished(savedCredentials = savedCredentials, numberSkipped = numberSkipped), + Finished(savedCredentials = savedCredentials, numberSkipped = numberSkipped, source = TEST_SOURCE), ).asFlow(), ) } 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 447ad71d8f00..301c1192bbfc 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 @@ -168,6 +168,7 @@ class AutofillInternalSettingsActivity : DuckDuckGoActivity() { credentialImporter.import( parseResult.loginCredentialsToImport, parseResult.numberCredentialsInSource, + Unknown, ) observePasswordInputUpdates() }