PM-27046: Add overflow to Authenticator (#6039)

This commit is contained in:
David Perez 2025-10-16 13:15:41 -05:00 committed by GitHub
parent 53d04375b1
commit 714f7cfadc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 138 additions and 204 deletions

View File

@ -677,7 +677,7 @@ private fun ContentPreview() {
authCode = "123456",
favorite = false,
showMoveToBitwarden = true,
allowLongPressActions = true,
showOverflow = true,
),
),
sharedItems = SharedCodesDisplayState.Codes(
@ -698,7 +698,7 @@ private fun ContentPreview() {
authCode = "123456",
favorite = false,
showMoveToBitwarden = false,
allowLongPressActions = false,
showOverflow = false,
),
),
isExpanded = true,

View File

@ -512,7 +512,7 @@ class ItemListingViewModel @Inject constructor(
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
allowLongPressActions = true,
showOverflow = true,
)
}
.toImmutableList(),
@ -524,7 +524,7 @@ class ItemListingViewModel @Inject constructor(
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
allowLongPressActions = true,
showOverflow = true,
)
}
.toImmutableList(),

View File

@ -194,7 +194,7 @@ class ItemSearchViewModel @Inject constructor(
sharedVerificationCodesState = authenticatorRepository
.sharedCodesStateFlow
.value,
allowLongPressActions = false,
showOverflow = false,
)
}
.toImmutableList(),

View File

@ -27,7 +27,7 @@ fun SharedVerificationCodesState.Success.toSharedCodesDisplayState(
// Always map based on Error state, because shared codes will never
// show "Copy to Bitwarden vault" action.
sharedVerificationCodesState = SharedVerificationCodesState.Error,
allowLongPressActions = false,
showOverflow = false,
),
)
}

View File

