diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt index e57c44b296..b3b57399b7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconResource * @param placeholder the optional placeholder to be displayed when the text field is in focus and * the [value] is empty. * @param leadingIconResource the optional resource for the leading icon on the text field. + * @param trailingIconContent the content for the trailing icon in the text field. * @param hint optional hint text that will appear below the text input. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. @@ -51,6 +52,7 @@ fun BitwardenTextField( modifier: Modifier = Modifier, placeholder: String? = null, leadingIconResource: IconResource? = null, + trailingIconContent: (@Composable () -> Unit)? = null, hint: String? = null, singleLine: Boolean = true, readOnly: Boolean = false, @@ -88,6 +90,9 @@ fun BitwardenTextField( ) } }, + trailingIcon = trailingIconContent?.let { + trailingIconContent + }, placeholder = placeholder?.let { { Text(text = it) } }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextFieldWithActions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextFieldWithActions.kt index b744c18a81..55bc2e9e52 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextFieldWithActions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextFieldWithActions.kt @@ -25,6 +25,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param readOnly `true` if the input should be read-only and not accept user interactions. * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. + * @param trailingIconContent the content for the trailing icon in the text field. * @param actions A lambda containing the set of actions (usually icons or similar) to display * next to the text field. This lambda extends [RowScope], * providing flexibility in the layout definition. @@ -38,6 +39,7 @@ fun BitwardenTextFieldWithActions( readOnly: Boolean = false, singleLine: Boolean = true, keyboardType: KeyboardType = KeyboardType.Text, + trailingIconContent: (@Composable () -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, ) { Row( @@ -53,6 +55,7 @@ fun BitwardenTextFieldWithActions( singleLine = singleLine, onValueChange = onValueChange, keyboardType = keyboardType, + trailingIconContent = trailingIconContent, ) BitwardenRowOfActions(actions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt index 5457401079..90fe949f40 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLoginItems.kt @@ -130,6 +130,16 @@ fun LazyListScope.vaultAddEditLoginItems( .padding(horizontal = 16.dp), label = stringResource(id = R.string.totp), value = loginState.totp, + trailingIconContent = { + IconButton( + onClick = loginItemTypeHandlers.onClearTotpKeyClick, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(id = R.string.delete), + ) + } + }, onValueChange = {}, readOnly = true, singleLine = true, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index b72158636f..e5e087b3a0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -382,6 +382,10 @@ class VaultAddEditViewModel @Inject constructor( is VaultAddEditAction.ItemType.LoginType.CopyTotpKeyClick -> { handleLoginCopyTotpKeyText(action) } + + is VaultAddEditAction.ItemType.LoginType.ClearTotpKeyClick -> { + handleLoginClearTotpKey() + } } } @@ -455,6 +459,12 @@ class VaultAddEditViewModel @Inject constructor( clipboardManager.setText(text = action.totpKey) } + private fun handleLoginClearTotpKey() { + updateLoginContent { loginType -> + loginType.copy(totp = null) + } + } + private fun handleLoginUriSettingsClick() { viewModelScope.launch { sendEvent( @@ -1450,6 +1460,11 @@ sealed class VaultAddEditAction { */ data class CopyTotpKeyClick(val totpKey: String) : LoginType() + /** + * Represents the action to clear the totp code. + */ + data object ClearTotpKeyClick : LoginType() + /** * Represents the action to open the username generator. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt index cfc745b252..2ff0f7a285 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/handlers/VaultAddEditLoginTypeHandlers.kt @@ -18,6 +18,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditViewModel * @property onOpenPasswordGeneratorClick Handles the action when the password generator * button is clicked. * @property onSetupTotpClick Handles the action when the setup TOTP button is clicked. + * @property onCopyTotpKeyClick Handles the action when the copy TOTP text button is clicked. + * @property onClearTotpKeyClick Handles the action when the clear TOTP text button is clicked. * @property onUriSettingsClick Handles the action when the URI settings button is clicked. * @property onAddNewUriClick Handles the action when the add new URI button is clicked. */ @@ -31,6 +33,7 @@ data class VaultAddEditLoginTypeHandlers( val onOpenPasswordGeneratorClick: () -> Unit, val onSetupTotpClick: (Boolean) -> Unit, val onCopyTotpKeyClick: (String) -> Unit, + val onClearTotpKeyClick: () -> Unit, val onUriSettingsClick: () -> Unit, val onAddNewUriClick: () -> Unit, ) { @@ -94,6 +97,11 @@ data class VaultAddEditLoginTypeHandlers( ), ) }, + onClearTotpKeyClick = { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.ClearTotpKeyClick, + ) + }, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index 3295544732..8ac44de2d7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.FakePermissionManager import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.util.isProgressBar +import com.x8bit.bitwarden.ui.util.onAllNodesWithContentDescriptionAfterScroll import com.x8bit.bitwarden.ui.util.onAllNodesWithTextAfterScroll import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll @@ -457,6 +458,25 @@ class VaultAddEditScreenTest : BaseComposeTest() { .assertDoesNotExist() } + @Suppress("MaxLineLength") + @Test + fun `in ItemType_Login state the totp text field click on trailing icon should call ClearTotpKeyClick`() { + mutableStateFlow.update { currentState -> + updateLoginType(currentState) { copy(totp = "TestCode") } + } + + composeTestRule + .onAllNodesWithContentDescriptionAfterScroll("Delete") + .onFirst() + .performClick() + + verify { + viewModel.trySendAction( + VaultAddEditAction.ItemType.LoginType.ClearTotpKeyClick, + ) + } + } + @Suppress("MaxLineLength") @Test fun `in ItemType_Login state clicking the copy totp code button should trigger CopyTotpKeyClick`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index d5cb4bd193..9988b63cb2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -674,6 +674,38 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { } } + @Test + fun `ClearTotpKeyClick call should clear the totp code`() { + val viewModel = createAddVaultItemViewModel( + savedStateHandle = createSavedStateHandleWithState( + state = createVaultAddItemState( + typeContentViewState = createLoginTypeContentViewState( + totpCode = "testCode", + ), + ), + vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID), + ), + ) + + val expectedState = loginInitialState.copy( + viewState = VaultAddEditState.ViewState.Content( + common = createCommonContentViewState(), + type = createLoginTypeContentViewState( + totpCode = null, + ), + ), + ) + + viewModel.actionChannel.trySend( + VaultAddEditAction.ItemType.LoginType.ClearTotpKeyClick, + ) + + assertEquals( + expectedState, + viewModel.stateFlow.value, + ) + } + @Test fun `TotpCodeReceive should update totp code in state`() = runTest { val viewModel = createAddVaultItemViewModel()