Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 },
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(minutes))
}
},
is24Hour = true,
supportingContent = null,
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
) {
shouldShowViolatesPoliciesDialog = true
} else {
onCustomVaultTimeoutSelect(
VaultTimeout.Custom(
vaultTimeoutInMinutes = totalMinutes,
),
)
}
},
onDismissRequest = { shouldShowTimePickerDialog = false },
is24Hour = true,
)
}
)

if (shouldShowViolatesPoliciesDialog) {
BitwardenBasicDialog(
Expand All @@ -582,11 +552,7 @@ private fun SessionCustomTimeoutRow(
onDismissRequest = {
shouldShowViolatesPoliciesDialog = false
vaultTimeoutPolicy?.minutes?.let {
onCustomVaultTimeoutSelect(
VaultTimeout.Custom(
vaultTimeoutInMinutes = it,
),
)
onCustomVaultTimeoutSelect(VaultTimeout.Custom(it))
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -1056,15 +1056,15 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
}

composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.assertIsDisplayed()

mutableStateFlow.update {
it.copy(vaultTimeout = VaultTimeout.Custom(vaultTimeoutInMinutes = 1234))
}

composeTestRule
.onNode(hasTextExactly("Custom", "20:34"))
.onNode(hasTextExactly("Custom timeout", "20 hours, 34 minutes"))
.assertIsDisplayed()
}

Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -1158,7 +1158,7 @@ class AccountSecurityScreenTest : BitwardenComposeTest() {
)
}
composeTestRule
.onNode(hasTextExactly("Custom", "02:03"))
.onNode(hasTextExactly("Custom timeout", "2 hours, 3 minutes"))
.performScrollTo()
.performClick()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -161,11 +162,15 @@ fun BitwardenTextSelectionButton(
BitwardenRowOfActions(
modifier = Modifier.padding(paddingValues = actionsPadding),
actions = {
Icon(
painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down),
contentDescription = null,
modifier = Modifier.minimumInteractiveComponentSize(),
)
if (showChevron) {
Icon(
painter = rememberVectorPainter(
id = BitwardenDrawable.ic_chevron_down,
),
contentDescription = null,
modifier = Modifier.minimumInteractiveComponentSize(),
)
}
actions()
},
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
2 changes: 2 additions & 0 deletions ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Loading