diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt index fccb5be33..f67159f00 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSource.kt @@ -306,4 +306,19 @@ interface AuthDiskSource { * if any exists. */ fun getOnboardingStatusFlow(userId: String): Flow + + /** + * 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 } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt index 6ebc9b925..4efa1c2cc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceImpl.kt @@ -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>() private val mutableOnboardingStatusFlowMap = mutableMapOf>() + private val mutableShowImportLoginsFlowMap = mutableMapOf>() private val mutableUserStateFlow = bufferedMutableSharedFlow(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 = + 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 = mutableShowImportLoginsFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + private fun migrateAccountTokens() { userState ?.accounts diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index fc316f28d..7d38f885e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 8dcb931c2..cf09d1af0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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 val userIsUsingKeyConnectorList = array[3] as List val onboardingStatus = array[4] as OnboardingStatus? - val vaultState = array[5] as List - val hasPendingAccountAddition = array[6] as Boolean + val firstTimeState = array[5] as UserState.FirstTimeState + val vaultState = array[6] as List + 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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt index c8dfb7ff4..5d51209e2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/UserState.kt @@ -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, + ) + } } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensions.kt index a9acced50..8b08770db 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensions.kt @@ -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 } .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 + 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, + ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt index 77ee9d6d1..9fb962217 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensions.kt @@ -111,6 +111,7 @@ fun UserStateJson.toUserState( userIsUsingKeyConnectorList: List, 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, diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index 809d10a3d..df3c47b39 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt index 4bf506af3..88ccd5f74 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/FlagKey.kt @@ -30,6 +30,7 @@ sealed class FlagKey { EmailVerification, OnboardingFlow, OnboardingCarousel, + ImportLoginsFlow, ) } } @@ -70,6 +71,15 @@ sealed class FlagKey { override val isRemotelyConfigured: Boolean = false } + /** + * Data object holding the feature flag key for the import logins feature. + */ + data object ImportLoginsFlow : FlagKey() { + 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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt index fbf6ab0ce..2f2405e30 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt @@ -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, + ) + }, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt index cb7cedecc..6d264cb92 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/components/FeatureFlagListItems.kt @@ -26,6 +26,7 @@ fun FlagKey.ListItemContent( FlagKey.EmailVerification, FlagKey.OnboardingCarousel, FlagKey.OnboardingFlow, + FlagKey.ImportLoginsFlow, -> BooleanFlagItem( label = flagKey.getDisplayLabel(), key = flagKey as FlagKey, @@ -67,4 +68,5 @@ private fun FlagKey.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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index af938bb8b..06bacec57 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -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(), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 4e5c3d965..aacccf23f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -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( 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() /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt index 2d06d7dab..3e1106d62 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt @@ -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) + }, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f7df14f92..ff7cebfc0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1018,4 +1018,6 @@ Do you want to switch to this account? New login Share files and data securely with anyone, on any platform. Your information will remain end-to-end encrypted while limiting exposure. Send sensitive information, safely + Import saved logins + Use a computer to import logins from an existing password manager diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml index 0d84a34a2..ab58f44e3 100644 --- a/app/src/main/res/values/strings_non_localized.xml +++ b/app/src/main/res/values/strings_non_localized.xml @@ -11,6 +11,7 @@ Email Verification Onboarding Carousel Onboarding Flow + Import Logins Flow Feature Flags: Debug Menu Reset values diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index ceb6f4857..f4463619b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt index 7bb008fbf..f27a32320 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/AuthDiskSourceTest.kt @@ -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 = """ diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt index 05e6bbb21..bbe0526e2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/disk/util/FakeAuthDiskSource.kt @@ -29,6 +29,7 @@ class FakeAuthDiskSource : AuthDiskSource { mutableMapOf?>>() private val mutableAccountTokensFlowMap = mutableMapOf>() + private val mutableShowImportLoginsFlowMap = mutableMapOf>() private val mutableOnboardingStatusFlowMap = mutableMapOf>() @@ -55,6 +56,7 @@ class FakeAuthDiskSource : AuthDiskSource { private val storedAuthenticationSyncKeys = mutableMapOf() private val storedPolicies = mutableMapOf?>() private val storedOnboardingStatus = mutableMapOf() + private val storedShowImportLogins = mutableMapOf() 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 = + 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 = mutableShowImportLoginsFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + //endregion Private helper functions } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 2119cab67..de62924d5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -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, +) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt index f91e231cc..ec66e87aa 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt @@ -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" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt index d86579547..a6c877f77 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt @@ -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, + ), ), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt index 6086e7f3e..596879d34 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/autofill/fido2/processor/Fido2ProviderProcessorTest.kt @@ -537,6 +537,7 @@ private fun createMockAccounts(number: Int): List { hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index 7de051b5d..9724f8d96 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -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)) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt index 2ce77ef62..350b6d576 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/manager/FlagKeyTest.kt @@ -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) + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt index b94f198b6..29c35f1d5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt @@ -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() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt index 994563228..a61b6516c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index 64438cb48..92caa280c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -131,6 +131,7 @@ class LoginViewModelTest : BaseViewModelTest() { hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt index 9533159a6..ca2ac66fe 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt index a2051517d..26f223735 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 33df32539..5484ec38b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index 9a59db9e9..da866387d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -111,6 +111,7 @@ private val DEFAULT_MAP_VALUE: Map, Any> = mapOf( FlagKey.EmailVerification to true, FlagKey.OnboardingCarousel to true, FlagKey.OnboardingFlow to true, + FlagKey.ImportLoginsFlow to true, ) private val UPDATED_MAP_VALUE: Map, Any> = mapOf( @@ -118,6 +119,7 @@ private val UPDATED_MAP_VALUE: Map, Any> = mapOf( FlagKey.EmailVerification to false, FlagKey.OnboardingCarousel to true, FlagKey.OnboardingFlow to false, + FlagKey.ImportLoginsFlow to false, ) private val DEFAULT_STATE = DebugMenuState( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 001ac1313..0764fbf84 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -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, + ), ), ), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index 09d06af4d..deaa780f0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -1537,6 +1537,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index dd0bdee34..7821e9bd2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -792,6 +792,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt index 7e758b329..cdbc86393 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt @@ -244,6 +244,7 @@ private val DEFAULT_USER_STATE: UserState = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt index 30d0e77db..2013d82f6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt @@ -372,6 +372,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt index 74a09f37f..55c3b597a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt @@ -724,6 +724,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt index 5115dff37..3b85ca159 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt @@ -2610,6 +2610,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index f73487399..8f548f023 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index c2f8b9f67..3bcc0eb71 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -3965,6 +3965,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), hasPendingAccountAddition = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index bb533d1f3..d2f245d67 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -513,6 +513,7 @@ class CipherViewExtensionsTest { hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt index 41ba591ae..926458ee2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt @@ -564,6 +564,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index 7d154ad54..4ac0fcfdf 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -2581,6 +2581,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 1928ac029..802d11c8a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt index 625d63bfd..e91faa724 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt @@ -515,6 +515,7 @@ private val DEFAULT_USER_STATE = UserState( hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt index 26f756fda..a74f0af7a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt @@ -130,6 +130,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState = hasMasterPassword = true, isUsingKeyConnector = false, onboardingStatus = OnboardingStatus.COMPLETE, + firstTimeState = UserState.FirstTimeState(showImportLoginsCard = true), ), ), ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 33877783f..2ddfda13e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 97904d533..86b5ed044 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt index 45bb6315b..52be5a25e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt @@ -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,