mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-783: Enforce Send restriction policy (#915)
This commit is contained in:
parent
d538e37606
commit
05a171e71c
25 changed files with 416 additions and 16 deletions
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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) },
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,6 +285,7 @@ private fun VaultScreenScaffold(
|
|||
|
||||
is VaultState.ViewState.NoItems -> VaultNoItems(
|
||||
modifier = innerModifier,
|
||||
policyDisablesSend = false,
|
||||
addItemClickAction = vaultHandlers.addItemClickAction,
|
||||
)
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue