[PM-9833] Allow passkey deletion edit view (#3654)

This commit is contained in:
Carlos Gonçalves 2024-08-08 21:17:09 +01:00 committed by GitHub
parent 9ed30d7913
commit 722726882b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 47 deletions

View file

@ -92,6 +92,8 @@ fun LazyListScope.vaultAddEditLoginItems(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
canRemovePasskey = loginState.canViewPassword,
loginItemTypeHandlers = loginItemTypeHandlers,
)
}
}
@ -517,14 +519,29 @@ private fun PasswordRow(
@Composable
private fun PasskeyField(
creationDateTime: Text,
canRemovePasskey: Boolean,
loginItemTypeHandlers: VaultAddEditLoginTypeHandlers,
modifier: Modifier = Modifier,
) {
BitwardenTextField(
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.passkey),
value = creationDateTime.invoke(),
onValueChange = { },
readOnly = true,
singleLine = true,
modifier = modifier,
actions = {
if (canRemovePasskey) {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = rememberVectorPainter(id = R.drawable.ic_minus),
contentDescription = stringResource(id = R.string.remove_passkey),
),
onClick = loginItemTypeHandlers.onClearFido2CredentialClick,
modifier = Modifier
.testTag("RemovePasskeyButton"),
)
}
},
)
}

View file

@ -934,6 +934,10 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.ItemType.LoginType.PasswordVisibilityChange -> {
handlePasswordVisibilityChange(action)
}
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick -> {
handleLoginClearFido2Credential()
}
}
}
@ -1034,6 +1038,13 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginClearFido2Credential() {
updateLoginContent { loginType ->
loginType.copy(fido2CredentialCreationDateTime = null)
}
sendEvent(event = VaultAddEditEvent.ShowToast(R.string.passkey_removed.asText()))
}
private fun handlePasswordVisibilityChange(
action: VaultAddEditAction.ItemType.LoginType.PasswordVisibilityChange,
) {
@ -2700,6 +2711,11 @@ sealed class VaultAddEditAction {
* @property isVisible The new password visibility state.
*/
data class PasswordVisibilityChange(val isVisible: Boolean) : LoginType()
/**
* Represents the action to clear the fido2 credential.
*/
data object ClearFido2CredentialClick : LoginType()
}
/**

View file

@ -24,6 +24,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
* @property onAddNewUriClick Handles the action when the add new URI button is clicked.
* @property onPasswordVisibilityChange Handles the action when the password visibility button is
* clicked.
* @property onClearFido2CredentialClick Handles the action when the clear Fido2 credential button
* is clicked.
*/
@Suppress("LongParameterList")
data class VaultAddEditLoginTypeHandlers(
@ -39,6 +41,7 @@ data class VaultAddEditLoginTypeHandlers(
val onClearTotpKeyClick: () -> Unit,
val onAddNewUriClick: () -> Unit,
val onPasswordVisibilityChange: (Boolean) -> Unit,
val onClearFido2CredentialClick: () -> Unit,
) {
companion object {
@ -114,6 +117,11 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.PasswordVisibilityChange(it),
)
},
onClearFido2CredentialClick = {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick,
)
},
)
}
}

View file

@ -147,7 +147,8 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toLoginView(
uris = it.uriList.toLoginUriView(),
totp = it.totp,
autofillOnPageLoad = common.originalCipher?.login?.autofillOnPageLoad,
fido2Credentials = common.originalCipher?.login?.fido2Credentials,
fido2Credentials = common.originalCipher?.login?.fido2Credentials
.takeIf { _ -> it.fido2CredentialCreationDateTime != null },
)
}

View file

@ -935,4 +935,6 @@ Do you want to switch to this account?</string>
<string name="welcome_message_3">Use the generator to create and save strong, unique passwords for all your accounts.</string>
<string name="your_data_when_and_where_you_need_it">Your data, when and where you need it</string>
<string name="welcome_message_4">Save unlimited passwords across unlimited devices with Bitwarden mobile, browser, and desktop apps.</string>
<string name="remove_passkey">Remove passkey</string>
<string name="passkey_removed">Passkey removed</string>
</resources>

View file

