Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BWA-124 - 'Copy' Option Missing from Long-Press Menu #300

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
Expand Down Expand Up @@ -211,17 +212,10 @@ fun ItemListingScreen(
)
}
},
onEditItemClick = remember(viewModel) {
{
onDropdownMenuClick = remember(viewModel) {
{ action, itemId ->
viewModel.trySendAction(
ItemListingAction.EditItemClick(it),
)
}
},
onDeleteItemClick = remember(viewModel) {
{
viewModel.trySendAction(
ItemListingAction.DeleteItemClick(it),
ItemListingAction.DropdownMenuClick(action, itemId),
)
}
},
Expand All @@ -245,11 +239,6 @@ fun ItemListingScreen(
viewModel.trySendAction(ItemListingAction.SyncWithBitwardenDismiss)
}
},
onMoveToBitwardenClick = remember(viewModel) {
{
viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick(it))
}
},
)
}

Expand Down Expand Up @@ -354,9 +343,7 @@ private fun ItemListingContent(
onScanQrCodeClick: () -> Unit,
onEnterSetupKeyClick: () -> Unit,
onItemClick: (String) -> Unit,
onEditItemClick: (String) -> Unit,
onDeleteItemClick: (String) -> Unit,
onMoveToBitwardenClick: (String) -> Unit,
onDropdownMenuClick: (VaultDropdownMenuAction, String) -> Unit,
onDownloadBitwardenClick: () -> Unit,
onDismissDownloadBitwardenClick: () -> Unit,
onSyncWithBitwardenClick: () -> Unit,
Expand Down Expand Up @@ -467,9 +454,7 @@ private fun ItemListingContent(
alertThresholdSeconds = it.alertThresholdSeconds,
startIcon = it.startIcon,
onItemClick = { onItemClick(it.authCode) },
onEditItemClick = { onEditItemClick(it.id) },
onDeleteItemClick = { onDeleteItemClick(it.id) },
onMoveToBitwardenClick = { onMoveToBitwardenClick(it.id) },
onDropdownMenuClick = { action -> onDropdownMenuClick(action, it.id) },
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -508,9 +493,7 @@ private fun ItemListingContent(
alertThresholdSeconds = it.alertThresholdSeconds,
startIcon = it.startIcon,
onItemClick = { onItemClick(it.authCode) },
onEditItemClick = { onEditItemClick(it.id) },
onDeleteItemClick = { onDeleteItemClick(it.id) },
onMoveToBitwardenClick = { onMoveToBitwardenClick(it.id) },
onDropdownMenuClick = { action -> onDropdownMenuClick(action, it.id) },
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -544,9 +527,9 @@ private fun ItemListingContent(
alertThresholdSeconds = it.alertThresholdSeconds,
startIcon = it.startIcon,
onItemClick = { onItemClick(it.authCode) },
onEditItemClick = { },
onDeleteItemClick = { },
onMoveToBitwardenClick = { },
onDropdownMenuClick = { action ->
onDropdownMenuClick(action, it.id)
},
showMoveToBitwarden = it.showMoveToBitwarden,
allowLongPress = it.allowLongPressActions,
modifier = Modifier.fillMaxWidth(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.bitwarden.authenticator.data.platform.manager.imports.model.GoogleAut
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toDisplayItem
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toSharedCodesDisplayState
Expand Down Expand Up @@ -62,7 +63,6 @@ class ItemListingViewModel @Inject constructor(
) {

init {

settingsRepository
.authenticatorAlertThresholdSecondsFlow
.map { ItemListingAction.Internal.AlertThresholdSecondsReceive(it) }
Expand Down Expand Up @@ -142,6 +142,12 @@ class ItemListingViewModel @Inject constructor(
handleInternalAction(action)
}

is ItemListingAction.DropdownMenuClick -> {
viewModelScope.launch {
handleDropdownMenuClick(action)
david-livefront marked this conversation as resolved.
Show resolved Hide resolved
}
}

ItemListingAction.DownloadBitwardenClick -> {
handleDownloadBitwardenClick()
}
Expand Down Expand Up @@ -528,6 +534,17 @@ class ItemListingViewModel @Inject constructor(
sendEvent(ItemListingEvent.NavigateToBitwardenListing)
}

private suspend fun handleDropdownMenuClick(action: ItemListingAction.DropdownMenuClick) {
sendAction(
when (action.menuAction) {
VaultDropdownMenuAction.COPY -> ItemListingAction.ItemClick(action.id)
david-livefront marked this conversation as resolved.
Show resolved Hide resolved
VaultDropdownMenuAction.EDIT -> ItemListingAction.EditItemClick(action.id)
VaultDropdownMenuAction.MOVE -> ItemListingAction.MoveToBitwardenClick(action.id)
VaultDropdownMenuAction.DELETE -> ItemListingAction.DeleteItemClick(action.id)
},
)
}

private fun handleDownloadBitwardenDismiss() {
settingsRepository.hasUserDismissedDownloadBitwardenCard = true
mutableStateFlow.update {
Expand Down Expand Up @@ -830,6 +847,16 @@ sealed class ItemListingEvent {
* Each subclass of this sealed class denotes a distinct action that can be taken.
*/
sealed class ItemListingAction {
/**
* Represents an action triggered when the user clicks an item in the dropdown menu.
*
* @param menuAction The action selected from the dropdown menu.
* @param id The identifier of the item on which the action is being performed.
*/
data class DropdownMenuClick(
val menuAction: VaultDropdownMenuAction,
val id: String,
) : ItemListingAction()

/**
* The user clicked the back button.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.authenticator.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
import com.bitwarden.authenticator.ui.platform.components.model.IconData
Expand All @@ -46,8 +47,12 @@ import com.bitwarden.authenticator.ui.platform.theme.AuthenticatorTheme
* @param secondaryLabel The supporting label for the item. Represents the OTP account name.
* @param periodSeconds The times span where the code is valid.
* @param timeLeftSeconds The seconds remaining until a new code is needed.
* @param alertThresholdSeconds The time threshold in seconds to display an expiration warning.
* @param startIcon The leading icon for the item.
* @param onItemClick The lambda function to be invoked when the item is clicked.
* @param onDropdownMenuClick A lambda function invoked when a dropdown menu action is clicked.
* @param allowLongPress Whether long-press interactions are enabled for the item.
* @param showMoveToBitwarden Whether the option to move the item to Bitwarden is displayed.
* @param modifier The modifier for the item.
*/
@OptIn(ExperimentalFoundationApi::class)
Expand All @@ -62,9 +67,7 @@ fun VaultVerificationCodeItem(
alertThresholdSeconds: Int,
startIcon: IconData,
onItemClick: () -> Unit,
onEditItemClick: () -> Unit,
onDeleteItemClick: () -> Unit,
onMoveToBitwardenClick: () -> Unit,
onDropdownMenuClick: (VaultDropdownMenuAction) -> Unit,
allowLongPress: Boolean,
showMoveToBitwarden: Boolean,
modifier: Modifier = Modifier,
Expand Down Expand Up @@ -155,13 +158,29 @@ fun VaultVerificationCodeItem(
expanded = shouldShowDropdownMenu,
onDismissRequest = { shouldShowDropdownMenu = false },
) {
DropdownMenuItem(
text = {
Text(text = stringResource(id = R.string.copy))
},
onClick = {
shouldShowDropdownMenu = false
onDropdownMenuClick(VaultDropdownMenuAction.COPY)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_copy),
contentDescription = stringResource(id = R.string.copy),
)
},
)
HorizontalDivider()
DropdownMenuItem(
text = {
Text(text = stringResource(id = R.string.edit_item))
},
onClick = {
shouldShowDropdownMenu = false
onEditItemClick()
onDropdownMenuClick(VaultDropdownMenuAction.EDIT)
},
leadingIcon = {
Icon(
Expand All @@ -174,16 +193,16 @@ fun VaultVerificationCodeItem(
HorizontalDivider()
DropdownMenuItem(
text = {
Text(text = stringResource(id = R.string.copy_to_bitwarden))
Text(text = stringResource(id = R.string.move_to_bitwarden))
},
onClick = {
shouldShowDropdownMenu = false
onMoveToBitwardenClick()
onDropdownMenuClick(VaultDropdownMenuAction.MOVE)
},
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_right),
contentDescription = stringResource(id = R.string.copy_to_bitwarden),
contentDescription = stringResource(id = R.string.move_to_bitwarden),
)
},
)
Expand All @@ -195,7 +214,7 @@ fun VaultVerificationCodeItem(
},
onClick = {
shouldShowDropdownMenu = false
onDeleteItemClick()
onDropdownMenuClick(VaultDropdownMenuAction.DELETE)
},
leadingIcon = {
Icon(
Expand All @@ -222,9 +241,7 @@ private fun VerificationCodeItem_preview() {
alertThresholdSeconds = 7,
startIcon = IconData.Local(R.drawable.ic_login_item),
onItemClick = {},
onEditItemClick = {},
onDeleteItemClick = {},
onMoveToBitwardenClick = {},
onDropdownMenuClick = {},
allowLongPress = true,
modifier = Modifier.padding(horizontal = 16.dp),
showMoveToBitwarden = true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model

/**
* Enum representing the available actions in the Vault dropdown menu.
*/
enum class VaultDropdownMenuAction {
COPY,
EDIT,
MOVE,
DELETE,
}
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
<string name="sync_with_bitwarden_action_card_message">Allow Authenticator app syncing in settings to view all of your verification codes here.</string>
<string name="something_went_wrong">Something went wrong</string>
<string name="please_try_again">Please try again</string>
<string name="copy_to_bitwarden">Copy to Bitwarden</string>
<string name="move_to_bitwarden">Move to Bitwarden</string>
<string name="default_save_option">Default save option</string>
<string name="save_to_bitwarden">Save to Bitwarden</string>
<string name="save_here">Save here</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
import com.bitwarden.authenticator.ui.platform.base.BaseComposeTest
import com.bitwarden.authenticator.ui.platform.base.util.asText
Expand Down Expand Up @@ -202,7 +203,7 @@ class ItemListingScreenTest : BaseComposeTest() {
}

@Test
fun `clicking Copy to Bitwarden should send MoveToBitwardenClick`() {
fun `clicking Move to Bitwarden should send MoveToBitwardenClick`() {
mutableStateFlow.value = DEFAULT_STATE.copy(
viewState = ItemListingState.ViewState.Content(
actionCard = ItemListingState.ActionCardState.None,
Expand All @@ -216,10 +217,14 @@ class ItemListingScreenTest : BaseComposeTest() {
.performTouchInput { longClick() }

composeTestRule
.onNodeWithText("Copy to Bitwarden")
.onNodeWithText("Move to Bitwarden")
.performClick()

verify { viewModel.trySendAction(ItemListingAction.MoveToBitwardenClick("1")) }
verify {
viewModel.trySendAction(
ItemListingAction.DropdownMenuClick(VaultDropdownMenuAction.MOVE, "1"),
)
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.bitwarden.authenticator.data.platform.manager.clipboard.BitwardenClip
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toDisplayItem
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toSharedCodesDisplayState
import com.bitwarden.authenticator.ui.platform.base.BaseViewModelTest
Expand All @@ -31,7 +32,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class ItemListViewModelTest : BaseViewModelTest() {
class ItemListingViewModelTest : BaseViewModelTest() {

private val mutableAuthenticatorAlertThresholdFlow =
MutableStateFlow(AUTHENTICATOR_ALERT_SECONDS)
Expand Down Expand Up @@ -423,6 +424,70 @@ class ItemListViewModelTest : BaseViewModelTest() {
}
}

@Test
fun `should copy text to clipboard when DropdownMenuClick COPY is triggered`() = runTest {
val viewModel = createViewModel()
val testId = "123456"

every { clipboardManager.setText(text = testId) } just runs

viewModel.eventFlow.test {
viewModel.trySendAction(
ItemListingAction.DropdownMenuClick(VaultDropdownMenuAction.COPY, testId),
)

verify(exactly = 1) {
clipboardManager.setText(text = testId)
}

assertEquals(
ItemListingEvent.ShowToast(
message = R.string.value_has_been_copied.asText(testId),
),
awaitItem(),
)
}
}

@Test
fun `should trigger edit action when DropdownMenuClick EDIT is triggered`() = runTest {
val viewModel = createViewModel()
val testId = "123456"

viewModel.eventFlow.test {
viewModel.trySendAction(
ItemListingAction.DropdownMenuClick(VaultDropdownMenuAction.EDIT, testId),
)

assertEquals(
ItemListingEvent.NavigateToEditItem(testId),
awaitItem(),
)
}
}

@Test
fun `should trigger delete prompt when DropdownMenuClick DELETE is triggered`() = runTest {
val viewModel = createViewModel()
val testId = "123456"

val expectedState = DEFAULT_STATE.copy(
dialog = ItemListingState.DialogState.DeleteConfirmationPrompt(
message = R.string.do_you_really_want_to_permanently_delete_cipher.asText(),
testId,
),
)

viewModel.trySendAction(
ItemListingAction.DropdownMenuClick(VaultDropdownMenuAction.DELETE, testId),
)

assertEquals(
expectedState,
viewModel.stateFlow.value,
)
}

private fun createViewModel() = ItemListingViewModel(
authenticatorRepository = authenticatorRepository,
authenticatorBridgeManager = authenticatorBridgeManager,
Expand Down
Loading