Skip to content

Commit c16d31f

Browse files
PM-27494: Update custom vault timeout UI (#6085)
1 parent 43d7b84 commit c16d31f

File tree

5 files changed

+208
-61
lines changed

5 files changed

+208
-61
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt

Lines changed: 15 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import androidx.compose.ui.unit.dp
3232
import androidx.core.net.toUri
3333
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
3434
import androidx.lifecycle.compose.collectAsStateWithLifecycle
35-
import com.bitwarden.core.data.util.toFormattedPattern
3635
import com.bitwarden.ui.platform.base.util.EventsEffect
3736
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
3837
import com.bitwarden.ui.platform.components.account.dialog.BitwardenLogoutConfirmationDialog
@@ -43,9 +42,9 @@ import com.bitwarden.ui.platform.components.card.BitwardenActionCard
4342
import com.bitwarden.ui.platform.components.card.actionCardExitAnimation
4443
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
4544
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
46-
import com.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
4745
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
4846
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
47+
import com.bitwarden.ui.platform.components.dropdown.BitwardenTimePickerButton
4948
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
5049
import com.bitwarden.ui.platform.components.model.CardStyle
5150
import com.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
@@ -73,11 +72,8 @@ import com.x8bit.bitwarden.ui.platform.util.displayLabel
7372
import com.x8bit.bitwarden.ui.platform.util.minutes
7473
import kotlinx.collections.immutable.ImmutableList
7574
import kotlinx.collections.immutable.toImmutableList
76-
import java.time.LocalTime
7775
import javax.crypto.Cipher
7876

79-
private const val MINUTES_PER_HOUR = 60
80-
8177
/**
8278
* Displays the account security screen.
8379
*/
@@ -532,48 +528,22 @@ private fun SessionCustomTimeoutRow(
532528
onCustomVaultTimeoutSelect: (VaultTimeout.Custom) -> Unit,
533529
modifier: Modifier = Modifier,
534530
) {
535-
var shouldShowTimePickerDialog by rememberSaveable { mutableStateOf(false) }
536531
var shouldShowViolatesPoliciesDialog by remember { mutableStateOf(false) }
537-
val vaultTimeoutInMinutes = customVaultTimeout.vaultTimeoutInMinutes
538-
BitwardenTextRow(
539-
text = stringResource(id = BitwardenString.custom),
540-
onClick = { shouldShowTimePickerDialog = true },
532+
BitwardenTimePickerButton(
533+
label = stringResource(id = BitwardenString.custom_timeout),
534+
totalMinutes = customVaultTimeout.vaultTimeoutInMinutes,
535+
onTimeSelect = { minutes ->
536+
if (vaultTimeoutPolicy?.minutes != null && minutes > vaultTimeoutPolicy.minutes) {
537+
shouldShowViolatesPoliciesDialog = true
538+
} else {
539+
onCustomVaultTimeoutSelect(VaultTimeout.Custom(minutes))
540+
}
541+
},
542+
is24Hour = true,
543+
supportingContent = null,
541544
cardStyle = CardStyle.Middle(),
542545
modifier = modifier,
543-
) {
544-
Text(
545-
text = LocalTime
546-
.ofSecondOfDay(vaultTimeoutInMinutes * MINUTES_PER_HOUR.toLong())
547-
.toFormattedPattern(pattern = "HH:mm"),
548-
style = BitwardenTheme.typography.labelSmall,
549-
color = BitwardenTheme.colorScheme.text.primary,
550-
)
551-
}
552-
553-
if (shouldShowTimePickerDialog) {
554-
BitwardenTimePickerDialog(
555-
initialHour = vaultTimeoutInMinutes / MINUTES_PER_HOUR,
556-
initialMinute = vaultTimeoutInMinutes.mod(MINUTES_PER_HOUR),
557-
onTimeSelect = { hour, minute ->
558-
shouldShowTimePickerDialog = false
559-
560-
val totalMinutes = (hour * MINUTES_PER_HOUR) + minute
561-
if (vaultTimeoutPolicy?.minutes != null &&
562-
totalMinutes > vaultTimeoutPolicy.minutes
563-
) {
564-
shouldShowViolatesPoliciesDialog = true
565-
} else {
566-
onCustomVaultTimeoutSelect(
567-
VaultTimeout.Custom(
568-
vaultTimeoutInMinutes = totalMinutes,
569-
),
570-
)
571-
}
572-
},
573-
onDismissRequest = { shouldShowTimePickerDialog = false },
574-
is24Hour = true,
575-
)
576-
}
546+
)
577547

578548
if (shouldShowViolatesPoliciesDialog) {
579549
BitwardenBasicDialog(
@@ -582,11 +552,7 @@ private fun SessionCustomTimeoutRow(
582552
onDismissRequest = {
583553
shouldShowViolatesPoliciesDialog = false
584554
vaultTimeoutPolicy?.minutes?.let {
585-
onCustomVaultTimeoutSelect(
586-
VaultTimeout.Custom(
587-
vaultTimeoutInMinutes = it,
588-
),
589-
)
555+
onCustomVaultTimeoutSelect(VaultTimeout.Custom(it))
590556
}
591557
},
592558
)

app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
10471047
composeTestRule
10481048
// Check for exact text to differentiate from the Custom label on the Vault Timeout
10491049
// item above.
1050-
.onNode(hasTextExactly("Custom", "00:00"))
1050+
.onNode(hasTextExactly("Custom timeout", "0 minutes"))
10511051
.performScrollTo()
10521052
.assertIsDisplayed()
10531053

@@ -1056,15 +1056,15 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
10561056
}
10571057

10581058
composeTestRule
1059-
.onNode(hasTextExactly("Custom", "02:03"))
1059+
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
10601060
.assertIsDisplayed()
10611061

10621062
mutableStateFlow.update {
10631063
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 1234))
10641064
}
10651065

10661066
composeTestRule
1067-
.onNode(hasTextExactly("Custom", "20:34"))
1067+
.onNode(hasTextExactly("Custom timeout", "20 hours, 34 minutes"))
10681068
.assertIsDisplayed()
10691069
}
10701070

