PM-11174 Action card for import logins flow (#4057)

This commit is contained in:
Dave Severns 2024-10-11 10:49:34 -04:00 committed by GitHub
parent 028242c4be
commit ba8e3a6c51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 856 additions and 11 deletions

View file

@ -306,4 +306,19 @@ interface AuthDiskSource {
* if any exists.
*/
fun getOnboardingStatusFlow(userId: String): Flow<OnboardingStatus?>
/**
* Gets the show import logins flag for the given [userId].
*/
fun getShowImportLogins(userId: String): Boolean?
/**
* Stores the show import logins flag for the given [userId].
*/
fun storeShowImportLogins(userId: String, showImportLogins: Boolean?)
/**
* Emits updates that track [getShowImportLogins]. This will replay the last known value,
*/
fun getShowImportLoginsFlow(userId: String): Flow<Boolean?>
}

View file

@ -45,6 +45,7 @@ private const val SHOULD_TRUST_DEVICE_KEY = "shouldTrustDevice"
private const val TDE_LOGIN_COMPLETE = "tdeLoginComplete"
private const val USES_KEY_CONNECTOR = "usesKeyConnector"
private const val ONBOARDING_STATUS_KEY = "onboardingStatus"
private const val SHOW_IMPORT_LOGINS_KEY = "showImportLogins"
/**
* Primary implementation of [AuthDiskSource].
@ -72,6 +73,7 @@ class AuthDiskSourceImpl(
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
override var userState: UserStateJson?
@ -143,9 +145,11 @@ class AuthDiskSourceImpl(
storeShouldUseKeyConnector(userId = userId, shouldUseKeyConnector = null)
storeIsTdeLoginComplete(userId = userId, isTdeLoginComplete = null)
storeAuthenticatorSyncUnlockKey(userId = userId, authenticatorSyncUnlockKey = null)
storeShowImportLogins(userId = userId, showImportLogins = null)
// Do not remove the DeviceKey or PendingAuthRequest on logout, these are persisted
// indefinitely unless the TDE flow explicitly removes them.
// Do not remove OnboardingStatus we want to keep track of this even after logout.
}
override fun getAuthenticatorSyncUnlockKey(userId: String): String? =
@ -437,6 +441,22 @@ class AuthDiskSourceImpl(
.onSubscription { emit(getOnboardingStatus(userId = userId)) }
}
override fun getShowImportLogins(userId: String): Boolean? {
return getBoolean(SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId))
}
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
putBoolean(
key = SHOW_IMPORT_LOGINS_KEY.appendIdentifier(userId),
value = showImportLogins,
)
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
}
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
private fun generateAndStoreUniqueAppId(): String =
UUID
.randomUUID()
@ -480,6 +500,12 @@ class AuthDiskSourceImpl(
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun migrateAccountTokens() {
userState
?.accounts

View file

@ -393,4 +393,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager {
* Update the value of the onboarding status for the user.
*/
fun setOnboardingStatus(userId: String, status: OnboardingStatus?)
/**
* Update the value of the showImportLogins status for the user.
*/
fun setShowImportLogins(showImportLogins: Boolean)
}

View file

