From 978e72899b901dadb75338e1f9f06bb31c42b9a2 Mon Sep 17 00:00:00 2001 From: David Perez Date: Sun, 7 Jan 2024 20:05:33 -0600 Subject: [PATCH] Refactor AddSendViewModel to support loading and error states. (#524) --- .../feature/send/addsend/AddSendContent.kt | 281 +++++++++++++++++ .../feature/send/addsend/AddSendScreen.kt | 291 ++---------------- .../feature/send/addsend/AddSendViewModel.kt | 176 ++++++++--- .../send/addsend/handlers/AddSendHandlers.kt | 53 ++++ .../feature/send/addsend/AddSendScreenTest.kt | 109 +++++-- .../send/addsend/AddSendViewModelTest.kt | 92 ++++-- 6 files changed, 635 insertions(+), 367 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt new file mode 100644 index 0000000000..49029610fc --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendContent.kt @@ -0,0 +1,281 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.addsend + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText +import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper +import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField +import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch +import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState +import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser +import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser +import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers + +/** + * Content view for the [AddSendScreen]. + */ +@Suppress("LongMethod") +@Composable +fun AddSendContent( + state: AddSendState.ViewState.Content, + addSendHandlers: AddSendHandlers, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()), + ) { + BitwardenTextField( + modifier = Modifier.padding(horizontal = 16.dp), + label = stringResource(id = R.string.name), + hint = stringResource(id = R.string.name_info), + value = state.common.name, + onValueChange = addSendHandlers.onNamChange, + ) + + Spacer(modifier = Modifier.height(16.dp)) + BitwardenListHeaderText( + label = stringResource(id = R.string.type), + modifier = Modifier.padding(horizontal = 16.dp), + ) + + Spacer(modifier = Modifier.height(16.dp)) + BitwardenSegmentedButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + options = listOf( + SegmentedButtonState( + text = stringResource(id = R.string.file), + onClick = addSendHandlers.onFileTypeSelect, + isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.File, + ), + SegmentedButtonState( + text = stringResource(id = R.string.text), + onClick = addSendHandlers.onTextTypeSelect, + isChecked = state.selectedType is AddSendState.ViewState.Content.SendType.Text, + ), + ), + ) + + Spacer(modifier = Modifier.height(16.dp)) + when (val type = state.selectedType) { + is AddSendState.ViewState.Content.SendType.File -> { + BitwardenListHeaderText( + label = stringResource(id = R.string.file), + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.no_file_chosen), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenFilledTonalButton( + label = stringResource(id = R.string.choose_file), + onClick = addSendHandlers.onChooseFileCLick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.max_file_size), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 32.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringResource(id = R.string.type_file_info), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + + is AddSendState.ViewState.Content.SendType.Text -> { + BitwardenTextField( + modifier = Modifier.padding(horizontal = 16.dp), + label = stringResource(id = R.string.text), + hint = stringResource(id = R.string.type_text_info), + value = type.input, + onValueChange = addSendHandlers.onTextChange, + ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenWideSwitch( + modifier = Modifier.padding(horizontal = 16.dp), + label = stringResource(id = R.string.hide_text_by_default), + isChecked = type.isHideByDefaultChecked, + onCheckedChange = addSendHandlers.onIsHideByDefaultToggle, + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + AddSendOptions( + state = state, + onMaxAccessCountChange = addSendHandlers.onMaxAccessCountChange, + onPasswordChange = addSendHandlers.onPasswordChange, + onNoteChange = addSendHandlers.onNoteChange, + onHideEmailChecked = addSendHandlers.onHideEmailToggle, + onDeactivateSendChecked = addSendHandlers.onDeactivateSendToggle, + ) + + Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.navigationBarsPadding()) + } +} + +/** + * Displays a collapsable set of new send options. + * + * @param state The content state. + * @param onMaxAccessCountChange called when max access count changes. + * @param onPasswordChange called when the password changes. + * @param onNoteChange called when the notes changes. + * @param onHideEmailChecked called when hide email is checked. + * @param onDeactivateSendChecked called when deactivate send is checked. + */ +@Suppress("LongMethod") +@Composable +private fun AddSendOptions( + state: AddSendState.ViewState.Content, + onMaxAccessCountChange: (Int) -> Unit, + onPasswordChange: (String) -> Unit, + onNoteChange: (String) -> Unit, + onHideEmailChecked: (Boolean) -> Unit, + onDeactivateSendChecked: (Boolean) -> Unit, +) { + var isExpanded by rememberSaveable { mutableStateOf(false) } + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClickLabel = if (isExpanded) { + stringResource(id = R.string.options_expanded) + } else { + stringResource(id = R.string.options_collapsed) + }, + onClick = { isExpanded = !isExpanded }, + ) + .padding(16.dp) + .semantics(mergeDescendants = true) {}, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.options), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(end = 8.dp), + ) + Icon( + painter = if (isExpanded) { + painterResource(R.drawable.ic_expand_up) + } else { + painterResource(R.drawable.ic_expand_down) + }, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + // Hide all content if not expanded: + AnimatedVisibility( + visible = isExpanded, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically(), + modifier = Modifier.clipToBounds(), + ) { + Column { + SendDeletionDateChooser( + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + SendExpirationDateChooser( + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenStepper( + label = stringResource(id = R.string.maximum_access_count), + value = state.common.maxAccessCount, + onValueChange = onMaxAccessCountChange, + isDecrementEnabled = state.common.maxAccessCount != null, + modifier = Modifier + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.maximum_access_count_info), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenPasswordField( + label = stringResource(id = R.string.new_password), + hint = stringResource(id = R.string.password_info), + value = state.common.passwordInput, + onValueChange = onPasswordChange, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + BitwardenTextField( + label = stringResource(id = R.string.notes), + hint = stringResource(id = R.string.notes_info), + value = state.common.noteInput, + onValueChange = onNoteChange, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenWideSwitch( + modifier = Modifier.padding(horizontal = 16.dp), + label = stringResource(id = R.string.hide_email), + isChecked = state.common.isHideEmailChecked, + onCheckedChange = onHideEmailChecked, + ) + Spacer(modifier = Modifier.height(16.dp)) + BitwardenWideSwitch( + modifier = Modifier.padding(horizontal = 16.dp), + label = stringResource(id = R.string.disable_send), + isChecked = state.common.isDeactivateChecked, + onCheckedChange = onDeactivateSendChecked, + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt index c0215ebc3b..7376ba6b0b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreen.kt @@ -1,61 +1,30 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds 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.semantics.semantics -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect -import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledTonalButton -import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText -import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField +import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent +import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold -import com.x8bit.bitwarden.ui.platform.components.BitwardenSegmentedButton -import com.x8bit.bitwarden.ui.platform.components.BitwardenStepper import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton -import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar -import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch -import com.x8bit.bitwarden.ui.platform.components.SegmentedButtonState -import com.x8bit.bitwarden.ui.tools.feature.send.SendDeletionDateChooser -import com.x8bit.bitwarden.ui.tools.feature.send.SendExpirationDateChooser +import com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers.AddSendHandlers /** * Displays new send UX. @@ -104,249 +73,25 @@ fun AddSendScreen( ) }, ) { innerPadding -> - Column( - modifier = Modifier - .imePadding() - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(paddingValues = innerPadding), - ) { - BitwardenTextField( - modifier = Modifier.padding(horizontal = 16.dp), - label = stringResource(id = R.string.name), - hint = stringResource(id = R.string.name_info), - value = state.name, - onValueChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.NameChange(it)) } - }, - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenListHeaderText( - label = stringResource(id = R.string.type), - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenSegmentedButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - options = listOf( - SegmentedButtonState( - text = stringResource(id = R.string.file), - onClick = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.FileTypeClick) } - }, - isChecked = state.selectedType is AddSendState.SendType.File, - ), - SegmentedButtonState( - text = stringResource(id = R.string.text), - onClick = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.TextTypeClick) } - }, - isChecked = state.selectedType is AddSendState.SendType.Text, - ), - ), - ) - Spacer(modifier = Modifier.height(16.dp)) - when (val type = state.selectedType) { - is AddSendState.SendType.File -> { - BitwardenListHeaderText( - label = stringResource(id = R.string.file), - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = stringResource(id = R.string.no_file_chosen), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenFilledTonalButton( - label = stringResource(id = R.string.choose_file), - onClick = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.ChooseFileClick) } - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.max_file_size), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(horizontal = 32.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.type_file_info), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(horizontal = 16.dp), - ) - } + val modifier = Modifier + .imePadding() + .fillMaxSize() + .padding(paddingValues = innerPadding) - is AddSendState.SendType.Text -> { - BitwardenTextField( - modifier = Modifier.padding(horizontal = 16.dp), - label = stringResource(id = R.string.text), - hint = stringResource(id = R.string.type_text_info), - value = type.input, - onValueChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.TextChange(it)) } - }, - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenWideSwitch( - modifier = Modifier.padding(horizontal = 16.dp), - label = stringResource(id = R.string.hide_text_by_default), - isChecked = type.isHideByDefaultChecked, - onCheckedChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it)) } - }, - ) - } - } - Spacer(modifier = Modifier.height(16.dp)) - NewSendOptions( - state = state, - onMaxAccessCountChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it)) } - }, - onPasswordChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.PasswordChange(it)) } - }, - onNoteChange = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.NoteChange(it)) } - }, - onHideEmailChecked = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it)) } - }, - onDeactivateSendChecked = remember(viewModel) { - { viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it)) } - }, + when (val viewState = state.viewState) { + is AddSendState.ViewState.Content -> AddSendContent( + state = viewState, + addSendHandlers = remember(viewModel) { AddSendHandlers.create(viewModel) }, + modifier = modifier, ) - Spacer(modifier = Modifier.height(24.dp)) - Spacer(modifier = Modifier.navigationBarsPadding()) - } - } -} -/** - * Displays a collapsable set of new send options. - * - * @param state state. - * @param onMaxAccessCountChange called when max access count changes. - * @param onPasswordChange called when the password changes. - * @param onNoteChange called when the notes changes. - * @param onHideEmailChecked called when hide email is checked. - * @param onDeactivateSendChecked called when deactivate send is checked. - */ -@Suppress("LongMethod") -@Composable -private fun NewSendOptions( - state: AddSendState, - onMaxAccessCountChange: (Int) -> Unit, - onPasswordChange: (String) -> Unit, - onNoteChange: (String) -> Unit, - onHideEmailChecked: (Boolean) -> Unit, - onDeactivateSendChecked: (Boolean) -> Unit, -) { - var isExpanded by rememberSaveable { mutableStateOf(false) } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - onClickLabel = if (isExpanded) { - stringResource(id = R.string.options_expanded) - } else { - stringResource(id = R.string.options_collapsed) - }, - onClick = { isExpanded = !isExpanded }, + is AddSendState.ViewState.Error -> BitwardenErrorContent( + message = viewState.message(), + modifier = modifier, ) - .padding(16.dp) - .semantics(mergeDescendants = true) {}, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.options), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(end = 8.dp), - ) - Icon( - painter = if (isExpanded) { - painterResource(R.drawable.ic_expand_up) - } else { - painterResource(R.drawable.ic_expand_down) - }, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - // Hide all content if not expanded: - AnimatedVisibility( - visible = isExpanded, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically(), - modifier = Modifier.clipToBounds(), - ) { - Column { - SendDeletionDateChooser( - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - SendExpirationDateChooser( - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenStepper( - label = stringResource(id = R.string.maximum_access_count), - value = state.maxAccessCount, - onValueChange = onMaxAccessCountChange, - isDecrementEnabled = state.maxAccessCount != null, - modifier = Modifier - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.maximum_access_count_info), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 32.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenPasswordField( - label = stringResource(id = R.string.new_password), - hint = stringResource(id = R.string.password_info), - value = state.passwordInput, - onValueChange = onPasswordChange, - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - BitwardenTextField( - label = stringResource(id = R.string.notes), - hint = stringResource(id = R.string.notes_info), - value = state.noteInput, - onValueChange = onNoteChange, - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenWideSwitch( - modifier = Modifier.padding(horizontal = 16.dp), - label = stringResource(id = R.string.hide_email), - isChecked = state.isHideEmailChecked, - onCheckedChange = onHideEmailChecked, - ) - Spacer(modifier = Modifier.height(16.dp)) - BitwardenWideSwitch( - modifier = Modifier.padding(horizontal = 16.dp), - label = stringResource(id = R.string.disable_send), - isChecked = state.isDeactivateChecked, - onCheckedChange = onDeactivateSendChecked, + + AddSendState.ViewState.Loading -> BitwardenLoadingContent( + modifier = modifier, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt index eecd583c46..e9dd19e94d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.ui.platform.base.BaseViewModel +import com.x8bit.bitwarden.ui.platform.base.util.Text import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -22,15 +23,19 @@ class AddSendViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: AddSendState( - name = "", - maxAccessCount = null, - passwordInput = "", - noteInput = "", - isHideEmailChecked = false, - isDeactivateChecked = false, - selectedType = AddSendState.SendType.Text( - input = "", - isHideByDefaultChecked = false, + viewState = AddSendState.ViewState.Content( + common = AddSendState.ViewState.Content.Common( + name = "", + maxAccessCount = null, + passwordInput = "", + noteInput = "", + isHideEmailChecked = false, + isDeactivateChecked = false, + ), + selectedType = AddSendState.ViewState.Content.SendType.Text( + input = "", + isHideByDefaultChecked = false, + ), ), ), ) { @@ -58,25 +63,25 @@ class AddSendViewModel @Inject constructor( } private fun handlePasswordChange(action: AddSendAction.PasswordChange) { - mutableStateFlow.update { + updateCommonContent { it.copy(passwordInput = action.input) } } private fun handleNoteChange(action: AddSendAction.NoteChange) { - mutableStateFlow.update { + updateCommonContent { it.copy(noteInput = action.input) } } private fun handleHideMyEmailToggle(action: AddSendAction.HideMyEmailToggle) { - mutableStateFlow.update { + updateCommonContent { it.copy(isHideEmailChecked = action.isChecked) } } private fun handleDeactivateThisSendToggle(action: AddSendAction.DeactivateThisSendToggle) { - mutableStateFlow.update { + updateCommonContent { it.copy(isDeactivateChecked = action.isChecked) } } @@ -86,36 +91,37 @@ class AddSendViewModel @Inject constructor( private fun handleSaveClick() = sendEvent(AddSendEvent.ShowToast("Save Not Implemented")) private fun handleNameChange(action: AddSendAction.NameChange) { - mutableStateFlow.update { + updateCommonContent { it.copy(name = action.input) } } private fun handleFileTypeClick() { - mutableStateFlow.update { - it.copy(selectedType = AddSendState.SendType.File) + updateContent { + it.copy(selectedType = AddSendState.ViewState.Content.SendType.File) } } private fun handleTextTypeClick() { - mutableStateFlow.update { - it.copy(selectedType = AddSendState.SendType.Text("", isHideByDefaultChecked = false)) + updateContent { + it.copy( + selectedType = AddSendState.ViewState.Content.SendType.Text( + input = "", + isHideByDefaultChecked = false, + ), + ) } } private fun handleTextChange(action: AddSendAction.TextChange) { - val currentSendInput = - mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return - mutableStateFlow.update { - it.copy(selectedType = currentSendInput.copy(input = action.input)) + updateTextContent { + it.copy(input = action.input) } } private fun handleHideByDefaultToggle(action: AddSendAction.HideByDefaultToggle) { - val currentSendInput = - mutableStateFlow.value.selectedType as? AddSendState.SendType.Text ?: return - mutableStateFlow.update { - it.copy(selectedType = currentSendInput.copy(isHideByDefaultChecked = action.isChecked)) + updateTextContent { + it.copy(isHideByDefaultChecked = action.isChecked) } } @@ -125,8 +131,54 @@ class AddSendViewModel @Inject constructor( } private fun handleMaxAccessCountChange(action: AddSendAction.MaxAccessCountChange) { - mutableStateFlow.update { - it.copy(maxAccessCount = action.value) + updateCommonContent { it.copy(maxAccessCount = action.value) } + } + + private inline fun onContent( + crossinline block: (AddSendState.ViewState.Content) -> Unit, + ) { + (state.viewState as? AddSendState.ViewState.Content)?.let(block) + } + + private inline fun updateContent( + crossinline block: ( + AddSendState.ViewState.Content, + ) -> AddSendState.ViewState.Content?, + ) { + val currentViewState = state.viewState + val updatedContent = (currentViewState as? AddSendState.ViewState.Content) + ?.let(block) + ?: return + mutableStateFlow.update { it.copy(viewState = updatedContent) } + } + + private inline fun updateCommonContent( + crossinline block: ( + AddSendState.ViewState.Content.Common, + ) -> AddSendState.ViewState.Content.Common, + ) { + updateContent { it.copy(common = block(it.common)) } + } + + private inline fun updateFileContent( + crossinline block: ( + AddSendState.ViewState.Content.SendType.File, + ) -> AddSendState.ViewState.Content.SendType.File, + ) { + updateContent { currentContent -> + (currentContent.selectedType as? AddSendState.ViewState.Content.SendType.File) + ?.let { currentContent.copy(selectedType = block(it)) } + } + } + + private inline fun updateTextContent( + crossinline block: ( + AddSendState.ViewState.Content.SendType.Text, + ) -> AddSendState.ViewState.Content.SendType.Text, + ) { + updateContent { currentContent -> + (currentContent.selectedType as? AddSendState.ViewState.Content.SendType.Text) + ?.let { currentContent.copy(selectedType = block(it)) } } } } @@ -136,34 +188,68 @@ class AddSendViewModel @Inject constructor( */ @Parcelize data class AddSendState( - val name: String, - val selectedType: SendType, - // Null here means "not set" - val maxAccessCount: Int?, - val passwordInput: String, - val noteInput: String, - val isHideEmailChecked: Boolean, - val isDeactivateChecked: Boolean, + val viewState: ViewState, ) : Parcelable { /** - * Models what type the user is trying to send. + * Represents the specific view states for the [AddSendScreen]. */ - sealed class SendType : Parcelable { + sealed class ViewState : Parcelable { /** - * Sending a file. + * Represents an error state for the [AddSendScreen]. */ @Parcelize - data object File : SendType() + data class Error(val message: Text) : ViewState() /** - * Sending text. + * Loading state for the [AddSendScreen], signifying that the content is being processed. */ @Parcelize - data class Text( - val input: String, - val isHideByDefaultChecked: Boolean, - ) : SendType() + data object Loading : ViewState() + + /** + * Represents a loaded content state for the [AddSendScreen]. + */ + @Parcelize + data class Content( + val common: Common, + val selectedType: SendType, + ) : ViewState() { + + /** + * Content data that is common for all item types. + */ + @Parcelize + data class Common( + val name: String, + // Null here means "not set" + val maxAccessCount: Int?, + val passwordInput: String, + val noteInput: String, + val isHideEmailChecked: Boolean, + val isDeactivateChecked: Boolean, + ) : Parcelable + + /** + * Models what type the user is trying to send. + */ + sealed class SendType : Parcelable { + /** + * Sending a file. + */ + @Parcelize + data object File : SendType() + + /** + * Sending text. + */ + @Parcelize + data class Text( + val input: String, + val isHideByDefaultChecked: Boolean, + ) : SendType() + } + } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt new file mode 100644 index 0000000000..186858a3cf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/handlers/AddSendHandlers.kt @@ -0,0 +1,53 @@ +package com.x8bit.bitwarden.ui.tools.feature.send.addsend.handlers + +import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendAction +import com.x8bit.bitwarden.ui.tools.feature.send.addsend.AddSendViewModel + +/** + * A collection of handler functions for managing actions within the context of adding + * send items. + */ +data class AddSendHandlers( + val onNamChange: (String) -> Unit, + val onFileTypeSelect: () -> Unit, + val onTextTypeSelect: () -> Unit, + val onChooseFileCLick: () -> Unit, + val onTextChange: (String) -> Unit, + val onIsHideByDefaultToggle: (Boolean) -> Unit, + val onMaxAccessCountChange: (Int) -> Unit, + val onPasswordChange: (String) -> Unit, + val onNoteChange: (String) -> Unit, + val onHideEmailToggle: (Boolean) -> Unit, + val onDeactivateSendToggle: (Boolean) -> Unit, +) { + companion object { + /** + * Creates an instance of [AddSendHandlers] by binding actions to the provided + * [AddSendViewModel]. + */ + fun create( + viewModel: AddSendViewModel, + ): AddSendHandlers = + AddSendHandlers( + onNamChange = { viewModel.trySendAction(AddSendAction.NameChange(it)) }, + onFileTypeSelect = { viewModel.trySendAction(AddSendAction.FileTypeClick) }, + onTextTypeSelect = { viewModel.trySendAction(AddSendAction.TextTypeClick) }, + onChooseFileCLick = { viewModel.trySendAction(AddSendAction.ChooseFileClick) }, + onTextChange = { viewModel.trySendAction(AddSendAction.TextChange(it)) }, + onIsHideByDefaultToggle = { + viewModel.trySendAction(AddSendAction.HideByDefaultToggle(it)) + }, + onMaxAccessCountChange = { + viewModel.trySendAction(AddSendAction.MaxAccessCountChange(it)) + }, + onPasswordChange = { viewModel.trySendAction(AddSendAction.PasswordChange(it)) }, + onNoteChange = { viewModel.trySendAction(AddSendAction.NoteChange(it)) }, + onHideEmailToggle = { + viewModel.trySendAction(AddSendAction.HideMyEmailToggle(it)) + }, + onDeactivateSendToggle = { + viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(it)) + }, + ) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt index 9da26587a1..d0c11af3ae 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendScreenTest.kt @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.ui.tools.feature.send.addsend +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsOff import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertTextEquals @@ -13,6 +14,8 @@ import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow 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 io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -83,7 +86,11 @@ class AddSendScreenTest : BaseComposeTest() { ) mutableStateFlow.update { - it.copy(name = "input") + it.copy( + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(name = "input"), + ), + ) } composeTestRule .onNodeWithText("Name") @@ -113,7 +120,9 @@ class AddSendScreenTest : BaseComposeTest() { @Test fun `Choose file button click should send ChooseFileClick`() { mutableStateFlow.value = DEFAULT_STATE.copy( - selectedType = AddSendState.SendType.File, + viewState = DEFAULT_VIEW_STATE.copy( + selectedType = AddSendState.ViewState.Content.SendType.File, + ), ) composeTestRule .onNodeWithText("Choose file") @@ -143,9 +152,11 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - selectedType = AddSendState.SendType.Text( - input = "input", - isHideByDefaultChecked = false, + viewState = DEFAULT_VIEW_STATE.copy( + selectedType = AddSendState.ViewState.Content.SendType.Text( + input = "input", + isHideByDefaultChecked = false, + ), ), ) } @@ -175,9 +186,11 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - selectedType = AddSendState.SendType.Text( - input = "", - isHideByDefaultChecked = true, + viewState = DEFAULT_VIEW_STATE.copy( + selectedType = AddSendState.ViewState.Content.SendType.Text( + input = "", + isHideByDefaultChecked = true, + ), ), ) } @@ -253,7 +266,11 @@ class AddSendScreenTest : BaseComposeTest() { @Test fun `max access count decrement should send MaxAccessCountChange`() = runTest { mutableStateFlow.update { - it.copy(maxAccessCount = 3) + it.copy( + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 3), + ), + ) } // Expand options section: composeTestRule @@ -272,7 +289,11 @@ class AddSendScreenTest : BaseComposeTest() { fun `max access count decrement when set to 1 should do nothing`() = runTest { mutableStateFlow.update { - it.copy(maxAccessCount = 1) + it.copy( + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 1), + ), + ) } // Expand options section: composeTestRule @@ -333,7 +354,9 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - passwordInput = "input", + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(passwordInput = "input"), + ), ) } composeTestRule @@ -376,7 +399,9 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - noteInput = "input", + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(noteInput = "input"), + ), ) } composeTestRule @@ -416,7 +441,9 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - isHideEmailChecked = true, + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(isHideEmailChecked = true), + ), ) } composeTestRule @@ -452,7 +479,9 @@ class AddSendScreenTest : BaseComposeTest() { mutableStateFlow.update { it.copy( - isDeactivateChecked = true, + viewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(isDeactivateChecked = true), + ), ) } composeTestRule @@ -460,18 +489,60 @@ class AddSendScreenTest : BaseComposeTest() { .assertIsOn() } + @Test + fun `progressbar should be displayed according to state`() { + mutableStateFlow.update { + it.copy(viewState = AddSendState.ViewState.Loading) + } + composeTestRule.onNode(isProgressBar).assertIsDisplayed() + + mutableStateFlow.update { + it.copy(viewState = AddSendState.ViewState.Error("Fail".asText())) + } + composeTestRule.onNode(isProgressBar).assertDoesNotExist() + + mutableStateFlow.update { + it.copy(viewState = DEFAULT_VIEW_STATE) + } + composeTestRule.onNode(isProgressBar).assertDoesNotExist() + } + + @Test + fun `error should be displayed according to state`() { + val errorMessage = "Fail" + mutableStateFlow.update { + it.copy(viewState = AddSendState.ViewState.Error(errorMessage.asText())) + } + composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed() + + mutableStateFlow.update { + it.copy(viewState = AddSendState.ViewState.Loading) + } + composeTestRule.onNodeWithText(errorMessage).assertDoesNotExist() + } + companion object { - private val DEFAULT_STATE = AddSendState( + private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common( name = "", maxAccessCount = null, passwordInput = "", noteInput = "", isHideEmailChecked = false, isDeactivateChecked = false, - selectedType = AddSendState.SendType.Text( - input = "", - isHideByDefaultChecked = false, - ), + ) + + private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text( + input = "", + isHideByDefaultChecked = false, + ) + + private val DEFAULT_VIEW_STATE = AddSendState.ViewState.Content( + common = DEFAULT_COMMON_STATE, + selectedType = DEFAULT_SELECTED_TYPE_STATE, + ) + + private val DEFAULT_STATE = AddSendState( + viewState = DEFAULT_VIEW_STATE, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index 945bdc9668..53f0e2bb8b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -19,9 +19,7 @@ class AddSendViewModelTest : BaseViewModelTest() { @Test fun `initial state should read from saved state when present`() { val savedState = mockk() - val viewModel = createViewModel( - savedStateHandle = SavedStateHandle(mapOf("state" to savedState)), - ) + val viewModel = createViewModel(savedState) assertEquals(savedState, viewModel.stateFlow.value) } @@ -55,13 +53,14 @@ class AddSendViewModelTest : BaseViewModelTest() { @Test fun `FileTypeClick and TextTypeClick should toggle sendType`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + selectedType = AddSendState.ViewState.Content.SendType.File, + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.FileTypeClick) - assertEquals( - DEFAULT_STATE.copy(selectedType = AddSendState.SendType.File), - awaitItem(), - ) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) viewModel.trySendAction(AddSendAction.TextTypeClick) assertEquals(DEFAULT_STATE, awaitItem()) } @@ -70,99 +69,132 @@ class AddSendViewModelTest : BaseViewModelTest() { @Test fun `NameChange should update name input`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(name = "input"), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.NameChange("input")) - assertEquals(DEFAULT_STATE.copy(name = "input"), awaitItem()) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `MaxAccessCountChange should update maxAccessCount`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(maxAccessCount = 5), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.MaxAccessCountChange(5)) - assertEquals(DEFAULT_STATE.copy(maxAccessCount = 5), awaitItem()) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `TextChange should update text input`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + selectedType = AddSendState.ViewState.Content.SendType.Text( + input = "input", + isHideByDefaultChecked = false, + ), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.TextChange("input")) - assertEquals( - DEFAULT_STATE.copy( - selectedType = AddSendState.SendType.Text( - input = "input", - isHideByDefaultChecked = false, - ), - ), - awaitItem(), - ) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `NoteChange should update note input`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(noteInput = "input"), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.NoteChange("input")) - assertEquals(DEFAULT_STATE.copy(noteInput = "input"), awaitItem()) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `PasswordChange should update note input`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(passwordInput = "input"), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.PasswordChange("input")) - assertEquals(DEFAULT_STATE.copy(passwordInput = "input"), awaitItem()) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `DeactivateThisSendToggle should update isDeactivateChecked`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(isDeactivateChecked = true), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) viewModel.trySendAction(AddSendAction.DeactivateThisSendToggle(true)) - assertEquals(DEFAULT_STATE.copy(isDeactivateChecked = true), awaitItem()) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } @Test fun `HideMyEmailToggle should update isHideEmailChecked`() = runTest { val viewModel = createViewModel() + val expectedViewState = DEFAULT_VIEW_STATE.copy( + common = DEFAULT_COMMON_STATE.copy(isHideEmailChecked = true), + ) + viewModel.stateFlow.test { assertEquals(DEFAULT_STATE, awaitItem()) - viewModel.trySendAction(AddSendAction.HideMyEmailToggle(true)) - assertEquals(DEFAULT_STATE.copy(isHideEmailChecked = true), awaitItem()) + viewModel.trySendAction(AddSendAction.HideMyEmailToggle(isChecked = true)) + assertEquals(DEFAULT_STATE.copy(viewState = expectedViewState), awaitItem()) } } private fun createViewModel( - savedStateHandle: SavedStateHandle = SavedStateHandle(), + state: AddSendState? = null, ): AddSendViewModel = AddSendViewModel( - savedStateHandle = savedStateHandle, + savedStateHandle = SavedStateHandle().apply { set("state", state) }, ) companion object { - private val DEFAULT_STATE = AddSendState( + private val DEFAULT_COMMON_STATE = AddSendState.ViewState.Content.Common( name = "", maxAccessCount = null, passwordInput = "", noteInput = "", isHideEmailChecked = false, isDeactivateChecked = false, - selectedType = AddSendState.SendType.Text( - input = "", - isHideByDefaultChecked = false, - ), + ) + + private val DEFAULT_SELECTED_TYPE_STATE = AddSendState.ViewState.Content.SendType.Text( + input = "", + isHideByDefaultChecked = false, + ) + + private val DEFAULT_VIEW_STATE = AddSendState.ViewState.Content( + common = DEFAULT_COMMON_STATE, + selectedType = DEFAULT_SELECTED_TYPE_STATE, + ) + + private val DEFAULT_STATE = AddSendState( + viewState = DEFAULT_VIEW_STATE, ) } }