From b3033028e8d189ae3755fc1b4b9174964ed0de69 Mon Sep 17 00:00:00 2001 From: Andre Rosado Date: Wed, 17 Jun 2026 19:33:00 +0100 Subject: [PATCH] Add fill assist option to the setting --- .../data/autofill/di/FillAssistModule.kt | 3 ++ .../autofill/manager/FillAssistManagerImpl.kt | 15 ++++-- .../autofill/parser/AutofillParserImpl.kt | 5 +- .../datasource/disk/SettingsDiskSource.kt | 10 ++++ .../datasource/disk/SettingsDiskSourceImpl.kt | 11 +++++ .../platform/repository/SettingsRepository.kt | 5 ++ .../repository/SettingsRepositoryImpl.kt | 14 +++++- .../settings/autofill/AutoFillScreen.kt | 29 +++++++++++ .../settings/autofill/AutoFillViewModel.kt | 43 +++++++++++++++- .../autofill/handlers/AutoFillHandlers.kt | 8 +++ .../autofill/manager/FillAssistManagerTest.kt | 16 ++++++ .../disk/util/FakeSettingsDiskSource.kt | 8 +++ .../settings/autofill/AutoFillScreenTest.kt | 49 +++++++++++++++++++ .../autofill/AutoFillViewModelTest.kt | 49 +++++++++++++++++++ ui/src/main/res/values/strings.xml | 2 + 15 files changed, 261 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/di/FillAssistModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/di/FillAssistModule.kt index e8480610b20..7a08c3ec2e3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/di/FillAssistModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/di/FillAssistModule.kt @@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager import com.x8bit.bitwarden.data.autofill.manager.FillAssistManagerImpl import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager +import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,6 +45,7 @@ object FillAssistModule { fillAssistDiskSource: FillAssistDiskSource, featureFlagManager: FeatureFlagManager, serverConfigRepository: ServerConfigRepository, + settingsRepository: SettingsRepository, environmentDiskSource: EnvironmentDiskSource, clock: Clock, dispatcherManager: DispatcherManager, @@ -53,6 +55,7 @@ object FillAssistModule { fillAssistDiskSource = fillAssistDiskSource, featureFlagManager = featureFlagManager, serverConfigRepository = serverConfigRepository, + settingsRepository = settingsRepository, environmentDiskSource = environmentDiskSource, clock = clock, dispatcherManager = dispatcherManager, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerImpl.kt index 24ed13b8a63..ffc4733fead 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerImpl.kt @@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.autofill.model.FillAssistRules import com.x8bit.bitwarden.data.autofill.model.FillAssistRules.SelectorClause import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager +import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterNotNull @@ -57,6 +58,7 @@ class FillAssistManagerImpl( private val fillAssistDiskSource: FillAssistDiskSource, private val featureFlagManager: FeatureFlagManager, private val serverConfigRepository: ServerConfigRepository, + private val settingsRepository: SettingsRepository, private val environmentDiskSource: EnvironmentDiskSource, private val clock: Clock, dispatcherManager: DispatcherManager, @@ -78,7 +80,11 @@ class FillAssistManagerImpl( } override fun syncIfNecessary() { - if (!featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules)) return + if (!featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) || + !settingsRepository.isFillAssistEnabled + ) { + return + } val serverUrl = serverConfigRepository .serverConfigStateFlow .value @@ -87,8 +93,11 @@ class FillAssistManagerImpl( ?.fillAssistRulesUrl ?: return val lastFetch = fillAssistDiskSource.getLastFetchTimestamp(serverUrl) ?: 0L - if (clock.millis() - lastFetch < UPDATE_INTERVAL_MS) return - if (!syncJob.isCompleted) return + if (clock.millis() - lastFetch < UPDATE_INTERVAL_MS || + !syncJob.isCompleted + ) { + return + } syncJob = ioScope.launch { sync(serverUrl) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt index 60b33f11df4..0d6622ab25d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/parser/AutofillParserImpl.kt @@ -150,7 +150,10 @@ class AutofillParserImpl( // over heuristics; unmatched view nodes are excluded entirely (no heuristic fallback). val fillAssistHostRules = uri ?.takeUnless { it.startsWith("androidapp://") }?.toUri()?.host - ?.takeIf { featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) } + ?.takeIf { + featureFlagManager.getFeatureFlag(FlagKey.FillAssistTargetingRules) && + settingsRepository.isFillAssistEnabled + } ?.let { host -> fillAssistManager.getFillAssistRules()?.hostRules?.get(host.removePrefix("www.")) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index cbaf366a296..adfd188efc9 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -357,6 +357,16 @@ interface SettingsDiskSource : FlightRecorderDiskSource { */ fun storeInlineAutofillEnabled(userId: String, isInlineAutofillEnabled: Boolean?) + /** + * Gets the value determining if fill assist is enabled for the given [userId]. + */ + fun getFillAssistEnabled(userId: String): Boolean? + + /** + * Stores the given [isFillAssistEnabled] value for the given [userId]. + */ + fun storeFillAssistEnabled(userId: String, isFillAssistEnabled: Boolean?) + /** * Gets a list of blocked autofill URI's for the given [userId]. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index 7d67a378105..18c4ffba3fe 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -21,6 +21,7 @@ private const val APP_LANGUAGE_KEY = "appLocale" private const val APP_THEME_KEY = "theme" private const val PULL_TO_REFRESH_KEY = "syncOnRefresh" private const val INLINE_AUTOFILL_ENABLED_KEY = "inlineAutofillEnabled" +private const val FILL_ASSIST_ENABLED_KEY = "fillAssistEnabled" private const val BLOCKED_AUTOFILL_URIS_KEY = "autofillBlacklistedUris" private const val VAULT_LAST_SYNC_TIME = "vaultLastSyncTime" private const val VAULT_TIMEOUT_ACTION_KEY = "vaultTimeoutAction" @@ -543,6 +544,16 @@ class SettingsDiskSourceImpl( ) } + override fun getFillAssistEnabled(userId: String): Boolean? = + getBoolean(key = FILL_ASSIST_ENABLED_KEY.appendIdentifier(userId)) + + override fun storeFillAssistEnabled(userId: String, isFillAssistEnabled: Boolean?) { + putBoolean( + key = FILL_ASSIST_ENABLED_KEY.appendIdentifier(userId), + value = isFillAssistEnabled, + ) + } + override fun getBlockedAutofillUris(userId: String): List? = getString(key = BLOCKED_AUTOFILL_URIS_KEY.appendIdentifier(userId))?.let { json.decodeFromStringOrNull(it) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 67a7a9f7961..2511de6393b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -145,6 +145,11 @@ interface SettingsRepository : FlightRecorderManager { */ var isInlineAutofillEnabled: Boolean + /** + * Whether fill assist is enabled for the current user. + */ + var isFillAssistEnabled: Boolean + /** * Whether the auto copying totp when autofilling is disabled for the current user. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index 40a076a8a8c..33e610830be 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -46,7 +46,7 @@ private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG /** * Primary implementation of [SettingsRepository]. */ -@Suppress("TooManyFunctions", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") class SettingsRepositoryImpl( private val autofillManager: AutofillManager, private val autofillEnabledManager: AutofillEnabledManager, @@ -314,6 +314,18 @@ class SettingsRepositoryImpl( ) } + override var isFillAssistEnabled: Boolean + get() = activeUserId + ?.let { settingsDiskSource.getFillAssistEnabled(userId = it) } + ?: false + set(value) { + val userId = activeUserId ?: return + settingsDiskSource.storeFillAssistEnabled( + userId = userId, + isFillAssistEnabled = value, + ) + } + override var isAutoCopyTotpDisabled: Boolean get() = activeUserId ?.let { settingsDiskSource.getAutoCopyTotpDisabled(userId = it) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 816e4de0a37..c4f9d465eb8 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -130,6 +130,12 @@ fun AutoFillScreen( onNavigateToPrivilegedAppsList() } + AutoFillEvent.NavigateToFillAssistHelp -> { + intentManager.launchUri( + "https://bitwarden.com/help/fill-assist/".toUri(), + ) + } + AutoFillEvent.NavigateToLearnMore -> { intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri()) } @@ -301,6 +307,29 @@ private fun AutoFillScreenContent( .standardHorizontalMargin() .padding(horizontal = 16.dp), ) + if (state.showFillAssistOption) { + Spacer(modifier = Modifier.height(8.dp)) + BitwardenSwitch( + label = stringResource(id = BitwardenString.turn_on_fill_assist), + supportingText = stringResource( + id = BitwardenString.fill_assist_improves_autofill_accuracy_on_supported_sites, + ), + isChecked = state.isFillAssistEnabled, + onCheckedChange = autoFillHandlers.onFillAssistToggleClick, + helpData = BitwardenHelpButtonData( + onClick = autoFillHandlers.onFillAssistInfoClick, + contentDescription = stringResource( + id = BitwardenString.learn_more, + ), + isExternalLink = true, + ), + cardStyle = CardStyle.Full, + modifier = Modifier + .fillMaxWidth() + .testTag("FillAssistSwitch") + .standardHorizontalMargin(), + ) + } Spacer(modifier = Modifier.height(8.dp)) BitwardenSwitch( label = stringResource(id = BitwardenString.copy_totp_automatically), diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index 16534223047..be6167c2d47 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -4,15 +4,18 @@ import android.os.Build import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -33,11 +36,13 @@ private const val KEY_STATE = "state" /** * View model for the auto-fill screen. */ -@Suppress("TooManyFunctions") +@Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel class AutoFillViewModel @Inject constructor( authRepository: AuthRepository, browserThirdPartyAutofillEnabledManager: BrowserThirdPartyAutofillEnabledManager, + featureFlagManager: FeatureFlagManager, + private val fillAssistManager: FillAssistManager, private val savedStateHandle: SavedStateHandle, private val settingsRepository: SettingsRepository, private val firstTimeActionManager: FirstTimeActionManager, @@ -47,6 +52,9 @@ class AutoFillViewModel @Inject constructor( val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState AutoFillState( + showFillAssistOption = featureFlagManager + .getFeatureFlag(FlagKey.FillAssistTargetingRules), + isFillAssistEnabled = settingsRepository.isFillAssistEnabled, isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled, isAccessibilityAutofillEnabled = settingsRepository .isAccessibilityEnabledStateFlow @@ -109,6 +117,8 @@ class AutoFillViewModel @Inject constructor( } override fun handleAction(action: AutoFillAction) = when (action) { + is AutoFillAction.FillAssistToggleClick -> handleFillAssistToggleClick(action) + AutoFillAction.FillAssistInfoClick -> handleFillAssistInfoClick() is AutoFillAction.AskToAddLoginClick -> handleAskToAddLoginClick(action) is AutoFillAction.AutoFillServicesClick -> handleAutoFillServicesClick(action) AutoFillAction.BackClick -> handleBackClick() @@ -215,6 +225,18 @@ class AutoFillViewModel @Inject constructor( } } + private fun handleFillAssistToggleClick(action: AutoFillAction.FillAssistToggleClick) { + settingsRepository.isFillAssistEnabled = action.isEnabled + mutableStateFlow.update { it.copy(isFillAssistEnabled = action.isEnabled) } + if (action.isEnabled) { + fillAssistManager.syncIfNecessary() + } + } + + private fun handleFillAssistInfoClick() { + sendEvent(AutoFillEvent.NavigateToFillAssistHelp) + } + private fun handleAskToAddLoginClick(action: AutoFillAction.AskToAddLoginClick) { settingsRepository.isAutofillSavePromptDisabled = !action.isEnabled mutableStateFlow.update { it.copy(isAskToAddLoginEnabled = action.isEnabled) } @@ -293,6 +315,8 @@ class AutoFillViewModel @Inject constructor( */ @Parcelize data class AutoFillState( + val showFillAssistOption: Boolean, + val isFillAssistEnabled: Boolean, val isAskToAddLoginEnabled: Boolean, val isAccessibilityAutofillEnabled: Boolean, val isAutoFillServicesEnabled: Boolean, @@ -422,12 +446,29 @@ sealed class AutoFillEvent { * Navigate to the autofill help page. */ data object NavigateToAutofillHelp : AutoFillEvent() + + /** + * Navigate to the fill assist help page. + */ + data object NavigateToFillAssistHelp : AutoFillEvent() } /** * Models actions for the auto-fill screen. */ sealed class AutoFillAction { + /** + * User toggled the fill assist switch. + */ + data class FillAssistToggleClick( + val isEnabled: Boolean, + ) : AutoFillAction() + + /** + * User clicked the fill assist info icon. + */ + data object FillAssistInfoClick : AutoFillAction() + /** * User clicked ask to add login button. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt index c305164006e..f88ae51b6df 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/handlers/AutoFillHandlers.kt @@ -27,6 +27,8 @@ class AutoFillHandlers( val onAskToAddLoginClick: (isEnabled: Boolean) -> Unit, val onDefaultUriMatchTypeSelect: (defaultUriMatchType: UriMatchType) -> Unit, val onBlockAutoFillClick: () -> Unit, + val onFillAssistToggleClick: (isEnabled: Boolean) -> Unit, + val onFillAssistInfoClick: () -> Unit, val onLearnMoreClick: () -> Unit, val onHelpCardClick: () -> Unit, ) { @@ -82,6 +84,12 @@ class AutoFillHandlers( viewModel.trySendAction(AutoFillAction.DefaultUriMatchTypeSelect(it)) }, onBlockAutoFillClick = { viewModel.trySendAction(AutoFillAction.BlockAutoFillClick) }, + onFillAssistToggleClick = { + viewModel.trySendAction(AutoFillAction.FillAssistToggleClick(it)) + }, + onFillAssistInfoClick = { + viewModel.trySendAction(AutoFillAction.FillAssistInfoClick) + }, onLearnMoreClick = { viewModel.trySendAction(AutoFillAction.LearnMoreClick) }, onHelpCardClick = { viewModel.trySendAction(AutoFillAction.HelpCardClick) }, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerTest.kt index f530a1044b4..56a340b6f05 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/autofill/manager/FillAssistManagerTest.kt @@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.autofill.datasource.disk.FillAssistDiskSource import com.x8bit.bitwarden.data.autofill.model.FillAssistRules import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager +import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify @@ -75,11 +76,16 @@ class FillAssistManagerTest { every { fillAssistRulesUrl = any() } just runs } + private val settingsRepository: SettingsRepository = mockk { + every { isFillAssistEnabled } returns true + } + private val manager = FillAssistManagerImpl( fillAssistService = fillAssistService, fillAssistDiskSource = fillAssistDiskSource, featureFlagManager = featureFlagManager, serverConfigRepository = serverConfigRepository, + settingsRepository = settingsRepository, environmentDiskSource = environmentDiskSource, clock = FIXED_CLOCK, dispatcherManager = FakeDispatcherManager(), @@ -114,6 +120,16 @@ class FillAssistManagerTest { verify(exactly = 0) { fillAssistDiskSource.storeFillAssistRules(any(), any()) } } + @Test + fun `sync returns success and does nothing when fill assist is disabled by user`() = runTest { + every { settingsRepository.isFillAssistEnabled } returns false + + manager.syncIfNecessary() + + coVerify(exactly = 0) { fillAssistService.getManifest() } + verify(exactly = 0) { fillAssistDiskSource.storeFillAssistRules(any(), any()) } + } + @Test fun `sync returns success and does nothing when fillAssistRulesUrl is null`() = runTest { serverConfigFlow.value = null diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 48f992c9051..a8cc978270b 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -72,6 +72,7 @@ class FakeSettingsDiskSource( private val storedUpgradedToPremiumCardPending = mutableMapOf() private val storedPremiumUpgradePending = mutableMapOf() private val storedInlineAutofillEnabled = mutableMapOf() + private val storedFillAssistEnabled = mutableMapOf() private val storedBlockedAutofillUris = mutableMapOf?>() private var storedIsIconLoadingDisabled: Boolean? = null private var storedIsCrashLoggingEnabled: Boolean? = null @@ -430,6 +431,13 @@ class FakeSettingsDiskSource( storedInlineAutofillEnabled[userId] = isInlineAutofillEnabled } + override fun getFillAssistEnabled(userId: String): Boolean? = + storedFillAssistEnabled[userId] + + override fun storeFillAssistEnabled(userId: String, isFillAssistEnabled: Boolean?) { + storedFillAssistEnabled[userId] = isFillAssistEnabled + } + override fun getBlockedAutofillUris(userId: String): List? = storedBlockedAutofillUris[userId] diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index 95a3db3019c..dc1852fc0e5 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo @@ -866,9 +867,57 @@ class AutoFillScreenTest : BitwardenComposeTest() { intentManager.launchUri("https://bitwarden.com/help/uri-match-detection/".toUri()) } } + + @Test + fun `fill assist switch should not be displayed when showFillAssistOption is false`() { + mutableStateFlow.update { it.copy(showFillAssistOption = false) } + composeTestRule + .onNodeWithTag("FillAssistSwitch") + .assertDoesNotExist() + } + + @Test + fun `fill assist switch should be displayed when showFillAssistOption is true`() { + mutableStateFlow.update { it.copy(showFillAssistOption = true) } + composeTestRule + .onNodeWithTag("FillAssistSwitch") + .performScrollTo() + .assertIsDisplayed() + } + + @Test + fun `fill assist switch toggles should send FillAssistToggleClick action`() { + mutableStateFlow.update { + it.copy( + showFillAssistOption = true, + isFillAssistEnabled = false, + ) + } + composeTestRule + .onNodeWithTag("FillAssistSwitch") + .performScrollTo() + .performClick() + verify { + viewModel.trySendAction( + AutoFillAction.FillAssistToggleClick( + isEnabled = true, + ), + ) + } + } + + @Test + fun `on NavigateToFillAssistHelp should call launchUri`() { + mutableEventFlow.tryEmit(AutoFillEvent.NavigateToFillAssistHelp) + verify(exactly = 1) { + intentManager.launchUri("https://bitwarden.com/help/fill-assist/".toUri()) + } + } } private val DEFAULT_STATE: AutoFillState = AutoFillState( + showFillAssistOption = false, + isFillAssistEnabled = false, isAskToAddLoginEnabled = false, isAccessibilityAutofillEnabled = false, isAutoFillServicesEnabled = false, diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt index 3cbf9ceb456..419406c3dad 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt @@ -5,11 +5,14 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.util.isBuildVersionAtLeast import com.bitwarden.ui.platform.base.BaseViewModelTest +import com.bitwarden.core.data.manager.model.FlagKey import com.x8bit.bitwarden.data.auth.repository.AuthRepository +import com.x8bit.bitwarden.data.autofill.manager.FillAssistManager import com.x8bit.bitwarden.data.autofill.manager.browser.BrowserThirdPartyAutofillEnabledManager import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutoFillData import com.x8bit.bitwarden.data.autofill.model.browser.BrowserThirdPartyAutofillStatus +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -55,6 +58,12 @@ class AutoFillViewModelTest : BaseViewModelTest() { every { browserThirdPartyAutofillStatus } returns DEFAULT_AUTOFILL_STATUS } + private val featureFlagManager: FeatureFlagManager = mockk { + every { getFeatureFlag(FlagKey.FillAssistTargetingRules) } returns false + } + + private val fillAssistManager: FillAssistManager = mockk(relaxed = true) + private val settingsRepository: SettingsRepository = mockk { every { isInlineAutofillEnabled } returns true every { isInlineAutofillEnabled = any() } just runs @@ -62,6 +71,8 @@ class AutoFillViewModelTest : BaseViewModelTest() { every { isAutoCopyTotpDisabled = any() } just runs every { isAutofillSavePromptDisabled } returns true every { isAutofillSavePromptDisabled = any() } just runs + every { isFillAssistEnabled } returns false + every { isFillAssistEnabled = any() } just runs every { defaultUriMatchType } returns UriMatchType.DOMAIN every { defaultUriMatchType = any() } just runs every { isAccessibilityEnabledStateFlow } returns mutableIsAccessibilityEnabledStateFlow @@ -512,6 +523,40 @@ class AutoFillViewModelTest : BaseViewModelTest() { } } + @Test + fun `FillAssistToggleClick with isEnabled true persists setting and triggers sync`() = runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(AutoFillAction.FillAssistToggleClick(isEnabled = true)) + assertEquals(DEFAULT_STATE.copy(isFillAssistEnabled = true), awaitItem()) + } + verify { settingsRepository.isFillAssistEnabled = true } + verify { fillAssistManager.syncIfNecessary() } + } + + @Test + fun `FillAssistToggleClick with isEnabled false persists setting and skips sync`() = runTest { + val enabledState = DEFAULT_STATE.copy(isFillAssistEnabled = true) + val viewModel = createViewModel(state = enabledState) + viewModel.stateFlow.test { + assertEquals(enabledState, awaitItem()) + viewModel.trySendAction(AutoFillAction.FillAssistToggleClick(isEnabled = false)) + assertEquals(enabledState.copy(isFillAssistEnabled = false), awaitItem()) + } + verify { settingsRepository.isFillAssistEnabled = false } + verify(exactly = 0) { fillAssistManager.syncIfNecessary() } + } + + @Test + fun `FillAssistInfoClick emits NavigateToFillAssistHelp event`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(AutoFillAction.FillAssistInfoClick) + assertEquals(AutoFillEvent.NavigateToFillAssistHelp, awaitItem()) + } + } + private fun createViewModel( state: AutoFillState? = DEFAULT_STATE, ): AutoFillViewModel = AutoFillViewModel( @@ -520,10 +565,14 @@ class AutoFillViewModelTest : BaseViewModelTest() { authRepository = authRepository, firstTimeActionManager = firstTimeActionManager, browserThirdPartyAutofillEnabledManager = browserThirdPartyAutofillEnabledManager, + featureFlagManager = featureFlagManager, + fillAssistManager = fillAssistManager, ) } private val DEFAULT_STATE: AutoFillState = AutoFillState( + showFillAssistOption = false, + isFillAssistEnabled = false, isAskToAddLoginEnabled = false, isAccessibilityAutofillEnabled = false, isAutoFillServicesEnabled = false, diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 937f2b27f97..f9d66c55f2f 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -380,6 +380,8 @@ Scanning will happen automatically. Do you want to require unlocking with your master password when the application is restarted? Ask to add item Ask to add an item if one isn’t found in your vault. + Turn on fill assist + Fill assist improves autofill accuracy on supported sites by using site specific rules. On app restart Capitalize Include number