@ -76,6 +76,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
import com.x8bit.bitwarden.data.auth.repository.util.WebAuthResult
import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.currentOnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.util.currentOrDefaultUserFirstTimeState
import com.x8bit.bitwarden.data.auth.repository.util.firstTimeStateFlow
import com.x8bit.bitwarden.data.auth.repository.util.onboardingStatusChangesFlow
import com.x8bit.bitwarden.data.auth.repository.util.policyInformation
import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson
@ -254,6 +256,7 @@ class AuthRepositoryImpl(
authDiskSource.userOrganizationsListFlow,
authDiskSource.userKeyConnectorStateFlow,
authDiskSource.onboardingStatusChangesFlow,
authDiskSource.firstTimeStateFlow,
vaultRepository.vaultUnlockDataStateFlow,
mutableHasPendingAccountAdditionStateFlow,
// Ignore the data in the merge, but trigger an update when they emit.
@ -267,8 +270,9 @@ class AuthRepositoryImpl(
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val onboardingStatus = array[4] as OnboardingStatus?
val vaultState = array[5] as List<VaultUnlockData>
val hasPendingAccountAddition = array[6] as Boolean
val firstTimeState = array[5] as UserState.FirstTimeState
val vaultState = array[6] as List<VaultUnlockData>
val hasPendingAccountAddition = array[7] as Boolean
userStateJson?.toUserState(
vaultState = vaultState,
userAccountTokens = userAccountTokens,
@ -279,6 +283,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = firstTimeState,
)
}
.filterNot { mutableHasPendingAccountDeletionStateFlow.value }
@ -298,6 +303,7 @@ class AuthRepositoryImpl(
isBiometricsEnabledProvider = ::isBiometricsEnabled,
vaultUnlockTypeProvider = ::getVaultUnlockType,
isDeviceTrustedProvider = ::isDeviceTrusted,
firstTimeState = authDiskSource.currentOrDefaultUserFirstTimeState,
),
)
@ -1297,6 +1303,11 @@ class AuthRepositoryImpl(
authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status)
}
override fun setShowImportLogins(showImportLogins: Boolean) {
val userId: String = activeUserId ?: return
authDiskSource.storeShowImportLogins(userId = userId, showImportLogins = showImportLogins)
}
@Suppress("CyclomaticComplexMethod")
private suspend fun validatePasswordAgainstPolicy(
password: String,

View file

@ -29,6 +29,9 @@ data class UserState(
val activeAccount: Account
get() = accounts.first { it.userId == activeUserId }
val activeUserFirstTimeState: FirstTimeState
get() = activeAccount.firstTimeState
/**
* Basic account information about a given user.
*
@ -71,6 +74,7 @@ data class UserState(
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
val isUsingKeyConnector: Boolean,
val onboardingStatus: OnboardingStatus,
val firstTimeState: FirstTimeState,
) {
/**
* Indicates that the user does or does not have a means to manually unlock the vault.
@ -91,4 +95,21 @@ data class UserState(
val hasLoginApprovingDevice: Boolean,
val hasResetPasswordPermission: Boolean,
)
/**
* Model to encapsulate different states for a user's first time experience.
*/
data class FirstTimeState(
val showImportLoginsCard: Boolean,
) {
/**
* Constructs a [FirstTimeState] accepting nullable values. If a value is null, the default
* is used.
*/
constructor(
showImportLoginsCoachMarker: Boolean?,
) : this(
showImportLoginsCard = showImportLoginsCoachMarker ?: true,
)
}
}

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.UserSwitchingData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@ -183,8 +184,50 @@ val AuthDiskSource.onboardingStatusChangesFlow: Flow<OnboardingStatus?>
}
.distinctUntilChanged()
/**
* Returns the current [OnboardingStatus] of the active user.
*/
val AuthDiskSource.currentOnboardingStatus: OnboardingStatus?
get() = this
.userState
?.activeUserId
?.let { this.getOnboardingStatus(userId = it) }
/**
* Returns a [Flow] that emits every time the active user's first time state is changed.
*/
@OptIn(ExperimentalCoroutinesApi::class)
val AuthDiskSource.firstTimeStateFlow: Flow<UserState.FirstTimeState>
get() = activeUserIdChangesFlow
.flatMapLatest { activeUserId ->
combine(
listOf(
activeUserId
?.let {
getShowImportLoginsFlow(it)
}
?: flowOf(null),
),
) {
UserState.FirstTimeState(
showImportLoginsCoachMarker = it[0],
)
}
}
.distinctUntilChanged()
/**
* Get the current [UserState.FirstTimeState] of the active user if available, otherwise return
* a default configuration.
*/
val AuthDiskSource.currentOrDefaultUserFirstTimeState
get() = userState
?.activeUserId
?.let {
UserState.FirstTimeState(
showImportLoginsCoachMarker = getShowImportLogins(it),
)
}
?: UserState.FirstTimeState(
showImportLoginsCoachMarker = true,
)

View file

@ -111,6 +111,7 @@ fun UserStateJson.toUserState(
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
hasPendingAccountAddition: Boolean,
onboardingStatus: OnboardingStatus?,
firstTimeState: UserState.FirstTimeState,
isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
@ -180,6 +181,7 @@ fun UserStateJson.toUserState(
// If the user exists with no onboarding status we can assume they have been
// using the app prior to the release of the onboarding flow.
onboardingStatus = onboardingStatus ?: OnboardingStatus.COMPLETE,
firstTimeState = firstTimeState,
)
},
hasPendingAccountAddition = hasPendingAccountAddition,

View file

