Repository: bitwarden/android
Pull request #5177: [PM-21567] Implement `CredentialEntryBuilder` interface
Author: login: SaintPatrck
Created: 2025-05-12T21:05:37Z | Merged: 2025-05-13T21:46:59Z

## 🎟️ Tracking

PM-21567

## 📔 Objective

Extract logic responsible for building CredentialEntry objects to a dedicated CredentialEntryBuilder class. This reduces the responsibilities of `BitwardenCredentialManager` and improves testability of the process and its components.

## ⏰ Reminders before review

- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Protected functional changes with optionality (feature flags)
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

## 🦮 Reviewer guidelines

<!-- Suggested interactions but feel free to use (or not) as you desire! -->

- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes



Diff:
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilder.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilder.kt
new file mode 100644
index 00000000000..89831162c1b
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilder.kt
@@ -0,0 +1,21 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.fido.Fido2CredentialAutofillView
+
+/**
+ * Builder for credential entries.
+ */
+interface CredentialEntryBuilder {
+
+    /**
+     * Build public key credential entries from the given cipher views and options.
+     */
+    fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): List<PublicKeyCredentialEntry>
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
new file mode 100644
index 00000000000..a680910b530
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
@@ -0,0 +1,96 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
+import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import kotlin.random.Random
+
+/**
+ * Primary implementation of [CredentialEntryBuilder].
+ */
+class CredentialEntryBuilderImpl(
+    private val context: Context,
+    private val intentManager: IntentManager,
+    private val featureFlagManager: FeatureFlagManager,
+    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+) : CredentialEntryBuilder {
+
+    override fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): List<PublicKeyCredentialEntry> = beginGetPublicKeyCredentialOptions
+        .flatMap { option ->
+            fido2CredentialAutofillViews
+                .toPublicKeyCredentialEntryList(
+                    userId = userId,
+                    option = option,
+                    isUserVerified = isUserVerified,
+                )
+        }
+
+    private fun List<Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
+        userId: String,
+        option: BeginGetPublicKeyCredentialOption,
+        isUserVerified: Boolean,
+    ): List<PublicKeyCredentialEntry> = this
+        .map { fido2AutofillView ->
+            PublicKeyCredentialEntry
+                .Builder(
+                    context = context,
+                    username = fido2AutofillView.userNameForUi
+                        ?: context.getString(R.string.no_username),
+                    pendingIntent = intentManager
+                        .createFido2GetCredentialPendingIntent(
+                            action = GET_PASSKEY_INTENT,
+                            userId = userId,
+                            credentialId = fido2AutofillView.credentialId.toString(),
+                            cipherId = fido2AutofillView.cipherId,
+                            isUserVerified = isUserVerified,
+                            requestCode = Random.nextInt(),
+                        ),
+                    beginGetPublicKeyCredentialOption = option,
+                )
+                .setIcon(
+                    getCredentialEntryIcon(
+                        isPasskey = true,
+                    ),
+                )
+                .also { builder ->
+                    if (!isUserVerified) {
+                        builder.setBiometricPromptDataIfSupported(
+                            cipher = biometricsEncryptionManager
+                                .getOrCreateCipher(userId),
+                            isSingleTapAuthEnabled = featureFlagManager
+                                .getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication),
+                        )
+                    }
+                }
+                .build()
+        }
+
+    // TODO: [PM-20176] Enable web icons in credential entries
+    // Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
+    // are addressed. See https://issuetracker.google.com/issues/355141766 for details.
+    private fun getCredentialEntryIcon(isPasskey: Boolean): Icon = IconCompat
+        .createWithResource(
+            context,
+            if (isPasskey) {
+                R.drawable.ic_bw_passkey
+            } else {
+                R.drawable.ic_globe
+            },
+        )
+        .toIcon(context)
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/di/CredentialProviderModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/di/CredentialProviderModule.kt
index bf4fc5f626f..41bfb715ace 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/credentials/di/CredentialProviderModule.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/di/CredentialProviderModule.kt
@@ -7,6 +7,8 @@ import com.bitwarden.data.manager.DispatcherManager
 import com.bitwarden.network.service.DigitalAssetLinkService
 import com.bitwarden.sdk.Fido2CredentialStore
 import com.x8bit.bitwarden.data.auth.repository.AuthRepository
+import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
+import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilderImpl
 import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
 import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManagerImpl
 import com.x8bit.bitwarden.data.credentials.manager.OriginManager
@@ -16,7 +18,6 @@ import com.x8bit.bitwarden.data.credentials.processor.CredentialProviderProcesso
 import com.x8bit.bitwarden.data.platform.manager.AssetManager
 import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
 import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
