diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt index dc0955310f..2d3a954bb5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSource.kt @@ -106,6 +106,20 @@ interface SettingsDiskSource { */ fun storeDefaultUriMatchType(userId: String, uriMatchType: UriMatchType?) + /** + * Gets the value for whether or not the autofill save prompt should be disabled for the + * given [userId]. + */ + fun getAutofillSavePromptDisabled(userId: String): Boolean? + + /** + * Stores the given [isAutofillSavePromptDisabled] for the given [userId]. + */ + fun storeAutofillSavePromptDisabled( + userId: String, + isAutofillSavePromptDisabled: Boolean?, + ) + /** * Gets the current state of the pull to refresh feature for the given [userId]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index e0a32ea3e5..a829c288a5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -23,6 +23,7 @@ private const val VAULT_LAST_SYNC_TIME = "$BASE_KEY:vaultLastSyncTime" private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction" private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout" private const val DEFAULT_URI_MATCH_TYPE_KEY = "$BASE_KEY:defaultUriMatch" +private const val DISABLE_AUTOFILL_SAVE_PROMPT_KEY = "$BASE_KEY:autofillDisableSavePrompt" private const val DISABLE_ICON_LOADING_KEY = "$BASE_KEY:disableFavicon" private const val APPROVE_PASSWORDLESS_LOGINS_KEY = "$BASE_KEY:approvePasswordlessLogins" @@ -97,6 +98,7 @@ class SettingsDiskSourceImpl( storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null) storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null) storeDefaultUriMatchType(userId = userId, uriMatchType = null) + storeAutofillSavePromptDisabled(userId = userId, isAutofillSavePromptDisabled = null) storePullToRefreshEnabled(userId = userId, isPullToRefreshEnabled = null) storeInlineAutofillEnabled(userId = userId, isInlineAutofillEnabled = null) storeBlockedAutofillUris(userId = userId, blockedAutofillUris = null) @@ -175,6 +177,19 @@ class SettingsDiskSourceImpl( ) } + override fun getAutofillSavePromptDisabled(userId: String): Boolean? = + getBoolean(key = "${DISABLE_AUTOFILL_SAVE_PROMPT_KEY}_$userId") + + override fun storeAutofillSavePromptDisabled( + userId: String, + isAutofillSavePromptDisabled: Boolean?, + ) { + putBoolean( + key = "${DISABLE_AUTOFILL_SAVE_PROMPT_KEY}_$userId", + value = isAutofillSavePromptDisabled, + ) + } + override fun getPullToRefreshEnabled(userId: String): Boolean? = getBoolean(key = "${PULL_TO_REFRESH_KEY}_$userId") diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt index 9b622c9cd1..8f17a98833 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepository.kt @@ -75,6 +75,11 @@ interface SettingsRepository { */ var isInlineAutofillEnabled: Boolean + /** + * Whether or not the autofill save prompt is disabled for the current user. + */ + var isAutofillSavePromptDisabled: Boolean + /** * A list of blocked autofill URI's for the current user. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt index f2346aeb2d..101e7cb98a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryImpl.kt @@ -163,6 +163,18 @@ class SettingsRepositoryImpl( ) } + override var isAutofillSavePromptDisabled: Boolean + get() = activeUserId + ?.let { settingsDiskSource.getAutofillSavePromptDisabled(userId = it) } + ?: false + set(value) { + val userId = activeUserId ?: return + settingsDiskSource.storeAutofillSavePromptDisabled( + userId = userId, + isAutofillSavePromptDisabled = value, + ) + } + override var blockedAutofillUris: List get() = activeUserId ?.let { settingsDiskSource.getBlockedAutofillUris(userId = it) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt index b7c6248426..a81c30313b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModel.kt @@ -29,7 +29,7 @@ class AutoFillViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: AutoFillState( - isAskToAddLoginEnabled = false, + isAskToAddLoginEnabled = !settingsRepository.isAutofillSavePromptDisabled, isAutoFillServicesEnabled = settingsRepository.isAutofillEnabledStateFlow.value, isCopyTotpAutomaticallyEnabled = false, isUseInlineAutoFillEnabled = settingsRepository.isInlineAutofillEnabled, @@ -65,8 +65,7 @@ class AutoFillViewModel @Inject constructor( } private fun handleAskToAddLoginClick(action: AutoFillAction.AskToAddLoginClick) { - // TODO BIT-1092: Persist selection - sendEvent(AutoFillEvent.ShowToast("Not yet implemented.".asText())) + settingsRepository.isAutofillSavePromptDisabled = !action.isEnabled mutableStateFlow.update { it.copy(isAskToAddLoginEnabled = action.isEnabled) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index 323f904d40..baa984bdb5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.time.Instant +@Suppress("LargeClass") class SettingsDiskSourceTest { private val fakeSharedPreferences = FakeSharedPreferences() private val json = PlatformNetworkModule.providesJson() @@ -79,6 +80,10 @@ class SettingsDiskSourceTest { userId = userId, uriMatchType = UriMatchType.REGULAR_EXPRESSION, ) + settingsDiskSource.storeAutofillSavePromptDisabled( + userId = userId, + isAutofillSavePromptDisabled = true, + ) settingsDiskSource.storePullToRefreshEnabled( userId = userId, isPullToRefreshEnabled = true, @@ -105,6 +110,7 @@ class SettingsDiskSourceTest { assertNull(settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)) assertNull(settingsDiskSource.getVaultTimeoutAction(userId = userId)) assertNull(settingsDiskSource.getDefaultUriMatchType(userId = userId)) + assertNull(settingsDiskSource.getAutofillSavePromptDisabled(userId = userId)) assertNull(settingsDiskSource.getPullToRefreshEnabled(userId = userId)) assertNull(settingsDiskSource.getInlineAutofillEnabled(userId = userId)) assertNull(settingsDiskSource.getBlockedAutofillUris(userId = userId)) @@ -445,6 +451,50 @@ class SettingsDiskSourceTest { assertFalse(fakeSharedPreferences.contains(defaultUriMatchTypeKey)) } + @Suppress("MaxLineLength") + @Test + fun `getAutofillSavePromptDisabled when values are present should pull from SharedPreferences`() { + val disableAutofillSavePromptBaseKey = "bwPreferencesStorage:autofillDisableSavePrompt" + val mockUserId = "mockUserId" + val disableAutofillSavePromptKey = "${disableAutofillSavePromptBaseKey}_$mockUserId" + fakeSharedPreferences + .edit { + putBoolean(disableAutofillSavePromptKey, true) + } + assertEquals(true, settingsDiskSource.getAutofillSavePromptDisabled(userId = mockUserId)) + } + + @Test + fun `getAutofillSavePromptDisabled when values are absent should return null`() { + val mockUserId = "mockUserId" + assertNull(settingsDiskSource.getAutofillSavePromptDisabled(userId = mockUserId)) + } + + @Test + fun `storeAutofillSavePromptDisabled for non-null values should update SharedPreferences`() { + val disableAutofillSavePromptBaseKey = "bwPreferencesStorage:autofillDisableSavePrompt" + val mockUserId = "mockUserId" + val disableAutofillSavePromptKey = "${disableAutofillSavePromptBaseKey}_$mockUserId" + settingsDiskSource.storeAutofillSavePromptDisabled( + userId = mockUserId, + isAutofillSavePromptDisabled = true, + ) + assertTrue(fakeSharedPreferences.getBoolean(disableAutofillSavePromptKey, false)) + } + + @Test + fun `storeAutofillSavePromptDisabled for null values should clear SharedPreferences`() { + val disableAutofillSavePromptBaseKey = "bwPreferencesStorage:autofillDisableSavePrompt" + val mockUserId = "mockUserId" + val disableAutofillSavePromptKey = "${disableAutofillSavePromptBaseKey}_$mockUserId" + fakeSharedPreferences.edit { putBoolean(disableAutofillSavePromptKey, false) } + settingsDiskSource.storeAutofillSavePromptDisabled( + userId = mockUserId, + isAutofillSavePromptDisabled = null, + ) + assertFalse(fakeSharedPreferences.contains(disableAutofillSavePromptKey)) + } + @Test fun `getPullToRefreshEnabled when values are present should pull from SharedPreferences`() { val pullToRefreshBaseKey = "bwPreferencesStorage:syncOnRefresh" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index 28a994c368..7c6ebd1050 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -38,6 +38,7 @@ class FakeSettingsDiskSource : SettingsDiskSource { private val storedVaultTimeoutActions = mutableMapOf() private val storedVaultTimeoutInMinutes = mutableMapOf() private val storedUriMatchTypes = mutableMapOf() + private val storedDisableAutofillSavePrompt = mutableMapOf() private val storedPullToRefreshEnabled = mutableMapOf() private val storedInlineAutofillEnabled = mutableMapOf() private val storedBlockedAutofillUris = mutableMapOf?>() @@ -76,6 +77,7 @@ class FakeSettingsDiskSource : SettingsDiskSource { storedVaultTimeoutActions.remove(userId) storedVaultTimeoutInMinutes.remove(userId) storedUriMatchTypes.remove(userId) + storedDisableAutofillSavePrompt.remove(userId) storedPullToRefreshEnabled.remove(userId) storedInlineAutofillEnabled.remove(userId) storedBlockedAutofillUris.remove(userId) @@ -136,6 +138,16 @@ class FakeSettingsDiskSource : SettingsDiskSource { storedUriMatchTypes[userId] = uriMatchType } + override fun getAutofillSavePromptDisabled(userId: String): Boolean? = + storedDisableAutofillSavePrompt[userId] + + override fun storeAutofillSavePromptDisabled( + userId: String, + isAutofillSavePromptDisabled: Boolean?, + ) { + storedDisableAutofillSavePrompt[userId] = isAutofillSavePromptDisabled + } + override fun getPullToRefreshEnabled(userId: String): Boolean? = storedPullToRefreshEnabled[userId] diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt index 6e9e33c922..ed420cd6bd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/SettingsRepositoryTest.kt @@ -443,6 +443,24 @@ class SettingsRepositoryTest { assertTrue(fakeSettingsDiskSource.getInlineAutofillEnabled(userId = userId)!!) } + @Test + fun `isAutofillSavePromptDisabled should pull from and update SettingsDiskSource`() { + val userId = "userId" + fakeAuthDiskSource.userState = MOCK_USER_STATE + assertFalse(settingsRepository.isAutofillSavePromptDisabled) + + // Updates to the disk source change the repository value. + fakeSettingsDiskSource.storeAutofillSavePromptDisabled( + userId = userId, + isAutofillSavePromptDisabled = true, + ) + assertTrue(settingsRepository.isAutofillSavePromptDisabled) + + // Updates to the repository change the disk source value + settingsRepository.isAutofillSavePromptDisabled = false + assertFalse(fakeSettingsDiskSource.getAutofillSavePromptDisabled(userId = userId)!!) + } + @Test fun `blockedAutofillUris should pull from and update SettingsDiskSource`() { val userId = "userId" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt index 7fef7a0fc2..20cde84f48 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillViewModelTest.kt @@ -22,6 +22,8 @@ class AutoFillViewModelTest : BaseViewModelTest() { private val settingsRepository: SettingsRepository = mockk() { every { isInlineAutofillEnabled } returns true every { isInlineAutofillEnabled = any() } just runs + every { isAutofillSavePromptDisabled } returns true + every { isAutofillSavePromptDisabled = any() } just runs every { defaultUriMatchType } returns UriMatchType.DOMAIN every { defaultUriMatchType = any() } just runs every { isAutofillEnabledStateFlow } returns mutableIsAutofillEnabledStateFlow @@ -66,16 +68,15 @@ class AutoFillViewModelTest : BaseViewModelTest() { } @Test - fun `on AskToAddLoginClick should emit ShowToast`() = runTest { + fun `on AskToAddLoginClick should update the state and save the new value to settings`() { val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(true)) - assertEquals(AutoFillEvent.ShowToast("Not yet implemented.".asText()), awaitItem()) - } + viewModel.trySendAction(AutoFillAction.AskToAddLoginClick(true)) assertEquals( DEFAULT_STATE.copy(isAskToAddLoginEnabled = true), viewModel.stateFlow.value, ) + // The UI enables the value, so the value gets flipped to save it as a "disabled" value. + verify { settingsRepository.isAutofillSavePromptDisabled = false } } @Test