[PM-24148] add credential manager provider for create passwords (#5579)

Co-authored-by: Patrick Honkonen <phonkonen@bitwarden.com>
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
This commit is contained in:
Nailik 2025-11-19 23:00:53 +01:00 committed by GitHub
parent 839e9e8a1a
commit 5ec0a1986d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 879 additions and 336 deletions

View File

@ -86,6 +86,7 @@
<intent-filter> <intent-filter>
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" /> <action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" /> <action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" /> <action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" /> <action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />

View File

@ -58,6 +58,13 @@ interface CredentialManagerPendingIntentManager {
userId: String, userId: String,
): PendingIntent ): PendingIntent
/**
* Creates a pending intent to use when providing options for Password credential creation.
*/
fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent
/** /**
* Creates a pending intent to use when providing options for Password credential filling. * Creates a pending intent to use when providing options for Password credential filling.
*/ */

View File

@ -75,6 +75,24 @@ class CredentialManagerPendingIntentManagerImpl(
) )
} }
/**
* Creates a pending intent to use when providing options for FIDO 2 credential creation.
*/
override fun createPasswordCreationPendingIntent(
userId: String,
): PendingIntent {
val intent = Intent(CREATE_PASSWORD_ACTION)
.setPackage(context.packageName)
.putExtra(EXTRA_KEY_USER_ID, userId)
return PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ Random.nextInt(),
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_UPDATE_CURRENT.toPendingIntentMutabilityFlag(),
)
}
/** /**
* Creates a pending intent to use when providing options for Password credential filling. * Creates a pending intent to use when providing options for Password credential filling.
*/ */
@ -101,4 +119,5 @@ class CredentialManagerPendingIntentManagerImpl(
private const val CREATE_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY" private const val CREATE_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSKEY"
private const val UNLOCK_ACCOUNT_ACTION = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" private const val UNLOCK_ACCOUNT_ACTION = "com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT"
private const val GET_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY" private const val GET_PASSKEY_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSKEY"
private const val CREATE_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_CREATE_PASSWORD"
private const val GET_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" private const val GET_PASSWORD_ACTION = "com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD"

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderCreateCredentialRequest
@ -48,6 +49,15 @@ data class CreateCredentialRequest(
providerRequest.callingRequest as? CreatePublicKeyCredentialRequest providerRequest.callingRequest as? CreatePublicKeyCredentialRequest
} }
/**
* The [CreatePasswordRequest] of the [providerRequest], or null if the calling
* request is not a [CreatePasswordRequest].
*/
@IgnoredOnParcel
val createPasswordCredentialRequest: CreatePasswordRequest? by lazy {
providerRequest.callingRequest as? CreatePasswordRequest
}
/** /**
* The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request * The [requestJson] of the [createPublicKeyCredentialRequest], or null if the calling request
* is not a [CreatePublicKeyCredentialRequest]. * is not a [CreatePublicKeyCredentialRequest].

View File

@ -19,6 +19,7 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.AuthenticationAction import androidx.credentials.provider.AuthenticationAction
import androidx.credentials.provider.BeginCreateCredentialRequest import androidx.credentials.provider.BeginCreateCredentialRequest
import androidx.credentials.provider.BeginCreateCredentialResponse import androidx.credentials.provider.BeginCreateCredentialResponse
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
import androidx.credentials.provider.BeginGetCredentialRequest import androidx.credentials.provider.BeginGetCredentialRequest
import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.BeginGetCredentialResponse
@ -69,7 +70,7 @@ class CredentialProviderProcessorImpl(
} }
val createCredentialJob = ioScope.launch { val createCredentialJob = ioScope.launch {
processCreateCredentialRequest(request = request) (handleCreatePasskeyQuery(request) ?: handleCreatePasswordQuery(request))
?.let { callback.onResult(it) } ?.let { callback.onResult(it) }
?: callback.onError(CreateCredentialUnknownException()) ?: callback.onError(CreateCredentialUnknownException())
} }
@ -137,21 +138,11 @@ class CredentialProviderProcessorImpl(
callback.onError(ClearCredentialUnsupportedException()) callback.onError(ClearCredentialUnsupportedException())
} }
private fun processCreateCredentialRequest( private fun handleCreatePasskeyQuery(
request: BeginCreateCredentialRequest, request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? { ): BeginCreateCredentialResponse? {
return when (request) { if (request !is BeginCreatePublicKeyCredentialRequest) return null
is BeginCreatePublicKeyCredentialRequest -> {
handleCreatePasskeyQuery(request)
}
else -> null
}
}
private fun handleCreatePasskeyQuery(
request: BeginCreatePublicKeyCredentialRequest,
): BeginCreateCredentialResponse? {
val requestJson = request val requestJson = request
.candidateQueryData .candidateQueryData
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON") .getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
@ -161,14 +152,19 @@ class CredentialProviderProcessorImpl(
val userState = authRepository.userStateFlow.value ?: return null val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder() return BeginCreateCredentialResponse.Builder()
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId)) .setCreateEntries(
userState.accounts.toCreatePasskeyEntry(userState.activeUserId),
)
.build() .build()
} }
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) = private fun List<UserState.Account>.toCreatePasskeyEntry(
map { it.toCreateEntry(isActive = activeUserId == it.userId) } activeUserId: String,
): List<CreateEntry> = map { it.toCreatePasskeyEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreateEntry(isActive: Boolean): CreateEntry { private fun UserState.Account.toCreatePasskeyEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email val accountName = name ?: email
val entryBuilder = CreateEntry val entryBuilder = CreateEntry
.Builder( .Builder(
@ -196,6 +192,54 @@ class CredentialProviderProcessorImpl(
return entryBuilder.build() return entryBuilder.build()
} }
private fun handleCreatePasswordQuery(
request: BeginCreateCredentialRequest,
): BeginCreateCredentialResponse? {
if (request !is BeginCreatePasswordCredentialRequest) return null
val userState = authRepository.userStateFlow.value ?: return null
return BeginCreateCredentialResponse.Builder()
.setCreateEntries(
userState.accounts.toCreatePasswordEntry(userState.activeUserId),
)
.build()
}
private fun List<UserState.Account>.toCreatePasswordEntry(
activeUserId: String,
) = map { it.toCreatePasswordEntry(isActive = activeUserId == it.userId) }
private fun UserState.Account.toCreatePasswordEntry(
isActive: Boolean,
): CreateEntry {
val accountName = name ?: email
val entryBuilder = CreateEntry
.Builder(
accountName = accountName,
pendingIntent = pendingIntentManager.createPasswordCreationPendingIntent(
userId = userId,
),
)
.setDescription(
context.getString(
BitwardenString.your_password_will_be_saved_to_your_bitwarden_vault_for_x,
accountName,
),
)
// Set the last used time to "now" so the active account is the default option in the
// system prompt.
.setLastUsedTime(if (isActive) clock.instant() else null)
.setAutoSelectAllowed(true)
if (isVaultUnlocked) {
biometricsEncryptionManager
.getOrCreateCipher(userId)
?.let { entryBuilder.setBiometricPromptDataIfSupported(cipher = it) }
}
return entryBuilder.build()
}
private fun CreateEntry.Builder.setBiometricPromptDataIfSupported( private fun CreateEntry.Builder.setBiometricPromptDataIfSupported(
cipher: Cipher, cipher: Cipher,
): CreateEntry.Builder { ): CreateEntry.Builder {

View File

@ -1,19 +1,19 @@
package com.x8bit.bitwarden.ui.credentials.manager package com.x8bit.bitwarden.ui.credentials.manager
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/** /**
* A manager for completing the FIDO 2 creation process. * A manager for completing the credential creation process.
*/ */
interface CredentialProviderCompletionManager { interface CredentialProviderCompletionManager {
/** /**
* Completes the FIDO 2 registration process with the provided [result]. * Completes the credential registration process with the provided [result].
*/ */
fun completeFido2Registration(result: RegisterFido2CredentialResult) fun completeCredentialRegistration(result: CreateCredentialResult)
/** /**
* Complete the FIDO 2 credential assertion process with the provided [result]. * Complete the FIDO 2 credential assertion process with the provided [result].

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.CreatePublicKeyCredentialResponse import androidx.credentials.CreatePublicKeyCredentialResponse
import androidx.credentials.GetCredentialResponse import androidx.credentials.GetCredentialResponse
import androidx.credentials.PasswordCredential import androidx.credentials.PasswordCredential
@ -15,9 +16,9 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.BeginGetCredentialResponse
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/** /**
* Primary implementation of [CredentialProviderCompletionManager] when the build version is * Primary implementation of [CredentialProviderCompletionManager] when the build version is
@ -28,11 +29,11 @@ class CredentialProviderCompletionManagerImpl(
private val activity: Activity, private val activity: Activity,
) : CredentialProviderCompletionManager { ) : CredentialProviderCompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) { override fun completeCredentialRegistration(result: CreateCredentialResult) {
activity.also { activity.also {
val intent = Intent() val intent = Intent()
when (result) { when (result) {
is RegisterFido2CredentialResult.Error -> { is CreateCredentialResult.Error -> {
PendingIntentHandler PendingIntentHandler
.setCreateCredentialException( .setCreateCredentialException(
intent = intent, intent = intent,
@ -42,7 +43,7 @@ class CredentialProviderCompletionManagerImpl(
) )
} }
is RegisterFido2CredentialResult.Success -> { is CreateCredentialResult.Success.Fido2CredentialRegistered -> {
PendingIntentHandler PendingIntentHandler
.setCreateCredentialResponse( .setCreateCredentialResponse(
intent = intent, intent = intent,
@ -52,7 +53,15 @@ class CredentialProviderCompletionManagerImpl(
) )
} }
is RegisterFido2CredentialResult.Cancelled -> { is CreateCredentialResult.Success.PasswordCreated -> {
PendingIntentHandler
.setCreateCredentialResponse(
intent = intent,
response = CreatePasswordResponse(),
)
}
is CreateCredentialResult.Cancelled -> {
PendingIntentHandler PendingIntentHandler
.setCreateCredentialException( .setCreateCredentialException(
intent = intent, intent = intent,

View File

@ -3,9 +3,9 @@ package com.x8bit.bitwarden.ui.credentials.manager
import androidx.credentials.CredentialProvider import androidx.credentials.CredentialProvider
import com.bitwarden.annotation.OmitFromCoverage import com.bitwarden.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
/** /**
* A no-op implementation of [CredentialProviderCompletionManagerImpl] provided when the build * A no-op implementation of [CredentialProviderCompletionManagerImpl] provided when the build
@ -13,7 +13,7 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialR
*/ */
@OmitFromCoverage @OmitFromCoverage
object CredentialProviderCompletionManagerUnsupportedApiImpl : CredentialProviderCompletionManager { object CredentialProviderCompletionManagerUnsupportedApiImpl : CredentialProviderCompletionManager {
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit override fun completeCredentialRegistration(result: CreateCredentialResult) = Unit
override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit

View File

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.ui.credentials.manager.model
import com.bitwarden.ui.util.Text
/**
* Represents the result of a credential creation attempt.
*/
sealed class CreateCredentialResult {
/**
* Represents a successful credential creation attempt.
*/
sealed class Success : CreateCredentialResult() {
/**
* Indicates that the FIDO2 registration was successful.
*/
data class Fido2CredentialRegistered(val responseJson: String) : Success()
/**
* Indicates that the Password creation was successful.
*/
data object PasswordCreated : Success()
}
/**
* Indicates that an error occurred during credential creation.
*/
data class Error(val message: Text) : CreateCredentialResult()
/**
* Indicates that credential creation was cancelled by the user.
*/
data object Cancelled : CreateCredentialResult()
}

View File

@ -1,23 +0,0 @@
package com.x8bit.bitwarden.ui.credentials.manager.model
import com.bitwarden.ui.util.Text
/**
* Represents the result of a FIDO2 credential registration attempt.
*/
sealed class RegisterFido2CredentialResult {
/**
* Indicates that the registration was successful.
*/
data class Success(val responseJson: String) : RegisterFido2CredentialResult()
/**
* Indicates that an error occurred during registration.
*/
data class Error(val message: Text) : RegisterFido2CredentialResult()
/**
* Indicates that the registration was cancelled by the user.
*/
data object Cancelled : RegisterFido2CredentialResult()
}

View File

@ -6,21 +6,22 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
/** /**
* A reusable dialog for confirming whether or not the user wants to overwrite an existing FIDO 2 * A reusable dialog for confirming whether or not the user wants to overwrite an existing credential.
* credential.
* *
* @param onConfirmClick A callback for when the overwrite confirmation button is clicked. * @param onConfirmClick A callback for when the overwrite confirmation button is clicked.
* @param onDismissRequest A callback for when the dialog is requesting dismissal. * @param onDismissRequest A callback for when the dialog is requesting dismissal.
*/ */
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Composable @Composable
fun BitwardenOverwritePasskeyConfirmationDialog( fun BitwardenOverwriteCredentialConfirmationDialog(
title: String,
message: String,
onConfirmClick: () -> Unit, onConfirmClick: () -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
BitwardenTwoButtonDialog( BitwardenTwoButtonDialog(
title = stringResource(id = BitwardenString.overwrite_passkey), title = title,
message = stringResource(id = BitwardenString.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey), message = message,
confirmButtonText = stringResource(id = BitwardenString.okay), confirmButtonText = stringResource(id = BitwardenString.okay),
dismissButtonText = stringResource(id = BitwardenString.cancel), dismissButtonText = stringResource(id = BitwardenString.cancel),
onConfirmClick = onConfirmClick, onConfirmClick = onConfirmClick,

View File

@ -71,6 +71,7 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.navigateToExportItemsGra
import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.navigateToVerifyPassword import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.navigateToVerifyPassword
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -141,6 +142,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForFido2Assertion, is RootNavState.VaultUnlockedForFido2Assertion,
is RootNavState.VaultUnlockedForPasswordGet, is RootNavState.VaultUnlockedForPasswordGet,
is RootNavState.VaultUnlockedForProviderGetCredentials, is RootNavState.VaultUnlockedForProviderGetCredentials,
is RootNavState.VaultUnlockedForCreatePasswordRequest,
-> VaultUnlockedGraphRoute -> VaultUnlockedGraphRoute
is RootNavState.CredentialExchangeExport, is RootNavState.CredentialExchangeExport,
@ -245,6 +247,17 @@ fun RootNavScreen(
) )
} }
is RootNavState.VaultUnlockedForCreatePasswordRequest -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultAddEdit(
args = VaultAddEditArgs(
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
navOptions = rootNavOptions,
)
}
is RootNavState.VaultUnlockedForAutofillSelection -> { is RootNavState.VaultUnlockedForAutofillSelection -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions) navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultItemListingAsRoot( navController.navigateToVaultItemListingAsRoot(

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.network.model.OrganizationType import com.bitwarden.network.model.OrganizationType
import com.bitwarden.network.util.parseJwtTokenDataOrNull import com.bitwarden.network.util.parseJwtTokenDataOrNull
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.util.toAndroidAppUriString
import com.bitwarden.ui.platform.manager.share.model.ShareData import com.bitwarden.ui.platform.manager.share.model.ShareData
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -144,11 +145,31 @@ class RootNavViewModel @Inject constructor(
} }
is SpecialCircumstance.ProviderCreateCredential -> { is SpecialCircumstance.ProviderCreateCredential -> {
RootNavState.VaultUnlockedForFido2Save( val request = specialCircumstance.createCredentialRequest
activeUserId = userState.activeUserId, val publicKeyRequest = request.createPublicKeyCredentialRequest
createCredentialRequest = val passwordRequest = request.createPasswordCredentialRequest
specialCircumstance.createCredentialRequest,
) when {
publicKeyRequest != null -> {
RootNavState.VaultUnlockedForFido2Save(
activeUserId = userState.activeUserId,
createCredentialRequest = request,
)
}
passwordRequest != null -> {
RootNavState.VaultUnlockedForCreatePasswordRequest(
username = passwordRequest.id,
password = passwordRequest.password,
uri = request
.callingAppInfo
.packageName
.toAndroidAppUriString(),
)
}
else -> throw IllegalStateException("Should not have entered here.")
}
} }
is SpecialCircumstance.Fido2Assertion -> { is SpecialCircumstance.Fido2Assertion -> {
@ -336,6 +357,21 @@ sealed class RootNavState : Parcelable {
val createCredentialRequest: CreateCredentialRequest, val createCredentialRequest: CreateCredentialRequest,
) : RootNavState() ) : RootNavState()
/**
* App should show an add item screen for a user to complete the saving of data collected by
* the credential manager framework.
*
* @param username The username of the user.
* @param password The password of the user.
* @param uri The URI to associate this credential with.
*/
@Parcelize
data class VaultUnlockedForCreatePasswordRequest(
val username: String,
val password: String,
val uri: String,
) : RootNavState()
/** /**
* App should perform FIDO 2 credential assertion for the user. * App should perform FIDO 2 credential assertion for the user.
*/ */

View File

@ -71,7 +71,7 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwriteCredentialConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
@ -161,8 +161,8 @@ fun VaultAddEditScreen(
) )
} }
is VaultAddEditEvent.CompleteFido2Registration -> { is VaultAddEditEvent.CompleteCredentialRegistration -> {
credentialProviderCompletionManager.completeFido2Registration( credentialProviderCompletionManager.completeCredentialRegistration(
result = event.result, result = event.result,
) )
} }
@ -227,10 +227,14 @@ fun VaultAddEditScreen(
onAutofillDismissRequest = remember(viewModel) { onAutofillDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) } { viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) }
}, },
onFido2ErrorDismiss = remember(viewModel) { onCredentialErrorDismiss = remember(viewModel) {
{ errorMessage -> { errorMessage ->
viewModel.trySendAction( viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(message = errorMessage), VaultAddEditAction
.Common
.CredentialErrorDialogDismissed(
message = errorMessage,
),
) )
} }
}, },
@ -463,7 +467,7 @@ private fun VaultAddEditItemDialogs(
dialogState: VaultAddEditState.DialogState?, dialogState: VaultAddEditState.DialogState?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onAutofillDismissRequest: () -> Unit, onAutofillDismissRequest: () -> Unit,
onFido2ErrorDismiss: (Text) -> Unit, onCredentialErrorDismiss: (Text) -> Unit,
onConfirmOverwriteExistingPasskey: () -> Unit, onConfirmOverwriteExistingPasskey: () -> Unit,
onSubmitMasterPasswordFido2Verification: (password: String) -> Unit, onSubmitMasterPasswordFido2Verification: (password: String) -> Unit,
onRetryFido2PasswordVerification: () -> Unit, onRetryFido2PasswordVerification: () -> Unit,
@ -495,16 +499,22 @@ private fun VaultAddEditItemDialogs(
) )
} }
is VaultAddEditState.DialogState.Fido2Error -> { is VaultAddEditState.DialogState.CredentialError -> {
BitwardenBasicDialog( BitwardenBasicDialog(
title = stringResource(id = BitwardenString.an_error_has_occurred), title = stringResource(id = BitwardenString.an_error_has_occurred),
message = dialogState.message(), message = dialogState.message(),
onDismissRequest = { onFido2ErrorDismiss(dialogState.message) }, onDismissRequest = { onCredentialErrorDismiss(dialogState.message) },
) )
} }
is VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt -> { is VaultAddEditState.DialogState.OverwritePasskeyConfirmationPrompt -> {
BitwardenOverwritePasskeyConfirmationDialog( @Suppress("MaxLineLength")
BitwardenOverwriteCredentialConfirmationDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(
id = BitwardenString
.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey,
),
onConfirmClick = onConfirmOverwriteExistingPasskey, onConfirmClick = onConfirmOverwriteExistingPasskey,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )

View File

@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
import android.os.Parcelable import android.os.Parcelable
import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
@ -34,7 +33,6 @@ import com.x8bit.bitwarden.data.credentials.manager.BitwardenCredentialManager
import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest import com.x8bit.bitwarden.data.credentials.model.CreateCredentialRequest
import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult import com.x8bit.bitwarden.data.credentials.model.Fido2RegisterCredentialResult
import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement import com.x8bit.bitwarden.data.credentials.model.UserVerificationRequirement
import com.x8bit.bitwarden.data.credentials.util.getCreatePasskeyCredentialRequestOrNull
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@ -59,7 +57,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
@ -150,7 +148,7 @@ class VaultAddEditViewModel @Inject constructor(
val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull() val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull()
// Check for totp data to pre-populate // Check for totp data to pre-populate
val totpData = specialCircumstance?.toTotpDataOrNull() val totpData = specialCircumstance?.toTotpDataOrNull()
// Check for Fido2 data to pre-populate // Check for Fido2 or Password credential data to pre-populate
val providerCreateCredentialRequest = val providerCreateCredentialRequest =
specialCircumstance?.toCreateCredentialRequestOrNull() specialCircumstance?.toCreateCredentialRequestOrNull()
val fido2AttestationOptions = providerCreateCredentialRequest?.requestJson val fido2AttestationOptions = providerCreateCredentialRequest?.requestJson
@ -348,8 +346,8 @@ class VaultAddEditViewModel @Inject constructor(
handleUserVerificationCancelled() handleUserVerificationCancelled()
} }
is VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> { is VaultAddEditAction.Common.CredentialErrorDialogDismissed -> {
handleFido2ErrorDialogDismissed(action) handleCredentialErrorDialogDismissed(action)
} }
VaultAddEditAction.Common.UserVerificationNotSupported -> { VaultAddEditAction.Common.UserVerificationNotSupported -> {
@ -401,31 +399,7 @@ class VaultAddEditViewModel @Inject constructor(
@Suppress("LongMethod") @Suppress("LongMethod")
private fun handleSaveClick() = onContent { content -> private fun handleSaveClick() = onContent { content ->
if (content.common.name.isBlank()) { if (hasValidationErrors(content)) return@onContent
showGenericErrorDialog(
message = BitwardenString.validation_field_required
.asText(BitwardenString.name.asText()),
)
return@onContent
} else if (
content.common.selectedOwnerId != null &&
content.common.selectedOwner?.collections?.all { !it.isSelected } == true
) {
showGenericErrorDialog(
message = BitwardenString.select_one_collection.asText(),
)
return@onContent
} else if (
!networkConnectionManager.isNetworkConnected
) {
showDialog(
dialogState = VaultAddEditState.DialogState.Generic(
title = BitwardenString.internet_connection_required_title.asText(),
message = BitwardenString.internet_connection_required_message.asText(),
),
)
return@onContent
}
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
@ -435,14 +409,17 @@ class VaultAddEditViewModel @Inject constructor(
) )
} }
state.createCredentialRequest state.createCredentialRequest?.run {
?.let { request -> createPublicKeyCredentialRequest
handleProviderCreateCredentialRequest( ?.let { createPublicKeyCredentialRequest ->
request.providerRequest, handleCreatePublicKeyCredentialRequest(
content.toCipherView(), request = createPublicKeyCredentialRequest,
) callingAppInfo = this.callingAppInfo,
return@onContent cipherView = content.toCipherView(),
} )
return@onContent
}
}
viewModelScope.launch { viewModelScope.launch {
when (val vaultAddEditType = state.vaultAddEditType) { when (val vaultAddEditType = state.vaultAddEditType) {
@ -467,15 +444,34 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
private fun handleProviderCreateCredentialRequest( private fun hasValidationErrors(content: VaultAddEditState.ViewState.Content): Boolean =
request: ProviderCreateCredentialRequest, if (content.common.name.isBlank()) {
cipherView: CipherView, showGenericErrorDialog(
) { message = BitwardenString.validation_field_required
request .asText(BitwardenString.name.asText()),
.getCreatePasskeyCredentialRequestOrNull() )
?.let { handleCreatePublicKeyCredentialRequest(request.callingAppInfo, it, cipherView) } true
?: run { handleUnsupportedProviderCreateCredentialRequest() } } else if (
} content.common.selectedOwnerId != null &&
content.common.selectedOwner?.collections?.all { !it.isSelected } == true
) {
showGenericErrorDialog(
message = BitwardenString.select_one_collection.asText(),
)
true
} else if (
!networkConnectionManager.isNetworkConnected
) {
showDialog(
dialogState = VaultAddEditState.DialogState.Generic(
title = BitwardenString.internet_connection_required_title.asText(),
message = BitwardenString.internet_connection_required_message.asText(),
),
)
true
} else {
false
}
private fun handleCreatePublicKeyCredentialRequest( private fun handleCreatePublicKeyCredentialRequest(
callingAppInfo: CallingAppInfo, callingAppInfo: CallingAppInfo,
@ -520,7 +516,7 @@ class VaultAddEditViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
val userId = authRepository.activeUserId val userId = authRepository.activeUserId
?: run { ?: run {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -539,12 +535,6 @@ class VaultAddEditViewModel @Inject constructor(
} }
} }
private fun handleUnsupportedProviderCreateCredentialRequest() {
showFido2ErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_unsupported.asText(),
)
}
private fun handleAttachmentsClick() { private fun handleAttachmentsClick() {
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) } onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
} }
@ -613,63 +603,69 @@ class VaultAddEditViewModel @Inject constructor(
private fun handleConfirmOverwriteExistingPasskeyClick() { private fun handleConfirmOverwriteExistingPasskeyClick() {
state state
.createCredentialRequest .createCredentialRequest
?.providerRequest
?.let { request -> ?.let { request ->
onContent { content -> request.createPublicKeyCredentialRequest
handleProviderCreateCredentialRequest( ?.let { createPublicKeyCredentialRequest ->
request, onContent { content ->
content.toCipherView(), handleCreatePublicKeyCredentialRequest(
) request = createPublicKeyCredentialRequest,
} callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
)
}
}
} }
?: showFido2ErrorDialog( ?: showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_invalid.asText(), BitwardenString.passkey_operation_failed_because_the_request_is_invalid.asText(),
) )
} }
private fun handleUserVerificationLockOut() { private fun handleUserVerificationLockOut() {
bitwardenCredentialManager.isUserVerified = false bitwardenCredentialManager.isUserVerified = false
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
) )
} }
private fun handleUserVerificationSuccess() { private fun handleUserVerificationSuccess() {
bitwardenCredentialManager.isUserVerified = true bitwardenCredentialManager.isUserVerified = true
getRequestAndRegisterCredential() getRequestAndRegisterFido2Credential()
} }
private fun getRequestAndRegisterCredential() = private fun getRequestAndRegisterFido2Credential() =
state.createCredentialRequest state.createCredentialRequest
?.providerRequest
?.let { request -> ?.let { request ->
onContent { content -> request.createPublicKeyCredentialRequest
handleProviderCreateCredentialRequest( ?.let { createPublicKeyCredentialRequest ->
request = request, onContent { content ->
cipherView = content.toCipherView(), handleCreatePublicKeyCredentialRequest(
) request = createPublicKeyCredentialRequest,
} callingAppInfo = request.callingAppInfo,
cipherView = content.toCipherView(),
)
}
}
} }
?: showFido2ErrorDialog( ?: showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_the_request_is_unsupported BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.asText(), .asText(),
) )
private fun handleUserVerificationFail() { private fun handleUserVerificationFail() {
bitwardenCredentialManager.isUserVerified = false bitwardenCredentialManager.isUserVerified = false
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
) )
} }
private fun handleFido2ErrorDialogDismissed( private fun handleCredentialErrorDialogDismissed(
action: VaultAddEditAction.Common.Fido2ErrorDialogDismissed, action: VaultAddEditAction.Common.CredentialErrorDialogDismissed,
) { ) {
bitwardenCredentialManager.isUserVerified = false bitwardenCredentialManager.isUserVerified = false
clearDialogState() clearDialogState()
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Error(action.message), result = CreateCredentialResult.Error(action.message),
), ),
) )
} }
@ -678,8 +674,8 @@ class VaultAddEditViewModel @Inject constructor(
bitwardenCredentialManager.isUserVerified = false bitwardenCredentialManager.isUserVerified = false
clearDialogState() clearDialogState()
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Cancelled, result = CreateCredentialResult.Cancelled,
), ),
) )
} }
@ -692,7 +688,7 @@ class VaultAddEditViewModel @Inject constructor(
.value .value
?.activeAccount ?.activeAccount
?: run { ?: run {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -780,7 +776,7 @@ class VaultAddEditViewModel @Inject constructor(
} }
private fun handleDismissFido2VerificationDialogClick() { private fun handleDismissFido2VerificationDialogClick() {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -1657,7 +1653,13 @@ class VaultAddEditViewModel @Inject constructor(
if (state.shouldClearSpecialCircumstance) { if (state.shouldClearSpecialCircumstance) {
specialCircumstanceManager.specialCircumstance = null specialCircumstanceManager.specialCircumstance = null
} }
if (state.shouldExitOnSave) { if (state.createCredentialRequest?.createPasswordCredentialRequest != null) {
sendEvent(
VaultAddEditEvent.CompleteCredentialRegistration(
CreateCredentialResult.Success.PasswordCreated,
),
)
} else if (state.shouldExitOnSave) {
sendEvent(event = VaultAddEditEvent.ExitApp) sendEvent(event = VaultAddEditEvent.ExitApp)
} else { } else {
snackbarRelayManager.sendSnackbarData( snackbarRelayManager.sendSnackbarData(
@ -1970,8 +1972,8 @@ class VaultAddEditViewModel @Inject constructor(
// Use toast here because we are closing the activity. // Use toast here because we are closing the activity.
toastManager.show(BitwardenString.an_error_has_occurred) toastManager.show(BitwardenString.an_error_has_occurred)
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Error( CreateCredentialResult.Error(
action.result.messageResourceId.asText(), action.result.messageResourceId.asText(),
), ),
), ),
@ -1982,8 +1984,10 @@ class VaultAddEditViewModel @Inject constructor(
// Use toast here because we are closing the activity. // Use toast here because we are closing the activity.
toastManager.show(BitwardenString.item_updated) toastManager.show(BitwardenString.item_updated)
sendEvent( sendEvent(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Success(action.result.responseJson), CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = action.result.responseJson,
),
), ),
) )
} }
@ -1997,7 +2001,7 @@ class VaultAddEditViewModel @Inject constructor(
when (action.result) { when (action.result) {
is ValidatePasswordResult.Error -> { is ValidatePasswordResult.Error -> {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -2022,7 +2026,7 @@ class VaultAddEditViewModel @Inject constructor(
when (action.result) { when (action.result) {
is ValidatePinResult.Error -> { is ValidatePinResult.Error -> {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -2047,7 +2051,7 @@ class VaultAddEditViewModel @Inject constructor(
it.copy(dialog = errorDialogState) it.copy(dialog = errorDialogState)
} }
} else { } else {
showFido2ErrorDialog( showCredentialErrorDialog(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
) )
@ -2058,7 +2062,7 @@ class VaultAddEditViewModel @Inject constructor(
bitwardenCredentialManager.isUserVerified = true bitwardenCredentialManager.isUserVerified = true
bitwardenCredentialManager.authenticationAttempts = 0 bitwardenCredentialManager.authenticationAttempts = 0
getRequestAndRegisterCredential() getRequestAndRegisterFido2Credential()
} }
private fun handleAuthenticatorHelpToolTipClick() { private fun handleAuthenticatorHelpToolTipClick() {
@ -2072,10 +2076,10 @@ class VaultAddEditViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) } mutableStateFlow.update { it.copy(dialog = null) }
} }
private fun showFido2ErrorDialog(message: Text) { private fun showCredentialErrorDialog(message: Text) {
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
dialog = VaultAddEditState.DialogState.Fido2Error(message), dialog = VaultAddEditState.DialogState.CredentialError(message),
) )
} }
} }
@ -2754,10 +2758,10 @@ data class VaultAddEditState(
data object InitialAutofillPrompt : DialogState() data object InitialAutofillPrompt : DialogState()
/** /**
* Displays a FIDO 2 operation error dialog to the user. * Displays a credential operation error dialog to the user.
*/ */
@Parcelize @Parcelize
data class Fido2Error(val message: Text) : DialogState() data class CredentialError(val message: Text) : DialogState()
/** /**
* Displays the overwrite passkey confirmation prompt to the user. * Displays the overwrite passkey confirmation prompt to the user.
@ -2888,12 +2892,12 @@ sealed class VaultAddEditEvent {
) : VaultAddEditEvent() ) : VaultAddEditEvent()
/** /**
* Complete the current FIDO 2 credential registration process. * Complete the current credential registration process.
* *
* @property result the result of FIDO 2 credential registration. * @property result the result of FIDO 2 credential registration.
*/ */
data class CompleteFido2Registration( data class CompleteCredentialRegistration(
val result: RegisterFido2CredentialResult, val result: CreateCredentialResult,
) : BackgroundEvent, VaultAddEditEvent() ) : BackgroundEvent, VaultAddEditEvent()
/** /**
@ -3083,9 +3087,9 @@ sealed class VaultAddEditAction {
data object UserVerificationCancelled : Common() data object UserVerificationCancelled : Common()
/** /**
* The user has dismissed the FIDO 2 credential error dialog. * The user has dismissed the credential error dialog.
*/ */
data class Fido2ErrorDialogDismissed(val message: Text) : Common() data class CredentialErrorDialogDismissed(val message: Text) : Common()
/** /**
* User verification cannot be performed with device biometrics or credentials. * User verification cannot be performed with device biometrics or credentials.

View File

@ -10,7 +10,7 @@ import java.util.UUID
/** /**
* Returns pre-filled content that may be used for an "add" type * Returns pre-filled content that may be used for an "add" type
* [VaultAddEditState.ViewState.Content] during FIDO 2 credential creation. * [VaultAddEditState.ViewState.Content] during FIDO 2 or Password credential creation.
*/ */
fun CreateCredentialRequest.toDefaultAddTypeContent( fun CreateCredentialRequest.toDefaultAddTypeContent(
attestationOptions: PasskeyAttestationOptions?, attestationOptions: PasskeyAttestationOptions?,
@ -26,11 +26,17 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
val rpName = attestationOptions val rpName = attestationOptions
?.relyingParty ?.relyingParty
?.name ?.name
.orEmpty() ?: callingAppInfo.packageName
val username = attestationOptions val username = attestationOptions
?.user ?.user
?.name ?.name
?: createPasswordCredentialRequest
?.id
.orEmpty()
val password = createPasswordCredentialRequest
?.password
.orEmpty() .orEmpty()
return VaultAddEditState.ViewState.Content( return VaultAddEditState.ViewState.Content(
@ -40,6 +46,7 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
isIndividualVaultDisabled = isIndividualVaultDisabled, isIndividualVaultDisabled = isIndividualVaultDisabled,
type = VaultAddEditState.ViewState.Content.ItemType.Login( type = VaultAddEditState.ViewState.Content.ItemType.Login(
username = username, username = username,
password = password,
uriList = listOf( uriList = listOf(
UriItem( UriItem(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),

View File

@ -50,7 +50,7 @@ import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwriteCredentialConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
@ -179,8 +179,8 @@ fun VaultItemListingScreen(
onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId)) onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId))
} }
is VaultItemListingEvent.CompleteFido2Registration -> { is VaultItemListingEvent.CompleteCredentialRegistration -> {
credentialProviderCompletionManager.completeFido2Registration(event.result) credentialProviderCompletionManager.completeCredentialRegistration(event.result)
} }
is VaultItemListingEvent.CredentialManagerUserVerification -> { is VaultItemListingEvent.CredentialManagerUserVerification -> {
@ -392,7 +392,13 @@ private fun VaultItemListingDialogs(
) )
is VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt -> { is VaultItemListingState.DialogState.OverwritePasskeyConfirmationPrompt -> {
BitwardenOverwritePasskeyConfirmationDialog( @Suppress("MaxLineLength")
BitwardenOverwriteCredentialConfirmationDialog(
title = stringResource(id = BitwardenString.overwrite_passkey),
message = stringResource(
id = BitwardenString
.this_item_already_contains_a_passkey_are_you_sure_you_want_to_overwrite_the_current_passkey,
),
onConfirmClick = { onConfirmOverwriteExistingPasskey(dialogState.cipherViewId) }, onConfirmClick = { onConfirmOverwriteExistingPasskey(dialogState.cipherViewId) },
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
) )

View File

@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.GetPublicKeyCredentialOption import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.CredentialEntry import androidx.credentials.provider.CredentialEntry
@ -77,9 +79,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
@ -168,9 +170,11 @@ class VaultItemListingViewModel @Inject constructor(
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value, isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
dialogState = providerCreateCredentialRequest?.let { dialogState = providerCreateCredentialRequest
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText()) ?.createPublicKeyCredentialRequest
}, ?.let {
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
},
policyDisablesSend = policyManager policyDisablesSend = policyManager
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND) .getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
.any(), .any(),
@ -426,8 +430,8 @@ class VaultItemListingViewModel @Inject constructor(
state.createCredentialRequest state.createCredentialRequest
?.let { ?.let {
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Cancelled, result = CreateCredentialResult.Cancelled,
), ),
) )
} }
@ -973,7 +977,7 @@ class VaultItemListingViewModel @Inject constructor(
createCredentialRequest createCredentialRequest
.providerRequest .providerRequest
.getCreatePasskeyCredentialRequestOrNull() .getCreatePasskeyCredentialRequestOrNull()
?.let { createPasskeyCredentialRequest -> ?.let {
handleItemClickForCreatePublicKeyCredentialRequest( handleItemClickForCreatePublicKeyCredentialRequest(
cipherId = action.id, cipherId = action.id,
cipherView = cipherView, cipherView = cipherView,
@ -984,7 +988,7 @@ class VaultItemListingViewModel @Inject constructor(
VaultItemListingsAction.Internal.CredentialOperationFailureReceive( VaultItemListingsAction.Internal.CredentialOperationFailureReceive(
title = BitwardenString.an_error_has_occurred.asText(), title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString message = BitwardenString
.passkey_operation_failed_because_the_request_is_unsupported .credential_operation_failed_because_the_request_is_unsupported
.asText(), .asText(),
error = null, error = null,
), ),
@ -1085,6 +1089,27 @@ class VaultItemListingViewModel @Inject constructor(
} }
} }
private fun registerCredentialToCipher(
cipherView: CipherView,
providerRequest: ProviderCreateCredentialRequest,
) {
when (providerRequest.callingRequest) {
is CreatePublicKeyCredentialRequest -> {
registerFido2CredentialToCipher(
cipherView = cipherView,
providerRequest = providerRequest,
)
}
else -> {
showCredentialManagerErrorDialog(
BitwardenString.credential_operation_failed_because_the_request_is_invalid
.asText(),
)
}
}
}
private fun registerFido2CredentialToCipher( private fun registerFido2CredentialToCipher(
cipherView: CipherView, cipherView: CipherView,
providerRequest: ProviderCreateCredentialRequest, providerRequest: ProviderCreateCredentialRequest,
@ -1326,8 +1351,8 @@ class VaultItemListingViewModel @Inject constructor(
when { when {
state.createCredentialRequest != null -> { state.createCredentialRequest != null -> {
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Error(action.message), result = CreateCredentialResult.Error(action.message),
), ),
) )
} }
@ -1530,7 +1555,7 @@ class VaultItemListingViewModel @Inject constructor(
} }
is VaultItemListingsAction.Internal.CreateCredentialRequestReceive -> { is VaultItemListingsAction.Internal.CreateCredentialRequestReceive -> {
handleRegisterFido2CredentialRequestReceive(action) handleRegisterCredentialRequestReceive(action)
} }
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> { is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
@ -1900,7 +1925,7 @@ class VaultItemListingViewModel @Inject constructor(
state.createCredentialRequest state.createCredentialRequest
?.providerRequest ?.providerRequest
?.let { request -> ?.let { request ->
registerFido2CredentialToCipher( registerCredentialToCipher(
cipherView = cipherView, cipherView = cipherView,
providerRequest = request, providerRequest = request,
) )
@ -2012,6 +2037,32 @@ class VaultItemListingViewModel @Inject constructor(
} }
} }
private fun handleRegisterCredentialRequestReceive(
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
) {
when (action.request.providerRequest.callingRequest) {
is CreatePublicKeyCredentialRequest -> {
handleRegisterFido2CredentialRequestReceive(action)
}
is CreatePasswordRequest -> {
observeVaultData()
}
else -> {
mutableStateFlow.update {
it.copy(
dialogState =
VaultItemListingState.DialogState.CredentialManagerOperationFail(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.generic_error_message.asText(),
),
)
}
}
}
}
private fun handleRegisterFido2CredentialRequestReceive( private fun handleRegisterFido2CredentialRequestReceive(
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive, action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
) { ) {
@ -2062,8 +2113,10 @@ class VaultItemListingViewModel @Inject constructor(
// user to have time to see the message. // user to have time to see the message.
toastManager.show(messageId = BitwardenString.item_updated) toastManager.show(messageId = BitwardenString.item_updated)
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Success(action.result.responseJson), CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = action.result.responseJson,
),
), ),
) )
} }
@ -2077,8 +2130,8 @@ class VaultItemListingViewModel @Inject constructor(
// user to have time to see the message. // user to have time to see the message.
toastManager.show(messageId = BitwardenString.an_error_has_occurred) toastManager.show(messageId = BitwardenString.an_error_has_occurred)
sendEvent( sendEvent(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Error( CreateCredentialResult.Error(
message = error.messageResourceId.asText(), message = error.messageResourceId.asText(),
), ),
), ),
@ -3240,12 +3293,12 @@ sealed class VaultItemListingEvent {
} }
/** /**
* Complete the current FIDO 2 credential registration process. * Complete the current credential registration process.
* *
* @property result The result of FIDO 2 credential registration. * @property result The result of the credential registration.
*/ */
data class CompleteFido2Registration( data class CompleteCredentialRegistration(
val result: RegisterFido2CredentialResult, val result: CreateCredentialResult,
) : BackgroundEvent, VaultItemListingEvent() ) : BackgroundEvent, VaultItemListingEvent()
/** /**

View File

@ -11,7 +11,7 @@ fun createMockCreateCredentialRequest(
isUserPreVerified: Boolean = false, isUserPreVerified: Boolean = false,
requestData: Bundle = bundleOf(), requestData: Bundle = bundleOf(),
): CreateCredentialRequest = CreateCredentialRequest( ): CreateCredentialRequest = CreateCredentialRequest(
userId = "mockUserId-$number", userId = "mockUserIdx-$number",
isUserPreVerified = isUserPreVerified, isUserPreVerified = isUserPreVerified,
requestData = requestData, requestData = requestData,
) )

View File

@ -125,7 +125,7 @@ class CredentialProviderProcessorTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `processCreateCredentialRequest should invoke callback with error on password create request`() { fun `processCreateCredentialRequest should invoke callback with error on password create request when userState is null`() {
val request: BeginCreatePasswordCredentialRequest = mockk { val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true) every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle() every { candidateQueryData } returns Bundle()
@ -148,6 +148,198 @@ class CredentialProviderProcessorTest {
assert(captureSlot.captured is CreateCredentialUnknownException) assert(captureSlot.captured is CreateCredentialUnknownException)
} }
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should invoke callback with result on password create request with valid userState`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { callback.onError(any()) }
assertEquals(DEFAULT_USER_STATE.accounts.size, captureSlot.captured.createEntries.size)
val capturedEntry = captureSlot.captured.createEntries[0]
assertEquals(DEFAULT_USER_STATE.accounts[0].email, capturedEntry.accountName)
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should generate correct password entries based on state`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
every { context.packageName } returns "com.x8bit.bitwarden.dev"
every { context.getString(any(), any()) } returns "mockDescription"
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(any())
} returns mockk<Cipher>()
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { callback.onError(any()) }
assertEquals(DEFAULT_USER_STATE.accounts.size, captureSlot.captured.createEntries.size)
// Verify only the active account entry has a lastUsedTime
assertEquals(
1,
captureSlot.captured.createEntries.filter { it.lastUsedTime != null }.size,
)
DEFAULT_USER_STATE.accounts.forEachIndexed { index, mockAccount ->
assertEquals(mockAccount.email, captureSlot.captured.createEntries[index].accountName)
}
// Verify all entries have biometric prompt data when feature flag is enabled
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData != null }) {
"Expected all entries to have biometric prompt data."
}
// Verify entries have the correct authenticators when cipher is not null
assertTrue(
captureSlot.captured
.createEntries
.all {
it.biometricPromptData?.allowedAuthenticators ==
BiometricManager.Authenticators.BIOMETRIC_STRONG
},
) { "Expected all entries to have BIOMETRIC_STRONG authenticators." }
// Verify entries have no biometric prompt data when cipher is null
every { biometricsEncryptionManager.getOrCreateCipher(any()) } returns null
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
assertTrue(
captureSlot.captured.createEntries.all { it.biometricPromptData == null },
) { "Expected all entries to have null biometric prompt data." }
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should not add biometric data to password entries on pre-V devices`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every {
biometricsEncryptionManager.getOrCreateCipher(userId = any())
} returns mockk<Cipher>()
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns false
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
// Verify entries have no biometric prompt data on older devices
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData == null }) {
"Expected all entries to have null biometric prompt data on pre-V devices."
}
}
@Suppress("MaxLineLength")
@Test
fun `processCreateCredentialRequest should not add biometric data to password entries when vault is locked`() {
val request: BeginCreatePasswordCredentialRequest = mockk {
every { callingAppInfo } returns mockk(relaxed = true)
every { candidateQueryData } returns Bundle()
}
val callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException> =
mockk()
val captureSlot = slot<BeginCreateCredentialResponse>()
val mockIntent: PendingIntent = mockk()
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map { it.copy(isVaultUnlocked = false) },
)
every { context.packageName } returns "com.x8bit.bitwarden"
every { context.getString(any(), any()) } returns "mockDescription"
every {
pendingIntentManager.createPasswordCreationPendingIntent(
userId = any(),
)
} returns mockIntent
every { cancellationSignal.setOnCancelListener(any()) } just runs
every { callback.onResult(capture(captureSlot)) } just runs
every { isBuildVersionAtLeast(Build.VERSION_CODES.VANILLA_ICE_CREAM) } returns true
credentialProviderProcessor.processCreateCredentialRequest(
request = request,
cancellationSignal = cancellationSignal,
callback = callback,
)
verify(exactly = 1) { callback.onResult(any()) }
verify(exactly = 0) { biometricsEncryptionManager.getOrCreateCipher(any()) }
// Verify entries have no biometric prompt data when vault is locked
assertTrue(captureSlot.captured.createEntries.all { it.biometricPromptData == null }) {
"Expected all entries to have null biometric prompt data when vault is locked."
}
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() { fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() {

View File

@ -48,7 +48,7 @@ fun createMockCipherView(
organizationId: String? = "mockOrganizationId-$number", organizationId: String? = "mockOrganizationId-$number",
folderId: String? = "mockId-$number", folderId: String? = "mockId-$number",
notes: String? = "mockNotes-$number", notes: String? = "mockNotes-$number",
password: String = "mockPassword-$number", password: String? = "mockPassword-$number",
clock: Clock = FIXED_CLOCK, clock: Clock = FIXED_CLOCK,
fido2Credentials: List<Fido2Credential>? = null, fido2Credentials: List<Fido2Credential>? = null,
sshKey: SshKeyView? = createMockSshKeyView(number = number), sshKey: SshKeyView? = createMockSshKeyView(number = number),

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import androidx.credentials.CreatePasswordResponse
import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialUnknownException import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.BeginGetCredentialResponse
@ -16,9 +17,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFido2Creden
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPasswordCredentialAutofillCipherLogin import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockPasswordCredentialAutofillCipherLogin
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import io.mockk.Called import io.mockk.Called
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
import io.mockk.Ordering import io.mockk.Ordering
@ -60,9 +61,11 @@ class CredentialProviderCompletionManagerTest {
} }
@Test @Test
fun `completeFido2Registration should perform no operations`() { fun `completeCredentialRegistration should perform no operations`() {
val mockRegistrationResult = mockk<RegisterFido2CredentialResult>() val mockRegistrationResult = mockk<CreateCredentialResult>()
credentialProviderCompletionManager.completeFido2Registration(mockRegistrationResult) credentialProviderCompletionManager.completeCredentialRegistration(
mockRegistrationResult,
)
verify { verify {
mockRegistrationResult wasNot Called mockRegistrationResult wasNot Called
mockActivity wasNot Called mockActivity wasNot Called
@ -132,10 +135,10 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `completeFido2Registration should set CreateCredentialResponse, set activity result, then finish activity when result is Success`() { fun `completeCredentialRegistration should set CreateCredentialResponse, set activity result, then finish activity when result is SuccessFido2`() {
credentialProviderCompletionManager credentialProviderCompletionManager
.completeFido2Registration( .completeCredentialRegistration(
RegisterFido2CredentialResult.Success( CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "registrationResponse", responseJson = "registrationResponse",
), ),
) )
@ -147,9 +150,24 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Error`() { fun `completeCredentialRegistration should set CreateCredentialResponse, set activity result, then finish activity when result is SuccessPassword`() {
credentialProviderCompletionManager.completeCredentialRegistration(
CreateCredentialResult.Success.PasswordCreated,
)
verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setCreateCredentialResponse(
intent = any(),
response = any<CreatePasswordResponse>(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `completeCredentialRegistration should set CreateCredentialException, set activity result, then finish activity when result is Error`() {
credentialProviderCompletionManager credentialProviderCompletionManager
.completeFido2Registration(RegisterFido2CredentialResult.Error("".asText())) .completeCredentialRegistration(CreateCredentialResult.Error("".asText()))
verifyActivityResultIsSetAndFinishedAfter { verifyActivityResultIsSetAndFinishedAfter {
mockActivity.resources mockActivity.resources
@ -159,9 +177,9 @@ class CredentialProviderCompletionManagerTest {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `completeFido2Registration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() { fun `completeCredentialRegistration should set CreateCredentialException, set activity result, then finish activity when result is Cancelled`() {
credentialProviderCompletionManager credentialProviderCompletionManager
.completeFido2Registration(RegisterFido2CredentialResult.Cancelled) .completeCredentialRegistration(CreateCredentialResult.Cancelled)
verifyActivityResultIsSetAndFinishedAfter { verifyActivityResultIsSetAndFinishedAfter {
PendingIntentHandler.setCreateCredentialException(any(), any()) PendingIntentHandler.setCreateCredentialException(any(), any())

View File

@ -313,6 +313,31 @@ class RootNavScreenTest : BitwardenComposeTest() {
} }
} }
// Make sure navigating to vault unlocked for create password request works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForCreatePasswordRequest(
username = "activeUserId",
password = "mockPassword",
uri = "mockUri",
)
composeTestRule.runOnIdle {
verify {
mockNavHostController.navigate(
route = VaultUnlockedGraphRoute,
navOptions = expectedNavOptions,
)
mockNavHostController.navigate(
route = VaultAddEditRoute(
vaultAddEditMode = VaultAddEditMode.ADD,
vaultItemId = null,
vaultItemCipherType = VaultItemCipherType.LOGIN,
selectedFolderId = null,
selectedCollectionId = null,
),
navOptions = expectedNavOptions,
)
}
}
// Make sure navigating to vault unlocked for CreateCredentialRequest works as expected: // Make sure navigating to vault unlocked for CreateCredentialRequest works as expected:
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save( rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save(
activeUserId = "activeUserId", activeUserId = "activeUserId",

View File

@ -1,6 +1,9 @@
package com.x8bit.bitwarden.ui.platform.feature.rootnav package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.ProviderCreateCredentialRequest
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.cxf.model.ImportCredentialsRequestData import com.bitwarden.cxf.model.ImportCredentialsRequestData
import com.bitwarden.data.repository.model.Environment import com.bitwarden.data.repository.model.Environment
@ -28,7 +31,9 @@ import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -64,6 +69,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
@AfterEach @AfterEach
fun tearDown() { fun tearDown() {
unmockkStatic(::parseJwtTokenDataOrNull) unmockkStatic(::parseJwtTokenDataOrNull)
unmockkObject(ProviderCreateCredentialRequest.Companion)
} }
@Test @Test
@ -682,11 +688,18 @@ class RootNavViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an unlocked vault but there is a Fido2Save special circumstance the nav state should be VaultUnlockedForFido2Save`() { fun `when the active user has an unlocked vault but there is a Fido2Save special circumstance the nav state should be VaultUnlockedForFido2Save`() {
mockkObject(ProviderCreateCredentialRequest.Companion)
val createCredentialRequest = CreateCredentialRequest( val createCredentialRequest = CreateCredentialRequest(
userId = "activeUserId", userId = "activeUserId",
isUserPreVerified = false, isUserPreVerified = false,
requestData = bundleOf(), requestData = bundleOf(),
) )
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk {
every { callingRequest } returns mockk<CreatePublicKeyCredentialRequest>()
}
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(createCredentialRequest) SpecialCircumstance.ProviderCreateCredential(createCredentialRequest)
mutableUserStateFlow.tryEmit( mutableUserStateFlow.tryEmit(
@ -728,6 +741,73 @@ class RootNavViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault but there is a ProviderCreateCredential with password request the nav state should be VaultUnlockedForCreatePasswordRequest`() {
mockkObject(ProviderCreateCredentialRequest.Companion)
val mockUsername = "testUser"
val mockPassword = "testPassword123"
val mockPackageName = "com.example.app"
val createCredentialRequest = CreateCredentialRequest(
userId = "activeUserId",
isUserPreVerified = false,
requestData = bundleOf(),
)
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk {
every { callingRequest } returns mockk<CreatePasswordRequest> {
every { id } returns mockUsername
every { password } returns mockPassword
}
every { callingAppInfo } returns mockk {
every { packageName } returns mockPackageName
}
}
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(createCredentialRequest)
mutableUserStateFlow.tryEmit(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "name",
email = "email",
avatarColorHex = "avatarHexColor",
environment = Environment.Us,
isPremium = true,
isLoggedIn = true,
isVaultUnlocked = true,
needsPasswordReset = false,
isBiometricsEnabled = false,
organizations = emptyList(),
needsMasterPassword = false,
trustedDevice = null,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = FirstTimeState(
showImportLoginsCard = true,
),
isExportable = true,
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.VaultUnlockedForCreatePasswordRequest(
username = mockUsername,
password = mockPassword,
uri = "androidapp://$mockPackageName",
),
viewModel.stateFlow.value,
)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an unlocked vault but there is a Fido2Assertion special circumstance the nav state should be VaultUnlockedForFido2Assertion`() { fun `when the active user has an unlocked vault but there is a Fido2Assertion special circumstance the nav state should be VaultUnlockedForFido2Assertion`() {

View File

@ -57,7 +57,7 @@ import com.bitwarden.vault.UriMatchType
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
@ -112,7 +112,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
} }
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk { private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
every { completeFido2Registration(any()) } just runs every { completeCredentialRegistration(any()) } just runs
} }
private val biometricsManager: BiometricsManager = mockk { private val biometricsManager: BiometricsManager = mockk {
every { isUserVerificationSupported } returns true every { isUserVerificationSupported } returns true
@ -233,18 +233,18 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
} }
@Test @Test
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() { fun `on CompleteCredentialCreate event should invoke CredentialProviderCompletionManager`() {
val result = RegisterFido2CredentialResult.Success( val result = CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockRegistrationResponse", responseJson = "mockRegistrationResponse",
) )
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result)) mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteCredentialRegistration(result = result))
verify { credentialProviderCompletionManager.completeFido2Registration(result) } verify { credentialProviderCompletionManager.completeCredentialRegistration(result) }
} }
@Test @Test
fun `Fido2Error dialog should display based on state`() { fun `CredentialError dialog should display based on state`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy( mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()), dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
) )
composeTestRule composeTestRule
@ -464,7 +464,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
@Test @Test
fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() { fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() {
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy( mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()), dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
) )
composeTestRule composeTestRule
@ -474,7 +474,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
verify { verify {
viewModel.trySendAction( viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed("mockMessage".asText()), VaultAddEditAction.Common.CredentialErrorDialogDismissed("mockMessage".asText()),
) )
} }
} }

View File

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.credentials.CreatePasswordRequest
import androidx.credentials.CreatePublicKeyCredentialRequest import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderCreateCredentialRequest
@ -78,7 +77,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
@ -1085,8 +1084,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
assertEquals(stateWithSavingDialog, stateFlow.awaitItem()) assertEquals(stateWithSavingDialog, stateFlow.awaitItem())
assertEquals(stateWithName, stateFlow.awaitItem()) assertEquals(stateWithName, stateFlow.awaitItem())
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Success( CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse", responseJson = "mockResponse",
), ),
), ),
@ -1183,55 +1182,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `in add mode during fido2, SaveClick should show fido2 error dialog when request type is not supported`() =
runTest {
val fido2CredentialRequest = createMockCreateCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential(
createCredentialRequest = fido2CredentialRequest,
)
val stateWithName = createVaultAddItemState(
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
createCredentialRequest = fido2CredentialRequest,
)
.copy(shouldExitOnSave = true)
val mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
mockk<ProviderCreateCredentialRequest>(relaxed = true) {
every { callingAppInfo } returns mockk(relaxed = true)
every { callingRequest } returns mockk<CreatePasswordRequest>(relaxed = true)
}
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
mutableVaultDataFlow.value = DataState.Loaded(
createVaultData(),
)
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
)
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(
VaultAddEditState.DialogState.Fido2Error(
message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.asText(),
),
viewModel.stateFlow.value.dialog,
)
}
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() = fun `in add mode during fido2, SaveClick should emit fido user verification as optional when verification is PREFERRED`() =
@ -2276,7 +2226,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() = fun `DismissFido2ErrorDialogClick should clear the dialog state then complete FIDO 2 create`() =
runTest { runTest {
val errorState = createVaultAddItemState( val errorState = createVaultAddItemState(
dialogState = VaultAddEditState.DialogState.Fido2Error( dialogState = VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
), ),
) )
@ -2288,15 +2238,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
), ),
) )
viewModel.trySendAction( viewModel.trySendAction(
VaultAddEditAction.Common.Fido2ErrorDialogDismissed( VaultAddEditAction.Common.CredentialErrorDialogDismissed(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
), ),
) )
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialog) assertNull(viewModel.stateFlow.value.dialog)
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Error( result = CreateCredentialResult.Error(
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
), ),
@ -4034,12 +3984,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationLockout should set isUserVerified to false and display Fido2ErrorDialog`() { fun `UserVerificationLockout should set isUserVerified to false and display CredentialErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationLockOut)
verify { bitwardenCredentialManager.isUserVerified = false } verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
), ),
viewModel.stateFlow.value.dialog, viewModel.stateFlow.value.dialog,
@ -4048,7 +3998,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Create with cancelled result`() = fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteCredentialRegistration with cancelled result`() =
runTest { runTest {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
@ -4056,8 +4006,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
assertNull(viewModel.stateFlow.value.dialog) assertNull(viewModel.stateFlow.value.dialog)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Cancelled, result = CreateCredentialResult.Cancelled,
), ),
awaitItem(), awaitItem(),
) )
@ -4066,12 +4016,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationFail should set isUserVerified to false, and display Fido2ErrorDialog`() { fun `UserVerificationFail should set isUserVerified to false, and display CredentialErrorDialog`() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationFail)
verify { bitwardenCredentialManager.isUserVerified = false } verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
), ),
viewModel.stateFlow.value.dialog, viewModel.stateFlow.value.dialog,
@ -4080,12 +4030,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationNotSupported should display Fido2ErrorDialog when active account not found`() { fun `UserVerificationNotSupported should display CredentialErrorDialog when active account not found`() {
mutableUserStateFlow.value = null mutableUserStateFlow.value = null
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
verify { bitwardenCredentialManager.isUserVerified = false } verify { bitwardenCredentialManager.isUserVerified = false }
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(), message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
), ),
viewModel.stateFlow.value.dialog, viewModel.stateFlow.value.dialog,
@ -4181,7 +4131,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when password verification fails`() { fun `MasterPasswordFido2VerificationSubmit should display CredentialError when password verification fails`() {
val password = "password" val password = "password"
coEvery { coEvery {
authRepository.validatePassword(password = password) authRepository.validatePassword(password = password)
@ -4194,7 +4144,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
), ),
@ -4230,7 +4180,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when user has no retries remaining`() { fun `MasterPasswordFido2VerificationSubmit should display CredentialError when user has no retries remaining`() {
val password = "password" val password = "password"
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
coEvery { coEvery {
@ -4244,7 +4194,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
), ),
@ -4286,7 +4236,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `PinFido2VerificationSubmit should display Fido2Error when Pin verification fails`() { fun `PinFido2VerificationSubmit should display CredentialError when Pin verification fails`() {
val pin = "PIN" val pin = "PIN"
coEvery { coEvery {
authRepository.validatePin(pin = pin) authRepository.validatePin(pin = pin)
@ -4299,7 +4249,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
), ),
@ -4335,7 +4285,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `PinFido2VerificationSubmit should display Fido2Error when user has no retries remaining`() { fun `PinFido2VerificationSubmit should display CredentialError when user has no retries remaining`() {
val pin = "PIN" val pin = "PIN"
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
coEvery { coEvery {
@ -4349,7 +4299,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
) )
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
), ),
@ -4430,13 +4380,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `DismissFido2VerificationDialogClick should display Fido2ErrorDialog`() { fun `DismissFido2VerificationDialogClick should display CredentialErrorDialog`() {
viewModel.trySendAction( viewModel.trySendAction(
VaultAddEditAction.Common.DismissFido2VerificationDialogClick, VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
) )
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString message = BitwardenString
.passkey_operation_failed_because_user_could_not_be_verified .passkey_operation_failed_because_user_could_not_be_verified
.asText(), .asText(),
@ -4447,7 +4397,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationSuccess should display Fido2ErrorDialog when request is invalid`() { fun `UserVerificationSuccess should display CredentialErrorDialog when request is invalid`() {
every { authRepository.activeUserId } returns null every { authRepository.activeUserId } returns null
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential( SpecialCircumstance.ProviderCreateCredential(
@ -4459,7 +4409,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess) viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
assertEquals( assertEquals(
VaultAddEditState.DialogState.Fido2Error( VaultAddEditState.DialogState.CredentialError(
message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
.asText(), .asText(),
), ),
@ -4499,7 +4449,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() = fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
runTest { runTest {
val mockRequest = createMockCreateCredentialRequest(number = 1) val mockRequest = createMockCreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Error.InternalError val mockResult = Fido2RegisterCredentialResult.Error.InternalError
@ -4527,8 +4477,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Error( CreateCredentialResult.Error(
BitwardenString.passkey_registration_failed_due_to_an_internal_error BitwardenString.passkey_registration_failed_due_to_an_internal_error
.asText(), .asText(),
), ),
@ -4543,7 +4493,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() = fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
runTest { runTest {
val mockRequest = createMockCreateCredentialRequest(number = 1) val mockRequest = createMockCreateCredentialRequest(number = 1)
val mockResult = Fido2RegisterCredentialResult.Success( val mockResult = Fido2RegisterCredentialResult.Success(
@ -4572,8 +4522,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultAddEditEvent.CompleteFido2Registration( VaultAddEditEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Success( CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse", responseJson = "mockResponse",
), ),
), ),

View File

@ -49,9 +49,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager import com.x8bit.bitwarden.ui.credentials.manager.CredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
@ -106,7 +106,7 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
every { launchUri(any()) } just runs every { launchUri(any()) } just runs
} }
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk { private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
every { completeFido2Registration(any()) } just runs every { completeCredentialRegistration(any()) } just runs
every { completeFido2Assertion(any()) } just runs every { completeFido2Assertion(any()) } just runs
every { completePasswordGet(any()) } just runs every { completePasswordGet(any()) } just runs
every { completeProviderGetCredentialsRequest(any()) } just runs every { completeProviderGetCredentialsRequest(any()) } just runs
@ -1966,11 +1966,11 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `CompleteFido2Registration event should call CredentialProviderCompletionManager with result`() { fun `CompleteCredentialRegistration event should call CredentialProviderCompletionManager with result`() {
val result = RegisterFido2CredentialResult.Success("mockResponse") val result = CreateCredentialResult.Success.Fido2CredentialRegistered("mockResponse")
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Registration(result)) mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteCredentialRegistration(result))
verify { verify {
credentialProviderCompletionManager.completeFido2Registration(result) credentialProviderCompletionManager.completeCredentialRegistration(result)
} }
} }

View File

@ -99,9 +99,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.AssertFido2CredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.CreateCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult import com.x8bit.bitwarden.ui.credentials.manager.model.GetPasswordCredentialResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
@ -261,10 +261,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
private val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> { private val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> {
every { requestJson } returns "mockRequestJson" every { requestJson } returns "mockRequestJson"
} }
private val mockCreatePublicKeyCredentialOption = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
}
private val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> { private val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> {
every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption) every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption)
every { callingAppInfo } returns mockCallingAppInfo every { callingAppInfo } returns mockCallingAppInfo
} }
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
every { callingRequest } returns mockCreatePublicKeyCredentialOption
every { callingAppInfo } returns mockCallingAppInfo
}
private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption> { private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption> {
every { requestJson } returns "mockRequestJson" every { requestJson } returns "mockRequestJson"
} }
@ -272,14 +280,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { beginGetCredentialOptions } returns listOf(mockBeginGetPublicKeyCredentialOption) every { beginGetCredentialOptions } returns listOf(mockBeginGetPublicKeyCredentialOption)
every { callingAppInfo } returns mockCallingAppInfo every { callingAppInfo } returns mockCallingAppInfo
} }
private val mockCreatePublicKeyCredentialRequest = mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
}
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
every { callingAppInfo } returns mockCallingAppInfo
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> = private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow() bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay> = mockk { private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay> = mockk {
@ -303,12 +304,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
ProviderGetCredentialRequest.Companion, ProviderGetCredentialRequest.Companion,
BeginGetCredentialRequest.Companion, BeginGetCredentialRequest.Companion,
) )
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
every { every {
ProviderGetCredentialRequest.fromBundle(any()) ProviderGetCredentialRequest.fromBundle(any())
} returns mockProviderGetCredentialRequest } returns mockProviderGetCredentialRequest
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
every { every {
BeginGetCredentialRequest.fromBundle(any()) BeginGetCredentialRequest.fromBundle(any())
} returns mockBeginGetCredentialRequest } returns mockBeginGetCredentialRequest
@ -348,6 +350,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
coEvery { coEvery {
originManager.validateOrigin(any(), any()) originManager.validateOrigin(any(), any())
} returns ValidateOriginResult.Success(null) } returns ValidateOriginResult.Success(null)
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.stateFlow.test { viewModel.stateFlow.test {
@ -511,6 +515,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
shouldFinishWhenComplete = false, shouldFinishWhenComplete = false,
) )
val searchType = SearchType.Vault.All val searchType = SearchType.Vault.All
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.eventFlow.test { viewModel.eventFlow.test {
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick) viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)
@ -677,7 +682,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `ItemClick for vault item during FIDO 2 registration should show FIDO 2 error dialog when cipherView is null`() { fun `ItemClick for vault item during credential registration should show credential error dialog when cipherView is null`() {
val cipherView = createMockCipherView(number = 1) val cipherView = createMockCipherView(number = 1)
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential( SpecialCircumstance.ProviderCreateCredential(
@ -755,6 +760,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns Fido2RegisterCredentialResult.Success("mockResponse") } returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.ItemClick( VaultItemListingsAction.ItemClick(
@ -810,6 +816,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns Fido2RegisterCredentialResult.Success("mockResponse") } returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.ItemClick( VaultItemListingsAction.ItemClick(
@ -881,6 +888,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns Fido2RegisterCredentialResult.Success("mockResponse") } returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.ItemClick( VaultItemListingsAction.ItemClick(
@ -934,6 +942,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns Fido2RegisterCredentialResult.Success("mockResponse") } returns Fido2RegisterCredentialResult.Success("mockResponse")
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.ItemClick( VaultItemListingsAction.ItemClick(
@ -2313,6 +2322,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
) )
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
mutableVaultDataStateFlow.value = dataState mutableVaultDataStateFlow.value = dataState
@ -2990,6 +3000,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest { fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled) assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
@ -3065,6 +3076,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Success("mockOrigin") } returns ValidateOriginResult.Success("mockOrigin")
setupFido2CreateRequest()
createVaultItemListingViewModel() createVaultItemListingViewModel()
coVerify(ordering = Ordering.ORDERED) { coVerify(ordering = Ordering.ORDERED) {
@ -3075,9 +3087,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `ValidateOriginResult should update dialog state on Unknown error`() = runTest { fun `ValidateOriginResult should update dialog state on Unknown error`() = runTest {
val mockCredentialsRequest = createMockCreateCredentialRequest(number = 1)
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential( SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1), mockCredentialsRequest,
) )
coEvery { coEvery {
originManager.validateOrigin( originManager.validateOrigin(
@ -3086,6 +3099,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Error.Unknown } returns ValidateOriginResult.Error.Unknown
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertEquals( assertEquals(
@ -3111,6 +3125,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed } returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertEquals( assertEquals(
@ -3139,6 +3154,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Error.PrivilegedAppSignatureNotFound } returns ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertEquals( assertEquals(
@ -3165,6 +3181,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Error.PasskeyNotSupportedForApp } returns ValidateOriginResult.Error.PasskeyNotSupportedForApp
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertEquals( assertEquals(
@ -3191,6 +3208,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns ValidateOriginResult.Error.AssetLinkNotFound } returns ValidateOriginResult.Error.AssetLinkNotFound
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
assertEquals( assertEquals(
@ -3204,7 +3222,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() = fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
runTest { runTest {
val mockResult = Fido2RegisterCredentialResult.Error.InternalError val mockResult = Fido2RegisterCredentialResult.Error.InternalError
@ -3217,8 +3235,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Error( CreateCredentialResult.Error(
BitwardenString.passkey_registration_failed_due_to_an_internal_error.asText(), BitwardenString.passkey_registration_failed_due_to_an_internal_error.asText(),
), ),
), ),
@ -3232,7 +3250,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() = fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
runTest { runTest {
val mockResult = Fido2RegisterCredentialResult.Success( val mockResult = Fido2RegisterCredentialResult.Success(
responseJson = "mockResponse", responseJson = "mockResponse",
@ -3247,8 +3265,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
RegisterFido2CredentialResult.Success( CreateCredentialResult.Success.Fido2CredentialRegistered(
responseJson = "mockResponse", responseJson = "mockResponse",
), ),
), ),
@ -3268,6 +3286,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
SpecialCircumstance.ProviderCreateCredential( SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1), createMockCreateCredentialRequest(number = 1),
) )
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.DismissCredentialManagerErrorDialogClick( VaultItemListingsAction.DismissCredentialManagerErrorDialogClick(
@ -3277,8 +3296,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertNull(viewModel.stateFlow.value.dialogState) assertNull(viewModel.stateFlow.value.dialogState)
assertEquals( assertEquals(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Error( result = CreateCredentialResult.Error(
"".asText(), "".asText(),
), ),
), ),
@ -4130,12 +4149,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteFido2Registration with cancelled result`() = fun `UserVerificationCancelled should clear dialog state, set isUserVerified to false, and emit CompleteCredentialRegistration with cancelled result`() =
runTest { runTest {
specialCircumstanceManager.specialCircumstance = specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ProviderCreateCredential( SpecialCircumstance.ProviderCreateCredential(
createMockCreateCredentialRequest(number = 1), createMockCreateCredentialRequest(number = 1),
) )
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled) viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
@ -4143,8 +4163,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertNull(viewModel.stateFlow.value.dialogState) assertNull(viewModel.stateFlow.value.dialogState)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
VaultItemListingEvent.CompleteFido2Registration( VaultItemListingEvent.CompleteCredentialRegistration(
result = RegisterFido2CredentialResult.Cancelled, result = CreateCredentialResult.Cancelled,
), ),
awaitItem(), awaitItem(),
) )
@ -4277,6 +4297,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
), ),
) )
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess( VaultItemListingsAction.UserVerificationSuccess(
@ -4314,6 +4335,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
responseJson = "mockResponse", responseJson = "mockResponse",
) )
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.UserVerificationSuccess( VaultItemListingsAction.UserVerificationSuccess(
@ -5013,6 +5035,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
) )
} returns UserVerificationRequirement.REQUIRED } returns UserVerificationRequirement.REQUIRED
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick( VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
@ -5339,6 +5362,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test @Test
fun `InternetConnectionErrorReceived should show network error if no internet connection`() = fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
runTest { runTest {
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
VaultItemListingsAction.Internal.InternetConnectionErrorReceived, VaultItemListingsAction.Internal.InternetConnectionErrorReceived,
@ -5372,6 +5396,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
originManager.validateOrigin(any(), any()) originManager.validateOrigin(any(), any())
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed } returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
@ -5403,6 +5428,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
mockCallingAppInfo.getSignatureFingerprintAsHexString() mockCallingAppInfo.getSignatureFingerprintAsHexString()
} returns null } returns null
setupFido2CreateRequest()
val viewModel = createVaultItemListingViewModel() val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction( viewModel.trySendAction(
@ -5791,6 +5817,24 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
} }
} }
private fun setupFido2CreateRequest(
mockCallingAppInfo: CallingAppInfo = this.mockCallingAppInfo,
mockCreatePublicKeyCredentialRequest: CreatePublicKeyCredentialRequest =
mockk<CreatePublicKeyCredentialRequest> {
every { requestJson } returns "mockRequestJson"
every { origin } returns "mockOrigin"
},
mockProviderCreateCredentialRequest: ProviderCreateCredentialRequest =
mockk<ProviderCreateCredentialRequest> {
every { callingAppInfo } returns mockCallingAppInfo
every { callingRequest } returns mockCreatePublicKeyCredentialRequest
},
) {
every {
ProviderCreateCredentialRequest.fromBundle(any())
} returns mockProviderCreateCredentialRequest
}
private fun createSavedStateHandleWithVaultItemListingType( private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType, vaultItemListingType: VaultItemListingType,
): SavedStateHandle = SavedStateHandle().apply { ): SavedStateHandle = SavedStateHandle().apply {

View File

@ -691,6 +691,7 @@ Do you want to switch to this account?</string>
<string name="follow_the_steps_from_duo_to_finish_logging_in">Follow the steps from Duo to finish logging in.</string> <string name="follow_the_steps_from_duo_to_finish_logging_in">Follow the steps from Duo to finish logging in.</string>
<string name="launch_duo">Launch Duo</string> <string name="launch_duo">Launch Duo</string>
<string name="your_passkey_will_be_saved_to_your_bitwarden_vault_for_x">Your passkey will be saved to your Bitwarden vault for %1$s</string> <string name="your_passkey_will_be_saved_to_your_bitwarden_vault_for_x">Your passkey will be saved to your Bitwarden vault for %1$s</string>
<string name="your_password_will_be_saved_to_your_bitwarden_vault_for_x">Your password will be saved to your Bitwarden vault for %1$s</string>
<string name="passkeys_not_supported_for_this_app">Passkeys not supported for this app</string> <string name="passkeys_not_supported_for_this_app">Passkeys not supported for this app</string>
<string name="passkey_operation_failed_because_browser_is_not_privileged">Passkey operation failed because browser is not privileged</string> <string name="passkey_operation_failed_because_browser_is_not_privileged">Passkey operation failed because browser is not privileged</string>
<string name="passkey_operation_failed_because_browser_signature_does_not_match">Passkey operation failed because browser signature does not match</string> <string name="passkey_operation_failed_because_browser_signature_does_not_match">Passkey operation failed because browser signature does not match</string>
@ -917,6 +918,8 @@ Do you want to switch to this account?</string>
<string name="passkey_operation_failed_because_the_request_is_unsupported">Passkey operation failed because the request is unsupported.</string> <string name="passkey_operation_failed_because_the_request_is_unsupported">Passkey operation failed because the request is unsupported.</string>
<string name="password_operation_failed_because_the_selected_item_does_not_exist">Password operation failed because the selected item does not exist.</string> <string name="password_operation_failed_because_the_selected_item_does_not_exist">Password operation failed because the selected item does not exist.</string>
<string name="password_operation_failed_because_no_item_was_selected">Password operation failed because no item was selected.</string> <string name="password_operation_failed_because_no_item_was_selected">Password operation failed because no item was selected.</string>
<string name="credential_operation_failed_because_the_request_is_invalid">Credential operation failed because the request is invalid.</string>
<string name="credential_operation_failed_because_the_request_is_unsupported">Credential operation failed because the request is unsupported.</string>
<string name="credential_operation_failed_because_user_verification_attempts_exceeded">Credential operation failed because user verification attempts exceeded.</string> <string name="credential_operation_failed_because_user_verification_attempts_exceeded">Credential operation failed because user verification attempts exceeded.</string>
<string name="credential_operation_failed_because_user_is_locked_out">Credential operation failed because user is locked out.</string> <string name="credential_operation_failed_because_user_is_locked_out">Credential operation failed because user is locked out.</string>
<string name="credential_operation_failed_because_the_selected_item_does_not_exist">Credential operation failed because the selected item does not exist.</string> <string name="credential_operation_failed_because_the_selected_item_does_not_exist">Credential operation failed because the selected item does not exist.</string>