PM-9681: Setup Bitwarden PIN (#3626)

This commit is contained in:
Shannon Draeker
2024-07-25 10:59:20 -06:00
committed by GitHub
parent 5c2ac2e037
commit c09fe554bc
6 changed files with 334 additions and 19 deletions
@@ -417,7 +417,7 @@ private fun UnlockWithPinRow(
) {
var shouldShowPinInputDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowPinConfirmationDialog by rememberSaveable { mutableStateOf(false) }
var pin by rememberSaveable { mutableStateOf("") }
var pin by remember { mutableStateOf("") }
BitwardenWideSwitch(
label = stringResource(id = R.string.unlock_with_pin),
isChecked = isUnlockWithPinEnabled,
@@ -439,8 +439,6 @@ private fun UnlockWithPinRow(
when {
shouldShowPinInputDialog -> {
PinInputDialog(
pin = pin,
onPinChange = { pin = it },
onCancelClick = {
shouldShowPinInputDialog = false
onUnlockWithPinToggleAction(
@@ -449,6 +447,7 @@ private fun UnlockWithPinRow(
pin = ""
},
onSubmitClick = {
pin = it
if (pin.isNotEmpty()) {
shouldShowPinInputDialog = false
if (isUnlockWithPasswordEnabled) {
@@ -14,6 +14,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -35,8 +39,6 @@ import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight
/**
* A dialog for setting a user's PIN.
*
* @param pin The current value of the PIN.
* @param onPinChange A callback for internal changes to the PIN.
* @param onCancelClick A callback for when the "Cancel" button is clicked.
* @param onSubmitClick A callback for when the "Submit" button is clicked.
* @param onDismissRequest A callback for when the dialog is requesting to be dismissed.
@@ -45,12 +47,11 @@ import com.x8bit.bitwarden.ui.platform.components.util.maxDialogHeight
@Suppress("LongMethod")
@Composable
fun PinInputDialog(
pin: String,
onPinChange: (String) -> Unit,
onCancelClick: () -> Unit,
onSubmitClick: () -> Unit,
onSubmitClick: (String) -> Unit,
onDismissRequest: () -> Unit,
) {
var pin by remember { mutableStateOf("") }
Dialog(
onDismissRequest = onDismissRequest,
) {
@@ -107,7 +108,7 @@ fun PinInputDialog(
BitwardenTextField(
label = stringResource(id = R.string.pin),
value = pin,
onValueChange = onPinChange,
onValueChange = { pin = it },
keyboardType = KeyboardType.Number,
modifier = Modifier
.testTag("AlertInputField")
@@ -135,7 +136,7 @@ fun PinInputDialog(
BitwardenFilledButton(
label = stringResource(id = R.string.submit),
onClick = onSubmitClick,
onClick = { onSubmitClick(pin) },
modifier = Modifier.testTag("AcceptAlertButton"),
)
}
@@ -51,6 +51,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
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.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
@@ -222,6 +223,23 @@ fun VaultItemListingScreen(
)
}
},
onSubmitPinSetUpFido2Verification = remember(viewModel) {
{ pin, cipherId ->
viewModel.trySendAction(
VaultItemListingsAction.PinFido2SetUpSubmit(
pin = pin,
selectedCipherId = cipherId,
),
)
}
},
onRetryPinSetUpFido2Verification = remember(viewModel) {
{
viewModel.trySendAction(
VaultItemListingsAction.PinFido2SetUpRetryClick(it),
)
}
},
onDismissFido2Verification = remember(viewModel) {
{
viewModel.trySendAction(
@@ -248,9 +266,11 @@ private fun VaultItemListingDialogs(
onDismissFido2ErrorDialog: () -> Unit,
onConfirmOverwriteExistingPasskey: (cipherViewId: String) -> Unit,
onSubmitMasterPasswordFido2Verification: (password: String, cipherId: String) -> Unit,
onRetryFido2PasswordVerification: (cipherViewId: String) -> Unit,
onRetryFido2PasswordVerification: (cipherId: String) -> Unit,
onSubmitPinFido2Verification: (pin: String, cipherId: String) -> Unit,
onRetryFido2PinVerification: (cipherViewId: String) -> Unit,
onSubmitPinSetUpFido2Verification: (pin: String, cipherId: String) -> Unit,
onRetryPinSetUpFido2Verification: (cipherId: String) -> Unit,
onDismissFido2Verification: () -> Unit,
) {
when (dialogState) {
@@ -329,6 +349,28 @@ private fun VaultItemListingDialogs(
)
}
is VaultItemListingState.DialogState.Fido2PinSetUpPrompt -> {
PinInputDialog(
onCancelClick = onDismissFido2Verification,
onSubmitClick = { pin ->
onSubmitPinSetUpFido2Verification(pin, dialogState.selectedCipherId)
},
onDismissRequest = onDismissFido2Verification,
)
}
is VaultItemListingState.DialogState.Fido2PinSetUpError -> {
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,
),
onDismissRequest = {
onRetryPinSetUpFido2Verification(dialogState.selectedCipherId)
},
)
}
null -> Unit
}
}
@@ -176,6 +176,7 @@ class VaultItemListingViewModel @Inject constructor(
.launchIn(viewModelScope)
}
@Suppress("LongMethod")
override fun handleAction(action: VaultItemListingsAction) {
when (action) {
is VaultItemListingsAction.LockAccountClick -> handleLockAccountClick(action)
@@ -202,6 +203,12 @@ class VaultItemListingViewModel @Inject constructor(
handleRetryFido2PinVerificationClick(action)
}
is VaultItemListingsAction.PinFido2SetUpSubmit -> handlePinFido2SetUpSubmit(action)
is VaultItemListingsAction.PinFido2SetUpRetryClick -> {
handlePinFido2SetUpRetryClick(action)
}
VaultItemListingsAction.DismissFido2VerificationDialogClick -> {
handleDismissFido2VerificationDialogClick()
}
@@ -370,7 +377,13 @@ class VaultItemListingViewModel @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(
dialogState = VaultItemListingState.DialogState.Fido2PinSetUpPrompt(
selectedCipherId = selectedCipherId,
),
)
}
}
}
@@ -426,6 +439,44 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun handlePinFido2SetUpSubmit(action: VaultItemListingsAction.PinFido2SetUpSubmit) {
if (action.pin.isBlank()) {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2PinSetUpError(
title = null,
message = R.string.validation_field_required.asText(R.string.pin.asText()),
selectedCipherId = action.selectedCipherId,
),
)
}
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(selectedCipherId = action.selectedCipherId)
}
private fun handlePinFido2SetUpRetryClick(
action: VaultItemListingsAction.PinFido2SetUpRetryClick,
) {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Fido2PinSetUpPrompt(
selectedCipherId = action.selectedCipherId,
),
)
}
}
private fun handleDismissFido2VerificationDialogClick() {
showFido2ErrorDialog()
}
@@ -1496,6 +1547,25 @@ data class VaultItemListingState(
val message: Text,
val selectedCipherId: String,
) : DialogState()
/**
* Represents a dialog to prompt the user to set up a PIN for the FIDO 2 user
* verification flow.
*/
@Parcelize
data class Fido2PinSetUpPrompt(
val selectedCipherId: String,
) : DialogState()
/**
* Represents a dialog to alert the user that the PIN is a required field.
*/
@Parcelize
data class Fido2PinSetUpError(
val title: Text?,
val message: Text,
val selectedCipherId: String,
) : DialogState()
}
/**
@@ -1882,11 +1952,6 @@ sealed class VaultItemListingsAction {
val selectedCipherId: String,
) : VaultItemListingsAction()
/**
* Click to dismiss the FIDO 2 password or PIN verification dialog.
*/
data object DismissFido2VerificationDialogClick : VaultItemListingsAction()
/**
* Click to retry the FIDO 2 password verification.
*/
@@ -1895,7 +1960,7 @@ sealed class VaultItemListingsAction {
) : VaultItemListingsAction()
/**
* Click to submit the PIN for FIDO 2 verification.
* Click to submit the PIN for FIDO 2 user verification.
*/
data class PinFido2VerificationSubmit(
val pin: String,
@@ -1909,6 +1974,26 @@ sealed class VaultItemListingsAction {
val selectedCipherId: String,
) : VaultItemListingsAction()
/**
* Click to submit to set up a PIN for the FIDO 2 user verification flow.
*/
data class PinFido2SetUpSubmit(
val pin: String,
val selectedCipherId: String,
) : VaultItemListingsAction()
/**
* Click to retry setting up a PIN for the FIDO 2 user verification flow.
*/
data class PinFido2SetUpRetryClick(
val selectedCipherId: String,
) : VaultItemListingsAction()
/**
* Click to dismiss the FIDO 2 password or PIN verification dialog.
*/
data object DismissFido2VerificationDialogClick : VaultItemListingsAction()
/**
* Click the refresh button.
*/