mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
This PR adds the TOTP matching flow to the app (#4042)
This commit is contained in:
parent
641a48fe44
commit
e7450171cd
14 changed files with 449 additions and 67 deletions
|
@ -89,6 +89,15 @@
|
|||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="otpauth" />
|
||||
<data android:host="totp" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
|||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.takeUntilLoaded
|
||||
|
@ -53,6 +54,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
|
|||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.validateCipherOrReturnErrorState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toCipherView
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
||||
|
@ -108,33 +110,31 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
.any()
|
||||
|
||||
val specialCircumstance = specialCircumstanceManager.specialCircumstance
|
||||
// Check for autofill data to pre-populate
|
||||
val autofillSaveItem = specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toAutofillSelectionDataOrNull()
|
||||
val autofillSaveItem = specialCircumstance?.toAutofillSaveItemOrNull()
|
||||
val autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull()
|
||||
// Check for totp data to pre-populate
|
||||
val totpData = specialCircumstance?.toTotpDataOrNull()
|
||||
// Check for Fido2 data to pre-populate
|
||||
val fido2CreationRequest = specialCircumstance?.toFido2RequestOrNull()
|
||||
val fido2AttestationOptions = fido2CreationRequest?.let { request ->
|
||||
fido2CredentialManager.getPasskeyAttestationOptionsOrNull(request.requestJson)
|
||||
}
|
||||
|
||||
val fido2CreationRequest = specialCircumstanceManager
|
||||
.specialCircumstance
|
||||
?.toFido2RequestOrNull()
|
||||
// Exit on save if handling an autofill, Fido2 Attestation, or TOTP link
|
||||
val shouldExitOnSave = autofillSaveItem != null ||
|
||||
fido2AttestationOptions != null ||
|
||||
totpData != null
|
||||
|
||||
val fido2AttestationOptions = fido2CreationRequest
|
||||
?.let { request ->
|
||||
fido2CredentialManager
|
||||
.getPasskeyAttestationOptionsOrNull(request.requestJson)
|
||||
}
|
||||
|
||||
val dialogState =
|
||||
if (!settingsRepository.initialAutofillDialogShown &&
|
||||
vaultAddEditType is VaultAddEditType.AddItem &&
|
||||
autofillSelectionData == null
|
||||
) {
|
||||
VaultAddEditState.DialogState.InitialAutofillPrompt
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val dialogState = if (!settingsRepository.initialAutofillDialogShown &&
|
||||
vaultAddEditType is VaultAddEditType.AddItem &&
|
||||
autofillSelectionData == null
|
||||
) {
|
||||
VaultAddEditState.DialogState.InitialAutofillPrompt
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
VaultAddEditState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
|
@ -142,13 +142,12 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is VaultAddEditType.AddItem -> {
|
||||
autofillSelectionData
|
||||
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
|
||||
?: autofillSaveItem
|
||||
?.toDefaultAddTypeContent(isIndividualVaultDisabled)
|
||||
?: fido2CreationRequest
|
||||
?.toDefaultAddTypeContent(
|
||||
attestationOptions = fido2AttestationOptions,
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
)
|
||||
?: autofillSaveItem?.toDefaultAddTypeContent(isIndividualVaultDisabled)
|
||||
?: fido2CreationRequest?.toDefaultAddTypeContent(
|
||||
attestationOptions = fido2AttestationOptions,
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
)
|
||||
?: totpData?.toDefaultAddTypeContent(isIndividualVaultDisabled)
|
||||
?: VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(),
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
|
@ -160,9 +159,10 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is VaultAddEditType.CloneItem -> VaultAddEditState.ViewState.Loading
|
||||
},
|
||||
dialog = dialogState,
|
||||
totpData = totpData,
|
||||
// Set special conditions for autofill and fido2 save
|
||||
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
|
||||
shouldExitOnSave = autofillSaveItem != null || fido2AttestationOptions != null,
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -1412,18 +1412,14 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is CreateCipherResult.Success -> {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
if (state.shouldExitOnSave) {
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ExitApp,
|
||||
)
|
||||
sendEvent(event = VaultAddEditEvent.ExitApp)
|
||||
} else {
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(R.string.new_item_created.asText()),
|
||||
)
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.NavigateBack,
|
||||
)
|
||||
sendEvent(event = VaultAddEditEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1444,10 +1440,13 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is UpdateCipherResult.Success -> {
|
||||
sendEvent(
|
||||
event = VaultAddEditEvent.ShowToast(R.string.item_updated.asText()),
|
||||
)
|
||||
sendEvent(VaultAddEditEvent.NavigateBack)
|
||||
specialCircumstanceManager.specialCircumstance = null
|
||||
if (state.shouldExitOnSave) {
|
||||
sendEvent(event = VaultAddEditEvent.ExitApp)
|
||||
} else {
|
||||
sendEvent(event = VaultAddEditEvent.ShowToast(R.string.item_updated.asText()))
|
||||
sendEvent(event = VaultAddEditEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1544,15 +1543,19 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
) { currentAccount, cipherView ->
|
||||
// Derive the view state from the current Cipher for Edit mode
|
||||
// or use the current state for Add
|
||||
(cipherView?.toViewState(
|
||||
isClone = isCloneMode,
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
resourceManager = resourceManager,
|
||||
clock = clock,
|
||||
) ?: viewState)
|
||||
(cipherView
|
||||
?.toViewState(
|
||||
isClone = isCloneMode,
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
totpData = totpData,
|
||||
resourceManager = resourceManager,
|
||||
clock = clock,
|
||||
)
|
||||
?: viewState)
|
||||
.appendFolderAndOwnerData(
|
||||
folderViewList = vaultData.folderViewList,
|
||||
collectionViewList = vaultData.collectionViewList
|
||||
collectionViewList = vaultData
|
||||
.collectionViewList
|
||||
.filter { !it.readOnly },
|
||||
activeAccount = currentAccount,
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
|
@ -1911,6 +1914,7 @@ data class VaultAddEditState(
|
|||
val shouldShowCloseButton: Boolean = true,
|
||||
// Internal
|
||||
val shouldExitOnSave: Boolean = false,
|
||||
val totpData: TotpData? = null,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@ import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
|
|||
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
||||
|
@ -37,6 +38,7 @@ private const val PASSKEY_CREATION_TIME_PATTERN: String = "hh:mm a"
|
|||
fun CipherView.toViewState(
|
||||
isClone: Boolean,
|
||||
isIndividualVaultDisabled: Boolean,
|
||||
totpData: TotpData?,
|
||||
resourceManager: ResourceManager,
|
||||
clock: Clock,
|
||||
): VaultAddEditState.ViewState =
|
||||
|
@ -46,7 +48,7 @@ fun CipherView.toViewState(
|
|||
VaultAddEditState.ViewState.Content.ItemType.Login(
|
||||
username = login?.username.orEmpty(),
|
||||
password = login?.password.orEmpty(),
|
||||
totp = login?.totp,
|
||||
totp = totpData?.uri ?: login?.totp,
|
||||
canViewPassword = this.viewPassword,
|
||||
canEditItem = this.edit,
|
||||
uriList = login?.uris.toUriItems(),
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
|
||||
/**
|
||||
* Returns pre-filled content that may be used for an "add" type
|
||||
* [VaultAddEditState.ViewState.Content] during a TOTP creation event.
|
||||
*/
|
||||
fun TotpData.toDefaultAddTypeContent(
|
||||
isIndividualVaultDisabled: Boolean,
|
||||
): VaultAddEditState.ViewState.Content = VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
name = (this.issuer ?: this.accountName).orEmpty(),
|
||||
),
|
||||
isIndividualVaultDisabled = isIndividualVaultDisabled,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(totp = this.uri),
|
||||
)
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog
|
||||
|
@ -27,6 +28,7 @@ import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData
|
|||
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenPolicyWarningText
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
|
@ -38,6 +40,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
fun VaultItemListingContent(
|
||||
state: VaultItemListingState.ViewState.Content,
|
||||
policyDisablesSend: Boolean,
|
||||
showAddTotpBanner: Boolean,
|
||||
collectionClick: (id: String) -> Unit,
|
||||
folderClick: (id: String) -> Unit,
|
||||
vaultItemClick: (id: String) -> Unit,
|
||||
|
@ -100,8 +103,23 @@ fun VaultItemListingContent(
|
|||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
item {
|
||||
if (showAddTotpBanner) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenPolicyWarningText(
|
||||
text = stringResource(id = R.string.add_this_authenticator_key_to_a_login),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Start,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (policyDisablesSend) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenPolicyWarningText(
|
||||
text = stringResource(id = R.string.send_disabled_warning),
|
||||
modifier = Modifier
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
@ -45,11 +46,13 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
|||
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalFido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInputDialog
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingUserVerificationHandlers
|
||||
|
@ -74,6 +77,7 @@ fun VaultItemListingScreen(
|
|||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
onNavigateToSearch: (searchType: SearchType) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
fido2CompletionManager: Fido2CompletionManager = LocalFido2CompletionManager.current,
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
viewModel: VaultItemListingViewModel = hiltViewModel(),
|
||||
|
@ -168,6 +172,8 @@ fun VaultItemListingScreen(
|
|||
is VaultItemListingEvent.CompleteFido2GetCredentialsRequest -> {
|
||||
fido2CompletionManager.completeFido2GetCredentialRequest(event.result)
|
||||
}
|
||||
|
||||
VaultItemListingEvent.ExitApp -> exitManager.exitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,12 +258,15 @@ fun VaultItemListingScreen(
|
|||
},
|
||||
)
|
||||
|
||||
val vaultItemListingHandlers = remember(viewModel) {
|
||||
VaultItemListingHandlers.create(viewModel)
|
||||
}
|
||||
|
||||
BackHandler(onBack = vaultItemListingHandlers.backClick)
|
||||
VaultItemListingScaffold(
|
||||
state = state,
|
||||
pullToRefreshState = pullToRefreshState,
|
||||
vaultItemListingHandlers = remember(viewModel) {
|
||||
VaultItemListingHandlers.create(viewModel)
|
||||
},
|
||||
vaultItemListingHandlers = vaultItemListingHandlers,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -451,6 +460,7 @@ private fun VaultItemListingScaffold(
|
|||
is VaultItemListingState.ViewState.Content -> {
|
||||
VaultItemListingContent(
|
||||
state = state.viewState,
|
||||
showAddTotpBanner = state.isTotp,
|
||||
policyDisablesSend = state.policyDisablesSend &&
|
||||
state.itemListingType is VaultItemListingState.ItemListingType.Send,
|
||||
vaultItemClick = vaultItemListingHandlers.itemClick,
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrN
|
|||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2AssertionRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2GetCredentialsRequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toTotpDataOrNull
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.DataState
|
||||
|
@ -57,7 +58,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
|||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
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
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
|
||||
|
@ -69,6 +72,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
|||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
@ -127,6 +131,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
.any(),
|
||||
autofillSelectionData = specialCircumstance?.toAutofillSelectionDataOrNull(),
|
||||
hasMasterPassword = userState.activeAccount.hasMasterPassword,
|
||||
totpData = specialCircumstance?.toTotpDataOrNull(),
|
||||
fido2CredentialRequest = fido2CredentialRequest,
|
||||
fido2CredentialAssertionRequest = specialCircumstance?.toFido2AssertionRequestOrNull(),
|
||||
fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull(),
|
||||
|
@ -184,7 +189,8 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
it
|
||||
.filterForAutofillIfNecessary()
|
||||
.filterForFido2CreationIfNecessary()
|
||||
.filterForFidoGetCredentialsIfNecessary(),
|
||||
.filterForFidoGetCredentialsIfNecessary()
|
||||
.filterForTotpIfNecessary(),
|
||||
)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
|
@ -560,6 +566,10 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
return
|
||||
}
|
||||
state.totpData?.let {
|
||||
sendEvent(VaultItemListingEvent.NavigateToEditCipher(cipherId = action.id))
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isFido2Creation) {
|
||||
handleFido2RegistrationRequestReceive(action)
|
||||
|
@ -832,7 +842,11 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
|
||||
private fun handleBackClick() {
|
||||
sendEvent(
|
||||
event = VaultItemListingEvent.NavigateBack,
|
||||
event = if (state.isTotp) {
|
||||
VaultItemListingEvent.ExitApp
|
||||
} else {
|
||||
VaultItemListingEvent.NavigateBack
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1639,6 +1653,22 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the given vault data and filters it for totp data.
|
||||
*/
|
||||
private fun DataState<VaultData>.filterForTotpIfNecessary(): DataState<VaultData> {
|
||||
val totpData = state.totpData ?: return this
|
||||
val query = totpData.issuer ?: totpData.accountName ?: return this
|
||||
return this.map { vaultData ->
|
||||
vaultData.copy(
|
||||
cipherViewList = vaultData.cipherViewList.filterAndOrganize(
|
||||
searchTypeData = SearchTypeData.Vault.Logins,
|
||||
searchTerm = query,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and filter the fido 2 autofill credentials.
|
||||
*/
|
||||
|
@ -1687,6 +1717,7 @@ data class VaultItemListingState(
|
|||
val policyDisablesSend: Boolean,
|
||||
// Internal
|
||||
private val isPullToRefreshSettingEnabled: Boolean,
|
||||
val totpData: TotpData? = null,
|
||||
val autofillSelectionData: AutofillSelectionData? = null,
|
||||
val fido2CredentialRequest: Fido2CredentialRequest? = null,
|
||||
val fido2CredentialAssertionRequest: Fido2CredentialAssertionRequest? = null,
|
||||
|
@ -1707,6 +1738,11 @@ data class VaultItemListingState(
|
|||
val isFido2Creation: Boolean
|
||||
get() = fido2CredentialRequest != null
|
||||
|
||||
/**
|
||||
* Whether or not this represents a listing screen for totp.
|
||||
*/
|
||||
val isTotp: Boolean get() = totpData != null
|
||||
|
||||
/**
|
||||
* A displayable title for the AppBar.
|
||||
*/
|
||||
|
@ -1719,6 +1755,7 @@ data class VaultItemListingState(
|
|||
?.callingAppInfo
|
||||
?.getFido2RpIdOrNull()
|
||||
?.let { R.string.items_for_uri.asText(it) }
|
||||
?: totpData?.let { R.string.items_for_uri.asText(it.issuer ?: it.accountName ?: "--") }
|
||||
?: itemListingType.titleText
|
||||
|
||||
/**
|
||||
|
@ -1730,17 +1767,17 @@ data class VaultItemListingState(
|
|||
/**
|
||||
* Whether or not the account switcher should be shown.
|
||||
*/
|
||||
val shouldShowAccountSwitcher: Boolean get() = isAutofill || isFido2Creation
|
||||
val shouldShowAccountSwitcher: Boolean get() = isAutofill || isFido2Creation || isTotp
|
||||
|
||||
/**
|
||||
* Whether or not the navigation icon should be shown.
|
||||
*/
|
||||
val shouldShowNavigationIcon: Boolean get() = !isAutofill && !isFido2Creation
|
||||
val shouldShowNavigationIcon: Boolean get() = !isAutofill && !isFido2Creation && !isTotp
|
||||
|
||||
/**
|
||||
* Whether or not the overflow menu should be shown.
|
||||
*/
|
||||
val shouldShowOverflowMenu: Boolean get() = !isAutofill && !isFido2Creation
|
||||
val shouldShowOverflowMenu: Boolean get() = !isAutofill && !isFido2Creation && !isTotp
|
||||
|
||||
/**
|
||||
* Represents the current state of any dialogs on the screen.
|
||||
|
@ -2081,6 +2118,11 @@ data class VaultItemListingState(
|
|||
* Models events for the [VaultItemListingScreen].
|
||||
*/
|
||||
sealed class VaultItemListingEvent {
|
||||
/**
|
||||
* Closes the app.
|
||||
*/
|
||||
data object ExitApp : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the Create Account screen.
|
||||
*/
|
||||
|
|
|
@ -605,6 +605,7 @@ Scanning will happen automatically.</string>
|
|||
<string name="thirty_days">30 days</string>
|
||||
<string name="custom">Custom</string>
|
||||
<string name="share_on_save">Share this Send upon save</string>
|
||||
<string name="add_this_authenticator_key_to_a_login">Add this authenticator key to an existing login, or create a new login.</string>
|
||||
<string name="send_disabled_warning">Due to an enterprise policy, you are only able to delete an existing Send.</string>
|
||||
<string name="about_send">About Send</string>
|
||||
<string name="hide_email">Hide my email address from recipients</string>
|
||||
|
|
|
@ -62,6 +62,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
|||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardExpirationMonth
|
||||
|
@ -699,6 +700,60 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during totp fill, SaveClick should show dialog, remove it once an item is saved, and emit ExitApp`() =
|
||||
runTest {
|
||||
val totpData = TotpData(
|
||||
uri = "otpauth://totp/issuer:accountName?secret=secret",
|
||||
issuer = "issuer",
|
||||
accountName = "accountName",
|
||||
secret = "secret",
|
||||
digits = 6,
|
||||
period = 30,
|
||||
algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData)
|
||||
val stateWithDialog = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
dialogState = VaultAddEditState.DialogState.Loading(R.string.saving.asText()),
|
||||
commonContentViewState = createCommonContentViewState(name = "issuer"),
|
||||
totpData = totpData,
|
||||
shouldExitOnSave = true,
|
||||
)
|
||||
val stateWithName = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState = createCommonContentViewState(name = "issuer"),
|
||||
totpData = totpData,
|
||||
shouldExitOnSave = true,
|
||||
)
|
||||
mutableVaultDataFlow.value = DataState.Loaded(createVaultData())
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = stateWithName,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
} returns CreateCipherResult.Success
|
||||
|
||||
viewModel.stateEventFlow(backgroundScope) { stateTurbine, eventTurbine ->
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
|
||||
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithDialog, stateTurbine.awaitItem())
|
||||
assertEquals(stateWithName, stateTurbine.awaitItem())
|
||||
|
||||
assertEquals(VaultAddEditEvent.ExitApp, eventTurbine.awaitItem())
|
||||
}
|
||||
assertNull(specialCircumstanceManager.specialCircumstance)
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.createCipherInOrganization(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode during fido2 registration, SaveClick should show saving dialog, and request user verification when required`() =
|
||||
|
@ -1204,6 +1259,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1234,6 +1290,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1267,6 +1324,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1328,6 +1386,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1392,6 +1451,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1447,6 +1507,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -1507,6 +1568,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = fixedClock,
|
||||
)
|
||||
|
@ -3737,13 +3799,15 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
|
||||
//region Helper functions
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Suppress("MaxLineLength", "LongParameterList")
|
||||
private fun createVaultAddItemState(
|
||||
vaultAddEditType: VaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
commonContentViewState: VaultAddEditState.ViewState.Content.Common = createCommonContentViewState(),
|
||||
isIndividualVaultDisabled: Boolean = false,
|
||||
shouldExitOnSave: Boolean = false,
|
||||
typeContentViewState: VaultAddEditState.ViewState.Content.ItemType = createLoginTypeContentViewState(),
|
||||
dialogState: VaultAddEditState.DialogState? = null,
|
||||
totpData: TotpData? = null,
|
||||
): VaultAddEditState =
|
||||
VaultAddEditState(
|
||||
vaultAddEditType = vaultAddEditType,
|
||||
|
@ -3753,6 +3817,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
type = typeContentViewState,
|
||||
),
|
||||
dialog = dialogState,
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
totpData = totpData,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
@ -70,6 +70,7 @@ class CipherViewExtensionsTest {
|
|||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
@ -115,6 +116,7 @@ class CipherViewExtensionsTest {
|
|||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = true,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
@ -165,6 +167,7 @@ class CipherViewExtensionsTest {
|
|||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
@ -214,6 +217,66 @@ class CipherViewExtensionsTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Login ViewState with a predefined totp`() {
|
||||
val totp = "otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP"
|
||||
val cipherView = DEFAULT_LOGIN_CIPHER_VIEW.copy(
|
||||
login = DEFAULT_LOGIN_CIPHER_VIEW.login?.copy(totp = null),
|
||||
)
|
||||
|
||||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = mockk { every { uri } returns totp },
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(
|
||||
originalCipher = cipherView,
|
||||
name = "cipher",
|
||||
favorite = false,
|
||||
masterPasswordReprompt = true,
|
||||
notes = "Lots of notes",
|
||||
availableFolders = emptyList(),
|
||||
availableOwners = emptyList(),
|
||||
customFieldData = listOf(
|
||||
VaultAddEditState.Custom.BooleanField(TEST_ID, "TestBoolean", false),
|
||||
VaultAddEditState.Custom.TextField(TEST_ID, "TestText", "TestText"),
|
||||
VaultAddEditState.Custom.HiddenField(TEST_ID, "TestHidden", "TestHidden"),
|
||||
VaultAddEditState.Custom.LinkedField(
|
||||
TEST_ID,
|
||||
"TestLinked",
|
||||
VaultLinkedFieldType.USERNAME,
|
||||
),
|
||||
),
|
||||
),
|
||||
isIndividualVaultDisabled = false,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(
|
||||
username = "username",
|
||||
password = "password",
|
||||
uriList = listOf(
|
||||
UriItem(
|
||||
id = TEST_ID,
|
||||
uri = "www.example.com",
|
||||
match = null,
|
||||
checksum = null,
|
||||
),
|
||||
),
|
||||
totp = totp,
|
||||
canViewPassword = false,
|
||||
fido2CredentialCreationDateTime = R.string.created_xy.asText(
|
||||
"10/27/23",
|
||||
"12:00 PM",
|
||||
),
|
||||
),
|
||||
),
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should create a Secure Notes ViewState`() {
|
||||
val cipherView = DEFAULT_SECURE_NOTES_CIPHER_VIEW
|
||||
|
@ -221,6 +284,7 @@ class CipherViewExtensionsTest {
|
|||
val result = cipherView.toViewState(
|
||||
isClone = false,
|
||||
isIndividualVaultDisabled = true,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
@ -255,6 +319,7 @@ class CipherViewExtensionsTest {
|
|||
val result = cipherView.toViewState(
|
||||
isClone = true,
|
||||
isIndividualVaultDisabled = false,
|
||||
totpData = null,
|
||||
resourceManager = resourceManager,
|
||||
clock = FIXED_CLOCK,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditState
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.UUID
|
||||
|
||||
class TotpDataExtensionsTest {
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(UUID::class)
|
||||
every { UUID.randomUUID().toString() } returns "uuid"
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(UUID::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toDefaultAddTypeContent should return the correct content with totp data`() {
|
||||
val uri = "otpauth://totp/issuer:accountName?secret=secret"
|
||||
val totpData = TotpData(
|
||||
uri = uri,
|
||||
issuer = "issuer",
|
||||
accountName = "accountName",
|
||||
secret = "secret",
|
||||
digits = 6,
|
||||
period = 30,
|
||||
algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
|
||||
)
|
||||
assertEquals(
|
||||
VaultAddEditState.ViewState.Content(
|
||||
common = VaultAddEditState.ViewState.Content.Common(name = "issuer"),
|
||||
isIndividualVaultDisabled = false,
|
||||
type = VaultAddEditState.ViewState.Content.ItemType.Login(totp = uri),
|
||||
),
|
||||
totpData.toDefaultAddTypeContent(isIndividualVaultDisabled = false),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
|||
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
|
||||
|
@ -84,6 +85,9 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
private var onNavigateToSearchType: SearchType? = null
|
||||
private var onNavigateToVaultItemListingScreenType: VaultItemListingType? = null
|
||||
|
||||
private val exitManager: ExitManager = mockk {
|
||||
every { exitApplication() } just runs
|
||||
}
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { shareText(any()) } just runs
|
||||
every { launchUri(any()) } just runs
|
||||
|
@ -105,9 +109,10 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
fun setUp() {
|
||||
mockkStatic(String::toHostOrPathOrNull)
|
||||
every { AUTOFILL_SELECTION_DATA.uri?.toHostOrPathOrNull() } returns "www.test.com"
|
||||
composeTestRule.setContent {
|
||||
setContentWithBackDispatcher {
|
||||
VaultItemListingScreen(
|
||||
viewModel = viewModel,
|
||||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
fido2CompletionManager = fido2CompletionManager,
|
||||
biometricsManager = biometricsManager,
|
||||
|
@ -339,6 +344,23 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ExitApp event should invoke exitApplication`() {
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.ExitApp)
|
||||
verify(exactly = 1) {
|
||||
exitManager.exitApplication()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `back gesture should send BackClick action`() {
|
||||
backDispatcher?.onBackPressed()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemListingsAction.BackClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking back button should send BackClick action`() {
|
||||
composeTestRule
|
||||
|
|
|
@ -70,6 +70,7 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockDisplayIt
|
|||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.model.TotpData
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.Ordering
|
||||
|
@ -259,6 +260,26 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
verify { authRepository.switchAccount(userId = updatedUserId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClick with TotpData should emit ExitApp`() = runTest {
|
||||
val totpData = TotpData(
|
||||
uri = "otpauth://totp/issuer:accountName?secret=secret",
|
||||
issuer = "issuer",
|
||||
accountName = "accountName",
|
||||
secret = "secret",
|
||||
digits = 6,
|
||||
period = 30,
|
||||
algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
|
||||
)
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemListingsAction.BackClick)
|
||||
assertEquals(VaultItemListingEvent.ExitApp, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -1400,6 +1421,61 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items and totp data filtering should update ViewState to Content with filtered data`() =
|
||||
runTest {
|
||||
setupMockUri()
|
||||
|
||||
val uri = "otpauth://totp/issuer:accountName?secret=secret"
|
||||
val totpData = TotpData(
|
||||
uri = uri,
|
||||
issuer = null,
|
||||
accountName = "Name-1",
|
||||
secret = "secret",
|
||||
digits = 6,
|
||||
period = 30,
|
||||
algorithm = TotpData.CryptoHashAlgorithm.SHA_1,
|
||||
)
|
||||
val cipherView1 = createMockCipherView(number = 1)
|
||||
val cipherView2 = createMockCipherView(number = 2)
|
||||
|
||||
// Filtering comes later, so we return everything here
|
||||
mockFilteredCiphers = listOf(cipherView1, cipherView2)
|
||||
|
||||
specialCircumstanceManager.specialCircumstance =
|
||||
SpecialCircumstance.AddTotpLoginItem(data = totpData)
|
||||
val dataState = DataState.Loaded(
|
||||
data = VaultData(
|
||||
cipherViewList = listOf(cipherView1, cipherView2),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
),
|
||||
)
|
||||
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
mutableVaultDataStateFlow.value = dataState
|
||||
|
||||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayCollectionList = emptyList(),
|
||||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(
|
||||
number = 1,
|
||||
secondSubtitleTestTag = "PasskeySite",
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
.copy(totpData = totpData),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `vaultDataStateFlow Loaded with items and fido2 filtering should update ViewState to Content with filtered data`() =
|
||||
|
@ -3887,7 +3963,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
|
||||
private fun createVaultItemListingViewModel(
|
||||
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
|
||||
vaultRepository: VaultRepository = this.vaultRepository,
|
||||
): VaultItemListingViewModel =
|
||||
VaultItemListingViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
|
@ -3922,6 +3997,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
isPullToRefreshSettingEnabled = false,
|
||||
dialogState = null,
|
||||
totpData = null,
|
||||
autofillSelectionData = null,
|
||||
policyDisablesSend = false,
|
||||
hasMasterPassword = true,
|
||||
|
|
|
@ -16,6 +16,7 @@ fun createMockDisplayItemForCipher(
|
|||
number: Int,
|
||||
cipherType: CipherType = CipherType.LOGIN,
|
||||
subtitle: String? = "mockUsername-$number",
|
||||
secondSubtitleTestTag: String? = null,
|
||||
requiresPasswordReprompt: Boolean = true,
|
||||
): VaultItemListingState.DisplayItem =
|
||||
when (cipherType) {
|
||||
|
@ -25,11 +26,11 @@ fun createMockDisplayItemForCipher(
|
|||
title = "mockName-$number",
|
||||
titleTestTag = "CipherNameLabel",
|
||||
secondSubtitle = null,
|
||||
secondSubtitleTestTag = null,
|
||||
secondSubtitleTestTag = secondSubtitleTestTag,
|
||||
subtitle = subtitle,
|
||||
subtitleTestTag = "CipherSubTitleLabel",
|
||||
iconData = IconData.Network(
|
||||
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
|
||||
uri = "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
|
||||
fallbackIconRes = R.drawable.ic_globe,
|
||||
),
|
||||
extraIconList = listOf(
|
||||
|
@ -79,7 +80,7 @@ fun createMockDisplayItemForCipher(
|
|||
title = "mockName-$number",
|
||||
titleTestTag = "CipherNameLabel",
|
||||
secondSubtitle = null,
|
||||
secondSubtitleTestTag = null,
|
||||
secondSubtitleTestTag = secondSubtitleTestTag,
|
||||
subtitle = subtitle,
|
||||
subtitleTestTag = "CipherSubTitleLabel",
|
||||
iconData = IconData.Local(R.drawable.ic_note),
|
||||
|
@ -119,7 +120,7 @@ fun createMockDisplayItemForCipher(
|
|||
title = "mockName-$number",
|
||||
titleTestTag = "CipherNameLabel",
|
||||
secondSubtitle = null,
|
||||
secondSubtitleTestTag = null,
|
||||
secondSubtitleTestTag = secondSubtitleTestTag,
|
||||
subtitle = subtitle,
|
||||
subtitleTestTag = "CipherSubTitleLabel",
|
||||
iconData = IconData.Local(R.drawable.ic_payment_card),
|
||||
|
@ -165,7 +166,7 @@ fun createMockDisplayItemForCipher(
|
|||
title = "mockName-$number",
|
||||
titleTestTag = "CipherNameLabel",
|
||||
secondSubtitle = null,
|
||||
secondSubtitleTestTag = null,
|
||||
secondSubtitleTestTag = secondSubtitleTestTag,
|
||||
subtitle = subtitle,
|
||||
subtitleTestTag = "CipherSubTitleLabel",
|
||||
iconData = IconData.Local(R.drawable.ic_id_card),
|
||||
|
|
Loading…
Reference in a new issue