From 5e689276bfb00d8f3ecfdcbe24662fe81d0b13a5 Mon Sep 17 00:00:00 2001 From: aj-rosado <109146700+aj-rosado@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:11:46 +0000 Subject: [PATCH] [PM-41296] feat: Display Identities vault list when autofilling for an Identity (#7373) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../autofill/model/AutofillSelectionData.kt | 1 + .../autofill/util/FilledDataExtensions.kt | 5 +- .../util/AutofillSelectionDataExtensions.kt | 1 + .../util/AutofillSelectionDataExtensions.kt | 4 ++ .../itemlisting/VaultItemListingViewModel.kt | 42 +++++++---- .../AutofillSelectionDataExtensionsTest.kt | 1 + .../AutofillSelectionDataExtensionsTest.kt | 17 +++++ .../itemlisting/VaultItemListingScreenTest.kt | 27 +++++++ .../VaultItemListingViewModelTest.kt | 70 +++++++++++++++++++ .../util/VaultItemListingDataUtil.kt | 20 +++--- ui/src/main/res/values/strings.xml | 1 + 11 files changed, 163 insertions(+), 26 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt index 0327ebf241..5254eb93ef 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/model/AutofillSelectionData.kt @@ -23,6 +23,7 @@ data class AutofillSelectionData( enum class Type { CARD, LOGIN, + IDENTITY, } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt index 4af7c3545a..67baa1be9d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/autofill/util/FilledDataExtensions.kt @@ -37,10 +37,7 @@ fun FilledData.buildVaultItemDataset( type = when (this.originalPartition) { is AutofillPartition.Card -> AutofillSelectionData.Type.CARD is AutofillPartition.Login -> AutofillSelectionData.Type.LOGIN - // AutofillSelectionData.Type has no IDENTITY value yet (deliberately deferred -- it - // cascades into the vault item listing/add-edit UI flows). This is a no-op placeholder - // today since nothing yet produces an Identity-only partition. - is AutofillPartition.Identity -> AutofillSelectionData.Type.LOGIN + is AutofillPartition.Identity -> AutofillSelectionData.Type.IDENTITY }, uri = this.uri, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt index 89e5f2da7e..ffad59450d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensions.kt @@ -10,4 +10,5 @@ fun AutofillSelectionData.Type.toVaultItemListingType(): VaultItemListingType = when (this) { AutofillSelectionData.Type.CARD -> VaultItemListingType.Card AutofillSelectionData.Type.LOGIN -> VaultItemListingType.Login + AutofillSelectionData.Type.IDENTITY -> VaultItemListingType.Identity } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensions.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensions.kt index d7b0927540..479aa2980f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensions.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensions.kt @@ -32,6 +32,10 @@ fun AutofillSelectionData.toDefaultAddTypeContent( ), ) } + + AutofillSelectionData.Type.IDENTITY -> { + VaultAddEditState.ViewState.Content.ItemType.Identity() + } } return VaultAddEditState.ViewState.Content( common = VaultAddEditState.ViewState.Content.Common( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 9107d6871d..cc747a24ac 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -2867,6 +2867,20 @@ class VaultItemListingViewModel @Inject constructor( ) } } + + AutofillSelectionData.Type.IDENTITY -> { + this.map { vaultData -> + vaultData.copy( + decryptCipherListResult = vaultData.decryptCipherListResult.copy( + successes = vaultData + .decryptCipherListResult + .successes + .filter { it.type is CipherListViewType.Identity }, + failures = emptyList(), + ), + ) + } + } } } @@ -3039,19 +3053,23 @@ data class VaultItemListingState( val appBarTitle: Text get() = autofillSelectionData ?.let { data -> - data.uri - ?.toHostOrPathOrNull() - ?.let { - when (data.type) { - AutofillSelectionData.Type.CARD -> { - BitwardenString.select_a_card_for_x.asText(it) - } - - AutofillSelectionData.Type.LOGIN -> { - BitwardenString.items_for_uri.asText(it) - } - } + when (data.type) { + AutofillSelectionData.Type.IDENTITY -> { + BitwardenString.choose_an_identity.asText() } + + AutofillSelectionData.Type.CARD -> { + data.uri + ?.toHostOrPathOrNull() + ?.let { BitwardenString.select_a_card_for_x.asText(it) } + } + + AutofillSelectionData.Type.LOGIN -> { + data.uri + ?.toHostOrPathOrNull() + ?.let { BitwardenString.items_for_uri.asText(it) } + } + } } ?: createCredentialRequest ?.relyingPartyIdOrNull diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt index 30881e342a..81c4a4017a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/util/AutofillSelectionDataExtensionsTest.kt @@ -11,6 +11,7 @@ class AutofillSelectionDataExtensionsTest { mapOf( AutofillSelectionData.Type.CARD to VaultItemListingType.Card, AutofillSelectionData.Type.LOGIN to VaultItemListingType.Login, + AutofillSelectionData.Type.IDENTITY to VaultItemListingType.Identity, ) .forEach { (type, expected) -> assertEquals( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt index 4b0f6d9c1a..5308b5da30 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/util/AutofillSelectionDataExtensionsTest.kt @@ -39,6 +39,23 @@ class AutofillSelectionDataExtensionsTest { ) } + @Test + fun `toDefaultAddTypeContent for an Identity type should return the correct Content`() { + assertEquals( + VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + isIndividualVaultDisabled = false, + type = VaultAddEditState.ViewState.Content.ItemType.Identity(), + ), + AutofillSelectionData( + type = AutofillSelectionData.Type.IDENTITY, + framework = AutofillSelectionData.Framework.AUTOFILL, + uri = null, + ) + .toDefaultAddTypeContent(isIndividualVaultDisabled = false), + ) + } + @Test fun `toDefaultAddTypeContent for a Login type should return the correct Content`() { every { UUID.randomUUID().toString() } returns "uuid" diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index d25192a97e..faf87247bb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -1371,6 +1371,33 @@ class VaultItemListingScreenTest : BitwardenComposeTest() { .assertIsDisplayed() } + @Test + fun `topBar title should be displayed for identity autofill selection regardless of the uri`() { + mutableStateFlow.update { + it.copy( + itemListingType = VaultItemListingState.ItemListingType.Vault.Identity, + autofillSelectionData = AUTOFILL_SELECTION_DATA.copy( + type = AutofillSelectionData.Type.IDENTITY, + uri = null, + ), + ) + } + composeTestRule + .onNodeWithText(text = "Choose an identity") + .assertIsDisplayed() + + mutableStateFlow.update { + it.copy( + autofillSelectionData = AUTOFILL_SELECTION_DATA.copy( + type = AutofillSelectionData.Type.IDENTITY, + ), + ) + } + composeTestRule + .onNodeWithText(text = "Choose an identity") + .assertIsDisplayed() + } + @Test fun `on overflow item click should display menu`() { composeTestRule diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 9aee5faef8..c03b62138d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -2906,6 +2906,76 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `vaultDataStateFlow Loaded with items and autofill filtering for Identity should update ViewState to Content with filtered data`() = + runTest { + setupMockUri() + + val cipherView1 = createMockCipherListView( + number = 1, + type = CipherListViewType.Identity, + ) + val cipherView2 = createMockCipherListView( + number = 2, + type = CipherListViewType.Login( + createMockLoginListView(number = 2), + ), + ) + + val autofillSelectionData = AutofillSelectionData( + type = AutofillSelectionData.Type.IDENTITY, + framework = AutofillSelectionData.Framework.AUTOFILL, + uri = "https://www.test.com", + ) + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.AutofillSelection( + autofillSelectionData = autofillSelectionData, + shouldFinishWhenComplete = true, + ) + val dataState = DataState.Loaded( + data = VaultData( + decryptCipherListResult = createMockDecryptCipherListResult( + number = 1, + successes = listOf(cipherView1, cipherView2), + ), + folderViewList = listOf(createMockFolderView(number = 1)), + collectionViewList = listOf(createMockCollectionView(number = 1)), + sendViewList = listOf(createMockSendView(number = 1)), + ), + ) + + val viewModel = createVaultItemListingViewModel( + savedStateHandle = createSavedStateHandleWithVaultItemListingType( + vaultItemListingType = VaultItemListingType.Identity, + ), + ) + + mutableVaultDataStateFlow.value = dataState + + assertEquals( + createVaultItemListingState( + itemListingType = VaultItemListingState.ItemListingType.Vault.Identity, + viewState = VaultItemListingState.ViewState.Content( + displayCollectionList = emptyList(), + displayItemList = listOf( + createMockDisplayItemForCipher( + number = 1, + cipherType = CipherType.IDENTITY, + subtitle = "mockSubtitle-1", + secondSubtitleTestTag = "PasskeySite", + subtitleTestTag = "PasswordName", + isAutofill = true, + ), + ), + displayFolderList = emptyList(), + ), + ) + .copy(autofillSelectionData = autofillSelectionData), + viewModel.stateFlow.value, + ) + } + @Suppress("MaxLineLength") @Test fun `vaultDataStateFlow Loaded with items and totp data filtering should update ViewState to Content with filtered data`() = diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt index f08bead83a..a2d55a0bbc 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataUtil.kt @@ -97,7 +97,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_note), extraIconList = persistentListOf( IconData.Local( @@ -145,7 +145,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_payment_card), extraIconList = persistentListOf( IconData.Local( @@ -197,7 +197,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_id_card), extraIconList = persistentListOf( IconData.Local( @@ -225,9 +225,9 @@ fun createMockDisplayItemForCipher( ListingItemOverflowAction.VaultAction.ArchiveClick(cipherId = "mockId-$number"), ), optionsTestTag = "CipherOptionsButton", - isAutofill = false, - isCredentialCreation = false, - shouldShowMasterPasswordReprompt = false, + isAutofill = isAutofill, + isCredentialCreation = isCredentialCreation, + shouldShowMasterPasswordReprompt = shouldShowMasterPasswordReprompt, iconTestTag = "IdentityCipherIcon", itemType = VaultItemListingState.DisplayItem.ItemType.Vault(type = cipherType), ) @@ -241,7 +241,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_ssh_key), extraIconList = persistentListOf( IconData.Local( @@ -285,7 +285,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_payment_card), extraIconList = persistentListOf( IconData.Local( @@ -329,7 +329,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_note), extraIconList = persistentListOf( IconData.Local( @@ -373,7 +373,7 @@ fun createMockDisplayItemForCipher( secondSubtitle = secondSubtitle, secondSubtitleTestTag = secondSubtitleTestTag, subtitle = subtitle, - subtitleTestTag = "CipherSubTitleLabel", + subtitleTestTag = subtitleTestTag, iconData = IconData.Local(BitwardenDrawable.ic_note), extraIconList = persistentListOf( IconData.Local( diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index f38700c934..94d7682b02 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -1161,6 +1161,7 @@ Do you want to switch to this account? Bitwarden could not decrypt %1$d vault items. Copy and share this error report with customer success to avoid additional data loss. Select a card for %s + Choose an identity Advanced options More about match detection Keep your credentials secure