BIT-783: Enforce Send restriction policy (#915)

This commit is contained in:
Shannon Draeker 2024-01-31 19:21:18 -07:00 committed by Álison Fernandes
parent d538e37606
commit 05a171e71c
25 changed files with 416 additions and 16 deletions

View file

@ -41,6 +41,7 @@ import java.time.ZonedDateTime
* @param currentZonedDateTime The currently displayed time.
* @param formatPattern The pattern to format the displayed time.
* @param onDateSelect The callback to be invoked when a new date is selected.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
*/
@Suppress("LongMethod")
@ -50,6 +51,7 @@ fun BitwardenDateSelectButton(
currentZonedDateTime: ZonedDateTime?,
formatPattern: String,
onDateSelect: (ZonedDateTime) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
var shouldShowDialog: Boolean by rememberSaveable { mutableStateOf(false) }
@ -70,6 +72,7 @@ fun BitwardenDateSelectButton(
contentDescription = "$label, $formattedDate"
}
.clickable(
enabled = isEnabled,
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { shouldShowDialog = !shouldShowDialog },

View file

@ -34,6 +34,7 @@ import java.time.ZonedDateTime
* @param currentZonedDateTime The currently displayed time.
* @param formatPattern The pattern to format the displayed time.
* @param onTimeSelect The callback to be invoked when a new time is selected.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param is24Hour Indicates if the time selector should use a 24 hour format or a 12 hour format
* with AM/PM.
@ -43,6 +44,7 @@ fun BitwardenTimeSelectButton(
currentZonedDateTime: ZonedDateTime?,
formatPattern: String,
onTimeSelect: (hour: Int, minute: Int) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
is24Hour: Boolean = false,
) {
@ -62,6 +64,7 @@ fun BitwardenTimeSelectButton(
contentDescription = "$label, $formattedTime"
}
.clickable(
enabled = isEnabled,
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { shouldShowDialog = !shouldShowDialog },

View file

@ -18,6 +18,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
@ -27,11 +28,23 @@ import com.x8bit.bitwarden.ui.tools.feature.send.handlers.SendHandlers
@Suppress("LongMethod")
@Composable
fun SendContent(
policyDisablesSend: Boolean,
state: SendState.ViewState.Content,
sendHandlers: SendHandlers,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
item {
if (policyDisablesSend) {
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
}
item {
BitwardenListHeaderText(
label = stringResource(id = R.string.types),
@ -82,6 +95,7 @@ fun SendContent(
label = it.name,
supportingLabel = it.deletionDate,
trailingLabelIcons = it.iconList,
showMoreOptions = !policyDisablesSend,
onClick = { sendHandlers.onSendClick(it) },
onCopyClick = { sendHandlers.onCopySendClick(it) },
onEditClick = { sendHandlers.onEditSendClick(it) },

View file

@ -22,6 +22,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
/**
* Content for the empty state of the [SendScreen].
@ -29,35 +30,47 @@ import com.x8bit.bitwarden.R
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SendEmpty(
policyDisablesSend: Boolean,
onAddItemClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.semantics { testTagsAsResourceId = true },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.semantics { testTagsAsResourceId = true },
) {
if (policyDisablesSend) {
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.weight(1F))
Text(
textAlign = TextAlign.Center,
text = stringResource(id = R.string.no_sends),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.semantics { testTag = "NoSearchResultsLabel" }
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = stringResource(id = R.string.no_sends),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(24.dp))
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = onAddItemClick,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.add_a_send),
@ -65,5 +78,7 @@ fun SendEmpty(
)
}
Spacer(modifier = Modifier.navigationBarsPadding())
Spacer(modifier = Modifier.weight(1F))
}
}

View file

@ -27,6 +27,7 @@ import kotlinx.collections.immutable.toPersistentList
* @param label The primary text label to display for the item.
* @param supportingLabel An secondary text label to display beneath the label.
* @param startIcon The [Painter] object used to draw the icon at the start of the item.
* @param showMoreOptions Whether to show the button for the overflow options.
* @param onClick The lambda to be invoked when the item is clicked.
* @param onEditClick The lambda to be invoked when the edit option is clicked from the menu.
* @param onCopyClick The lambda to be invoked when the copy option is clicked from the menu.
@ -44,6 +45,7 @@ fun SendListItem(
supportingLabel: String,
startIcon: IconData,
trailingLabelIcons: List<IconRes>,
showMoreOptions: Boolean,
onClick: () -> Unit,
onEditClick: () -> Unit,
onCopyClick: () -> Unit,
@ -89,7 +91,10 @@ fun SendListItem(
text = stringResource(id = R.string.delete),
onClick = { shouldShowDeleteConfirmationDialog = true },
),
),
)
// Only show options if allowed
.filter { showMoreOptions }
.toPersistentList(),
modifier = modifier,
)
if (shouldShowDeleteConfirmationDialog) {
@ -117,6 +122,7 @@ private fun SendListItem_preview() {
supportingLabel = "Jan 3, 2024, 10:35 AM",
startIcon = IconData.Local(R.drawable.ic_send_text),
trailingLabelIcons = emptyList(),
showMoreOptions = true,
onClick = {},
onCopyClick = {},
onEditClick = {},

View file

@ -176,24 +176,26 @@ fun SendScreen(
.padding(padding)
when (val viewState = state.viewState) {
is SendState.ViewState.Content -> SendContent(
modifier = modifier,
policyDisablesSend = state.policyDisablesSend,
state = viewState,
sendHandlers = sendHandlers,
modifier = modifier,
)
SendState.ViewState.Empty -> SendEmpty(
modifier = modifier,
policyDisablesSend = state.policyDisablesSend,
onAddItemClick = remember(viewModel) {
{ viewModel.trySendAction(SendAction.AddSendClick) }
},
modifier = modifier,
)
is SendState.ViewState.Error -> BitwardenErrorContent(
modifier = modifier,
message = viewState.message(),
onTryAgainClick = remember(viewModel) {
{ viewModel.trySendAction(SendAction.RefreshClick) }
},
modifier = modifier,
)
SendState.ViewState.Loading -> BitwardenLoadingContent(modifier = modifier)

View file

@ -5,11 +5,13 @@ import androidx.annotation.DrawableRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
@ -43,6 +45,7 @@ class SendViewModel @Inject constructor(
private val environmentRepo: EnvironmentRepository,
private val settingsRepo: SettingsRepository,
private val vaultRepo: VaultRepository,
private val policyManager: PolicyManager,
) : BaseViewModel<SendState, SendEvent, SendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE]
@ -50,6 +53,9 @@ class SendViewModel @Inject constructor(
viewState = SendState.ViewState.Loading,
dialogState = null,
isPullToRefreshSettingEnabled = settingsRepo.getPullToRefreshEnabledFlow().value,
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
),
) {
@ -59,6 +65,11 @@ class SendViewModel @Inject constructor(
.map { SendAction.Internal.PullToRefreshEnableReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND)
.map { SendAction.Internal.PolicyUpdateReceive(it.any()) }
.onEach(::sendAction)
.launchIn(viewModelScope)
vaultRepo
.sendDataStateFlow
.map { SendAction.Internal.SendDataReceive(it) }
@ -96,6 +107,8 @@ class SendViewModel @Inject constructor(
}
is SendAction.Internal.SendDataReceive -> handleSendDataReceive(action)
is SendAction.Internal.PolicyUpdateReceive -> handlePolicyUpdateReceive(action)
}
private fun handlePullToRefreshEnableReceive(
@ -215,6 +228,14 @@ class SendViewModel @Inject constructor(
}
}
private fun handlePolicyUpdateReceive(action: SendAction.Internal.PolicyUpdateReceive) {
mutableStateFlow.update {
it.copy(
policyDisablesSend = action.policyDisablesSend,
)
}
}
private fun handleAboutSendClick() {
sendEvent(SendEvent.NavigateToAboutSend)
}
@ -306,6 +327,7 @@ data class SendState(
val viewState: ViewState,
val dialogState: DialogState?,
private val isPullToRefreshSettingEnabled: Boolean,
val policyDisablesSend: Boolean,
) : Parcelable {
/**
@ -533,6 +555,13 @@ sealed class SendAction {
data class SendDataReceive(
val sendDataState: DataState<SendData>,
) : Internal()
/**
* Indicates that a policy update has been received.
*/
data class PolicyUpdateReceive(
val policyDisablesSend: Boolean,
) : Internal()
}
}

View file

@ -37,6 +37,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
@ -54,6 +55,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandler
@Composable
fun AddSendContent(
state: AddSendState.ViewState.Content,
policyDisablesSend: Boolean,
isAddMode: Boolean,
isShared: Boolean,
addSendHandlers: AddSendHandlers,
@ -68,12 +70,23 @@ fun AddSendContent(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
if (policyDisablesSend) {
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
}
BitwardenTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
label = stringResource(id = R.string.name),
hint = stringResource(id = R.string.name_info),
readOnly = policyDisablesSend,
value = state.common.name,
onValueChange = addSendHandlers.onNamChange,
)
@ -205,6 +218,7 @@ fun AddSendContent(
.padding(horizontal = 16.dp),
label = stringResource(id = R.string.text),
hint = stringResource(id = R.string.type_text_info),
readOnly = policyDisablesSend,
value = type.input,
singleLine = false,
onValueChange = addSendHandlers.onTextChange,
@ -217,6 +231,7 @@ fun AddSendContent(
label = stringResource(id = R.string.hide_text_by_default),
isChecked = type.isHideByDefaultChecked,
onCheckedChange = addSendHandlers.onIsHideByDefaultToggle,
readOnly = policyDisablesSend,
)
}
}
@ -224,6 +239,7 @@ fun AddSendContent(
Spacer(modifier = Modifier.height(16.dp))
AddSendOptions(
state = state,
sendRestrictionPolicy = policyDisablesSend,
isAddMode = isAddMode,
addSendHandlers = addSendHandlers,
)
@ -237,6 +253,8 @@ fun AddSendContent(
* Displays a collapsable set of new send options.
*
* @param state The content state.
* @param sendRestrictionPolicy When `true`, indicates that there's a policy preventing the user
* from editing or creating sends.
* @param isAddMode When `true`, indicates that we are creating a new send and `false` when editing
* an existing send.
* @param addSendHandlers THe handlers various events.
@ -245,6 +263,7 @@ fun AddSendContent(
@Composable
private fun AddSendOptions(
state: AddSendState.ViewState.Content,
sendRestrictionPolicy: Boolean,
isAddMode: Boolean,
addSendHandlers: AddSendHandlers,
) {
@ -297,6 +316,7 @@ private fun AddSendOptions(
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
onDateSelect = addSendHandlers.onDeletionDateChange,
isEnabled = !sendRestrictionPolicy,
)
Spacer(modifier = Modifier.height(8.dp))
SendExpirationDateChooser(
@ -307,6 +327,7 @@ private fun AddSendOptions(
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.expirationDate,
onDateSelect = addSendHandlers.onExpirationDateChange,
isEnabled = !sendRestrictionPolicy,
)
} else {
BitwardenListHeaderText(
@ -323,6 +344,7 @@ private fun AddSendOptions(
dateFormatPattern = state.common.dateFormatPattern,
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.deletionDate,
isEnabled = !sendRestrictionPolicy,
onDateSelect = { addSendHandlers.onDeletionDateChange(requireNotNull(it)) },
)
Spacer(modifier = Modifier.height(4.dp))
@ -351,6 +373,7 @@ private fun AddSendOptions(
timeFormatPattern = state.common.timeFormatPattern,
currentZonedDateTime = state.common.expirationDate,
onDateSelect = addSendHandlers.onExpirationDateChange,
isEnabled = !sendRestrictionPolicy,
)
Spacer(modifier = Modifier.height(4.dp))
Row(
@ -369,7 +392,7 @@ private fun AddSendOptions(
BitwardenTextButton(
label = stringResource(id = R.string.clear),
onClick = addSendHandlers.onClearExpirationDateClick,
isEnabled = state.common.expirationDate != null,
isEnabled = state.common.expirationDate != null && !sendRestrictionPolicy,
modifier = Modifier.wrapContentWidth(),
)
}
@ -379,9 +402,10 @@ private fun AddSendOptions(
label = stringResource(id = R.string.maximum_access_count),
value = state.common.maxAccessCount,
onValueChange = addSendHandlers.onMaxAccessCountChange,
isDecrementEnabled = state.common.maxAccessCount != null,
isDecrementEnabled = state.common.maxAccessCount != null && !sendRestrictionPolicy,
isIncrementEnabled = !sendRestrictionPolicy,
range = 0..Int.MAX_VALUE,
textFieldReadOnly = false,
textFieldReadOnly = sendRestrictionPolicy,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
@ -423,6 +447,7 @@ private fun AddSendOptions(
BitwardenPasswordField(
label = stringResource(id = R.string.new_password),
hint = stringResource(id = R.string.password_info),
readOnly = sendRestrictionPolicy,
value = state.common.passwordInput,
onValueChange = addSendHandlers.onPasswordChange,
modifier = Modifier
@ -433,6 +458,7 @@ private fun AddSendOptions(
BitwardenTextField(
label = stringResource(id = R.string.notes),
hint = stringResource(id = R.string.notes_info),
readOnly = sendRestrictionPolicy,
value = state.common.noteInput,
singleLine = false,
onValueChange = addSendHandlers.onNoteChange,
@ -448,6 +474,7 @@ private fun AddSendOptions(
label = stringResource(id = R.string.hide_email),
isChecked = state.common.isHideEmailChecked,
onCheckedChange = addSendHandlers.onHideEmailToggle,
readOnly = sendRestrictionPolicy,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenWideSwitch(
@ -457,6 +484,7 @@ private fun AddSendOptions(
label = stringResource(id = R.string.disable_send),
isChecked = state.common.isDeactivateChecked,
onCheckedChange = addSendHandlers.onDeactivateSendToggle,
readOnly = sendRestrictionPolicy,
)
}
}

View file

@ -27,6 +27,7 @@ import kotlin.time.Duration.Companion.minutes
* @param timeFormatPattern The pattern for displaying the time.
* @param onDateSelect The callback for being notified of updates to the selected date and time.
* This will only be `null` when there is no selected time.
* @param isEnabled Whether the button is enabled.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
*/
@Composable
@ -35,6 +36,7 @@ fun AddSendCustomDateChooser(
dateFormatPattern: String,
timeFormatPattern: String,
onDateSelect: (ZonedDateTime?) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
// This tracks the date component (year, month, and day) and ignores lower level
@ -60,6 +62,7 @@ fun AddSendCustomDateChooser(
modifier = Modifier.weight(1f),
formatPattern = dateFormatPattern,
currentZonedDateTime = currentZonedDateTime,
isEnabled = isEnabled,
onDateSelect = {
date = it
onDateSelect(derivedDateTimeMillis)
@ -70,6 +73,7 @@ fun AddSendCustomDateChooser(
modifier = Modifier.weight(1f),
formatPattern = timeFormatPattern,
currentZonedDateTime = currentZonedDateTime,
isEnabled = isEnabled,
onTimeSelect = { hour, minute ->
timeMillis = hour.hours.inWholeMilliseconds + minute.minutes.inWholeMilliseconds
onDateSelect(derivedDateTimeMillis)

View file

@ -36,6 +36,7 @@ fun SendDeletionDateChooser(
dateFormatPattern: String,
timeFormatPattern: String,
onDateSelect: (ZonedDateTime) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
val defaultOption = DeletionOptions.SEVEN_DAYS
@ -46,6 +47,7 @@ fun SendDeletionDateChooser(
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.deletion_date),
isEnabled = isEnabled,
options = options.values.toImmutableList(),
selectedOption = selectedOption.text(),
onOptionSelected = { selected ->
@ -67,6 +69,7 @@ fun SendDeletionDateChooser(
dateFormatPattern = dateFormatPattern,
timeFormatPattern = timeFormatPattern,
onDateSelect = { onDateSelect(requireNotNull(it)) },
isEnabled = isEnabled,
)
}
}

View file

@ -36,6 +36,7 @@ fun SendExpirationDateChooser(
dateFormatPattern: String,
timeFormatPattern: String,
onDateSelect: (ZonedDateTime?) -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier,
) {
val defaultOption = ExpirationOptions.NEVER
@ -46,6 +47,7 @@ fun SendExpirationDateChooser(
) {
BitwardenMultiSelectButton(
label = stringResource(id = R.string.expiration_date),
isEnabled = isEnabled,
options = options.values.toImmutableList(),
selectedOption = selectedOption.text(),
onOptionSelected = { selected ->
@ -69,6 +71,7 @@ fun SendExpirationDateChooser(
dateFormatPattern = dateFormatPattern,
timeFormatPattern = timeFormatPattern,
onDateSelect = onDateSelect,
isEnabled = isEnabled,
)
}
}

View file

@ -140,6 +140,7 @@ fun AddSendScreen(
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.save),
isEnabled = !state.policyDisablesSend,
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.SaveClick) }
},
@ -157,19 +158,21 @@ fun AddSendScreen(
}
},
)
.takeIf { state.hasPassword },
.takeIf { state.hasPassword && !state.policyDisablesSend },
OverflowMenuItemData(
text = stringResource(id = R.string.copy_link),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.CopyLinkClick) }
},
),
)
.takeIf { !state.policyDisablesSend },
OverflowMenuItemData(
text = stringResource(id = R.string.share_link),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AddSendAction.ShareLinkClick) }
},
),
)
.takeIf { !state.policyDisablesSend },
OverflowMenuItemData(
text = stringResource(id = R.string.delete),
onClick = { shouldShowDeleteConfirmationDialog = true },
@ -189,6 +192,7 @@ fun AddSendScreen(
when (val viewState = state.viewState) {
is AddSendState.ViewState.Content -> AddSendContent(
state = viewState,
policyDisablesSend = state.policyDisablesSend,
isAddMode = state.isAddMode,
isShared = state.isShared,
addSendHandlers = addSendHandlers,

View file

@ -8,12 +8,14 @@ import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
@ -64,6 +66,7 @@ class AddSendViewModel @Inject constructor(
private val environmentRepo: EnvironmentRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val vaultRepo: VaultRepository,
private val policyManager: PolicyManager,
) : BaseViewModel<AddSendState, AddSendEvent, AddSendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
@ -106,6 +109,9 @@ class AddSendViewModel @Inject constructor(
dialogState = null,
isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true,
baseWebSendUrl = environmentRepo.environment.environmentUrlData.baseWebSendUrl,
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
)
},
) {
@ -541,6 +547,17 @@ class AddSendViewModel @Inject constructor(
}
private fun handleFileTypeClick() {
if (state.policyDisablesSend) {
mutableStateFlow.update {
it.copy(
dialogState = AddSendState.DialogState.Error(
title = null,
message = R.string.send_disabled_warning.asText(),
),
)
}
return
}
if (!state.isPremiumUser) {
mutableStateFlow.update {
it.copy(
@ -675,6 +692,7 @@ data class AddSendState(
val isPremiumUser: Boolean,
val isShared: Boolean,
val baseWebSendUrl: String,
val policyDisablesSend: Boolean,
) : Parcelable {
/**

View file

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
@ -33,6 +34,7 @@ import kotlinx.collections.immutable.toPersistentList
@Composable
fun VaultItemListingContent(
state: VaultItemListingState.ViewState.Content,
policyDisablesSend: Boolean,
vaultItemClick: (id: String) -> Unit,
masterPasswordRepromptSubmit: (id: String, password: String) -> Unit,
onOverflowItemClick: (action: ListingItemOverflowAction) -> Unit,
@ -95,6 +97,17 @@ fun VaultItemListingContent(
LazyColumn(
modifier = modifier,
) {
item {
if (policyDisablesSend) {
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
}
item {
BitwardenListHeaderTextWithSupportLabel(
label = stringResource(id = R.string.items),
@ -136,6 +149,8 @@ fun VaultItemListingContent(
},
)
}
// Only show options if allowed
.filter { !policyDisablesSend }
.toPersistentList(),
modifier = Modifier
.fillMaxWidth()

View file

@ -19,11 +19,13 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.VaultNoItems
@Composable
fun VaultItemListingEmpty(
state: VaultItemListingState.ViewState.NoItems,
policyDisablesSend: Boolean,
addItemClickAction: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.shouldShowAddButton) {
VaultNoItems(
policyDisablesSend = policyDisablesSend,
message = state.message(),
modifier = modifier,
addItemClickAction = addItemClickAction,

View file

@ -234,6 +234,8 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.Content -> {
VaultItemListingContent(
state = state.viewState,
policyDisablesSend = state.policyDisablesSend &&
state.itemListingType is VaultItemListingState.ItemListingType.Send,
vaultItemClick = vaultItemListingHandlers.itemClick,
masterPasswordRepromptSubmit =
vaultItemListingHandlers.masterPasswordRepromptSubmit,
@ -245,6 +247,8 @@ private fun VaultItemListingScaffold(
is VaultItemListingState.ViewState.NoItems -> {
VaultItemListingEmpty(
state = state.viewState,
policyDisablesSend = state.policyDisablesSend &&
state.itemListingType is VaultItemListingState.ItemListingType.Send,
addItemClickAction = vaultItemListingHandlers.addVaultItemClick,
modifier = modifier,
)

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@ -18,6 +19,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.platform.repository.util.map
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
@ -69,6 +71,7 @@ class VaultItemListingViewModel @Inject constructor(
private val autofillSelectionManager: AutofillSelectionManager,
private val cipherMatchingManager: CipherMatchingManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val policyManager: PolicyManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@ -89,6 +92,9 @@ class VaultItemListingViewModel @Inject constructor(
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
dialogState = null,
policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(),
autofillSelectionData = specialCircumstance?.autofillSelectionData,
shouldFinishOnComplete = specialCircumstance?.shouldFinishWhenComplete ?: false,
)
@ -117,6 +123,12 @@ class VaultItemListingViewModel @Inject constructor(
)
}
.launchIn(viewModelScope)
policyManager
.getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND)
.map { VaultItemListingsAction.Internal.PolicyUpdateReceive(it.any()) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: VaultItemListingsAction) {
@ -425,6 +437,10 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.Internal.ValidatePasswordResultReceive -> {
handleMasterPasswordRepromptResultReceive(action)
}
is VaultItemListingsAction.Internal.PolicyUpdateReceive -> {
handlePolicyUpdateReceive(action)
}
}
}
@ -612,6 +628,16 @@ class VaultItemListingViewModel @Inject constructor(
updateStateWithVaultData(vaultData = vaultData.data, clearDialogState = false)
}
private fun handlePolicyUpdateReceive(
action: VaultItemListingsAction.Internal.PolicyUpdateReceive,
) {
mutableStateFlow.update {
it.copy(
policyDisablesSend = action.policyDisablesSend,
)
}
}
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
mutableStateFlow.update { currentState ->
currentState.copy(
@ -698,6 +724,7 @@ data class VaultItemListingState(
val baseIconUrl: String,
val isIconLoadingDisabled: Boolean,
val dialogState: DialogState?,
val policyDisablesSend: Boolean,
// Internal
private val isPullToRefreshSettingEnabled: Boolean,
val autofillSelectionData: AutofillSelectionData? = null,
@ -1157,5 +1184,12 @@ sealed class VaultItemListingsAction {
val cipherId: String,
val result: ValidatePasswordResult,
) : Internal()
/**
* Indicates that a policy update has been received.
*/
data class PolicyUpdateReceive(
val policyDisablesSend: Boolean,
) : Internal()
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
/**
* No items view for the [VaultScreen].
@ -24,6 +25,7 @@ import com.x8bit.bitwarden.R
@Composable
fun VaultNoItems(
addItemClickAction: () -> Unit,
policyDisablesSend: Boolean,
modifier: Modifier = Modifier,
message: String = stringResource(id = R.string.no_items),
) {
@ -32,6 +34,17 @@ fun VaultNoItems(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (policyDisablesSend) {
BitwardenPolicyWarningText(
text = stringResource(id = R.string.send_disabled_warning),
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.weight(1F))
Text(
textAlign = TextAlign.Center,
modifier = Modifier
@ -58,5 +71,7 @@ fun VaultNoItems(
style = MaterialTheme.typography.labelLarge,
)
}
Spacer(modifier = Modifier.weight(1F))
}
}

View file

@ -285,6 +285,7 @@ private fun VaultScreenScaffold(
is VaultState.ViewState.NoItems -> VaultNoItems(
modifier = innerModifier,
policyDisablesSend = false,
addItemClickAction = vaultHandlers.addItemClickAction,
)

View file

@ -186,6 +186,26 @@ class SendScreenTest : BaseComposeTest() {
}
}
@Test
fun `policy warning should update according to state`() {
val policyText = "Due to an enterprise policy, you are only " +
"able to delete an existing Send."
composeTestRule
.onNodeWithText(policyText)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Empty,
policyDisablesSend = true,
)
}
composeTestRule
.onNodeWithText(policyText)
.assertIsDisplayed()
}
@Test
fun `fab should be displayed according to state`() {
mutableStateFlow.update {
@ -379,6 +399,32 @@ class SendScreenTest : BaseComposeTest() {
}
}
@Test
fun `send item overflow button should update according to state`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
mutableStateFlow.update {
it.copy(
policyDisablesSend = true,
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertDoesNotExist()
}
@Test
fun `on send item overflow click should display dialog`() {
mutableStateFlow.update {
@ -664,6 +710,7 @@ private val DEFAULT_STATE: SendState = SendState(
viewState = SendState.ViewState.Loading,
dialogState = null,
isPullToRefreshSettingEnabled = false,
policyDisablesSend = false,
)
private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =

View file

@ -3,12 +3,14 @@ package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
@ -26,6 +28,7 @@ import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
@ -48,6 +51,10 @@ class SendViewModelTest : BaseViewModelTest() {
private val vaultRepo: VaultRepository = mockk {
every { sendDataStateFlow } returns mutableSendDataFlow
}
private val policyManager: PolicyManager = mockk {
every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList()
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
}
@BeforeEach
fun setup() {
@ -435,12 +442,14 @@ class SendViewModelTest : BaseViewModelTest() {
)
}
@Suppress("LongParameterList")
private fun createViewModel(
state: SendState? = null,
bitwardenClipboardManager: BitwardenClipboardManager = clipboardManager,
environmentRepository: EnvironmentRepository = environmentRepo,
settingsRepository: SettingsRepository = settingsRepo,
vaultRepository: VaultRepository = vaultRepo,
policyManager: PolicyManager = this.policyManager,
): SendViewModel = SendViewModel(
savedStateHandle = SavedStateHandle().apply {
set("state", state)
@ -449,6 +458,7 @@ class SendViewModelTest : BaseViewModelTest() {
environmentRepo = environmentRepository,
settingsRepo = settingsRepository,
vaultRepo = vaultRepository,
policyManager = policyManager,
)
}
@ -456,4 +466,5 @@ private val DEFAULT_STATE: SendState = SendState(
viewState = SendState.ViewState.Loading,
dialogState = null,
isPullToRefreshSettingEnabled = false,
policyDisablesSend = false,
)

View file

@ -148,6 +148,32 @@ class AddSendScreenTest : BaseComposeTest() {
.isDisplayed()
}
@Test
fun `on overflow button click should only display delete when policy disables send`() {
mutableStateFlow.value = DEFAULT_STATE.copy(
addSendType = AddSendType.EditItem(sendItemId = "sendId"),
policyDisablesSend = true,
)
composeTestRule
.onNodeWithContentDescription("More")
.performClick()
composeTestRule
.onNodeWithText("Remove password")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Copy link")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Share link")
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Delete")
.assert(hasAnyAncestor(isPopup()))
.isDisplayed()
}
@Test
fun `overflow remove password button should be hidden when hasPassword is false`() {
mutableStateFlow.value = DEFAULT_STATE.copy(
@ -267,6 +293,25 @@ class AddSendScreenTest : BaseComposeTest() {
}
}
@Test
fun `policy warning should update according to state`() {
val policyText = "Due to an enterprise policy, you are only " +
"able to delete an existing Send."
composeTestRule
.onNodeWithText(policyText)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
policyDisablesSend = true,
)
}
composeTestRule
.onNodeWithText(policyText)
.assertIsDisplayed()
}
@Test
fun `on name input change should send NameChange`() {
composeTestRule
@ -932,6 +977,7 @@ class AddSendScreenTest : BaseComposeTest() {
isShared = false,
isPremiumUser = false,
baseWebSendUrl = "https://vault.bitwarden.com/#/send/",
policyDisablesSend = false,
)
}
}

View file

@ -7,11 +7,14 @@ import com.bitwarden.core.SendView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
@ -70,6 +73,9 @@ class AddSendViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk {
every { getSendStateFlow(any()) } returns mutableSendDataStateFlow
}
private val policyManager: PolicyManager = mockk {
every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList()
}
@BeforeEach
fun setup() {
@ -811,6 +817,27 @@ class AddSendViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `FileTypeClick should display error dialog when policy disables send`() {
every {
policyManager.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
} returns listOf(createMockPolicy())
val viewModel = createViewModel()
viewModel.trySendAction(AddSendAction.FileTypeClick)
assertEquals(
DEFAULT_STATE.copy(
dialogState = AddSendState.DialogState.Error(
title = null,
message = R.string.send_disabled_warning.asText(),
),
policyDisablesSend = true,
),
viewModel.stateFlow.value,
)
}
@Test
fun `NameChange should update name input`() = runTest {
val viewModel = createViewModel()
@ -954,6 +981,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
clock = clock,
clipboardManager = clipboardManager,
vaultRepo = vaultRepository,
policyManager = policyManager,
)
companion object {
@ -991,6 +1019,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
isShared = false,
isPremiumUser = false,
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
policyDisablesSend = false,
)
private val DEFAULT_USER_ACCOUNT_STATE = UserState.Account(

View file

@ -291,6 +291,34 @@ class VaultItemListingScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(VaultItemListingsAction.SearchIconClick) }
}
@Test
fun `policy warning should update according to state`() {
mutableStateFlow.update {
it.copy(
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
viewState = VaultItemListingState.ViewState.NoItems(
message = "There are no Sends in your account.".asText(),
shouldShowAddButton = true,
),
)
}
val policyText = "Due to an enterprise policy, you are only " +
"able to delete an existing Send."
composeTestRule
.onNodeWithText(policyText)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
policyDisablesSend = true,
)
}
composeTestRule
.onNodeWithText(policyText)
.assertIsDisplayed()
}
@Test
fun `floating action button click should send AddItemClick action`() {
composeTestRule
@ -833,6 +861,31 @@ class VaultItemListingScreenTest : BaseComposeTest() {
}
}
@Test
fun `send item overflow item button should update according to state`() {
mutableStateFlow.update {
it.copy(
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
viewState = VaultItemListingState.ViewState.Content(
displayItemList = listOf(createDisplayItem(number = 1)),
),
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
mutableStateFlow.update {
it.copy(
policyDisablesSend = true,
)
}
composeTestRule
.onNodeWithContentDescription("Options")
.assertDoesNotExist()
}
@Test
fun `on send item overflow click should display dialog`() {
val number = 1
@ -1071,6 +1124,7 @@ private val DEFAULT_STATE = VaultItemListingState(
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isPullToRefreshSettingEnabled = false,
dialogState = null,
policyDisablesSend = false,
)
private val STATE_FOR_AUTOFILL = DEFAULT_STATE.copy(

View file

@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.manager.AutofillSelectionManagerImpl
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
@ -22,6 +23,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.baseWebSendUrl
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@ -51,6 +53,7 @@ import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -112,6 +115,11 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
}
private val specialCircumstanceManager = SpecialCircumstanceManagerImpl()
private val policyManager: PolicyManager = mockk {
every { getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) } returns emptyList()
every { getActivePoliciesFlow(type = PolicyTypeJson.DISABLE_SEND) } returns emptyFlow()
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@ -1340,6 +1348,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
autofillSelectionManager = autofillSelectionManager,
cipherMatchingManager = cipherMatchingManager,
specialCircumstanceManager = specialCircumstanceManager,
policyManager = policyManager,
)
@Suppress("MaxLineLength")
@ -1360,6 +1369,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
dialogState = null,
autofillSelectionData = null,
shouldFinishOnComplete = false,
policyDisablesSend = false,
)
}