@ -167,6 +167,8 @@ class SettingsDiskSourceImpl(
// The following are intentionally not cleared so they can be
// restored after logging out and back in:
// - screen capture allowed
// - show autofill setting badge
// - show unlock setting badge
}
override fun getAccountBiometricIntegrityValidity(

View file

@ -30,6 +30,7 @@ sealed class FlagKey<out T : Any> {
EmailVerification,
OnboardingFlow,
OnboardingCarousel,
ImportLoginsFlow,
)
}
}
@ -70,6 +71,15 @@ sealed class FlagKey<out T : Any> {
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the feature flag key for the import logins feature.
*/
data object ImportLoginsFlow : FlagKey<Boolean>() {
override val keyName: String = "import-logins-flow"
override val defaultValue: Boolean = false
override val isRemotelyConfigured: Boolean = false
}
/**
* Data object holding the key for a [Boolean] flag to be used in tests.
*/

View file

@ -47,6 +47,7 @@ fun BitwardenActionCard(
onActionClick: () -> Unit,
onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
cardSubtitle: String? = null,
leadingContent: @Composable (() -> Unit)? = null,
) {
Card(
@ -70,7 +71,6 @@ fun BitwardenActionCard(
Text(
text = cardTitle,
style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
)
Spacer(Modifier.weight(1f))
BitwardenStandardIconButton(
@ -80,6 +80,13 @@ fun BitwardenActionCard(
modifier = Modifier.offset(x = 8.dp),
)
}
cardSubtitle?.let {
Spacer(Modifier.height(4.dp))
Text(
text = it,
style = BitwardenTheme.typography.bodyMedium,
)
}
Spacer(Modifier.height(16.dp))
BitwardenFilledButton(
label = actionText,
@ -128,3 +135,23 @@ private fun BitwardenActionCardWithLeadingContent_preview() {
)
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BitwardenActionCardWithSubtitle_preview() {
BitwardenTheme {
BitwardenActionCard(
cardTitle = "Title",
cardSubtitle = "Subtitle",
actionText = "Action",
onActionClick = {},
onDismissClick = {},
leadingContent = {
NotificationBadge(
notificationCount = 1,
)
},
)
}
}

View file

@ -26,6 +26,7 @@ fun <T : Any> FlagKey<T>.ListItemContent(
FlagKey.EmailVerification,
FlagKey.OnboardingCarousel,
FlagKey.OnboardingFlow,
FlagKey.ImportLoginsFlow,
-> BooleanFlagItem(
label = flagKey.getDisplayLabel(),
key = flagKey as FlagKey<Boolean>,
@ -67,4 +68,5 @@ private fun <T : Any> FlagKey<T>.getDisplayLabel(): String = when (this) {
FlagKey.EmailVerification -> stringResource(R.string.email_verification)
FlagKey.OnboardingCarousel -> stringResource(R.string.onboarding_carousel)
FlagKey.OnboardingFlow -> stringResource(R.string.onboarding_flow)
FlagKey.ImportLoginsFlow -> stringResource(R.string.import_logins_flow)
}

View file

@ -33,12 +33,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.account.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenSearchActionItem
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
@ -304,11 +307,32 @@ private fun VaultScreenScaffold(
modifier = innerModifier,
)
is VaultState.ViewState.NoItems -> VaultNoItems(
modifier = innerModifier,
policyDisablesSend = false,
addItemClickAction = vaultHandlers.addItemClickAction,
)
is VaultState.ViewState.NoItems -> {
AnimatedVisibility(
visible = state.showImportActionCard,
exit = actionCardExitAnimation(),
label = "VaultNoItemsActionCard",
) {
BitwardenActionCard(
cardTitle = stringResource(R.string.import_saved_logins),
cardSubtitle = stringResource(
R.string.use_a_computer_to_import_logins,
),
actionText = stringResource(R.string.get_started),
onActionClick = vaultHandlers.importActionCardClick,
onDismissClick = vaultHandlers.dismissImportActionCard,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.padding(top = 12.dp),
)
}
VaultNoItems(
modifier = innerModifier,
policyDisablesSend = false,
addItemClickAction = vaultHandlers.addItemClickAction,
)
}
is VaultState.ViewState.Error -> BitwardenErrorContent(
message = viewState.message(),

View file

@ -9,9 +9,11 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -45,6 +47,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import com.x8bit.bitwarden.ui.vault.util.shortName
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -67,6 +70,7 @@ class VaultViewModel @Inject constructor(
private val policyManager: PolicyManager,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@ -92,6 +96,7 @@ class VaultViewModel @Inject constructor(
hasMasterPassword = userState.activeAccount.hasMasterPassword,
hideNotificationsDialog = isBuildVersionBelow(Build.VERSION_CODES.TIRAMISU) || isFdroid,
isRefreshing = false,
showImportActionCard = false,
)
},
) {
@ -126,9 +131,15 @@ class VaultViewModel @Inject constructor(
authRepository
.userStateFlow
.onEach {
sendAction(VaultAction.Internal.UserStateUpdateReceive(userState = it))
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.ImportLoginsFlow),
) { userState, importLoginsEnabled ->
VaultAction.Internal.UserStateUpdateReceive(
userState = userState,
importLoginsFlowEnabled = importLoginsEnabled,
)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
}
@ -163,9 +174,20 @@ class VaultViewModel @Inject constructor(
}
is VaultAction.Internal -> handleInternalAction(action)
VaultAction.DismissImportActionCard -> handleDismissImportActionCard()
VaultAction.ImportActionCardClick -> handleImportActionCardClick()
}
}
private fun handleImportActionCardClick() {
dismissImportLoginCard()
// TODO: PM-11179 - navigate to import logins screen
}
private fun handleDismissImportActionCard() {
dismissImportLoginCard()
}
private fun handleIconLoadingSettingReceive(
action: VaultAction.Internal.IconLoadingSettingReceive,
) {
@ -459,6 +481,7 @@ class VaultViewModel @Inject constructor(
// Leave the current data alone if there is no UserState; we are in the process of logging
// out.
val userState = action.userState ?: return
val firstTimeState = userState.activeUserFirstTimeState
// Avoid updating the UI if we are actively switching users to avoid changes while
// navigating.
@ -470,6 +493,8 @@ class VaultViewModel @Inject constructor(
.any(),
)
val appBarTitle = vaultFilterData.toAppBarTitle()
val shouldShowImportActionCard = action.importLoginsFlowEnabled &&
firstTimeState.showImportLoginsCard
mutableStateFlow.update {
val accountSummaries = userState.toAccountSummaries()
val activeAccountSummary = userState.toActiveAccountSummary()
@ -480,6 +505,7 @@ class VaultViewModel @Inject constructor(
accountSummaries = accountSummaries,
vaultFilterData = vaultFilterData,
isPremium = userState.activeAccount.isPremium,
showImportActionCard = shouldShowImportActionCard,
)
}
}
@ -605,6 +631,11 @@ class VaultViewModel @Inject constructor(
}
//endregion VaultAction Handlers
private fun dismissImportLoginCard() {
if (!state.showImportActionCard) return
authRepository.setShowImportLogins(false)
}
}
/**
@ -636,6 +667,7 @@ data class VaultState(
val isIconLoadingDisabled: Boolean,
val hideNotificationsDialog: Boolean,
val isRefreshing: Boolean,
val showImportActionCard: Boolean,
) : Parcelable {
/**
@ -1110,6 +1142,16 @@ sealed class VaultAction {
*/
data object TryAgainClick : VaultAction()
/**
* The user has dismissed the import action card.
*/
data object DismissImportActionCard : VaultAction()
/**
* The user has clicked the import action card.
*/
data object ImportActionCardClick : VaultAction()
/**
* User clicked an overflow action.
*/
@ -1155,6 +1197,7 @@ sealed class VaultAction {
*/
data class UserStateUpdateReceive(
val userState: UserState?,
val importLoginsFlowEnabled: Boolean,
) : Internal()
/**

View file

@ -34,6 +34,8 @@ data class VaultHandlers(
val dialogDismiss: () -> Unit,
val overflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit,
val masterPasswordRepromptSubmit: (ListingItemOverflowAction.VaultAction, String) -> Unit,
val dismissImportActionCard: () -> Unit,
val importActionCardClick: () -> Unit,
) {
@Suppress("UndocumentedPublicClass")
companion object {
@ -89,6 +91,12 @@ data class VaultHandlers(
),
)
},
dismissImportActionCard = {
viewModel.trySendAction(VaultAction.DismissImportActionCard)
},
importActionCardClick = {
viewModel.trySendAction(VaultAction.ImportActionCardClick)
},
)
}
}

View file

@ -1018,4 +1018,6 @@ Do you want to switch to this account?</string>
<string name="new_login">New login</string>
<string name="share_files_and_data_securely_with_anyone_on_any_platform">Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure.</string>
<string name="send_sensitive_information_safely">Send sensitive information, safely</string>
<string name="import_saved_logins">Import saved logins</string>
<string name="use_a_computer_to_import_logins">Use a computer to import logins from an existing password manager</string>
</resources>

View file

@ -11,6 +11,7 @@
<string name="email_verification">Email Verification</string>
<string name="onboarding_carousel">Onboarding Carousel</string>
<string name="onboarding_flow">Onboarding Flow</string>
<string name="import_logins_flow">Import Logins Flow</string>
<string name="feature_flags">Feature Flags:</string>
<string name="debug_menu">Debug Menu</string>
<string name="reset_values">Reset values</string>

View file

@ -1079,6 +1079,10 @@ private val DEFAULT_STATE: MainState = MainState(
isScreenCaptureAllowed = true,
)
private val DEFAULT_FIRST_TIME_STATE = UserState.FirstTimeState(
showImportLoginsCard = true,
)
private const val SPECIAL_CIRCUMSTANCE_KEY: String = "special-circumstance"
private val DEFAULT_ACCOUNT = UserState.Account(
userId = "activeUserId",
@ -1097,6 +1101,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -302,12 +302,21 @@ class AuthDiskSourceTest {
authenticatorSyncUnlockKey = "authenticatorSyncUnlockKey",
)
authDiskSource.storeOnboardingStatus(
userId = userId,
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
)
authDiskSource.clearData(userId = userId)
// We do not clear these even when you call clear storage
assertEquals(pendingAuthRequestJson, authDiskSource.getPendingAuthRequest(userId = userId))
assertEquals(deviceKey, authDiskSource.getDeviceKey(userId = userId))
assertEquals(shouldTrustDevice, authDiskSource.getShouldTrustDevice(userId = userId))
assertEquals(
OnboardingStatus.AUTOFILL_SETUP,
authDiskSource.getOnboardingStatus(userId = userId),
)
// These should be cleared
assertNull(authDiskSource.getUserBiometricUnlockKey(userId = userId))
@ -325,6 +334,7 @@ class AuthDiskSourceTest {
assertNull(authDiskSource.getShouldUseKeyConnector(userId = userId))
assertNull(authDiskSource.getIsTdeLoginComplete(userId = userId))
assertNull(authDiskSource.getAuthenticatorSyncUnlockKey(userId = userId))
assertNull(authDiskSource.getShowImportLogins(userId = userId))
}
@Test
@ -1088,7 +1098,7 @@ class AuthDiskSourceTest {
}
@Test
fun `getOnboardingStatus should update SharedPreferences`() {
fun `getOnboardingStatus should pull from SharedPreferences`() {
val onboardingStatusBaseKey = "bwPreferencesStorage:onboardingStatus"
val mockUserId = "mockUserId"
val expectedStatus = OnboardingStatus.AUTOFILL_SETUP
@ -1154,6 +1164,43 @@ class AuthDiskSourceTest {
// Retrieving the key from repository should give same byte array despite String conversion:
assertTrue(authDiskSource.authenticatorSyncSymmetricKey.contentEquals(symmetricKey))
}
@Test
fun `getShowImportLogins should pull from SharedPreferences`() {
val showImportLoginsBaseKey = "bwPreferencesStorage:showImportLogins"
val mockUserId = "mockUserId"
fakeSharedPreferences.edit {
putBoolean("${showImportLoginsBaseKey}_$mockUserId", true)
}
val actual = authDiskSource.getShowImportLogins(userId = mockUserId) ?: false
assertTrue(actual)
}
@Test
fun `storeShowImportLogins should update SharedPreferences`() {
val showImportLoginsBaseKey = "bwPreferencesStorage:showImportLogins"
val mockUserId = "mockUserId"
authDiskSource.storeShowImportLogins(
userId = mockUserId,
showImportLogins = true,
)
val actual = fakeSharedPreferences.getBoolean(
"${showImportLoginsBaseKey}_$mockUserId",
false,
)
assertTrue(actual)
}
@Test
fun `getShowImportLoginsFlow should react to changes from storeShowImportLogins`() = runTest {
val mockUserId = "mockUserId"
authDiskSource.getShowImportLoginsFlow(userId = mockUserId).test {
// The initial values of the Flow and the property are in sync
assertNull(awaitItem())
authDiskSource.storeShowImportLogins(userId = mockUserId, true)
assertTrue(awaitItem() ?: false)
}
}
}
private const val USER_STATE_JSON = """

View file

@ -29,6 +29,7 @@ class FakeAuthDiskSource : AuthDiskSource {
mutableMapOf<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
private val mutableAccountTokensFlowMap =
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableShowImportLoginsFlowMap = mutableMapOf<String, MutableSharedFlow<Boolean?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
@ -55,6 +56,7 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedAuthenticationSyncKeys = mutableMapOf<String, String?>()
private val storedPolicies = mutableMapOf<String, List<SyncResponseJson.Policy>?>()
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
private val storedShowImportLogins = mutableMapOf<String, Boolean?>()
override var userState: UserStateJson? = null
set(value) {
@ -269,6 +271,18 @@ class FakeAuthDiskSource : AuthDiskSource {
getMutableOnboardingStatusFlow(userId = userId)
.onSubscription { emit(getOnboardingStatus(userId)) }
override fun getShowImportLogins(userId: String): Boolean? =
storedShowImportLogins[userId]
override fun storeShowImportLogins(userId: String, showImportLogins: Boolean?) {
storedShowImportLogins[userId] = showImportLogins
getMutableShowImportLoginsFlow(userId = userId).tryEmit(showImportLogins)
}
override fun getShowImportLoginsFlow(userId: String): Flow<Boolean?> =
getMutableShowImportLoginsFlow(userId)
.onSubscription { emit(getShowImportLogins(userId)) }
/**
* Assert the the [isTdeLoginComplete] was stored successfully using the [userId].
*/
@ -449,5 +463,11 @@ class FakeAuthDiskSource : AuthDiskSource {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShowImportLoginsFlow(
userId: String,
): MutableSharedFlow<Boolean?> = mutableShowImportLoginsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
//endregion Private helper functions
}

View file

@ -82,6 +82,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePinResult
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
@ -344,6 +345,7 @@ class AuthRepositoryTest {
isBiometricsEnabledProvider = { false },
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
firstTimeState = FIRST_TIME_STATE,
),
repository.userStateFlow.value,
)
@ -370,6 +372,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
),
repository.userStateFlow.value,
)
@ -387,6 +390,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
),
repository.userStateFlow.value,
)
@ -416,6 +420,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
),
repository.userStateFlow.value,
)
@ -645,6 +650,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
)
val finalUserState = SINGLE_USER_STATE_2.toUserState(
vaultState = VAULT_UNLOCK_DATA,
@ -656,6 +662,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
)
val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams()
coEvery {
@ -5363,6 +5370,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
@ -5397,6 +5405,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
)
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
assertEquals(
@ -5429,6 +5438,7 @@ class AuthRepositoryTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = null,
firstTimeState = FIRST_TIME_STATE,
)
fakeAuthDiskSource.userState = MULTI_USER_STATE
assertEquals(
@ -6304,6 +6314,13 @@ class AuthRepositoryTest {
assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1))
}
@Test
fun `setShowImportLogins should save the showImportLogins to disk`() {
fakeAuthDiskSource.userState = MULTI_USER_STATE
repository.setShowImportLogins(showImportLogins = true)
assertEquals(true, fakeAuthDiskSource.getShowImportLogins(USER_ID_1))
}
companion object {
private const val UNIQUE_APP_ID = "testUniqueAppId"
private const val NAME = "Example Name"
@ -6494,5 +6511,9 @@ class AuthRepositoryTest {
status = VaultUnlockData.Status.UNLOCKED,
),
)
private val FIRST_TIME_STATE = UserState.FirstTimeState(
showImportLoginsCard = true,
)
}
}

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens
import com.x8bit.bitwarden.data.auth.repository.model.UserKeyConnectorState
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.UserSwitchingData
import com.x8bit.bitwarden.data.vault.datasource.network.model.OrganizationType
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
@ -506,6 +507,48 @@ class AuthDiskSourceExtensionsTest {
authDiskSource.currentOnboardingStatus,
)
}
@Test
fun `firstTimeStateFlow should emit changes when items in the first time state change`() =
runTest {
authDiskSource.firstTimeStateFlow.test {
authDiskSource.userState = MOCK_USER_STATE
assertEquals(
UserState.FirstTimeState(
showImportLoginsCard = true,
),
awaitItem(),
)
authDiskSource.storeShowImportLogins(MOCK_USER_ID, false)
assertEquals(
UserState.FirstTimeState(
showImportLoginsCard = false,
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `currentOrDefaultUserFirstTimeState should return the current first time state or a default state`() {
authDiskSource.userState = MOCK_USER_STATE
// Assert default state when no values set
assertEquals(
UserState.FirstTimeState(
showImportLoginsCard = true,
),
authDiskSource.currentOrDefaultUserFirstTimeState,
)
authDiskSource.storeShowImportLogins(MOCK_USER_ID, false)
assertEquals(
UserState.FirstTimeState(
showImportLoginsCard = false,
),
authDiskSource.currentOrDefaultUserFirstTimeState,
)
}
}
private const val MOCK_USER_ID: String = "mockId-1"

View file

@ -358,6 +358,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
),
@ -427,6 +428,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.PIN },
isDeviceTrustedProvider = { false },
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -464,6 +466,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -529,6 +532,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { false },
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -572,6 +576,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -640,6 +645,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -683,6 +689,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -751,6 +758,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -794,6 +802,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -862,6 +871,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -909,6 +919,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -977,6 +988,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -1005,6 +1017,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -1053,6 +1066,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -1081,6 +1095,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -1131,6 +1146,7 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@ -1175,6 +1191,7 @@ class UserStateJsonExtensionsTest {
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = true,
@ -1245,6 +1262,124 @@ class UserStateJsonExtensionsTest {
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = null,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}
@Suppress("MaxLineLength")
@Test
fun `toUserState should set the correct first time state values result`() {
assertEquals(
UserState(
activeUserId = "activeUserId",
accounts = listOf(
UserState.Account(
userId = "activeUserId",
name = "activeName",
email = "activeEmail",
// This value is calculated from the userId
avatarColorHex = "#ffecbc49",
environment = Environment.Eu,
isPremium = true,
isLoggedIn = false,
isVaultUnlocked = false,
needsPasswordReset = false,
organizations = listOf(
Organization(
id = "organizationId",
name = "organizationName",
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
),
),
isBiometricsEnabled = false,
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
needsMasterPassword = true,
trustedDevice = UserState.TrustedDevice(
isDeviceTrusted = true,
hasAdminApproval = false,
hasLoginApprovingDevice = true,
hasResetPasswordPermission = false,
),
hasMasterPassword = false,
isUsingKeyConnector = true,
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = false,
),
),
),
hasPendingAccountAddition = true,
),
UserStateJson(
activeUserId = "activeUserId",
accounts = mapOf(
"activeUserId" to AccountJson(
profile = mockk {
every { userId } returns "activeUserId"
every { name } returns "activeName"
every { email } returns "activeEmail"
every { avatarColorHex } returns null
every { hasPremium } returns true
every { forcePasswordResetReason } returns null
every { userDecryptionOptions } returns UserDecryptionOptionsJson(
hasMasterPassword = false,
trustedDeviceUserDecryptionOptions = TrustedDeviceUserDecryptionOptionsJson(
encryptedPrivateKey = null,
encryptedUserKey = null,
hasAdminApproval = false,
hasLoginApprovingDevice = true,
hasManageResetPasswordPermission = false,
),
keyConnectorUserDecryptionOptions = null,
)
},
tokens = null,
settings = AccountJson.Settings(
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_EU,
),
),
),
)
.toUserState(
vaultState = emptyList(),
userAccountTokens = listOf(
UserAccountTokens(
userId = "activeUserId",
accessToken = null,
refreshToken = null,
),
),
userOrganizationsList = listOf(
UserOrganizations(
userId = "activeUserId",
organizations = listOf(
Organization(
id = "organizationId",
name = "organizationName",
shouldManageResetPassword = false,
shouldUseKeyConnector = false,
role = OrganizationType.ADMIN,
),
),
),
),
userIsUsingKeyConnectorList = listOf(
UserKeyConnectorState(
userId = "activeUserId",
isUsingKeyConnector = true,
),
),
hasPendingAccountAddition = true,
isBiometricsEnabledProvider = { false },
vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD },
isDeviceTrustedProvider = { true },
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = false,
),
),
)
}

