PM-27494: Update custom vault timeout UI (#6085)

This commit is contained in:
David Perez
2025-10-27 16:13:36 -05:00
committed by GitHub
parent 43d7b84d0a
commit c16d31fb33
5 changed files with 208 additions and 61 deletions

View File

@@ -32,7 +32,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.core.data.util.toFormattedPattern
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.account.dialog.BitwardenLogoutConfirmationDialog
@@ -43,9 +42,9 @@ import com.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.dropdown.BitwardenTimePickerButton
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
@@ -73,11 +72,8 @@ import com.x8bit.bitwarden.ui.platform.util.displayLabel
import com.x8bit.bitwarden.ui.platform.util.minutes
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.time.LocalTime
import javax.crypto.Cipher
private const val MINUTES_PER_HOUR = 60
/**
* Displays the account security screen.
*/
@@ -532,48 +528,22 @@ private fun SessionCustomTimeoutRow(
onCustomVaultTimeoutSelect: (VaultTimeout.Custom) -> Unit,
modifier: Modifier = Modifier,
) {
var shouldShowTimePickerDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowViolatesPoliciesDialog by remember { mutableStateOf(false) }
val vaultTimeoutInMinutes = customVaultTimeout.vaultTimeoutInMinutes
BitwardenTextRow(
text = stringResource(id = BitwardenString.custom),
onClick = { shouldShowTimePickerDialog = true },
cardStyle = CardStyle.Middle(),
modifier = modifier,
) {
Text(
text = LocalTime
.ofSecondOfDay(vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong())
.toFormattedPattern(pattern = "HH:mm"),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
if (shouldShowTimePickerDialog) {
BitwardenTimePickerDialog(
initialHour = vaultTimeoutInMinutes / MINUTES_PER_HOUR,
initialMinute = vaultTimeoutInMinutes.mod(MINUTES_PER_HOUR),
onTimeSelect = { hour, minute ->
shouldShowTimePickerDialog = false
val totalMinutes = (hour * MINUTES_PER_HOUR) + minute
if (vaultTimeoutPolicy?.minutes != null &&
totalMinutes > vaultTimeoutPolicy.minutes
) {
BitwardenTimePickerButton(
label = stringResource(id = BitwardenString.custom_timeout),
totalMinutes = customVaultTimeout.vaultTimeoutInMinutes,
onTimeSelect = { minutes ->
if (vaultTimeoutPolicy?.minutes != null && minutes > vaultTimeoutPolicy.minutes) {
shouldShowViolatesPoliciesDialog = true
} else {
onCustomVaultTimeoutSelect(
VaultTimeout.Custom(
vaultTimeoutInMinutes = totalMinutes,
),
)
onCustomVaultTimeoutSelect(VaultTimeout.Custom(minutes))
}
},
onDismissRequest = { shouldShowTimePickerDialog = false },
is24Hour = true,
supportingContent = null,
cardStyle = CardStyle.Middle(),
modifier = modifier,
)
}
if (shouldShowViolatesPoliciesDialog) {
BitwardenBasicDialog(
@@ -582,11 +552,7 @@ private fun SessionCustomTimeoutRow(
onDismissRequest = {
shouldShowViolatesPoliciesDialog = false
vaultTimeoutPolicy?.minutes?.let {
onCustomVaultTimeoutSelect(
VaultTimeout.Custom(
vaultTimeoutInMinutes = it,
),
)
onCustomVaultTimeoutSelect(VaultTimeout.Custom(it))
}
},
)

View File

@@ -1047,7 +1047,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
composeTestRule
// Check for exact text to differentiate from the Custom label on the Vault Timeout
// item above.
.onNode(hasTextExactly("Custom", "00:00"))
.onNode(hasTextExactly("Custom timeout", "0 minutes"))
.performScrollTo()
.assertIsDisplayed()
@@ -1056,7 +1056,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.assertIsDisplayed()
mutableStateFlow.update {
@@ -1064,7 +1064,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
}
composeTestRule
.onNode(hasTextExactly("Custom", "20:34"))
.onNode(hasTextExactly("Custom timeout", "20 hours, 34 minutes"))
.assertIsDisplayed()
}
@@ -1076,7 +1076,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.performScrollTo()
.performClick()
@@ -1102,7 +1102,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.performScrollTo()
.performClick()
@@ -1123,7 +1123,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.performScrollTo()
.performClick()
@@ -1158,7 +1158,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
)
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.performScrollTo()
.performClick()

View File

@@ -99,6 +99,7 @@ fun BitwardenTextSelectionButton(
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
enabled: Boolean = true,
showChevron: Boolean = true,
tooltip: TooltipData? = null,
insets: PaddingValues = PaddingValues(),
textFieldTestTag: String? = null,
@@ -161,11 +162,15 @@ fun BitwardenTextSelectionButton(
BitwardenRowOfActions(
modifier = Modifier.padding(paddingValues = actionsPadding),
actions = {
if (showChevron) {
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down),
painter = rememberVectorPainter(
id = BitwardenDrawable.ic_chevron_down,
),
contentDescription = null,
modifier = Modifier.minimumInteractiveComponentSize(),
)
}
actions()
},
)

View File

@@ -0,0 +1,174 @@
package com.bitwarden.ui.platform.components.dropdown
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
import com.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.model.TooltipData
import com.bitwarden.ui.platform.resource.BitwardenPlurals
import com.bitwarden.ui.platform.resource.BitwardenString
private const val MINUTES_PER_HOUR: Int = 60
/**
* A button that displays a selected time duration and opens a time picker dialog when clicked.
*
* @param label The descriptive text label for the [OutlinedTextField].
* @param totalMinutes The currently selected time value in minutes.
* @param onTimeSelect A lambda that is invoked when a time is selected from the menu.
* @param is24Hour Whether or not the time should be displayed in 24-hour format.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param isEnabled Whether or not the button is enabled.
* @param supportingContent An optional supporting content that will appear below the button.
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
* @param insets Inner padding to be applied within the card.
* @param textFieldTestTag The optional test tag associated with the inner text field.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
*/
@Composable
fun BitwardenTimePickerButton(
label: String,
totalMinutes: Int,
onTimeSelect: (minutes: Int) -> Unit,
is24Hour: Boolean,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
supportingContent: @Composable (ColumnScope.() -> Unit)?,
tooltip: TooltipData? = null,
insets: PaddingValues = PaddingValues(),
textFieldTestTag: String? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actions: @Composable RowScope.() -> Unit = {},
) {
BitwardenTimePickerButton(
label = label,
hours = totalMinutes / MINUTES_PER_HOUR,
minutes = totalMinutes.mod(MINUTES_PER_HOUR),
onTimeSelect = { hour, minute -> onTimeSelect((hour * MINUTES_PER_HOUR) + minute) },
cardStyle = cardStyle,
is24Hour = is24Hour,
modifier = modifier,
isEnabled = isEnabled,
supportingContent = supportingContent,
tooltip = tooltip,
insets = insets,
textFieldTestTag = textFieldTestTag,
actionsPadding = actionsPadding,
actions = actions,
)
}
/**
* A button that displays a selected time duration and opens a time picker dialog when clicked.
*
* @param label The descriptive text label for the [OutlinedTextField].
* @param hours The currently selected time value in hours.
* @param minutes The currently selected time value in minutes.
* @param onTimeSelect A lambda that is invoked when a time is selected from the menu.
* @param is24Hour Whether or not the time should be displayed in 24-hour format.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param isEnabled Whether or not the button is enabled.
* @param supportingContent An optional supporting content that will appear below the button.
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
* @param insets Inner padding to be applied within the card.
* @param textFieldTestTag The optional test tag associated with the inner text field.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions.
*/
@Composable
fun BitwardenTimePickerButton(
label: String,
hours: Int,
minutes: Int,
onTimeSelect: (hour: Int, minute: Int) -> Unit,
is24Hour: Boolean,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
supportingContent: @Composable (ColumnScope.() -> Unit)?,
tooltip: TooltipData? = null,
insets: PaddingValues = PaddingValues(),
textFieldTestTag: String? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
actions: @Composable RowScope.() -> Unit = {},
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
BitwardenTextSelectionButton(
label = label,
selectedOption = if (hours != 0 && minutes != 0) {
// Since both hours and minutes are non-zero, we display both of them.
stringResource(
id = BitwardenString.hours_minutes_format,
formatArgs = arrayOf(
pluralStringResource(
id = BitwardenPlurals.hours_format,
count = hours,
formatArgs = arrayOf(hours),
),
pluralStringResource(
id = BitwardenPlurals.minutes_format,
count = minutes,
formatArgs = arrayOf(minutes),
),
),
)
} else if (hours != 0) {
// Since only hours are non-zero, we only display hours.
pluralStringResource(
id = BitwardenPlurals.hours_format,
count = hours,
formatArgs = arrayOf(hours),
)
} else {
// We display this if there are only minutes or if both hours and minutes are 0.
pluralStringResource(
id = BitwardenPlurals.minutes_format,
count = minutes,
formatArgs = arrayOf(minutes),
)
},
onClick = { shouldShowDialog = true },
cardStyle = cardStyle,
enabled = isEnabled,
showChevron = false,
supportingContent = supportingContent,
tooltip = tooltip,
insets = insets,
textFieldTestTag = textFieldTestTag,
actionsPadding = actionsPadding,
actions = actions,
modifier = modifier,
)
if (shouldShowDialog) {
BitwardenTimePickerDialog(
initialHour = hours,
initialMinute = minutes,
onTimeSelect = { hour, minute ->
onTimeSelect(hour, minute)
shouldShowDialog = false
},
onDismissRequest = { shouldShowDialog = false },
is24Hour = is24Hour,
)
}
}

View File

@@ -479,6 +479,7 @@ Scanning will happen automatically.</string>
<string name="seven_days">7 days</string>
<string name="thirty_days">30 days</string>
<string name="custom">Custom</string>
<string name="custom_timeout">Custom timeout</string>
<string name="add_this_authenticator_key_to_a_login">Add this authenticator key to an existing login, or create a new login.</string>
<string name="send_disabled_warning">Due to an enterprise policy, you are only able to delete an existing Send.</string>
<string name="about_send">About Send</string>
@@ -497,6 +498,7 @@ Scanning will happen automatically.</string>
<string name="fido2_authenticate_web_authn">Authenticate WebAuthn</string>
<string name="fido2_return_to_app">Return to app</string>
<string name="reset_password_auto_enroll_invite_warning">This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password.</string>
<string name="hours_minutes_format" comment="Used to display a number of hours and minutes">%1$s, %2$s</string>
<plurals name="hours_format" comment="Can be injected into a sentence with %1$s and %2$s">
<item quantity="one">%1$d hour</item>
<item quantity="other">%1$d hours</item>