mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
[PM-42110] feat: Make a copy of a policy-restricted Send (#7276)
This commit is contained in:
1 parent
f5a96ebfd8
commit
a26ddf45dc
14 files changed
+688
-61
No files matched your search
+11
-11
@@ -68,7 +68,7 @@ fun AddEditSendContent(
|
||||
policyDisablesSend: Boolean,
|
||||
policySendOptionsInEffect: Boolean,
|
||||
shouldHideEmailAddressToggle: Boolean,
|
||||
isAddMode: Boolean,
|
||||
isNewSend: Boolean,
|
||||
isShared: Boolean,
|
||||
addSendHandlers: AddEditSendHandlers,
|
||||
permissionsManager: PermissionsManager,
|
||||
@@ -126,7 +126,7 @@ fun AddEditSendContent(
|
||||
fileType = type,
|
||||
addSendHandlers = addSendHandlers,
|
||||
permissionsManager = permissionsManager,
|
||||
isAddMode = isAddMode,
|
||||
isNewSend = isNewSend,
|
||||
isShared = isShared,
|
||||
)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ fun AddEditSendContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
|
||||
if (isAddMode) {
|
||||
if (isNewSend) {
|
||||
AddEditSendDeletionDateChooser(
|
||||
enforcedDeletionHours = enforcedDeletionHours,
|
||||
onDateSelect = addSendHandlers.onDeletionDateChange,
|
||||
@@ -188,11 +188,11 @@ fun AddEditSendContent(
|
||||
state = state,
|
||||
isSendsRestrictedByPolicy = policyDisablesSend,
|
||||
shouldHideEmailAddressToggle = shouldHideEmailAddressToggle,
|
||||
isAddMode = isAddMode,
|
||||
isNewSend = isNewSend,
|
||||
addSendHandlers = addSendHandlers,
|
||||
)
|
||||
|
||||
if (!isAddMode) {
|
||||
if (!isNewSend) {
|
||||
DeleteButton(
|
||||
onDeleteClick = addSendHandlers.onDeleteClick,
|
||||
modifier = Modifier
|
||||
@@ -273,7 +273,7 @@ private fun ColumnScope.FileTypeContent(
|
||||
fileType: AddEditSendState.ViewState.Content.SendType.File,
|
||||
addSendHandlers: AddEditSendHandlers,
|
||||
permissionsManager: PermissionsManager,
|
||||
isAddMode: Boolean,
|
||||
isNewSend: Boolean,
|
||||
isShared: Boolean,
|
||||
) {
|
||||
val chooseFileCameraPermissionLauncher = permissionsManager.getLauncher { isGranted ->
|
||||
@@ -300,7 +300,7 @@ private fun ColumnScope.FileTypeContent(
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
} else if (isAddMode) {
|
||||
} else if (isNewSend) {
|
||||
fileType.name?.let {
|
||||
Box(
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
@@ -381,8 +381,8 @@ private fun ColumnScope.FileTypeContent(
|
||||
* @param state The content state.
|
||||
* @param isSendsRestrictedByPolicy 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 isNewSend When `true`, indicates that we are creating a new send and `false` when editing
|
||||
* an existing send. Copying a send creates a new one, so it is also `true` in that case.
|
||||
* @param addSendHandlers THe handlers various events.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@@ -391,7 +391,7 @@ private fun AddEditSendOptions(
|
||||
state: AddEditSendState.ViewState.Content,
|
||||
isSendsRestrictedByPolicy: Boolean,
|
||||
shouldHideEmailAddressToggle: Boolean,
|
||||
isAddMode: Boolean,
|
||||
isNewSend: Boolean,
|
||||
addSendHandlers: AddEditSendHandlers,
|
||||
) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -424,7 +424,7 @@ private fun AddEditSendOptions(
|
||||
targetState = state
|
||||
.common
|
||||
.currentAccessCount
|
||||
?.takeUnless { isAddMode || state.common.maxAccessCount == null },
|
||||
?.takeUnless { isNewSend || state.common.maxAccessCount == null },
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
|
||||
+2
@@ -27,6 +27,7 @@ data class AddEditSendRoute(
|
||||
@Serializable
|
||||
enum class ModeType {
|
||||
ADD,
|
||||
COPY,
|
||||
EDIT,
|
||||
}
|
||||
|
||||
@@ -47,6 +48,7 @@ fun SavedStateHandle.toAddEditSendArgs(): AddEditSendArgs {
|
||||
sendType = route.sendType,
|
||||
addEditSendType = when (route.modeType) {
|
||||
ModeType.ADD -> AddEditSendType.AddItem
|
||||
ModeType.COPY -> AddEditSendType.CopyItem(sendItemId = requireNotNull(route.sendId))
|
||||
ModeType.EDIT -> AddEditSendType.EditItem(sendItemId = requireNotNull(route.sendId))
|
||||
},
|
||||
)
|
||||
|
||||
+2
-2
@@ -136,7 +136,7 @@ fun AddEditSendScreen(
|
||||
modifier = Modifier.testTag("SaveButton"),
|
||||
)
|
||||
BitwardenOverflowActionItem(
|
||||
isVisible = !state.isAddMode,
|
||||
isVisible = !state.isNewSend,
|
||||
menuItemDataList = persistentListOfNotNull(
|
||||
OverflowMenuItemData(
|
||||
text = stringResource(id = BitwardenString.remove_password),
|
||||
@@ -178,7 +178,7 @@ fun AddEditSendScreen(
|
||||
policyDisablesSend = state.policyDisablesSend,
|
||||
policySendOptionsInEffect = state.shouldDisplayPolicyWarning,
|
||||
shouldHideEmailAddressToggle = state.shouldHideEmailAddressToggle,
|
||||
isAddMode = state.isAddMode,
|
||||
isNewSend = state.isNewSend,
|
||||
isShared = state.isShared,
|
||||
addSendHandlers = addSendHandlers,
|
||||
permissionsManager = permissionsManager,
|
||||
|
||||
+61
-38
@@ -43,11 +43,13 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AddEditSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AuthEmail
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.DEFAULT_DELETION_HOURS
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.shouldFinishOnComplete
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toAddEditViewState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toEnforcedSendAuth
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendName
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -68,11 +70,6 @@ import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The deletion window applied to a new Send when no deletion date is enforced by policy (7 days).
|
||||
*/
|
||||
private const val DEFAULT_DELETION_HOURS: Long = 7 * 24L
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
@@ -102,17 +99,6 @@ private fun String?.splitToDomains(): ImmutableList<String> = this
|
||||
.orEmpty()
|
||||
.toImmutableList()
|
||||
|
||||
/**
|
||||
* Returns the [SendAuth] this access type restricts a Send to, preserving [current] when it already
|
||||
* matches the restricted type so that any emails already entered are kept.
|
||||
*/
|
||||
private fun SendAccessTypeJson.toEnforcedSendAuth(current: SendAuth): SendAuth = when (this) {
|
||||
// Every option stays available, so the current selection is left alone.
|
||||
SendAccessTypeJson.ANY -> current
|
||||
SendAccessTypeJson.PASSWORD_PROTECTED -> SendAuth.Password
|
||||
SendAccessTypeJson.SPECIFIC_PEOPLE -> current as? SendAuth.Email ?: SendAuth.Email()
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for the add/edit send screen.
|
||||
*/
|
||||
@@ -196,7 +182,10 @@ class AddEditSendViewModel @Inject constructor(
|
||||
},
|
||||
)
|
||||
|
||||
is AddEditSendType.EditItem -> AddEditSendState.ViewState.Loading
|
||||
// A copy is loaded from the send it is based on, the same way an edit is.
|
||||
is AddEditSendType.CopyItem,
|
||||
is AddEditSendType.EditItem,
|
||||
-> AddEditSendState.ViewState.Loading
|
||||
},
|
||||
dialogState = null,
|
||||
baseWebSendUrl = environmentRepo.environment.baseWebSendUrl,
|
||||
@@ -212,18 +201,17 @@ class AddEditSendViewModel @Inject constructor(
|
||||
) {
|
||||
|
||||
init {
|
||||
when (val addSendType = state.addEditSendType) {
|
||||
AddEditSendType.AddItem -> Unit
|
||||
is AddEditSendType.EditItem -> {
|
||||
state
|
||||
.sourceSendItemIdOrNull
|
||||
?.let { sendItemId ->
|
||||
vaultRepo
|
||||
.getSendStateFlow(addSendType.sendItemId)
|
||||
.getSendStateFlow(sendItemId)
|
||||
// We'll stop getting updates as soon as we get some loaded data.
|
||||
.takeUntilLoaded()
|
||||
.map { AddEditSendAction.Internal.SendDataReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
generatorRepository
|
||||
.generatorResultFlow
|
||||
@@ -472,7 +460,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
// created with.
|
||||
val newEnforcedWhoCanAccess = effectiveSendPolicy
|
||||
.whoCanAccess
|
||||
?.takeIf { action.isSendControlsEnabled && state.isAddMode }
|
||||
?.takeIf { action.isSendControlsEnabled && state.isNewSend }
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
policyDisablesSend = effectiveSendPolicy.disableSend,
|
||||
@@ -507,7 +495,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
* unlocks.
|
||||
*/
|
||||
private fun AddEditSendState.newDeletionDateOrNull(newEnforcedDeletionHours: Int?): Instant? {
|
||||
if (!isAddMode) return null
|
||||
if (!isNewSend) return null
|
||||
val hours = when {
|
||||
newEnforcedDeletionHours != null -> newEnforcedDeletionHours.toLong()
|
||||
// The enforcement was just dropped, so the default window is restored.
|
||||
@@ -535,7 +523,22 @@ class AddEditSendViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
/**
|
||||
* Maps a loaded [SendView] into the content for whichever mode this screen is in, supplying the
|
||||
* mapping with the collaborators it needs from this view model.
|
||||
*
|
||||
* @param currentState The state being updated, which is the in-flight value rather than the
|
||||
* [state] property so that the mapping sees the same policy data as the update it belongs to.
|
||||
*/
|
||||
private fun SendView.toCurrentModeViewState(
|
||||
currentState: AddEditSendState,
|
||||
): AddEditSendState.ViewState.Content = toAddEditViewState(
|
||||
state = currentState,
|
||||
clock = clock,
|
||||
baseWebSendUrl = environmentRepo.environment.baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
)
|
||||
|
||||
private fun handleSendDataReceive(action: AddEditSendAction.Internal.SendDataReceive) {
|
||||
when (val sendDataState = action.sendDataState) {
|
||||
is DataState.Error -> {
|
||||
@@ -553,10 +556,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
it.copy(
|
||||
viewState = sendDataState
|
||||
.data
|
||||
?.toViewState(
|
||||
baseWebSendUrl = environmentRepo.environment.baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
)
|
||||
?.toCurrentModeViewState(currentState = it)
|
||||
?: AddEditSendState.ViewState.Error(
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
),
|
||||
@@ -590,10 +590,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
it.copy(
|
||||
viewState = sendDataState
|
||||
.data
|
||||
?.toViewState(
|
||||
baseWebSendUrl = environmentRepo.environment.baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
)
|
||||
?.toCurrentModeViewState(currentState = it)
|
||||
?: AddEditSendState.ViewState.Error(
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
),
|
||||
@@ -908,7 +905,10 @@ class AddEditSendViewModel @Inject constructor(
|
||||
}
|
||||
viewModelScope.launch {
|
||||
when (val addSendType = state.addEditSendType) {
|
||||
AddEditSendType.AddItem -> {
|
||||
// A copy is saved as a brand new Send, leaving the original untouched.
|
||||
AddEditSendType.AddItem,
|
||||
is AddEditSendType.CopyItem,
|
||||
-> {
|
||||
val fileType = content
|
||||
.selectedType as? AddEditSendState.ViewState.Content.SendType.File
|
||||
val result = vaultRepo.createSend(
|
||||
@@ -1098,7 +1098,10 @@ data class AddEditSendState(
|
||||
*/
|
||||
val screenDisplayName: Text
|
||||
get() = when (addEditSendType) {
|
||||
AddEditSendType.AddItem -> when (sendType) {
|
||||
// A copy is a new Send, so it is titled the same way as one.
|
||||
AddEditSendType.AddItem,
|
||||
is AddEditSendType.CopyItem,
|
||||
-> when (sendType) {
|
||||
SendItemType.FILE -> BitwardenString.add_file_send.asText()
|
||||
SendItemType.TEXT -> BitwardenString.add_text_send.asText()
|
||||
}
|
||||
@@ -1129,9 +1132,29 @@ data class AddEditSendState(
|
||||
(viewState as? ViewState.Content)?.common?.isHideEmailAddressEnabled == false
|
||||
|
||||
/**
|
||||
* Helper to determine if the UI should display the content in add send mode.
|
||||
* Helper to determine if this screen creates a brand-new Send rather than modifying an
|
||||
* existing one. A copy is a new Send that happens to be pre-filled from another, so it counts
|
||||
* as new despite being loaded from an existing one.
|
||||
*/
|
||||
val isAddMode: Boolean get() = addEditSendType is AddEditSendType.AddItem
|
||||
val isNewSend: Boolean
|
||||
get() = when (addEditSendType) {
|
||||
AddEditSendType.AddItem,
|
||||
is AddEditSendType.CopyItem,
|
||||
-> true
|
||||
|
||||
is AddEditSendType.EditItem -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine the ID of the send whose data this screen loads, or `null` when the send
|
||||
* is being created from scratch and there is nothing to load.
|
||||
*/
|
||||
val sourceSendItemIdOrNull: String?
|
||||
get() = when (val type = addEditSendType) {
|
||||
AddEditSendType.AddItem -> null
|
||||
is AddEditSendType.CopyItem -> type.sendItemId
|
||||
is AddEditSendType.EditItem -> type.sendItemId
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine if the currently displayed send has a password already set.
|
||||
|
||||
+11
@@ -13,6 +13,17 @@ sealed class AddEditSendType : Parcelable {
|
||||
@Parcelize
|
||||
data object AddItem : AddEditSendType()
|
||||
|
||||
/**
|
||||
* Indicates that we want to create a new send item pre-filled from an existing one.
|
||||
*
|
||||
* This creates a send like [AddItem] does, but its initial values are loaded from the send
|
||||
* being copied.
|
||||
*
|
||||
* @param sendItemId The ID of the send item to copy.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CopyItem(val sendItemId: String) : AddEditSendType()
|
||||
|
||||
/**
|
||||
* Indicates that we want to edit an existing send item.
|
||||
*
|
||||
|
||||
+134
-10
@@ -1,12 +1,85 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit.util
|
||||
|
||||
import com.bitwarden.network.model.SendAccessTypeJson
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AddEditSendType
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AuthEmail
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendUrl
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
/**
|
||||
* The deletion window applied to a new Send when no deletion date is enforced by policy (7 days).
|
||||
*/
|
||||
internal const val DEFAULT_DELETION_HOURS: Long = 7 * 24L
|
||||
|
||||
/**
|
||||
* Returns the [SendAuth] this access type restricts a Send to, preserving [current] when it already
|
||||
* matches the restricted type so that any emails already entered are kept.
|
||||
*/
|
||||
internal fun SendAccessTypeJson.toEnforcedSendAuth(current: SendAuth): SendAuth = when (this) {
|
||||
// Every option stays available, so the current selection is left alone.
|
||||
SendAccessTypeJson.ANY -> current
|
||||
SendAccessTypeJson.PASSWORD_PROTECTED -> SendAuth.Password
|
||||
SendAccessTypeJson.SPECIFIC_PEOPLE -> current as? SendAuth.Email ?: SendAuth.Email()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [SendAuth] describing who this Send is shared with.
|
||||
*/
|
||||
fun SendView.toSendAuth(): SendAuth = when {
|
||||
hasPassword -> SendAuth.Password
|
||||
emails.isNotEmpty() -> {
|
||||
SendAuth.Email(emails = this.emails.map { AuthEmail(value = it) }.toImmutableList())
|
||||
}
|
||||
|
||||
else -> SendAuth.None
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps this loaded [SendView] into the content for whichever mode the add/edit screen is in: the
|
||||
* send's own values when editing it, or the values a compliant copy of it should start from.
|
||||
*
|
||||
* @param state The state being updated, which is the in-flight value rather than the view model's
|
||||
* `state` property so that the mapping sees the same policy data as the update it belongs to.
|
||||
* @param clock Used to derive the copy's deletion date from the enforced window.
|
||||
* @param baseWebSendUrl The base URL used to build an existing send's share URL.
|
||||
* @param isHideEmailAddressEnabled Whether hiding the sender's email is currently allowed.
|
||||
*/
|
||||
fun SendView.toAddEditViewState(
|
||||
state: AddEditSendState,
|
||||
clock: Clock,
|
||||
baseWebSendUrl: String,
|
||||
isHideEmailAddressEnabled: Boolean,
|
||||
): AddEditSendState.ViewState.Content = when (state.addEditSendType) {
|
||||
is AddEditSendType.CopyItem -> {
|
||||
toCopyViewState(
|
||||
deletionDate = clock.instant().plus(
|
||||
state.enforcedDeletionHours?.toLong() ?: DEFAULT_DELETION_HOURS,
|
||||
ChronoUnit.HOURS,
|
||||
),
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
sendAuth = state
|
||||
.enforcedWhoCanAccess
|
||||
?.toEnforcedSendAuth(current = this.toSendAuth())
|
||||
?: this.toSendAuth(),
|
||||
)
|
||||
}
|
||||
|
||||
AddEditSendType.AddItem,
|
||||
is AddEditSendType.EditItem,
|
||||
-> {
|
||||
toViewState(
|
||||
baseWebSendUrl = baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [SendView] into [AddEditSendState.ViewState.Content].
|
||||
@@ -32,16 +105,7 @@ fun SendView.toViewState(
|
||||
sendUrl = this.toSendUrl(baseWebSendUrl),
|
||||
hasPassword = this.hasPassword,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
sendAuth = when {
|
||||
hasPassword -> SendAuth.Password
|
||||
emails.isNotEmpty() -> {
|
||||
SendAuth.Email(
|
||||
emails = this.emails.map { AuthEmail(value = it) }.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> SendAuth.None
|
||||
},
|
||||
sendAuth = this.toSendAuth(),
|
||||
),
|
||||
selectedType = when (type) {
|
||||
SendType.TEXT -> {
|
||||
@@ -64,3 +128,63 @@ fun SendView.toViewState(
|
||||
SendType.ITEM -> TODO("[PM-41095] Support Item SendType")
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Transforms [SendView] into the [AddEditSendState.ViewState.Content] for a copy of it.
|
||||
*
|
||||
* A copy is a new Send rather than an edit of this one, so anything that belongs to the original is
|
||||
* left behind: it has no share URL or access count yet, it is never created deactivated, and the
|
||||
* original password cannot be carried over because it is never readable. [deletionDate] comes from
|
||||
* the caller so that a policy-enforced window is applied to the copy.
|
||||
*
|
||||
* Recipient emails are copied as they are, even when a policy restricts their domains. The user is
|
||||
* told which domains are allowed when they save, which is friendlier than silently dropping
|
||||
* recipients they chose.
|
||||
*
|
||||
* @param deletionDate The deletion date to apply to the copy.
|
||||
* @param isHideEmailAddressEnabled Whether hiding the sender's email is currently allowed.
|
||||
* @param sendAuth The access type to apply to the copy, which the policy may have forced.
|
||||
*/
|
||||
fun SendView.toCopyViewState(
|
||||
deletionDate: Instant,
|
||||
isHideEmailAddressEnabled: Boolean,
|
||||
sendAuth: SendAuth,
|
||||
): AddEditSendState.ViewState.Content =
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = AddEditSendState.ViewState.Content.Common(
|
||||
originalSendView = null,
|
||||
name = this.name,
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = this.maxAccessCount?.toInt(),
|
||||
passwordInput = "",
|
||||
noteInput = this.notes.orEmpty(),
|
||||
isHideEmailChecked = this.hideEmail && isHideEmailAddressEnabled,
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = deletionDate,
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
sendAuth = sendAuth,
|
||||
),
|
||||
selectedType = when (type) {
|
||||
SendType.TEXT -> {
|
||||
AddEditSendState.ViewState.Content.SendType.Text(
|
||||
input = this.text?.text.orEmpty(),
|
||||
isHideByDefaultChecked = this.text?.hidden == true,
|
||||
)
|
||||
}
|
||||
|
||||
// A file send's attachment cannot be uploaded again, so it is never offered a copy.
|
||||
SendType.FILE -> {
|
||||
AddEditSendState.ViewState.Content.SendType.File(
|
||||
uri = null,
|
||||
name = null,
|
||||
displaySize = null,
|
||||
sizeBytes = null,
|
||||
)
|
||||
}
|
||||
|
||||
SendType.ITEM -> TODO("[PM-41095] Support Item SendType")
|
||||
},
|
||||
)
|
||||
+20
@@ -51,6 +51,7 @@ import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedErrorButton
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData
|
||||
import com.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenErrorContent
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
|
||||
@@ -97,6 +98,16 @@ fun ViewSendScreen(
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
is ViewSendEvent.NavigateBack -> onNavigateBack()
|
||||
is ViewSendEvent.NavigateToCopy -> {
|
||||
onNavigateToAddEditSend(
|
||||
AddEditSendRoute(
|
||||
sendType = event.sendType,
|
||||
modeType = ModeType.COPY,
|
||||
sendId = event.sendId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ViewSendEvent.NavigateToEdit -> {
|
||||
onNavigateToAddEditSend(
|
||||
AddEditSendRoute(
|
||||
@@ -158,6 +169,7 @@ fun ViewSendScreen(
|
||||
onCopyClick = { viewModel.trySendAction(ViewSendAction.CopyClick) },
|
||||
onCopyNotesClick = { viewModel.trySendAction(ViewSendAction.CopyNotesClick) },
|
||||
onDeleteClick = { viewModel.trySendAction(ViewSendAction.DeleteClick) },
|
||||
onMakeACopyClick = { viewModel.trySendAction(ViewSendAction.MakeACopyClick) },
|
||||
onShareClick = { viewModel.trySendAction(ViewSendAction.ShareClick) },
|
||||
)
|
||||
}
|
||||
@@ -192,6 +204,7 @@ private fun ViewSendScreenContent(
|
||||
onCopyClick: () -> Unit,
|
||||
onCopyNotesClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
onMakeACopyClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -203,6 +216,7 @@ private fun ViewSendScreenContent(
|
||||
onCopyClick = onCopyClick,
|
||||
onCopyNotesClick = onCopyNotesClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
onMakeACopyClick = onMakeACopyClick,
|
||||
onShareClick = onShareClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
@@ -229,6 +243,7 @@ private fun ViewStateContent(
|
||||
onCopyClick: () -> Unit,
|
||||
onCopyNotesClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
onMakeACopyClick: () -> Unit,
|
||||
onShareClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -240,6 +255,11 @@ private fun ViewStateContent(
|
||||
BitwardenActionCard(
|
||||
cardTitle = stringResource(id = BitwardenString.organization_policy_restriction),
|
||||
cardSubtitle = it.message(),
|
||||
actionButton = BitwardenButtonData(
|
||||
label = BitwardenString.make_a_copy.asText(),
|
||||
onClick = onMakeACopyClick,
|
||||
)
|
||||
.takeIf { _ -> it.isCopyable },
|
||||
leadingContent = {
|
||||
BitwardenIcon(
|
||||
iconData = IconData.Local(iconRes = BitwardenDrawable.ic_info_circle),
|
||||
|
||||
+18
@@ -109,6 +109,7 @@ class ViewSendViewModel @Inject constructor(
|
||||
ViewSendAction.DeleteClick -> handleDeleteClick()
|
||||
ViewSendAction.DialogDismiss -> handleDialogDismiss()
|
||||
ViewSendAction.EditClick -> handleEditClick()
|
||||
ViewSendAction.MakeACopyClick -> handleMakeACopyClick()
|
||||
ViewSendAction.ShareClick -> handleShareClick()
|
||||
is ViewSendAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
@@ -172,6 +173,10 @@ class ViewSendViewModel @Inject constructor(
|
||||
sendEvent(ViewSendEvent.NavigateToEdit(sendType = state.sendType, sendId = state.sendId))
|
||||
}
|
||||
|
||||
private fun handleMakeACopyClick() {
|
||||
sendEvent(ViewSendEvent.NavigateToCopy(sendType = state.sendType, sendId = state.sendId))
|
||||
}
|
||||
|
||||
private fun handleShareClick() {
|
||||
onContent { sendEvent(ViewSendEvent.ShareText(text = it.shareLink.asText())) }
|
||||
}
|
||||
@@ -434,6 +439,14 @@ sealed class ViewSendEvent {
|
||||
*/
|
||||
data object NavigateBack : ViewSendEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the add send screen, pre-filled from the current send.
|
||||
*/
|
||||
data class NavigateToCopy(
|
||||
val sendType: SendItemType,
|
||||
val sendId: String,
|
||||
) : ViewSendEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the edit send screen for the current send.
|
||||
*/
|
||||
@@ -491,6 +504,11 @@ sealed class ViewSendAction {
|
||||
*/
|
||||
data object EditClick : ViewSendAction()
|
||||
|
||||
/**
|
||||
* The user clicked the make a copy button on the policy restriction warning.
|
||||
*/
|
||||
data object MakeACopyClick : ViewSendAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the share button.
|
||||
*/
|
||||
|
||||
+11
@@ -11,6 +11,11 @@ import kotlinx.parcelize.Parcelize
|
||||
* warning shown on the view send screen and whether a compliant copy can be made.
|
||||
*/
|
||||
sealed class SendPolicyRestriction : Parcelable {
|
||||
/**
|
||||
* Whether a compliant copy of the Send can be made to replace it.
|
||||
*/
|
||||
abstract val isCopyable: Boolean
|
||||
|
||||
/**
|
||||
* The explanation shown to the user for this restriction.
|
||||
*/
|
||||
@@ -22,6 +27,8 @@ sealed class SendPolicyRestriction : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data object FileNotCompliant : SendPolicyRestriction() {
|
||||
override val isCopyable: Boolean get() = false
|
||||
|
||||
override val message: Text
|
||||
get() = BitwardenString
|
||||
.this_send_is_not_compliant_with_your_organizations_send_policy
|
||||
@@ -34,6 +41,8 @@ sealed class SendPolicyRestriction : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data object TypeNotAllowed : SendPolicyRestriction() {
|
||||
override val isCopyable: Boolean get() = false
|
||||
|
||||
override val message: Text
|
||||
get() = BitwardenString.text_sends_are_not_allowed_for_your_organization.asText()
|
||||
}
|
||||
@@ -43,6 +52,8 @@ sealed class SendPolicyRestriction : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data object CopyRequired : SendPolicyRestriction() {
|
||||
override val isCopyable: Boolean get() = true
|
||||
|
||||
override val message: Text
|
||||
get() = BitwardenString.to_edit_this_send_make_a_copy.asText()
|
||||
}
|
||||
|
||||
+138
@@ -8,6 +8,7 @@ import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.network.model.SendAccessTypeJson
|
||||
import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
@@ -61,6 +62,7 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
@@ -623,6 +625,101 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `copy mode should apply the enforced deletion window to the copy`() {
|
||||
mutableSendControlsFlagFlow.value = true
|
||||
mutableEffectiveSendPolicyFlow.value =
|
||||
DEFAULT_EFFECTIVE_SEND_POLICY.copy(deletionHours = ENFORCED_DELETION_HOURS)
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
|
||||
val state = createViewModel(
|
||||
addEditSendType = AddEditSendType.CopyItem(sendItemId = COPY_SEND_ID),
|
||||
)
|
||||
.stateFlow
|
||||
.value
|
||||
|
||||
// The copy adopts the policy window rather than the original send's deletion date.
|
||||
assertEquals(
|
||||
copyModeState(
|
||||
sendAuth = SendAuth.Password,
|
||||
deletionDate = ENFORCED_DELETION_DATE,
|
||||
isSendControlsEnabled = true,
|
||||
deletionHours = ENFORCED_DELETION_HOURS,
|
||||
),
|
||||
state,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `copy mode should apply the enforced access type to the copy`() {
|
||||
mutableSendControlsFlagFlow.value = true
|
||||
mutableEffectiveSendPolicyFlow.value = DEFAULT_EFFECTIVE_SEND_POLICY
|
||||
.copy(whoCanAccess = SendAccessTypeJson.PASSWORD_PROTECTED)
|
||||
val mockSendView = createMockSendView(
|
||||
number = 1,
|
||||
type = SendType.TEXT,
|
||||
hasPassword = false,
|
||||
)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
|
||||
val state = createViewModel(
|
||||
addEditSendType = AddEditSendType.CopyItem(sendItemId = COPY_SEND_ID),
|
||||
)
|
||||
.stateFlow
|
||||
.value
|
||||
|
||||
assertEquals(
|
||||
copyModeState(
|
||||
sendAuth = SendAuth.Password,
|
||||
isSendControlsEnabled = true,
|
||||
whoCanAccess = SendAccessTypeJson.PASSWORD_PROTECTED,
|
||||
),
|
||||
state,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `copy mode should be treated as a new send`() {
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
|
||||
val state = createViewModel(
|
||||
addEditSendType = AddEditSendType.CopyItem(sendItemId = COPY_SEND_ID),
|
||||
)
|
||||
.stateFlow
|
||||
.value
|
||||
|
||||
assertEquals(copyModeState(sendAuth = SendAuth.Password), state)
|
||||
assertTrue(state.isNewSend)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SaveClick in copy mode should create a new send rather than update the original`() =
|
||||
runTest {
|
||||
val sendId = "sendId-1"
|
||||
val mockSendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
val viewModel = createViewModel(
|
||||
addEditSendType = AddEditSendType.CopyItem(sendItemId = sendId),
|
||||
)
|
||||
val content = viewModel.stateFlow.value.viewState as AddEditSendState.ViewState.Content
|
||||
val createdSendView = mockk<SendView>()
|
||||
every { content.toSendView(clock) } returns createdSendView
|
||||
coEvery {
|
||||
vaultRepository.createSend(sendView = createdSendView, fileUri = null)
|
||||
} returns CreateSendResult.Error(message = null, error = null)
|
||||
|
||||
viewModel.trySendAction(AddEditSendAction.SaveClick)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createSend(sendView = createdSendView, fileUri = null)
|
||||
}
|
||||
coVerify(exactly = 0) {
|
||||
vaultRepository.updateSend(sendId = any(), sendView = any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should read from saved state when present`() {
|
||||
val savedState = DEFAULT_STATE.copy(
|
||||
@@ -2203,6 +2300,47 @@ private fun enforcedAccessState(
|
||||
),
|
||||
)
|
||||
|
||||
private const val COPY_SEND_ID: String = "sendId-1"
|
||||
|
||||
/**
|
||||
* Builds the state expected in copy mode, where the content is derived from the send being copied
|
||||
* rather than from [DEFAULT_VIEW_STATE].
|
||||
*/
|
||||
private fun copyModeState(
|
||||
sendAuth: SendAuth,
|
||||
deletionDate: Instant = Instant.parse("2023-11-03T12:00:00Z"),
|
||||
isSendControlsEnabled: Boolean = false,
|
||||
deletionHours: Int? = null,
|
||||
whoCanAccess: SendAccessTypeJson? = null,
|
||||
): AddEditSendState = DEFAULT_STATE.copy(
|
||||
addEditSendType = AddEditSendType.CopyItem(sendItemId = COPY_SEND_ID),
|
||||
isSendControlsEnabled = isSendControlsEnabled,
|
||||
deletionHours = deletionHours,
|
||||
whoCanAccess = whoCanAccess,
|
||||
viewState = AddEditSendState.ViewState.Content(
|
||||
common = AddEditSendState.ViewState.Content.Common(
|
||||
originalSendView = null,
|
||||
name = "mockName-1",
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = 1,
|
||||
passwordInput = "",
|
||||
noteInput = "mockNotes-1",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = deletionDate,
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = sendAuth,
|
||||
),
|
||||
selectedType = AddEditSendState.ViewState.Content.SendType.Text(
|
||||
input = "mockText-1",
|
||||
isHideByDefaultChecked = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val ENFORCED_DELETION_DATE: Instant = Instant.parse("2023-10-28T12:00:00Z")
|
||||
|
||||
private const val ENFORCED_DELETION_HOURS: Int = 24
|
||||
|
||||
+200
@@ -3,13 +3,31 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addedit.util
|
||||
import com.bitwarden.send.SendType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AuthEmail
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
class SendViewExtensionsTest {
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(UUID::randomUUID)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(UUID::randomUUID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create an appropriate ViewState for file type`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.FILE)
|
||||
@@ -50,8 +68,172 @@ class SendViewExtensionsTest {
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendAuth should use a password when the original has one`() {
|
||||
val sendView = createMockSendView(number = 1, hasPassword = true)
|
||||
|
||||
assertEquals(SendAuth.Password, sendView.toSendAuth())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendAuth should carry over the original recipients`() {
|
||||
every { UUID.randomUUID().toString() } returns "uuid"
|
||||
val sendView = createMockSendView(number = 1, hasPassword = false)
|
||||
.copy(emails = listOf("one@example.com", "two@example.com"))
|
||||
|
||||
assertEquals(
|
||||
SendAuth.Email(
|
||||
emails = persistentListOf(
|
||||
AuthEmail(id = "uuid", value = "one@example.com"),
|
||||
AuthEmail(id = "uuid", value = "two@example.com"),
|
||||
),
|
||||
),
|
||||
sendView.toSendAuth(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendAuth should use link access when the original has neither`() {
|
||||
val sendView = createMockSendView(number = 1, hasPassword = false)
|
||||
|
||||
assertEquals(SendAuth.None, sendView.toSendAuth())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should carry over the copyable fields of a text send`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON,
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should not carry over anything belonging to the original send`() {
|
||||
val sendView = createMockSendView(
|
||||
number = 1,
|
||||
type = SendType.TEXT,
|
||||
disabled = true,
|
||||
hasPassword = true,
|
||||
)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
// The copy is a brand new Send, so the original's URL, access count, expiration date,
|
||||
// password and deactivated state are all left behind.
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON,
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should use the deletion date it is given`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = OTHER_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON.copy(deletionDate = OTHER_DELETION_DATE),
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should use the access type it is given`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT)
|
||||
val sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(AuthEmail(id = "id", value = "test@example.com")),
|
||||
)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = sendAuth,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON.copy(sendAuth = sendAuth),
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should not hide the email when the policy disallows it`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT, hideEmail = true)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = false,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON.copy(isHideEmailAddressEnabled = false),
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toCopyViewState should drop the attachment of a file send`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.FILE)
|
||||
|
||||
val result = sendView.toCopyViewState(
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = COPY_COMMON,
|
||||
selectedType = AddEditSendState.ViewState.Content.SendType.File(
|
||||
uri = null,
|
||||
name = null,
|
||||
displaySize = null,
|
||||
sizeBytes = null,
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val COPY_DELETION_DATE: Instant = Instant.parse("2023-11-03T12:00:00Z")
|
||||
|
||||
private val OTHER_DELETION_DATE: Instant = Instant.parse("2023-12-25T12:00:00Z")
|
||||
|
||||
private val DEFAULT_COMMON: AddEditSendState.ViewState.Content.Common =
|
||||
AddEditSendState.ViewState.Content.Common(
|
||||
name = "mockName-1",
|
||||
@@ -88,3 +270,21 @@ private val DEFAULT_STATE: AddEditSendState.ViewState.Content =
|
||||
common = DEFAULT_COMMON,
|
||||
selectedType = DEFAULT_FILE_TYPE,
|
||||
)
|
||||
|
||||
private val COPY_COMMON: AddEditSendState.ViewState.Content.Common =
|
||||
AddEditSendState.ViewState.Content.Common(
|
||||
originalSendView = null,
|
||||
name = "mockName-1",
|
||||
currentAccessCount = null,
|
||||
maxAccessCount = 1,
|
||||
passwordInput = "",
|
||||
noteInput = "mockNotes-1",
|
||||
isHideEmailChecked = false,
|
||||
isDeactivateChecked = false,
|
||||
deletionDate = COPY_DELETION_DATE,
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isHideEmailAddressEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
+67
@@ -177,6 +177,73 @@ class ViewSendScreenTest : BitwardenComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `make a copy button should only be displayed when a copy is possible`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isSendDisabled = true,
|
||||
isSendControlsEnabled = true,
|
||||
isSendControlsExistingSendsEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Make a copy")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
// A file send cannot be copied, so the action is not offered.
|
||||
mutableStateFlow.update { it.copy(sendType = SendItemType.FILE) }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Make a copy")
|
||||
.assertDoesNotExist()
|
||||
|
||||
// Neither can a send whose type the policy no longer allows.
|
||||
mutableStateFlow.update {
|
||||
it.copy(sendType = SendItemType.TEXT, allowedSendTypes = listOf(SendTypeJson.FILE))
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Make a copy")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on make a copy click should send MakeACopyClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isSendDisabled = true,
|
||||
isSendControlsEnabled = true,
|
||||
isSendControlsExistingSendsEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Make a copy")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(ViewSendAction.MakeACopyClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on NavigateToCopy event should navigate to add send in copy mode`() {
|
||||
val sendType = SendItemType.TEXT
|
||||
val sendId = "send_id"
|
||||
|
||||
mutableEventFlow.tryEmit(
|
||||
ViewSendEvent.NavigateToCopy(sendType = sendType, sendId = sendId),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
AddEditSendRoute(sendId = sendId, sendType = sendType, modeType = ModeType.COPY),
|
||||
onNavigateToAddEditRoute,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edit fab should be hidden when a policy restriction applies`() {
|
||||
composeTestRule
|
||||
|
||||
+12
@@ -234,6 +234,18 @@ class ViewSendViewModelTest : BaseViewModelTest() {
|
||||
assertFalse(state.isFabVisible)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on MakeACopyClick should send NavigateToCopy`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ViewSendAction.MakeACopyClick)
|
||||
assertEquals(
|
||||
ViewSendEvent.NavigateToCopy(sendType = SendItemType.TEXT, sendId = "send_id"),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CloseClick should send NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
@@ -516,6 +516,7 @@ Scanning will happen automatically.</string>
|
||||
<string name="this_send_is_not_compliant_with_your_organizations_send_policy">This Send is not compliant with your organization’s Send policy and will automatically expire at the set deletion date.</string>
|
||||
<string name="text_sends_are_not_allowed_for_your_organization">Text Sends are not allowed for your organization and this Send will automatically expire at the set deletion date.</string>
|
||||
<string name="to_edit_this_send_make_a_copy">To edit this Send, make a copy. If this Send can expire at the set deletion date, no action is needed.</string>
|
||||
<string name="make_a_copy">Make a copy</string>
|
||||
<string name="send_file_premium_required">Free accounts are restricted to sharing text only. A Premium membership is required to use files with Send.</string>
|
||||
<string name="sharing_with_specific_people_is_a_premium_feature">Sharing with specific people is a Premium feature. Your current plan does not include access to this feature.</string>
|
||||
<string name="password_prompt">Master password re-prompt</string>
|
||||
|
||||
Reference in new issue
Block a user