View file

@ -537,6 +537,7 @@ private fun createMockAccounts(number: Int): List<UserState.Account> {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
)
}

View file

@ -143,10 +143,15 @@ class SettingsDiskSourceTest {
value = true,
)
settingsDiskSource.storeShowUnlockSettingBadge(userId = userId, showBadge = true)
settingsDiskSource.storeShowAutoFillSettingBadge(userId = userId, showBadge = true)
settingsDiskSource.clearData(userId = userId)
// We do not clear these even when you call clear storage
assertEquals(true, settingsDiskSource.getScreenCaptureAllowed(userId = userId))
assertTrue(settingsDiskSource.getShowUnlockSettingBadge(userId = userId) ?: false)
assertTrue(settingsDiskSource.getShowAutoFillSettingBadge(userId = userId) ?: false)
// These should be cleared
assertNull(settingsDiskSource.getVaultTimeoutInMinutes(userId = userId))

View file

@ -10,4 +10,24 @@ class FlagKeyTest {
fun `AuthenticatorSync default value should be false`() {
assertFalse(FlagKey.AuthenticatorSync.defaultValue)
}
@Test
fun `EmailVerification default value should be false`() {
assertFalse(FlagKey.EmailVerification.defaultValue)
}
@Test
fun `OnboardingCarousel default value should be false`() {
assertFalse(FlagKey.OnboardingCarousel.defaultValue)
}
@Test
fun `OnboardingFlow default value should be false`() {
assertFalse(FlagKey.OnboardingFlow.defaultValue)
}
@Test
fun `ImportLoginsFlow default value should be false`() {
assertFalse(FlagKey.ImportLoginsFlow.defaultValue)
}
}

