diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt index 9a6f7783a4..a6920f6806 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenDateSelectButton.kt @@ -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 }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt index bfb2617915..74feacf5be 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/dialog/BitwardenTimeSelectButton.kt @@ -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 }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt index a8c413562e..7188561ccf 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendContent.kt @@ -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) }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt index 339d44f045..b0a99c8939 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendEmpty.kt @@ -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)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt index e93b4eca59..4d43dac777 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendListItem.kt @@ -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, + 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 = {}, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt index a8594bb428..c87e53042e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreen.kt @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt index 1da20eced7..11f161ebed 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModel.kt @@ -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( // 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, ) : Internal() + + /** + * Indicates that a policy update has been received. + */ + data class PolicyUpdateReceive( + val policyDisablesSend: Boolean, + ) : Internal() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt index f5c391e510..67c5577bfc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendCustomDateChooser.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendCustomDateChooser.kt index 0901db7e4e..f983cfdcda 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendCustomDateChooser.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendCustomDateChooser.kt @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt index dc7d7e9543..2007943486 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendDeletionDateChooser.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt index af6b212f36..aa8456c219 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendExpirationDateChooser.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt index a97b20055f..e605eb8f48 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt index c004a0e2a7..94f02976a6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt @@ -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( // 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 { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index 8f9ec09512..7804fc1b8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -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() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingEmpty.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingEmpty.kt index 23610890a7..bc951ea088 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingEmpty.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingEmpty.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index 4c5134e845..9593abc08d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -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, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 85b8d805bd..08bdbd2997 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -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( 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() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItems.kt index 24ff2133b1..c988404088 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNoItems.kt @@ -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)) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 328bc91513..2d9d5865a3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -285,6 +285,7 @@ private fun VaultScreenScaffold( is VaultState.ViewState.NoItems -> VaultNoItems( modifier = innerModifier, + policyDisablesSend = false, addItemClickAction = vaultHandlers.addItemClickAction, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt index dcf5975d36..51504524e5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendScreenTest.kt @@ -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 = diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt index 0e593f0d5e..8f0208a3ce 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/SendViewModelTest.kt @@ -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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 7144bb7527..94b2b76f12 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -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, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index 8b6b0c6c69..027489e995 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index ee149ea6b5..6e9c79ae3a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 1ce52357d3..d245cb244a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -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, ) }