-import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
 import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
 import com.x8bit.bitwarden.data.vault.repository.VaultRepository
 import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -63,28 +64,20 @@ object CredentialProviderModule {
     @Provides
     @Singleton
     fun provideBitwardenCredentialManager(
-        @ApplicationContext context: Context,
-        intentManager: IntentManager,
-        featureFlagManager: FeatureFlagManager,
-        biometricsEncryptionManager: BiometricsEncryptionManager,
         vaultSdkSource: VaultSdkSource,
         fido2CredentialStore: Fido2CredentialStore,
         json: Json,
-        environmentRepository: EnvironmentRepository,
         vaultRepository: VaultRepository,
         dispatcherManager: DispatcherManager,
+        credentialEntryBuilder: CredentialEntryBuilder,
     ): BitwardenCredentialManager =
         BitwardenCredentialManagerImpl(
-            context = context,
             vaultSdkSource = vaultSdkSource,
             fido2CredentialStore = fido2CredentialStore,
-            intentManager = intentManager,
-            featureFlagManager = featureFlagManager,
-            biometricsEncryptionManager = biometricsEncryptionManager,
             json = json,
-            environmentRepository = environmentRepository,
             vaultRepository = vaultRepository,
             dispatcherManager = dispatcherManager,
+            credentialEntryBuilder = credentialEntryBuilder,
         )
 
     @Provides
@@ -97,4 +90,18 @@ object CredentialProviderModule {
             assetManager = assetManager,
             digitalAssetLinkService = digitalAssetLinkService,
         )
+
+    @Provides
+    @Singleton
+    fun provideCredentialEntryBuilder(
+        @ApplicationContext context: Context,
+        intentManager: IntentManager,
+        featureFlagManager: FeatureFlagManager,
+        biometricsEncryptionManager: BiometricsEncryptionManager,
+    ): CredentialEntryBuilder = CredentialEntryBuilderImpl(
+        context = context,
+        intentManager = intentManager,
+        featureFlagManager = featureFlagManager,
+        biometricsEncryptionManager = biometricsEncryptionManager,
+    )
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManager.kt
index 95c39f8c9ad..1eab518f501 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManager.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManager.kt
@@ -2,13 +2,13 @@ package com.x8bit.bitwarden.data.credentials.manager
 
 import androidx.credentials.CreatePublicKeyCredentialRequest
 import androidx.credentials.GetPublicKeyCredentialOption
-import androidx.credentials.provider.BeginGetCredentialOption
 import androidx.credentials.provider.CallingAppInfo
 import androidx.credentials.provider.CredentialEntry
 import androidx.credentials.provider.ProviderGetCredentialRequest
 import com.bitwarden.vault.CipherView
 import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
 import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
+import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
 import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
 import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
 
@@ -91,10 +91,9 @@ interface BitwardenCredentialManager {
 
     /**
      * Retrieve a list of [CredentialEntry] objects representing vault items matching the given
-     * request [options].
+     * [getCredentialsRequest].
      */
     suspend fun getCredentialEntries(
-        userId: String,
-        options: List<BeginGetCredentialOption>,
+        getCredentialsRequest: GetCredentialsRequest,
     ): Result<List<CredentialEntry>>
 }
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
index 5fd8e6a2edc..bca98e1a4af 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
@@ -1,52 +1,34 @@
 package com.x8bit.bitwarden.data.credentials.manager
 
-import android.content.Context
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.annotation.WorkerThread
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
-import androidx.core.graphics.drawable.IconCompat
 import androidx.credentials.CreatePublicKeyCredentialRequest
 import androidx.credentials.GetPublicKeyCredentialOption
 import androidx.credentials.exceptions.GetCredentialUnknownException
-import androidx.credentials.provider.BeginGetCredentialOption
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
-import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.CallingAppInfo
 import androidx.credentials.provider.CredentialEntry
 import androidx.credentials.provider.ProviderGetCredentialRequest
-import androidx.credentials.provider.PublicKeyCredentialEntry
-import com.bitwarden.core.annotation.OmitFromCoverage
 import com.bitwarden.core.data.repository.model.DataState
 import com.bitwarden.core.data.repository.util.takeUntilLoaded
 import com.bitwarden.core.data.util.asFailure
 import com.bitwarden.core.data.util.asSuccess
 import com.bitwarden.core.data.util.decodeFromStringOrNull
 import com.bitwarden.data.manager.DispatcherManager
-import com.bitwarden.data.repository.util.baseIconUrl
 import com.bitwarden.fido.ClientData
-import com.bitwarden.fido.Fido2CredentialAutofillView
 import com.bitwarden.fido.Origin
 import com.bitwarden.fido.UnverifiedAssetLink
 import com.bitwarden.sdk.Fido2CredentialStore
 import com.bitwarden.vault.CipherView
-import com.bumptech.glide.Glide
-import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
+import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
 import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
 import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
+import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
 import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
 import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
 import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
-import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
-import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
-import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
-import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
-import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
 import com.x8bit.bitwarden.data.platform.util.getAppOrigin
 import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
 import com.x8bit.bitwarden.data.platform.util.getSignatureFingerprintAsHexString
-import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
 import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
 import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
 import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
@@ -55,32 +37,22 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKe
 import com.x8bit.bitwarden.data.vault.repository.VaultRepository
 import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
 import com.x8bit.bitwarden.ui.platform.base.util.prefixHttpsIfNecessaryOrNull
-import com.x8bit.bitwarden.ui.platform.components.model.IconData
-import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
-import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.fold
 import kotlinx.coroutines.withContext
 import kotlinx.serialization.json.Json
 import timber.log.Timber
-import java.util.concurrent.ExecutionException
-import javax.crypto.Cipher
-import kotlin.random.Random
 
 /**
  * Primary implementation of [BitwardenCredentialManager].
  */
 @Suppress("TooManyFunctions", "LongParameterList")
 class BitwardenCredentialManagerImpl(
-    private val context: Context,
     private val vaultSdkSource: VaultSdkSource,
     private val fido2CredentialStore: Fido2CredentialStore,
-    private val intentManager: IntentManager,
-    private val featureFlagManager: FeatureFlagManager,
-    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+    private val credentialEntryBuilder: CredentialEntryBuilder,
     private val json: Json,
     private val vaultRepository: VaultRepository,
-    private val environmentRepository: EnvironmentRepository,
     dispatcherManager: DispatcherManager,
 ) : BitwardenCredentialManager,
     Fido2CredentialStore by fido2CredentialStore {
@@ -194,8 +166,7 @@ class BitwardenCredentialManagerImpl(
         ?: fallbackRequirement
 
     override suspend fun getCredentialEntries(
-        userId: String,
-        options: List<BeginGetCredentialOption>,
+        getCredentialsRequest: GetCredentialsRequest,
     ): Result<List<CredentialEntry>> = withContext(ioScope.coroutineContext) {
         val cipherViews = vaultRepository
             .ciphersStateFlow
@@ -209,33 +180,18 @@ class BitwardenCredentialManagerImpl(
                     else -> emptyList()
                 }
             }
+            .filter { it.isActiveWithFido2Credentials }
             .ifEmpty {
                 return@withContext emptyList<CredentialEntry>().asSuccess()
             }
 
-        val publicKeyCredentialOptions = options
-            .filterIsInstance<BeginGetPublicKeyCredentialOption>()
-            .ifEmpty { return@withContext emptyList<CredentialEntry>().asSuccess() }
-
-        when (
-            val decryptResult =
-                vaultRepository.getDecryptedFido2CredentialAutofillViews(cipherViews)
-        ) {
-            is DecryptFido2CredentialAutofillViewResult.Error -> {
-                GetCredentialUnknownException(
-                    "Error decrypting user's FIDO 2 credentials.",
-                )
-                    .asFailure()
-            }
-
-            is DecryptFido2CredentialAutofillViewResult.Success -> {
-                publicKeyCredentialOptions.toPublicKeyCredentialEntries(
-                    userId = userId,
-                    cipherViews = cipherViews,
-                    fido2CredentialAutofillViews = decryptResult.fido2CredentialAutofillViews,
-                )
-            }
-        }
+        getCredentialsRequest
+            .beginGetPublicKeyCredentialOptions
+            .toPublicKeyCredentialEntries(
+                userId = getCredentialsRequest.userId,
+                cipherViewsWithPublicKeyCredentials = cipherViews,
+            )
+            .onFailure { Timber.e(it, "Failed to get FIDO 2 credential entries.") }
     }
 
     private fun getPasskeyAssertionOptionsOrNull(
@@ -244,156 +200,37 @@ class BitwardenCredentialManagerImpl(
 
     private suspend fun List<BeginGetPublicKeyCredentialOption>.toPublicKeyCredentialEntries(
         userId: String,
-        cipherViews: List<CipherView>,
-        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
-    ): Result<List<PublicKeyCredentialEntry>> {
-        val baseIconUrl = environmentRepository
-            .environment
-            .environmentUrlData
-            .baseIconUrl
-
-        var options: PasskeyAssertionOptions
-        var relyingPartyId: String
-        return this
-            .flatMap { option ->
-                options = getPasskeyAssertionOptionsOrNull(option.requestJson)
-                    ?: return GetCredentialUnknownException(
-                        "Invalid passkey request. Could not deserialize request options.",
-                    )
-                        .asFailure()
-
-                relyingPartyId = options.relyingPartyId
-                    ?: return GetCredentialUnknownException(
-                        "Invalid passkey request. Relying party ID is required.",
-                    )
-                        .asFailure()
-
-                val autofillViews = fido2CredentialAutofillViews
-                    .filter { it.rpId == relyingPartyId }
-                    .ifEmpty {
-                        return emptyList<PublicKeyCredentialEntry>().asSuccess()
-                    }
-
-                val cipherIdsToMatch = autofillViews
-                    .map { it.cipherId }
-                    .toSet()
-
-                cipherViews
-                    .filter { cipherView -> cipherView.id in cipherIdsToMatch }
-                    .associateWith { cipherView ->
-                        // We can safely call first() here because we know the cipherId exists in
-                        // the collection of autofill views.
-                        autofillViews.first { it.cipherId == cipherView.id }
-                    }
-                    .toPublicKeyCredentialEntryList(
-                        baseIconUrl = baseIconUrl,
-                        userId = userId,
-                        option = option,
-                    )
+        cipherViewsWithPublicKeyCredentials: List<CipherView>,
+    ): Result<List<CredentialEntry>> {
+        val relyingPartyIds = this
+            .mapNotNull { getPasskeyAssertionOptionsOrNull(it.requestJson)?.relyingPartyId }
+            .distinct()
+            .ifEmpty {
+                return GetCredentialUnknownException("Relying party id required.").asFailure()
             }
-            .asSuccess()
-    }
 
-    private suspend fun Map<CipherView, Fido2CredentialAutofillView>.toPublicKeyCredentialEntryList(
-        baseIconUrl: String,
-        userId: String,
-        option: BeginGetPublicKeyCredentialOption,
-    ): List<PublicKeyCredentialEntry> = this.map { (cipherView, autofillView) ->
-        val loginIconData = cipherView.login
-            ?.uris
-            .toLoginIconData(
-                // TODO: [PM-20176] Enable web icons in passkey credential entries
-                // Leave web icons disabled until CredentialManager TransactionTooLargeExceptions
-                // are addressed. See https://issuetracker.google.com/issues/355141766 for details.
-                isIconLoadingDisabled = true,
-                baseIconUrl = baseIconUrl,
-                usePasskeyDefaultIcon = true,
-            )
-        val iconCompat = when (loginIconData) {
-            is IconData.Local -> {
-                IconCompat.createWithResource(context, loginIconData.iconRes)
-            }
+        val decryptResult = vaultRepository
+            .getDecryptedFido2CredentialAutofillViews(cipherViewsWithPublicKeyCredentials)
 
-            is IconData.Network -> {
-                loginIconData.toIconCompat()
+        return when (decryptResult) {
+            is DecryptFido2CredentialAutofillViewResult.Error -> {
+                GetCredentialUnknownException("Error decrypting credentials.").asFailure()
             }
-        }
 
-        val pkEntryBuilder = PublicKeyCredentialEntry
-            .Builder(
-                context = context,
-                username = autofillView.userNameForUi
-                    ?: context.getString(R.string.no_username),
-                pendingIntent = intentManager
-                    .createFido2GetCredentialPendingIntent(
-                        action = GET_PASSKEY_INTENT,
+            is DecryptFido2CredentialAutofillViewResult.Success -> {
+                credentialEntryBuilder
+                    .buildPublicKeyCredentialEntries(
                         userId = userId,
-                        credentialId = autofillView.credentialId.toString(),
-                        cipherId = autofillView.cipherId,
+                        fido2CredentialAutofillViews = decryptResult
+                            .fido2CredentialAutofillViews
+                            .filter { it.rpId in relyingPartyIds },
+                        beginGetPublicKeyCredentialOptions = this,
                         isUserVerified = isUserVerified,
-                        requestCode = Random.nextInt(),
-                    ),
-                beginGetPublicKeyCredentialOption = option,
-            )
-            .setIcon(iconCompat.toIcon(context))
-
-        if (featureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication) &&
-            !isUserVerified
-        ) {
-            biometricsEncryptionManager
-                .getOrCreateCipher(userId)
-                ?.let { cipher ->
-                    pkEntryBuilder
-                        .setBiometricPromptDataIfSupported(cipher = cipher)
-                }
-        }
-
-        pkEntryBuilder.build()
-    }
-
-    private fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
-        cipher: Cipher,
-    ): PublicKeyCredentialEntry.Builder =
-        if (isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM)) {
-            this
-        } else {
-            setBiometricPromptData(
-                biometricPromptData = buildPromptDataWithCipher(cipher),
-            )
+                    )
+                    .asSuccess()
+            }
         }
-
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    private fun buildPromptDataWithCipher(
-        cipher: Cipher,
-    ): BiometricPromptData = BiometricPromptData.Builder()
-        .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
-        .setCryptoObject(BiometricPrompt.CryptoObject(cipher))
-        .build()
-
-    /**
-     * Converts a network icon to an [IconCompat]. Performs a blocking network request to fetch the
-     * icon, so only call this method from a background thread or coroutine.
-     */
-    @OmitFromCoverage
-    @WorkerThread
-    private suspend fun IconData.Network.toIconCompat(): IconCompat = try {
-        val futureTargetBitmap = Glide
-            .with(context)
-            .asBitmap()
-            .load(this.uri)
-            .placeholder(R.drawable.ic_bw_passkey)
-            .submit()
-
-        IconCompat.createWithBitmap(futureTargetBitmap.get())
-    } catch (_: ExecutionException) {
-        null
-    } catch (_: InterruptedException) {
-        null
     }
-        ?: IconCompat.createWithResource(
-            context,
-            this.fallbackIconRes,
-        )
 
     private suspend fun registerFido2CredentialForUnprivilegedApp(
         userId: String,
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/GetCredentialsRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/GetCredentialsRequest.kt
index bf13e98f32f..2df9df844bc 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/GetCredentialsRequest.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/GetCredentialsRequest.kt
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.credentials.model
 import android.os.Bundle
 import android.os.Parcelable
 import androidx.credentials.provider.BeginGetCredentialRequest
+import androidx.credentials.provider.BeginGetPasswordOption
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
 import androidx.credentials.provider.CallingAppInfo
 import kotlinx.parcelize.IgnoredOnParcel
@@ -30,7 +31,7 @@ data class GetCredentialsRequest(
 
     /**
      * The [BeginGetPublicKeyCredentialOption]s of the [providerRequest], or an empty list if no
-     * public key credentials are present.
+     * public key options are present.
      */
     @IgnoredOnParcel
     val beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption> by lazy {
@@ -40,6 +41,18 @@ data class GetCredentialsRequest(
             .orEmpty()
     }
 
+    /**
+     * The [BeginGetPasswordOption]s of the [providerRequest], or an empty list if no password
+     * options are present.
+     */
+    @IgnoredOnParcel
+    val beginGetPasswordOptions: List<BeginGetPasswordOption> by lazy {
+        providerRequest
+            ?.beginGetCredentialOptions
+            ?.filterIsInstance<BeginGetPasswordOption>()
+            .orEmpty()
+    }
+
     /**
      * The [CallingAppInfo] of the [providerRequest], or null if the [providerRequest] is not a
      * [BeginGetCredentialRequest].
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/ProviderGetPasswordCredentialRequest.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/ProviderGetPasswordCredentialRequest.kt
new file mode 100644
index 00000000000..a9a04e31219
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/model/ProviderGetPasswordCredentialRequest.kt
@@ -0,0 +1,23 @@
+package com.x8bit.bitwarden.data.credentials.model
+
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.credentials.provider.ProviderGetCredentialRequest
+import kotlinx.parcelize.Parcelize
+
+/**
+ * A wrapper around [ProviderGetCredentialRequest] that includes additional information needed to
+ * fulfill the request.
+ *
+ * @param userId The ID of the user that owns the credential being requested.
+ * @param cipherId The ID of the cipher containing the password to be retrieved.
+ * @param isUserVerified Whether the user has been verified prior to this request.
+ * @param requestData The original request data from the system.
+ */
+@Parcelize
+data class ProviderGetPasswordCredentialRequest(
+    val userId: String,
+    val cipherId: String,
+    val isUserVerified: Boolean,
+    val requestData: Bundle,
+) : Parcelable
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorImpl.kt
index 4d8b47c6556..c0de02c3008 100644
--- a/app/src/main/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorImpl.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorImpl.kt
@@ -22,16 +22,15 @@ import androidx.credentials.provider.BeginCreateCredentialResponse
 import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
 import androidx.credentials.provider.BeginGetCredentialRequest
 import androidx.credentials.provider.BeginGetCredentialResponse
-import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
 import androidx.credentials.provider.BiometricPromptData
 import androidx.credentials.provider.CreateEntry
-import androidx.credentials.provider.CredentialEntry
 import androidx.credentials.provider.ProviderClearCredentialStateRequest
 import com.bitwarden.data.manager.DispatcherManager
 import com.x8bit.bitwarden.R
 import com.x8bit.bitwarden.data.auth.repository.AuthRepository
 import com.x8bit.bitwarden.data.auth.repository.model.UserState
 import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
+import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
 import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
 import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
 import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
@@ -124,15 +123,14 @@ class CredentialProviderProcessorImpl(
 
         // Otherwise, find all matching credentials from the current vault.
         val getCredentialJob = ioScope.launch {
-            getMatchingFido2CredentialEntries(
-                userId = userState.activeUserId,
-                request = request,
-            )
-                .onSuccess {
-                    callback.onResult(
-                        BeginGetCredentialResponse(credentialEntries = it),
-                    )
-                }
+            bitwardenCredentialManager
+                .getCredentialEntries(
+                    getCredentialsRequest = GetCredentialsRequest(
+                        userId = userState.activeUserId,
+                        BeginGetCredentialRequest.asBundle(request),
+                    ),
+                )
+                .onSuccess { callback.onResult(BeginGetCredentialResponse(credentialEntries = it)) }
                 .onFailure { callback.onError(GetCredentialUnknownException(it.message)) }
         }
         cancellationSignal.setOnCancelListener {
@@ -213,18 +211,6 @@ class CredentialProviderProcessorImpl(
         return entryBuilder.build()
     }
 
-    private suspend fun getMatchingFido2CredentialEntries(
-        userId: String,
-        request: BeginGetCredentialRequest,
-    ): Result<List<CredentialEntry>> =
-        bitwardenCredentialManager
-            .getCredentialEntries(
-                userId = userId,
-                options = request
-                    .beginGetCredentialOptions
-                    .filterIsInstance<BeginGetPublicKeyCredentialOption>(),
-            )
-
     private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
         cipher: Cipher,
     ): CreateEntry.Builder {
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/BiometricPromptDataUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/BiometricPromptDataUtils.kt
new file mode 100644
index 00000000000..99c4fe1bd76
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/BiometricPromptDataUtils.kt
@@ -0,0 +1,22 @@
+@file:OmitFromCoverage
+
+package com.x8bit.bitwarden.data.credentials.util
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.credentials.provider.BiometricPromptData
+import com.bitwarden.core.annotation.OmitFromCoverage
+import javax.crypto.Cipher
+
+/**
+ * Builds a [BiometricPromptData] instance with the provided [Cipher].
+ */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+fun buildPromptDataWithCipher(
+    cipher: Cipher,
+): BiometricPromptData = BiometricPromptData.Builder()
+    .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
+    .setCryptoObject(BiometricPrompt.CryptoObject(cipher))
+    .build()
diff --git a/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/PublicKeyCredentialEntryBuilderExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/PublicKeyCredentialEntryBuilderExtensions.kt
new file mode 100644
index 00000000000..823e37ba6f8
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/data/credentials/util/PublicKeyCredentialEntryBuilderExtensions.kt
@@ -0,0 +1,27 @@
+@file:OmitFromCoverage
+
+package com.x8bit.bitwarden.data.credentials.util
+
+import android.os.Build
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.core.annotation.OmitFromCoverage
+import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
+import javax.crypto.Cipher
+
+/**
+ * Sets the biometric prompt data on the [PublicKeyCredentialEntry.Builder] if supported.
+ */
+fun PublicKeyCredentialEntry.Builder.setBiometricPromptDataIfSupported(
+    cipher: Cipher?,
+    isSingleTapAuthEnabled: Boolean,
+): PublicKeyCredentialEntry.Builder =
+    if (!isBuildVersionBelow(Build.VERSION_CODES.VANILLA_ICE_CREAM) &&
+        cipher != null &&
+        isSingleTapAuthEnabled
+    ) {
+        setBiometricPromptData(
+            biometricPromptData = buildPromptDataWithCipher(cipher),
+        )
+    } else {
+        this
+    }
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
index 6b4061b63fb..1686a5144b4 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
 import android.os.Parcelable
 import androidx.annotation.DrawableRes
 import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.provider.CredentialEntry
 import androidx.credentials.provider.ProviderCreateCredentialRequest
 import androidx.credentials.provider.ProviderGetCredentialRequest
 import androidx.lifecycle.SavedStateHandle
@@ -1252,6 +1253,10 @@ class VaultItemListingViewModel @Inject constructor(
             VaultItemListingsAction.Internal.InternetConnectionErrorReceived -> {
                 handleInternetConnectionErrorReceived()
             }
+
+            is VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive -> {
+                handleGetCredentialEntriesResultReceive(action)
+            }
         }
     }
 
@@ -1689,14 +1694,6 @@ class VaultItemListingViewModel @Inject constructor(
     private fun handleProviderGetCredentialsRequest(
         request: GetCredentialsRequest,
     ) {
-        val beginGetCredentialOption = request
-            .beginGetPublicKeyCredentialOptions
-            .ifEmpty {
-                showCredentialManagerErrorDialog(
-                    R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
-                )
-                return
-            }
         val callingAppInfo = request.callingAppInfo
             ?: run {
                 showCredentialManagerErrorDialog(
@@ -1711,17 +1708,11 @@ class VaultItemListingViewModel @Inject constructor(
             )
             when (validateOriginResult) {
                 is ValidateOriginResult.Success -> {
-                    sendEvent(
-                        VaultItemListingEvent.CompleteProviderGetCredentialsRequest(
-                            GetCredentialsResult.Success(
-                                credentialEntries = bitwardenCredentialManager
-                                    .getCredentialEntries(
-                                        userId = request.userId,
-                                        options = beginGetCredentialOption,
-                                    )
-                                    .getOrNull()
-                                    .orEmpty(),
-                                userId = request.userId,
+                    sendAction(
+                        VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive(
+                            userId = request.userId,
+                            result = bitwardenCredentialManager.getCredentialEntries(
+                                getCredentialsRequest = request,
                             ),
                         ),
                     )
@@ -1860,6 +1851,27 @@ class VaultItemListingViewModel @Inject constructor(
         }
     }
 
+    private fun handleGetCredentialEntriesResultReceive(
+        action: VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive,
+    ) {
+        action.result
+            .onFailure {
+                showCredentialManagerErrorDialog(
+                    message = R.string.generic_error_message.asText(),
+                )
+            }
+            .onSuccess { credentialEntries ->
+                sendEvent(
+                    VaultItemListingEvent.CompleteProviderGetCredentialsRequest(
+                        GetCredentialsResult.Success(
+                            credentialEntries = credentialEntries,
+                            userId = action.userId,
+                        ),
+                    ),
+                )
+            }
+    }
+
     private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
         mutableStateFlow.update { currentState ->
             currentState.copy(
@@ -2907,6 +2919,14 @@ sealed class VaultItemListingsAction {
          * Indicates that the there is not internet connection.
          */
         data object InternetConnectionErrorReceived : Internal()
+
+        /**
+         * Indicates that a result for building credential entries has been received.
+         */
+        data class GetCredentialEntriesResultReceive(
+            val userId: String,
+            val result: Result<List<CredentialEntry>>,
+        ) : Internal()
     }
 }
 
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderTest.kt
new file mode 100644
index 00000000000..86827075092
--- /dev/null
+++ b/app/src/test/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderTest.kt
@@ -0,0 +1,243 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
+import com.x8bit.bitwarden.data.util.mockBuilder
+import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2CredentialAutofillView
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkConstructor
+import io.mockk.mockkStatic
+import io.mockk.unmockkConstructor
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class CredentialEntryBuilderTest {
+
+    private val mockContext = mockk<Context>()
+    private val mockGetPublicKeyCredentialIntent = mockk<PendingIntent>(relaxed = true)
+    private val mockIntentManager = mockk<IntentManager> {
+        every {
+            createFido2GetCredentialPendingIntent(
+                action = any(),
+                userId = any(),
+                cipherId = any(),
+                credentialId = any(),
+                requestCode = any(),
+                isUserVerified = any(),
+            )
+        } returns mockGetPublicKeyCredentialIntent
+    }
+    private val mockFeatureFlagManager = mockk<FeatureFlagManager>()
+    private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
+    private val mockBeginGetPublicKeyOption = mockk<BeginGetPublicKeyCredentialOption>()
+    private val credentialEntryBuilder = CredentialEntryBuilderImpl(
+        context = mockContext,
+        intentManager = mockIntentManager,
+        featureFlagManager = mockFeatureFlagManager,
+        biometricsEncryptionManager = mockBiometricsEncryptionManager,
+    )
+    private val mockPublicKeyCredentialEntry = mockk<PublicKeyCredentialEntry>(relaxed = true)
+    private val mockIcon = mockk<Icon>()
+
+    @BeforeEach
+    @Test
+    fun setUp() {
+        mockkConstructor(PublicKeyCredentialEntry.Builder::class)
+        mockkStatic(IconCompat::class)
+        mockBuilder<PublicKeyCredentialEntry.Builder> { it.setIcon(any()) }
+        every { IconCompat.createWithResource(any(), any()) } returns mockk {
+            every { toIcon(mockContext) } returns mockIcon
+        }
+        every {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().build()
+        } returns mockPublicKeyCredentialEntry
+    }
+
+    @AfterEach
+    @Test
+    fun tearDown() {
+        unmockkStatic(IconCompat::class)
+        unmockkStatic(::isBuildVersionBelow)
+        unmockkConstructor(PublicKeyCredentialEntry.Builder::class)
+    }
+
+    @Suppress("MaxLineLength")
+    @Test
+    fun `buildPublicKeyCredentialEntries should return Success with empty list when options list is empty`() =
+        runTest {
+            val options = emptyList<BeginGetPublicKeyCredentialOption>()
+            val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
+                createMockFido2CredentialAutofillView(number = 1),
+            )
+
+            val result = credentialEntryBuilder
+                .buildPublicKeyCredentialEntries(
+                    userId = "userId",
+                    isUserVerified = false,
+                    fido2CredentialAutofillViews = fido2AutofillViews,
+                    beginGetPublicKeyCredentialOptions = options,
+                )
+            assertTrue(result.isEmpty())
+        }
+
+    @Suppress("MaxLineLength")
+    @Test
+    fun `buildPublicKeyCredentialEntries should return Success with empty list when fido2AutofillViews is empty`() =
+        runTest {
+            val options = listOf(mockBeginGetPublicKeyOption)
+            val fido2AutofillViews = emptyList<Fido2CredentialAutofillView>()
+            val result = credentialEntryBuilder
+                .buildPublicKeyCredentialEntries(
+                    userId = "userId",
+                    isUserVerified = false,
+                    fido2CredentialAutofillViews = fido2AutofillViews,
+                    beginGetPublicKeyCredentialOptions = options,
+                )
+            assertTrue(result.isEmpty())
+        }
+
+    @Suppress("MaxLineLength")
+    @Test
+    fun `buildPublicKeyCredentialEntries should return Success with list of PublicKeyCredentialEntry`() =
+        runTest {
+            val options = listOf(mockBeginGetPublicKeyOption)
+            val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
+                createMockFido2CredentialAutofillView(number = 1),
+            )
+
+            every {
+                mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
+            } returns false
+            every {
+                mockBiometricsEncryptionManager.getOrCreateCipher("userId")
+            } returns null
+
+            val result = credentialEntryBuilder
+                .buildPublicKeyCredentialEntries(
+                    userId = "userId",
+                    isUserVerified = false,
+                    fido2CredentialAutofillViews = fido2AutofillViews,
+                    beginGetPublicKeyCredentialOptions = options,
+                )
+
+            assertTrue(result.isNotEmpty())
+
+            verify {
+                mockIntentManager.createFido2GetCredentialPendingIntent(
+                    action = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY",
+                    userId = "userId",
+                    cipherId = "mockCipherId-1",
+                    credentialId = fido2AutofillViews.first().credentialId.toString(),
+                    requestCode = any(),
+                    isUserVerified = false,
+                )
+
+                anyConstructed<PublicKeyCredentialEntry.Builder>().setIcon(mockIcon)
+            }
+        }
+
+    @Test
+    fun `buildPublicKeyCredentialEntries should set biometric prompt data correctly`() = runTest {
+        mockkStatic(::isBuildVersionBelow)
+        val options = listOf(mockBeginGetPublicKeyOption)
+        val fido2AutofillViews: List<Fido2CredentialAutofillView> = listOf(
+            createMockFido2CredentialAutofillView(number = 1),
+        )
+
+        // Verify biometric prompt data is not set when flag is false, buildVersion is < 35, and
+        // cipher is null.
+        every {
+            mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
+        } returns false
+        every {
+            mockBiometricsEncryptionManager.getOrCreateCipher("userId")
+        } returns null
+        every { isBuildVersionBelow(any()) } returns false
+
+        credentialEntryBuilder
+            .buildPublicKeyCredentialEntries(
+                userId = "userId",
+                isUserVerified = false,
+                fido2CredentialAutofillViews = fido2AutofillViews,
+                beginGetPublicKeyCredentialOptions = options,
+            )
+        verify(exactly = 0) {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
+        }
+
+        // Verify biometric prompt data is not set when flag is true, buildVersion is < 35, and
+        // cipher is null.
+        every {
+            mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
+        } returns true
+        credentialEntryBuilder
+            .buildPublicKeyCredentialEntries(
+                userId = "userId",
+                isUserVerified = false,
+                fido2CredentialAutofillViews = fido2AutofillViews,
+                beginGetPublicKeyCredentialOptions = options,
+            )
+
+        verify(exactly = 0) {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
+        }
+
+        // Verify biometric prompt data is not set when flag is true, buildVersion is >= 35, and
+        // cipher is null
+        every { isBuildVersionBelow(any()) } returns false
+        credentialEntryBuilder
+            .buildPublicKeyCredentialEntries(
+                userId = "userId",
+                isUserVerified = false,
+                fido2CredentialAutofillViews = fido2AutofillViews,
+                beginGetPublicKeyCredentialOptions = options,
+            )
+        verify(exactly = 0) {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
+        }
+
+        // Verify biometric prompt data is not set when user is verified
+        every {
+            mockBiometricsEncryptionManager.getOrCreateCipher(any())
+        } returns mockk(relaxed = true)
+        credentialEntryBuilder
+            .buildPublicKeyCredentialEntries(
+                userId = "userId",
+                isUserVerified = true,
+                fido2CredentialAutofillViews = fido2AutofillViews,
+                beginGetPublicKeyCredentialOptions = options,
+            )
+        verify(exactly = 0) {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
+        }
+
+        // Verify biometric prompt data is set when flag is true, buildVersion is >= 35, cipher is
+        // not null, and user is not verified
+        credentialEntryBuilder
+            .buildPublicKeyCredentialEntries(
+                userId = "userId",
+                isUserVerified = false,
+                fido2CredentialAutofillViews = fido2AutofillViews,
+                beginGetPublicKeyCredentialOptions = options,
+            )
+        verify(exactly = 1) {
+            anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
+        }
+    }
+}
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt
index 5dac6fef24c..4bc67a3e432 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerTest.kt
@@ -1,9 +1,7 @@
 package com.x8bit.bitwarden.data.credentials.manager
 
-import android.content.Context
 import android.content.pm.Signature
 import android.content.pm.SigningInfo
-import android.graphics.drawable.Icon
 import android.net.Uri
 import android.util.Base64
 import androidx.core.graphics.drawable.IconCompat
@@ -25,20 +23,18 @@ import com.bitwarden.fido.PublicKeyCredentialAuthenticatorAssertionResponse
 import com.bitwarden.fido.UnverifiedAssetLink
 import com.bitwarden.sdk.BitwardenException
 import com.bitwarden.sdk.Fido2CredentialStore
+import com.bitwarden.vault.CipherView
+import com.x8bit.bitwarden.data.credentials.builder.CredentialEntryBuilder
 import com.x8bit.bitwarden.data.credentials.model.Fido2AttestationResponse
 import com.x8bit.bitwarden.data.credentials.model.Fido2CredentialAssertionResult
 import com.x8bit.bitwarden.data.credentials.model.Fido2PublicKeyCredential
 import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
+import com.x8bit.bitwarden.data.credentials.model.GetCredentialsRequest
 import com.x8bit.bitwarden.data.credentials.model.PasskeyAssertionOptions
 import com.x8bit.bitwarden.data.credentials.model.PasskeyAttestationOptions
 import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
-import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
-import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
-import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
-import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
 import com.x8bit.bitwarden.data.platform.util.getAppSigningSignatureFingerprint
 import com.x8bit.bitwarden.data.platform.util.isBuildVersionBelow
-import com.x8bit.bitwarden.data.util.mockBuilder
 import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource
 import com.x8bit.bitwarden.data.vault.datasource.sdk.model.AuthenticateFido2CredentialRequest
 import com.x8bit.bitwarden.data.vault.datasource.sdk.model.RegisterFido2CredentialRequest
@@ -51,14 +47,12 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSdkFido2Cre
 import com.x8bit.bitwarden.data.vault.datasource.sdk.util.toAndroidFido2PublicKeyCredential
 import com.x8bit.bitwarden.data.vault.repository.VaultRepository
 import com.x8bit.bitwarden.data.vault.repository.model.DecryptFido2CredentialAutofillViewResult
-import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
 import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
 import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
 import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.every
 import io.mockk.mockk
-import io.mockk.mockkConstructor
 import io.mockk.mockkStatic
 import io.mockk.slot
 import io.mockk.unmockkConstructor
@@ -82,6 +76,9 @@ class BitwardenCredentialManagerTest {
 
     private lateinit var bitwardenCredentialManager: BitwardenCredentialManager
 
+    private val mutableCipherStateFlow =
+        MutableStateFlow<DataState<List<CipherView>>>(DataState.Loading)
+
     private val json = mockk<Json> {
         every {
             decodeFromStringOrNull<PasskeyAttestationOptions>(any())
@@ -122,11 +119,10 @@ class BitwardenCredentialManagerTest {
     }
     private val mockVaultSdkSource = mockk<VaultSdkSource>()
     private val mockFido2CredentialStore = mockk<Fido2CredentialStore>()
-    private val mockIntentManager = mockk<IntentManager>()
-    private val mockVaultRepository = mockk<VaultRepository>()
-    private val mockFeatureFlagManager = mockk<FeatureFlagManager>()
-    private val mockBiometricsEncryptionManager = mockk<BiometricsEncryptionManager>()
-    private val fakeEnvironmentRepository = FakeEnvironmentRepository()
+    private val mockVaultRepository = mockk<VaultRepository> {
+        every { ciphersStateFlow } returns mutableCipherStateFlow
+    }
+    private val mockCredentialEntryBuilder = mockk<CredentialEntryBuilder>()
 
     @BeforeEach
     fun setUp() {
@@ -138,16 +134,12 @@ class BitwardenCredentialManagerTest {
         every { Base64.encodeToString(any(), any()) } returns DEFAULT_APP_SIGNATURE
 
         bitwardenCredentialManager = BitwardenCredentialManagerImpl(
-            context = mockk(relaxed = true),
             vaultSdkSource = mockVaultSdkSource,
             fido2CredentialStore = mockFido2CredentialStore,
             json = json,
-            intentManager = mockIntentManager,
             dispatcherManager = FakeDispatcherManager(),
             vaultRepository = mockVaultRepository,
-            featureFlagManager = mockFeatureFlagManager,
-            biometricsEncryptionManager = mockBiometricsEncryptionManager,
-            environmentRepository = fakeEnvironmentRepository,
+            credentialEntryBuilder = mockCredentialEntryBuilder,
         )
     }
 
@@ -879,65 +871,75 @@ class BitwardenCredentialManagerTest {
             )
         }
 
+    @Suppress("MaxLineLength")
     @Test
-    fun `getCredentialEntries should return empty list when cipherViews are empty`() =
+    fun `getCredentialEntries with public key credential options should return empty list when no ciphers have FIDO 2 credentials`() =
         runTest {
             val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
+            val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
+                every { callingAppInfo } returns mockCallingAppInfo
+                every {
+                    beginGetPublicKeyCredentialOptions
+                } returns listOf(mockBeginGetPublicKeyCredentialOption)
+            }
             every {
                 mockBeginGetPublicKeyCredentialOption.requestJson
             } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
-            every {
-                mockVaultRepository.ciphersStateFlow
-            } returns MutableStateFlow(DataState.Loaded(emptyList()))
-            val result = bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockBeginGetPublicKeyCredentialOption),
-            )
+            mutableCipherStateFlow.value = DataState.Loaded(emptyList())
+            val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
             assertEquals(emptyList<CredentialEntry>(), result.getOrNull())
         }
 
     @Suppress("MaxLineLength")
     @Test
-    fun `getCredentialEntries should return error when FIDO 2 credential decryption fails`() = runTest {
-        val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
-        every {
-            mockBeginGetPublicKeyCredentialOption.requestJson
-        } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
-        every {
-            mockVaultRepository.ciphersStateFlow
-        } returns MutableStateFlow(
-            DataState.Loaded(
+    fun `getCredentialEntries with public key credential options should return error when FIDO 2 credential decryption fails`() =
+        runTest {
+            val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
+            val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
+                every { callingAppInfo } returns mockCallingAppInfo
+                every {
+                    beginGetPublicKeyCredentialOptions
+                } returns listOf(mockBeginGetPublicKeyCredentialOption)
+                every { beginGetPasswordOptions } returns emptyList()
+                every { userId } returns "mockUserId"
+            }
+            every {
+                mockBeginGetPublicKeyCredentialOption.requestJson
+            } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
+            mutableCipherStateFlow.value = DataState.Loaded(
                 listOf(
                     createMockCipherView(
                         number = 1,
                         fido2Credentials = createMockSdkFido2CredentialList(number = 1),
                     ),
                 ),
-            ),
-        )
-        coEvery {
-            mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
-        } returns DecryptFido2CredentialAutofillViewResult.Error(
-            BitwardenException.E("Error decrypting credentials."),
-        )
-        val result = bitwardenCredentialManager.getCredentialEntries(
-            userId = "mockUserId",
-            options = listOf(mockBeginGetPublicKeyCredentialOption),
-        )
-        assertTrue(result.isFailure)
-        assertTrue(result.exceptionOrNull() is GetCredentialUnknownException)
-    }
+            )
+            coEvery {
+                mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
+            } returns DecryptFido2CredentialAutofillViewResult.Error(
+                BitwardenException.E("Error decrypting credentials."),
+            )
+            val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
+            assertTrue(result.isFailure)
+            assertTrue(result.exceptionOrNull() is GetCredentialUnknownException)
+        }
 
+    @Suppress("MaxLineLength")
     @Test
-    fun `getCredentialEntries should return error when passkey assertion options are null`() =
+    fun `getCredentialEntries with public key credential options should return error when passkey assertion options are null`() =
         runTest {
-            val mockRequest = mockk<BeginGetPublicKeyCredentialOption> {
+            val mockOption = mockk<BeginGetPublicKeyCredentialOption> {
                 every { requestJson } returns ""
             }
-            every {
-                mockVaultRepository.ciphersStateFlow
-            } returns MutableStateFlow(
-                DataState.Loaded(
+            val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
+                every { callingAppInfo } returns mockCallingAppInfo
+                every {
+                    beginGetPublicKeyCredentialOptions
+                } returns listOf(mockOption)
+                every { beginGetPasswordOptions } returns emptyList()
+                every { userId } returns "mockUserId"
+            }
+            mutableCipherStateFlow.value = DataState.Loaded(
                     listOf(
                         createMockCipherView(
                             number = 1,
@@ -945,7 +947,6 @@ class BitwardenCredentialManagerTest {
                             fido2Credentials = createMockSdkFido2CredentialList(number = 1),
                         ),
                     ),
-                ),
             )
             coEvery {
                 mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
@@ -961,25 +962,28 @@ class BitwardenCredentialManagerTest {
             every {
                 json.decodeFromStringOrNull<PasskeyAssertionOptions>(any())
             } returns null
-            val result = bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockRequest),
-            )
+            val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
             assertTrue(
                 result.exceptionOrNull() is GetCredentialUnknownException,
             )
         }
 
+    @Suppress("MaxLineLength")
     @Test
-    fun `getCredentialEntries should return error when FIDO 2 relyingPartyId is null`() =
+    fun `getCredentialEntries with public key credential options should return error when FIDO 2 relyingPartyId is null`() =
         runTest {
-            val mockRequest = mockk<BeginGetPublicKeyCredentialOption> {
+            val mockOption = mockk<BeginGetPublicKeyCredentialOption> {
                 every { requestJson } returns ""
             }
-            every {
-                mockVaultRepository.ciphersStateFlow
-            } returns MutableStateFlow(
-                DataState.Loaded(
+            val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
+                every { callingAppInfo } returns mockCallingAppInfo
+                every {
+                    beginGetPublicKeyCredentialOptions
+                } returns listOf(mockOption)
+                every { beginGetPasswordOptions } returns emptyList()
+                every { userId } returns "mockUserId"
+            }
+            mutableCipherStateFlow.value = DataState.Loaded(
                     listOf(
                         createMockCipherView(
                             number = 1,
@@ -987,7 +991,6 @@ class BitwardenCredentialManagerTest {
                             fido2Credentials = createMockSdkFido2CredentialList(number = 1),
                         ),
                     ),
-                ),
             )
             coEvery {
                 mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
@@ -1003,10 +1006,8 @@ class BitwardenCredentialManagerTest {
             every {
                 json.decodeFromStringOrNull<PasskeyAssertionOptions>(any())
             } returns createMockPasskeyAssertionOptions(number = 1, relyingPartyId = null)
-            val result = bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockRequest),
-            )
+            val result = bitwardenCredentialManager
+                .getCredentialEntries(mockGetCredentialsRequest)
             assertTrue(
                 result.exceptionOrNull() is GetCredentialUnknownException,
             )
@@ -1014,116 +1015,56 @@ class BitwardenCredentialManagerTest {
 
     @Suppress("MaxLineLength")
     @Test
-    fun `getCredentialEntries should return list of PublicKeyCredentialEntry when FIDO 2 credential decryption succeeds`() =
+    fun `getCredentialEntries should build public key credential entries when decryption succeeds`() =
         runTest {
-            setupMockUri()
-            mockkStatic(IconCompat::class)
-            mockkStatic(::isBuildVersionBelow)
-            mockkConstructor(PublicKeyCredentialEntry.Builder::class)
-            every {
-                anyConstructed<PublicKeyCredentialEntry.Builder>().build()
-            } returns mockk()
             val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption>()
+            val mockGetCredentialsRequest = mockk<GetCredentialsRequest> {
+                every { callingAppInfo } returns mockCallingAppInfo
+                every {
+                    beginGetPublicKeyCredentialOptions
+                } returns listOf(mockBeginGetPublicKeyCredentialOption)
+                every { userId } returns "mockUserId"
+            }
+            val fido2CredentialAutofillViews = listOf(
+                createMockFido2CredentialAutofillView(
+                    number = 1,
+                    cipherId = "mockId-1",
+                    rpId = "mockRelyingPartyId-1",
+                ),
+            )
             every {
                 mockBeginGetPublicKeyCredentialOption.requestJson
             } returns DEFAULT_FIDO2_AUTH_REQUEST_JSON
-            every {
-                mockVaultRepository.ciphersStateFlow
-            } returns MutableStateFlow(
-                DataState.Loaded(
-                    listOf(
-                        createMockCipherView(
-                            number = 1,
-                            login = createMockLoginView(number = 1, hasUris = false),
-                            fido2Credentials = createMockSdkFido2CredentialList(number = 1),
-                        ),
+            mutableCipherStateFlow.value = DataState.Loaded(
+                listOf(
+                    createMockCipherView(
+                        number = 1,
+                        login = createMockLoginView(number = 1, hasUris = false),
+                        fido2Credentials = createMockSdkFido2CredentialList(number = 1),
                     ),
                 ),
             )
             coEvery {
                 mockVaultRepository.getDecryptedFido2CredentialAutofillViews(any())
             } returns DecryptFido2CredentialAutofillViewResult.Success(
-                listOf(
-                    createMockFido2CredentialAutofillView(
-                        number = 1,
-                        cipherId = "mockId-1",
-                        rpId = "mockRelyingPartyId-1",
-                    ),
-                ),
+                fido2CredentialAutofillViews,
             )
-            val mockIcon = mockk<Icon>()
             every {
-                IconCompat.createWithResource(any<Context>(), any<Int>())
-            } returns mockk {
-                every { toIcon(any()) } returns mockIcon
-            }
-            every {
-                mockIntentManager.createFido2GetCredentialPendingIntent(
-                    action = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY",
+                mockCredentialEntryBuilder.buildPublicKeyCredentialEntries(
                     userId = "mockUserId",
-                    credentialId = any(),
-                    cipherId = "mockId-1",
+                    fido2CredentialAutofillViews = fido2CredentialAutofillViews,
+                    beginGetPublicKeyCredentialOptions = listOf(
+                        mockBeginGetPublicKeyCredentialOption,
+                    ),
                     isUserVerified = false,
-                    requestCode = any(),
                 )
-            } returns mockk()
-            every {
-                mockBiometricsEncryptionManager.getOrCreateCipher("mockUserId")
-            } returns mockk()
-            every {
-                mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
-            } returns false
-            mockBuilder<PublicKeyCredentialEntry.Builder> { it.setIcon(mockIcon) }
-            mockBuilder<PublicKeyCredentialEntry.Builder> { it.setBiometricPromptData(any()) }
-            val result = bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockBeginGetPublicKeyCredentialOption),
-            )
+            } returns listOf(mockk<PublicKeyCredentialEntry>())
+
+            val result = bitwardenCredentialManager.getCredentialEntries(mockGetCredentialsRequest)
             assertTrue(result.isSuccess)
             assertEquals(1, result.getOrNull()?.size)
             assertTrue(result.getOrNull()?.first() is PublicKeyCredentialEntry)
-            verify {
-                anyConstructed<PublicKeyCredentialEntry.Builder>().setIcon(mockIcon)
-            }
-            verify(exactly = 0) {
-                anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
-            }
-
-            // Verify biometric prompt data IS NOT set when single tap feature flag is enabled and
-            // build version is < 35
-            every {
-                mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
-            } returns true
-            every { isBuildVersionBelow(35) } returns true
-            bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockBeginGetPublicKeyCredentialOption),
-            )
-            verify(exactly = 0) {
-                anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
-            }
-
-            // Verify biometric prompt data IS set when single tap feature flag is enabled and build
-            // version is 35+
-            every {
-                mockFeatureFlagManager.getFeatureFlag(FlagKey.SingleTapPasskeyAuthentication)
-            } returns true
-            every { isBuildVersionBelow(35) } returns false
-            bitwardenCredentialManager.getCredentialEntries(
-                userId = "mockUserId",
-                options = listOf(mockBeginGetPublicKeyCredentialOption),
-            )
-            verify {
-                anyConstructed<PublicKeyCredentialEntry.Builder>().setBiometricPromptData(any())
-            }
         }
-
-    private fun setupMockUri() {
-        mockkStatic(Uri::class)
-        val uriMock = mockk<Uri>()
-        every { Uri.parse(any()) } returns uriMock
-        every { uriMock.host } returns "www.mockuri.com"
-    }
 }
 
 private const val DEFAULT_PACKAGE_NAME = "com.x8bit.bitwarden"
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorTest.kt
index 0c66c8f0dd5..271a31f9dd7 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/credentials/processor/CredentialProviderProcessorTest.kt
@@ -69,9 +69,7 @@ class CredentialProviderProcessorTest {
     }
     private val credentialEntries = listOf(mockk<CredentialEntry>(relaxed = true))
     private val bitwardenCredentialManager: BitwardenCredentialManager = mockk {
-        coEvery {
-            getCredentialEntries(any(), any())
-        } returns credentialEntries.asSuccess()
+        coEvery { getCredentialEntries(any()) } returns credentialEntries.asSuccess()
     }
     private val intentManager: IntentManager = mockk()
     private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
@@ -459,10 +457,7 @@ class CredentialProviderProcessorTest {
             every { cancellationSignal.setOnCancelListener(any()) } just runs
             every { callback.onError(capture(captureSlot)) } just runs
             coEvery {
-                bitwardenCredentialManager.getCredentialEntries(
-                    userId = DEFAULT_USER_STATE.activeUserId,
-                    options = listOf(mockOption),
-                )
+                bitwardenCredentialManager.getCredentialEntries(any())
             } returns Result.failure(Exception("Error decrypting credentials."))
 
             credentialProviderProcessor.processGetCredentialRequest(
diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
index 570a3e45f8a..8fc012d2ddb 100644
--- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt
@@ -95,11 +95,13 @@ fun createMockCipherView(
 /**
  * Create a mock [LoginView] with a given [number].
  */
+@Suppress("LongParameterList")
 fun createMockLoginView(
     number: Int,
     totp: String? = "mockTotp-$number",
     clock: Clock = FIXED_CLOCK,
     hasUris: Boolean = true,
+    uris: List<LoginUriView>? = listOf(createMockUriView(number = number)),
     fido2Credentials: List<Fido2Credential>? = createMockSdkFido2CredentialList(number, clock),
 ): LoginView =
     LoginView(
@@ -107,7 +109,7 @@ fun createMockLoginView(
         password = "mockPassword-$number",
         passwordRevisionDate = clock.instant(),
         autofillOnPageLoad = false,
-        uris = listOf(createMockUriView(number = number)).takeIf { hasUris },
+        uris = uris.takeIf { hasUris },
         totp = totp,
         fido2Credentials = fido2Credentials,
     )
@@ -162,9 +164,9 @@ fun createMockFido2CredentialAutofillView(
 /**
  * Create a mock [LoginUriView] with a given [number].
  */
-fun createMockUriView(number: Int): LoginUriView =
+fun createMockUriView(number: Int, uri: String = "www.mockuri$number.com"): LoginUriView =
     LoginUriView(
-        uri = "www.mockuri$number.com",
+        uri = uri,
         match = UriMatchType.HOST,
         uriChecksum = "mockUriChecksum-$number",
     )
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
index 7b4b7f63d48..8058be23068 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt
@@ -4,6 +4,7 @@ import android.net.Uri
 import androidx.core.os.bundleOf
 import androidx.credentials.CreatePublicKeyCredentialRequest
 import androidx.credentials.GetPublicKeyCredentialOption
+import androidx.credentials.exceptions.GetCredentialUnknownException
 import androidx.credentials.provider.BeginGetCredentialRequest
 import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
 import androidx.credentials.provider.ProviderCreateCredentialRequest
@@ -12,6 +13,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry
 import androidx.lifecycle.SavedStateHandle
 import app.cash.turbine.test
 import com.bitwarden.core.data.repository.model.DataState
+import com.bitwarden.core.data.util.asFailure
 import com.bitwarden.core.data.util.asSuccess
 import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
 import com.bitwarden.data.repository.model.Environment
@@ -2967,10 +2969,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
                     mockGetCredentialsRequest,
                 )
             coEvery {
-                bitwardenCredentialManager.getCredentialEntries(
-                    userId = "mockUserId-1",
-                    options = listOf(mockBeginGetPublicKeyCredentialOption),
-                )
+                bitwardenCredentialManager.getCredentialEntries(any())
             } returns emptyList<PublicKeyCredentialEntry>().asSuccess()
             coEvery {
                 originManager.validateOrigin(callingAppInfo = any())
@@ -3007,8 +3006,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
             }
         }
 
+    @Suppress("MaxLineLength")
     @Test
-    fun `GetCredentialsRequest should display error dialog when getCredentialEntries is failure`() =
+    fun `GetCredentialsRequest should emit GetCredentialEntriesResultReceive when result is received`() =
         runTest {
             setupMockUri()
             val mockGetCredentialsRequest = createMockGetCredentialsRequest(number = 1)
@@ -3031,6 +3031,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
             every {
                 mockBeginGetCredentialRequest.beginGetCredentialOptions
             } returns emptyList()
+            coEvery {
+                bitwardenCredentialManager.getCredentialEntries(any())
+            } returns GetCredentialUnknownException("Internal error").asFailure()
 
             val dataState = DataState.Loaded(
                 data = VaultData(
@@ -3047,8 +3050,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
             assertEquals(
                 VaultItemListingState.DialogState.CredentialManagerOperationFail(
                     title = R.string.an_error_has_occurred.asText(),
-                    message =
-                        R.string.passkey_operation_failed_because_the_request_is_invalid.asText(),
+                    message = R.string.generic_error_message.asText(),
                 ),
                 viewModel.stateFlow.value.dialogState,
             )


Reviews:
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:38:38Z
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T17:40:46Z
user: login: SaintPatrck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:44:08Z
user: login: SaintPatrck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:44:37Z
user: login: david-livefront
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T18:48:19Z
user: login: SaintPatrck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T18:50:27Z
user: login: david-livefront
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-13T20:17:33Z

Review comments:
diff_hunk: @@ -0,0 +1,98 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.core.data.util.asSuccess
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
+import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import kotlin.random.Random
+
+/**
+ * Primary implementation of [CredentialEntryBuilder].
+ */
+class CredentialEntryBuilderImpl(
+    private val context: Context,
+    private val intentManager: IntentManager,
+    private val featureFlagManager: FeatureFlagManager,
+    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+) : CredentialEntryBuilder {
+
+    override fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): Result<List<PublicKeyCredentialEntry>> = beginGetPublicKeyCredentialOptions
+        .flatMap { option ->
+            fido2CredentialAutofillViews
+                .toPublicKeyCredentialEntryList(
+                    userId = userId,
+                    option = option,
+                    isUserVerified = isUserVerified,
+                )
+        }
+        .asSuccess()
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
user: login: david-livefront
body: Can this fail?
created_at: 2025-05-13T17:38:38Z
author_association: COLLABORATOR
diff_hunk: @@ -209,33 +180,18 @@ class BitwardenCredentialManagerImpl(
                     else -> emptyList()
                 }
             }
+            .filter { it.isActiveWithFido2Credentials }
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
user: login: david-livefront
body: Is this function still directly tied to FIDO 2?

I see this reference as well as the `onFailure` logging down below
created_at: 2025-05-13T17:40:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.core.data.util.asSuccess
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
+import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import kotlin.random.Random
+
+/**
+ * Primary implementation of [CredentialEntryBuilder].
+ */
+class CredentialEntryBuilderImpl(
+    private val context: Context,
+    private val intentManager: IntentManager,
+    private val featureFlagManager: FeatureFlagManager,
+    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+) : CredentialEntryBuilder {
+
+    override fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): Result<List<PublicKeyCredentialEntry>> = beginGetPublicKeyCredentialOptions
+        .flatMap { option ->
+            fido2CredentialAutofillViews
+                .toPublicKeyCredentialEntryList(
+                    userId = userId,
+                    option = option,
+                    isUserVerified = isUserVerified,
+                )
+        }
+        .asSuccess()
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
user: login: SaintPatrck
body: No
created_at: 2025-05-13T18:44:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -209,33 +180,18 @@ class BitwardenCredentialManagerImpl(
                     else -> emptyList()
                 }
             }
+            .filter { it.isActiveWithFido2Credentials }
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/manager/BitwardenCredentialManagerImpl.kt
user: login: SaintPatrck
body: Technically, yes. We've not introduced Password credential handling yet, but this is where it will be. When that's added I plan to extract the FIDO2 and Password credential handling into their own functions.
created_at: 2025-05-13T18:44:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.core.data.util.asSuccess
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
+import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import kotlin.random.Random
+
+/**
+ * Primary implementation of [CredentialEntryBuilder].
+ */
+class CredentialEntryBuilderImpl(
+    private val context: Context,
+    private val intentManager: IntentManager,
+    private val featureFlagManager: FeatureFlagManager,
+    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+) : CredentialEntryBuilder {
+
+    override fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): Result<List<PublicKeyCredentialEntry>> = beginGetPublicKeyCredentialOptions
+        .flatMap { option ->
+            fido2CredentialAutofillViews
+                .toPublicKeyCredentialEntryList(
+                    userId = userId,
+                    option = option,
+                    isUserVerified = isUserVerified,
+                )
+        }
+        .asSuccess()
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
user: login: david-livefront
body: Do we need the result then?
created_at: 2025-05-13T18:48:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+package com.x8bit.bitwarden.data.credentials.builder
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.IconCompat
+import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import com.bitwarden.core.data.util.asSuccess
+import com.bitwarden.fido.Fido2CredentialAutofillView
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.data.credentials.processor.GET_PASSKEY_INTENT
+import com.x8bit.bitwarden.data.credentials.util.setBiometricPromptDataIfSupported
+import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
+import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
+import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
+import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
+import kotlin.random.Random
+
+/**
+ * Primary implementation of [CredentialEntryBuilder].
+ */
+class CredentialEntryBuilderImpl(
+    private val context: Context,
+    private val intentManager: IntentManager,
+    private val featureFlagManager: FeatureFlagManager,
+    private val biometricsEncryptionManager: BiometricsEncryptionManager,
+) : CredentialEntryBuilder {
+
+    override fun buildPublicKeyCredentialEntries(
+        userId: String,
+        fido2CredentialAutofillViews: List<Fido2CredentialAutofillView>,
+        beginGetPublicKeyCredentialOptions: List<BeginGetPublicKeyCredentialOption>,
+        isUserVerified: Boolean,
+    ): Result<List<PublicKeyCredentialEntry>> = beginGetPublicKeyCredentialOptions
+        .flatMap { option ->
+            fido2CredentialAutofillViews
+                .toPublicKeyCredentialEntryList(
+                    userId = userId,
+                    option = option,
+                    isUserVerified = isUserVerified,
+                )
+        }
+        .asSuccess()
path: app/src/main/java/com/x8bit/bitwarden/data/credentials/builder/CredentialEntryBuilderImpl.kt
user: login: SaintPatrck
body: 🤔 Don't believe so, actually. It was when we were calling Glide, but that's been removed for now. Good catch!
created_at: 2025-05-13T18:50:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-12T21:11:27Z
body: ![Logo](https://cdn.ast.checkmarx.net/integrations/logo/Checkmarx.png)
**Checkmarx One – Scan Summary & Details** – [d0e5912a\-28c0\-4e4f\-b45f\-db5845941d8e](https://ast.checkmarx.net/projects/7b48c130-9c07-4ac2-a6dd-15ed4933d967/scans?id=d0e5912a-28c0-4e4f-b45f-db5845941d8e&branch=PM-21567%2Fcredential-entry-builder-interface)
### Great job, no security vulnerabilities found in this Pull Request

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-13T14:09:18Z
body: ## [Codecov](https://app.codecov.io/gh/bitwarden/android/pull/5177?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) Report
Attention: Patch coverage is `96.96970%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 82.31%. Comparing base [(`cd4db46`)](https://app.codecov.io/gh/bitwarden/android/commit/cd4db467e326e9c8c379fd62552aefaf57ab721d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) to head [(`e814f69`)](https://app.codecov.io/gh/bitwarden/android/commit/e814f69ada92a43c175acea1e3dcdae8be19a743?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).
> Report is 8 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/bitwarden/android/pull/5177?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) | Patch % | Lines |
|---|---|---|
| [.../credentials/builder/CredentialEntryBuilderImpl.kt](https://app.codecov.io/gh/bitwarden/android/pull/5177?src=pr&el=tree&filepath=app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fx8bit%2Fbitwarden%2Fdata%2Fcredentials%2Fbuilder%2FCredentialEntryBuilderImpl.kt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden#diff-YXBwL3NyYy9tYWluL2phdmEvY29tL3g4Yml0L2JpdHdhcmRlbi9kYXRhL2NyZWRlbnRpYWxzL2J1aWxkZXIvQ3JlZGVudGlhbEVudHJ5QnVpbGRlckltcGwua3Q=) | 93.47% | [1 Missing and 2 partials :warning: ](https://app.codecov.io/gh/bitwarden/android/pull/5177?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5177      +/-   ##
==========================================
+ Coverage   81.58%   82.31%   +0.73%     
==========================================
  Files        1019      694     -325     
  Lines       58680    51226    -7454     
  Branches     7328     6916     -412     
==========================================
- Hits        47876    42169    -5707     
+ Misses       8272     6752    -1520     
+ Partials     2532     2305     -227     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bitwarden/android/pull/5177?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bitwarden).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE