mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
Separate common and type-specific state in VaultItemViewModel (#447)
This commit is contained in:
parent
e31febb1c4
commit
0dd162598f
9 changed files with 1373 additions and 1012 deletions
|
@ -26,6 +26,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
|
||||
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
|
||||
/**
|
||||
* The top level content UI state for the [VaultItemScreen] when viewing a Login cipher.
|
||||
|
@ -33,9 +35,11 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
|
|||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun VaultItemLoginContent(
|
||||
viewState: VaultItemState.ViewState.Content.Login,
|
||||
commonState: VaultItemState.ViewState.Content.Common,
|
||||
loginItemState: VaultItemState.ViewState.Content.ItemType.Login,
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
loginHandlers: LoginHandlers,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
|
@ -52,7 +56,7 @@ fun VaultItemLoginContent(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.name),
|
||||
value = viewState.name,
|
||||
value = commonState.name,
|
||||
onValueChange = { },
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
|
@ -62,12 +66,12 @@ fun VaultItemLoginContent(
|
|||
)
|
||||
}
|
||||
|
||||
viewState.username?.let { username ->
|
||||
loginItemState.username?.let { username ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
UsernameField(
|
||||
username = username,
|
||||
onCopyUsernameClick = loginHandlers.onCopyUsernameClick,
|
||||
onCopyUsernameClick = vaultLoginItemTypeHandlers.onCopyUsernameClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
|
@ -75,14 +79,14 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
viewState.passwordData?.let { passwordData ->
|
||||
loginItemState.passwordData?.let { passwordData ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
PasswordField(
|
||||
passwordData = passwordData,
|
||||
onShowPasswordClick = loginHandlers.onShowPasswordClick,
|
||||
onCheckForBreachClick = loginHandlers.onCheckForBreachClick,
|
||||
onCopyPasswordClick = loginHandlers.onCopyPasswordClick,
|
||||
onShowPasswordClick = vaultLoginItemTypeHandlers.onShowPasswordClick,
|
||||
onCheckForBreachClick = vaultLoginItemTypeHandlers.onCheckForBreachClick,
|
||||
onCopyPasswordClick = vaultLoginItemTypeHandlers.onCopyPasswordClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
|
@ -93,14 +97,14 @@ fun VaultItemLoginContent(
|
|||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TotpField(
|
||||
isPremiumUser = viewState.isPremiumUser,
|
||||
isPremiumUser = commonState.isPremiumUser,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
viewState.uris.takeUnless { it.isEmpty() }?.let { uris ->
|
||||
loginItemState.uris.takeUnless { it.isEmpty() }?.let { uris ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
BitwardenListHeaderText(
|
||||
|
@ -114,8 +118,8 @@ fun VaultItemLoginContent(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
UriField(
|
||||
uriData = uriData,
|
||||
onCopyUriClick = loginHandlers.onCopyUriClick,
|
||||
onLaunchUriClick = loginHandlers.onLaunchUriClick,
|
||||
onCopyUriClick = vaultLoginItemTypeHandlers.onCopyUriClick,
|
||||
onLaunchUriClick = vaultLoginItemTypeHandlers.onLaunchUriClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
|
@ -123,7 +127,7 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
viewState.notes?.let { notes ->
|
||||
commonState.notes?.let { notes ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
BitwardenListHeaderText(
|
||||
|
@ -142,7 +146,7 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
viewState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
commonState.customFields.takeUnless { it.isEmpty() }?.let { customFields ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
BitwardenListHeaderText(
|
||||
|
@ -156,9 +160,9 @@ fun VaultItemLoginContent(
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
CustomField(
|
||||
customField = customField,
|
||||
onCopyCustomHiddenField = loginHandlers.onCopyCustomHiddenField,
|
||||
onCopyCustomTextField = loginHandlers.onCopyCustomTextField,
|
||||
onShowHiddenFieldClick = loginHandlers.onShowHiddenFieldClick,
|
||||
onCopyCustomHiddenField = vaultCommonItemTypeHandlers.onCopyCustomHiddenField,
|
||||
onCopyCustomTextField = vaultCommonItemTypeHandlers.onCopyCustomTextField,
|
||||
onShowHiddenFieldClick = vaultCommonItemTypeHandlers.onShowHiddenFieldClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
|
@ -170,14 +174,14 @@ fun VaultItemLoginContent(
|
|||
Spacer(modifier = Modifier.height(24.dp))
|
||||
UpdateText(
|
||||
header = "${stringResource(id = R.string.date_updated)}: ",
|
||||
text = viewState.lastUpdated,
|
||||
text = commonState.lastUpdated,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
viewState.passwordRevisionDate?.let { revisionDate ->
|
||||
loginItemState.passwordRevisionDate?.let { revisionDate ->
|
||||
item {
|
||||
UpdateText(
|
||||
header = "${stringResource(id = R.string.date_password_updated)}: ",
|
||||
|
@ -189,11 +193,11 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
viewState.passwordHistoryCount?.let { passwordHistoryCount ->
|
||||
loginItemState.passwordHistoryCount?.let { passwordHistoryCount ->
|
||||
item {
|
||||
PasswordHistoryCount(
|
||||
passwordHistoryCount = passwordHistoryCount,
|
||||
onPasswordHistoryClick = loginHandlers.onPasswordHistoryClick,
|
||||
onPasswordHistoryClick = vaultLoginItemTypeHandlers.onPasswordHistoryClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
|
@ -208,17 +212,17 @@ fun VaultItemLoginContent(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Suppress("LongMethod", "MaxLineLength")
|
||||
@Composable
|
||||
private fun CustomField(
|
||||
customField: VaultItemState.ViewState.Content.Custom,
|
||||
customField: VaultItemState.ViewState.Content.Common.Custom,
|
||||
onCopyCustomHiddenField: (String) -> Unit,
|
||||
onCopyCustomTextField: (String) -> Unit,
|
||||
onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Custom.HiddenField, Boolean) -> Unit,
|
||||
onShowHiddenFieldClick: (VaultItemState.ViewState.Content.Common.Custom.HiddenField, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (customField) {
|
||||
is VaultItemState.ViewState.Content.Custom.BooleanField -> {
|
||||
is VaultItemState.ViewState.Content.Common.Custom.BooleanField -> {
|
||||
BitwardenWideSwitch(
|
||||
label = customField.name,
|
||||
isChecked = customField.value,
|
||||
|
@ -228,7 +232,7 @@ private fun CustomField(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.Custom.HiddenField -> {
|
||||
is VaultItemState.ViewState.Content.Common.Custom.HiddenField -> {
|
||||
BitwardenPasswordFieldWithActions(
|
||||
label = customField.name,
|
||||
value = customField.value,
|
||||
|
@ -254,7 +258,7 @@ private fun CustomField(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.Custom.LinkedField -> {
|
||||
is VaultItemState.ViewState.Content.Common.Custom.LinkedField -> {
|
||||
BitwardenTextField(
|
||||
label = customField.name,
|
||||
value = customField.vaultLinkedFieldType.label.invoke(),
|
||||
|
@ -269,7 +273,7 @@ private fun CustomField(
|
|||
)
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.Custom.TextField -> {
|
||||
is VaultItemState.ViewState.Content.Common.Custom.TextField -> {
|
||||
BitwardenTextFieldWithActions(
|
||||
label = customField.name,
|
||||
value = customField.value,
|
||||
|
@ -310,7 +314,7 @@ private fun NotesField(
|
|||
|
||||
@Composable
|
||||
private fun PasswordField(
|
||||
passwordData: VaultItemState.ViewState.Content.PasswordData,
|
||||
passwordData: VaultItemState.ViewState.Content.ItemType.Login.PasswordData,
|
||||
onShowPasswordClick: (Boolean) -> Unit,
|
||||
onCheckForBreachClick: () -> Unit,
|
||||
onCopyPasswordClick: () -> Unit,
|
||||
|
@ -414,7 +418,7 @@ private fun UpdateText(
|
|||
|
||||
@Composable
|
||||
private fun UriField(
|
||||
uriData: VaultItemState.ViewState.Content.UriData,
|
||||
uriData: VaultItemState.ViewState.Content.ItemType.Login.UriData,
|
||||
onCopyUriClick: (String) -> Unit,
|
||||
onLaunchUriClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -473,72 +477,3 @@ private fun UsernameField(
|
|||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A class dedicated to handling user interactions related to view login cipher UI.
|
||||
* Each lambda corresponds to a specific user action, allowing for easy delegation of
|
||||
* logic when user input is detected.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class LoginHandlers(
|
||||
val onCheckForBreachClick: () -> Unit,
|
||||
val onCopyCustomHiddenField: (String) -> Unit,
|
||||
val onCopyCustomTextField: (String) -> Unit,
|
||||
val onCopyPasswordClick: () -> Unit,
|
||||
val onCopyUriClick: (String) -> Unit,
|
||||
val onCopyUsernameClick: () -> Unit,
|
||||
val onLaunchUriClick: (String) -> Unit,
|
||||
val onPasswordHistoryClick: () -> Unit,
|
||||
val onShowHiddenFieldClick: (
|
||||
VaultItemState.ViewState.Content.Custom.HiddenField,
|
||||
Boolean,
|
||||
) -> Unit,
|
||||
val onShowPasswordClick: (isVisible: Boolean) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates the [LoginHandlers] using the [viewModel] to send the desired actions.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun create(
|
||||
viewModel: VaultItemViewModel,
|
||||
): LoginHandlers =
|
||||
LoginHandlers(
|
||||
onCheckForBreachClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CheckForBreachClick)
|
||||
},
|
||||
onCopyCustomHiddenField = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyCustomHiddenFieldClick(it))
|
||||
},
|
||||
onCopyCustomTextField = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyCustomTextFieldClick(it))
|
||||
},
|
||||
onCopyPasswordClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyPasswordClick)
|
||||
},
|
||||
onCopyUriClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUriClick(it))
|
||||
},
|
||||
onCopyUsernameClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUsernameClick)
|
||||
},
|
||||
onLaunchUriClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.LaunchClick(it))
|
||||
},
|
||||
onPasswordHistoryClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordHistoryClick)
|
||||
},
|
||||
onShowHiddenFieldClick = { customField, isVisible ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.HiddenFieldVisibilityClicked(
|
||||
isVisible = isVisible,
|
||||
field = customField,
|
||||
),
|
||||
)
|
||||
},
|
||||
onShowPasswordClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordVisibilityClicked(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
|||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
|
||||
/**
|
||||
* Displays the vault item screen.
|
||||
|
@ -82,10 +84,10 @@ fun VaultItemScreen(
|
|||
VaultItemDialogs(
|
||||
dialog = state.dialog,
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.DismissDialogClick) }
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick) }
|
||||
},
|
||||
onSubmitMasterPassword = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.MasterPasswordSubmit(it)) }
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(it)) }
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -101,7 +103,7 @@ fun VaultItemScreen(
|
|||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.CloseClick) }
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.CloseClick) }
|
||||
},
|
||||
actions = {
|
||||
BitwardenOverflowActionItem()
|
||||
|
@ -117,7 +119,7 @@ fun VaultItemScreen(
|
|||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.EditClick) }
|
||||
{ viewModel.trySendAction(VaultItemAction.Common.EditClick) }
|
||||
},
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
) {
|
||||
|
@ -135,11 +137,11 @@ fun VaultItemScreen(
|
|||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
onRefreshClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultItemAction.RefreshClick) }
|
||||
vaultCommonItemTypeHandlers = remember(viewModel) {
|
||||
VaultCommonItemTypeHandlers.create(viewModel = viewModel)
|
||||
},
|
||||
loginHandlers = remember(viewModel) {
|
||||
LoginHandlers.create(viewModel)
|
||||
vaultLoginItemTypeHandlers = remember(viewModel) {
|
||||
VaultLoginItemTypeHandlers.create(viewModel = viewModel)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -178,23 +180,41 @@ private fun VaultItemDialogs(
|
|||
@Composable
|
||||
private fun VaultItemContent(
|
||||
viewState: VaultItemState.ViewState,
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
vaultLoginItemTypeHandlers: VaultLoginItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
onRefreshClick: () -> Unit,
|
||||
loginHandlers: LoginHandlers,
|
||||
) {
|
||||
when (viewState) {
|
||||
is VaultItemState.ViewState.Error -> VaultItemError(
|
||||
errorState = viewState,
|
||||
onRefreshClick = onRefreshClick,
|
||||
onRefreshClick = vaultCommonItemTypeHandlers.onRefreshClick,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
is VaultItemState.ViewState.Content -> when (viewState) {
|
||||
is VaultItemState.ViewState.Content.Login -> VaultItemLoginContent(
|
||||
viewState = viewState,
|
||||
modifier = modifier,
|
||||
loginHandlers = loginHandlers,
|
||||
)
|
||||
is VaultItemState.ViewState.Content -> {
|
||||
when (viewState.type) {
|
||||
is VaultItemState.ViewState.Content.ItemType.Login -> {
|
||||
VaultItemLoginContent(
|
||||
commonState = viewState.common,
|
||||
loginItemState = viewState.type,
|
||||
modifier = modifier,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
vaultLoginItemTypeHandlers = vaultLoginItemTypeHandlers,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.Card -> {
|
||||
// TODO UI for viewing Card BIT-513
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.Identity -> {
|
||||
// TODO UI for viewing Identity BIT-514
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.SecureNote -> {
|
||||
// TODO UI for viewing SecureNote BIT-515
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VaultItemState.ViewState.Loading -> VaultItemLoading(
|
||||
|
|
|
@ -47,6 +47,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
),
|
||||
) {
|
||||
|
||||
//region Initialization and Overrides
|
||||
init {
|
||||
combine(
|
||||
vaultRepository.getVaultItemStateFlow(state.vaultItemId),
|
||||
|
@ -63,124 +64,45 @@ class VaultItemViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: VaultItemAction) {
|
||||
when (action) {
|
||||
VaultItemAction.CloseClick -> handleCloseClick()
|
||||
VaultItemAction.DismissDialogClick -> handleDismissDialogClick()
|
||||
VaultItemAction.EditClick -> handleEditClick()
|
||||
is VaultItemAction.MasterPasswordSubmit -> handleMasterPasswordSubmit(action)
|
||||
VaultItemAction.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemAction.Login -> handleLoginActions(action)
|
||||
is VaultItemAction.ItemType.Login -> handleLoginTypeActions(action)
|
||||
is VaultItemAction.Common -> handleCommonActions(action)
|
||||
is VaultItemAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
//endregion Initialization and Overrides
|
||||
|
||||
private fun handleLoginActions(action: VaultItemAction.Login) {
|
||||
//region Common Handlers
|
||||
|
||||
private fun handleCommonActions(action: VaultItemAction.Common) {
|
||||
when (action) {
|
||||
VaultItemAction.Login.CheckForBreachClick -> handleCheckForBreachClick()
|
||||
VaultItemAction.Login.CopyPasswordClick -> handleCopyPasswordClick()
|
||||
is VaultItemAction.Login.CopyCustomHiddenFieldClick -> {
|
||||
is VaultItemAction.Common.CloseClick -> handleCloseClick()
|
||||
is VaultItemAction.Common.DismissDialogClick -> handleDismissDialogClick()
|
||||
is VaultItemAction.Common.EditClick -> handleEditClick()
|
||||
is VaultItemAction.Common.MasterPasswordSubmit -> handleMasterPasswordSubmit(action)
|
||||
is VaultItemAction.Common.RefreshClick -> handleRefreshClick()
|
||||
is VaultItemAction.Common.CopyCustomHiddenFieldClick -> {
|
||||
handleCopyCustomHiddenFieldClick(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.Login.CopyCustomTextFieldClick -> {
|
||||
is VaultItemAction.Common.CopyCustomTextFieldClick -> {
|
||||
handleCopyCustomTextFieldClick(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.Login.CopyUriClick -> handleCopyUriClick(action)
|
||||
VaultItemAction.Login.CopyUsernameClick -> handleCopyUsernameClick()
|
||||
is VaultItemAction.Login.LaunchClick -> handleLaunchClick(action)
|
||||
VaultItemAction.Login.PasswordHistoryClick -> handlePasswordHistoryClick()
|
||||
is VaultItemAction.Login.PasswordVisibilityClicked -> {
|
||||
handlePasswordVisibilityClicked(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.Login.HiddenFieldVisibilityClicked -> {
|
||||
is VaultItemAction.Common.HiddenFieldVisibilityClicked -> {
|
||||
handleHiddenFieldVisibilityClicked(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||
when (action) {
|
||||
is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action)
|
||||
is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VaultItemAction.Internal.VerifyPasswordReceive -> handleVerifyPasswordReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(VaultItemEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleCheckForBreachClick() {
|
||||
onLoginContent { login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Loading)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.getPasswordBreachCount(password = password)
|
||||
sendAction(VaultItemAction.Internal.PasswordBreachReceive(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPasswordClick() {
|
||||
onLoginContent { login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
if (login.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(password.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyCustomHiddenFieldClick(
|
||||
action: VaultItemAction.Login.CopyCustomHiddenFieldClick,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyCustomTextFieldClick(
|
||||
action: VaultItemAction.Login.CopyCustomTextFieldClick,
|
||||
) {
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
|
||||
}
|
||||
|
||||
private fun handleCopyUriClick(action: VaultItemAction.Login.CopyUriClick) {
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.uri.asText()))
|
||||
}
|
||||
|
||||
private fun handleCopyUsernameClick() {
|
||||
onLoginContent { login ->
|
||||
val username = requireNotNull(login.username)
|
||||
if (login.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(username.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDismissDialogClick() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
private fun handleEditClick() {
|
||||
onContent { content ->
|
||||
if (content.requiresReprompt) {
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
|
@ -190,11 +112,7 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleLaunchClick(action: VaultItemAction.Login.LaunchClick) {
|
||||
sendEvent(VaultItemEvent.NavigateToUri(action.uri))
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordSubmit(action: VaultItemAction.MasterPasswordSubmit) {
|
||||
private fun handleMasterPasswordSubmit(action: VaultItemAction.Common.MasterPasswordSubmit) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Loading)
|
||||
}
|
||||
|
@ -213,9 +131,147 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleRefreshClick() {
|
||||
// No need to update the view state, the vault repo will emit a new state during this time
|
||||
vaultRepository.sync()
|
||||
}
|
||||
|
||||
private fun handleCopyCustomHiddenFieldClick(
|
||||
action: VaultItemAction.Common.CopyCustomHiddenFieldClick,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyCustomTextFieldClick(
|
||||
action: VaultItemAction.Common.CopyCustomTextFieldClick,
|
||||
) {
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.field.asText()))
|
||||
}
|
||||
|
||||
private fun handleHiddenFieldVisibilityClicked(
|
||||
action: VaultItemAction.Common.HiddenFieldVisibilityClicked,
|
||||
) {
|
||||
onContent { content ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onContent
|
||||
}
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
common = content.common.copy(
|
||||
customFields = content.common.customFields.map { customField ->
|
||||
if (customField == action.field) {
|
||||
action.field.copy(isVisible = action.isVisible)
|
||||
} else {
|
||||
customField
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Common Handlers
|
||||
|
||||
//region Login Type Handlers
|
||||
|
||||
private fun handleLoginTypeActions(action: VaultItemAction.ItemType.Login) {
|
||||
when (action) {
|
||||
is VaultItemAction.ItemType.Login.CheckForBreachClick -> {
|
||||
handleCheckForBreachClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.CopyPasswordClick -> {
|
||||
handleCopyPasswordClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.CopyUriClick -> {
|
||||
handleCopyUriClick(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.CopyUsernameClick -> {
|
||||
handleCopyUsernameClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.LaunchClick -> {
|
||||
handleLaunchClick(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.PasswordHistoryClick -> {
|
||||
handlePasswordHistoryClick()
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Login.PasswordVisibilityClicked -> {
|
||||
handlePasswordVisibilityClicked(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckForBreachClick() {
|
||||
onLoginContent { _, login ->
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.Loading)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = authRepository.getPasswordBreachCount(password = password)
|
||||
sendAction(VaultItemAction.Internal.PasswordBreachReceive(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPasswordClick() {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
val password = requireNotNull(login.passwordData?.password)
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(password.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyUriClick(action: VaultItemAction.ItemType.Login.CopyUriClick) {
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(action.uri.asText()))
|
||||
}
|
||||
|
||||
private fun handleCopyUsernameClick() {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
val username = requireNotNull(login.username)
|
||||
sendEvent(VaultItemEvent.CopyToClipboard(username.asText()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLaunchClick(
|
||||
action: VaultItemAction.ItemType.Login.LaunchClick,
|
||||
) {
|
||||
sendEvent(VaultItemEvent.NavigateToUri(action.uri))
|
||||
}
|
||||
|
||||
private fun handlePasswordHistoryClick() {
|
||||
onContent { content ->
|
||||
if (content.requiresReprompt) {
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
|
@ -225,57 +281,39 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleRefreshClick() {
|
||||
// No need to update the view state, the vault repo will emit a new state during this time
|
||||
vaultRepository.sync()
|
||||
}
|
||||
|
||||
private fun handlePasswordVisibilityClicked(
|
||||
action: VaultItemAction.Login.PasswordVisibilityClicked,
|
||||
action: VaultItemAction.ItemType.Login.PasswordVisibilityClicked,
|
||||
) {
|
||||
onLoginContent { login ->
|
||||
if (login.requiresReprompt) {
|
||||
onLoginContent { content, login ->
|
||||
if (content.common.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = login.copy(
|
||||
passwordData = login.passwordData?.copy(
|
||||
isVisible = action.isVisible,
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = content.copy(
|
||||
type = login.copy(
|
||||
passwordData = login.passwordData?.copy(
|
||||
isVisible = action.isVisible,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHiddenFieldVisibilityClicked(
|
||||
action: VaultItemAction.Login.HiddenFieldVisibilityClicked,
|
||||
) {
|
||||
onLoginContent { login ->
|
||||
if (login.requiresReprompt) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog)
|
||||
}
|
||||
return@onLoginContent
|
||||
}
|
||||
//endregion Login Type Handlers
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = login.copy(
|
||||
customFields = login.customFields.map { customField ->
|
||||
if (customField == action.field) {
|
||||
action.field.copy(isVisible = action.isVisible)
|
||||
} else {
|
||||
customField
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
//region Internal Type Handlers
|
||||
|
||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||
when (action) {
|
||||
is VaultItemAction.Internal.PasswordBreachReceive -> handlePasswordBreachReceive(action)
|
||||
is VaultItemAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
|
||||
is VaultItemAction.Internal.VerifyPasswordReceive -> handleVerifyPasswordReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -372,24 +410,24 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is VerifyPasswordResult.Success -> {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
viewState = when (val viewState = state.viewState) {
|
||||
is VaultItemState.ViewState.Content.Login -> viewState.copy(
|
||||
requiresReprompt = !result.isVerified,
|
||||
)
|
||||
|
||||
is VaultItemState.ViewState.Error -> viewState
|
||||
|
||||
VaultItemState.ViewState.Loading -> viewState
|
||||
},
|
||||
)
|
||||
onContent { content ->
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
viewState = content.copy(
|
||||
common = content.common.copy(
|
||||
requiresReprompt = !result.isVerified,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Internal Type Handlers
|
||||
|
||||
private inline fun onContent(
|
||||
crossinline block: (VaultItemState.ViewState.Content) -> Unit,
|
||||
) {
|
||||
|
@ -397,9 +435,18 @@ class VaultItemViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private inline fun onLoginContent(
|
||||
crossinline block: (VaultItemState.ViewState.Content.Login) -> Unit,
|
||||
crossinline block: (
|
||||
VaultItemState.ViewState.Content,
|
||||
VaultItemState.ViewState.Content.ItemType.Login,
|
||||
) -> Unit,
|
||||
) {
|
||||
(state.viewState as? VaultItemState.ViewState.Content.Login)?.let(block)
|
||||
(state.viewState as? VaultItemState.ViewState.Content)
|
||||
?.let { content ->
|
||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Login)
|
||||
?.let { loginContent ->
|
||||
block(content, loginContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -434,126 +481,142 @@ data class VaultItemState(
|
|||
/**
|
||||
* Represents a loaded content state for the [VaultItemScreen].
|
||||
*/
|
||||
sealed class Content : ViewState() {
|
||||
@Parcelize
|
||||
data class Content(
|
||||
val common: Common,
|
||||
val type: ItemType,
|
||||
) : ViewState() {
|
||||
|
||||
/**
|
||||
* The name of the cipher.
|
||||
*/
|
||||
abstract val name: String
|
||||
|
||||
/**
|
||||
* A formatted date string indicating when the cipher was last updated.
|
||||
*/
|
||||
abstract val lastUpdated: String
|
||||
|
||||
/**
|
||||
* An integer indicating how many times the password has been changed.
|
||||
*/
|
||||
abstract val passwordHistoryCount: Int?
|
||||
|
||||
/**
|
||||
* Contains general notes taken by the user.
|
||||
*/
|
||||
abstract val notes: String?
|
||||
|
||||
/**
|
||||
* Indicates if the user has subscribed to a premium account or not.
|
||||
*/
|
||||
abstract val isPremiumUser: Boolean
|
||||
|
||||
/**
|
||||
* A list of custom fields that user has added.
|
||||
*/
|
||||
abstract val customFields: List<Custom>
|
||||
|
||||
/**
|
||||
* Indicates if a master password prompt is required to view secure fields.
|
||||
*/
|
||||
abstract val requiresReprompt: Boolean
|
||||
|
||||
/**
|
||||
* Represents a loaded content state for the [VaultItemScreen] when displaying a
|
||||
* login cipher.
|
||||
* Content data that is common for all item types.
|
||||
*
|
||||
* @property name The name of the item.
|
||||
* @property lastUpdated A formatted date string indicating when the item was last
|
||||
* updated.
|
||||
* @property notes Contains general notes taken by the user.
|
||||
* @property isPremiumUser Indicates if the user has subscribed to a premium account.
|
||||
* @property customFields A list of custom fields that user has added.
|
||||
* @property requiresReprompt Indicates if a master password prompt is required to view
|
||||
* secure fields.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Login(
|
||||
override val name: String,
|
||||
override val lastUpdated: String,
|
||||
override val passwordHistoryCount: Int?,
|
||||
override val notes: String?,
|
||||
override val isPremiumUser: Boolean,
|
||||
override val customFields: List<Custom>,
|
||||
override val requiresReprompt: Boolean,
|
||||
val username: String?,
|
||||
val passwordData: PasswordData?,
|
||||
val uris: List<UriData>,
|
||||
val passwordRevisionDate: String?,
|
||||
val totp: String?,
|
||||
) : Content()
|
||||
data class Common(
|
||||
val name: String,
|
||||
val lastUpdated: String,
|
||||
val notes: String?,
|
||||
val isPremiumUser: Boolean,
|
||||
val customFields: List<Custom>,
|
||||
val requiresReprompt: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
* Represents a custom field, TextField, HiddenField, BooleanField, or LinkedField.
|
||||
*/
|
||||
sealed class Custom : Parcelable {
|
||||
/**
|
||||
* Represents the data for displaying a custom text field.
|
||||
*/
|
||||
@Parcelize
|
||||
data class TextField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val isCopyable: Boolean,
|
||||
) : Custom()
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom hidden text field.
|
||||
*/
|
||||
@Parcelize
|
||||
data class HiddenField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val isCopyable: Boolean,
|
||||
val isVisible: Boolean,
|
||||
) : Custom()
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom boolean property field.
|
||||
*/
|
||||
@Parcelize
|
||||
data class BooleanField(
|
||||
val name: String,
|
||||
val value: Boolean,
|
||||
) : Custom()
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom linked field.
|
||||
*/
|
||||
@Parcelize
|
||||
data class LinkedField(
|
||||
val vaultLinkedFieldType: VaultLinkedFieldType,
|
||||
val name: String,
|
||||
) : Custom()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for the password data, this includes the [password] itself and whether it
|
||||
* should be visible.
|
||||
* Content data specific to an item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class PasswordData(
|
||||
val password: String,
|
||||
val isVisible: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* A wrapper for URI data, including the [uri] itself and whether it is copyable and
|
||||
* launchable.
|
||||
*/
|
||||
@Parcelize
|
||||
data class UriData(
|
||||
val uri: String,
|
||||
val isCopyable: Boolean,
|
||||
val isLaunchable: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Represents a custom field, TextField, HiddenField, BooleanField, or LinkedField.
|
||||
*/
|
||||
sealed class Custom : Parcelable {
|
||||
/**
|
||||
* Represents the data for displaying a custom text field.
|
||||
*/
|
||||
@Parcelize
|
||||
data class TextField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val isCopyable: Boolean,
|
||||
) : Custom()
|
||||
sealed class ItemType : Parcelable {
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom hidden text field.
|
||||
* Represents the `Login` item type.
|
||||
*
|
||||
* @property username The username required for the login item.
|
||||
* @property passwordData The password required for the login item.
|
||||
* @property passwordHistoryCount An integer indicating how many times the password
|
||||
* has been changed.
|
||||
* @property uris The URI associated with the login item.
|
||||
* @property passwordRevisionDate
|
||||
* @property totp
|
||||
*/
|
||||
@Parcelize
|
||||
data class HiddenField(
|
||||
val name: String,
|
||||
val value: String,
|
||||
val isCopyable: Boolean,
|
||||
val isVisible: Boolean,
|
||||
) : Custom()
|
||||
data class Login(
|
||||
val username: String?,
|
||||
val passwordData: PasswordData?,
|
||||
val passwordHistoryCount: Int?,
|
||||
val uris: List<UriData>,
|
||||
val passwordRevisionDate: String?,
|
||||
val totp: String?,
|
||||
) : ItemType() {
|
||||
|
||||
/**
|
||||
* A wrapper for the password data, this includes the [password] itself
|
||||
* and whether it should be visible.
|
||||
*/
|
||||
@Parcelize
|
||||
data class PasswordData(
|
||||
val password: String,
|
||||
val isVisible: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* A wrapper for URI data, including the [uri] itself and whether it is
|
||||
* copyable and launch-able.
|
||||
*/
|
||||
@Parcelize
|
||||
data class UriData(
|
||||
val uri: String,
|
||||
val isCopyable: Boolean,
|
||||
val isLaunchable: Boolean,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom boolean property field.
|
||||
* Represents the `SecureNote` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class BooleanField(
|
||||
val name: String,
|
||||
val value: Boolean,
|
||||
) : Custom()
|
||||
data object SecureNote : ItemType()
|
||||
|
||||
/**
|
||||
* Represents the data for displaying a custom linked field.
|
||||
* Represents the `Identity` item type.
|
||||
*/
|
||||
@Parcelize
|
||||
data class LinkedField(
|
||||
val vaultLinkedFieldType: VaultLinkedFieldType,
|
||||
val name: String,
|
||||
) : Custom()
|
||||
data object Identity : ItemType()
|
||||
|
||||
/**
|
||||
* Represents the `Card` item type.
|
||||
*/
|
||||
data object Card : ItemType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -631,102 +694,116 @@ sealed class VaultItemEvent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents a set of actions related view a vault item.
|
||||
* Represents a set of actions related to viewing a vault item.
|
||||
* Each subclass of this sealed class denotes a distinct action that can be taken.
|
||||
*/
|
||||
sealed class VaultItemAction {
|
||||
/**
|
||||
* The user has clicked the close button.
|
||||
*/
|
||||
data object CloseClick : VaultItemAction()
|
||||
|
||||
/**
|
||||
* The user has clicked to dismiss the dialog.
|
||||
* Represents actions common across all item types.
|
||||
*/
|
||||
data object DismissDialogClick : VaultItemAction()
|
||||
sealed class Common : VaultItemAction() {
|
||||
|
||||
/**
|
||||
* The user has clicked the edit button.
|
||||
*/
|
||||
data object EditClick : VaultItemAction()
|
||||
|
||||
/**
|
||||
* The user has submitted their master password.
|
||||
*/
|
||||
data class MasterPasswordSubmit(
|
||||
val masterPassword: String,
|
||||
) : VaultItemAction()
|
||||
|
||||
/**
|
||||
* The user has clicked the refresh button.
|
||||
*/
|
||||
data object RefreshClick : VaultItemAction()
|
||||
|
||||
/**
|
||||
* Models actions that are associated with the [VaultItemState.ViewState.Content.Login] state.
|
||||
*/
|
||||
sealed class Login : VaultItemAction() {
|
||||
/**
|
||||
* The user has clicked the check for breach button.
|
||||
* The user has clicked the close button.
|
||||
*/
|
||||
data object CheckForBreachClick : Login()
|
||||
data object CloseClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to dismiss the dialog.
|
||||
*/
|
||||
data object DismissDialogClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked the edit button.
|
||||
*/
|
||||
data object EditClick : Common()
|
||||
|
||||
/**
|
||||
* The user has submitted their master password.
|
||||
*/
|
||||
data class MasterPasswordSubmit(
|
||||
val masterPassword: String,
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked the refresh button.
|
||||
*/
|
||||
data object RefreshClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for a custom hidden field.
|
||||
*/
|
||||
data class CopyCustomHiddenFieldClick(
|
||||
val field: String,
|
||||
) : Login()
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for a custom text field.
|
||||
*/
|
||||
data class CopyCustomTextFieldClick(
|
||||
val field: String,
|
||||
) : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the password.
|
||||
*/
|
||||
data object CopyPasswordClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for a URI.
|
||||
*/
|
||||
data class CopyUriClick(
|
||||
val uri: String,
|
||||
) : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the username.
|
||||
*/
|
||||
data object CopyUsernameClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the launch button for a URI.
|
||||
*/
|
||||
data class LaunchClick(
|
||||
val uri: String,
|
||||
) : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the password history text.
|
||||
*/
|
||||
data object PasswordHistoryClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked to display the password.
|
||||
*/
|
||||
data class PasswordVisibilityClicked(
|
||||
val isVisible: Boolean,
|
||||
) : Login()
|
||||
) : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked to display the a hidden field.
|
||||
*/
|
||||
data class HiddenFieldVisibilityClicked(
|
||||
val field: VaultItemState.ViewState.Content.Custom.HiddenField,
|
||||
val field: VaultItemState.ViewState.Content.Common.Custom.HiddenField,
|
||||
val isVisible: Boolean,
|
||||
) : Login()
|
||||
) : Common()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions specific to an item type.
|
||||
*/
|
||||
sealed class ItemType : VaultItemAction() {
|
||||
|
||||
/**
|
||||
* Represents actions specific to the Login type.
|
||||
*/
|
||||
sealed class Login : ItemType() {
|
||||
/**
|
||||
* The user has clicked the check for breach button.
|
||||
*/
|
||||
data object CheckForBreachClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the password.
|
||||
*/
|
||||
data object CopyPasswordClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for a URI.
|
||||
*/
|
||||
data class CopyUriClick(
|
||||
val uri: String,
|
||||
) : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the username.
|
||||
*/
|
||||
data object CopyUsernameClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the launch button for a URI.
|
||||
*/
|
||||
data class LaunchClick(
|
||||
val uri: String,
|
||||
) : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked the password history text.
|
||||
*/
|
||||
data object PasswordHistoryClick : Login()
|
||||
|
||||
/**
|
||||
* The user has clicked to display the password.
|
||||
*/
|
||||
data class PasswordVisibilityClicked(
|
||||
val isVisible: Boolean,
|
||||
) : Login()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions common within the context of viewing
|
||||
* items in a vault.
|
||||
*
|
||||
* @property onCopyCustomHiddenField
|
||||
* @property onCopyCustomTextField
|
||||
* @property onShowHiddenFieldClick
|
||||
*/
|
||||
class VaultCommonItemTypeHandlers(
|
||||
val onRefreshClick: () -> Unit,
|
||||
val onCopyCustomHiddenField: (String) -> Unit,
|
||||
val onCopyCustomTextField: (String) -> Unit,
|
||||
val onShowHiddenFieldClick: (
|
||||
VaultItemState.ViewState.Content.Common.Custom.HiddenField,
|
||||
Boolean,
|
||||
) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [VaultCommonItemTypeHandlers] by binding actions
|
||||
* to the provided [VaultItemViewModel].
|
||||
*/
|
||||
fun create(
|
||||
viewModel: VaultItemViewModel,
|
||||
): VaultCommonItemTypeHandlers =
|
||||
VaultCommonItemTypeHandlers(
|
||||
onRefreshClick = {
|
||||
viewModel.trySendAction(VaultItemAction.Common.RefreshClick)
|
||||
},
|
||||
onCopyCustomHiddenField = {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(it))
|
||||
},
|
||||
onCopyCustomTextField = {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomTextFieldClick(it))
|
||||
},
|
||||
onShowHiddenFieldClick = { customField, isVisible ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
isVisible = isVisible,
|
||||
field = customField,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of viewing identity
|
||||
* items in a vault.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class VaultLoginItemTypeHandlers(
|
||||
val onCheckForBreachClick: () -> Unit,
|
||||
val onCopyPasswordClick: () -> Unit,
|
||||
val onCopyUriClick: (String) -> Unit,
|
||||
val onCopyUsernameClick: () -> Unit,
|
||||
val onLaunchUriClick: (String) -> Unit,
|
||||
val onPasswordHistoryClick: () -> Unit,
|
||||
val onShowPasswordClick: (isVisible: Boolean) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates the [VaultLoginItemTypeHandlers] using the [viewModel] to send desired actions.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
fun create(
|
||||
viewModel: VaultItemViewModel,
|
||||
): VaultLoginItemTypeHandlers =
|
||||
VaultLoginItemTypeHandlers(
|
||||
onCheckForBreachClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CheckForBreachClick)
|
||||
},
|
||||
onCopyPasswordClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
},
|
||||
onCopyUriClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(it))
|
||||
},
|
||||
onCopyUsernameClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
},
|
||||
onLaunchUriClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.LaunchClick(it))
|
||||
},
|
||||
onPasswordHistoryClick = {
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
},
|
||||
onShowPasswordClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Login.PasswordVisibilityClicked(it),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -7,11 +7,10 @@ import com.bitwarden.core.FieldType
|
|||
import com.bitwarden.core.FieldView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orZeroWidthSpace
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.TimeZone
|
||||
|
||||
|
@ -26,70 +25,77 @@ private val dateTimeFormatter
|
|||
fun CipherView.toViewState(
|
||||
isPremiumUser: Boolean,
|
||||
): VaultItemState.ViewState =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
name = name,
|
||||
isPremiumUser = isPremiumUser,
|
||||
requiresReprompt = reprompt == CipherRepromptType.PASSWORD,
|
||||
customFields = fields.orEmpty().map { it.toCustomField() },
|
||||
lastUpdated = dateTimeFormatter.format(revisionDate),
|
||||
notes = notes,
|
||||
),
|
||||
type = when (type) {
|
||||
CipherType.LOGIN -> {
|
||||
val loginValues = requireNotNull(login)
|
||||
VaultItemState.ViewState.Content.ItemType.Login(
|
||||
username = loginValues.username,
|
||||
passwordData = loginValues.password?.let {
|
||||
VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = it,
|
||||
isVisible = false,
|
||||
)
|
||||
},
|
||||
uris = loginValues.uris.orEmpty().map { it.toUriData() },
|
||||
passwordRevisionDate = loginValues.passwordRevisionDate?.let {
|
||||
dateTimeFormatter.format(it)
|
||||
},
|
||||
passwordHistoryCount = passwordHistory?.count(),
|
||||
totp = loginValues.totp,
|
||||
)
|
||||
}
|
||||
|
||||
CipherType.SECURE_NOTE -> {
|
||||
VaultItemState.ViewState.Content.ItemType.SecureNote
|
||||
}
|
||||
|
||||
CipherType.CARD -> {
|
||||
VaultItemState.ViewState.Content.ItemType.Card
|
||||
}
|
||||
|
||||
CipherType.IDENTITY -> {
|
||||
VaultItemState.ViewState.Content.ItemType.Identity
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun FieldView.toCustomField(): VaultItemState.ViewState.Content.Common.Custom =
|
||||
when (type) {
|
||||
CipherType.LOGIN -> {
|
||||
val loginValues = requireNotNull(this.login)
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
name = this.name,
|
||||
username = loginValues.username,
|
||||
passwordData = loginValues.password?.let {
|
||||
VaultItemState.ViewState.Content.PasswordData(password = it, isVisible = false)
|
||||
},
|
||||
isPremiumUser = isPremiumUser,
|
||||
requiresReprompt = this.reprompt == CipherRepromptType.PASSWORD,
|
||||
customFields = this.fields.orEmpty().map { it.toCustomField() },
|
||||
uris = loginValues.uris.orEmpty().map { it.toUriData() },
|
||||
lastUpdated = dateTimeFormatter.format(this.revisionDate),
|
||||
passwordRevisionDate = loginValues.passwordRevisionDate?.let {
|
||||
dateTimeFormatter.format(it)
|
||||
},
|
||||
passwordHistoryCount = this.passwordHistory?.count(),
|
||||
totp = loginValues.totp,
|
||||
notes = this.notes,
|
||||
)
|
||||
}
|
||||
|
||||
CipherType.SECURE_NOTE -> VaultItemState.ViewState.Error(
|
||||
message = "Not yet implemented.".asText(),
|
||||
)
|
||||
|
||||
CipherType.CARD -> VaultItemState.ViewState.Error(
|
||||
message = "Not yet implemented.".asText(),
|
||||
)
|
||||
|
||||
CipherType.IDENTITY -> VaultItemState.ViewState.Error(
|
||||
message = "Not yet implemented.".asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun FieldView.toCustomField(): VaultItemState.ViewState.Content.Custom =
|
||||
when (type) {
|
||||
FieldType.TEXT -> VaultItemState.ViewState.Content.Custom.TextField(
|
||||
FieldType.TEXT -> VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = name.orEmpty(),
|
||||
value = value.orZeroWidthSpace(),
|
||||
isCopyable = !value.isNullOrBlank(),
|
||||
)
|
||||
|
||||
FieldType.HIDDEN -> VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
FieldType.HIDDEN -> VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = name.orEmpty(),
|
||||
value = value.orZeroWidthSpace(),
|
||||
isCopyable = !value.isNullOrBlank(),
|
||||
isVisible = false,
|
||||
)
|
||||
|
||||
FieldType.BOOLEAN -> VaultItemState.ViewState.Content.Custom.BooleanField(
|
||||
FieldType.BOOLEAN -> VaultItemState.ViewState.Content.Common.Custom.BooleanField(
|
||||
name = name.orEmpty(),
|
||||
value = value?.toBoolean() ?: false,
|
||||
)
|
||||
|
||||
FieldType.LINKED -> VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
FieldType.LINKED -> VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.fromId(requireNotNull(linkedId)),
|
||||
name = name.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun LoginUriView.toUriData() =
|
||||
VaultItemState.ViewState.Content.UriData(
|
||||
VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = uri.orZeroWidthSpace(),
|
||||
isCopyable = !uri.isNullOrBlank(),
|
||||
isLaunchable = !uri.isNullOrBlank(),
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.junit.Assert.assertTrue
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultItemScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
|
@ -79,7 +80,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithContentDescription(label = "Close").performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.CloseClick)
|
||||
viewModel.trySendAction(VaultItemAction.Common.CloseClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,7 +144,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.DismissDialogClick)
|
||||
viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,15 +192,26 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.MasterPasswordSubmit(enteredPassword))
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit(enteredPassword))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on username copy click should send CopyUsernameClick`() {
|
||||
val username = "username1234"
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(username = username))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = VaultItemState.ViewState.Content.ItemType.Login(
|
||||
username = username,
|
||||
passwordData = null,
|
||||
passwordHistoryCount = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -209,18 +221,24 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUsernameClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on breach check click should send CheckForBreachClick`() {
|
||||
val passwordData = VaultItemState.ViewState.Content.PasswordData(
|
||||
val passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "12345",
|
||||
isVisible = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(passwordData = passwordData))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
passwordData = passwordData,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -230,7 +248,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CheckForBreachClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CheckForBreachClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,18 +263,24 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordVisibilityClicked(true))
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordVisibilityClicked(true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on copy password click should send CopyPasswordClick`() {
|
||||
val passwordData = VaultItemState.ViewState.Content.PasswordData(
|
||||
val passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "12345",
|
||||
isVisible = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(passwordData = passwordData))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
passwordData = passwordData,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -266,19 +290,25 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyPasswordClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, launch uri button should be displayed according to state`() {
|
||||
val uriData = VaultItemState.ViewState.Content.UriData(
|
||||
val uriData = VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(uris = listOf(uriData)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
uris = listOf(uriData),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -287,12 +317,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.filterToOne(hasContentDescription("Launch"))
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
uris = listOf(uriData.copy(isLaunchable = false)),
|
||||
),
|
||||
)
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) {
|
||||
copy(uris = listOf(uriData.copy(isLaunchable = false)))
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -304,13 +332,19 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `in login state, copy uri button should be displayed according to state`() {
|
||||
val uriData = VaultItemState.ViewState.Content.UriData(
|
||||
val uriData = VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(uris = listOf(uriData)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
uris = listOf(uriData),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -319,12 +353,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.filterToOne(hasContentDescription("Copy"))
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
uris = listOf(uriData.copy(isCopyable = false)),
|
||||
),
|
||||
)
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(uris = listOf(uriData.copy(isCopyable = false))) }
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -336,13 +366,19 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `in login state, on launch URI click should send LaunchClick`() {
|
||||
val uriData = VaultItemState.ViewState.Content.UriData(
|
||||
val uriData = VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(uris = listOf(uriData)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
uris = listOf(uriData),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -352,19 +388,25 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.LaunchClick(uriData.uri))
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.LaunchClick(uriData.uri))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on copy URI click should send CopyUriClick`() {
|
||||
val uriData = VaultItemState.ViewState.Content.UriData(
|
||||
val uriData = VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(uris = listOf(uriData)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
uris = listOf(uriData),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -374,20 +416,26 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUriClick(uriData.uri))
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(uriData.uri))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on show hidden field click should send HiddenFieldVisibilityClicked`() {
|
||||
val textField = VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
fun `on show hidden field click should send HiddenFieldVisibilityClicked`() {
|
||||
val textField = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "hidden password",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(customFields = listOf(textField)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = EMPTY_COMMON.copy(
|
||||
customFields = listOf(textField),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -398,7 +446,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.HiddenFieldVisibilityClicked(
|
||||
VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
field = textField,
|
||||
isVisible = true,
|
||||
),
|
||||
|
@ -407,15 +455,21 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, copy hidden field button should be displayed according to state`() {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
fun `copy hidden field button should be displayed according to state`() {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "hidden password",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(customFields = listOf(hiddenField)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = EMPTY_COMMON.copy(
|
||||
customFields = listOf(hiddenField),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -424,12 +478,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.filterToOne(hasContentDescription("Copy"))
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
customFields = listOf(hiddenField.copy(isCopyable = false)),
|
||||
),
|
||||
)
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) {
|
||||
copy(customFields = listOf(hiddenField.copy(isCopyable = false)))
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -440,15 +492,21 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on copy hidden field click should send CopyCustomHiddenFieldClick`() {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
fun `on copy hidden field click should send CopyCustomHiddenFieldClick`() {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "hidden password",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(customFields = listOf(hiddenField)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = EMPTY_COMMON.copy(
|
||||
customFields = listOf(hiddenField),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -459,20 +517,26 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.CopyCustomHiddenFieldClick(hiddenField.value),
|
||||
VaultItemAction.Common.CopyCustomHiddenFieldClick(hiddenField.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, on copy text field click should send CopyCustomTextFieldClick`() {
|
||||
val textField = VaultItemState.ViewState.Content.Custom.TextField(
|
||||
fun `on copy text field click should send CopyCustomTextFieldClick`() {
|
||||
val textField = VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(customFields = listOf(textField)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = EMPTY_COMMON.copy(
|
||||
customFields = listOf(textField),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -483,20 +547,26 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.CopyCustomTextFieldClick(textField.value),
|
||||
VaultItemAction.Common.CopyCustomTextFieldClick(textField.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, text field copy button should be displayed according to state`() {
|
||||
val textField = VaultItemState.ViewState.Content.Custom.TextField(
|
||||
fun `text field copy button should be displayed according to state`() {
|
||||
val textField = VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(customFields = listOf(textField)))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = EMPTY_COMMON.copy(
|
||||
customFields = listOf(textField),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -505,12 +575,10 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
.filterToOne(hasContentDescription("Copy"))
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
customFields = listOf(textField.copy(isCopyable = false)),
|
||||
),
|
||||
)
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) {
|
||||
copy(customFields = listOf(textField.copy(isCopyable = false)))
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
|
@ -522,15 +590,21 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `in login state, on password history click should send PasswordHistoryClick`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = EMPTY_LOGIN_VIEW_STATE.copy(passwordHistoryCount = 5))
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
type = EMPTY_LOGIN_TYPE.copy(
|
||||
passwordHistoryCount = 5,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTextAfterScroll("5")
|
||||
composeTestRule.onNodeWithText("5").performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordHistoryClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -551,7 +625,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
composeTestRule.onNodeWithContentDescription("Edit item").performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemAction.EditClick)
|
||||
viewModel.trySendAction(VaultItemAction.Common.EditClick)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,8 +664,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(username).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(username = null))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(username = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(username)
|
||||
|
@ -604,8 +678,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithTextAfterScroll("URI").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("www.example.com").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(uris = emptyList()))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(uris = emptyList()) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist("URIs")
|
||||
|
@ -614,13 +688,13 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, notes should be displayed according to state`() {
|
||||
fun `notes should be displayed according to state`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) }
|
||||
composeTestRule.onFirstNodeWithTextAfterScroll("Notes").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("Lots of notes").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(notes = null))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) { copy(notes = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist("Notes")
|
||||
|
@ -628,7 +702,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `in login state, custom views should be displayed according to state`() {
|
||||
fun `custom views should be displayed according to state`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll("Custom fields").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("text").assertIsDisplayed()
|
||||
|
@ -638,8 +712,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithTextAfterScroll("linked username").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("linked password").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(customFields = emptyList()))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) { copy(customFields = emptyList()) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist("Custom fields")
|
||||
|
@ -657,8 +731,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithTextAfterScroll("Password updated: ").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("4/14/83 3:56 PM").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(passwordRevisionDate = null))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(passwordRevisionDate = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist("Password updated: ")
|
||||
|
@ -671,8 +745,8 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNodeWithTextAfterScroll("Password history: ").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("1").assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(viewState = DEFAULT_LOGIN_VIEW_STATE.copy(passwordHistoryCount = null))
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateLoginType(currentState) { copy(passwordHistoryCount = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist("Password history: ")
|
||||
|
@ -680,6 +754,49 @@ class VaultItemScreenTest : BaseComposeTest() {
|
|||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateLoginType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Login.() ->
|
||||
VaultItemState.ViewState.Content.ItemType.Login,
|
||||
): VaultItemState {
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultItemState.ViewState.Content -> {
|
||||
when (val type = viewState.type) {
|
||||
is VaultItemState.ViewState.Content.ItemType.Login -> {
|
||||
viewState.copy(
|
||||
type = type.transform(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
private fun updateCommonContent(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.Common.()
|
||||
-> VaultItemState.ViewState.Content.Common,
|
||||
): VaultItemState {
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultItemState.ViewState.Content ->
|
||||
viewState.copy(common = viewState.common.transform())
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
//endregion Helper functions
|
||||
|
||||
private const val VAULT_ITEM_ID = "vault_item_id"
|
||||
|
||||
private val DEFAULT_STATE: VaultItemState = VaultItemState(
|
||||
|
@ -688,67 +805,81 @@ private val DEFAULT_STATE: VaultItemState = VaultItemState(
|
|||
dialog = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content.Login =
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
name = "login cipher",
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
passwordHistoryCount = 1,
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "hidden password",
|
||||
isCopyable = true,
|
||||
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
type = VaultItemState.ViewState.Content.ItemType.Login(
|
||||
passwordHistoryCount = 1,
|
||||
username = "the username",
|
||||
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "the password",
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
),
|
||||
),
|
||||
passwordRevisionDate = "4/14/83 3:56 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
),
|
||||
requiresReprompt = true,
|
||||
username = "the username",
|
||||
passwordData = VaultItemState.ViewState.Content.PasswordData(
|
||||
password = "the password",
|
||||
isVisible = false,
|
||||
),
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
name = "login cipher",
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "hidden password",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
),
|
||||
),
|
||||
requiresReprompt = true,
|
||||
),
|
||||
passwordRevisionDate = "4/14/83 3:56 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
)
|
||||
|
||||
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content.Login =
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
private val EMPTY_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "login cipher",
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
passwordHistoryCount = null,
|
||||
notes = null,
|
||||
isPremiumUser = true,
|
||||
customFields = emptyList(),
|
||||
requiresReprompt = true,
|
||||
)
|
||||
|
||||
private val EMPTY_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login =
|
||||
VaultItemState.ViewState.Content.ItemType.Login(
|
||||
username = null,
|
||||
passwordData = null,
|
||||
passwordHistoryCount = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
)
|
||||
|
||||
private val EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = EMPTY_COMMON,
|
||||
type = EMPTY_LOGIN_TYPE,
|
||||
)
|
||||
|
|
|
@ -71,114 +71,264 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
assertEquals(state, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.CloseClick)
|
||||
assertEquals(VaultItemEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
@Nested
|
||||
inner class CommonActions {
|
||||
private lateinit var viewModel: VaultItemViewModel
|
||||
|
||||
@Test
|
||||
fun `on DismissDialogClick should clear the dialog state`() = runTest {
|
||||
val initialState = DEFAULT_STATE.copy(dialog = VaultItemState.DialogState.Loading)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.DismissDialogClick)
|
||||
assertEquals(initialState.copy(dialog = null), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should do nothing when ViewState is not Content`() = runTest {
|
||||
val initialState = DEFAULT_STATE
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.EditClick)
|
||||
expectNoEvents()
|
||||
}
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should prompt for master password when required`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.EditClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should navigate password is not required`() = runTest {
|
||||
val loginViewState = DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.EditClick)
|
||||
assertEquals(VaultItemEvent.NavigateToEdit(VAULT_ITEM_ID), awaitItem())
|
||||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on MasterPasswordSubmit should verify the password`() = runTest {
|
||||
val loginViewState = DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.MasterPasswordSubmit("password"))
|
||||
assertEquals(loginState.copy(dialog = VaultItemState.DialogState.Loading), awaitItem())
|
||||
assertEquals(
|
||||
loginState.copy(viewState = loginViewState.copy(requiresReprompt = false)),
|
||||
awaitItem(),
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE),
|
||||
)
|
||||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
@Test
|
||||
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CloseClick)
|
||||
assertEquals(VaultItemEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on RefreshClick should sync`() = runTest {
|
||||
every { vaultRepo.sync() } just runs
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
@Test
|
||||
fun `on DismissDialogClick should clear the dialog state`() = runTest {
|
||||
val initialState = DEFAULT_STATE.copy(dialog = VaultItemState.DialogState.Loading)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.RefreshClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
vaultRepo.sync()
|
||||
viewModel.trySendAction(VaultItemAction.Common.DismissDialogClick)
|
||||
assertEquals(initialState.copy(dialog = null), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should do nothing when ViewState is not Content`() = runTest {
|
||||
val initialState = DEFAULT_STATE
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.EditClick)
|
||||
expectNoEvents()
|
||||
}
|
||||
assertEquals(initialState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should prompt for master password when required`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.Common.EditClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on EditClick should navigate password is not required`() = runTest {
|
||||
val loginViewState = createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.EditClick)
|
||||
assertEquals(VaultItemEvent.NavigateToEdit(VAULT_ITEM_ID), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on MasterPasswordSubmit should verify the password`() = runTest {
|
||||
val loginViewState = createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val viewModel = createViewModel(state = loginState)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.Common.MasterPasswordSubmit("password"))
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.Loading),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
viewState = loginViewState.copy(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on RefreshClick should sync`() = runTest {
|
||||
every { vaultRepo.sync() } just runs
|
||||
val viewModel = createViewModel(state = DEFAULT_STATE)
|
||||
|
||||
viewModel.trySendAction(VaultItemAction.Common.RefreshClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
vaultRepo.sync()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyCustomHiddenFieldClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick("field"))
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyCustomHiddenFieldClick should emit CopyToClipboard when re-prompt is not required`() =
|
||||
runTest {
|
||||
val field = "field"
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
}
|
||||
.returns(
|
||||
createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomHiddenFieldClick(field))
|
||||
assertEquals(
|
||||
VaultItemEvent.CopyToClipboard(field.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyCustomTextFieldClick should emit CopyToClipboard`() = runTest {
|
||||
val field = "field"
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Common.CopyCustomTextFieldClick(field))
|
||||
assertEquals(VaultItemEvent.CopyToClipboard(field.asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HiddenFieldVisibilityClicked should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
field = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
isVisible = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HiddenFieldVisibilityClicked should update hidden field visibility when re-prompt is not required`() =
|
||||
runTest {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
val loginViewState = DEFAULT_EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
common = DEFAULT_EMPTY_LOGIN_VIEW_STATE.common.copy(
|
||||
requiresReprompt = false,
|
||||
customFields = listOf(hiddenField),
|
||||
),
|
||||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Common.HiddenFieldVisibilityClicked(
|
||||
field = hiddenField,
|
||||
isVisible = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
viewState = loginViewState.copy(
|
||||
common = DEFAULT_EMPTY_LOGIN_VIEW_STATE.common.copy(
|
||||
requiresReprompt = false,
|
||||
customFields = listOf(hiddenField.copy(isVisible = true)),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
@ -188,17 +338,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@BeforeEach
|
||||
fun setup() {
|
||||
viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE),
|
||||
state = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CheckForBreachClick should process a password`() = runTest {
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val breachCount = 5
|
||||
coEvery {
|
||||
authRepo.getPasswordBreachCount(password = DEFAULT_LOGIN_PASSWORD)
|
||||
|
@ -206,7 +356,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(loginState, awaitItem())
|
||||
viewModel.trySendAction(VaultItemAction.Login.CheckForBreachClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CheckForBreachClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.Loading),
|
||||
awaitItem(),
|
||||
|
@ -232,14 +382,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on CopyPasswordClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyPasswordClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -256,12 +406,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} returns DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
}
|
||||
.returns(
|
||||
createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyPasswordClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyPasswordClick)
|
||||
assertEquals(
|
||||
VaultItemEvent.CopyToClipboard(DEFAULT_LOGIN_PASSWORD.asText()),
|
||||
awaitItem(),
|
||||
|
@ -273,67 +428,11 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyCustomHiddenFieldClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyCustomHiddenFieldClick("field"))
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on CopyCustomHiddenFieldClick should emit CopyToClipboard when re-prompt is not required`() =
|
||||
runTest {
|
||||
val field = "field"
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} returns DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyCustomHiddenFieldClick(field))
|
||||
assertEquals(
|
||||
VaultItemEvent.CopyToClipboard(field.asText()),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyCustomTextFieldClick should emit CopyToClipboard`() = runTest {
|
||||
val field = "field"
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyCustomTextFieldClick(field))
|
||||
assertEquals(VaultItemEvent.CopyToClipboard(field.asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyUriClick should emit CopyToClipboard`() = runTest {
|
||||
val uri = "uri"
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUriClick(uri))
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUriClick(uri))
|
||||
assertEquals(VaultItemEvent.CopyToClipboard(uri.asText()), awaitItem())
|
||||
}
|
||||
}
|
||||
|
@ -341,14 +440,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on CopyUsernameClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUsernameClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -365,12 +464,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} returns DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
}
|
||||
.returns(
|
||||
createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.CopyUsernameClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.CopyUsernameClick)
|
||||
assertEquals(
|
||||
VaultItemEvent.CopyToClipboard(DEFAULT_LOGIN_USERNAME.asText()),
|
||||
awaitItem(),
|
||||
|
@ -386,7 +490,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
fun `on LaunchClick should emit NavigateToUri`() = runTest {
|
||||
val uri = "uri"
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.LaunchClick(uri))
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.LaunchClick(uri))
|
||||
assertEquals(VaultItemEvent.NavigateToUri(uri), awaitItem())
|
||||
}
|
||||
}
|
||||
|
@ -394,14 +498,14 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on PasswordHistoryClick should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordHistoryClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -419,12 +523,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
val mockCipherView = mockk<CipherView> {
|
||||
every {
|
||||
toViewState(isPremiumUser = true)
|
||||
} returns DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
}
|
||||
.returns(
|
||||
createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordHistoryClick)
|
||||
viewModel.trySendAction(VaultItemAction.ItemType.Login.PasswordHistoryClick)
|
||||
assertEquals(
|
||||
VaultItemEvent.NavigateToPasswordHistory(VAULT_ITEM_ID),
|
||||
awaitItem(),
|
||||
|
@ -440,14 +549,18 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on PasswordVisibilityClicked should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordVisibilityClicked(true))
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
|
||||
true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -462,74 +575,8 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
@Test
|
||||
fun `on PasswordVisibilityClicked should update password visibility when re-prompt is not required`() =
|
||||
runTest {
|
||||
val loginViewState = DEFAULT_LOGIN_VIEW_STATE.copy(requiresReprompt = false)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns loginViewState
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(VaultItemAction.Login.PasswordVisibilityClicked(true))
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
viewState = loginViewState.copy(
|
||||
passwordData = loginViewState.passwordData!!.copy(isVisible = true),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HiddenFieldVisibilityClicked should show password dialog when re-prompt is required`() =
|
||||
runTest {
|
||||
val loginState = DEFAULT_STATE.copy(viewState = DEFAULT_LOGIN_VIEW_STATE)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
every { toViewState(isPremiumUser = true) } returns DEFAULT_LOGIN_VIEW_STATE
|
||||
}
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.HiddenFieldVisibilityClicked(
|
||||
field = VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
isVisible = true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(dialog = VaultItemState.DialogState.MasterPasswordDialog),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
||||
verify(exactly = 1) {
|
||||
mockCipherView.toViewState(isPremiumUser = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on HiddenFieldVisibilityClicked should update hidden field visibility when re-prompt is not required`() =
|
||||
runTest {
|
||||
val hiddenField = VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
)
|
||||
val loginViewState = DEFAULT_EMPTY_LOGIN_VIEW_STATE.copy(
|
||||
requiresReprompt = false,
|
||||
customFields = listOf(hiddenField),
|
||||
val loginViewState = createViewState(
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
)
|
||||
val loginState = DEFAULT_STATE.copy(viewState = loginViewState)
|
||||
val mockCipherView = mockk<CipherView> {
|
||||
|
@ -539,15 +586,17 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
|
||||
assertEquals(loginState, viewModel.stateFlow.value)
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.Login.HiddenFieldVisibilityClicked(
|
||||
field = hiddenField,
|
||||
isVisible = true,
|
||||
VaultItemAction.ItemType.Login.PasswordVisibilityClicked(
|
||||
true,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
loginState.copy(
|
||||
viewState = loginViewState.copy(
|
||||
customFields = listOf(hiddenField.copy(isVisible = true)),
|
||||
common = DEFAULT_COMMON.copy(requiresReprompt = false),
|
||||
type = DEFAULT_LOGIN_TYPE.copy(
|
||||
passwordData = DEFAULT_LOGIN_TYPE.passwordData!!.copy(isVisible = true),
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -572,82 +621,104 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
|||
authRepository = authRepository,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
private fun createViewState(
|
||||
common: VaultItemState.ViewState.Content.Common = DEFAULT_COMMON,
|
||||
type: VaultItemState.ViewState.Content.ItemType = DEFAULT_LOGIN_TYPE,
|
||||
): VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = common,
|
||||
type = type,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val CIPHER_VIEW_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.vault.feature.item.util.CipherViewExtensionsKt"
|
||||
|
||||
private const val VAULT_ITEM_ID = "vault_item_id"
|
||||
private const val DEFAULT_LOGIN_PASSWORD = "password"
|
||||
private const val DEFAULT_LOGIN_USERNAME = "username"
|
||||
|
||||
private val DEFAULT_STATE: VaultItemState = VaultItemState(
|
||||
vaultItemId = VAULT_ITEM_ID,
|
||||
viewState = VaultItemState.ViewState.Loading,
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE: UserState = UserState(
|
||||
activeUserId = "user_id_1",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "user_id_1",
|
||||
name = "Bit",
|
||||
email = "bitwarden@gmail.com",
|
||||
avatarColorHex = "#ff00ff",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_TYPE: VaultItemState.ViewState.Content.ItemType.Login =
|
||||
VaultItemState.ViewState.Content.ItemType.Login(
|
||||
passwordHistoryCount = 1,
|
||||
username = DEFAULT_LOGIN_USERNAME,
|
||||
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = DEFAULT_LOGIN_PASSWORD,
|
||||
isVisible = false,
|
||||
),
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
),
|
||||
),
|
||||
passwordRevisionDate = "12/31/69 06:16 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com" +
|
||||
"?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
)
|
||||
|
||||
private val DEFAULT_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "login cipher",
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
),
|
||||
),
|
||||
requiresReprompt = true,
|
||||
)
|
||||
|
||||
private val DEFAULT_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_LOGIN_TYPE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CIPHER_VIEW_EXTENSIONS_PATH: String =
|
||||
"com.x8bit.bitwarden.ui.vault.feature.item.util.CipherViewExtensionsKt"
|
||||
|
||||
private const val VAULT_ITEM_ID = "vault_item_id"
|
||||
private const val DEFAULT_LOGIN_PASSWORD = "password"
|
||||
private const val DEFAULT_LOGIN_USERNAME = "username"
|
||||
|
||||
private val DEFAULT_STATE: VaultItemState = VaultItemState(
|
||||
vaultItemId = VAULT_ITEM_ID,
|
||||
viewState = VaultItemState.ViewState.Loading,
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
private val DEFAULT_USER_STATE: UserState = UserState(
|
||||
activeUserId = "user_id_1",
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = "user_id_1",
|
||||
name = "Bit",
|
||||
email = "bitwarden@gmail.com",
|
||||
avatarColorHex = "#ff00ff",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isVaultUnlocked = true,
|
||||
organizations = emptyList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content.Login =
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
name = "login cipher",
|
||||
lastUpdated = "12/31/69 06:16 PM",
|
||||
passwordHistoryCount = 1,
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
),
|
||||
),
|
||||
requiresReprompt = true,
|
||||
username = DEFAULT_LOGIN_USERNAME,
|
||||
passwordData = VaultItemState.ViewState.Content.PasswordData(
|
||||
password = DEFAULT_LOGIN_PASSWORD,
|
||||
isVisible = false,
|
||||
),
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
),
|
||||
),
|
||||
passwordRevisionDate = "12/31/69 06:16 PM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
)
|
||||
|
|
|
@ -44,7 +44,12 @@ class CipherViewExtensionsTest {
|
|||
val isPremiumUser = false
|
||||
val viewState = DEFAULT_FULL_LOGIN_CIPHER_VIEW.toViewState(isPremiumUser = isPremiumUser)
|
||||
|
||||
assertEquals(DEFAULT_FULL_LOGIN_VIEW_STATE.copy(isPremiumUser = isPremiumUser), viewState)
|
||||
assertEquals(
|
||||
DEFAULT_FULL_LOGIN_VIEW_STATE.copy(
|
||||
common = DEFAULT_FULL_LOGIN_VIEW_STATE.common.copy(isPremiumUser = isPremiumUser),
|
||||
),
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -168,67 +173,77 @@ val DEFAULT_EMPTY_LOGIN_CIPHER_VIEW: CipherView = CipherView(
|
|||
revisionDate = Instant.ofEpochSecond(1_000L),
|
||||
)
|
||||
|
||||
val DEFAULT_FULL_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content.Login =
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
name = "login cipher",
|
||||
lastUpdated = "1/1/70 12:16 AM",
|
||||
passwordHistoryCount = 1,
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
val DEFAULT_FULL_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
name = "login cipher",
|
||||
lastUpdated = "1/1/70 12:16 AM",
|
||||
notes = "Lots of notes",
|
||||
isPremiumUser = true,
|
||||
customFields = listOf(
|
||||
VaultItemState.ViewState.Content.Common.Custom.TextField(
|
||||
name = "text",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Common.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
),
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.HiddenField(
|
||||
name = "hidden",
|
||||
value = "value",
|
||||
isCopyable = true,
|
||||
requiresReprompt = true,
|
||||
),
|
||||
type = VaultItemState.ViewState.Content.ItemType.Login(
|
||||
passwordHistoryCount = 1,
|
||||
username = "username",
|
||||
passwordData = VaultItemState.ViewState.Content.ItemType.Login.PasswordData(
|
||||
password = "password",
|
||||
isVisible = false,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.BooleanField(
|
||||
name = "boolean",
|
||||
value = true,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked username",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
VaultItemState.ViewState.Content.Custom.LinkedField(
|
||||
name = "linked password",
|
||||
vaultLinkedFieldType = VaultLinkedFieldType.PASSWORD,
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.ItemType.Login.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
),
|
||||
),
|
||||
passwordRevisionDate = "1/1/70 12:16 AM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
),
|
||||
requiresReprompt = true,
|
||||
username = "username",
|
||||
passwordData = VaultItemState.ViewState.Content.PasswordData(
|
||||
password = "password",
|
||||
isVisible = false,
|
||||
),
|
||||
uris = listOf(
|
||||
VaultItemState.ViewState.Content.UriData(
|
||||
uri = "www.example.com",
|
||||
isCopyable = true,
|
||||
isLaunchable = true,
|
||||
),
|
||||
),
|
||||
passwordRevisionDate = "1/1/70 12:16 AM",
|
||||
totp = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example",
|
||||
)
|
||||
|
||||
val DEFAULT_EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content.Login =
|
||||
VaultItemState.ViewState.Content.Login(
|
||||
name = "login cipher",
|
||||
lastUpdated = "1/1/70 12:16 AM",
|
||||
passwordHistoryCount = null,
|
||||
notes = null,
|
||||
isPremiumUser = true,
|
||||
customFields = emptyList(),
|
||||
requiresReprompt = true,
|
||||
username = null,
|
||||
passwordData = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
val DEFAULT_EMPTY_LOGIN_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = VaultItemState.ViewState.Content.Common(
|
||||
name = "login cipher",
|
||||
lastUpdated = "1/1/70 12:16 AM",
|
||||
|
||||
notes = null,
|
||||
isPremiumUser = true,
|
||||
customFields = emptyList(),
|
||||
requiresReprompt = true,
|
||||
|
||||
),
|
||||
type = VaultItemState.ViewState.Content.ItemType.Login(
|
||||
passwordHistoryCount = null,
|
||||
username = null,
|
||||
passwordData = null,
|
||||
uris = emptyList(),
|
||||
passwordRevisionDate = null,
|
||||
totp = null,
|
||||
),
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue