mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 03:08:50 +03:00
PM-9681: Setup Bitwarden PIN on add edit view (#3627)
This commit is contained in:
parent
b0f0c0f33b
commit
680ebc2e47
4 changed files with 230 additions and 21 deletions
|
@ -47,6 +47,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
|
|||
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInputDialog
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
|
@ -206,6 +207,20 @@ fun VaultAddEditScreen(
|
|||
)
|
||||
}
|
||||
},
|
||||
onSubmitPinSetUpFido2Verification = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.PinFido2SetUpSubmit(it),
|
||||
)
|
||||
}
|
||||
},
|
||||
onRetryPinSetUpFido2Verification = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.PinFido2SetUpRetryClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
onDismissFido2Verification = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
|
@ -353,6 +368,8 @@ private fun VaultAddEditItemDialogs(
|
|||
onRetryFido2PasswordVerification: () -> Unit,
|
||||
onSubmitPinFido2Verification: (pin: String) -> Unit,
|
||||
onRetryFido2PinVerification: () -> Unit,
|
||||
onSubmitPinSetUpFido2Verification: (pin: String) -> Unit,
|
||||
onRetryPinSetUpFido2Verification: () -> Unit,
|
||||
onDismissFido2Verification: () -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
|
@ -433,6 +450,24 @@ private fun VaultAddEditItemDialogs(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.DialogState.Fido2PinSetUpPrompt -> {
|
||||
PinInputDialog(
|
||||
onCancelClick = onDismissFido2Verification,
|
||||
onSubmitClick = onSubmitPinSetUpFido2Verification,
|
||||
onDismissRequest = onDismissFido2Verification,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.DialogState.Fido2PinSetUpError -> {
|
||||
BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = null,
|
||||
message = R.string.validation_field_required.asText(R.string.pin.asText()),
|
||||
),
|
||||
onDismissRequest = onRetryPinSetUpFido2Verification,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -305,6 +305,9 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
handleRetryFido2PinVerificationClick()
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Common.PinFido2SetUpSubmit -> handlePinFido2SetUpSubmit(action)
|
||||
VaultAddEditAction.Common.PinFido2SetUpRetryClick -> handlePinFido2SetUpRetryClick()
|
||||
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick -> {
|
||||
handleDismissFido2VerificationDialogClick()
|
||||
}
|
||||
|
@ -644,7 +647,9 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
} else {
|
||||
// Prompt the user to set up a PIN for their account.
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-9681
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Fido2PinSetUpPrompt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -686,6 +691,32 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handlePinFido2SetUpSubmit(action: VaultAddEditAction.Common.PinFido2SetUpSubmit) {
|
||||
if (action.pin.isBlank()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Fido2PinSetUpError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// There's no need to ask the user whether or not they want to use their master password
|
||||
// on login, and shouldRequireMasterPasswordOnRestart is hardcoded to false, because the
|
||||
// user can only reach this part of the flow if they have no master password.
|
||||
settingsRepository.storeUnlockPin(
|
||||
pin = action.pin,
|
||||
shouldRequireMasterPasswordOnRestart = false,
|
||||
)
|
||||
|
||||
// After storing the PIN, the user can proceed with their original FIDO 2 request.
|
||||
handleValidAuthentication()
|
||||
}
|
||||
|
||||
private fun handlePinFido2SetUpRetryClick() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Fido2PinSetUpPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDismissFido2VerificationDialogClick() {
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
@ -2273,6 +2304,19 @@ data class VaultAddEditState(
|
|||
*/
|
||||
@Parcelize
|
||||
data object Fido2PinError : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a dialog to prompt the user to set up a PIN as part of the FIDO 2
|
||||
* user verification flow.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Fido2PinSetUpPrompt : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a dialog to alert the user that the PIN is a required field.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Fido2PinSetUpError : DialogState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2563,6 +2607,18 @@ sealed class VaultAddEditAction {
|
|||
*/
|
||||
data object RetryFido2PinVerificationClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to submit a PIN to set up for the FIDO 2 user verification flow.
|
||||
*/
|
||||
data class PinFido2SetUpSubmit(
|
||||
val pin: String,
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to retry setting up a PIN for the FIDO 2 user verification flow.
|
||||
*/
|
||||
data object PinFido2SetUpRetryClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to dismiss the FIDO 2 password or PIN verification dialog.
|
||||
*/
|
||||
|
|
|
@ -244,16 +244,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Master password")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
|
@ -323,16 +313,6 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
|
@ -377,6 +357,75 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fido2 pin set up prompt dialog should display based on state`() {
|
||||
val dialogMessage = "Enter your PIN code."
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(dialogMessage).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Fido2PinSetUpPrompt)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogMessage)
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput("PIN")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.PinFido2SetUpSubmit(
|
||||
pin = "PIN",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fido2 pin set up error dialog should display based on state`() {
|
||||
val dialogMessage = "The PIN field is required."
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
composeTestRule.onNodeWithText(dialogMessage).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultAddEditState.DialogState.Fido2PinSetUpError)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(dialogMessage)
|
||||
.assertIsDisplayed()
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Ok")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.PinFido2SetUpRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() {
|
||||
|
|
|
@ -3215,6 +3215,34 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationNotSupported should display Fido2PinSetUpPrompt when user has no password or pin`() {
|
||||
val userState = createUserState()
|
||||
mutableUserStateFlow.value = userState.copy(
|
||||
accounts = listOf(
|
||||
userState.accounts.first().copy(
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
trustedDevice = UserState.TrustedDevice(
|
||||
isDeviceTrusted = true,
|
||||
hasMasterPassword = false,
|
||||
hasAdminApproval = true,
|
||||
hasLoginApprovingDevice = true,
|
||||
hasResetPasswordPermission = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
|
||||
|
||||
verify { fido2CredentialManager.isUserVerified = false }
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2PinSetUpPrompt,
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when password verification fails`() {
|
||||
|
@ -3424,6 +3452,47 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PinFido2SetUpSubmit should display Fido2PinSetUpError for empty PIN`() {
|
||||
val pin = ""
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.PinFido2SetUpSubmit(pin = pin))
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2PinSetUpError,
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PinFido2SetUpSubmit should save PIN and register credential for non-empty PIN`() {
|
||||
val pin = "PIN"
|
||||
every {
|
||||
settingsRepository.storeUnlockPin(
|
||||
pin = pin,
|
||||
shouldRequireMasterPasswordOnRestart = false,
|
||||
)
|
||||
} just runs
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.PinFido2SetUpSubmit(pin = pin))
|
||||
|
||||
verify(exactly = 1) {
|
||||
settingsRepository.storeUnlockPin(
|
||||
pin = pin,
|
||||
shouldRequireMasterPasswordOnRestart = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PinFido2SetUpRetryClick should display Fido2PinSetUpPrompt`() {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.PinFido2SetUpRetryClick)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2PinSetUpPrompt,
|
||||
viewModel.stateFlow.value.dialog,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissFido2VerificationDialogClick should display Fido2ErrorDialog`() {
|
||||
viewModel.trySendAction(
|
||||
|
|
Loading…
Add table
Reference in a new issue