diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 9e106c8c0e..f96311e35c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository +import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction import com.x8bit.bitwarden.data.platform.repository.util.baseWebVaultUrlOrDefault @@ -220,9 +221,21 @@ class AccountSecurityViewModel @Inject constructor( private fun handleUnlockWithBiometricToggle( action: AccountSecurityAction.UnlockWithBiometricToggle, ) { - // TODO Display alert - mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = action.enabled) } - sendEvent(AccountSecurityEvent.ShowToast("Handle unlock with biometrics.".asText())) + if (action.enabled) { + mutableStateFlow.update { + it.copy( + dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), + isUnlockWithBiometricsEnabled = true, + ) + } + viewModelScope.launch { + val result = settingsRepository.setupBiometricsKey() + sendAction(AccountSecurityAction.Internal.BiometricsKeyResultReceive(result)) + } + } else { + settingsRepository.clearBiometricsKey() + mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = false) } + } } private fun handleUnlockWithPinToggle(action: AccountSecurityAction.UnlockWithPinToggle) { @@ -248,12 +261,40 @@ class AccountSecurityViewModel @Inject constructor( private fun handleInternalAction(action: AccountSecurityAction.Internal) { when (action) { + is AccountSecurityAction.Internal.BiometricsKeyResultReceive -> { + handleBiometricsKeyResultReceive(action) + } + is AccountSecurityAction.Internal.FingerprintResultReceive -> { handleFingerprintResultReceived(action) } } } + private fun handleBiometricsKeyResultReceive( + action: AccountSecurityAction.Internal.BiometricsKeyResultReceive, + ) { + when (action.result) { + BiometricsKeyResult.Error -> { + mutableStateFlow.update { + it.copy( + dialog = null, + isUnlockWithBiometricsEnabled = false, + ) + } + } + + BiometricsKeyResult.Success -> { + mutableStateFlow.update { + it.copy( + dialog = null, + isUnlockWithBiometricsEnabled = true, + ) + } + } + } + } + private fun handleFingerprintResultReceived( action: AccountSecurityAction.Internal.FingerprintResultReceive, ) { @@ -515,6 +556,13 @@ sealed class AccountSecurityAction { * Models actions that can be sent by the view model itself. */ sealed class Internal : AccountSecurityAction() { + /** + * A biometrics key result has been received. + */ + data class BiometricsKeyResultReceive( + val result: BiometricsKeyResult, + ) : Internal() + /** * A fingerprint has been received. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index c2ff0ff631..81bb9c4a16 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository +import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction @@ -259,20 +261,84 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { } @Test - fun `on UnlockWithBiometricToggle should emit ShowToast`() = runTest { - val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + fun `on UnlockWithBiometricToggle false should call clearBiometricsKey and update the state`() = + runTest { + val initialState = DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true) + every { settingsRepository.isUnlockWithBiometricsEnabled } returns true + every { settingsRepository.clearBiometricsKey() } just runs + val viewModel = createViewModel(initialState) + assertEquals(initialState, viewModel.stateFlow.value) + + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(false)) + assertEquals( - AccountSecurityEvent.ShowToast("Handle unlock with biometrics.".asText()), - awaitItem(), + initialState.copy(isUnlockWithBiometricsEnabled = false), + viewModel.stateFlow.value, ) + verify(exactly = 1) { + settingsRepository.clearBiometricsKey() + } + } + + @Suppress("MaxLineLength") + @Test + fun `on UnlockWithBiometricToggle true and setupBiometricsKey error should call update the state accordingly`() = + runTest { + coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Error + val viewModel = createViewModel() + + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + assertEquals( + DEFAULT_STATE.copy( + dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), + isUnlockWithBiometricsEnabled = true, + ), + awaitItem(), + ) + assertEquals( + DEFAULT_STATE.copy( + dialog = null, + isUnlockWithBiometricsEnabled = false, + ), + awaitItem(), + ) + } + coVerify(exactly = 1) { + settingsRepository.setupBiometricsKey() + } + } + + @Suppress("MaxLineLength") + @Test + fun `on UnlockWithBiometricToggle true and setupBiometricsKey success should call update the state accordingly`() = + runTest { + coEvery { settingsRepository.setupBiometricsKey() } returns BiometricsKeyResult.Success + val viewModel = createViewModel() + + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(AccountSecurityAction.UnlockWithBiometricToggle(true)) + assertEquals( + DEFAULT_STATE.copy( + dialog = AccountSecurityDialog.Loading(R.string.saving.asText()), + isUnlockWithBiometricsEnabled = true, + ), + awaitItem(), + ) + assertEquals( + DEFAULT_STATE.copy( + dialog = null, + isUnlockWithBiometricsEnabled = true, + ), + awaitItem(), + ) + } + coVerify(exactly = 1) { + settingsRepository.setupBiometricsKey() + } } - assertEquals( - DEFAULT_STATE.copy(isUnlockWithBiometricsEnabled = true), - viewModel.stateFlow.value, - ) - } @Suppress("MaxLineLength") @Test