@@ -1076,7 +1076,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
10761076
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
10771077
}
10781078
composeTestRule
1079-
.onNode(hasTextExactly("Custom", "02:03"))
1079+
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
10801080
.performScrollTo()
10811081
.performClick()
10821082

@@ -1102,7 +1102,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
11021102
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
11031103
}
11041104
composeTestRule
1105-
.onNode(hasTextExactly("Custom", "02:03"))
1105+
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
11061106
.performScrollTo()
11071107
.performClick()
11081108

@@ -1123,7 +1123,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
11231123
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 123))
11241124
}
11251125
composeTestRule
1126-
.onNode(hasTextExactly("Custom", "02:03"))
1126+
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
11271127
.performScrollTo()
11281128
.performClick()
11291129

@@ -1158,7 +1158,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
11581158
)
11591159
}
11601160
composeTestRule
1161-
.onNode(hasTextExactly("Custom", "02:03"))
1161+
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
11621162
.performScrollTo()
11631163
.performClick()
11641164

ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ fun BitwardenTextSelectionButton(
9999
cardStyle: CardStyle?,
100100
modifier: Modifier = Modifier,
101101
enabled: Boolean = true,
102+
showChevron: Boolean = true,
102103
tooltip: TooltipData? = null,
103104
insets: PaddingValues = PaddingValues(),
104105
textFieldTestTag: String? = null,
@@ -161,11 +162,15 @@ fun BitwardenTextSelectionButton(
161162
BitwardenRowOfActions(
162163
modifier = Modifier.padding(paddingValues = actionsPadding),
163164
actions = {
164-
Icon(
165-
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down),
166-
contentDescription = null,
167-
modifier = Modifier.minimumInteractiveComponentSize(),
168-
)
165+
if (showChevron) {
166+
Icon(
167+
painter = rememberVectorPainter(
168+
id = BitwardenDrawable.ic_chevron_down,
169+
),
170+
contentDescription = null,
171+
modifier = Modifier.minimumInteractiveComponentSize(),
172+
)
173+
}
169174
actions()
170175
},
171176
)
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package com.bitwarden.ui.platform.components.dropdown
2+
3+
import androidx.compose.foundation.layout.ColumnScope
4+
import androidx.compose.foundation.layout.PaddingValues
5+
import androidx.compose.foundation.layout.RowScope
6+
import androidx.compose.material3.OutlinedTextField
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.getValue
9+
import androidx.compose.runtime.mutableStateOf
10+
import androidx.compose.runtime.saveable.rememberSaveable
11+
import androidx.compose.runtime.setValue
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.res.pluralStringResource
14+
import androidx.compose.ui.res.stringResource
15+
import androidx.compose.ui.unit.dp
16+
import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
17+
import com.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
18+
import com.bitwarden.ui.platform.components.model.CardStyle
19+
import com.bitwarden.ui.platform.components.model.TooltipData
20+
import com.bitwarden.ui.platform.resource.BitwardenPlurals
21+
import com.bitwarden.ui.platform.resource.BitwardenString
22+
23+
private const val MINUTES_PER_HOUR: Int = 60
24+
25+
/**
26+
* A button that displays a selected time duration and opens a time picker dialog when clicked.
27+
*
28+
* @param label The descriptive text label for the [OutlinedTextField].
29+
* @param totalMinutes The currently selected time value in minutes.
30+
* @param onTimeSelect A lambda that is invoked when a time is selected from the menu.
31+
* @param is24Hour Whether or not the time should be displayed in 24-hour format.
32+
* @param cardStyle Indicates the type of card style to be applied.
33+
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
34+
* @param isEnabled Whether or not the button is enabled.
35+
* @param supportingContent An optional supporting content that will appear below the button.
36+
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
37+
* @param insets Inner padding to be applied within the card.
38+
* @param textFieldTestTag The optional test tag associated with the inner text field.
39+
* @param actionsPadding Padding to be applied to the [actions] block.
40+
* @param actions A lambda containing the set of actions (usually icons or similar) to display
41+
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
42+
* defining the layout of the actions.
43+
*/
44+
@Composable
45+
fun BitwardenTimePickerButton(
46+
label: String,
47+
totalMinutes: Int,
48+
onTimeSelect: (minutes: Int) -> Unit,
49+
is24Hour: Boolean,
50+
cardStyle: CardStyle?,
51+
modifier: Modifier = Modifier,
52+
isEnabled: Boolean = true,
53+
supportingContent: @Composable (ColumnScope.() -> Unit)?,
54+
tooltip: TooltipData? = null,
55+
insets: PaddingValues = PaddingValues(),
56+
textFieldTestTag: String? = null,
57+
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
58+
actions: @Composable RowScope.() -> Unit = {},
59+
) {
60+
BitwardenTimePickerButton(
61+
label = label,
62+
hours = totalMinutes / MINUTES_PER_HOUR,
63+
minutes = totalMinutes.mod(MINUTES_PER_HOUR),
64+
onTimeSelect = { hour, minute -> onTimeSelect((hour * MINUTES_PER_HOUR) + minute) },
65+
cardStyle = cardStyle,
66+
is24Hour = is24Hour,
67+
modifier = modifier,
68+
isEnabled = isEnabled,
69+
supportingContent = supportingContent,
70+
tooltip = tooltip,
71+
insets = insets,
72+
textFieldTestTag = textFieldTestTag,
73+
actionsPadding = actionsPadding,
74+
actions = actions,
75+
)
76+
}
77+
78+
/**
79+
* A button that displays a selected time duration and opens a time picker dialog when clicked.
80+
*
81+
* @param label The descriptive text label for the [OutlinedTextField].
82+
* @param hours The currently selected time value in hours.
83+
* @param minutes The currently selected time value in minutes.
84+
* @param onTimeSelect A lambda that is invoked when a time is selected from the menu.
85+
* @param is24Hour Whether or not the time should be displayed in 24-hour format.
86+
* @param cardStyle Indicates the type of card style to be applied.
87+
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
88+
* @param isEnabled Whether or not the button is enabled.
89+
* @param supportingContent An optional supporting content that will appear below the button.
90+
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
91+
* @param insets Inner padding to be applied within the card.
92+
* @param textFieldTestTag The optional test tag associated with the inner text field.
93+
* @param actionsPadding Padding to be applied to the [actions] block.
94+
* @param actions A lambda containing the set of actions (usually icons or similar) to display
95+
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
96+
* defining the layout of the actions.
97+
*/
98+
@Composable
99+
fun BitwardenTimePickerButton(
100+
label: String,
101+
hours: Int,
102+
minutes: Int,
103+
onTimeSelect: (hour: Int, minute: Int) -> Unit,
104+
is24Hour: Boolean,
105+
cardStyle: CardStyle?,
106+
modifier: Modifier = Modifier,
107+
isEnabled: Boolean = true,
108+
supportingContent: @Composable (ColumnScope.() -> Unit)?,
109+
tooltip: TooltipData? = null,
110+
insets: PaddingValues = PaddingValues(),
111+
textFieldTestTag: String? = null,
112+
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
113+
actions: @Composable RowScope.() -> Unit = {},
114+
) {
115+
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
116+
BitwardenTextSelectionButton(
117+
label = label,
118+
selectedOption = if (hours != 0 && minutes != 0) {
119+
// Since both hours and minutes are non-zero, we display both of them.
120+
stringResource(
121+
id = BitwardenString.hours_minutes_format,
122+
formatArgs = arrayOf(
123+
pluralStringResource(
124+
id = BitwardenPlurals.hours_format,
125+
count = hours,
126+
formatArgs = arrayOf(hours),
127+
),
128+
pluralStringResource(
129+
id = BitwardenPlurals.minutes_format,
130+
count = minutes,
131+
formatArgs = arrayOf(minutes),
132+
),
133+
),
134+
)
135+
} else if (hours != 0) {
136+
// Since only hours are non-zero, we only display hours.
137+
pluralStringResource(
138+
id = BitwardenPlurals.hours_format,
139+
count = hours,
140+
formatArgs = arrayOf(hours),
141+
)
142+
} else {
143+
// We display this if there are only minutes or if both hours and minutes are 0.
144+
pluralStringResource(
145+
id = BitwardenPlurals.minutes_format,
146+
count = minutes,
147+
formatArgs = arrayOf(minutes),
148+
)
149+
},
150+
onClick = { shouldShowDialog = true },
151+
cardStyle = cardStyle,
152+
enabled = isEnabled,
153+
showChevron = false,
154+
supportingContent = supportingContent,
155+
tooltip = tooltip,
156+
insets = insets,
157+
textFieldTestTag = textFieldTestTag,
158+
actionsPadding = actionsPadding,
159+
actions = actions,
160+
modifier = modifier,
161+
)
162+
if (shouldShowDialog) {
163+
BitwardenTimePickerDialog(
164+
initialHour = hours,
165+
initialMinute = minutes,
166+
onTimeSelect = { hour, minute ->
167+
onTimeSelect(hour, minute)
168+
shouldShowDialog = false
169+
},
170+
onDismissRequest = { shouldShowDialog = false },
171+
is24Hour = is24Hour,
172+
)
173+
}
174+
}

ui/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ Scanning will happen automatically.</string>
479479
<string name="seven_days">7 days</string>
480480
<string name="thirty_days">30 days</string>
481481
<string name="custom">Custom</string>
482+
<string name="custom_timeout">Custom timeout</string>
482483
<string name="add_this_authenticator_key_to_a_login">Add this authenticator key to an existing login, or create a new login.</string>
483484
<string name="send_disabled_warning">Due to an enterprise policy, you are only able to delete an existing Send.</string>
484485
<string name="about_send">About Send</string>
@@ -497,6 +498,7 @@ Scanning will happen automatically.</string>
497498
<string name="fido2_authenticate_web_authn">Authenticate WebAuthn</string>
498499
<string name="fido2_return_to_app">Return to app</string>
499500
<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>
501+
<string name="hours_minutes_format" comment="Used to display a number of hours and minutes">%1$s, %2$s</string>
500502
<plurals name="hours_format" comment="Can be injected into a sentence with %1$s and %2$s">
501503
<item quantity="one">%1$d hour</item>
502504
<item quantity="other">%1$d hours</item>

0 commit comments

Comments
 (0)