BIT-1630: Add unlock with biometrics flow (#827)

This commit is contained in:
David Perez
2024-01-28 11:43:46 -06:00
committed by Álison Fernandes
parent 31d54b3dc2
commit b199a67b7d
4 changed files with 263 additions and 1 deletions

View File

@@ -43,12 +43,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenLogoutConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.theme.LocalBiometricsManager
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -60,6 +63,7 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun VaultUnlockScreen(
viewModel: VaultUnlockViewModel = hiltViewModel(),
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@@ -113,6 +117,12 @@ fun VaultUnlockScreen(
)
}
val onBiometricsUnlockClick: () -> Unit = remember(viewModel) {
{ viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) }
}
val onBiometricsLockOut: () -> Unit = remember(viewModel) {
{ viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut) }
}
// Content
BitwardenScaffold(
modifier = Modifier
@@ -182,6 +192,27 @@ fun VaultUnlockScreen(
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
if (state.isBiometricEnabled) {
BitwardenOutlinedButton(
label = stringResource(id = R.string.use_biometrics_to_unlock),
onClick = {
biometricsManager.promptBiometrics(
onSuccess = onBiometricsUnlockClick,
onCancel = {
// no-op
},
onError = {
// no-op
},
onLockOut = onBiometricsLockOut,
)
},
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
}
BitwardenFilledButton(
label = stringResource(id = R.string.unlock),
onClick = remember(viewModel) {

View File

@@ -87,6 +87,8 @@ class VaultUnlockViewModel @Inject constructor(
is VaultUnlockAction.LockAccountClick -> handleLockAccountClick(action)
is VaultUnlockAction.LogoutAccountClick -> handleLogoutAccountClick(action)
is VaultUnlockAction.SwitchAccountClick -> handleSwitchAccountClick(action)
VaultUnlockAction.BiometricsLockOut -> handleBiometricsLockOut()
VaultUnlockAction.BiometricsUnlockClick -> handleBiometricsUnlockClick()
VaultUnlockAction.UnlockClick -> handleUnlockClick()
is VaultUnlockAction.Internal -> handleInternalAction(action)
}
@@ -122,6 +124,26 @@ class VaultUnlockViewModel @Inject constructor(
authRepository.switchAccount(userId = action.accountSummary.userId)
}
private fun handleBiometricsLockOut() {
// TODO: Handle biometrics lockout (BIT-1451)
sendEvent(VaultUnlockEvent.ShowToast("Lock out not yet implemented".asText()))
}
private fun handleBiometricsUnlockClick() {
val activeUserId = authRepository.activeUserId ?: return
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
viewModelScope.launch {
val vaultUnlockResult = vaultRepo.unlockVaultWithBiometrics()
sendAction(
VaultUnlockAction.Internal.ReceiveVaultUnlockResult(
userId = activeUserId,
vaultUnlockResult = vaultUnlockResult,
isBiometricLogin = true,
),
)
}
}
private fun handleUnlockClick() {
val activeUserId = authRepository.activeUserId ?: return
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
@@ -143,6 +165,7 @@ class VaultUnlockViewModel @Inject constructor(
VaultUnlockAction.Internal.ReceiveVaultUnlockResult(
userId = activeUserId,
vaultUnlockResult = vaultUnlockResult,
isBiometricLogin = false,
),
)
}
@@ -175,7 +198,11 @@ class VaultUnlockViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = VaultUnlockState.VaultUnlockDialog.Error(
state.vaultUnlockType.unlockScreenErrorMessage,
if (action.isBiometricLogin) {
R.string.generic_error_message.asText()
} else {
state.vaultUnlockType.unlockScreenErrorMessage
},
),
)
}
@@ -327,6 +354,16 @@ sealed class VaultUnlockAction {
val accountSummary: AccountSummary,
) : VaultUnlockAction()
/**
* The user has clicked the biometrics button.
*/
data object BiometricsUnlockClick : VaultUnlockAction()
/**
* The user has attempted to login with biometrics too many times and has been locked out.
*/
data object BiometricsLockOut : VaultUnlockAction()
/**
* The user has clicked the unlock button.
*/
@@ -342,6 +379,7 @@ sealed class VaultUnlockAction {
data class ReceiveVaultUnlockResult(
val userId: String,
val vaultUnlockResult: VaultUnlockResult,
val isBiometricLogin: Boolean,
) : Internal()
/**