mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[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:
parent
839e9e8a1a
commit
5ec0a1986d
@ -86,6 +86,7 @@
|
||||
<intent-filter>
|
||||
<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_CREATE_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_GET_PASSWORD" />
|
||||
<action android:name="com.x8bit.bitwarden.credentials.ACTION_UNLOCK_ACCOUNT" />
|
||||
|
||||
|
||||
@ -58,6 +58,13 @@ interface CredentialManagerPendingIntentManager {
|
||||
userId: String,
|
||||
): 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.
|
||||
*/
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
@ -101,4 +119,5 @@ class CredentialManagerPendingIntentManagerImpl(
|
||||
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 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"
|
||||
|
||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.credentials.model
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
@ -48,6 +49,15 @@ data class CreateCredentialRequest(
|
||||
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
|
||||
* is not a [CreatePublicKeyCredentialRequest].
|
||||
|
||||
@ -19,6 +19,7 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.AuthenticationAction
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginCreatePasswordCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
@ -69,7 +70,7 @@ class CredentialProviderProcessorImpl(
|
||||
}
|
||||
|
||||
val createCredentialJob = ioScope.launch {
|
||||
processCreateCredentialRequest(request = request)
|
||||
(handleCreatePasskeyQuery(request) ?: handleCreatePasswordQuery(request))
|
||||
?.let { callback.onResult(it) }
|
||||
?: callback.onError(CreateCredentialUnknownException())
|
||||
}
|
||||
@ -137,21 +138,11 @@ class CredentialProviderProcessorImpl(
|
||||
callback.onError(ClearCredentialUnsupportedException())
|
||||
}
|
||||
|
||||
private fun processCreateCredentialRequest(
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreateCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
return when (request) {
|
||||
is BeginCreatePublicKeyCredentialRequest -> {
|
||||
handleCreatePasskeyQuery(request)
|
||||
}
|
||||
if (request !is BeginCreatePublicKeyCredentialRequest) return null
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreatePasskeyQuery(
|
||||
request: BeginCreatePublicKeyCredentialRequest,
|
||||
): BeginCreateCredentialResponse? {
|
||||
val requestJson = request
|
||||
.candidateQueryData
|
||||
.getString("androidx.credentials.BUNDLE_KEY_REQUEST_JSON")
|
||||
@ -161,14 +152,19 @@ class CredentialProviderProcessorImpl(
|
||||
val userState = authRepository.userStateFlow.value ?: return null
|
||||
|
||||
return BeginCreateCredentialResponse.Builder()
|
||||
.setCreateEntries(userState.accounts.toCreateEntries(userState.activeUserId))
|
||||
.setCreateEntries(
|
||||
userState.accounts.toCreatePasskeyEntry(userState.activeUserId),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<UserState.Account>.toCreateEntries(activeUserId: String) =
|
||||
map { it.toCreateEntry(isActive = activeUserId == it.userId) }
|
||||
private fun List<UserState.Account>.toCreatePasskeyEntry(
|
||||
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 entryBuilder = CreateEntry
|
||||
.Builder(
|
||||
@ -196,6 +192,54 @@ class CredentialProviderProcessorImpl(
|
||||
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(
|
||||
cipher: Cipher,
|
||||
): CreateEntry.Builder {
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
package com.x8bit.bitwarden.ui.credentials.manager
|
||||
|
||||
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.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 {
|
||||
|
||||
/**
|
||||
* 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].
|
||||
|
||||
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.credentials.CreatePasswordResponse
|
||||
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||
import androidx.credentials.GetCredentialResponse
|
||||
import androidx.credentials.PasswordCredential
|
||||
@ -15,9 +16,9 @@ import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.PendingIntentHandler
|
||||
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.GetPasswordCredentialResult
|
||||
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
|
||||
|
||||
/**
|
||||
* Primary implementation of [CredentialProviderCompletionManager] when the build version is
|
||||
@ -28,11 +29,11 @@ class CredentialProviderCompletionManagerImpl(
|
||||
private val activity: Activity,
|
||||
) : CredentialProviderCompletionManager {
|
||||
|
||||
override fun completeFido2Registration(result: RegisterFido2CredentialResult) {
|
||||
override fun completeCredentialRegistration(result: CreateCredentialResult) {
|
||||
activity.also {
|
||||
val intent = Intent()
|
||||
when (result) {
|
||||
is RegisterFido2CredentialResult.Error -> {
|
||||
is CreateCredentialResult.Error -> {
|
||||
PendingIntentHandler
|
||||
.setCreateCredentialException(
|
||||
intent = intent,
|
||||
@ -42,7 +43,7 @@ class CredentialProviderCompletionManagerImpl(
|
||||
)
|
||||
}
|
||||
|
||||
is RegisterFido2CredentialResult.Success -> {
|
||||
is CreateCredentialResult.Success.Fido2CredentialRegistered -> {
|
||||
PendingIntentHandler
|
||||
.setCreateCredentialResponse(
|
||||
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
|
||||
.setCreateCredentialException(
|
||||
intent = intent,
|
||||
|
||||
@ -3,9 +3,9 @@ package com.x8bit.bitwarden.ui.credentials.manager
|
||||
import androidx.credentials.CredentialProvider
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
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.GetPasswordCredentialResult
|
||||
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
|
||||
|
||||
/**
|
||||
* A no-op implementation of [CredentialProviderCompletionManagerImpl] provided when the build
|
||||
@ -13,7 +13,7 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialR
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
object CredentialProviderCompletionManagerUnsupportedApiImpl : CredentialProviderCompletionManager {
|
||||
override fun completeFido2Registration(result: RegisterFido2CredentialResult) = Unit
|
||||
override fun completeCredentialRegistration(result: CreateCredentialResult) = Unit
|
||||
|
||||
override fun completeFido2Assertion(result: AssertFido2CredentialResult) = Unit
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
@ -6,21 +6,22 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
|
||||
/**
|
||||
* A reusable dialog for confirming whether or not the user wants to overwrite an existing FIDO 2
|
||||
* credential.
|
||||
* A reusable dialog for confirming whether or not the user wants to overwrite an existing credential.
|
||||
*
|
||||
* @param onConfirmClick A callback for when the overwrite confirmation button is clicked.
|
||||
* @param onDismissRequest A callback for when the dialog is requesting dismissal.
|
||||
*/
|
||||
@Suppress("MaxLineLength")
|
||||
@Composable
|
||||
fun BitwardenOverwritePasskeyConfirmationDialog(
|
||||
fun BitwardenOverwriteCredentialConfirmationDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
onConfirmClick: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
BitwardenTwoButtonDialog(
|
||||
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),
|
||||
title = title,
|
||||
message = message,
|
||||
confirmButtonText = stringResource(id = BitwardenString.okay),
|
||||
dismissButtonText = stringResource(id = BitwardenString.cancel),
|
||||
onConfirmClick = onConfirmClick,
|
||||
@ -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.itemlisting.navigateToVaultItemListingAsRoot
|
||||
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 java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
@ -141,6 +142,7 @@ fun RootNavScreen(
|
||||
is RootNavState.VaultUnlockedForFido2Assertion,
|
||||
is RootNavState.VaultUnlockedForPasswordGet,
|
||||
is RootNavState.VaultUnlockedForProviderGetCredentials,
|
||||
is RootNavState.VaultUnlockedForCreatePasswordRequest,
|
||||
-> VaultUnlockedGraphRoute
|
||||
|
||||
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 -> {
|
||||
navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||
navController.navigateToVaultItemListingAsRoot(
|
||||
|
||||
@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
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.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
@ -144,11 +145,31 @@ class RootNavViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is SpecialCircumstance.ProviderCreateCredential -> {
|
||||
RootNavState.VaultUnlockedForFido2Save(
|
||||
activeUserId = userState.activeUserId,
|
||||
createCredentialRequest =
|
||||
specialCircumstance.createCredentialRequest,
|
||||
)
|
||||
val request = specialCircumstance.createCredentialRequest
|
||||
val publicKeyRequest = request.createPublicKeyCredentialRequest
|
||||
val passwordRequest = request.createPasswordCredentialRequest
|
||||
|
||||
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 -> {
|
||||
@ -336,6 +357,21 @@ sealed class RootNavState : Parcelable {
|
||||
val createCredentialRequest: CreateCredentialRequest,
|
||||
) : 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.
|
||||
*/
|
||||
|
||||
@ -71,7 +71,7 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.bitwarden.ui.util.Text
|
||||
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.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.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
|
||||
@ -161,8 +161,8 @@ fun VaultAddEditScreen(
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddEditEvent.CompleteFido2Registration -> {
|
||||
credentialProviderCompletionManager.completeFido2Registration(
|
||||
is VaultAddEditEvent.CompleteCredentialRegistration -> {
|
||||
credentialProviderCompletionManager.completeCredentialRegistration(
|
||||
result = event.result,
|
||||
)
|
||||
}
|
||||
@ -227,10 +227,14 @@ fun VaultAddEditScreen(
|
||||
onAutofillDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultAddEditAction.Common.InitialAutofillDialogDismissed) }
|
||||
},
|
||||
onFido2ErrorDismiss = remember(viewModel) {
|
||||
onCredentialErrorDismiss = remember(viewModel) {
|
||||
{ errorMessage ->
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(message = errorMessage),
|
||||
VaultAddEditAction
|
||||
.Common
|
||||
.CredentialErrorDialogDismissed(
|
||||
message = errorMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -463,7 +467,7 @@ private fun VaultAddEditItemDialogs(
|
||||
dialogState: VaultAddEditState.DialogState?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onAutofillDismissRequest: () -> Unit,
|
||||
onFido2ErrorDismiss: (Text) -> Unit,
|
||||
onCredentialErrorDismiss: (Text) -> Unit,
|
||||
onConfirmOverwriteExistingPasskey: () -> Unit,
|
||||
onSubmitMasterPasswordFido2Verification: (password: String) -> Unit,
|
||||
onRetryFido2PasswordVerification: () -> Unit,
|
||||
@ -495,16 +499,22 @@ private fun VaultAddEditItemDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.DialogState.Fido2Error -> {
|
||||
is VaultAddEditState.DialogState.CredentialError -> {
|
||||
BitwardenBasicDialog(
|
||||
title = stringResource(id = BitwardenString.an_error_has_occurred),
|
||||
message = dialogState.message(),
|
||||
onDismissRequest = { onFido2ErrorDismiss(dialogState.message) },
|
||||
onDismissRequest = { onCredentialErrorDismiss(dialogState.message) },
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
@ -3,7 +3,6 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
import android.os.Parcelable
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.Fido2RegisterCredentialResult
|
||||
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.PolicyManager
|
||||
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.UpdateCipherResult
|
||||
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.model.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
@ -150,7 +148,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull()
|
||||
// Check for totp data to pre-populate
|
||||
val totpData = specialCircumstance?.toTotpDataOrNull()
|
||||
// Check for Fido2 data to pre-populate
|
||||
// Check for Fido2 or Password credential data to pre-populate
|
||||
val providerCreateCredentialRequest =
|
||||
specialCircumstance?.toCreateCredentialRequestOrNull()
|
||||
val fido2AttestationOptions = providerCreateCredentialRequest?.requestJson
|
||||
@ -348,8 +346,8 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
handleUserVerificationCancelled()
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Common.Fido2ErrorDialogDismissed -> {
|
||||
handleFido2ErrorDialogDismissed(action)
|
||||
is VaultAddEditAction.Common.CredentialErrorDialogDismissed -> {
|
||||
handleCredentialErrorDialogDismissed(action)
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.UserVerificationNotSupported -> {
|
||||
@ -401,31 +399,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleSaveClick() = onContent { content ->
|
||||
if (content.common.name.isBlank()) {
|
||||
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
|
||||
}
|
||||
if (hasValidationErrors(content)) return@onContent
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
@ -435,14 +409,17 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
state.createCredentialRequest
|
||||
?.let { request ->
|
||||
handleProviderCreateCredentialRequest(
|
||||
request.providerRequest,
|
||||
content.toCipherView(),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
state.createCredentialRequest?.run {
|
||||
createPublicKeyCredentialRequest
|
||||
?.let { createPublicKeyCredentialRequest ->
|
||||
handleCreatePublicKeyCredentialRequest(
|
||||
request = createPublicKeyCredentialRequest,
|
||||
callingAppInfo = this.callingAppInfo,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
return@onContent
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val vaultAddEditType = state.vaultAddEditType) {
|
||||
@ -467,15 +444,34 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProviderCreateCredentialRequest(
|
||||
request: ProviderCreateCredentialRequest,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
request
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { handleCreatePublicKeyCredentialRequest(request.callingAppInfo, it, cipherView) }
|
||||
?: run { handleUnsupportedProviderCreateCredentialRequest() }
|
||||
}
|
||||
private fun hasValidationErrors(content: VaultAddEditState.ViewState.Content): Boolean =
|
||||
if (content.common.name.isBlank()) {
|
||||
showGenericErrorDialog(
|
||||
message = BitwardenString.validation_field_required
|
||||
.asText(BitwardenString.name.asText()),
|
||||
)
|
||||
true
|
||||
} 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(
|
||||
callingAppInfo: CallingAppInfo,
|
||||
@ -520,7 +516,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
val userId = authRepository.activeUserId
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.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() {
|
||||
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
|
||||
}
|
||||
@ -613,63 +603,69 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
private fun handleConfirmOverwriteExistingPasskeyClick() {
|
||||
state
|
||||
.createCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
onContent { content ->
|
||||
handleProviderCreateCredentialRequest(
|
||||
request,
|
||||
content.toCipherView(),
|
||||
)
|
||||
}
|
||||
request.createPublicKeyCredentialRequest
|
||||
?.let { createPublicKeyCredentialRequest ->
|
||||
onContent { content ->
|
||||
handleCreatePublicKeyCredentialRequest(
|
||||
request = createPublicKeyCredentialRequest,
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog(
|
||||
?: showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_the_request_is_invalid.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationLockOut() {
|
||||
bitwardenCredentialManager.isUserVerified = false
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleUserVerificationSuccess() {
|
||||
bitwardenCredentialManager.isUserVerified = true
|
||||
getRequestAndRegisterCredential()
|
||||
getRequestAndRegisterFido2Credential()
|
||||
}
|
||||
|
||||
private fun getRequestAndRegisterCredential() =
|
||||
private fun getRequestAndRegisterFido2Credential() =
|
||||
state.createCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
onContent { content ->
|
||||
handleProviderCreateCredentialRequest(
|
||||
request = request,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
}
|
||||
request.createPublicKeyCredentialRequest
|
||||
?.let { createPublicKeyCredentialRequest ->
|
||||
onContent { content ->
|
||||
handleCreatePublicKeyCredentialRequest(
|
||||
request = createPublicKeyCredentialRequest,
|
||||
callingAppInfo = request.callingAppInfo,
|
||||
cipherView = content.toCipherView(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
?: showFido2ErrorDialog(
|
||||
?: showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
|
||||
.asText(),
|
||||
)
|
||||
|
||||
private fun handleUserVerificationFail() {
|
||||
bitwardenCredentialManager.isUserVerified = false
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleFido2ErrorDialogDismissed(
|
||||
action: VaultAddEditAction.Common.Fido2ErrorDialogDismissed,
|
||||
private fun handleCredentialErrorDialogDismissed(
|
||||
action: VaultAddEditAction.Common.CredentialErrorDialogDismissed,
|
||||
) {
|
||||
bitwardenCredentialManager.isUserVerified = false
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Error(action.message),
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Error(action.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -678,8 +674,8 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
bitwardenCredentialManager.isUserVerified = false
|
||||
clearDialogState()
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -692,7 +688,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
.value
|
||||
?.activeAccount
|
||||
?: run {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
@ -780,7 +776,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleDismissFido2VerificationDialogClick() {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
@ -1657,7 +1653,13 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
if (state.shouldClearSpecialCircumstance) {
|
||||
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)
|
||||
} else {
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
@ -1970,8 +1972,8 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
// Use toast here because we are closing the activity.
|
||||
toastManager.show(BitwardenString.an_error_has_occurred)
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Error(
|
||||
action.result.messageResourceId.asText(),
|
||||
),
|
||||
),
|
||||
@ -1982,8 +1984,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
// Use toast here because we are closing the activity.
|
||||
toastManager.show(BitwardenString.item_updated)
|
||||
sendEvent(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(action.result.responseJson),
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = action.result.responseJson,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -1997,7 +2001,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
when (action.result) {
|
||||
is ValidatePasswordResult.Error -> {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
@ -2022,7 +2026,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
|
||||
when (action.result) {
|
||||
is ValidatePinResult.Error -> {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
@ -2047,7 +2051,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
it.copy(dialog = errorDialogState)
|
||||
}
|
||||
} else {
|
||||
showFido2ErrorDialog(
|
||||
showCredentialErrorDialog(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
)
|
||||
@ -2058,7 +2062,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
bitwardenCredentialManager.isUserVerified = true
|
||||
bitwardenCredentialManager.authenticationAttempts = 0
|
||||
|
||||
getRequestAndRegisterCredential()
|
||||
getRequestAndRegisterFido2Credential()
|
||||
}
|
||||
|
||||
private fun handleAuthenticatorHelpToolTipClick() {
|
||||
@ -2072,10 +2076,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
|
||||
private fun showFido2ErrorDialog(message: Text) {
|
||||
private fun showCredentialErrorDialog(message: Text) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error(message),
|
||||
dialog = VaultAddEditState.DialogState.CredentialError(message),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -2754,10 +2758,10 @@ data class VaultAddEditState(
|
||||
data object InitialAutofillPrompt : DialogState()
|
||||
|
||||
/**
|
||||
* Displays a FIDO 2 operation error dialog to the user.
|
||||
* Displays a credential operation error dialog to the user.
|
||||
*/
|
||||
@Parcelize
|
||||
data class Fido2Error(val message: Text) : DialogState()
|
||||
data class CredentialError(val message: Text) : DialogState()
|
||||
|
||||
/**
|
||||
* Displays the overwrite passkey confirmation prompt to the user.
|
||||
@ -2888,12 +2892,12 @@ sealed class 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.
|
||||
*/
|
||||
data class CompleteFido2Registration(
|
||||
val result: RegisterFido2CredentialResult,
|
||||
data class CompleteCredentialRegistration(
|
||||
val result: CreateCredentialResult,
|
||||
) : BackgroundEvent, VaultAddEditEvent()
|
||||
|
||||
/**
|
||||
@ -3083,9 +3087,9 @@ sealed class VaultAddEditAction {
|
||||
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.
|
||||
|
||||
@ -10,7 +10,7 @@ import java.util.UUID
|
||||
|
||||
/**
|
||||
* 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(
|
||||
attestationOptions: PasskeyAttestationOptions?,
|
||||
@ -26,11 +26,17 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
|
||||
val rpName = attestationOptions
|
||||
?.relyingParty
|
||||
?.name
|
||||
.orEmpty()
|
||||
?: callingAppInfo.packageName
|
||||
|
||||
val username = attestationOptions
|
||||
?.user
|
||||
?.name
|
||||
?: createPasswordCredentialRequest
|
||||
?.id
|
||||
.orEmpty()
|
||||
|
||||
val password = createPasswordCredentialRequest
|
||||
?.password
|
||||
.orEmpty()
|
||||
|
||||
return VaultAddEditState.ViewState.Content(
|
||||
@ -40,6 +46,7 @@ fun CreateCredentialRequest.toDefaultAddTypeContent(
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(
|
||||
username = username,
|
||||
password = password,
|
||||
uriList = listOf(
|
||||
UriItem(
|
||||
id = UUID.randomUUID().toString(),
|
||||
|
||||
@ -50,7 +50,7 @@ import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
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.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.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
|
||||
@ -179,8 +179,8 @@ fun VaultItemListingScreen(
|
||||
onNavigateToVaultItemListing(VaultItemListingType.Collection(event.collectionId))
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.CompleteFido2Registration -> {
|
||||
credentialProviderCompletionManager.completeFido2Registration(event.result)
|
||||
is VaultItemListingEvent.CompleteCredentialRegistration -> {
|
||||
credentialProviderCompletionManager.completeCredentialRegistration(event.result)
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.CredentialManagerUserVerification -> {
|
||||
@ -392,7 +392,13 @@ private fun VaultItemListingDialogs(
|
||||
)
|
||||
|
||||
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) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
|
||||
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.GetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
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.VaultData
|
||||
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.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.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
|
||||
@ -168,9 +170,11 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
dialogState = providerCreateCredentialRequest?.let {
|
||||
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
|
||||
},
|
||||
dialogState = providerCreateCredentialRequest
|
||||
?.createPublicKeyCredentialRequest
|
||||
?.let {
|
||||
VaultItemListingState.DialogState.Loading(BitwardenString.loading.asText())
|
||||
},
|
||||
policyDisablesSend = policyManager
|
||||
.getActivePolicies(type = PolicyTypeJson.DISABLE_SEND)
|
||||
.any(),
|
||||
@ -426,8 +430,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
state.createCredentialRequest
|
||||
?.let {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Cancelled,
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -973,7 +977,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
createCredentialRequest
|
||||
.providerRequest
|
||||
.getCreatePasskeyCredentialRequestOrNull()
|
||||
?.let { createPasskeyCredentialRequest ->
|
||||
?.let {
|
||||
handleItemClickForCreatePublicKeyCredentialRequest(
|
||||
cipherId = action.id,
|
||||
cipherView = cipherView,
|
||||
@ -984,7 +988,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
VaultItemListingsAction.Internal.CredentialOperationFailureReceive(
|
||||
title = BitwardenString.an_error_has_occurred.asText(),
|
||||
message = BitwardenString
|
||||
.passkey_operation_failed_because_the_request_is_unsupported
|
||||
.credential_operation_failed_because_the_request_is_unsupported
|
||||
.asText(),
|
||||
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(
|
||||
cipherView: CipherView,
|
||||
providerRequest: ProviderCreateCredentialRequest,
|
||||
@ -1326,8 +1351,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
when {
|
||||
state.createCredentialRequest != null -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Error(action.message),
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Error(action.message),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -1530,7 +1555,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.CreateCredentialRequestReceive -> {
|
||||
handleRegisterFido2CredentialRequestReceive(action)
|
||||
handleRegisterCredentialRequestReceive(action)
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal.Fido2RegisterCredentialResultReceive -> {
|
||||
@ -1900,7 +1925,7 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
state.createCredentialRequest
|
||||
?.providerRequest
|
||||
?.let { request ->
|
||||
registerFido2CredentialToCipher(
|
||||
registerCredentialToCipher(
|
||||
cipherView = cipherView,
|
||||
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(
|
||||
action: VaultItemListingsAction.Internal.CreateCredentialRequestReceive,
|
||||
) {
|
||||
@ -2062,8 +2113,10 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
// user to have time to see the message.
|
||||
toastManager.show(messageId = BitwardenString.item_updated)
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(action.result.responseJson),
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = action.result.responseJson,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -2077,8 +2130,8 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
// user to have time to see the message.
|
||||
toastManager.show(messageId = BitwardenString.an_error_has_occurred)
|
||||
sendEvent(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Error(
|
||||
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(
|
||||
val result: RegisterFido2CredentialResult,
|
||||
data class CompleteCredentialRegistration(
|
||||
val result: CreateCredentialResult,
|
||||
) : BackgroundEvent, VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
|
||||
@ -11,7 +11,7 @@ fun createMockCreateCredentialRequest(
|
||||
isUserPreVerified: Boolean = false,
|
||||
requestData: Bundle = bundleOf(),
|
||||
): CreateCredentialRequest = CreateCredentialRequest(
|
||||
userId = "mockUserId-$number",
|
||||
userId = "mockUserIdx-$number",
|
||||
isUserPreVerified = isUserPreVerified,
|
||||
requestData = requestData,
|
||||
)
|
||||
|
||||
@ -125,7 +125,7 @@ class CredentialProviderProcessorTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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 {
|
||||
every { callingAppInfo } returns mockk(relaxed = true)
|
||||
every { candidateQueryData } returns Bundle()
|
||||
@ -148,6 +148,198 @@ class CredentialProviderProcessorTest {
|
||||
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")
|
||||
@Test
|
||||
fun `processCreateCredentialRequest should invoke callback with error when json is null or empty`() {
|
||||
|
||||
@ -48,7 +48,7 @@ fun createMockCipherView(
|
||||
organizationId: String? = "mockOrganizationId-$number",
|
||||
folderId: String? = "mockId-$number",
|
||||
notes: String? = "mockNotes-$number",
|
||||
password: String = "mockPassword-$number",
|
||||
password: String? = "mockPassword-$number",
|
||||
clock: Clock = FIXED_CLOCK,
|
||||
fido2Credentials: List<Fido2Credential>? = null,
|
||||
sshKey: SshKeyView? = createMockSshKeyView(number = number),
|
||||
|
||||
@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.credentials.CreatePasswordResponse
|
||||
import androidx.credentials.exceptions.GetCredentialCancellationException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
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.createMockPasswordCredentialAutofillCipherLogin
|
||||
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.GetPasswordCredentialResult
|
||||
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
|
||||
import io.mockk.Called
|
||||
import io.mockk.MockKVerificationScope
|
||||
import io.mockk.Ordering
|
||||
@ -60,9 +61,11 @@ class CredentialProviderCompletionManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `completeFido2Registration should perform no operations`() {
|
||||
val mockRegistrationResult = mockk<RegisterFido2CredentialResult>()
|
||||
credentialProviderCompletionManager.completeFido2Registration(mockRegistrationResult)
|
||||
fun `completeCredentialRegistration should perform no operations`() {
|
||||
val mockRegistrationResult = mockk<CreateCredentialResult>()
|
||||
credentialProviderCompletionManager.completeCredentialRegistration(
|
||||
mockRegistrationResult,
|
||||
)
|
||||
verify {
|
||||
mockRegistrationResult wasNot Called
|
||||
mockActivity wasNot Called
|
||||
@ -132,10 +135,10 @@ class CredentialProviderCompletionManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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
|
||||
.completeFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
.completeCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = "registrationResponse",
|
||||
),
|
||||
)
|
||||
@ -147,9 +150,24 @@ class CredentialProviderCompletionManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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
|
||||
.completeFido2Registration(RegisterFido2CredentialResult.Error("".asText()))
|
||||
.completeCredentialRegistration(CreateCredentialResult.Error("".asText()))
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
mockActivity.resources
|
||||
@ -159,9 +177,9 @@ class CredentialProviderCompletionManagerTest {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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
|
||||
.completeFido2Registration(RegisterFido2CredentialResult.Cancelled)
|
||||
.completeCredentialRegistration(CreateCredentialResult.Cancelled)
|
||||
|
||||
verifyActivityResultIsSetAndFinishedAfter {
|
||||
PendingIntentHandler.setCreateCredentialException(any(), any())
|
||||
|
||||
@ -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:
|
||||
rootNavStateFlow.value = RootNavState.VaultUnlockedForFido2Save(
|
||||
activeUserId = "activeUserId",
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
|
||||
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.cxf.model.ImportCredentialsRequestData
|
||||
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 io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@ -64,6 +69,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(::parseJwtTokenDataOrNull)
|
||||
unmockkObject(ProviderCreateCredentialRequest.Companion)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -682,11 +688,18 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
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(
|
||||
userId = "activeUserId",
|
||||
isUserPreVerified = false,
|
||||
requestData = bundleOf(),
|
||||
)
|
||||
|
||||
every { ProviderCreateCredentialRequest.fromBundle(any()) } returns mockk {
|
||||
every { callingRequest } returns mockk<CreatePublicKeyCredentialRequest>()
|
||||
}
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderCreateCredential(createCredentialRequest)
|
||||
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")
|
||||
@Test
|
||||
fun `when the active user has an unlocked vault but there is a Fido2Assertion special circumstance the nav state should be VaultUnlockedForFido2Assertion`() {
|
||||
|
||||
@ -57,7 +57,7 @@ import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
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.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.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
|
||||
@ -112,7 +112,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
|
||||
every { completeFido2Registration(any()) } just runs
|
||||
every { completeCredentialRegistration(any()) } just runs
|
||||
}
|
||||
private val biometricsManager: BiometricsManager = mockk {
|
||||
every { isUserVerificationSupported } returns true
|
||||
@ -233,18 +233,18 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on CompleteFido2Create event should invoke Fido2CompletionManager`() {
|
||||
val result = RegisterFido2CredentialResult.Success(
|
||||
fun `on CompleteCredentialCreate event should invoke CredentialProviderCompletionManager`() {
|
||||
val result = CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = "mockRegistrationResponse",
|
||||
)
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteFido2Registration(result = result))
|
||||
verify { credentialProviderCompletionManager.completeFido2Registration(result) }
|
||||
mutableEventFlow.tryEmit(VaultAddEditEvent.CompleteCredentialRegistration(result = result))
|
||||
verify { credentialProviderCompletionManager.completeCredentialRegistration(result) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fido2Error dialog should display based on state`() {
|
||||
fun `CredentialError dialog should display based on state`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
|
||||
dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
@ -464,7 +464,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
@Test
|
||||
fun `clicking dismiss dialog on Fido2Error dialog should send Fido2ErrorDialogDismissed action`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
dialog = VaultAddEditState.DialogState.Fido2Error("mockMessage".asText()),
|
||||
dialog = VaultAddEditState.DialogState.CredentialError("mockMessage".asText()),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
@ -474,7 +474,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() {
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.Fido2ErrorDialogDismissed("mockMessage".asText()),
|
||||
VaultAddEditAction.Common.CredentialErrorDialogDismissed("mockMessage".asText()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.credentials.CreatePasswordRequest
|
||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.CallingAppInfo
|
||||
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.UpdateCipherResult
|
||||
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.model.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
@ -1085,8 +1084,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(stateWithSavingDialog, stateFlow.awaitItem())
|
||||
assertEquals(stateWithName, stateFlow.awaitItem())
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
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")
|
||||
@Test
|
||||
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`() =
|
||||
runTest {
|
||||
val errorState = createVaultAddItemState(
|
||||
dialogState = VaultAddEditState.DialogState.Fido2Error(
|
||||
dialogState = VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
)
|
||||
@ -2288,15 +2238,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.Fido2ErrorDialogDismissed(
|
||||
VaultAddEditAction.Common.CredentialErrorDialogDismissed(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Error(
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Error(
|
||||
BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@ -4034,12 +3984,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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)
|
||||
|
||||
verify { bitwardenCredentialManager.isUserVerified = false }
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
@ -4048,7 +3998,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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 {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationCancelled)
|
||||
|
||||
@ -4056,8 +4006,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
assertNull(viewModel.stateFlow.value.dialog)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
@ -4066,12 +4016,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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)
|
||||
|
||||
verify { bitwardenCredentialManager.isUserVerified = false }
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
@ -4080,12 +4030,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationNotSupported should display Fido2ErrorDialog when active account not found`() {
|
||||
fun `UserVerificationNotSupported should display CredentialErrorDialog when active account not found`() {
|
||||
mutableUserStateFlow.value = null
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationNotSupported)
|
||||
verify { bitwardenCredentialManager.isUserVerified = false }
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified.asText(),
|
||||
),
|
||||
viewModel.stateFlow.value.dialog,
|
||||
@ -4181,7 +4131,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `MasterPasswordFido2VerificationSubmit should display Fido2Error when password verification fails`() {
|
||||
fun `MasterPasswordFido2VerificationSubmit should display CredentialError when password verification fails`() {
|
||||
val password = "password"
|
||||
coEvery {
|
||||
authRepository.validatePassword(password = password)
|
||||
@ -4194,7 +4144,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@ -4230,7 +4180,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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"
|
||||
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
|
||||
coEvery {
|
||||
@ -4244,7 +4194,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@ -4286,7 +4236,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PinFido2VerificationSubmit should display Fido2Error when Pin verification fails`() {
|
||||
fun `PinFido2VerificationSubmit should display CredentialError when Pin verification fails`() {
|
||||
val pin = "PIN"
|
||||
coEvery {
|
||||
authRepository.validatePin(pin = pin)
|
||||
@ -4299,7 +4249,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@ -4335,7 +4285,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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"
|
||||
every { bitwardenCredentialManager.hasAuthenticationAttemptsRemaining() } returns false
|
||||
coEvery {
|
||||
@ -4349,7 +4299,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
),
|
||||
@ -4430,13 +4380,13 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissFido2VerificationDialogClick should display Fido2ErrorDialog`() {
|
||||
fun `DismissFido2VerificationDialogClick should display CredentialErrorDialog`() {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString
|
||||
.passkey_operation_failed_because_user_could_not_be_verified
|
||||
.asText(),
|
||||
@ -4447,7 +4397,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `UserVerificationSuccess should display Fido2ErrorDialog when request is invalid`() {
|
||||
fun `UserVerificationSuccess should display CredentialErrorDialog when request is invalid`() {
|
||||
every { authRepository.activeUserId } returns null
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderCreateCredential(
|
||||
@ -4459,7 +4409,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.UserVerificationSuccess)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.DialogState.Fido2Error(
|
||||
VaultAddEditState.DialogState.CredentialError(
|
||||
message = BitwardenString.passkey_operation_failed_because_the_request_is_unsupported
|
||||
.asText(),
|
||||
),
|
||||
@ -4499,7 +4449,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockCreateCredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Error.InternalError
|
||||
@ -4527,8 +4477,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Error(
|
||||
BitwardenString.passkey_registration_failed_due_to_an_internal_error
|
||||
.asText(),
|
||||
),
|
||||
@ -4543,7 +4493,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
|
||||
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
|
||||
runTest {
|
||||
val mockRequest = createMockCreateCredentialRequest(number = 1)
|
||||
val mockResult = Fido2RegisterCredentialResult.Success(
|
||||
@ -4572,8 +4522,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultAddEditEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
VaultAddEditEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = "mockResponse",
|
||||
),
|
||||
),
|
||||
|
||||
@ -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.ui.credentials.manager.CredentialProviderCompletionManager
|
||||
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.GetPasswordCredentialResult
|
||||
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
|
||||
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
@ -106,7 +106,7 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
private val credentialProviderCompletionManager: CredentialProviderCompletionManager = mockk {
|
||||
every { completeFido2Registration(any()) } just runs
|
||||
every { completeCredentialRegistration(any()) } just runs
|
||||
every { completeFido2Assertion(any()) } just runs
|
||||
every { completePasswordGet(any()) } just runs
|
||||
every { completeProviderGetCredentialsRequest(any()) } just runs
|
||||
@ -1966,11 +1966,11 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `CompleteFido2Registration event should call CredentialProviderCompletionManager with result`() {
|
||||
val result = RegisterFido2CredentialResult.Success("mockResponse")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteFido2Registration(result))
|
||||
fun `CompleteCredentialRegistration event should call CredentialProviderCompletionManager with result`() {
|
||||
val result = CreateCredentialResult.Success.Fido2CredentialRegistered("mockResponse")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.CompleteCredentialRegistration(result))
|
||||
verify {
|
||||
credentialProviderCompletionManager.completeFido2Registration(result)
|
||||
credentialProviderCompletionManager.completeCredentialRegistration(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.VaultData
|
||||
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.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.model.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
|
||||
@ -261,10 +261,18 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
private val mockGetPublicKeyCredentialOption = mockk<GetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
}
|
||||
private val mockCreatePublicKeyCredentialOption = mockk<CreatePublicKeyCredentialRequest> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
every { origin } returns "mockOrigin"
|
||||
}
|
||||
private val mockProviderGetCredentialRequest = mockk<ProviderGetCredentialRequest> {
|
||||
every { credentialOptions } returns listOf(mockGetPublicKeyCredentialOption)
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
private val mockProviderCreateCredentialRequest = mockk<ProviderCreateCredentialRequest> {
|
||||
every { callingRequest } returns mockCreatePublicKeyCredentialOption
|
||||
every { callingAppInfo } returns mockCallingAppInfo
|
||||
}
|
||||
private val mockBeginGetPublicKeyCredentialOption = mockk<BeginGetPublicKeyCredentialOption> {
|
||||
every { requestJson } returns "mockRequestJson"
|
||||
}
|
||||
@ -272,14 +280,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
every { beginGetCredentialOptions } returns listOf(mockBeginGetPublicKeyCredentialOption)
|
||||
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> =
|
||||
bufferedMutableSharedFlow()
|
||||
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay> = mockk {
|
||||
@ -303,12 +304,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
ProviderGetCredentialRequest.Companion,
|
||||
BeginGetCredentialRequest.Companion,
|
||||
)
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
|
||||
every {
|
||||
ProviderGetCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderGetCredentialRequest
|
||||
every {
|
||||
ProviderCreateCredentialRequest.fromBundle(any())
|
||||
} returns mockProviderCreateCredentialRequest
|
||||
every {
|
||||
BeginGetCredentialRequest.fromBundle(any())
|
||||
} returns mockBeginGetCredentialRequest
|
||||
@ -348,6 +350,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
coEvery {
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Success(null)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
@ -511,6 +515,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
shouldFinishWhenComplete = false,
|
||||
)
|
||||
val searchType = SearchType.Vault.All
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)
|
||||
@ -677,7 +682,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderCreateCredential(
|
||||
@ -755,6 +760,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemClick(
|
||||
@ -810,6 +816,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemClick(
|
||||
@ -881,6 +888,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemClick(
|
||||
@ -934,6 +942,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns Fido2RegisterCredentialResult.Success("mockResponse")
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemClick(
|
||||
@ -2313,6 +2322,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
@ -2990,6 +3000,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
|
||||
@ -3065,6 +3076,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Success("mockOrigin")
|
||||
|
||||
setupFido2CreateRequest()
|
||||
createVaultItemListingViewModel()
|
||||
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
@ -3075,9 +3087,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `ValidateOriginResult should update dialog state on Unknown error`() = runTest {
|
||||
val mockCredentialsRequest = createMockCreateCredentialRequest(number = 1)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderCreateCredential(
|
||||
createMockCreateCredentialRequest(number = 1),
|
||||
mockCredentialsRequest,
|
||||
)
|
||||
coEvery {
|
||||
originManager.validateOrigin(
|
||||
@ -3086,6 +3099,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Error.Unknown
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertEquals(
|
||||
@ -3111,6 +3125,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertEquals(
|
||||
@ -3139,6 +3154,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppSignatureNotFound
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertEquals(
|
||||
@ -3165,6 +3181,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Error.PasskeyNotSupportedForApp
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertEquals(
|
||||
@ -3191,6 +3208,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns ValidateOriginResult.Error.AssetLinkNotFound
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
assertEquals(
|
||||
@ -3204,7 +3222,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteFido2Registration result`() =
|
||||
fun `Fido2RegisterCredentialResult Error should show toast and emit CompleteCredentialRegistration result`() =
|
||||
runTest {
|
||||
val mockResult = Fido2RegisterCredentialResult.Error.InternalError
|
||||
|
||||
@ -3217,8 +3235,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Error(
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Error(
|
||||
BitwardenString.passkey_registration_failed_due_to_an_internal_error.asText(),
|
||||
),
|
||||
),
|
||||
@ -3232,7 +3250,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteFido2Registration result`() =
|
||||
fun `Fido2RegisterCredentialResult Success should show toast and emit CompleteCredentialRegistration result`() =
|
||||
runTest {
|
||||
val mockResult = Fido2RegisterCredentialResult.Success(
|
||||
responseJson = "mockResponse",
|
||||
@ -3247,8 +3265,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
RegisterFido2CredentialResult.Success(
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
CreateCredentialResult.Success.Fido2CredentialRegistered(
|
||||
responseJson = "mockResponse",
|
||||
),
|
||||
),
|
||||
@ -3268,6 +3286,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
SpecialCircumstance.ProviderCreateCredential(
|
||||
createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.DismissCredentialManagerErrorDialogClick(
|
||||
@ -3277,8 +3296,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
viewModel.eventFlow.test {
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Error(
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Error(
|
||||
"".asText(),
|
||||
),
|
||||
),
|
||||
@ -4130,12 +4149,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@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 {
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.ProviderCreateCredential(
|
||||
createMockCreateCredentialRequest(number = 1),
|
||||
)
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(VaultItemListingsAction.UserVerificationCancelled)
|
||||
|
||||
@ -4143,8 +4163,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
assertNull(viewModel.stateFlow.value.dialogState)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
VaultItemListingEvent.CompleteFido2Registration(
|
||||
result = RegisterFido2CredentialResult.Cancelled,
|
||||
VaultItemListingEvent.CompleteCredentialRegistration(
|
||||
result = CreateCredentialResult.Cancelled,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
@ -4277,6 +4297,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
@ -4314,6 +4335,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
responseJson = "mockResponse",
|
||||
)
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.UserVerificationSuccess(
|
||||
@ -5013,6 +5035,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
} returns UserVerificationRequirement.REQUIRED
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ConfirmOverwriteExistingPasskeyClick(
|
||||
@ -5339,6 +5362,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
|
||||
runTest {
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.Internal.InternetConnectionErrorReceived,
|
||||
@ -5372,6 +5396,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
originManager.validateOrigin(any(), any())
|
||||
} returns ValidateOriginResult.Error.PrivilegedAppNotAllowed
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
@ -5403,6 +5428,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
mockCallingAppInfo.getSignatureFingerprintAsHexString()
|
||||
} returns null
|
||||
|
||||
setupFido2CreateRequest()
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
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(
|
||||
vaultItemListingType: VaultItemListingType,
|
||||
): SavedStateHandle = SavedStateHandle().apply {
|
||||
|
||||
@ -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="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_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="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>
|
||||
@ -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="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="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_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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user