mirror of
https://github.com/bitwarden/android.git
synced 2026-08-24 14:28:58 -05:00
[PM-31614] feat: Added new UI for the Email verification on sends (#6488)
This commit is contained in:
+56
-34
@@ -52,6 +52,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.components.AddEditSendAuthTypeChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.components.AddEditSendCustomDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.components.AddEditSendDeletionDateChooser
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.handlers.AddEditSendHandlers
|
||||
@@ -160,6 +161,24 @@ fun AddEditSendContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (state.common.isSendEmailVerificationEnabled) {
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
AddEditSendAuthTypeChooser(
|
||||
sendAuth = state.common.sendAuth,
|
||||
onAuthTypeSelect = addSendHandlers.onAuthTypeSelect,
|
||||
onPasswordChange = addSendHandlers.onAuthPasswordChange,
|
||||
onEmailValueChange = addSendHandlers.onEmailValueChange,
|
||||
onRemoveEmailClick = addSendHandlers.onEmailsRemoveClick,
|
||||
onAddNewEmailClick = addSendHandlers.onAddNewEmailClick,
|
||||
password = state.common.passwordInput,
|
||||
isEnabled = !policyDisablesSend,
|
||||
modifier = Modifier
|
||||
.testTag("SendAuthTypeChooser")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
|
||||
AddEditSendOptions(
|
||||
state = state,
|
||||
sendRestrictionPolicy = policyDisablesSend,
|
||||
@@ -427,40 +446,43 @@ private fun AddEditSendOptions(
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.new_password),
|
||||
supportingText = stringResource(id = BitwardenString.password_info),
|
||||
readOnly = sendRestrictionPolicy,
|
||||
value = state.common.passwordInput,
|
||||
onValueChange = addSendHandlers.onPasswordChange,
|
||||
passwordFieldTestTag = "SendNewPasswordEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
) {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_generate,
|
||||
contentDescription = stringResource(id = BitwardenString.generate_password),
|
||||
onClick = {
|
||||
if (state.common.passwordInput.isEmpty()) {
|
||||
addSendHandlers.onOpenPasswordGeneratorClick()
|
||||
} else {
|
||||
shouldShowDialog = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(tag = "RegeneratePasswordButton"),
|
||||
)
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = stringResource(id = BitwardenString.copy_password),
|
||||
isEnabled = state.common.passwordInput.isNotEmpty(),
|
||||
onClick = {
|
||||
addSendHandlers.onPasswordCopyClick(state.common.passwordInput)
|
||||
},
|
||||
modifier = Modifier.testTag(tag = "CopyPasswordButton"),
|
||||
)
|
||||
|
||||
if (!state.common.isSendEmailVerificationEnabled) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.new_password),
|
||||
supportingText = stringResource(id = BitwardenString.password_info),
|
||||
readOnly = sendRestrictionPolicy,
|
||||
value = state.common.passwordInput,
|
||||
onValueChange = addSendHandlers.onPasswordChange,
|
||||
passwordFieldTestTag = "SendNewPasswordEntry",
|
||||
cardStyle = CardStyle.Full,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
) {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_generate,
|
||||
contentDescription = stringResource(id = BitwardenString.generate_password),
|
||||
onClick = {
|
||||
if (state.common.passwordInput.isEmpty()) {
|
||||
addSendHandlers.onOpenPasswordGeneratorClick()
|
||||
} else {
|
||||
shouldShowDialog = true
|
||||
}
|
||||
},
|
||||
modifier = Modifier.testTag(tag = "RegeneratePasswordButton"),
|
||||
)
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = stringResource(id = BitwardenString.copy_password),
|
||||
isEnabled = state.common.passwordInput.isNotEmpty(),
|
||||
onClick = {
|
||||
addSendHandlers.onPasswordCopyClick(state.common.passwordInput)
|
||||
},
|
||||
modifier = Modifier.testTag(tag = "CopyPasswordButton"),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (shouldShowDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
|
||||
+134
-1
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.takeUntilLoaded
|
||||
import com.bitwarden.data.repository.util.baseWebSendUrl
|
||||
@@ -20,6 +21,7 @@ import com.bitwarden.ui.util.asText
|
||||
import com.bitwarden.ui.util.concat
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
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
|
||||
@@ -36,6 +38,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
|
||||
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.shouldFinishOnComplete
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendName
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendType
|
||||
@@ -44,6 +48,8 @@ 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
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -80,6 +86,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
private val policyManager: PolicyManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : BaseViewModel<AddEditSendState, AddEditSendEvent, AddEditSendAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
@@ -89,6 +96,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
val args = savedStateHandle.toAddEditSendArgs()
|
||||
val sendType = args.sendType
|
||||
val addEditSendType = args.addEditSendType
|
||||
|
||||
AddEditSendState(
|
||||
sendType = sendType,
|
||||
shouldFinishOnComplete = specialCircumstance.shouldFinishOnComplete(),
|
||||
@@ -111,6 +119,9 @@ class AddEditSendViewModel @Inject constructor(
|
||||
expirationDate = null,
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isSendEmailVerificationEnabled = featureFlagManager
|
||||
.getFeatureFlag(key = FlagKey.SendEmailVerification),
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
selectedType = shareSendType ?: when (sendType) {
|
||||
SendItemType.FILE -> {
|
||||
@@ -194,6 +205,12 @@ class AddEditSendViewModel @Inject constructor(
|
||||
is AddEditSendAction.PasswordCopyClick -> {
|
||||
handleCopyClick(password = action.password)
|
||||
}
|
||||
|
||||
is AddEditSendAction.AuthTypeSelect -> handleAuthTypeSelect(action)
|
||||
is AddEditSendAction.AuthPasswordChange -> handleAuthPasswordChange(action)
|
||||
is AddEditSendAction.AuthEmailChange -> handleAuthEmailsChange(action)
|
||||
is AddEditSendAction.AuthEmailAdd -> handleAuthEmailsAdd()
|
||||
is AddEditSendAction.AuthEmailRemove -> handleAuthEmailsRemove(action)
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: AddEditSendAction.Internal): Unit = when (action) {
|
||||
@@ -386,6 +403,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
.environmentUrlData
|
||||
.baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
isSendEmailVerificationEnabled = isSendEmailVerificationEnabled,
|
||||
)
|
||||
?: AddEditSendState.ViewState.Error(
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
@@ -427,6 +445,7 @@ class AddEditSendViewModel @Inject constructor(
|
||||
.environmentUrlData
|
||||
.baseWebSendUrl,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
isSendEmailVerificationEnabled = isSendEmailVerificationEnabled,
|
||||
)
|
||||
?: AddEditSendState.ViewState.Error(
|
||||
message = BitwardenString.generic_error_message.asText(),
|
||||
@@ -500,7 +519,14 @@ class AddEditSendViewModel @Inject constructor(
|
||||
|
||||
private fun handlePasswordChange(action: AddEditSendAction.PasswordChange) {
|
||||
updateCommonContent {
|
||||
it.copy(passwordInput = action.input)
|
||||
it.copy(
|
||||
passwordInput = action.input,
|
||||
sendAuth = when {
|
||||
action.input.isNotEmpty() -> SendAuth.Password
|
||||
!isSendEmailVerificationEnabled -> SendAuth.None
|
||||
else -> it.sendAuth
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +556,82 @@ class AddEditSendViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthTypeSelect(action: AddEditSendAction.AuthTypeSelect) {
|
||||
updateCommonContent { commonContent ->
|
||||
commonContent.copy(
|
||||
sendAuth = when (action.sendAuth) {
|
||||
is SendAuth.Email -> {
|
||||
// Preserve existing emails if switching back to EMAIL
|
||||
when (val currentAuth = commonContent.sendAuth) {
|
||||
is SendAuth.Email -> currentAuth
|
||||
else -> action.sendAuth
|
||||
}
|
||||
}
|
||||
|
||||
else -> action.sendAuth
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthPasswordChange(action: AddEditSendAction.AuthPasswordChange) {
|
||||
updateCommonContent {
|
||||
it.copy(passwordInput = action.password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthEmailsChange(action: AddEditSendAction.AuthEmailChange) {
|
||||
updateCommonContent { commonContent ->
|
||||
val currentAuth = commonContent.sendAuth as? SendAuth.Email
|
||||
?: return@updateCommonContent commonContent
|
||||
|
||||
val updatedEmails = currentAuth.emails.map { authEmail ->
|
||||
if (authEmail.id == action.authEmail.id) {
|
||||
action.authEmail
|
||||
} else {
|
||||
authEmail
|
||||
}
|
||||
}
|
||||
commonContent.copy(
|
||||
sendAuth = currentAuth.copy(emails = updatedEmails.toImmutableList()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthEmailsAdd() {
|
||||
updateCommonContent { commonContent ->
|
||||
val currentAuth = commonContent.sendAuth as? SendAuth.Email
|
||||
?: return@updateCommonContent commonContent
|
||||
|
||||
commonContent.copy(
|
||||
sendAuth = currentAuth.copy(
|
||||
emails = currentAuth
|
||||
.emails
|
||||
.plus(AuthEmail(value = ""))
|
||||
.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthEmailsRemove(action: AddEditSendAction.AuthEmailRemove) {
|
||||
updateCommonContent { commonContent ->
|
||||
val currentAuth = commonContent.sendAuth as? SendAuth.Email
|
||||
?: return@updateCommonContent commonContent
|
||||
|
||||
val updatedEmails = currentAuth.emails.filterNot { it.id == action.authEmail.id }
|
||||
commonContent.copy(
|
||||
sendAuth = currentAuth.copy(
|
||||
emails = if (updatedEmails.isEmpty()) {
|
||||
persistentListOf(AuthEmail(value = ""))
|
||||
} else {
|
||||
updatedEmails.toImmutableList()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleSaveClick() {
|
||||
onContent { content ->
|
||||
@@ -685,6 +787,10 @@ class AddEditSendViewModel @Inject constructor(
|
||||
.getActivePolicies<PolicyInformation.SendOptions>()
|
||||
.any { it.shouldDisableHideEmail ?: false }
|
||||
|
||||
private val isSendEmailVerificationEnabled: Boolean
|
||||
get() = featureFlagManager
|
||||
.getFeatureFlag(key = FlagKey.SendEmailVerification)
|
||||
|
||||
private inline fun onContent(
|
||||
crossinline block: (AddEditSendState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
@@ -834,6 +940,8 @@ data class AddEditSendState(
|
||||
val expirationDate: ZonedDateTime?,
|
||||
val sendUrl: String?,
|
||||
val hasPassword: Boolean,
|
||||
val isSendEmailVerificationEnabled: Boolean,
|
||||
val sendAuth: SendAuth,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
@@ -1043,6 +1151,31 @@ sealed class AddEditSendAction {
|
||||
*/
|
||||
data class DeletionDateChange(val deletionDate: ZonedDateTime) : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* The user selected an authentication type.
|
||||
*/
|
||||
data class AuthTypeSelect(val sendAuth: SendAuth) : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* The user changed the authentication password.
|
||||
*/
|
||||
data class AuthPasswordChange(val password: String) : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* The user changed the authentication email.
|
||||
*/
|
||||
data class AuthEmailChange(val authEmail: AuthEmail) : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* The user added a new authentication email field.
|
||||
*/
|
||||
data object AuthEmailAdd : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* The user removed an authentication email field.
|
||||
*/
|
||||
data class AuthEmailRemove(val authEmail: AuthEmail) : AddEditSendAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [AddEditSendViewModel] itself might send.
|
||||
*/
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AuthEmail
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
/**
|
||||
* Displays UX for choosing authentication type for a send.
|
||||
*
|
||||
* @param sendAuth The current authentication configuration.
|
||||
* @param onAuthTypeSelect Callback invoked when the authentication type is selected.
|
||||
* @param onPasswordChange Callback invoked when the password value changes
|
||||
* (only relevant for [SendAuth.Password]).
|
||||
* @param onEmailValueChange Callback invoked when an email value changes
|
||||
* (only relevant for [SendAuth.Email]).
|
||||
* @param onAddNewEmailClick Callback invoked when adding a new email.
|
||||
* @param onRemoveEmailClick Callback invoked when removing an email.
|
||||
* @param password The current password value (only relevant for [SendAuth.Password]).
|
||||
* @param isEnabled Whether the chooser is enabled.
|
||||
* @param modifier Modifier for the composable.
|
||||
*/
|
||||
@Composable
|
||||
fun AddEditSendAuthTypeChooser(
|
||||
sendAuth: SendAuth,
|
||||
onAuthTypeSelect: (SendAuth) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onEmailValueChange: (AuthEmail) -> Unit,
|
||||
onAddNewEmailClick: () -> Unit,
|
||||
onRemoveEmailClick: (AuthEmail) -> Unit,
|
||||
password: String,
|
||||
isEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// Map option texts to their corresponding SendAuth factory functions
|
||||
val textToNoneAuth = stringResource(id = BitwardenString.anyone_with_the_link)
|
||||
val textToEmailAuth = stringResource(id = BitwardenString.specific_people)
|
||||
val textToPasswordAuth = stringResource(id = BitwardenString.anyone_with_password)
|
||||
|
||||
val options = listOf(
|
||||
textToNoneAuth,
|
||||
textToEmailAuth,
|
||||
textToPasswordAuth,
|
||||
)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = BitwardenString.who_can_view),
|
||||
isEnabled = isEnabled,
|
||||
options = options.toPersistentList(),
|
||||
selectedOption = sendAuth.text(),
|
||||
onOptionSelected = { selected ->
|
||||
val newAuth = when (selected) {
|
||||
textToNoneAuth -> SendAuth.None
|
||||
textToEmailAuth -> SendAuth.Email()
|
||||
textToPasswordAuth -> SendAuth.Password
|
||||
else -> SendAuth.None // fallback
|
||||
}
|
||||
onAuthTypeSelect(newAuth)
|
||||
},
|
||||
supportingText = sendAuth.supportingText?.let { it() },
|
||||
insets = PaddingValues(top = 6.dp, bottom = 4.dp),
|
||||
cardStyle = if (sendAuth is SendAuth.None) {
|
||||
CardStyle.Full
|
||||
} else {
|
||||
CardStyle.Top()
|
||||
},
|
||||
)
|
||||
|
||||
when (sendAuth) {
|
||||
is SendAuth.Email -> {
|
||||
SpecificPeopleEmailContent(
|
||||
emails = sendAuth.emails,
|
||||
onEmailValueChange = onEmailValueChange,
|
||||
onAddNewEmailClick = onAddNewEmailClick,
|
||||
onRemoveEmailClick = onRemoveEmailClick,
|
||||
)
|
||||
}
|
||||
|
||||
is SendAuth.Password -> {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.password),
|
||||
value = password,
|
||||
onValueChange = onPasswordChange,
|
||||
cardStyle = CardStyle.Bottom,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
is SendAuth.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SpecificPeopleEmailContent(
|
||||
emails: ImmutableList<AuthEmail>,
|
||||
onEmailValueChange: (AuthEmail) -> Unit,
|
||||
onAddNewEmailClick: () -> Unit,
|
||||
onRemoveEmailClick: (AuthEmail) -> Unit,
|
||||
) {
|
||||
emails.forEachIndexed { index, authEmail ->
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.email),
|
||||
value = authEmail.value,
|
||||
onValueChange = { onEmailValueChange(authEmail.copy(value = it)) },
|
||||
singleLine = false,
|
||||
keyboardType = KeyboardType.Email,
|
||||
actions = {
|
||||
if (index > 0 || authEmail.value.isNotEmpty()) {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_delete,
|
||||
contentDescription = stringResource(id = BitwardenString.delete),
|
||||
contentColor = BitwardenTheme.colorScheme.status.error,
|
||||
onClick = {
|
||||
onRemoveEmailClick(authEmail)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
textFieldTestTag = "SendEmailEntry",
|
||||
cardStyle = CardStyle.Middle(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = BitwardenString.add_email),
|
||||
onClick = onAddNewEmailClick,
|
||||
leadingIcon = painterResource(id = BitwardenDrawable.ic_plus_small),
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
innerPadding = PaddingValues(all = 16.dp),
|
||||
cornerSize = 0.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(tag = "AddEditSendAddEmailButton")
|
||||
.cardStyle(cardStyle = CardStyle.Bottom, paddingVertical = 0.dp),
|
||||
)
|
||||
}
|
||||
+24
@@ -3,6 +3,8 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addedit.handlers
|
||||
import com.bitwarden.ui.platform.model.FileData
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendAction
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendViewModel
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AuthEmail
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,11 @@ data class AddEditSendHandlers(
|
||||
val onDeleteClick: () -> Unit,
|
||||
val onOpenPasswordGeneratorClick: () -> Unit,
|
||||
val onPasswordCopyClick: (String) -> Unit,
|
||||
val onAuthTypeSelect: (SendAuth) -> Unit,
|
||||
val onAuthPasswordChange: (String) -> Unit,
|
||||
val onEmailValueChange: (AuthEmail) -> Unit,
|
||||
val onAddNewEmailClick: () -> Unit,
|
||||
val onEmailsRemoveClick: (AuthEmail) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@@ -71,6 +78,23 @@ data class AddEditSendHandlers(
|
||||
),
|
||||
)
|
||||
},
|
||||
onAuthTypeSelect = {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthTypeSelect(it))
|
||||
},
|
||||
onAuthPasswordChange = {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthPasswordChange(it))
|
||||
},
|
||||
onAddNewEmailClick = {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthEmailAdd)
|
||||
},
|
||||
onEmailValueChange = { authEmail ->
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailChange(authEmail = authEmail),
|
||||
)
|
||||
},
|
||||
onEmailsRemoveClick = {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthEmailRemove(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Represents an authentication email with a unique identifier.
|
||||
*
|
||||
* @property id A unique identifier for this email entry.
|
||||
* @property value The email address value.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AuthEmail(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val value: String,
|
||||
) : Parcelable
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Sealed class representing the authentication types for a send.
|
||||
*/
|
||||
sealed class SendAuth : Parcelable {
|
||||
/**
|
||||
* The display text for the authentication type.
|
||||
*/
|
||||
abstract val text: Text
|
||||
|
||||
/**
|
||||
* The display text for the supporting component.
|
||||
*/
|
||||
abstract val supportingText: Text?
|
||||
|
||||
/**
|
||||
* Anyone with the link can view the send.
|
||||
*/
|
||||
@Parcelize
|
||||
data object None : SendAuth() {
|
||||
override val text: Text
|
||||
get() = BitwardenString.anyone_with_the_link.asText()
|
||||
|
||||
override val supportingText: Text
|
||||
get() = BitwardenString.anyone_with_link_can_view_send.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific people who verify their email can view the send.
|
||||
*
|
||||
* @property emails The list of email addresses that can view the send.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Email(
|
||||
val emails: ImmutableList<AuthEmail> = persistentListOf(AuthEmail(value = "")),
|
||||
) : SendAuth() {
|
||||
override val text: Text
|
||||
get() = BitwardenString.specific_people.asText()
|
||||
|
||||
override val supportingText: Text
|
||||
get() = BitwardenString
|
||||
.specific_people_verification_info.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Anyone with the password set by the user can view the send.
|
||||
* Note: The password value is stored separately in the state's `passwordInput` field.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Password : SendAuth() {
|
||||
override val text: Text
|
||||
get() = BitwardenString.anyone_with_password.asText()
|
||||
|
||||
override val supportingText: Text?
|
||||
get() = null // Not used, password field shown instead
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -7,6 +7,7 @@ import com.bitwarden.send.SendType
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.SendAuth
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@@ -21,7 +22,10 @@ fun AddEditSendState.ViewState.Content.toSendView(
|
||||
name = common.name,
|
||||
notes = common.noteInput.orNullIfBlank(),
|
||||
key = common.originalSendView?.key,
|
||||
newPassword = common.passwordInput.orNullIfBlank(),
|
||||
newPassword = common
|
||||
.passwordInput
|
||||
.takeIf { common.sendAuth is SendAuth.Password }
|
||||
.orNullIfBlank(),
|
||||
hasPassword = false,
|
||||
type = selectedType.toSendType(),
|
||||
file = toSendFileView(),
|
||||
@@ -37,8 +41,16 @@ fun AddEditSendState.ViewState.Content.toSendView(
|
||||
// we just update it to match the deletion date.
|
||||
common.deletionDate.toInstant()
|
||||
},
|
||||
emails = emptyList(),
|
||||
authType = AuthType.NONE,
|
||||
emails = (common.sendAuth as? SendAuth.Email)
|
||||
?.emails
|
||||
?.map { it.value }
|
||||
?.filter { it.isNotBlank() }
|
||||
.orEmpty(),
|
||||
authType = when (common.sendAuth) {
|
||||
is SendAuth.Password -> AuthType.PASSWORD
|
||||
is SendAuth.Email -> AuthType.EMAIL
|
||||
is SendAuth.None -> AuthType.NONE
|
||||
},
|
||||
)
|
||||
|
||||
private fun AddEditSendState.ViewState.Content.SendType.toSendType(): SendType =
|
||||
|
||||
+15
@@ -3,7 +3,10 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addedit.util
|
||||
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.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.ZonedDateTime
|
||||
|
||||
@@ -14,6 +17,7 @@ fun SendView.toViewState(
|
||||
clock: Clock,
|
||||
baseWebSendUrl: String,
|
||||
isHideEmailAddressEnabled: Boolean,
|
||||
isSendEmailVerificationEnabled: Boolean,
|
||||
): AddEditSendState.ViewState.Content =
|
||||
AddEditSendState.ViewState.Content(
|
||||
common = AddEditSendState.ViewState.Content.Common(
|
||||
@@ -32,6 +36,17 @@ fun SendView.toViewState(
|
||||
sendUrl = this.toSendUrl(baseWebSendUrl),
|
||||
hasPassword = this.hasPassword,
|
||||
isHideEmailAddressEnabled = isHideEmailAddressEnabled,
|
||||
isSendEmailVerificationEnabled = isSendEmailVerificationEnabled,
|
||||
sendAuth = when {
|
||||
hasPassword -> SendAuth.Password
|
||||
emails.isNotEmpty() -> {
|
||||
SendAuth.Email(
|
||||
emails = this.emails.map { AuthEmail(value = it) }.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> SendAuth.None
|
||||
},
|
||||
),
|
||||
selectedType = when (type) {
|
||||
SendType.TEXT -> {
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ fun createMockSendView(
|
||||
deletionDate = deletionDate,
|
||||
expirationDate = expirationDate,
|
||||
emails = emptyList(),
|
||||
authType = AuthType.NONE,
|
||||
authType = AuthType.PASSWORD,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+461
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
@@ -32,12 +34,15 @@ import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
|
||||
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.model.SendItemType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -956,6 +961,460 @@ class AddEditSendScreenTest : BitwardenComposeTest() {
|
||||
.onNodeWithText(text)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
//region Authentication UI Tests
|
||||
|
||||
@Test
|
||||
fun `auth type chooser should be displayed when feature flag is enabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Who can view")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth type chooser should not be displayed when feature flag is disabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting EMAIL auth type should display email fields`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Click to expand dropdown
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// Select "Specific people"
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
match {
|
||||
it is AddEditSendAction.AuthTypeSelect &&
|
||||
it.sendAuth is SendAuth.Email
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting PASSWORD auth type should display password field`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
hasPassword = false,
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Click to expand dropdown
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// Select "Anyone with a password set by you"
|
||||
composeTestRule
|
||||
.onNodeWithText("Anyone with a password set by you")
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthTypeSelect(
|
||||
SendAuth.Password,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `typing in auth password field should send AuthPasswordChange action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
hasPassword = false,
|
||||
isSendEmailVerificationEnabled = true,
|
||||
passwordInput = "",
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Switch to PASSWORD auth type first
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Anyone with a password set by you")
|
||||
.performClick()
|
||||
|
||||
// Update state to show password field with authType set to PASSWORD
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
passwordInput = "",
|
||||
sendAuth = SendAuth.Password,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Password")
|
||||
.performTextInput("testpassword")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthPasswordChange("testpassword"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `typing in email field should send AuthEmailChange action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Switch to EMAIL auth type first
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.performClick()
|
||||
|
||||
// Update state to show email field with authType set to EMAIL
|
||||
val testEmail = AuthEmail(id = "test-id", value = "")
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(emails = persistentListOf(testEmail)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendEmailEntry")
|
||||
.performTextInput("test@example.com")
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailChange(
|
||||
AuthEmail(
|
||||
value = "test@example.com",
|
||||
id = "test-id",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking add email button should send AuthEmailAdd action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
hasPassword = false,
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(
|
||||
AuthEmail(
|
||||
id = "id1",
|
||||
value = "test@example.com",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add email")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(AddEditSendAction.AuthEmailAdd)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking delete email button should send AuthEmailRemove action`() {
|
||||
val email1 = AuthEmail(id = "id1", value = "test1@example.com")
|
||||
val email2 = AuthEmail(id = "id2", value = "test2@example.com")
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
hasPassword = false,
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(emails = persistentListOf(email1, email2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Switch to EMAIL auth type
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.performClick()
|
||||
|
||||
// Update state to show email fields with authType set to EMAIL
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1, email2),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Delete")[0]
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailRemove(
|
||||
AuthEmail(
|
||||
value = "test1@example.com",
|
||||
id = "id1",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth type chooser should show EMAIL option for premium users`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isPremium = true,
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Click to expand dropdown
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// "Specific people" should be visible
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `legacy password field should be hidden when auth chooser is displayed`() {
|
||||
// Expand options section
|
||||
composeTestRule
|
||||
.onNodeWithText("Additional options")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// With feature flag OFF, legacy password field should be visible
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("New password")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
|
||||
// With feature flag ON, legacy password field should be hidden
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("New password")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `email fields should display provided emails from state`() {
|
||||
val email1 = AuthEmail(id = "id1", value = "user1@example.com")
|
||||
val email2 = AuthEmail(id = "id2", value = "user2@example.com")
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(emails = persistentListOf(email1, email2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Switch to EMAIL auth type
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.performClick()
|
||||
|
||||
// Update state to show emails with authType set to EMAIL
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(emails = persistentListOf(email1, email2)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Verify both email fields are present
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Email")
|
||||
.assertCountEquals(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `supporting text should display for EMAIL auth type`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Click to select EMAIL
|
||||
composeTestRule
|
||||
.onNodeWithTag("SendAuthTypeChooser")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Specific people")
|
||||
.performClick()
|
||||
|
||||
// Update state with authType set to EMAIL
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
isSendEmailVerificationEnabled = true,
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(
|
||||
AuthEmail(
|
||||
id = "id1",
|
||||
value = "",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
"After sharing this Send link, individuals will need to verify " +
|
||||
"their email with a code to view this Send",
|
||||
)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
//endregion Authentication UI Tests
|
||||
}
|
||||
|
||||
private val DEFAULT_COMMON_STATE = AddEditSendState.ViewState.Content.Common(
|
||||
@@ -971,6 +1430,8 @@ private val DEFAULT_COMMON_STATE = AddEditSendState.ViewState.Content.Common(
|
||||
sendUrl = null,
|
||||
hasPassword = true,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddEditSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
+231
-2
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addedit
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.repository.model.DataState
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
@@ -20,6 +21,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
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
|
||||
@@ -37,6 +39,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
|
||||
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
|
||||
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.toSendView
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
|
||||
@@ -50,6 +54,7 @@ import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -79,7 +84,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
private val authRepository = mockk<AuthRepository> {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val generatorRepository = mockk<GeneratorRepository> (relaxed = true) {
|
||||
private val generatorRepository = mockk<GeneratorRepository>(relaxed = true) {
|
||||
every { generatorResultFlow } returns mutableGeneratorResultFlow
|
||||
}
|
||||
private val environmentRepository: EnvironmentRepository = mockk {
|
||||
@@ -104,6 +109,16 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
every { sendSnackbarData(data = any(), relay = any()) } just runs
|
||||
}
|
||||
|
||||
private val mutableSendEmailVerificationFeatureFlagFlow = MutableStateFlow(false)
|
||||
private val featureFlagManager: FeatureFlagManager = mockk {
|
||||
every {
|
||||
getFeatureFlagFlow(FlagKey.SendEmailVerification)
|
||||
} returns mutableSendEmailVerificationFeatureFlagFlow
|
||||
every {
|
||||
getFeatureFlag(FlagKey.SendEmailVerification)
|
||||
} answers { mutableSendEmailVerificationFeatureFlagFlow.value }
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(
|
||||
@@ -304,6 +319,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns viewState
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
@@ -348,6 +364,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns viewState
|
||||
every { viewState.toSendView(clock) } returns mockSendView
|
||||
@@ -532,6 +549,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns viewState
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
@@ -594,6 +612,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
@@ -640,6 +659,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
@@ -715,6 +735,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns DEFAULT_VIEW_STATE
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
@@ -783,6 +804,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
clock = clock,
|
||||
baseWebSendUrl = DEFAULT_ENVIRONMENT_URL,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
} returns viewState
|
||||
mutableSendDataStateFlow.value = DataState.Loaded(mockSendView)
|
||||
@@ -966,7 +988,10 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
fun `PasswordChange should update note input`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(passwordInput = "input"),
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
passwordInput = "input",
|
||||
sendAuth = SendAuth.Password,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
@@ -1059,6 +1084,207 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
//region Authentication Tests
|
||||
|
||||
@Test
|
||||
fun `Changing AuthTypeSelect should not clear emails and password`() = runTest {
|
||||
val email1 = AuthEmail(id = "id1", value = "test@example.com")
|
||||
val initialCommonState = DEFAULT_COMMON_STATE.copy(
|
||||
passwordInput = "oldpassword",
|
||||
sendAuth = SendAuth.Password,
|
||||
)
|
||||
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = initialCommonState,
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
val expectedNoneViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = initialCommonState.copy(
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthTypeSelect(
|
||||
SendAuth.None,
|
||||
),
|
||||
)
|
||||
assertEquals(initialState.copy(viewState = expectedNoneViewState), awaitItem())
|
||||
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthTypeSelect(
|
||||
SendAuth.Email(),
|
||||
),
|
||||
)
|
||||
|
||||
// Check the structure rather than exact equality due to random UUIDs
|
||||
val emailState = awaitItem()
|
||||
val sendAuth = (emailState.viewState as AddEditSendState.ViewState.Content)
|
||||
.common
|
||||
.sendAuth as SendAuth.Email
|
||||
assertEquals(1, sendAuth.emails.size)
|
||||
assertEquals("", sendAuth.emails[0].value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthPasswordChange should update passwordInput`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(passwordInput = "newpassword"),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
viewModel.trySendAction(AddEditSendAction.AuthPasswordChange("newpassword"))
|
||||
assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthEmailChange should update email at specified index`() = runTest {
|
||||
val email1 = AuthEmail(id = "id1", value = "test1@example.com")
|
||||
val email2 = AuthEmail(id = "id2", value = "test2@example.com")
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1, email2),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(
|
||||
email1,
|
||||
email2.copy(value = "updated@example.com"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailChange(
|
||||
AuthEmail(
|
||||
value = "updated@example.com",
|
||||
id = "id2",
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(initialState.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthEmailAdd should add empty email to list`() = runTest {
|
||||
val email1 = AuthEmail(id = "id1", value = "test@example.com")
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(AddEditSendAction.AuthEmailAdd)
|
||||
val newState = awaitItem()
|
||||
val actualEmails = (newState.viewState as AddEditSendState.ViewState.Content)
|
||||
.common
|
||||
.sendAuth as SendAuth.Email
|
||||
// Verify we have 2 emails, the first unchanged and the second empty
|
||||
assertEquals(2, actualEmails.emails.size)
|
||||
assertEquals(email1, actualEmails.emails[0])
|
||||
assertEquals("", actualEmails.emails[1].value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthEmailRemove should remove email at specified index`() = runTest {
|
||||
val email1 = AuthEmail(id = "id1", value = "test1@example.com")
|
||||
val email2 = AuthEmail(id = "id2", value = "test2@example.com")
|
||||
val email3 = AuthEmail(id = "id3", value = "test3@example.com")
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1, email2, email3),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
val expectedViewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1, email3),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailRemove(
|
||||
AuthEmail(
|
||||
value = "test@example.com",
|
||||
id = "id2",
|
||||
),
|
||||
),
|
||||
)
|
||||
assertEquals(initialState.copy(viewState = expectedViewState), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AuthEmailRemove with last email should result in single empty AuthEmail`() = runTest {
|
||||
val email1 = AuthEmail(id = "id1", value = "test@example.com")
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
viewState = DEFAULT_VIEW_STATE.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(email1),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val viewModel = createViewModel(initialState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
viewModel.trySendAction(
|
||||
AddEditSendAction.AuthEmailRemove(
|
||||
AuthEmail(
|
||||
value = "test@example.com",
|
||||
id = "id1",
|
||||
),
|
||||
),
|
||||
)
|
||||
val newState = awaitItem()
|
||||
val actualEmails = (newState.viewState as AddEditSendState.ViewState.Content)
|
||||
.common
|
||||
.sendAuth as SendAuth.Email
|
||||
// Verify we have 1 empty email after removing the last one
|
||||
assertEquals(1, actualEmails.emails.size)
|
||||
assertEquals("", actualEmails.emails[0].value)
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Authentication Tests
|
||||
|
||||
private fun createViewModel(
|
||||
state: AddEditSendState? = null,
|
||||
addEditSendType: AddEditSendType = AddEditSendType.AddItem,
|
||||
@@ -1082,6 +1308,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
generatorRepository = generatorRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1098,6 +1325,8 @@ private val DEFAULT_COMMON_STATE = AddEditSendState.ViewState.Content.Common(
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddEditSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
+57
@@ -1,9 +1,13 @@
|
||||
package com.x8bit.bitwarden.ui.tools.feature.send.addedit.util
|
||||
|
||||
import com.bitwarden.send.AuthType
|
||||
import com.bitwarden.send.SendType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFileView
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
@@ -83,6 +87,57 @@ class AddEditSendStateExtensionsTest {
|
||||
|
||||
assertEquals(sendView, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendView should create an appropriate SendView with Auth type NONE`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT).copy(
|
||||
id = null,
|
||||
accessId = null,
|
||||
key = null,
|
||||
accessCount = 0U,
|
||||
hasPassword = false,
|
||||
newPassword = null,
|
||||
authType = AuthType.NONE,
|
||||
)
|
||||
|
||||
val result = DEFAULT_VIEW_STATE
|
||||
.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
passwordInput = "",
|
||||
sendAuth = SendAuth.None,
|
||||
),
|
||||
)
|
||||
.toSendView(FIXED_CLOCK)
|
||||
|
||||
assertEquals(sendView, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toSendView should create an appropriate SendView with Auth type EMAIL`() {
|
||||
val sendView = createMockSendView(number = 1, type = SendType.TEXT).copy(
|
||||
id = null,
|
||||
accessId = null,
|
||||
key = null,
|
||||
accessCount = 0U,
|
||||
hasPassword = false,
|
||||
newPassword = null,
|
||||
emails = listOf("email@email.com"),
|
||||
authType = AuthType.EMAIL,
|
||||
)
|
||||
|
||||
val result = DEFAULT_VIEW_STATE
|
||||
.copy(
|
||||
common = DEFAULT_COMMON_STATE.copy(
|
||||
passwordInput = "",
|
||||
sendAuth = SendAuth.Email(
|
||||
emails = persistentListOf(AuthEmail(id = "id1", value = "email@email.com")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toSendView(FIXED_CLOCK)
|
||||
|
||||
assertEquals(sendView, result)
|
||||
}
|
||||
}
|
||||
|
||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||
@@ -103,6 +158,8 @@ private val DEFAULT_COMMON_STATE = AddEditSendState.ViewState.Content.Common(
|
||||
sendUrl = null,
|
||||
hasPassword = false,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
sendAuth = SendAuth.Password,
|
||||
)
|
||||
|
||||
private val DEFAULT_SELECTED_TYPE_STATE = AddEditSendState.ViewState.Content.SendType.Text(
|
||||
|
||||
+13
-2
@@ -3,6 +3,7 @@ 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.SendAuth
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
@@ -20,11 +21,15 @@ class SendViewExtensionsTest {
|
||||
clock = FIXED_CLOCK,
|
||||
baseWebSendUrl = "www.test.com/",
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(originalSendView = sendView),
|
||||
common = DEFAULT_COMMON.copy(
|
||||
originalSendView = sendView,
|
||||
sendAuth = SendAuth.Password,
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
@@ -38,11 +43,15 @@ class SendViewExtensionsTest {
|
||||
clock = FIXED_CLOCK,
|
||||
baseWebSendUrl = "www.test.com/",
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
common = DEFAULT_COMMON.copy(originalSendView = sendView),
|
||||
common = DEFAULT_COMMON.copy(
|
||||
originalSendView = sendView,
|
||||
sendAuth = SendAuth.Password,
|
||||
),
|
||||
selectedType = DEFAULT_TEXT_TYPE,
|
||||
),
|
||||
result,
|
||||
@@ -75,6 +84,8 @@ private val DEFAULT_COMMON: AddEditSendState.ViewState.Content.Common =
|
||||
sendUrl = "www.test.com/mockAccessId-1/mockKey-1",
|
||||
hasPassword = true,
|
||||
isHideEmailAddressEnabled = true,
|
||||
isSendEmailVerificationEnabled = false,
|
||||
sendAuth = SendAuth.None,
|
||||
)
|
||||
|
||||
private val DEFAULT_TEXT_TYPE: AddEditSendState.ViewState.Content.SendType.Text =
|
||||
|
||||
@@ -448,6 +448,13 @@ Scanning will happen automatically.</string>
|
||||
<string name="deletion_date">Deletion date</string>
|
||||
<string name="deletion_time">Deletion time</string>
|
||||
<string name="deletion_date_info">The Send will be permanently deleted on the specified date and time.</string>
|
||||
<string name="who_can_view">Who can view</string>
|
||||
<string name="anyone_with_the_link">Anyone with the link</string>
|
||||
<string name="anyone_with_link_can_view_send">Anyone with this link can view this Send</string>
|
||||
<string name="specific_people">Specific people</string>
|
||||
<string name="specific_people_verification_info">After sharing this Send link, individuals will need to verify their email with a code to view this Send</string>
|
||||
<string name="anyone_with_password">Anyone with a password set by you</string>
|
||||
<string name="add_email">Add email</string>
|
||||
<string name="pending_delete">Pending deletion</string>
|
||||
<string name="expired">Expired</string>
|
||||
<string name="expires_at">Expires at %s</string>
|
||||
|
||||
Reference in New Issue
Block a user