mirror of
https://github.com/bitwarden/android.git
synced 2026-06-06 22:42:58 -05:00
BIT-783: Enforce Send restriction policy (#915)
This commit is contained in:
committed by
Álison Fernandes
parent
d538e37606
commit
05a171e71c
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user