mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
[PM-9833] Allow passkey deletion edit view (#3654)
This commit is contained in:
parent
9ed30d7913
commit
722726882b
8 changed files with 233 additions and 47 deletions
|
@ -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"),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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`() {
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue