BIT-614: Add ability to setup biometrics from account security screen (#826)

This commit is contained in:
David Perez 2024-01-27 23:27:59 -06:00 committed by Álison Fernandes
parent cf7f0ad7fe
commit 8f21fb466e
2 changed files with 128 additions and 14 deletions

View file

@ -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.
*/

View file

@ -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