@ -11,7 +11,7 @@ import com.bitwarden.authenticator.ui.platform.components.listitem.model.Verific
fun VerificationCodeItem.toDisplayItem(
alertThresholdSeconds: Int,
sharedVerificationCodesState: SharedVerificationCodesState,
allowLongPressActions: Boolean,
showOverflow: Boolean,
): VerificationCodeDisplayItem = VerificationCodeDisplayItem(
id = id,
title = issuer ?: label ?: "--",
@ -25,7 +25,7 @@ fun VerificationCodeItem.toDisplayItem(
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
authCode = code,
allowLongPressActions = allowLongPressActions,
showOverflow = showOverflow,
favorite = (source as? AuthenticatorItem.Source.Local)?.isFavorite ?: false,
showMoveToBitwarden = when (source) {
// Shared items should never show "Copy to Bitwarden vault" action:

View File

@ -1,39 +1,27 @@
package com.bitwarden.authenticator.ui.platform.components.listitem
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VerificationCodeDisplayItem
import com.bitwarden.ui.platform.base.util.cardBackground
import com.bitwarden.ui.platform.base.util.cardPadding
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
import com.bitwarden.core.util.persistentListOfNotNull
import com.bitwarden.ui.platform.base.util.cardStyle
import com.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.bitwarden.ui.platform.components.appbar.model.OverflowMenuItemData
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularCountdownIndicator
@ -69,7 +57,7 @@ fun VaultVerificationCodeItem(
startIcon = displayItem.startIcon,
onItemClick = onItemClick,
onDropdownMenuClick = onDropdownMenuClick,
allowLongPress = displayItem.allowLongPressActions,
showOverflow = displayItem.showOverflow,
showMoveToBitwarden = displayItem.showMoveToBitwarden,
cardStyle = cardStyle,
modifier = modifier,
@ -88,7 +76,7 @@ fun VaultVerificationCodeItem(
* @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 showOverflow Whether overflow menu should be available or not.
* @param showMoveToBitwarden Whether the option to move the item to Bitwarden is displayed.
* @param cardStyle The card style to be applied to this item.
* @param modifier The modifier for the item.
@ -105,166 +93,107 @@ fun VaultVerificationCodeItem(
startIcon: IconData,
onItemClick: () -> Unit,
onDropdownMenuClick: (VaultDropdownMenuAction) -> Unit,
allowLongPress: Boolean,
showOverflow: Boolean,
showMoveToBitwarden: Boolean,
cardStyle: CardStyle,
modifier: Modifier = Modifier,
) {
var shouldShowDropdownMenu by remember { mutableStateOf(value = false) }
Box(modifier = modifier) {
Row(
modifier = Modifier
.testTag(tag = "Item")
.defaultMinSize(minHeight = 60.dp)
.cardBackground(cardStyle = cardStyle)
.then(
if (allowLongPress) {
Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = onItemClick,
onLongClick = { shouldShowDropdownMenu = true },
)
} else {
Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(
color = BitwardenTheme.colorScheme.background.pressed,
),
onClick = onItemClick,
)
},
)
.cardPadding(
cardStyle = cardStyle,
paddingValues = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
Row(
modifier = modifier
.testTag(tag = "Item")
.defaultMinSize(minHeight = 60.dp)
.cardStyle(
cardStyle = cardStyle,
onClick = onItemClick,
paddingStart = 16.dp,
paddingEnd = 4.dp,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
BitwardenIcon(
iconData = startIcon,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(size = 24.dp),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.weight(weight = 1f),
) {
BitwardenIcon(
iconData = startIcon,
tint = BitwardenTheme.colorScheme.icon.primary,
modifier = Modifier.size(24.dp),
)
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.weight(1f),
) {
if (!primaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.testTag("Name"),
text = primaryLabel,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (!secondaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.testTag("Username"),
text = secondaryLabel,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (!primaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.testTag(tag = "Name"),
text = primaryLabel,
style = BitwardenTheme.typography.bodyLarge,
color = BitwardenTheme.colorScheme.text.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
BitwardenCircularCountdownIndicator(
modifier = Modifier.testTag("CircularCountDown"),
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
)
Text(
modifier = Modifier.testTag("AuthCode"),
text = authCode.chunked(3).joinToString(" "),
style = BitwardenTheme.typography.sensitiveInfoSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
if (!secondaryLabel.isNullOrEmpty()) {
Text(
modifier = Modifier.testTag(tag = "Username"),
text = secondaryLabel,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
DropdownMenu(
expanded = shouldShowDropdownMenu,
onDismissRequest = { shouldShowDropdownMenu = false },
shape = BitwardenTheme.shapes.menu,
containerColor = BitwardenTheme.colorScheme.background.primary,
) {
DropdownMenuItem(
text = {
Text(text = stringResource(id = BitwardenString.copy))
},
onClick = {
shouldShowDropdownMenu = false
onDropdownMenuClick(VaultDropdownMenuAction.COPY_CODE)
},
leadingIcon = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_copy),
contentDescription = stringResource(id = BitwardenString.copy),
)
},
)
BitwardenHorizontalDivider()
DropdownMenuItem(
text = {
Text(text = stringResource(id = BitwardenString.edit))
},
onClick = {
shouldShowDropdownMenu = false
onDropdownMenuClick(VaultDropdownMenuAction.EDIT)
},
leadingIcon = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_edit_item),
contentDescription = stringResource(BitwardenString.edit),
)
},
)
if (showMoveToBitwarden) {
BitwardenHorizontalDivider()
DropdownMenuItem(
text = {
Text(text = stringResource(id = BitwardenString.copy_to_bitwarden_vault))
},
onClick = {
shouldShowDropdownMenu = false
onDropdownMenuClick(VaultDropdownMenuAction.COPY_TO_BITWARDEN)
},
leadingIcon = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_arrow_right),
contentDescription = stringResource(
id = BitwardenString.copy_to_bitwarden_vault,
),
BitwardenCircularCountdownIndicator(
modifier = Modifier.testTag(tag = "CircularCountDown"),
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds,
)
Text(
modifier = Modifier.testTag(tag = "AuthCode"),
text = authCode.chunked(size = 3).joinToString(separator = " "),
style = BitwardenTheme.typography.sensitiveInfoSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
if (showOverflow) {
BitwardenOverflowActionItem(
contentDescription = stringResource(id = BitwardenString.more),
menuItemDataList = persistentListOfNotNull(
OverflowMenuItemData(
text = stringResource(id = BitwardenString.copy),
onClick = { onDropdownMenuClick(VaultDropdownMenuAction.COPY_CODE) },
),
OverflowMenuItemData(
text = stringResource(id = BitwardenString.edit),
onClick = { onDropdownMenuClick(VaultDropdownMenuAction.EDIT) },
),
if (showMoveToBitwarden) {
OverflowMenuItemData(
text = stringResource(id = BitwardenString.copy_to_bitwarden_vault),
onClick = {
onDropdownMenuClick(VaultDropdownMenuAction.COPY_TO_BITWARDEN)
},
)
} else {
null
},
)
}
BitwardenHorizontalDivider()
DropdownMenuItem(
text = {
Text(text = stringResource(id = BitwardenString.delete_item))
},
onClick = {
shouldShowDropdownMenu = false
onDropdownMenuClick(VaultDropdownMenuAction.DELETE)
},
leadingIcon = {
Icon(
painter = painterResource(id = BitwardenDrawable.ic_delete_item),
contentDescription = stringResource(id = BitwardenString.delete_item),
)
},
OverflowMenuItemData(
text = stringResource(id = BitwardenString.delete_item),
onClick = { onDropdownMenuClick(VaultDropdownMenuAction.DELETE) },
),
),
vectorIconRes = BitwardenDrawable.ic_ellipsis_horizontal,
testTag = "Options",
)
} else {
BitwardenStandardIconButton(
vectorIconRes = BitwardenDrawable.ic_copy,
contentDescription = stringResource(id = BitwardenString.copy),
onClick = onItemClick,
)
}
}
@ -285,7 +214,7 @@ private fun VerificationCodeItem_preview() {
startIcon = IconData.Local(BitwardenDrawable.ic_login_item),
onItemClick = {},
onDropdownMenuClick = {},
allowLongPress = true,
showOverflow = true,
modifier = Modifier.padding(horizontal = 16.dp),
showMoveToBitwarden = true,
cardStyle = CardStyle.Full,

View File

@ -22,6 +22,6 @@ data class VerificationCodeDisplayItem(
testTag = "BitwardenIcon",
),
val favorite: Boolean,
val allowLongPressActions: Boolean,
val showOverflow: Boolean,
val showMoveToBitwarden: Boolean,
) : Parcelable

View File

@ -2,7 +2,10 @@ package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@ -359,8 +362,10 @@ class ItemListingScreenTest : AuthenticatorComposeTest() {
),
)
composeTestRule
.onNodeWithText("issuer")
.performTouchInput { longClick() }
.onNodeWithText(text = "issuer")
.onChildren()
.filterToOne(hasContentDescription(value = "More"))
.performClick()
composeTestRule
.onNodeWithText(text = "Copy to Bitwarden vault")
@ -543,7 +548,7 @@ private val LOCAL_CODE = VerificationCodeDisplayItem(
alertThresholdSeconds = 7,
authCode = "123456",
favorite = false,
allowLongPressActions = true,
showOverflow = true,
showMoveToBitwarden = true,
)
@ -560,7 +565,7 @@ private val SHARED_ACCOUNTS_SECTION = SharedCodesDisplayState.SharedCodesAccount
alertThresholdSeconds = ALERT_THRESHOLD,
authCode = "123456",
favorite = false,
allowLongPressActions = false,
showOverflow = false,
showMoveToBitwarden = false,
),
),

View File

@ -571,7 +571,7 @@ private val LOCAL_CODE = VerificationCodeDisplayItem(
alertThresholdSeconds = 7,
authCode = "123456",
favorite = false,
allowLongPressActions = true,
showOverflow = true,
showMoveToBitwarden = true,
)
@ -620,7 +620,7 @@ private val LOCAL_DISPLAY_ITEMS = LOCAL_VERIFICATION_ITEMS.map {
it.toDisplayItem(
alertThresholdSeconds = AUTHENTICATOR_ALERT_SECONDS,
sharedVerificationCodesState = SharedVerificationCodesState.AppNotInstalled,
allowLongPressActions = true,
showOverflow = true,
)
}

View File

@ -144,7 +144,7 @@ private val SHARED_DISPLAY_ITEMS = SharedCodesDisplayState.Codes(
alertThresholdSeconds = 7,
authCode = "mockCode-2",
favorite = false,
allowLongPressActions = false,
showOverflow = false,
showMoveToBitwarden = false,
),
),
@ -167,7 +167,7 @@ private val LOCAL_DISPLAY_ITEMS = persistentListOf(
),
subtitle = LOCAL_ITEMS[0].label,
favorite = false,
allowLongPressActions = false,
showOverflow = false,
showMoveToBitwarden = true,
),
)

View File

@ -77,7 +77,7 @@ class SharedVerificationCodesStateTest {
title = "--",
subtitle = null,
favorite = false,
allowLongPressActions = false,
showOverflow = false,
alertThresholdSeconds = ALERT_THRESHOLD,
showMoveToBitwarden = false,
),
@ -100,7 +100,7 @@ class SharedVerificationCodesStateTest {
title = "issuer",
subtitle = "accountName",
favorite = false,
allowLongPressActions = false,
showOverflow = false,
alertThresholdSeconds = ALERT_THRESHOLD,
showMoveToBitwarden = false,
),
@ -169,7 +169,7 @@ class SharedVerificationCodesStateTest {
title = "--",
subtitle = null,
favorite = false,
allowLongPressActions = false,
showOverflow = false,
alertThresholdSeconds = ALERT_THRESHOLD,
showMoveToBitwarden = false,
),
@ -192,7 +192,7 @@ class SharedVerificationCodesStateTest {
title = "issuer",
subtitle = "accountName",
favorite = false,
allowLongPressActions = false,
showOverflow = false,
alertThresholdSeconds = ALERT_THRESHOLD,
showMoveToBitwarden = false,
),

View File

@ -29,7 +29,7 @@ class VerificationCodeItemExtensionsTest {
alertThresholdSeconds = alertThresholdSeconds,
authCode = favoriteItem.code,
favorite = (favoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
allowLongPressActions = true,
showOverflow = true,
showMoveToBitwarden = false,
)
@ -42,7 +42,7 @@ class VerificationCodeItemExtensionsTest {
alertThresholdSeconds = alertThresholdSeconds,
authCode = nonFavoriteItem.code,
favorite = (nonFavoriteItem.source as AuthenticatorItem.Source.Local).isFavorite,
allowLongPressActions = true,
showOverflow = true,
showMoveToBitwarden = false,
)
@ -51,7 +51,7 @@ class VerificationCodeItemExtensionsTest {
favoriteItem.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -59,7 +59,7 @@ class VerificationCodeItemExtensionsTest {
nonFavoriteItem.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
allowLongPressActions = true,
showOverflow = true,
),
)
}
@ -79,7 +79,7 @@ class VerificationCodeItemExtensionsTest {
alertThresholdSeconds = alertThresholdSeconds,
authCode = item.code,
favorite = false,
allowLongPressActions = true,
showOverflow = true,
showMoveToBitwarden = false,
)
@ -88,7 +88,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.AppNotInstalled,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -96,7 +96,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -104,7 +104,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.FeatureNotEnabled,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -112,7 +112,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Loading,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -120,7 +120,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.OsVersionNotSupported,
allowLongPressActions = true,
showOverflow = true,
),
)
assertEquals(
@ -128,7 +128,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.SyncNotEnabled,
allowLongPressActions = true,
showOverflow = true,
),
)
@ -140,7 +140,7 @@ class VerificationCodeItemExtensionsTest {
item.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Success(emptyList()),
allowLongPressActions = true,
showOverflow = true,
),
)
}
@ -168,7 +168,7 @@ class VerificationCodeItemExtensionsTest {
alertThresholdSeconds = alertThresholdSeconds,
authCode = favoriteItem.code,
favorite = false,
allowLongPressActions = false,
showOverflow = false,
showMoveToBitwarden = false,
)
@ -177,7 +177,7 @@ class VerificationCodeItemExtensionsTest {
favoriteItem.toDisplayItem(
alertThresholdSeconds = alertThresholdSeconds,
sharedVerificationCodesState = SharedVerificationCodesState.Error,
allowLongPressActions = false,
showOverflow = false,
),
)
}