mirror of
https://github.com/bitwarden/android.git
synced 2026-08-24 14:28:58 -05:00
[PM-32806] feat: View Passport item type (#6853)
This commit is contained in:
+1
-1
@@ -278,7 +278,7 @@ private val CipherListViewType.iconRes: Int
|
||||
CipherListViewType.SshKey -> BitwardenDrawable.ic_ssh_key
|
||||
CipherListViewType.BankAccount -> BitwardenDrawable.ic_payment_card
|
||||
CipherListViewType.DriversLicense -> BitwardenDrawable.ic_note
|
||||
CipherListViewType.Passport -> BitwardenDrawable.ic_note
|
||||
CipherListViewType.Passport -> BitwardenDrawable.ic_passport
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -3137,19 +3137,19 @@ data class VaultAddEditState(
|
||||
*/
|
||||
@Parcelize
|
||||
data class Passport(
|
||||
val surname: String = "",
|
||||
val givenName: String = "",
|
||||
val surname: String = "",
|
||||
val dateOfBirth: String = "",
|
||||
val birthPlace: String = "",
|
||||
val sex: String = "",
|
||||
val birthPlace: String = "",
|
||||
val nationality: String = "",
|
||||
val passportNumber: String = "",
|
||||
val passportType: String = "",
|
||||
val nationalIdentificationNumber: String = "",
|
||||
val issuingCountry: String = "",
|
||||
val issuingAuthority: String = "",
|
||||
val issueDate: String = "",
|
||||
val expirationDate: String = "",
|
||||
val nationalIdentificationNumber: String = "",
|
||||
) : ItemType() {
|
||||
override val itemTypeOption: ItemTypeOption
|
||||
get() = ItemTypeOption.PASSPORT
|
||||
|
||||
+4
-1
@@ -127,12 +127,15 @@ fun CipherView.toViewState(
|
||||
}
|
||||
|
||||
CipherType.PASSPORT -> VaultAddEditState.ViewState.Content.ItemType.Passport(
|
||||
surname = passport?.surname.orEmpty(),
|
||||
givenName = passport?.givenName.orEmpty(),
|
||||
surname = passport?.surname.orEmpty(),
|
||||
dateOfBirth = passport?.dateOfBirth.orEmpty(),
|
||||
sex = passport?.sex.orEmpty(),
|
||||
birthPlace = passport?.birthPlace.orEmpty(),
|
||||
nationality = passport?.nationality.orEmpty(),
|
||||
passportNumber = passport?.passportNumber.orEmpty(),
|
||||
passportType = passport?.passportType.orEmpty(),
|
||||
nationalIdentificationNumber = passport?.nationalIdentificationNumber.orEmpty(),
|
||||
issuingCountry = passport?.issuingCountry.orEmpty(),
|
||||
issuingAuthority = passport?.issuingAuthority.orEmpty(),
|
||||
issueDate = passport?.issueDate.orEmpty(),
|
||||
|
||||
+554
@@ -0,0 +1,554 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
|
||||
import com.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.itemHeader
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemAttachments
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemCustomFields
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemHistory
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.component.vaultItemNotes
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultPassportItemTypeHandlers
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* The top level content UI state for the [VaultItemScreen] when viewing a passport
|
||||
* cipher. Each populated field renders as a separate read-only row. The passport
|
||||
* number and national identification number render with a reveal toggle and an
|
||||
* inline copy affordance.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun VaultItemPassportContent(
|
||||
commonState: VaultItemState.ViewState.Content.Common,
|
||||
passportState: VaultItemState.ViewState.Content.ItemType.Passport,
|
||||
vaultCommonItemTypeHandlers: VaultCommonItemTypeHandlers,
|
||||
vaultPassportItemTypeHandlers: VaultPassportItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
LazyColumn(modifier = modifier.fillMaxWidth()) {
|
||||
item {
|
||||
Spacer(Modifier.height(height = 12.dp))
|
||||
}
|
||||
itemHeader(
|
||||
value = commonState.name,
|
||||
isFavorite = commonState.favorite,
|
||||
isArchived = commonState.archived,
|
||||
iconData = commonState.iconData,
|
||||
relatedLocations = commonState.relatedLocations,
|
||||
iconTestTag = "PassportItemNameIcon",
|
||||
textFieldTestTag = "PassportItemNameEntry",
|
||||
isExpanded = isExpanded,
|
||||
onExpandClick = { isExpanded = !isExpanded },
|
||||
applyIconBackground = commonState.iconData is IconData.Local,
|
||||
)
|
||||
item(key = "passportDetailsHeader") {
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.passport_details),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp)
|
||||
.animateItem(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
}
|
||||
|
||||
passportState.givenName?.let { givenName ->
|
||||
item(key = "givenName") {
|
||||
PassportCopyField(
|
||||
label = stringResource(id = BitwardenString.first_name),
|
||||
value = givenName,
|
||||
copyContentDescription = stringResource(id = BitwardenString.copy_first_name),
|
||||
textFieldTestTag = "PassportItemGivenNameEntry",
|
||||
copyActionTestTag = "PassportCopyGivenNameButton",
|
||||
onCopyClick = vaultPassportItemTypeHandlers.onCopyGivenNameClick,
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = givenName),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.surname?.let { surname ->
|
||||
item(key = "surname") {
|
||||
PassportCopyField(
|
||||
label = stringResource(id = BitwardenString.last_name),
|
||||
value = surname,
|
||||
copyContentDescription = stringResource(id = BitwardenString.copy_last_name),
|
||||
textFieldTestTag = "PassportItemSurnameEntry",
|
||||
copyActionTestTag = "PassportCopySurnameButton",
|
||||
onCopyClick = vaultPassportItemTypeHandlers.onCopySurnameClick,
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = surname),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.dateOfBirth?.let { dateOfBirth ->
|
||||
item(key = "dateOfBirth") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.date_of_birth),
|
||||
value = dateOfBirth,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemDateOfBirthEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = dateOfBirth),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.sex?.let { sex ->
|
||||
item(key = "sex") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.sex),
|
||||
value = sex,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemSexEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = sex),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.birthPlace?.let { birthPlace ->
|
||||
item(key = "birthPlace") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.birth_place),
|
||||
value = birthPlace,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemBirthPlaceEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = birthPlace),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.nationality?.let { nationality ->
|
||||
item(key = "nationality") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.nationality),
|
||||
value = nationality,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemNationalityEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = nationality),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.passportNumber?.let { passportNumber ->
|
||||
item(key = "passportNumber") {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = BitwardenString.passport_number),
|
||||
value = passportNumber,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
supportingContent = null,
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.copy_passport_number,
|
||||
),
|
||||
onClick = vaultPassportItemTypeHandlers.onCopyPassportNumberClick,
|
||||
modifier = Modifier.testTag(
|
||||
tag = "PassportCopyPassportNumberButton",
|
||||
),
|
||||
)
|
||||
},
|
||||
passwordFieldTestTag = "PassportItemPassportNumberEntry",
|
||||
showPasswordTestTag = "PassportViewPassportNumberButton",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState
|
||||
.propertyList
|
||||
.indexOf(element = passportNumber),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.passportType?.let { passportType ->
|
||||
item(key = "passportType") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.passport_type),
|
||||
value = passportType,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemPassportTypeEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = passportType),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.nationalIdentificationNumber?.let { nationalIdentificationNumber ->
|
||||
item(key = "nationalIdentificationNumber") {
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(
|
||||
id = BitwardenString.national_identification_number,
|
||||
),
|
||||
value = nationalIdentificationNumber,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
supportingContent = null,
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = stringResource(
|
||||
id = BitwardenString.copy_national_identification_number,
|
||||
),
|
||||
onClick = vaultPassportItemTypeHandlers
|
||||
.onCopyNationalIdentificationNumberClick,
|
||||
modifier = Modifier.testTag(
|
||||
tag = "PassportCopyNationalIdentificationNumberButton",
|
||||
),
|
||||
)
|
||||
},
|
||||
passwordFieldTestTag = "PassportItemNationalIdentificationNumberEntry",
|
||||
showPasswordTestTag = "PassportViewNationalIdentificationNumberButton",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState
|
||||
.propertyList
|
||||
.indexOf(element = nationalIdentificationNumber),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.issuingCountry?.let { issuingCountry ->
|
||||
item(key = "issuingCountry") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.issuing_country),
|
||||
value = issuingCountry,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemIssuingCountryEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = issuingCountry),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.issuingAuthority?.let { issuingAuthority ->
|
||||
item(key = "issuingAuthority") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.issuing_authority),
|
||||
value = issuingAuthority,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemIssuingAuthorityEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState
|
||||
.propertyList
|
||||
.indexOf(element = issuingAuthority),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.issueDate?.let { issueDate ->
|
||||
item(key = "issueDate") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.issue_date),
|
||||
value = issueDate,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemIssueDateEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = issueDate),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
passportState.expirationDate?.let { expirationDate ->
|
||||
item(key = "expirationDate") {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = BitwardenString.expiration_date),
|
||||
value = expirationDate,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
textFieldTestTag = "PassportItemExpirationDateEntry",
|
||||
cardStyle = passportState
|
||||
.propertyList
|
||||
.toListItemCardStyle(
|
||||
index = passportState.propertyList.indexOf(element = expirationDate),
|
||||
dividerPadding = 0.dp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin()
|
||||
.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
vaultItemNotes(
|
||||
notes = commonState.notes,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
)
|
||||
|
||||
vaultItemCustomFields(
|
||||
customFields = commonState.customFields,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
)
|
||||
|
||||
vaultItemAttachments(
|
||||
attachments = commonState.attachments,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
)
|
||||
|
||||
vaultItemHistory(
|
||||
commonState = commonState,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
loginPasswordRevisionDate = null,
|
||||
)
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(88.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PassportCopyField(
|
||||
label: String,
|
||||
value: String,
|
||||
copyContentDescription: String,
|
||||
textFieldTestTag: String,
|
||||
copyActionTestTag: String,
|
||||
onCopyClick: () -> Unit,
|
||||
cardStyle: CardStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenTextField(
|
||||
label = label,
|
||||
value = value,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = false,
|
||||
actions = {
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = BitwardenDrawable.ic_copy,
|
||||
contentDescription = copyContentDescription,
|
||||
onClick = onCopyClick,
|
||||
modifier = Modifier.testTag(tag = copyActionTestTag),
|
||||
)
|
||||
},
|
||||
textFieldTestTag = textFieldTestTag,
|
||||
cardStyle = cardStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
//region Previews
|
||||
@Composable
|
||||
@Preview(showBackground = true, heightDp = 1200)
|
||||
private fun VaultItemPassportContent_Preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenScaffold() {
|
||||
VaultItemPassportContent(
|
||||
commonState = PREVIEW_COMMON,
|
||||
passportState = PREVIEW_PASSPORT,
|
||||
vaultCommonItemTypeHandlers = PREVIEW_COMMON_HANDLERS,
|
||||
vaultPassportItemTypeHandlers = PREVIEW_PASSPORT_HANDLERS,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val PREVIEW_COMMON: VaultItemState.ViewState.Content.Common =
|
||||
VaultItemState.ViewState.Content.Common(
|
||||
name = "Passport",
|
||||
created = "May 13, 2026, 12:00 PM".asText(),
|
||||
lastUpdated = "May 13, 2026, 12:00 PM".asText(),
|
||||
notes = "Recovery code: 12323234324",
|
||||
customFields = persistentListOf(),
|
||||
requiresCloneConfirmation = false,
|
||||
currentCipher = null,
|
||||
attachments = persistentListOf(),
|
||||
canDelete = true,
|
||||
canRestore = false,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
favorite = false,
|
||||
archived = false,
|
||||
passwordHistoryCount = null,
|
||||
iconData = IconData.Local(iconRes = BitwardenDrawable.ic_passport),
|
||||
relatedLocations = persistentListOf(),
|
||||
hasOrganizations = false,
|
||||
)
|
||||
|
||||
private val PREVIEW_PASSPORT: VaultItemState.ViewState.Content.ItemType.Passport =
|
||||
VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "Mitchell Allen",
|
||||
surname = "Johnson",
|
||||
dateOfBirth = "August 10, 1990",
|
||||
sex = "Male",
|
||||
birthPlace = "Madison, Wisconsin",
|
||||
nationality = "United States of America",
|
||||
passportNumber = "P12345678",
|
||||
passportType = "P",
|
||||
nationalIdentificationNumber = "N-987-654-321",
|
||||
issuingCountry = "United States of America",
|
||||
issuingAuthority = "Department of State",
|
||||
issueDate = "August 10, 2021",
|
||||
expirationDate = "August 10, 2031",
|
||||
)
|
||||
|
||||
private val PREVIEW_COMMON_HANDLERS: VaultCommonItemTypeHandlers =
|
||||
VaultCommonItemTypeHandlers(
|
||||
onRefreshClick = {},
|
||||
onCopyCustomHiddenField = {},
|
||||
onCopyCustomTextField = {},
|
||||
onShowHiddenFieldClick = { _, _ -> },
|
||||
onAttachmentDownloadClick = {},
|
||||
onAttachmentPreviewClick = {},
|
||||
onCopyNotesClick = {},
|
||||
onPasswordHistoryClick = {},
|
||||
onUpgradeToPremiumClick = {},
|
||||
)
|
||||
|
||||
private val PREVIEW_PASSPORT_HANDLERS: VaultPassportItemTypeHandlers =
|
||||
VaultPassportItemTypeHandlers(
|
||||
onCopyGivenNameClick = {},
|
||||
onCopySurnameClick = {},
|
||||
onCopyPassportNumberClick = {},
|
||||
onCopyNationalIdentificationNumberClick = {},
|
||||
)
|
||||
//endregion Previews
|
||||
@@ -47,6 +47,8 @@ import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultBankAccountItemTy
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCardItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultCommonItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultDriversLicenseItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultPassportItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.rememberVaultPassportItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultIdentityItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultLoginItemTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.handlers.VaultSshKeyItemTypeHandlers
|
||||
@@ -292,6 +294,9 @@ fun VaultItemScreen(
|
||||
vaultDriversLicenseItemTypeHandlers = remember(viewModel) {
|
||||
VaultDriversLicenseItemTypeHandlers.create(viewModel = viewModel)
|
||||
},
|
||||
vaultPassportItemTypeHandlers = rememberVaultPassportItemTypeHandlers(
|
||||
viewModel = viewModel,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -378,6 +383,7 @@ private fun VaultItemContent(
|
||||
vaultIdentityItemTypeHandlers: VaultIdentityItemTypeHandlers,
|
||||
vaultBankAccountItemTypeHandlers: VaultBankAccountItemTypeHandlers,
|
||||
vaultDriversLicenseItemTypeHandlers: VaultDriversLicenseItemTypeHandlers,
|
||||
vaultPassportItemTypeHandlers: VaultPassportItemTypeHandlers,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (viewState) {
|
||||
@@ -462,12 +468,11 @@ private fun VaultItemContent(
|
||||
}
|
||||
|
||||
is VaultItemState.ViewState.Content.ItemType.Passport -> {
|
||||
// TODO(PM-32806): Render dedicated content for Passport once the UI ships
|
||||
// in its Story slice. Until then this branch is gated behind the
|
||||
// pm-32009-new-item-types feature flag and cannot be received.
|
||||
VaultItemSecureNoteContent(
|
||||
VaultItemPassportContent(
|
||||
commonState = viewState.common,
|
||||
passportState = viewState.type,
|
||||
vaultCommonItemTypeHandlers = vaultCommonItemTypeHandlers,
|
||||
vaultPassportItemTypeHandlers = vaultPassportItemTypeHandlers,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
+146
-2
@@ -244,6 +244,7 @@ class VaultItemViewModel @Inject constructor(
|
||||
handleDriversLicenseTypeActions(action)
|
||||
}
|
||||
|
||||
is VaultItemAction.ItemType.Passport -> handlePassportTypeActions(action)
|
||||
is VaultItemAction.Common -> handleCommonActions(action)
|
||||
is VaultItemAction.Internal -> handleInternalAction(action)
|
||||
}
|
||||
@@ -1253,6 +1254,83 @@ class VaultItemViewModel @Inject constructor(
|
||||
|
||||
//endregion Driver's License Type Handlers
|
||||
|
||||
//region Passport Type Handlers
|
||||
|
||||
private fun handlePassportTypeActions(action: VaultItemAction.ItemType.Passport) {
|
||||
when (action) {
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick -> {
|
||||
handleCopyPassportGivenNameClick()
|
||||
}
|
||||
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick -> {
|
||||
handleCopyPassportSurnameClick()
|
||||
}
|
||||
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick -> {
|
||||
handleCopyPassportItemNumberClick()
|
||||
}
|
||||
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick -> {
|
||||
handleCopyNationalIdentificationNumberClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPassportGivenNameClick() {
|
||||
onPassportContent { _, passport ->
|
||||
passport.givenName
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { givenName ->
|
||||
clipboardManager.setText(
|
||||
text = givenName,
|
||||
toastDescriptorOverride = BitwardenString.first_name.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPassportSurnameClick() {
|
||||
onPassportContent { _, passport ->
|
||||
passport.surname
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { surname ->
|
||||
clipboardManager.setText(
|
||||
text = surname,
|
||||
toastDescriptorOverride = BitwardenString.last_name.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyPassportItemNumberClick() {
|
||||
onPassportContent { _, passport ->
|
||||
passport.passportNumber
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { passportNumber ->
|
||||
clipboardManager.setText(
|
||||
text = passportNumber,
|
||||
toastDescriptorOverride = BitwardenString.passport_number.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCopyNationalIdentificationNumberClick() {
|
||||
onPassportContent { _, passport ->
|
||||
passport.nationalIdentificationNumber
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { nationalIdentificationNumber ->
|
||||
clipboardManager.setText(
|
||||
text = nationalIdentificationNumber,
|
||||
toastDescriptorOverride =
|
||||
BitwardenString.national_identification_number.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion Passport Type Handlers
|
||||
|
||||
//region Internal Type Handlers
|
||||
|
||||
private fun handleInternalAction(action: VaultItemAction.Internal) {
|
||||
@@ -1665,6 +1743,21 @@ class VaultItemViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun onPassportContent(
|
||||
crossinline block: (
|
||||
VaultItemState.ViewState.Content,
|
||||
VaultItemState.ViewState.Content.ItemType.Passport,
|
||||
) -> Unit,
|
||||
) {
|
||||
state.viewState.asContentOrNull()
|
||||
?.let { content ->
|
||||
(content.type as? VaultItemState.ViewState.Content.ItemType.Passport)
|
||||
?.let { passportContent ->
|
||||
block(content, passportContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2191,17 +2284,41 @@ data class VaultItemState(
|
||||
* Represents the `Passport` item type.
|
||||
*/
|
||||
data class Passport(
|
||||
val surname: String?,
|
||||
val givenName: String?,
|
||||
val surname: String?,
|
||||
val dateOfBirth: String?,
|
||||
val sex: String?,
|
||||
val birthPlace: String?,
|
||||
val nationality: String?,
|
||||
val passportNumber: String?,
|
||||
val passportType: String?,
|
||||
val nationalIdentificationNumber: String?,
|
||||
val issuingCountry: String?,
|
||||
val issuingAuthority: String?,
|
||||
val issueDate: String?,
|
||||
val expirationDate: String?,
|
||||
) : ItemType()
|
||||
) : ItemType() {
|
||||
|
||||
/**
|
||||
* An ordered list of populated Passport elements.
|
||||
*/
|
||||
val propertyList: ImmutableList<String>
|
||||
get() = persistentListOfNotNull(
|
||||
givenName,
|
||||
surname,
|
||||
dateOfBirth,
|
||||
sex,
|
||||
birthPlace,
|
||||
nationality,
|
||||
passportNumber,
|
||||
passportType,
|
||||
nationalIdentificationNumber,
|
||||
issuingCountry,
|
||||
issuingAuthority,
|
||||
issueDate,
|
||||
expirationDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2741,6 +2858,33 @@ sealed class VaultItemAction {
|
||||
*/
|
||||
data object CopyLicenseNumberClick : DriversLicense()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions specific to the Passport type.
|
||||
*/
|
||||
sealed class Passport : ItemType() {
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the given name.
|
||||
*/
|
||||
data object CopyGivenNameClick : Passport()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the surname.
|
||||
*/
|
||||
data object CopySurnameClick : Passport()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the passport number.
|
||||
*/
|
||||
data object CopyPassportNumberClick : Passport()
|
||||
|
||||
/**
|
||||
* The user has clicked the copy button for the national identification
|
||||
* number.
|
||||
*/
|
||||
data object CopyNationalIdentificationNumberClick : Passport()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.item.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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
|
||||
* passport items in a vault.
|
||||
*
|
||||
* @property onCopyGivenNameClick Handles the user clicking the copy button next to the
|
||||
* given name.
|
||||
* @property onCopySurnameClick Handles the user clicking the copy button next to the
|
||||
* surname.
|
||||
* @property onCopyPassportNumberClick Handles the user clicking the copy button next to the
|
||||
* passport number.
|
||||
* @property onCopyNationalIdentificationNumberClick Handles the user clicking the copy button
|
||||
* next to the national identification number.
|
||||
*/
|
||||
data class VaultPassportItemTypeHandlers(
|
||||
val onCopyGivenNameClick: () -> Unit,
|
||||
val onCopySurnameClick: () -> Unit,
|
||||
val onCopyPassportNumberClick: () -> Unit,
|
||||
val onCopyNationalIdentificationNumberClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Creates the [VaultPassportItemTypeHandlers] using the [viewModel] to send
|
||||
* desired actions.
|
||||
*/
|
||||
fun create(viewModel: VaultItemViewModel): VaultPassportItemTypeHandlers =
|
||||
VaultPassportItemTypeHandlers(
|
||||
onCopyGivenNameClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
},
|
||||
onCopySurnameClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
},
|
||||
onCopyPassportNumberClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
},
|
||||
onCopyNationalIdentificationNumberClick = {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport
|
||||
.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remember a [VaultPassportItemTypeHandlers] instance in a [Composable]
|
||||
* scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberVaultPassportItemTypeHandlers(
|
||||
viewModel: VaultItemViewModel,
|
||||
): VaultPassportItemTypeHandlers =
|
||||
remember(viewModel) {
|
||||
VaultPassportItemTypeHandlers.create(viewModel = viewModel)
|
||||
}
|
||||
+14
-11
@@ -247,16 +247,19 @@ fun CipherView.toViewState(
|
||||
}
|
||||
|
||||
CipherType.PASSPORT -> VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
surname = passport?.surname.orEmpty(),
|
||||
givenName = passport?.givenName.orEmpty(),
|
||||
dateOfBirth = passport?.dateOfBirth.orEmpty(),
|
||||
nationality = passport?.nationality.orEmpty(),
|
||||
passportNumber = passport?.passportNumber.orEmpty(),
|
||||
passportType = passport?.passportType.orEmpty(),
|
||||
issuingCountry = passport?.issuingCountry.orEmpty(),
|
||||
issuingAuthority = passport?.issuingAuthority.orEmpty(),
|
||||
issueDate = passport?.issueDate.orEmpty(),
|
||||
expirationDate = passport?.expirationDate.orEmpty(),
|
||||
givenName = passport?.givenName,
|
||||
surname = passport?.surname,
|
||||
dateOfBirth = passport?.dateOfBirth,
|
||||
sex = passport?.sex,
|
||||
birthPlace = passport?.birthPlace,
|
||||
nationality = passport?.nationality,
|
||||
passportNumber = passport?.passportNumber,
|
||||
passportType = passport?.passportType,
|
||||
nationalIdentificationNumber = passport?.nationalIdentificationNumber,
|
||||
issuingCountry = passport?.issuingCountry,
|
||||
issuingAuthority = passport?.issuingAuthority,
|
||||
issueDate = passport?.issueDate,
|
||||
expirationDate = passport?.expirationDate,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -350,7 +353,7 @@ private val CipherType.iconRes: Int
|
||||
CipherType.LOGIN -> BitwardenDrawable.ic_globe
|
||||
CipherType.BANK_ACCOUNT -> BitwardenDrawable.ic_payment_card
|
||||
CipherType.DRIVERS_LICENSE -> BitwardenDrawable.ic_note
|
||||
CipherType.PASSPORT -> BitwardenDrawable.ic_note
|
||||
CipherType.PASSPORT -> BitwardenDrawable.ic_passport
|
||||
}
|
||||
|
||||
@get:DrawableRes
|
||||
|
||||
+1
-1
@@ -622,7 +622,7 @@ private val CipherListViewType.iconRes: Int
|
||||
CipherListViewType.SshKey -> BitwardenDrawable.ic_ssh_key
|
||||
CipherListViewType.BankAccount -> BitwardenDrawable.ic_payment_card
|
||||
CipherListViewType.DriversLicense -> BitwardenDrawable.ic_note
|
||||
CipherListViewType.Passport -> BitwardenDrawable.ic_note
|
||||
CipherListViewType.Passport -> BitwardenDrawable.ic_passport
|
||||
}
|
||||
|
||||
private fun List<CipherListView>.applyFilters(
|
||||
|
||||
+1
-1
@@ -144,7 +144,7 @@ private fun VaultAddEditState.ViewState.Content.ItemType.toPassport(): PassportV
|
||||
surname = it.surname.orNullIfBlank(),
|
||||
givenName = it.givenName.orNullIfBlank(),
|
||||
dateOfBirth = it.dateOfBirth.orNullIfBlank(),
|
||||
birthPlace = it.dateOfBirth.orNullIfBlank(),
|
||||
birthPlace = it.birthPlace.orNullIfBlank(),
|
||||
sex = it.sex.orNullIfBlank(),
|
||||
nationality = it.nationality.orNullIfBlank(),
|
||||
passportNumber = it.passportNumber.orNullIfBlank(),
|
||||
|
||||
+92
@@ -16,6 +16,7 @@ import com.bitwarden.vault.LoginView
|
||||
import com.bitwarden.vault.PasswordHistoryView
|
||||
import com.bitwarden.vault.SecureNoteType
|
||||
import com.bitwarden.vault.SecureNoteView
|
||||
import com.bitwarden.vault.PassportView
|
||||
import com.bitwarden.vault.SshKeyView
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
@@ -376,6 +377,58 @@ class CipherViewExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Passport ViewState`() {
|
||||
val cipherView = DEFAULT_PASSPORT_CIPHER_VIEW
|
||||
|
||||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isPremium = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
canDelete = true,
|
||||
canAssignToCollections = true,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
originalCipher = cipherView,
|
||||
name = "cipher",
|
||||
favorite = false,
|
||||
masterPasswordReprompt = true,
|
||||
notes = "Lots of notes",
|
||||
customFieldData = listOf(
|
||||
VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false),
|
||||
VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"),
|
||||
VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
|
||||
),
|
||||
availableFolders = emptyList(),
|
||||
availableOwners = emptyList(),
|
||||
),
|
||||
isIndividualVaultDisabled = false,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "the given name",
|
||||
surname = "the surname",
|
||||
dateOfBirth = "the date of birth",
|
||||
sex = "the sex",
|
||||
birthPlace = "the birth place",
|
||||
nationality = "the nationality",
|
||||
passportNumber = "the passport number",
|
||||
passportType = "the passport type",
|
||||
nationalIdentificationNumber = "the national identification number",
|
||||
issuingCountry = "the issuing country",
|
||||
issuingAuthority = "the issuing authority",
|
||||
issueDate = "the issue date",
|
||||
expirationDate = "the expiration date",
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState with isClone true should append clone text to the cipher name`() {
|
||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||
@@ -879,6 +932,45 @@ private val DEFAULT_SSH_KEY_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.c
|
||||
),
|
||||
)
|
||||
|
||||
private val DEFAULT_PASSPORT_CIPHER_VIEW: CipherView = DEFAULT_BASE_CIPHER_VIEW.copy(
|
||||
type = CipherType.PASSPORT,
|
||||
fields = listOf(
|
||||
FieldView(
|
||||
name = "TestBoolean",
|
||||
value = false.toString(),
|
||||
type = FieldType.BOOLEAN,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "TestText",
|
||||
value = "TestText",
|
||||
type = FieldType.TEXT,
|
||||
linkedId = null,
|
||||
),
|
||||
FieldView(
|
||||
name = "TestHidden",
|
||||
value = "TestHidden",
|
||||
type = FieldType.HIDDEN,
|
||||
linkedId = null,
|
||||
),
|
||||
),
|
||||
passport = PassportView(
|
||||
surname = "the surname",
|
||||
givenName = "the given name",
|
||||
dateOfBirth = "the date of birth",
|
||||
birthPlace = "the birth place",
|
||||
sex = "the sex",
|
||||
nationality = "the nationality",
|
||||
passportNumber = "the passport number",
|
||||
passportType = "the passport type",
|
||||
issuingCountry = "the issuing country",
|
||||
issuingAuthority = "the issuing authority",
|
||||
issueDate = "the issue date",
|
||||
expirationDate = "the expiration date",
|
||||
nationalIdentificationNumber = "the national identification number",
|
||||
),
|
||||
)
|
||||
|
||||
private const val TEST_ID = "testID"
|
||||
private val NO_FOLDER_ITEM = VaultAddEditState.Folder(
|
||||
id = null,
|
||||
|
||||
@@ -3750,6 +3750,262 @@ class VaultItemScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
//endregion drivers license
|
||||
|
||||
//region passport
|
||||
|
||||
@Test
|
||||
fun `in passport state, all fields should be displayed when populated`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
|
||||
composeTestRule.onNodeWithTextAfterScroll("the given name").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the surname").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the date of birth").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the sex").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the birth place").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the nationality").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the passport type").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the issuing country").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the issuing authority").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the issue date").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithTextAfterScroll("the expiration date").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, on copy given name click should send CopyGivenNameClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy first name")
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, on copy surname click should send CopySurnameClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasTestTag("PassportCopySurnameButton"))
|
||||
composeTestRule
|
||||
.onNodeWithTag("PassportCopySurnameButton")
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, on copy passport number click should send CopyPassportNumberClick`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy passport number")
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, on copy national id click should send the matching action`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule
|
||||
.onNodeWithContentDescriptionAfterScroll("Copy national identification number")
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, givenName should be displayed according to state`() {
|
||||
val givenName = "the given name"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(givenName).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(givenName = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(givenName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, surname should be displayed according to state`() {
|
||||
val surname = "the surname"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(surname).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(surname = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(surname)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, dateOfBirth should be displayed according to state`() {
|
||||
val dateOfBirth = "the date of birth"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(dateOfBirth).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(dateOfBirth = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(dateOfBirth)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, sex should be displayed according to state`() {
|
||||
val sex = "the sex"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(sex).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(sex = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(sex)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, birthPlace should be displayed according to state`() {
|
||||
val birthPlace = "the birth place"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(birthPlace).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(birthPlace = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(birthPlace)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, nationality should be displayed according to state`() {
|
||||
val nationality = "the nationality"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(nationality).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(nationality = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(nationality)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, passportNumber should be displayed according to state`() {
|
||||
val passportNumberLabel = "Passport number"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(passportNumberLabel).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(passportNumber = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(passportNumberLabel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, passportType should be displayed according to state`() {
|
||||
val passportType = "the passport type"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(passportType).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(passportType = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(passportType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, nationalIdentificationNumber should be displayed according to state`() {
|
||||
val nationalIdLabel = "National identification number"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(nationalIdLabel).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(nationalIdentificationNumber = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(nationalIdLabel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, issuingCountry should be displayed according to state`() {
|
||||
val issuingCountry = "the issuing country"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(issuingCountry).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(issuingCountry = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(issuingCountry)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, issuingAuthority should be displayed according to state`() {
|
||||
val issuingAuthority = "the issuing authority"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(issuingAuthority).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(issuingAuthority = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(issuingAuthority)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, issueDate should be displayed according to state`() {
|
||||
val issueDate = "the issue date"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(issueDate).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(issueDate = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(issueDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, expirationDate should be displayed according to state`() {
|
||||
val expirationDate = "the expiration date"
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
composeTestRule.onNodeWithTextAfterScroll(expirationDate).assertIsDisplayed()
|
||||
|
||||
mutableStateFlow.update { currentState ->
|
||||
updatePassportType(currentState) { copy(expirationDate = null) }
|
||||
}
|
||||
|
||||
composeTestRule.assertScrollableNodeDoesNotExist(expirationDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in passport state, edit fab should be displayed`() {
|
||||
mutableStateFlow.update { it.copy(viewState = DEFAULT_PASSPORT_VIEW_STATE) }
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Edit item").assertIsDisplayed()
|
||||
}
|
||||
|
||||
//endregion passport
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
@@ -3869,6 +4125,29 @@ private fun updateDriversLicenseType(
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
private fun updatePassportType(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.ItemType.Passport.() ->
|
||||
VaultItemState.ViewState.Content.ItemType.Passport,
|
||||
): VaultItemState {
|
||||
val updatedType = when (val viewState = currentState.viewState) {
|
||||
is VaultItemState.ViewState.Content -> {
|
||||
when (val type = viewState.type) {
|
||||
is VaultItemState.ViewState.Content.ItemType.Passport -> {
|
||||
viewState.copy(
|
||||
type = type.transform(),
|
||||
)
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
}
|
||||
|
||||
else -> viewState
|
||||
}
|
||||
return currentState.copy(viewState = updatedType)
|
||||
}
|
||||
|
||||
private fun updateCommonContent(
|
||||
currentState: VaultItemState,
|
||||
transform: VaultItemState.ViewState.Content.Common.()
|
||||
@@ -4184,6 +4463,29 @@ private val DEFAULT_DRIVERS_LICENSE_VIEW_STATE: VaultItemState.ViewState.Content
|
||||
type = DEFAULT_DRIVERS_LICENSE,
|
||||
)
|
||||
|
||||
private val DEFAULT_PASSPORT: VaultItemState.ViewState.Content.ItemType.Passport =
|
||||
VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "the given name",
|
||||
surname = "the surname",
|
||||
dateOfBirth = "the date of birth",
|
||||
sex = "the sex",
|
||||
birthPlace = "the birth place",
|
||||
nationality = "the nationality",
|
||||
passportNumber = "the passport number",
|
||||
passportType = "the passport type",
|
||||
nationalIdentificationNumber = "the national identification number",
|
||||
issuingCountry = "the issuing country",
|
||||
issuingAuthority = "the issuing authority",
|
||||
issueDate = "the issue date",
|
||||
expirationDate = "the expiration date",
|
||||
)
|
||||
|
||||
private val DEFAULT_PASSPORT_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON.copy(iconData = IconData.Local(BitwardenDrawable.ic_globe)),
|
||||
type = DEFAULT_PASSPORT,
|
||||
)
|
||||
|
||||
private val EMPTY_VIEW_STATES = listOf(
|
||||
EMPTY_LOGIN_VIEW_STATE,
|
||||
EMPTY_IDENTITY_VIEW_STATE,
|
||||
|
||||
+293
@@ -3131,6 +3131,275 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class PassportActions {
|
||||
private lateinit var viewModel: VaultItemViewModel
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
viewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(viewState = PASSPORT_VIEW_STATE),
|
||||
)
|
||||
every {
|
||||
mockCipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
canDelete = true,
|
||||
canRestore = false,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = false,
|
||||
relatedLocations = persistentListOf(),
|
||||
hasOrganizations = true,
|
||||
)
|
||||
} returns PASSPORT_VIEW_STATE
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
|
||||
mutableFoldersStateFlow.value = DataState.Loaded(emptyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyGivenNameClick should copy given name to clipboard`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = "Missy",
|
||||
toastDescriptorOverride = BitwardenString.first_name.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyGivenNameClick with null given name should not copy to clipboard`() = runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(givenName = null),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyGivenNameClick with blank given name should not copy to clipboard`() = runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(givenName = " "),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopySurnameClick should copy surname to clipboard`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = "Katner",
|
||||
toastDescriptorOverride = BitwardenString.last_name.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopySurnameClick with null surname should not copy to clipboard`() = runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(surname = null),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopySurnameClick with blank surname should not copy to clipboard`() = runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(surname = " "),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyPassportNumberClick should copy passport number to clipboard`() = runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = "P12345678",
|
||||
toastDescriptorOverride = BitwardenString.passport_number.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyPassportNumberClick with null passport number should not copy to clipboard`() =
|
||||
runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(passportNumber = null),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyPassportNumberClick with blank passport number should not copy to clipboard`() =
|
||||
runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(passportNumber = " "),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyNationalIdentificationNumberClick should copy the value to clipboard`() =
|
||||
runTest {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
clipboardManager.setText(
|
||||
text = "N-987-654-321",
|
||||
toastDescriptorOverride =
|
||||
BitwardenString.national_identification_number.asText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyNationalIdentificationNumberClick with null value should not copy`() =
|
||||
runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(nationalIdentificationNumber = null),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CopyNationalIdentificationNumberClick with blank value should not copy`() =
|
||||
runTest {
|
||||
val emptyState = PASSPORT_VIEW_STATE.copy(
|
||||
type = DEFAULT_PASSPORT_TYPE.copy(nationalIdentificationNumber = " "),
|
||||
)
|
||||
viewModel = createViewModelWithPassportState(emptyState)
|
||||
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
|
||||
verify(exactly = 0) {
|
||||
clipboardManager.setText(
|
||||
text = any<String>(),
|
||||
toastDescriptorOverride = any<Text>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModelWithPassportState(
|
||||
viewState: VaultItemState.ViewState.Content,
|
||||
): VaultItemViewModel {
|
||||
every {
|
||||
mockCipherView.toViewState(
|
||||
previousState = any(),
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
canDelete = true,
|
||||
canRestore = false,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = false,
|
||||
relatedLocations = persistentListOf(),
|
||||
hasOrganizations = true,
|
||||
)
|
||||
} returns viewState
|
||||
val newViewModel = createViewModel(
|
||||
state = DEFAULT_STATE.copy(viewState = viewState),
|
||||
)
|
||||
mutableVaultItemFlow.value = DataState.Loaded(data = mockCipherView)
|
||||
mutableAuthCodeItemFlow.value = DataState.Loaded(data = null)
|
||||
mutableCollectionsStateFlow.value = DataState.Loaded(emptyList())
|
||||
mutableFoldersStateFlow.value = DataState.Loaded(emptyList())
|
||||
return newViewModel
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class VaultItemFlow {
|
||||
@BeforeEach
|
||||
@@ -3792,5 +4061,29 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_DRIVERS_LICENSE_TYPE,
|
||||
)
|
||||
|
||||
private val DEFAULT_PASSPORT_TYPE:
|
||||
VaultItemState.ViewState.Content.ItemType.Passport =
|
||||
VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "Missy",
|
||||
surname = "Katner",
|
||||
dateOfBirth = "August 10, 1990",
|
||||
sex = "Female",
|
||||
birthPlace = "Madison, WI",
|
||||
nationality = "USA",
|
||||
passportNumber = "P12345678",
|
||||
passportType = "Regular",
|
||||
nationalIdentificationNumber = "N-987-654-321",
|
||||
issuingCountry = "USA",
|
||||
issuingAuthority = "Department of State",
|
||||
issueDate = "August 10, 2021",
|
||||
expirationDate = "August 10, 2031",
|
||||
)
|
||||
|
||||
private val PASSPORT_VIEW_STATE: VaultItemState.ViewState.Content =
|
||||
VaultItemState.ViewState.Content(
|
||||
common = DEFAULT_COMMON,
|
||||
type = DEFAULT_PASSPORT_TYPE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
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
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VaultPassportItemTypeHandlersTest {
|
||||
|
||||
private val viewModel = mockk<VaultItemViewModel> {
|
||||
every { trySendAction(any()) } returns Unit
|
||||
}
|
||||
private val handlers = VaultPassportItemTypeHandlers.create(viewModel = viewModel)
|
||||
|
||||
@Test
|
||||
fun `onCopyGivenNameClick should send CopyGivenNameClick action`() {
|
||||
handlers.onCopyGivenNameClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyGivenNameClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCopySurnameClick should send CopySurnameClick action`() {
|
||||
handlers.onCopySurnameClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopySurnameClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCopyPassportNumberClick should send CopyPassportNumberClick action`() {
|
||||
handlers.onCopyPassportNumberClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyPassportNumberClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCopyNationalIdentificationNumberClick should send the matching action`() {
|
||||
handlers.onCopyNationalIdentificationNumberClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultItemAction.ItemType.Passport.CopyNationalIdentificationNumberClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -6,6 +6,7 @@ import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.vault.CipherType
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCardView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockDriversLicenseView
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPassportView
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.model.TotpCodeItemData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
@@ -22,6 +23,7 @@ import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class CipherViewExtensionsTest {
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
@@ -541,6 +543,99 @@ class CipherViewExtensionsTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState Passport Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.PASSPORT, isEmpty = false)
|
||||
.copy(passport = createMockPassportView(number = 1))
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
canRestore = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
hasOrganizations = true,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(
|
||||
isEmpty = false,
|
||||
isPremiumUser = true,
|
||||
iconResId = BitwardenDrawable.ic_passport,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = "mockGivenName-1",
|
||||
surname = "mockSurname-1",
|
||||
dateOfBirth = "mockDateOfBirth-1",
|
||||
sex = "mockSex-1",
|
||||
birthPlace = "mockBirthPlace-1",
|
||||
nationality = "mockNationality-1",
|
||||
passportNumber = "mockPassportNumber-1",
|
||||
passportType = "mockPassportType-1",
|
||||
nationalIdentificationNumber = "mockNationalIdentificationNumber-1",
|
||||
issuingCountry = "mockIssuingCountry-1",
|
||||
issuingAuthority = "mockIssuingAuthority-1",
|
||||
issueDate = "mockIssueDate-1",
|
||||
expirationDate = "mockExpirationDate-1",
|
||||
),
|
||||
),
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should transform empty CipherView into ViewState Passport Content`() {
|
||||
val cipherView = createCipherView(type = CipherType.PASSPORT, isEmpty = true)
|
||||
val viewState = cipherView.toViewState(
|
||||
previousState = null,
|
||||
isPremiumUser = true,
|
||||
totpCodeItemData = null,
|
||||
clock = fixedClock,
|
||||
canDelete = true,
|
||||
canRestore = true,
|
||||
canAssignToCollections = true,
|
||||
canEdit = true,
|
||||
baseIconUrl = "https://example.com/",
|
||||
isIconLoadingDisabled = true,
|
||||
relatedLocations = persistentListOf(),
|
||||
hasOrganizations = true,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemState.ViewState.Content(
|
||||
common = createCommonContent(
|
||||
isEmpty = true,
|
||||
isPremiumUser = true,
|
||||
iconResId = BitwardenDrawable.ic_passport,
|
||||
)
|
||||
.copy(currentCipher = cipherView),
|
||||
type = VaultItemState.ViewState.Content.ItemType.Passport(
|
||||
givenName = null,
|
||||
surname = null,
|
||||
dateOfBirth = null,
|
||||
sex = null,
|
||||
birthPlace = null,
|
||||
nationality = null,
|
||||
passportNumber = null,
|
||||
passportType = null,
|
||||
nationalIdentificationNumber = null,
|
||||
issuingCountry = null,
|
||||
issuingAuthority = null,
|
||||
issueDate = null,
|
||||
expirationDate = null,
|
||||
),
|
||||
),
|
||||
viewState,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform full CipherView into ViewState with iconData based on cipher type`() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M5.375,13.2H10.625V12H5.375V13.2ZM8,10.8C9.038,10.8 9.922,10.41 10.653,9.63C11.384,8.85 11.75,7.907 11.75,6.8C11.75,5.693 11.384,4.75 10.653,3.97C9.922,3.19 9.038,2.8 8,2.8C6.963,2.8 6.078,3.19 5.347,3.97C4.616,4.75 4.25,5.693 4.25,6.8C4.25,7.907 4.616,8.85 5.347,9.63C6.078,10.41 6.963,10.8 8,10.8ZM8,9.46C7.9,9.313 7.794,9.07 7.681,8.73C7.569,8.39 7.494,7.947 7.456,7.4H8.544C8.506,7.947 8.431,8.39 8.319,8.73C8.206,9.07 8.1,9.313 8,9.46ZM6.65,9.2C6.35,9 6.094,8.747 5.881,8.44C5.669,8.133 5.525,7.787 5.45,7.4H6.331C6.356,7.733 6.394,8.05 6.444,8.35C6.494,8.65 6.563,8.933 6.65,9.2ZM9.35,9.2C9.438,8.933 9.506,8.65 9.556,8.35C9.606,8.05 9.644,7.733 9.669,7.4H10.55C10.475,7.787 10.331,8.133 10.119,8.44C9.906,8.747 9.65,9 9.35,9.2ZM5.45,6.2C5.525,5.813 5.669,5.467 5.881,5.16C6.094,4.853 6.35,4.6 6.65,4.4C6.563,4.667 6.494,4.95 6.444,5.25C6.394,5.55 6.356,5.867 6.331,6.2H5.45ZM7.456,6.2C7.494,5.653 7.569,5.21 7.681,4.87C7.794,4.53 7.9,4.287 8,4.14C8.1,4.287 8.206,4.53 8.319,4.87C8.431,5.21 8.506,5.653 8.544,6.2H7.456ZM9.669,6.2C9.644,5.867 9.606,5.55 9.556,5.25C9.506,4.95 9.438,4.667 9.35,4.4C9.65,4.6 9.906,4.853 10.119,5.16C10.331,5.467 10.475,5.813 10.55,6.2H9.669ZM2,16V0H12.5C12.913,0 13.266,0.157 13.559,0.47C13.853,0.783 14,1.16 14,1.6V14.4C14,14.84 13.853,15.217 13.559,15.53C13.266,15.843 12.913,16 12.5,16H2ZM3.5,14.4H12.5V1.6H3.5V14.4Z"
|
||||
android:fillColor="#5A6D91"/>
|
||||
</vector>
|
||||
@@ -1360,4 +1360,9 @@ Do you want to switch to this account?</string>
|
||||
<string name="licenses">Licenses</string>
|
||||
<string name="no_licenses">There are no licenses in your vault.</string>
|
||||
<string name="your_request_was_interrupted_because_the_app_needed_to_reauthenticate">Your request was interrupted because the app needed to re-authenticate. Please try again.</string>
|
||||
<string name="passport_details">Passport details</string>
|
||||
<string name="sex">Sex</string>
|
||||
<string name="birth_place">Birth place</string>
|
||||
<string name="national_identification_number">National identification number</string>
|
||||
<string name="copy_national_identification_number">Copy national identification number</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user