View file

@ -388,6 +388,7 @@ private val DEFAULT_USER_ACCOUNT = UserState.Account(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.ACCOUNT_LOCK_SETUP,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val CIPHER = mockk<Cipher>()

View file

@ -88,6 +88,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)
@ -225,6 +226,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
val userState = UserState(
activeUserId = "activeUserId",
@ -281,6 +283,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
val userState = UserState(
activeUserId = "activeUserId",
@ -341,6 +344,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
val userState = UserState(
activeUserId = "activeUserId",
@ -517,6 +521,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
val userState = UserState(
@ -552,6 +557,7 @@ class LandingViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
val userState = UserState(

View file

@ -131,6 +131,7 @@ class LoginViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -167,6 +167,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -276,6 +276,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
hasMasterPassword = false,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -219,6 +219,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)
@ -258,6 +259,8 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)
@ -1131,6 +1134,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -111,6 +111,7 @@ private val DEFAULT_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.EmailVerification to true,
FlagKey.OnboardingCarousel to true,
FlagKey.OnboardingFlow to true,
FlagKey.ImportLoginsFlow to true,
)
private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
@ -118,6 +119,7 @@ private val UPDATED_MAP_VALUE: Map<FlagKey<Any>, Any> = mapOf(
FlagKey.EmailVerification to false,
FlagKey.OnboardingCarousel to true,
FlagKey.OnboardingFlow to false,
FlagKey.ImportLoginsFlow to false,
)
private val DEFAULT_STATE = DebugMenuState(

View file

@ -106,6 +106,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -140,6 +143,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -171,6 +177,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -212,6 +221,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = false,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -250,6 +262,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -288,6 +303,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = false,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -323,6 +341,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
hasPendingAccountAddition = true,
@ -371,6 +392,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -403,6 +427,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -440,6 +467,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -480,6 +510,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -520,6 +553,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -567,6 +603,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -614,6 +653,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -656,6 +698,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -697,6 +742,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -773,6 +821,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -824,6 +875,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -883,6 +937,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -922,6 +979,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -956,6 +1016,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -988,6 +1051,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1023,6 +1089,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.ACCOUNT_LOCK_SETUP,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1058,6 +1127,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.AUTOFILL_SETUP,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1093,6 +1165,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.FINAL_STEP,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1128,6 +1203,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1163,6 +1241,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),
@ -1198,6 +1279,9 @@ class RootNavViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(
showImportLoginsCard = true,
),
),
),
),