@ -916,6 +916,58 @@ class VaultAddEditScreenTest : BaseComposeTest() {
.assertTextContains("•••••••••••")
}
@Test
fun `in ItemType_Login state the Passkey should change according to state`() {
mutableStateFlow.update {
it.copy(
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
type = VaultAddEditState.ViewState.Content.ItemType.Login(
fido2CredentialCreationDateTime = "fido2Credentials".asText(),
canViewPassword = false,
),
isIndividualVaultDisabled = false,
),
)
}
composeTestRule
.onNodeWithTextAfterScroll("Passkey")
.assertTextEquals("Passkey", "fido2Credentials")
.assertIsEnabled()
composeTestRule
.onNodeWithContentDescription("Remove passkey")
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(),
type = VaultAddEditState.ViewState.Content.ItemType.Login(
fido2CredentialCreationDateTime = "fido2Credentials".asText(),
canViewPassword = true,
),
isIndividualVaultDisabled = false,
),
)
}
// Click on Remove Passkey button
composeTestRule
.onNodeWithTextAfterScroll("Passkey")
.assertExists()
composeTestRule
.onNodeWithContentDescription("Remove passkey")
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick,
)
}
}
@Test
fun `in ItemType_Login state the totp text field should be present based on state`() {
mutableStateFlow.update { currentState ->

View file

@ -1356,56 +1356,56 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `in edit mode during FIDO 2 registration, SaveClick should display ConfirmOverwriteExistingPasskeyDialog when original cipher has a passkey`() {
val cipherView = createMockCipherView(
number = 1,
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
)
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val stateWithName = createVaultAddItemState(
vaultAddEditType = vaultAddEditType,
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
originalCipher = cipherView,
customFieldData = listOf(
VaultAddEditState.Custom.HiddenField(
itemId = "testId",
name = "mockName-1",
value = "mockValue-1",
fun `in edit mode during FIDO 2 registration, SaveClick should display ConfirmOverwriteExistingPasskeyDialog when original cipher has a passkey`() =
runTest {
val cipherView = createMockCipherView(
number = 1,
fido2Credentials = createMockSdkFido2CredentialList(number = 1),
)
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
val stateWithName = createVaultAddItemState(
commonContentViewState = createCommonContentViewState(
name = cipherView.name,
originalCipher = cipherView,
),
typeContentViewState = createLoginTypeContentViewState(
fido2CredentialCreationDateTime = R.string.created_xy.asText(
"05/08/24",
"14:30 PM",
),
),
notes = "mockNotes-1",
),
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
)
every {
cipherView.toViewState(
isClone = false,
isIndividualVaultDisabled = false,
resourceManager = resourceManager,
clock = fixedClock,
)
} returns stateWithName.viewState
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(cipherView = cipherView),
)
val mockFido2CredentialRequest = createMockFido2CredentialRequest(number = 1)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = vaultAddEditType,
),
)
specialCircumstanceManager.specialCircumstance = SpecialCircumstance.Fido2Save(
fido2CredentialRequest = mockFido2CredentialRequest,
)
every {
cipherView.toViewState(
isClone = false,
isIndividualVaultDisabled = false,
resourceManager = resourceManager,
clock = fixedClock,
)
} returns stateWithName.viewState
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(cipherView = cipherView),
)
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = vaultAddEditType,
),
)
assertEquals(
VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt,
viewModel.stateFlow.value.dialog,
)
}
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(
VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt,
viewModel.stateFlow.value.dialog,
)
}
@Suppress("MaxLineLength")
@Test
@ -2107,6 +2107,42 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.stateFlow.value,
)
}
@Test
fun `ClearFido2CredentialClick call should clear the fido2 credential`() {
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(
fido2CredentialCreationDateTime = R.string.created_xy.asText(
"05/08/24",
"14:30 PM",
),
),
),
vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID),
),
)
val expectedState = loginInitialState.copy(
viewState = VaultAddEditState.ViewState.Content(
common = createCommonContentViewState(),
isIndividualVaultDisabled = false,
type = createLoginTypeContentViewState(
fido2CredentialCreationDateTime = null,
),
),
)
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick,
)
assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}
}
@Nested
@ -3436,7 +3472,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
shouldRequireMasterPasswordOnRestart = false,
)
}
}
}
@Test
fun `PinFido2SetUpRetryClick should display Fido2PinSetUpPrompt`() {

View file

@ -13,6 +13,7 @@ import com.bitwarden.vault.PasswordHistoryView
import com.bitwarden.vault.SecureNoteType
import com.bitwarden.vault.SecureNoteView
import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2CredentialList
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
@ -676,6 +677,59 @@ class VaultAddItemStateExtensionsTest {
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginView should transform Login ItemType to LoginView deleting fido2Credentials with original cipher`() {
val cipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
type = CipherType.LOGIN,
notes = null,
fields = emptyList(),
login = LoginView(
username = "mockUsername-1",
password = "mockPassword-1",
passwordRevisionDate = Instant.MIN,
uris = null,
totp = null,
autofillOnPageLoad = false,
fido2Credentials = createMockSdkFido2CredentialList(1),
),
)
val viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
originalCipher = cipherView,
name = "mockName-1",
customFieldData = emptyList(),
masterPasswordReprompt = true,
),
isIndividualVaultDisabled = false,
type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = "mockUsername-1",
password = "mockPassword-1",
totp = null,
fido2CredentialCreationDateTime = null,
),
)
val result = viewState.toCipherView()
assertEquals(
cipherView.copy(
name = "mockName-1",
login = LoginView(
username = "mockUsername-1",
password = "mockPassword-1",
totp = null,
fido2Credentials = null,
uris = null,
passwordRevisionDate = Instant.MIN,
autofillOnPageLoad = false,
),
),
result,
)
}
}
private val DEFAULT_BASE_CIPHER_VIEW: CipherView = CipherView(