mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Add ViewState VaultAddItemViewModel (#359)
This commit is contained in:
parent
d5a1592ef0
commit
1219aa20fd
11 changed files with 539 additions and 346 deletions
|
@ -0,0 +1,104 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* The top level content UI state for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Composable
|
||||
fun AddEditItemContent(
|
||||
viewState: VaultAddItemState.ViewState.Content,
|
||||
shouldShowTypeSelector: Boolean,
|
||||
onTypeOptionClicked: (VaultAddItemState.ItemTypeOption) -> Unit,
|
||||
loginItemTypeHandlers: VaultAddLoginItemTypeHandlers,
|
||||
secureNotesTypeHandlers: VaultAddSecureNotesItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
item {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.item_information),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
if (shouldShowTypeSelector) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TypeOptionsItem(
|
||||
content = viewState,
|
||||
onTypeOptionClicked = onTypeOptionClicked,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (viewState) {
|
||||
is VaultAddItemState.ViewState.Content.Login -> {
|
||||
addEditLoginItems(
|
||||
state = viewState,
|
||||
loginItemTypeHandlers = loginItemTypeHandlers,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddItemState.ViewState.Content.Card -> {
|
||||
// TODO(BIT-507): Create UI for card-type item creation
|
||||
}
|
||||
|
||||
is VaultAddItemState.ViewState.Content.Identity -> {
|
||||
// TODO(BIT-667): Create UI for identity-type item creation
|
||||
}
|
||||
|
||||
is VaultAddItemState.ViewState.Content.SecureNotes -> {
|
||||
addEditSecureNotesItems(
|
||||
state = viewState,
|
||||
secureNotesTypeHandlers = secureNotesTypeHandlers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TypeOptionsItem(
|
||||
content: VaultAddItemState.ViewState.Content,
|
||||
onTypeOptionClicked: (VaultAddItemState.ItemTypeOption) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val possibleMainStates = VaultAddItemState.ItemTypeOption.entries.toList()
|
||||
val optionsWithStrings = possibleMainStates.associateWith { stringResource(id = it.labelRes) }
|
||||
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = R.string.type),
|
||||
options = optionsWithStrings.values.toImmutableList(),
|
||||
selectedOption = stringResource(id = content.displayStringResId),
|
||||
onOptionSelected = { selectedOption ->
|
||||
val selectedOptionId = optionsWithStrings
|
||||
.entries
|
||||
.first { it.value == selectedOption }
|
||||
.key
|
||||
onTypeOptionClicked(selectedOptionId)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
|
@ -31,7 +31,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun LazyListScope.addEditLoginItems(
|
||||
state: VaultAddItemState.ItemType.Login,
|
||||
state: VaultAddItemState.ViewState.Content.Login,
|
||||
loginItemTypeHandlers: VaultAddLoginItemTypeHandlers,
|
||||
) {
|
||||
item {
|
||||
|
|
|
@ -26,7 +26,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun LazyListScope.addEditSecureNotesItems(
|
||||
state: VaultAddItemState.ItemType.SecureNotes,
|
||||
state: VaultAddItemState.ViewState.Content.SecureNotes,
|
||||
secureNotesTypeHandlers: VaultAddSecureNotesItemTypeHandlers,
|
||||
) {
|
||||
item {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* The top level error UI state for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Composable
|
||||
fun VaultAddEditError(
|
||||
viewState: VaultAddItemState.ViewState.Error,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = viewState.message(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
/**
|
||||
* The top level loading UI state for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Composable
|
||||
fun VaultAddEditItemLoading(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
|
@ -20,7 +15,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
|
@ -28,14 +22,11 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMultiSelectButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Top level composable for the vault add item screen.
|
||||
|
@ -68,25 +59,12 @@ fun VaultAddItemScreen(
|
|||
VaultAddSecureNotesItemTypeHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
|
||||
when (val dialogState = state.dialog) {
|
||||
is VaultAddItemState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(dialogState.label),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddItemState.DialogState.Error -> BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddItemAction.DismissDialog) }
|
||||
},
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
VaultAddEditItemDialogs(
|
||||
dialogState = state.dialog,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddItemAction.DismissDialog) }
|
||||
},
|
||||
)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
|
@ -113,88 +91,63 @@ fun VaultAddItemScreen(
|
|||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = R.string.item_information),
|
||||
when (val viewState = state.viewState) {
|
||||
is VaultAddItemState.ViewState.Content -> {
|
||||
AddEditItemContent(
|
||||
viewState = viewState,
|
||||
shouldShowTypeSelector = state.shouldShowTypeSelector,
|
||||
onTypeOptionClicked = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddItemAction.TypeOptionSelect(it)) }
|
||||
},
|
||||
loginItemTypeHandlers = loginItemTypeHandlers,
|
||||
secureNotesTypeHandlers = secureNotesTypeHandlers,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
.imePadding()
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
if (state.shouldShowTypeSelector) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TypeOptionsItem(
|
||||
selectedType = state.selectedType,
|
||||
onTypeOptionClicked = remember(viewModel) {
|
||||
{ typeOption: VaultAddItemState.ItemTypeOption ->
|
||||
viewModel.trySendAction(
|
||||
VaultAddItemAction.TypeOptionSelect(typeOption),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddItemState.ViewState.Error -> {
|
||||
VaultAddEditError(
|
||||
viewState = viewState,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
when (val selectedType = state.selectedType) {
|
||||
is VaultAddItemState.ItemType.Login -> {
|
||||
addEditLoginItems(
|
||||
state = selectedType,
|
||||
loginItemTypeHandlers = loginItemTypeHandlers,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddItemState.ItemType.Card -> {
|
||||
// TODO(BIT-507): Create UI for card-type item creation
|
||||
}
|
||||
|
||||
is VaultAddItemState.ItemType.Identity -> {
|
||||
// TODO(BIT-667): Create UI for identity-type item creation
|
||||
}
|
||||
|
||||
is VaultAddItemState.ItemType.SecureNotes -> {
|
||||
addEditSecureNotesItems(
|
||||
state = selectedType,
|
||||
secureNotesTypeHandlers = secureNotesTypeHandlers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
VaultAddItemState.ViewState.Loading -> {
|
||||
VaultAddEditItemLoading(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TypeOptionsItem(
|
||||
selectedType: VaultAddItemState.ItemType,
|
||||
onTypeOptionClicked: (VaultAddItemState.ItemTypeOption) -> Unit,
|
||||
modifier: Modifier,
|
||||
private fun VaultAddEditItemDialogs(
|
||||
dialogState: VaultAddItemState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val possibleMainStates = VaultAddItemState.ItemTypeOption.values().toList()
|
||||
when (dialogState) {
|
||||
is VaultAddItemState.DialogState.Loading -> {
|
||||
BitwardenLoadingDialog(
|
||||
visibilityState = LoadingDialogState.Shown(dialogState.label),
|
||||
)
|
||||
}
|
||||
|
||||
val optionsWithStrings =
|
||||
possibleMainStates.associateBy({ it }, { stringResource(id = it.labelRes) })
|
||||
is VaultAddItemState.DialogState.Error -> BitwardenBasicDialog(
|
||||
visibilityState = BasicDialogState.Shown(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = dialogState.message,
|
||||
),
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
BitwardenMultiSelectButton(
|
||||
label = stringResource(id = R.string.type),
|
||||
options = optionsWithStrings.values.toImmutableList(),
|
||||
selectedOption = stringResource(id = selectedType.displayStringResId),
|
||||
onOptionSelected = { selectedOption ->
|
||||
val selectedOptionId =
|
||||
optionsWithStrings.entries.first { it.value == selectedOption }.key
|
||||
onTypeOptionClicked(selectedOptionId)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.additem
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
|
@ -37,11 +38,17 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<VaultAddItemState, VaultAddItemEvent, VaultAddItemAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: VaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditItemArgs(savedStateHandle).vaultAddEditType,
|
||||
selectedType = VaultAddItemState.ItemType.Login(),
|
||||
dialog = null,
|
||||
),
|
||||
?: run {
|
||||
val vaultAddEditType = VaultAddEditItemArgs(savedStateHandle).vaultAddEditType
|
||||
VaultAddItemState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
viewState = when (vaultAddEditType) {
|
||||
VaultAddEditType.AddItem -> VaultAddItemState.ViewState.Content.Login()
|
||||
is VaultAddEditType.EditItem -> VaultAddItemState.ViewState.Loading
|
||||
},
|
||||
dialog = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
//region Initialization and Overrides
|
||||
|
@ -86,8 +93,8 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
|
||||
//region Top Level Handlers
|
||||
|
||||
private fun handleSaveClick() {
|
||||
if (state.selectedType.name.isBlank()) {
|
||||
private fun handleSaveClick() = onContent { content ->
|
||||
if (content.name.isBlank()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddItemState.DialogState.Error(
|
||||
|
@ -96,7 +103,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
return@onContent
|
||||
}
|
||||
|
||||
mutableStateFlow.update {
|
||||
|
@ -111,7 +118,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
sendAction(
|
||||
action = VaultAddItemAction.Internal.CreateCipherResultReceive(
|
||||
createCipherResult = vaultRepository.createCipher(
|
||||
cipherView = stateFlow.value.selectedType.toCipherView(),
|
||||
cipherView = content.toCipherView(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -144,35 +151,19 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleSwitchToAddLoginItem() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedType = VaultAddItemState.ItemType.Login(),
|
||||
)
|
||||
}
|
||||
updateContent { VaultAddItemState.ViewState.Content.Login() }
|
||||
}
|
||||
|
||||
private fun handleSwitchToAddSecureNotesItem() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedType = VaultAddItemState.ItemType.SecureNotes(),
|
||||
)
|
||||
}
|
||||
updateContent { VaultAddItemState.ViewState.Content.SecureNotes() }
|
||||
}
|
||||
|
||||
private fun handleSwitchToAddCardItem() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedType = VaultAddItemState.ItemType.Card(),
|
||||
)
|
||||
}
|
||||
updateContent { VaultAddItemState.ViewState.Content.Card() }
|
||||
}
|
||||
|
||||
private fun handleSwitchToAddIdentityItem() {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
selectedType = VaultAddItemState.ItemType.Identity(),
|
||||
)
|
||||
}
|
||||
updateContent { VaultAddItemState.ViewState.Content.Identity() }
|
||||
}
|
||||
|
||||
//endregion Type Option Handlers
|
||||
|
@ -257,7 +248,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginNameTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.NameTextChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(name = action.name)
|
||||
}
|
||||
}
|
||||
|
@ -265,7 +256,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginUsernameTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.UsernameTextChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(username = action.username)
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +264,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginPasswordTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.PasswordTextChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(password = action.password)
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +272,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginURITextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.UriTextChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(uri = action.uri)
|
||||
}
|
||||
}
|
||||
|
@ -289,7 +280,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginFolderTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.FolderChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(folderName = action.folder)
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +288,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginToggleFavorite(
|
||||
action: VaultAddItemAction.ItemType.LoginType.ToggleFavorite,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(favorite = action.isFavorite)
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +296,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginToggleMasterPasswordReprompt(
|
||||
action: VaultAddItemAction.ItemType.LoginType.ToggleMasterPasswordReprompt,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(masterPasswordReprompt = action.isMasterPasswordReprompt)
|
||||
}
|
||||
}
|
||||
|
@ -313,7 +304,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginNotesTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.NotesTextChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(notes = action.notes)
|
||||
}
|
||||
}
|
||||
|
@ -321,7 +312,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleLoginOwnershipTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.LoginType.OwnershipChange,
|
||||
) {
|
||||
updateLoginType { loginType ->
|
||||
updateLoginContent { loginType ->
|
||||
loginType.copy(ownership = action.ownership)
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +442,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteNameTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.NameTextChange,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(name = action.name)
|
||||
}
|
||||
}
|
||||
|
@ -459,7 +450,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteFolderTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.FolderChange,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(folderName = action.folderName)
|
||||
}
|
||||
}
|
||||
|
@ -467,7 +458,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteToggleFavorite(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.ToggleFavorite,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(favorite = action.isFavorite)
|
||||
}
|
||||
}
|
||||
|
@ -475,7 +466,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteToggleMasterPasswordReprompt(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.ToggleMasterPasswordReprompt,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(masterPasswordReprompt = action.isMasterPasswordReprompt)
|
||||
}
|
||||
}
|
||||
|
@ -483,7 +474,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteNotesTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.NotesTextChange,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(notes = action.note)
|
||||
}
|
||||
}
|
||||
|
@ -491,7 +482,7 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
private fun handleSecureNoteOwnershipTextInputChange(
|
||||
action: VaultAddItemAction.ItemType.SecureNotesType.OwnershipChange,
|
||||
) {
|
||||
updateSecureNoteType { secureNoteType ->
|
||||
updateSecureNoteContent { secureNoteType ->
|
||||
secureNoteType.copy(ownership = action.ownership)
|
||||
}
|
||||
}
|
||||
|
@ -546,34 +537,38 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
|
||||
//region Utility Functions
|
||||
|
||||
private inline fun updateLoginType(
|
||||
crossinline block: (VaultAddItemState.ItemType.Login) -> VaultAddItemState.ItemType.Login,
|
||||
private inline fun onContent(
|
||||
crossinline block: (VaultAddItemState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
val currentSelectedType = currentState.selectedType
|
||||
if (currentSelectedType !is VaultAddItemState.ItemType.Login) return@update currentState
|
||||
|
||||
val updatedLogin = block(currentSelectedType)
|
||||
|
||||
currentState.copy(selectedType = updatedLogin)
|
||||
}
|
||||
(state.viewState as? VaultAddItemState.ViewState.Content)?.let(block)
|
||||
}
|
||||
|
||||
private inline fun updateSecureNoteType(
|
||||
private inline fun updateContent(
|
||||
crossinline block: (
|
||||
VaultAddItemState.ItemType.SecureNotes,
|
||||
) -> VaultAddItemState.ItemType.SecureNotes,
|
||||
VaultAddItemState.ViewState.Content,
|
||||
) -> VaultAddItemState.ViewState.Content?,
|
||||
) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
val currentSelectedType = currentState.selectedType
|
||||
if (currentSelectedType !is VaultAddItemState.ItemType.SecureNotes) {
|
||||
return@update currentState
|
||||
}
|
||||
val currentViewState = state.viewState
|
||||
val updatedContent = (currentViewState as? VaultAddItemState.ViewState.Content)
|
||||
?.let(block)
|
||||
?: return
|
||||
mutableStateFlow.update { it.copy(viewState = updatedContent) }
|
||||
}
|
||||
|
||||
val updatedSecureNote = block(currentSelectedType)
|
||||
private inline fun updateLoginContent(
|
||||
crossinline block: (
|
||||
VaultAddItemState.ViewState.Content.Login,
|
||||
) -> VaultAddItemState.ViewState.Content.Login,
|
||||
) {
|
||||
updateContent { (it as? VaultAddItemState.ViewState.Content.Login)?.let(block) }
|
||||
}
|
||||
|
||||
currentState.copy(selectedType = updatedSecureNote)
|
||||
}
|
||||
private inline fun updateSecureNoteContent(
|
||||
crossinline block: (
|
||||
VaultAddItemState.ViewState.Content.SecureNotes,
|
||||
) -> VaultAddItemState.ViewState.Content.SecureNotes,
|
||||
) {
|
||||
updateContent { (it as? VaultAddItemState.ViewState.Content.SecureNotes)?.let(block) }
|
||||
}
|
||||
|
||||
//endregion Utility Functions
|
||||
|
@ -582,14 +577,14 @@ class VaultAddItemViewModel @Inject constructor(
|
|||
/**
|
||||
* Represents the state for adding an item to the vault.
|
||||
*
|
||||
* @property selectedType The type of the item (e.g., Card, Identity, SecureNotes)
|
||||
* that has been selected to be added to the vault.
|
||||
* @property vaultAddEditType Indicates whether the VM is in add or edit mode.
|
||||
* @property viewState indicates what view state the screen is in.
|
||||
* @property dialog the state for the dialogs that can be displayed
|
||||
*/
|
||||
@Parcelize
|
||||
data class VaultAddItemState(
|
||||
val vaultAddEditType: VaultAddEditType,
|
||||
val selectedType: ItemType,
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
) : Parcelable {
|
||||
|
||||
|
@ -626,125 +621,137 @@ data class VaultAddItemState(
|
|||
}
|
||||
|
||||
/**
|
||||
* A sealed class representing the item types that can be selected in the vault,
|
||||
* encapsulating the different configurations and properties each item type has.
|
||||
* Represents the specific view states for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
sealed class ItemType : Parcelable {
|
||||
|
||||
sealed class ViewState : Parcelable {
|
||||
/**
|
||||
* Represents the resource ID for the display string. This is an abstract property
|
||||
* that must be overridden by each subclass to provide the appropriate string resource ID
|
||||
* for display purposes.
|
||||
*/
|
||||
abstract val displayStringResId: Int
|
||||
|
||||
/**
|
||||
* Represents the name for the item type. This is an abstract property
|
||||
* that must be overridden to save the item
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
/**
|
||||
* Represents the login item information.
|
||||
*
|
||||
* @property username The username required for the login item.
|
||||
* @property password The password required for the login item.
|
||||
* @property uri The URI associated with the login item.
|
||||
* @property folderName The folder used for the login item
|
||||
* @property favorite Indicates whether this login item is marked as a favorite.
|
||||
* @property masterPasswordReprompt Indicates if a master password reprompt is required.
|
||||
* @property notes Any additional notes or comments associated with the login item.
|
||||
* @property ownership The ownership email associated with the login item.
|
||||
* @property availableFolders Retrieves a list of available folders.
|
||||
* @property availableOwners Retrieves a list of available owners.
|
||||
* Represents an error state for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
data class Login(
|
||||
override val name: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val uri: String = "",
|
||||
val folderName: Text = DEFAULT_FOLDER,
|
||||
val favorite: Boolean = false,
|
||||
val masterPasswordReprompt: Boolean = false,
|
||||
val notes: String = "",
|
||||
val ownership: String = DEFAULT_OWNERSHIP,
|
||||
// TODO: Update this property to pull available owners from the data layer. (BIT-501)
|
||||
val availableFolders: List<Text> = listOf(
|
||||
"Folder 1".asText(),
|
||||
"Folder 2".asText(),
|
||||
"Folder 3".asText(),
|
||||
),
|
||||
// TODO: Update this property to pull available owners from the data layer. (BIT-501)
|
||||
val availableOwners: List<String> = listOf("a@b.com", "c@d.com"),
|
||||
) : ItemType() {
|
||||
override val displayStringResId: Int
|
||||
get() = ItemTypeOption.LOGIN.labelRes
|
||||
data class Error(
|
||||
val message: Text,
|
||||
) : ViewState()
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_FOLDER: Text = R.string.folder_none.asText()
|
||||
private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com"
|
||||
/**
|
||||
* Loading state for the [VaultAddItemScreen], signifying that the content is being
|
||||
* processed.
|
||||
*/
|
||||
@Parcelize
|
||||
data object Loading : ViewState()
|
||||
|
||||
/**
|
||||
* Represents a loaded content state for the [VaultAddItemScreen].
|
||||
*/
|
||||
@Parcelize
|
||||
sealed class Content : ViewState() {
|
||||
/**
|
||||
* Represents the resource ID for the display string. This is an abstract property
|
||||
* that must be overridden by each subclass to provide the appropriate string resource
|
||||
* ID for display purposes.
|
||||
*/
|
||||
@get:StringRes
|
||||
abstract val displayStringResId: Int
|
||||
|
||||
/**
|
||||
* Represents the name for the item type. This is an abstract property that must be
|
||||
* overridden to save the item
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
/**
|
||||
* Represents the login item information.
|
||||
*
|
||||
* @property username The username required for the login item.
|
||||
* @property password The password required for the login item.
|
||||
* @property uri The URI associated with the login item.
|
||||
* @property folderName The folder used for the login item
|
||||
* @property favorite Indicates whether this login item is marked as a favorite.
|
||||
* @property masterPasswordReprompt Indicates if a master password reprompt is required.
|
||||
* @property notes Any additional notes or comments associated with the login item.
|
||||
* @property ownership The ownership email associated with the login item.
|
||||
* @property availableFolders Retrieves a list of available folders.
|
||||
* @property availableOwners Retrieves a list of available owners.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Login(
|
||||
override val name: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val uri: String = "",
|
||||
val folderName: Text = DEFAULT_FOLDER,
|
||||
val favorite: Boolean = false,
|
||||
val masterPasswordReprompt: Boolean = false,
|
||||
val notes: String = "",
|
||||
val ownership: String = DEFAULT_OWNERSHIP,
|
||||
// TODO: Update this property to get available owners from the data layer (BIT-501)
|
||||
val availableFolders: List<Text> = listOf(
|
||||
"Folder 1".asText(),
|
||||
"Folder 2".asText(),
|
||||
"Folder 3".asText(),
|
||||
),
|
||||
// TODO: Update this property to get available owners from the data layer (BIT-501)
|
||||
val availableOwners: List<String> = listOf("a@b.com", "c@d.com"),
|
||||
) : Content() {
|
||||
override val displayStringResId: Int get() = ItemTypeOption.LOGIN.labelRes
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_FOLDER: Text = R.string.folder_none.asText()
|
||||
private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the `Card` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Card(
|
||||
// TODO create the Card Item (BIT-509)
|
||||
override val name: String = "",
|
||||
) : ItemType() {
|
||||
override val displayStringResId: Int
|
||||
get() = ItemTypeOption.CARD.labelRes
|
||||
}
|
||||
/**
|
||||
* Represents the `Card` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Card(
|
||||
override val name: String = "",
|
||||
) : Content() {
|
||||
override val displayStringResId: Int get() = ItemTypeOption.CARD.labelRes
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the `Identity` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Identity(
|
||||
// TODO create the Identity Item (BIT-667)
|
||||
override val name: String = "",
|
||||
) : ItemType() {
|
||||
override val displayStringResId: Int
|
||||
get() = ItemTypeOption.IDENTITY.labelRes
|
||||
}
|
||||
/**
|
||||
* Represents the `Identity` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Identity(
|
||||
override val name: String = "",
|
||||
) : Content() {
|
||||
override val displayStringResId: Int get() = ItemTypeOption.IDENTITY.labelRes
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the `SecureNotes` item type.
|
||||
*
|
||||
* @property folder The folder used for the SecureNotes item
|
||||
* @property favorite Indicates whether this SecureNotes item is marked as a favorite.
|
||||
* @property masterPasswordReprompt Indicates if a master password reprompt is required.
|
||||
* @property notes Notes or comments associated with the SecureNotes item.
|
||||
* @property ownership The ownership email associated with the SecureNotes item.
|
||||
* @property availableFolders A list of available folders.
|
||||
* @property availableOwners A list of available owners.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SecureNotes(
|
||||
override val name: String = "",
|
||||
val folderName: Text = DEFAULT_FOLDER,
|
||||
val favorite: Boolean = false,
|
||||
val masterPasswordReprompt: Boolean = false,
|
||||
val notes: String = "",
|
||||
val ownership: String = DEFAULT_OWNERSHIP,
|
||||
val availableFolders: List<Text> = listOf(
|
||||
"Folder 1".asText(),
|
||||
"Folder 2".asText(),
|
||||
"Folder 3".asText(),
|
||||
),
|
||||
val availableOwners: List<String> = listOf("a@b.com", "c@d.com"),
|
||||
) : ItemType() {
|
||||
/**
|
||||
* Represents the `SecureNotes` item type.
|
||||
*
|
||||
* @property folderName The folder used for the SecureNotes item
|
||||
* @property favorite Indicates whether this SecureNotes item is marked as a favorite.
|
||||
* @property masterPasswordReprompt Indicates if a master password reprompt is required.
|
||||
* @property notes Notes or comments associated with the SecureNotes item.
|
||||
* @property ownership The ownership email associated with the SecureNotes item.
|
||||
* @property availableFolders A list of available folders.
|
||||
* @property availableOwners A list of available owners.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SecureNotes(
|
||||
override val name: String = "",
|
||||
val folderName: Text = DEFAULT_FOLDER,
|
||||
val favorite: Boolean = false,
|
||||
val masterPasswordReprompt: Boolean = false,
|
||||
val notes: String = "",
|
||||
val ownership: String = DEFAULT_OWNERSHIP,
|
||||
val availableFolders: List<Text> = listOf(
|
||||
"Folder 1".asText(),
|
||||
"Folder 2".asText(),
|
||||
"Folder 3".asText(),
|
||||
),
|
||||
val availableOwners: List<String> = listOf("a@b.com", "c@d.com"),
|
||||
) : Content() {
|
||||
override val displayStringResId: Int get() = ItemTypeOption.SECURE_NOTES.labelRes
|
||||
|
||||
override val displayStringResId: Int
|
||||
get() = ItemTypeOption.SECURE_NOTES.labelRes
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_FOLDER: Text = R.string.folder_none.asText()
|
||||
private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com"
|
||||
companion object {
|
||||
private val DEFAULT_FOLDER: Text = R.string.folder_none.asText()
|
||||
private const val DEFAULT_OWNERSHIP: String = "placeholder@email.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,20 +84,20 @@ fun VaultData.toViewState(): VaultState.ViewState =
|
|||
}
|
||||
|
||||
/**
|
||||
* Transforms a [VaultAddItemState.ItemType] into [CipherView].
|
||||
* Transforms a [VaultAddItemState.ViewState.Content] into [CipherView].
|
||||
*/
|
||||
fun VaultAddItemState.ItemType.toCipherView(): CipherView =
|
||||
fun VaultAddItemState.ViewState.Content.toCipherView(): CipherView =
|
||||
when (this) {
|
||||
is VaultAddItemState.ItemType.Card -> toCardCipherView()
|
||||
is VaultAddItemState.ItemType.Identity -> toIdentityCipherView()
|
||||
is VaultAddItemState.ItemType.Login -> toLoginCipherView()
|
||||
is VaultAddItemState.ItemType.SecureNotes -> toSecureNotesCipherView()
|
||||
is VaultAddItemState.ViewState.Content.Card -> toCardCipherView()
|
||||
is VaultAddItemState.ViewState.Content.Identity -> toIdentityCipherView()
|
||||
is VaultAddItemState.ViewState.Content.Login -> toLoginCipherView()
|
||||
is VaultAddItemState.ViewState.Content.SecureNotes -> toSecureNotesCipherView()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms [VaultAddItemState.ItemType.Login] into [CipherView].
|
||||
* Transforms [VaultAddItemState.ViewState.Content.Login] into [CipherView].
|
||||
*/
|
||||
private fun VaultAddItemState.ItemType.Login.toLoginCipherView(): CipherView =
|
||||
private fun VaultAddItemState.ViewState.Content.Login.toLoginCipherView(): CipherView =
|
||||
CipherView(
|
||||
id = null,
|
||||
// TODO use real organization id BIT-780
|
||||
|
@ -149,9 +149,9 @@ private fun VaultAddItemState.ItemType.Login.toLoginCipherView(): CipherView =
|
|||
)
|
||||
|
||||
/**
|
||||
* Transforms [VaultAddItemState.ItemType.SecureNotes] into [CipherView].
|
||||
* Transforms [VaultAddItemState.ViewState.Content.SecureNotes] into [CipherView].
|
||||
*/
|
||||
private fun VaultAddItemState.ItemType.SecureNotes.toSecureNotesCipherView(): CipherView =
|
||||
private fun VaultAddItemState.ViewState.Content.SecureNotes.toSecureNotesCipherView(): CipherView =
|
||||
CipherView(
|
||||
id = null,
|
||||
// TODO use real organization id BIT-780
|
||||
|
@ -189,13 +189,13 @@ private fun VaultAddItemState.ItemType.SecureNotes.toSecureNotesCipherView(): Ci
|
|||
)
|
||||
|
||||
/**
|
||||
* Transforms [VaultAddItemState.ItemType.Identity] into [CipherView].
|
||||
* Transforms [VaultAddItemState.ViewState.Content.Identity] into [CipherView].
|
||||
*/
|
||||
private fun VaultAddItemState.ItemType.Identity.toIdentityCipherView(): CipherView =
|
||||
private fun VaultAddItemState.ViewState.Content.Identity.toIdentityCipherView(): CipherView =
|
||||
TODO("create Identity CipherView BIT-508")
|
||||
|
||||
/**
|
||||
* Transforms [VaultAddItemState.ItemType.Card] into [CipherView].
|
||||
* Transforms [VaultAddItemState.ViewState.Content.Card] into [CipherView].
|
||||
*/
|
||||
private fun VaultAddItemState.ItemType.Card.toCardCipherView(): CipherView =
|
||||
private fun VaultAddItemState.ViewState.Content.Card.toCardCipherView(): CipherView =
|
||||
TODO("create Card CipherView BIT-668")
|
||||
|
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.test.performTextInput
|
|||
import androidx.compose.ui.test.performTouchInput
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.util.isProgressBar
|
||||
import com.x8bit.bitwarden.ui.util.onAllNodesWithTextAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithContentDescriptionAfterScroll
|
||||
import com.x8bit.bitwarden.ui.util.onNodeWithTextAfterScroll
|
||||
|
@ -126,6 +127,43 @@ class VaultAddItemScreenTest : BaseComposeTest() {
|
|||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `error text and retry should be displayed according to state`() {
|
||||
val message = "error_message"
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Loading)
|
||||
}
|
||||
composeTestRule.onNodeWithText(message).assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Content.Login())
|
||||
}
|
||||
composeTestRule.onNodeWithText(message).assertIsNotDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Error(message.asText()))
|
||||
}
|
||||
composeTestRule.onNodeWithText(message).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `progressbar should be displayed according to state`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Loading)
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Error("Fail".asText()))
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = VaultAddItemState.ViewState.Content.Login())
|
||||
}
|
||||
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking a Type Option should send TypeOptionSelect action`() {
|
||||
// Opens the menu
|
||||
|
@ -153,7 +191,7 @@ class VaultAddItemScreenTest : BaseComposeTest() {
|
|||
.onNodeWithContentDescriptionAfterScroll(label = "Type, Login")
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { it.copy(selectedType = VaultAddItemState.ItemType.Card()) }
|
||||
mutableStateFlow.update { it.copy(viewState = VaultAddItemState.ViewState.Content.Card()) }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll(label = "Type, Card")
|
||||
|
@ -815,47 +853,48 @@ class VaultAddItemScreenTest : BaseComposeTest() {
|
|||
|
||||
//region Helper functions
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateLoginType(
|
||||
currentState: VaultAddItemState,
|
||||
transform: VaultAddItemState.ItemType.Login.() -> VaultAddItemState.ItemType.Login,
|
||||
transform: VaultAddItemState.ViewState.Content.Login.() -> VaultAddItemState.ViewState.Content.Login,
|
||||
): VaultAddItemState {
|
||||
val updatedType = when (val currentType = currentState.selectedType) {
|
||||
is VaultAddItemState.ItemType.Login -> currentType.transform()
|
||||
else -> currentType
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultAddItemState.ViewState.Content.Login -> viewState.transform()
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(selectedType = updatedType)
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateSecureNotesType(
|
||||
currentState: VaultAddItemState,
|
||||
transform: VaultAddItemState.ItemType.SecureNotes.() -> VaultAddItemState.ItemType.SecureNotes,
|
||||
transform: VaultAddItemState.ViewState.Content.SecureNotes.() -> VaultAddItemState.ViewState.Content.SecureNotes,
|
||||
): VaultAddItemState {
|
||||
val updatedType = when (val currentType = currentState.selectedType) {
|
||||
is VaultAddItemState.ItemType.SecureNotes -> currentType.transform()
|
||||
else -> currentType
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultAddItemState.ViewState.Content.SecureNotes -> viewState.transform()
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(selectedType = updatedType)
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
//endregion Helper functions
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE_LOGIN_DIALOG = VaultAddItemState(
|
||||
selectedType = VaultAddItemState.ItemType.Login(),
|
||||
viewState = VaultAddItemState.ViewState.Content.Login(),
|
||||
dialog = VaultAddItemState.DialogState.Error("test".asText()),
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_LOGIN = VaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
selectedType = VaultAddItemState.ItemType.Login(),
|
||||
viewState = VaultAddItemState.ViewState.Content.Login(),
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
selectedType = VaultAddItemState.ItemType.SecureNotes(),
|
||||
viewState = VaultAddItemState.ViewState.Content.SecureNotes(),
|
||||
dialog = null,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -40,11 +40,29 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(initialState, awaitItem())
|
||||
}
|
||||
fun `initial add state should be correct`() = runTest {
|
||||
val vaultAddEditType = VaultAddEditType.AddItem
|
||||
val initState = createVaultAddLoginItemState(vaultAddEditType = vaultAddEditType)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = initState,
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
),
|
||||
)
|
||||
assertEquals(initState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial edit state should be correct`() = runTest {
|
||||
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||
val initState = createVaultAddLoginItemState(vaultAddEditType = vaultAddEditType)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = initState,
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
),
|
||||
)
|
||||
assertEquals(initState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -188,7 +206,9 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedState = initialState.copy(selectedType = VaultAddItemState.ItemType.Login())
|
||||
val expectedState = initialState.copy(
|
||||
viewState = VaultAddItemState.ViewState.Content.Login(),
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -210,10 +230,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(name = "newName")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -227,10 +247,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(username = "newUsername")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -244,10 +264,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(password = "newPassword")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -260,10 +280,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(uri = "newUri")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -276,10 +296,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(folderName = "newFolder".asText())
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -292,10 +312,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(favorite = true)
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -312,10 +332,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(masterPasswordReprompt = true)
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -328,10 +348,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(notes = "newNotes")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -346,10 +366,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedLoginItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.Login)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.Login)
|
||||
.copy(ownership = "newOwner")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedLoginItem)
|
||||
val expectedState = initialState.copy(viewState = expectedLoginItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -496,10 +516,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(name = "newName")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -513,10 +533,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(folderName = "newFolder".asText())
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -528,10 +548,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(favorite = true)
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -548,10 +568,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(masterPasswordReprompt = true)
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -565,10 +585,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(notes = "newNotes")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -582,10 +602,10 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
viewModel.actionChannel.trySend(action)
|
||||
|
||||
val expectedSecureNotesItem =
|
||||
(initialState.selectedType as VaultAddItemState.ItemType.SecureNotes)
|
||||
(initialState.viewState as VaultAddItemState.ViewState.Content.SecureNotes)
|
||||
.copy(ownership = "newOwner")
|
||||
|
||||
val expectedState = initialState.copy(selectedType = expectedSecureNotesItem)
|
||||
val expectedState = initialState.copy(viewState = expectedSecureNotesItem)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
@ -618,6 +638,7 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createVaultAddLoginItemState(
|
||||
vaultAddEditType: VaultAddEditType = VaultAddEditType.AddItem,
|
||||
name: String = "",
|
||||
username: String = "",
|
||||
password: String = "",
|
||||
|
@ -630,8 +651,8 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
dialogState: VaultAddItemState.DialogState? = null,
|
||||
): VaultAddItemState =
|
||||
VaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
selectedType = VaultAddItemState.ItemType.Login(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
viewState = VaultAddItemState.ViewState.Content.Login(
|
||||
name = name,
|
||||
username = username,
|
||||
password = password,
|
||||
|
@ -657,7 +678,7 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
): VaultAddItemState =
|
||||
VaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem,
|
||||
selectedType = VaultAddItemState.ItemType.SecureNotes(
|
||||
viewState = VaultAddItemState.ViewState.Content.SecureNotes(
|
||||
name = name,
|
||||
folderName = folder,
|
||||
favorite = favorite,
|
||||
|
@ -692,3 +713,5 @@ class VaultAddItemViewModelTest : BaseViewModelTest() {
|
|||
vaultRepository = vaultRepo,
|
||||
)
|
||||
}
|
||||
|
||||
private const val DEFAULT_EDIT_ITEM_ID: String = "edit_item_id"
|
||||
|
|
|
@ -109,7 +109,7 @@ class VaultDataExtensionsTest {
|
|||
fun `toCipherView should transform Login ItemType to CipherView`() {
|
||||
mockkStatic(Instant::class)
|
||||
every { Instant.now() } returns Instant.MIN
|
||||
val loginItemType = VaultAddItemState.ItemType.Login(
|
||||
val loginItemType = VaultAddItemState.ViewState.Content.Login(
|
||||
name = "mockName-1",
|
||||
username = "mockUsername-1",
|
||||
password = "mockPassword-1",
|
||||
|
@ -170,7 +170,7 @@ class VaultDataExtensionsTest {
|
|||
fun `toCipherView should transform SecureNotes ItemType to CipherView`() {
|
||||
mockkStatic(Instant::class)
|
||||
every { Instant.now() } returns Instant.MIN
|
||||
val secureNotesItemType = VaultAddItemState.ItemType.SecureNotes(
|
||||
val secureNotesItemType = VaultAddItemState.ViewState.Content.SecureNotes(
|
||||
name = "mockName-1",
|
||||
folderName = "mockFolder-1".asText(),
|
||||
favorite = false,
|
||||
|
|
Loading…
Add table
Reference in a new issue