View file

@ -1537,6 +1537,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -792,6 +792,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -244,6 +244,7 @@ private val DEFAULT_USER_STATE: UserState = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -372,6 +372,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -724,6 +724,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -2610,6 +2610,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -1103,6 +1103,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -3965,6 +3965,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
hasPendingAccountAddition = false,

View file

@ -513,6 +513,7 @@ class CipherViewExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
}

View file

@ -564,6 +564,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -2581,6 +2581,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -4032,6 +4032,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
private val DEFAULT_USER_STATE = UserState(

View file

@ -515,6 +515,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -130,6 +130,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)

View file

@ -1141,6 +1141,59 @@ class VaultScreenTest : BaseComposeTest() {
composeTestRule.waitForIdle()
assertTrue(permissionsManager.hasGetLauncherBeenCalled)
}
@Test
fun `action card for importing logins should show based on state`() {
mutableStateFlow.update {
it.copy(
viewState = VaultState.ViewState.NoItems,
)
}
val importSavedLogins = "Import saved logins"
composeTestRule
.onNodeWithText(importSavedLogins)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(
viewState = VaultState.ViewState.NoItems,
showImportActionCard = true,
)
}
composeTestRule
.onNodeWithText(importSavedLogins)
.assertIsDisplayed()
}
@Test
fun `when import action card is showing, clicking it should send ImportLoginsClick action`() {
mutableStateFlow.update {
it.copy(
viewState = VaultState.ViewState.NoItems,
showImportActionCard = true,
)
}
composeTestRule
.onNodeWithText("Get started")
.performClick()
verify { viewModel.trySendAction(VaultAction.ImportActionCardClick) }
}
@Suppress("MaxLineLength")
@Test
fun `when import action card is showing, dismissing it should send DismissImportActionCard action`() {
mutableStateFlow.update {
it.copy(
viewState = VaultState.ViewState.NoItems,
showImportActionCard = true,
)
}
composeTestRule
.onNodeWithContentDescription("Close")
.performClick()
verify { viewModel.trySendAction(VaultAction.DismissImportActionCard) }
}
}
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
@ -1195,6 +1248,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
hasMasterPassword = true,
hideNotificationsDialog = true,
isRefreshing = false,
showImportActionCard = false,
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(

View file

@ -8,9 +8,11 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -41,6 +43,7 @@ import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -84,6 +87,7 @@ class VaultViewModelTest : BaseViewModelTest() {
every { hasPendingAccountAddition = any() } just runs
every { logout(any()) } just runs
every { switchAccount(any()) } answers { switchAccountResult }
every { setShowImportLogins(any()) } just runs
}
private val settingsRepository: SettingsRepository = mockk {
@ -106,6 +110,13 @@ class VaultViewModelTest : BaseViewModelTest() {
every { trackEvent(event = any()) } just runs
}
private val mutableImportLoginsFeatureFlow = MutableStateFlow(true)
private val featureFlagManager: FeatureFlagManager = mockk {
every {
getFeatureFlagFlow(FlagKey.ImportLoginsFlow)
} returns mutableImportLoginsFeatureFlow
}
@Test
fun `initial state should be correct and should trigger a syncIfNecessary call`() {
val viewModel = createViewModel()
@ -198,6 +209,7 @@ class VaultViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
),
),
)
@ -283,6 +295,7 @@ class VaultViewModelTest : BaseViewModelTest() {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
),
),
)
@ -1484,6 +1497,113 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `when user first time state updates, vault state is updated`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE,
awaitItem(),
)
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map {
it.copy(
firstTimeState = DEFAULT_FIRST_TIME_STATE.copy(
showImportLoginsCard = false,
),
)
},
)
assertEquals(
DEFAULT_STATE.copy(
showImportActionCard = false,
),
awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `when feature flag ImportLoginsFlow is disabled, should show action card should always be false`() =
runTest {
mutableImportLoginsFeatureFlow.update { false }
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(
DEFAULT_STATE.copy(showImportActionCard = false),
awaitItem(),
)
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map {
it.copy(
firstTimeState = DEFAULT_FIRST_TIME_STATE.copy(
showImportLoginsCard = true,
),
)
},
)
expectNoEvents()
}
}
@Test
fun `when DismissImportActionCard is sent, repository called to set value to false`() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.DismissImportActionCard)
verify(exactly = 1) {
authRepository.setShowImportLogins(false)
}
}
@Suppress("MaxLineLength")
@Test
fun `when DismissImportActionCard is sent, repository is not called if value is already false`() {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map {
it.copy(
firstTimeState = DEFAULT_FIRST_TIME_STATE.copy(
showImportLoginsCard = false,
),
)
},
)
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.DismissImportActionCard)
verify(exactly = 0) {
authRepository.setShowImportLogins(false)
}
}
@Test
fun `when ImportActionCardClick is sent, repository called to set value to false`() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.ImportActionCardClick)
verify(exactly = 1) {
authRepository.setShowImportLogins(false)
}
}
@Suppress("MaxLineLength")
@Test
fun `when ImportActionCardClick is sent, repository is not called if value is already false`() {
mutableUserStateFlow.value = DEFAULT_USER_STATE.copy(
accounts = DEFAULT_USER_STATE.accounts.map {
it.copy(
firstTimeState = DEFAULT_FIRST_TIME_STATE.copy(
showImportLoginsCard = false,
),
)
},
)
val viewModel = createViewModel()
viewModel.trySendAction(VaultAction.ImportActionCardClick)
verify(exactly = 0) {
authRepository.setShowImportLogins(false)
}
}
private fun createViewModel(): VaultViewModel =
VaultViewModel(
authRepository = authRepository,
@ -1493,6 +1613,7 @@ class VaultViewModelTest : BaseViewModelTest() {
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
organizationEventManager = organizationEventManager,
featureFlagManager = featureFlagManager,
)
}
@ -1513,6 +1634,10 @@ private val VAULT_FILTER_DATA = VaultFilterData(
private val DEFAULT_STATE: VaultState =
createMockVaultState(viewState = VaultState.ViewState.Loading)
private val DEFAULT_FIRST_TIME_STATE = UserState.FirstTimeState(
showImportLoginsCard = true,
)
private val DEFAULT_USER_STATE = UserState(
activeUserId = "activeUserId",
accounts = listOf(
@ -1533,6 +1658,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
),
UserState.Account(
userId = "lockedUserId",
@ -1551,6 +1677,7 @@ private val DEFAULT_USER_STATE = UserState(
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = DEFAULT_FIRST_TIME_STATE,
),
),
)
@ -1594,5 +1721,6 @@ private fun createMockVaultState(
isIconLoadingDisabled = false,
hasMasterPassword = true,
hideNotificationsDialog = true,
showImportActionCard = true,
isRefreshing = false,
)

View file

@ -87,6 +87,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
UserState.Account(
userId = "lockedUserId",
@ -113,6 +114,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
UserState.Account(
userId = "unlockedUserId",
@ -143,6 +145,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
UserState.Account(
userId = "loggedOutUserId",
@ -173,6 +176,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)
@ -218,6 +222,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
.toAccountSummary(isActive = true),
)
@ -261,6 +266,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
.toAccountSummary(isActive = false),
)
@ -308,6 +314,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
),
),
)
@ -335,6 +342,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
.toVaultFilterData(isIndividualVaultDisabled = false),
)
@ -391,6 +399,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
.toVaultFilterData(
isIndividualVaultDisabled = false,
@ -448,6 +457,7 @@ class UserStateExtensionsTest {
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true),
)
.toVaultFilterData(
isIndividualVaultDisabled = true,