mirror of
https://github.com/bitwarden/android.git
synced 2026-03-09 03:33:36 -05:00
PM-33266: Allow the VaultUnlockViewModel and VaultViewModel to safely initialize without a UserState
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.ui.platform.base.util.toHexColorRepresentation
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
|
||||
@@ -43,7 +44,7 @@ data class UserState(
|
||||
* @property isPremium `true` if the account has a premium membership.
|
||||
* @property isLoggedIn `true` if the account is logged in, or `false` if it requires additional
|
||||
* authentication to view their vault.
|
||||
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
||||
* @property isVaultUnlocked Whether the user's vault is currently unlocked.
|
||||
* @property needsPasswordReset If the user needs to reset their password.
|
||||
* @property needsMasterPassword Indicates whether the user needs to create a password (e.g.
|
||||
* they logged in using SSO and don't yet have one). NOTE: This should **not** be used to
|
||||
@@ -96,4 +97,32 @@ data class UserState(
|
||||
val hasLoginApprovingDevice: Boolean,
|
||||
val hasResetPasswordPermission: Boolean,
|
||||
)
|
||||
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* A basic empty account model.
|
||||
*/
|
||||
val EMPTY_ACCOUNT: Account = Account(
|
||||
userId = "",
|
||||
name = null,
|
||||
email = "",
|
||||
avatarColorHex = "".toHexColorRepresentation(),
|
||||
environment = Environment.Us,
|
||||
isPremium = false,
|
||||
isLoggedIn = false,
|
||||
isVaultUnlocked = false,
|
||||
needsPasswordReset = false,
|
||||
organizations = emptyList(),
|
||||
isBiometricsEnabled = false,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
needsMasterPassword = false,
|
||||
hasMasterPassword = true,
|
||||
trustedDevice = null,
|
||||
isUsingKeyConnector = false,
|
||||
onboardingStatus = OnboardingStatus.COMPLETE,
|
||||
firstTimeState = FirstTimeState(),
|
||||
isExportable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.emptyInputDialogMessage
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -62,10 +63,16 @@ class VaultUnlockViewModel @Inject constructor(
|
||||
) : BaseViewModel<VaultUnlockState, VaultUnlockEvent, VaultUnlockAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
val activeAccount = userState.activeAccount
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
val userState = authRepository.userStateFlow.value
|
||||
val activeAccount = userState?.activeAccount ?: run {
|
||||
// We use this empty account to avoid a crash that can occur during a race condition.
|
||||
// The state-based navigation brought us here but the UserState has now been set to
|
||||
// null, we just need to wait here a short while longer and state-based navigation will
|
||||
// get us out of here.
|
||||
UserState.EMPTY_ACCOUNT
|
||||
}
|
||||
val accountSummaries = userState?.toAccountSummaries().orEmpty()
|
||||
val activeAccountSummary = activeAccount.toAccountSummary(isActive = true)
|
||||
val vaultUnlockType = activeAccount.vaultUnlockType
|
||||
val hasNoMasterPassword = !activeAccount.hasMasterPassword
|
||||
if (!activeAccount.hasManualUnlockMechanism) {
|
||||
@@ -90,11 +97,11 @@ class VaultUnlockViewModel @Inject constructor(
|
||||
environmentUrl = activeAccount.environment.label,
|
||||
input = "",
|
||||
isBiometricEnabled = activeAccount.isBiometricsEnabled,
|
||||
isBiometricsValid = authRepository.isBiometricIntegrityValid(userState.activeUserId),
|
||||
isBiometricsValid = authRepository.isBiometricIntegrityValid(activeAccount.userId),
|
||||
showAccountMenu = showAccountMenu,
|
||||
showBiometricInvalidatedMessage = false,
|
||||
vaultUnlockType = vaultUnlockType,
|
||||
userId = userState.activeUserId,
|
||||
userId = activeAccount.userId,
|
||||
getCredentialsRequest = specialCircumstance?.toGetCredentialsRequestOrNull(),
|
||||
fido2CredentialAssertionRequest = specialCircumstance?.toFido2AssertionRequestOrNull(),
|
||||
createCredentialRequest = specialCircumstance?.toCreateCredentialRequestOrNull(),
|
||||
|
||||
@@ -59,6 +59,7 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflo
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
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.toAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAppBarTitle
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toSnackbarData
|
||||
@@ -115,10 +116,17 @@ class VaultViewModel @Inject constructor(
|
||||
snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
val accountSummaries = userState.toAccountSummaries()
|
||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||
val vaultFilterData = userState.activeAccount.toVaultFilterData(
|
||||
val userState = authRepository.userStateFlow.value
|
||||
val accountSummaries = userState?.toAccountSummaries().orEmpty()
|
||||
val activeAccount = userState?.activeAccount ?: run {
|
||||
// We use this empty account to avoid a crash that can occur during a race condition.
|
||||
// The state-based navigation brought us here but the UserState has now been set to
|
||||
// null, we just need to wait here a short while longer and state-based navigation will
|
||||
// get us out of here.
|
||||
UserState.EMPTY_ACCOUNT
|
||||
}
|
||||
val activeAccountSummary = activeAccount.toAccountSummary(isActive = true)
|
||||
val vaultFilterData = activeAccount.toVaultFilterData(
|
||||
isIndividualVaultDisabled = policyManager
|
||||
.getActivePolicies(type = PolicyTypeJson.PERSONAL_OWNERSHIP)
|
||||
.any(),
|
||||
@@ -131,11 +139,11 @@ class VaultViewModel @Inject constructor(
|
||||
vaultFilterData = vaultFilterData,
|
||||
viewState = VaultState.ViewState.Loading,
|
||||
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
|
||||
isPremium = userState.activeAccount.isPremium,
|
||||
isPremium = activeAccount.isPremium,
|
||||
isArchiveEnabled = featureFlagManager.getFeatureFlag(FlagKey.ArchiveItems),
|
||||
isPullToRefreshSettingEnabled = settingsRepository.getPullToRefreshEnabledFlow().value,
|
||||
baseIconUrl = userState.activeAccount.environment.environmentUrlData.baseIconUrl,
|
||||
hasMasterPassword = userState.activeAccount.hasMasterPassword,
|
||||
baseIconUrl = activeAccount.environment.environmentUrlData.baseIconUrl,
|
||||
hasMasterPassword = activeAccount.hasMasterPassword,
|
||||
isRefreshing = false,
|
||||
showImportActionCard = false,
|
||||
flightRecorderSnackBar = settingsRepository
|
||||
|
||||
@@ -59,8 +59,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
every { logout(reason = any()) } just runs
|
||||
every { logout(userId = any(), reason = any()) } just runs
|
||||
every { switchAccount(any()) } returns SwitchAccountResult.AccountSwitched
|
||||
every { getOrCreateCipher(USER_ID) } returns CIPHER
|
||||
every { isBiometricIntegrityValid(userId = DEFAULT_USER_STATE.activeUserId) } returns true
|
||||
every { getOrCreateCipher(userId = any()) } returns CIPHER
|
||||
every { isBiometricIntegrityValid(userId = any()) } returns true
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true) {
|
||||
every { lockVault(any(), any()) } just runs
|
||||
@@ -113,6 +113,22 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||
verify { authRepository.getOrCreateCipher(USER_ID) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when UserState is not present`() {
|
||||
mutableUserStateFlow.update { null }
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
accountSummaries = emptyList(),
|
||||
avatarColorString = "#ff000000",
|
||||
initials = "",
|
||||
email = "",
|
||||
userId = "",
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when set`() {
|
||||
val state = DEFAULT_STATE.copy(
|
||||
|
||||
@@ -228,6 +228,22 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
unmockkStatic(FlightRecorderDataSet::toSnackbarData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when UserState is not present`() {
|
||||
mutableUserStateFlow.update { null }
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
accountSummaries = emptyList(),
|
||||
avatarColorString = "#ff000000",
|
||||
initials = "",
|
||||
showImportActionCard = false,
|
||||
isPremium = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct and should trigger a syncIfNecessary call`() {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
Reference in New Issue
Block a user