diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt index ce6061bfa0..48481e3523 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt @@ -2,10 +2,10 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend import android.Manifest import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -38,7 +38,9 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton +import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedErrorButton import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard +import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField @@ -46,6 +48,7 @@ import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeade import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch +import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers @@ -185,11 +188,49 @@ fun AddSendContent( isAddMode = isAddMode, addSendHandlers = addSendHandlers, ) + + if (!isAddMode) { + DeleteButton( + onDeleteClick = addSendHandlers.onDeleteClick, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) + } + Spacer(modifier = Modifier.height(height = 12.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } +@Composable +private fun DeleteButton( + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(value = false) } + if (shouldShowDeleteConfirmationDialog) { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.delete), + message = stringResource(id = R.string.are_you_sure_delete_send), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.cancel), + onConfirmClick = { + onDeleteClick() + shouldShowDeleteConfirmationDialog = false + }, + onDismissClick = { shouldShowDeleteConfirmationDialog = false }, + onDismissRequest = { shouldShowDeleteConfirmationDialog = false }, + ) + } + BitwardenOutlinedErrorButton( + label = stringResource(id = R.string.delete_send), + onClick = { shouldShowDeleteConfirmationDialog = true }, + icon = rememberVectorPainter(id = R.drawable.ic_trash_small), + modifier = modifier, + ) +} + @Composable private fun ColumnScope.TextTypeContent( textType: AddSendState.ViewState.Content.SendType.Text, @@ -357,11 +398,10 @@ private fun AddSendOptions( .standardHorizontalMargin() .fillMaxWidth(), ) - // Hide all content if not expanded: AnimatedVisibility( visible = isExpanded, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), + enter = fadeIn() + expandVertically(expandFrom = Alignment.Top), + exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top), modifier = Modifier.clipToBounds(), ) { Column { @@ -434,6 +474,7 @@ private fun AddSendOptions( .fillMaxWidth() .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt index d364a457a5..58ae1387c6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt @@ -9,10 +9,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -32,7 +29,6 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton @@ -109,24 +105,6 @@ fun AddSendScreen( { viewModel.trySendAction(AddSendAction.DismissDialogClick) } }, ) - var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) } - if (shouldShowDeleteConfirmationDialog) { - BitwardenTwoButtonDialog( - title = stringResource(id = R.string.delete), - message = stringResource(id = R.string.are_you_sure_delete_send), - confirmButtonText = stringResource(id = R.string.yes), - dismissButtonText = stringResource(id = R.string.cancel), - onConfirmClick = remember(viewModel) { - { - viewModel.trySendAction(AddSendAction.DeleteClick) - shouldShowDeleteConfirmationDialog = false - } - }, - onDismissClick = { shouldShowDeleteConfirmationDialog = false }, - onDismissRequest = { shouldShowDeleteConfirmationDialog = false }, - ) - } - BitwardenScaffold( modifier = Modifier .fillMaxSize() @@ -181,10 +159,6 @@ fun AddSendScreen( }, ) .takeIf { !state.policyDisablesSend }, - OverflowMenuItemData( - text = stringResource(id = R.string.delete), - onClick = { shouldShowDeleteConfirmationDialog = true }, - ), ), ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt index 97f427ecb9..f45381543d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt @@ -23,6 +23,7 @@ data class AddSendHandlers( val onHideEmailToggle: (Boolean) -> Unit, val onDeactivateSendToggle: (Boolean) -> Unit, val onDeletionDateChange: (ZonedDateTime) -> Unit, + val onDeleteClick: () -> Unit, ) { @Suppress("UndocumentedPublicClass") companion object { @@ -57,6 +58,7 @@ data class AddSendHandlers( onDeletionDateChange = { viewModel.trySendAction(AddSendAction.DeletionDateChange(it)) }, + onDeleteClick = { viewModel.trySendAction(AddSendAction.DeleteClick) }, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreen.kt index 068005eede..36d0dd8b28 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreen.kt @@ -58,6 +58,7 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog +import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader @@ -306,10 +307,8 @@ private fun ViewStateContent( AdditionalOptions(state = state) - BitwardenOutlinedErrorButton( - label = stringResource(id = R.string.delete_send), - onClick = onDeleteClick, - icon = rememberVectorPainter(id = R.drawable.ic_trash_small), + DeleteButton( + onDeleteClick = onDeleteClick, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), @@ -319,6 +318,34 @@ private fun ViewStateContent( } } +@Composable +private fun DeleteButton( + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(value = false) } + if (shouldShowDeleteConfirmationDialog) { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.delete), + message = stringResource(id = R.string.are_you_sure_delete_send), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.cancel), + onConfirmClick = { + onDeleteClick() + shouldShowDeleteConfirmationDialog = false + }, + onDismissClick = { shouldShowDeleteConfirmationDialog = false }, + onDismissRequest = { shouldShowDeleteConfirmationDialog = false }, + ) + } + BitwardenOutlinedErrorButton( + label = stringResource(id = R.string.delete_send), + onClick = { shouldShowDeleteConfirmationDialog = true }, + icon = rememberVectorPainter(id = R.drawable.ic_trash_small), + modifier = modifier, + ) +} + /** * A default content block which displays a header with an optional subtitle and an icon. * Implemented to match design component. diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index ce1eb9d3e1..4b019e2b73 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -162,14 +162,10 @@ class AddSendScreenTest : BaseComposeTest() { .onNodeWithText("Share link") .assert(hasAnyAncestor(isPopup())) .isDisplayed() - composeTestRule - .onNodeWithText("Delete") - .assert(hasAnyAncestor(isPopup())) - .isDisplayed() } @Test - fun `on overflow button click should only display delete when policy disables send`() { + fun `on overflow button should not be present when policy disables send`() { mutableStateFlow.value = DEFAULT_STATE.copy( addSendType = AddSendType.EditItem(sendItemId = "sendId"), policyDisablesSend = true, @@ -177,21 +173,7 @@ class AddSendScreenTest : BaseComposeTest() { composeTestRule .onNodeWithContentDescription("More") - .performClick() - - composeTestRule - .onNodeWithText("Remove password") .assertDoesNotExist() - composeTestRule - .onNodeWithText("Copy link") - .assertDoesNotExist() - composeTestRule - .onNodeWithText("Share link") - .assertDoesNotExist() - composeTestRule - .onNodeWithText("Delete") - .assert(hasAnyAncestor(isPopup())) - .isDisplayed() } @Test @@ -250,50 +232,6 @@ class AddSendScreenTest : BaseComposeTest() { } } - @Test - fun `on overflow Delete button click should Display delete confirmation dialog`() { - mutableStateFlow.value = DEFAULT_STATE.copy( - addSendType = AddSendType.EditItem(sendItemId = "sendId"), - ) - - composeTestRule - .onNodeWithContentDescription("More") - .performClick() - - composeTestRule - .onNodeWithText("Delete") - .performClick() - - composeTestRule - .onNodeWithText("Are you sure you want to delete this Send?") - .assert(hasAnyAncestor(isDialog())) - .assertIsDisplayed() - } - - @Test - fun `on delete confirmation dialog yes click should send DeleteClick`() { - mutableStateFlow.value = DEFAULT_STATE.copy( - addSendType = AddSendType.EditItem(sendItemId = "sendId"), - ) - - composeTestRule - .onNodeWithContentDescription("More") - .performClick() - - composeTestRule - .onNodeWithText("Delete") - .performClick() - - composeTestRule - .onNodeWithText("Yes") - .assert(hasAnyAncestor(isDialog())) - .performClick() - - verify(exactly = 1) { - viewModel.trySendAction(AddSendAction.DeleteClick) - } - } - @Test fun `on overflow remove Copy link button click should send CopyLinkClick`() { mutableStateFlow.value = DEFAULT_STATE.copy( @@ -313,6 +251,44 @@ class AddSendScreenTest : BaseComposeTest() { } } + @Test + fun `on Delete button click should Display delete confirmation dialog`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + addSendType = AddSendType.EditItem(sendItemId = "sendId"), + ) + + composeTestRule + .onNodeWithText(text = "Delete send") + .performScrollTo() + .performClick() + + composeTestRule + .onNodeWithText(text = "Are you sure you want to delete this Send?") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + } + + @Test + fun `on delete confirmation dialog yes click should send DeleteClick`() { + mutableStateFlow.value = DEFAULT_STATE.copy( + addSendType = AddSendType.EditItem(sendItemId = "sendId"), + ) + + composeTestRule + .onNodeWithText(text = "Delete send") + .performScrollTo() + .performClick() + + composeTestRule + .onNodeWithText(text = "Yes") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(AddSendAction.DeleteClick) + } + } + @Test fun `policy warning should update according to state`() { val policyText = "Due to an enterprise policy, you are only " + diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreenTest.kt index b94aa475ad..65b450d5e6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/viewsend/ViewSendScreenTest.kt @@ -125,11 +125,30 @@ class ViewSendScreenTest : BaseComposeTest() { } @Test - fun `on delete click should send DeleteClick`() { + fun `on Delete button click should Display delete confirmation dialog`() { composeTestRule .onNodeWithText(text = "Delete send") .performScrollTo() .performClick() + + composeTestRule + .onNodeWithText(text = "Are you sure you want to delete this Send?") + .assert(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + } + + @Test + fun `oon delete confirmation dialog yes click should send DeleteClick`() { + composeTestRule + .onNodeWithText(text = "Delete send") + .performScrollTo() + .performClick() + + composeTestRule + .onNodeWithText(text = "Yes") + .assert(hasAnyAncestor(isDialog())) + .performClick() + verify(exactly = 1) { viewModel.trySendAction(ViewSendAction.DeleteClick) }