diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt index 7f28f0332c..4c70d4ca19 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics @@ -68,6 +69,7 @@ import kotlinx.collections.immutable.persistentListOf * @param supportingText A optional supporting text that will appear below the text field. * @param tooltip A nullable [TooltipData], representing the tooltip icon. * @param insets Inner padding to be applied withing the card. + * @param textFieldTestTag The optional test tag associated with the inner text field. * @param cardStyle Indicates the type of card style to be applied. * @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 @@ -86,6 +88,7 @@ fun BitwardenMultiSelectButton( supportingText: String? = null, tooltip: TooltipData? = null, insets: PaddingValues = PaddingValues(), + textFieldTestTag: String? = null, cardStyle: CardStyle? = null, actionsPadding: PaddingValues = PaddingValues(), actions: @Composable RowScope.() -> Unit = {}, @@ -161,6 +164,7 @@ fun BitwardenMultiSelectButton( }, colors = bitwardenTextFieldButtonColors(), modifier = Modifier + .run { textFieldTestTag?.let { testTag(tag = it) } ?: this } .weight(weight = 1f) .fillMaxWidth(), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 8ba9ba91cb..791e1ed3f6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity +import android.content.res.Resources import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column @@ -47,10 +48,9 @@ import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLogoutConfirmationDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow @@ -68,6 +68,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.displayLabel import com.x8bit.bitwarden.ui.platform.util.minutes import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern +import kotlinx.collections.immutable.toImmutableList import java.time.LocalTime import javax.crypto.Cipher @@ -497,75 +498,51 @@ private fun SessionTimeoutPolicyRow( } } -@Suppress("LongMethod") @Composable private fun SessionTimeoutRow( vaultTimeoutPolicyMinutes: Int?, selectedVaultTimeoutType: VaultTimeout.Type, onVaultTimeoutTypeSelect: (VaultTimeout.Type) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var shouldShowSelectionDialog by remember { mutableStateOf(false) } var shouldShowNeverTimeoutConfirmationDialog by remember { mutableStateOf(false) } - BitwardenTextRow( - text = stringResource(id = R.string.session_timeout), - onClick = { shouldShowSelectionDialog = true }, + val vaultTimeoutOptions = VaultTimeout.Type + .entries + .filter { it.minutes <= (vaultTimeoutPolicyMinutes ?: Int.MAX_VALUE) } + BitwardenMultiSelectButton( + label = stringResource(id = R.string.session_timeout), + options = vaultTimeoutOptions.map { it.displayLabel() }.toImmutableList(), + selectedOption = selectedVaultTimeoutType.displayLabel(), + onOptionSelected = { selectedType -> + val selectedOption = vaultTimeoutOptions.first { + it.displayLabel.toString(resources) == selectedType + } + if (selectedOption == VaultTimeout.Type.NEVER) { + shouldShowNeverTimeoutConfirmationDialog = true + } else { + onVaultTimeoutTypeSelect(selectedOption) + } + }, + textFieldTestTag = "SessionTimeoutStatusLabel", cardStyle = CardStyle.Top(), modifier = modifier, - ) { - Text( - text = selectedVaultTimeoutType.displayLabel(), - style = BitwardenTheme.typography.labelSmall, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier.testTag("SessionTimeoutStatusLabel"), + ) + + if (shouldShowNeverTimeoutConfirmationDialog) { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.warning), + message = stringResource(id = R.string.never_lock_warning), + confirmButtonText = stringResource(id = R.string.ok), + dismissButtonText = stringResource(id = R.string.cancel), + onConfirmClick = { + shouldShowNeverTimeoutConfirmationDialog = false + onVaultTimeoutTypeSelect(VaultTimeout.Type.NEVER) + }, + onDismissClick = { shouldShowNeverTimeoutConfirmationDialog = false }, + onDismissRequest = { shouldShowNeverTimeoutConfirmationDialog = false }, ) } - - when { - shouldShowSelectionDialog -> { - val vaultTimeoutOptions = VaultTimeout.Type.entries - .filter { - it.minutes <= (vaultTimeoutPolicyMinutes ?: Int.MAX_VALUE) - } - - BitwardenSelectionDialog( - title = stringResource(id = R.string.session_timeout), - onDismissRequest = { shouldShowSelectionDialog = false }, - ) { - vaultTimeoutOptions.forEach { vaultTimeoutOption -> - BitwardenSelectionRow( - text = vaultTimeoutOption.displayLabel, - onClick = { - shouldShowSelectionDialog = false - val selectedType = - vaultTimeoutOptions.first { it == vaultTimeoutOption } - if (selectedType == VaultTimeout.Type.NEVER) { - shouldShowNeverTimeoutConfirmationDialog = true - } else { - onVaultTimeoutTypeSelect(selectedType) - } - }, - isSelected = selectedVaultTimeoutType == vaultTimeoutOption, - ) - } - } - } - - shouldShowNeverTimeoutConfirmationDialog -> { - BitwardenTwoButtonDialog( - title = stringResource(id = R.string.warning), - message = stringResource(id = R.string.never_lock_warning), - confirmButtonText = stringResource(id = R.string.ok), - dismissButtonText = stringResource(id = R.string.cancel), - onConfirmClick = { - shouldShowNeverTimeoutConfirmationDialog = false - onVaultTimeoutTypeSelect(VaultTimeout.Type.NEVER) - }, - onDismissClick = { shouldShowNeverTimeoutConfirmationDialog = false }, - onDismissRequest = { shouldShowNeverTimeoutConfirmationDialog = false }, - ) - } - } } @Suppress("LongMethod") @@ -640,7 +617,6 @@ private fun SessionCustomTimeoutRow( } } -@Suppress("LongMethod") @Composable private fun SessionTimeoutActionRow( isEnabled: Boolean, @@ -648,78 +624,52 @@ private fun SessionTimeoutActionRow( selectedVaultTimeoutAction: VaultTimeoutAction, onVaultTimeoutActionSelect: (VaultTimeoutAction) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var shouldShowSelectionDialog by rememberSaveable { mutableStateOf(false) } var shouldShowLogoutActionConfirmationDialog by rememberSaveable { mutableStateOf(false) } - BitwardenTextRow( + BitwardenMultiSelectButton( isEnabled = isEnabled, - text = stringResource(id = R.string.session_timeout_action), - description = stringResource( + label = stringResource(id = R.string.session_timeout_action), + options = VaultTimeoutAction.entries.map { it.displayLabel() }.toImmutableList(), + selectedOption = selectedVaultTimeoutAction.displayLabel(), + onOptionSelected = { action -> + // The option is not selectable if there's a policy in place. + if (vaultTimeoutPolicyAction != null) return@BitwardenMultiSelectButton + val selectedAction = VaultTimeoutAction.entries.first { + it.displayLabel.toString(resources) == action + } + if (selectedAction == VaultTimeoutAction.LOGOUT) { + shouldShowLogoutActionConfirmationDialog = true + } else { + onVaultTimeoutActionSelect(selectedAction) + } + }, + supportingText = stringResource( id = R.string.set_up_an_unlock_option_to_change_your_vault_timeout_action, ) .takeUnless { isEnabled }, - onClick = { - // The option is not selectable if there's a policy in place. - if (vaultTimeoutPolicyAction != null) return@BitwardenTextRow - shouldShowSelectionDialog = true - }, + textFieldTestTag = "SessionTimeoutActionStatusLabel", cardStyle = CardStyle.Bottom, modifier = modifier, - ) { - Text( - text = selectedVaultTimeoutAction.displayLabel(), - style = BitwardenTheme.typography.labelSmall, - color = if (isEnabled) { - BitwardenTheme.colorScheme.text.primary - } else { - BitwardenTheme.colorScheme.filledButton.foregroundDisabled - }, - modifier = Modifier.testTag("SessionTimeoutActionStatusLabel"), - ) - } - when { - shouldShowSelectionDialog -> { - BitwardenSelectionDialog( - title = stringResource(id = R.string.vault_timeout_action), - onDismissRequest = { shouldShowSelectionDialog = false }, - ) { - val vaultTimeoutActionOptions = VaultTimeoutAction.entries - vaultTimeoutActionOptions.forEach { option -> - BitwardenSelectionRow( - text = option.displayLabel, - isSelected = option == selectedVaultTimeoutAction, - onClick = { - shouldShowSelectionDialog = false - val selectedAction = vaultTimeoutActionOptions.first { it == option } - if (selectedAction == VaultTimeoutAction.LOGOUT) { - shouldShowLogoutActionConfirmationDialog = true - } else { - onVaultTimeoutActionSelect(selectedAction) - } - }, - ) - } - } - } + ) - shouldShowLogoutActionConfirmationDialog -> { - BitwardenTwoButtonDialog( - title = stringResource(id = R.string.warning), - message = stringResource(id = R.string.vault_timeout_log_out_confirmation), - confirmButtonText = stringResource(id = R.string.yes), - dismissButtonText = stringResource(id = R.string.cancel), - onConfirmClick = { - shouldShowLogoutActionConfirmationDialog = false - onVaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT) - }, - onDismissClick = { - shouldShowLogoutActionConfirmationDialog = false - }, - onDismissRequest = { - shouldShowLogoutActionConfirmationDialog = false - }, - ) - } + if (shouldShowLogoutActionConfirmationDialog) { + BitwardenTwoButtonDialog( + title = stringResource(id = R.string.warning), + message = stringResource(id = R.string.vault_timeout_log_out_confirmation), + confirmButtonText = stringResource(id = R.string.yes), + dismissButtonText = stringResource(id = R.string.cancel), + onConfirmClick = { + shouldShowLogoutActionConfirmationDialog = false + onVaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT) + }, + onDismissClick = { + shouldShowLogoutActionConfirmationDialog = false + }, + onDismissRequest = { + shouldShowLogoutActionConfirmationDialog = false + }, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt index 3e77212ed1..146c139cdb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreen.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.appearance +import android.content.res.Resources import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -9,7 +10,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -27,21 +28,18 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect -import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.model.CardStyle -import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme -import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.displayLabel +import kotlinx.collections.immutable.toImmutableList /** * Displays the appearance screen. @@ -128,48 +126,30 @@ private fun LanguageSelectionRow( currentSelection: AppLanguage, onLanguageSelection: (AppLanguage) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var languageChangedDialogOption: Text? by rememberSaveable { mutableStateOf(value = null) } - var shouldShowLanguageSelectionDialog by rememberSaveable { mutableStateOf(value = false) } + var languageChangedDialogOption: String? by rememberSaveable { mutableStateOf(value = null) } + BitwardenMultiSelectButton( + label = stringResource(id = R.string.language), + options = AppLanguage.entries.map { it.text() }.toImmutableList(), + selectedOption = currentSelection.text(), + onOptionSelected = { selectedLanguage -> + onLanguageSelection( + AppLanguage.entries.first { selectedLanguage == it.text.toString(resources) }, + ) + languageChangedDialogOption = selectedLanguage + }, + cardStyle = CardStyle.Full, + modifier = modifier, + ) languageChangedDialogOption?.let { BitwardenBasicDialog( title = stringResource(id = R.string.language), - message = stringResource(id = R.string.language_change_x_description, it.invoke()), + message = stringResource(id = R.string.language_change_x_description, it), onDismissRequest = { languageChangedDialogOption = null }, ) } - BitwardenTextRow( - text = stringResource(id = R.string.language), - onClick = { shouldShowLanguageSelectionDialog = true }, - cardStyle = CardStyle.Full, - modifier = modifier, - ) { - Text( - text = currentSelection.text(), - style = BitwardenTheme.typography.labelSmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } - - if (shouldShowLanguageSelectionDialog) { - BitwardenSelectionDialog( - title = stringResource(id = R.string.language), - onDismissRequest = { shouldShowLanguageSelectionDialog = false }, - ) { - AppLanguage.entries.forEach { option -> - BitwardenSelectionRow( - text = option.text, - isSelected = option == currentSelection, - onClick = { - shouldShowLanguageSelectionDialog = false - onLanguageSelection(option) - languageChangedDialogOption = option.text - }, - ) - } - } - } } @Composable @@ -177,40 +157,19 @@ private fun ThemeSelectionRow( currentSelection: AppTheme, onThemeSelection: (AppTheme) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var shouldShowThemeSelectionDialog by remember { mutableStateOf(false) } - - BitwardenTextRow( - text = stringResource(id = R.string.theme), - description = stringResource(id = R.string.theme_description), - onClick = { shouldShowThemeSelectionDialog = true }, + BitwardenMultiSelectButton( + label = stringResource(id = R.string.theme), + options = AppTheme.entries.map { it.displayLabel() }.toImmutableList(), + selectedOption = currentSelection.displayLabel(), + onOptionSelected = { selectedTheme -> + onThemeSelection( + AppTheme.entries.first { selectedTheme == it.displayLabel.toString(resources) }, + ) + }, + supportingText = stringResource(id = R.string.theme_description), cardStyle = CardStyle.Full, modifier = modifier, - ) { - Text( - text = currentSelection.displayLabel(), - style = BitwardenTheme.typography.labelSmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } - - if (shouldShowThemeSelectionDialog) { - BitwardenSelectionDialog( - title = stringResource(id = R.string.theme), - onDismissRequest = { shouldShowThemeSelectionDialog = false }, - ) { - AppTheme.entries.forEach { option -> - BitwardenSelectionRow( - text = option.displayLabel, - isSelected = option == currentSelection, - onClick = { - shouldShowThemeSelectionDialog = false - onThemeSelection( - AppTheme.entries.first { it == option }, - ) - }, - ) - } - } - } + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 4d50e61cfa..fb2d61c7c8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.autofill +import android.content.res.Resources import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column @@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -37,9 +37,8 @@ import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow @@ -50,7 +49,7 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager -import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.toImmutableList /** * Displays the auto-fill screen. @@ -336,41 +335,21 @@ private fun DefaultUriMatchTypeRow( selectedUriMatchType: UriMatchType, onUriMatchTypeSelect: (UriMatchType) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var shouldShowDialog by rememberSaveable { mutableStateOf(false) } - - BitwardenTextRow( - text = stringResource(id = R.string.default_uri_match_detection), - description = stringResource(id = R.string.default_uri_match_detection_description), - onClick = { shouldShowDialog = true }, + BitwardenMultiSelectButton( + label = stringResource(id = R.string.default_uri_match_detection), + options = UriMatchType.entries.map { it.displayLabel() }.toImmutableList(), + selectedOption = selectedUriMatchType.displayLabel(), + onOptionSelected = { selectedOption -> + onUriMatchTypeSelect( + UriMatchType + .entries + .first { it.displayLabel.toString(resources) == selectedOption }, + ) + }, + supportingText = stringResource(id = R.string.default_uri_match_detection_description), cardStyle = CardStyle.Full, modifier = modifier, - ) { - Text( - text = selectedUriMatchType.displayLabel(), - style = BitwardenTheme.typography.labelSmall, - color = BitwardenTheme.colorScheme.text.primary, - ) - } - - if (shouldShowDialog) { - BitwardenSelectionDialog( - title = stringResource(id = R.string.default_uri_match_detection), - onDismissRequest = { shouldShowDialog = false }, - ) { - val uriMatchTypes = UriMatchType.entries - uriMatchTypes.forEach { option -> - BitwardenSelectionRow( - text = option.displayLabel, - isSelected = option == selectedUriMatchType, - onClick = { - shouldShowDialog = false - onUriMatchTypeSelect( - uriMatchTypes.first { it == option }, - ) - }, - ) - } - } - } + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt index 6cd7fdd80d..5d641b6bde 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other +import android.content.res.Resources import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -39,15 +40,14 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog -import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton import com.x8bit.bitwarden.ui.platform.components.model.CardStyle -import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import kotlinx.collections.immutable.toImmutableList /** * Displays the other screen. @@ -169,7 +169,7 @@ fun OtherScreen( .standardHorizontalMargin(), ) - Spacer(modifier = Modifier.height(height = 16.dp)) + Spacer(modifier = Modifier.height(height = 8.dp)) ScreenCaptureRow( currentValue = state.allowScreenCapture, @@ -182,6 +182,7 @@ fun OtherScreen( .standardHorizontalMargin(), ) + Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } } @@ -230,43 +231,24 @@ private fun ClearClipboardFrequencyRow( currentSelection: ClearClipboardFrequency, onFrequencySelection: (ClearClipboardFrequency) -> Unit, modifier: Modifier = Modifier, + resources: Resources = LocalContext.current.resources, ) { - var shouldShowClearClipboardDialog by remember { mutableStateOf(false) } - - BitwardenTextRow( - text = stringResource(id = R.string.clear_clipboard), - description = stringResource(id = R.string.clear_clipboard_description), - onClick = { shouldShowClearClipboardDialog = true }, + BitwardenMultiSelectButton( + label = stringResource(id = R.string.clear_clipboard), + supportingText = stringResource(id = R.string.clear_clipboard_description), + options = ClearClipboardFrequency.entries.map { it.displayLabel() }.toImmutableList(), + selectedOption = currentSelection.displayLabel(), + onOptionSelected = { selectedFrequency -> + onFrequencySelection( + ClearClipboardFrequency + .entries + .first { it.displayLabel.toString(resources) == selectedFrequency }, + ) + }, + textFieldTestTag = "ClearClipboardAfterLabel", cardStyle = CardStyle.Full, modifier = modifier, - ) { - Text( - text = currentSelection.displayLabel.invoke(), - style = BitwardenTheme.typography.labelSmall, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier.testTag("ClearClipboardAfterLabel"), - ) - } - - if (shouldShowClearClipboardDialog) { - BitwardenSelectionDialog( - title = stringResource(id = R.string.clear_clipboard), - onDismissRequest = { shouldShowClearClipboardDialog = false }, - ) { - ClearClipboardFrequency.entries.forEach { option -> - BitwardenSelectionRow( - text = option.displayLabel, - isSelected = option == currentSelection, - onClick = { - shouldShowClearClipboardDialog = false - onFrequencySelection( - ClearClipboardFrequency.entries.first { it == option }, - ) - }, - ) - } - } - } + ) } @Composable diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt index 90e176e59a..2988a6718a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt @@ -5,10 +5,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsOff import androidx.compose.ui.test.assertIsOn -import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.filterToOne import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.hasTextExactly import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDisplayed @@ -552,16 +550,14 @@ class AccountSecurityScreenTest : BaseComposeTest() { @Test fun `session timeout should be updated on or off according to state`() { composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() - .assertTextEquals("Session timeout", "30 minutes") + .assertIsDisplayed() mutableStateFlow.update { it.copy(vaultTimeout = VaultTimeout.FourHours) } composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "4 hours. Session timeout") .performScrollTo() - .assertTextEquals("Session timeout", "4 hours") + .assertIsDisplayed() } @Test @@ -569,8 +565,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -634,8 +629,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { } composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -688,8 +682,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -707,8 +700,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -731,8 +723,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -769,8 +760,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -799,8 +789,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onAllNodesWithText("Session timeout") - .filterToOne(hasClickAction()) + .onNodeWithContentDescription(label = "30 minutes. Session timeout") .performScrollTo() .performClick() @@ -981,12 +970,12 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() composeTestRule - .onAllNodesWithText("Vault timeout action") + .onAllNodesWithText("Session timeout action") .filterToOne(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule @@ -1009,12 +998,12 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() composeTestRule - .onAllNodesWithText("Vault timeout action") + .onAllNodesWithText("Session timeout action") .filterToOne(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule @@ -1033,12 +1022,11 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() } - @Suppress("MaxLineLength") @Test fun `on session timeout action dialog Logout click should open a confirmation dialog`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() @@ -1077,7 +1065,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { fun `on session timeout action Logout confirmation dialog cancel click should dismiss the dialog`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() @@ -1106,7 +1094,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { fun `on session timeout action Logout confirmation dialog Yes click should dismiss the dialog and send VaultTimeoutActionSelect`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() @@ -1136,18 +1124,17 @@ class AccountSecurityScreenTest : BaseComposeTest() { } } - @Suppress("MaxLineLength") @Test fun `on session timeout action dialog cancel click should close the dialog`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() .performClick() composeTestRule - .onAllNodesWithText("Vault timeout action") + .onAllNodesWithText("Session timeout action") .filterToOne(hasAnyAncestor(isDialog())) .assertIsDisplayed() composeTestRule @@ -1163,14 +1150,14 @@ class AccountSecurityScreenTest : BaseComposeTest() { @Test fun `session timeout action should be updated according to state`() { composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Lock. Session timeout action") .performScrollTo() - .assertTextEquals("Session timeout action", "Lock") + .assertIsDisplayed() mutableStateFlow.update { it.copy(vaultTimeoutAction = VaultTimeoutAction.LOGOUT) } composeTestRule - .onNodeWithText("Session timeout action") + .onNodeWithContentDescription(label = "Log out. Session timeout action") .performScrollTo() - .assertTextEquals("Session timeout action", "Log out") + .assertIsDisplayed() } @Suppress("MaxLineLength") diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt index 47bd196abe..31fb849e1d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceScreenTest.kt @@ -8,7 +8,10 @@ import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.printToLog import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage @@ -50,7 +53,10 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on language row click should display language selection dialog`() { - composeTestRule.onNodeWithText("Language").performClick() + composeTestRule + .onNodeWithContentDescription(label = "Default (System). Language") + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Language") .filterToOne(hasAnyAncestor(isDialog())) @@ -60,7 +66,10 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on language selection dialog item click should send LanguageChange and show dialog`() { // Clicking the Language row shows the language selection dialog - composeTestRule.onNodeWithText("Language").performClick() + composeTestRule + .onNodeWithContentDescription(label = "Default (System). Language") + .performScrollTo() + .performClick() // Selecting a language dismisses this dialog and displays the confirmation composeTestRule .onAllNodesWithText("Afrikaans") @@ -93,7 +102,10 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on language selection dialog cancel click should dismiss dialog`() { - composeTestRule.onNodeWithText("Language").performClick() + composeTestRule + .onNodeWithContentDescription(label = "Default (System). Language") + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Cancel") .filterToOne(hasAnyAncestor(isDialog())) @@ -103,7 +115,13 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on theme row click should display theme selection dialog`() { - composeTestRule.onNodeWithText("Theme").performClick() + composeTestRule.onRoot().printToLog("Brian") + composeTestRule + .onNodeWithContentDescription( + label = "Default (System). Theme. Change the application's color theme.", + ) + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Theme") .filterToOne(hasAnyAncestor(isDialog())) @@ -112,7 +130,12 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on theme selection dialog item click should send ThemeChange`() { - composeTestRule.onNodeWithText("Theme").performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Default (System). Theme. Change the application's color theme.", + ) + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Dark") .filterToOne(hasAnyAncestor(isDialog())) @@ -130,7 +153,12 @@ class AppearanceScreenTest : BaseComposeTest() { @Test fun `on theme selection dialog cancel click should dismiss dialog`() { - composeTestRule.onNodeWithText("Theme").performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Default (System). Theme. Change the application's color theme.", + ) + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Cancel") .filterToOne(hasAnyAncestor(isDialog())) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt index 9f23b96ac2..73ba3a893d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreenTest.kt @@ -369,7 +369,7 @@ class AutoFillScreenTest : BaseComposeTest() { fun `on default URI match type click should display dialog`() { composeTestRule.assertNoDialogExists() composeTestRule - .onNodeWithText("Default URI match detection") + .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) .performScrollTo() .assert(!hasAnyAncestor(isDialog())) .performClick() @@ -383,7 +383,7 @@ class AutoFillScreenTest : BaseComposeTest() { @Test fun `on default URI match type dialog item click should send DefaultUriMatchTypeSelect and close the dialog`() { composeTestRule - .onNodeWithText("Default URI match detection") + .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) .performScrollTo() .performClick() @@ -402,11 +402,10 @@ class AutoFillScreenTest : BaseComposeTest() { composeTestRule.assertNoDialogExists() } - @Suppress("MaxLineLength") @Test fun `on default URI match type dialog cancel click should close the dialog`() { composeTestRule - .onNodeWithText("Default URI match detection") + .onNodeWithContentDescription(label = "Default URI match detection.", substring = true) .performScrollTo() .performClick() @@ -422,19 +421,19 @@ class AutoFillScreenTest : BaseComposeTest() { @Test fun `default URI match type should update according to state`() { composeTestRule - .onNodeWithText("Base domain") + .onNodeWithContentDescription(label = "Base domain", substring = true) .assertExists() composeTestRule - .onNodeWithText("Starts with") + .onNodeWithContentDescription(label = "Starts with", substring = true) .assertDoesNotExist() mutableStateFlow.update { it.copy(defaultUriMatchType = UriMatchType.STARTS_WITH) } composeTestRule - .onNodeWithText("Base domain") + .onNodeWithContentDescription(label = "Base domain", substring = true) .assertDoesNotExist() composeTestRule - .onNodeWithText("Starts with") + .onNodeWithContentDescription(label = "Starts with", substring = true) .assertExists() } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt index 18c62f42f1..01ab88d3e9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt @@ -88,7 +88,13 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on clear clipboard row click should show show clipboard selection dialog`() { - composeTestRule.onNodeWithText("Clear clipboard").performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Never. Clear clipboard. " + + "Automatically clear copied values from your clipboard.", + ) + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("Clear clipboard") .filterToOne(hasAnyAncestor(isDialog())) @@ -97,7 +103,13 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on clear clipboard dialog item click should send ClearClipboardFrequencyChange`() { - composeTestRule.onNodeWithText("Clear clipboard").performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Never. Clear clipboard. " + + "Automatically clear copied values from your clipboard.", + ) + .performScrollTo() + .performClick() composeTestRule .onAllNodesWithText("10 seconds") .filterToOne(hasAnyAncestor(isDialog())) @@ -115,7 +127,13 @@ class OtherScreenTest : BaseComposeTest() { @Test fun `on clear clipboard dialog cancel should dismiss dialog`() { - composeTestRule.onNodeWithText("Clear clipboard").performClick() + composeTestRule + .onNodeWithContentDescription( + label = "Never. Clear clipboard. " + + "Automatically clear copied values from your clipboard.", + ) + .performScrollTo() + .performClick() composeTestRule.onNodeWithText("Cancel").performClick() composeTestRule.assertNoDialogExists() }