From 759e9265882e35b8c9125c22c444e9e618becc5f Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:40:56 -0400 Subject: [PATCH] PM-11464 Add onboarding status to user Account to allow for root navigation to onboarding flow. (#3878) --- .../auth/datasource/disk/AuthDiskSource.kt | 17 ++ .../datasource/disk/AuthDiskSourceImpl.kt | 30 +++ .../datasource/disk/model/OnboardingStatus.kt | 35 +++ .../data/auth/repository/AuthRepository.kt | 6 + .../auth/repository/AuthRepositoryImpl.kt | 25 +- .../data/auth/repository/model/UserState.kt | 2 + .../util/AuthDiskSourceExtensions.kt | 21 ++ .../util/UserStateJsonExtensions.kt | 5 + .../accountsetup/SetupAutoFillNavigation.kt | 13 +- .../accountsetup/SetupAutoFillViewModel.kt | 9 +- .../accountsetup/SetupAutofillScreen.kt | 2 - .../accountsetup/SetupUnlockNavigation.kt | 13 +- .../feature/accountsetup/SetupUnlockScreen.kt | 2 - .../accountsetup/SetupUnlockViewModel.kt | 13 +- .../platform/feature/rootnav/RootNavScreen.kt | 19 ++ .../feature/rootnav/RootNavViewModel.kt | 24 ++ .../com/x8bit/bitwarden/MainViewModelTest.kt | 2 + .../datasource/disk/AuthDiskSourceTest.kt | 49 ++++ .../disk/util/FakeAuthDiskSource.kt | 24 ++ .../auth/repository/AuthRepositoryTest.kt | 206 ++++++++++++++- .../util/AuthDiskSourceExtensionsTest.kt | 34 +++ .../util/UserStateJsonExtensionsTest.kt | 238 +++++++++++++++++- .../processor/Fido2ProviderProcessorTest.kt | 2 + .../SetupAutoFillViewModelTest.kt | 18 -- .../accountsetup/SetupAutofillScreenTest.kt | 9 - .../accountsetup/SetupUnlockScreenTest.kt | 10 - .../accountsetup/SetupUnlockViewModelTest.kt | 25 +- .../feature/landing/LandingViewModelTest.kt | 7 + .../auth/feature/login/LoginViewModelTest.kt | 2 + .../RemovePasswordViewModelTest.kt | 2 + .../TrustedDeviceViewModelTest.kt | 2 + .../vaultunlock/VaultUnlockViewModelTest.kt | 4 + .../feature/rootnav/RootNavScreenTest.kt | 10 + .../feature/rootnav/RootNavViewModelTest.kt | 231 +++++++++++++++++ .../feature/search/SearchViewModelTest.kt | 2 + .../AccountSecurityViewModelTest.kt | 2 + .../DeleteAccountViewModelTest.kt | 2 + .../LoginApprovalViewModelTest.kt | 2 + .../exportvault/ExportVaultViewModelTest.kt | 2 + .../generator/GeneratorViewModelTest.kt | 2 + .../send/addsend/AddSendViewModelTest.kt | 2 + .../addedit/VaultAddEditViewModelTest.kt | 2 + .../addedit/util/CipherViewExtensionsTest.kt | 2 + .../attachments/AttachmentsViewModelTest.kt | 2 + .../feature/item/VaultItemViewModelTest.kt | 2 + .../VaultItemListingViewModelTest.kt | 2 + .../VaultMoveToOrganizationViewModelTest.kt | 2 + .../VaultMoveToOrganizationExtensionsTest.kt | 2 + .../vault/feature/vault/VaultViewModelTest.kt | 5 + .../vault/util/UserStateExtensionsTest.kt | 11 + 50 files changed, 1074 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt 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 9341639a6..1ce975292 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.vault.datasource.network.model.SyncResponseJson @@ -282,4 +283,20 @@ interface AuthDiskSource { * Stores the [accountTokens] for the given [userId]. */ fun storeAccountTokens(userId: String, accountTokens: AccountTokensJson?) + + /** + * Gets the onboarding status for the given [userId]. + */ + fun getOnboardingStatus(userId: String): OnboardingStatus? + + /** + * Stores the [onboardingStatus] for the given [userId]. + */ + fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) + + /** + * Emits updates that track [getOnboardingStatus]. This will replay the last known value, + * if any exists. + */ + fun getOnboardingStatusFlow(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 0ed21204b..dfd036695 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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk import android.content.SharedPreferences import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.platform.datasource.disk.BaseEncryptedDiskSource @@ -42,6 +43,7 @@ private const val POLICIES_KEY = "policies" 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" /** * Primary implementation of [AuthDiskSource]. @@ -67,6 +69,8 @@ class AuthDiskSourceImpl( mutableMapOf?>>() private val mutableAccountTokensFlowMap = mutableMapOf>() + private val mutableOnboardingStatusFlowMap = + mutableMapOf>() private val mutableUserStateFlow = bufferedMutableSharedFlow(replay = 1) override var userState: UserStateJson? @@ -405,6 +409,25 @@ class AuthDiskSourceImpl( getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens) } + override fun getOnboardingStatus(userId: String): OnboardingStatus? { + return getString(key = ONBOARDING_STATUS_KEY.appendIdentifier(userId))?.let { + json.decodeFromStringOrNull(it) + } + } + + override fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) { + putString( + key = ONBOARDING_STATUS_KEY.appendIdentifier(userId), + value = onboardingStatus?.let { json.encodeToString(it) }, + ) + getMutableOnboardingStatusFlow(userId = userId).tryEmit(onboardingStatus) + } + + override fun getOnboardingStatusFlow(userId: String): Flow { + return getMutableOnboardingStatusFlow(userId = userId) + .onSubscription { emit(getOnboardingStatus(userId = userId)) } + } + private fun generateAndStoreUniqueAppId(): String = UUID .randomUUID() @@ -413,6 +436,13 @@ class AuthDiskSourceImpl( putString(key = UNIQUE_APP_ID_KEY, value = it) } + private fun getMutableOnboardingStatusFlow( + userId: String, + ): MutableSharedFlow = + mutableOnboardingStatusFlowMap.getOrPut(userId) { + bufferedMutableSharedFlow(replay = 1) + } + private fun getMutableShouldUseKeyConnectorFlowMap( userId: String, ): MutableSharedFlow = diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt new file mode 100644 index 000000000..4e3e71ed5 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/disk/model/OnboardingStatus.kt @@ -0,0 +1,35 @@ +package com.x8bit.bitwarden.data.auth.datasource.disk.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Describes the current status of a user in the account onboarding steps. + */ +@Serializable +enum class OnboardingStatus { + + /** + * Onboarding has not yet started. + */ + @SerialName("notStarted") + NOT_STARTED, + + /** + * The user is completing the account lock setup. + */ + @SerialName("accountLockSetup") + ACCOUNT_LOCK_SETUP, + + /** + * The user is completing the auto fill service setup. + */ + @SerialName("autofillSetup") + AUTOFILL_SETUP, + + /** + * The user has completed all onboarding steps. + */ + @SerialName("complete") + COMPLETE, +} 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 2bae54c03..809ba1cf9 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.auth.repository import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager @@ -386,4 +387,9 @@ interface AuthRepository : AuthenticatorProvider, AuthRequestManager { email: String, token: String, ): EmailTokenResult + + /** + * Update the value of the onboarding status for the user. + */ + fun setOnboardingStatus(userId: String, status: OnboardingStatus?) } 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 dcca27efe..dd8cc582d 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 @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.DeviceDataModel @@ -74,6 +75,8 @@ import com.x8bit.bitwarden.data.auth.repository.util.DuoCallbackTokenResult 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.onboardingStatusChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.policyInformation import com.x8bit.bitwarden.data.auth.repository.util.toRemovedPasswordUserStateJson import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams @@ -250,6 +253,7 @@ class AuthRepositoryImpl( authDiskSource.userAccountTokensFlow, authDiskSource.userOrganizationsListFlow, authDiskSource.userKeyConnectorStateFlow, + authDiskSource.onboardingStatusChangesFlow, vaultRepository.vaultUnlockDataStateFlow, mutableHasPendingAccountAdditionStateFlow, // Ignore the data in the merge, but trigger an update when they emit. @@ -262,14 +266,16 @@ class AuthRepositoryImpl( val userAccountTokens = array[1] as List val userOrganizationsList = array[2] as List val userIsUsingKeyConnectorList = array[3] as List - val vaultState = array[4] as List - val hasPendingAccountAddition = array[5] as Boolean + val onboardingStatus = array[4] as OnboardingStatus? + val vaultState = array[5] as List + val hasPendingAccountAddition = array[6] as Boolean userStateJson?.toUserState( vaultState = vaultState, userAccountTokens = userAccountTokens, userOrganizationsList = userOrganizationsList, userIsUsingKeyConnectorList = userIsUsingKeyConnectorList, hasPendingAccountAddition = hasPendingAccountAddition, + onboardingStatus = onboardingStatus, isBiometricsEnabledProvider = ::isBiometricsEnabled, vaultUnlockTypeProvider = ::getVaultUnlockType, isDeviceTrustedProvider = ::isDeviceTrusted, @@ -288,6 +294,7 @@ class AuthRepositoryImpl( userOrganizationsList = authDiskSource.userOrganizationsList, userIsUsingKeyConnectorList = authDiskSource.userKeyConnectorStateList, hasPendingAccountAddition = mutableHasPendingAccountAdditionStateFlow.value, + onboardingStatus = authDiskSource.currentOnboardingStatus, isBiometricsEnabledProvider = ::isBiometricsEnabled, vaultUnlockTypeProvider = ::getVaultUnlockType, isDeviceTrustedProvider = ::isDeviceTrusted, @@ -1284,6 +1291,10 @@ class AuthRepositoryImpl( ) } + override fun setOnboardingStatus(userId: String, status: OnboardingStatus?) { + authDiskSource.storeOnboardingStatus(userId = userId, onboardingStatus = status) + } + @Suppress("CyclomaticComplexMethod") private suspend fun validatePasswordAgainstPolicy( password: String, @@ -1559,6 +1570,16 @@ class AuthRepositoryImpl( ), ) settingsRepository.hasUserLoggedInOrCreatedAccount = true + + val shouldSetOnboardingStatus = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) && + !settingsRepository.getUserHasLoggedInValue(userId = userId) + if (shouldSetOnboardingStatus) { + setOnboardingStatus( + userId = userId, + status = OnboardingStatus.NOT_STARTED, + ) + } + authDiskSource.userState = userStateJson loginResponse.key?.let { // Only set the value if it's present, since we may have set it already 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 ced9a7c88..c8dfb7ff4 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 @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.auth.repository.model +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.model.UserState.Account import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -69,6 +70,7 @@ data class UserState( val isBiometricsEnabled: Boolean, val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD, val isUsingKeyConnector: Boolean, + val onboardingStatus: OnboardingStatus, ) { /** * Indicates that the user does or does not have a means to manually unlock the vault. 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 e127b6f57..c3adedb3f 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.auth.repository.util import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource +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 @@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** @@ -167,3 +169,22 @@ val AuthDiskSource.activeUserIdChangesFlow: Flow .userStateFlow .map { it?.activeUserId } .distinctUntilChanged() + +/** + * Returns a [Flow] that emits every time the active user's onboarding status is changed + */ +@OptIn(ExperimentalCoroutinesApi::class) +val AuthDiskSource.onboardingStatusChangesFlow: Flow + get() = activeUserIdChangesFlow + .flatMapLatest { activeUserId -> + activeUserId + ?.let { this.getOnboardingStatusFlow(userId = it) } + ?: flowOf(null) + } + .distinctUntilChanged() + +val AuthDiskSource.currentOnboardingStatus: OnboardingStatus? + get() = this + .userState + ?.activeUserId + ?.let { this.getOnboardingStatus(userId = it) } 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 4d6f7e613..696704883 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 @@ -1,5 +1,6 @@ package com.x8bit.bitwarden.data.auth.repository.util +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson import com.x8bit.bitwarden.data.auth.repository.model.UserAccountTokens @@ -109,6 +110,7 @@ fun UserStateJson.toUserState( userOrganizationsList: List, userIsUsingKeyConnectorList: List, hasPendingAccountAddition: Boolean, + onboardingStatus: OnboardingStatus?, isBiometricsEnabledProvider: (userId: String) -> Boolean, vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType, isDeviceTrustedProvider: (userId: String) -> Boolean, @@ -171,6 +173,9 @@ fun UserStateJson.toUserState( isUsingKeyConnector = userIsUsingKeyConnectorList .find { it.userId == userId } ?.isUsingKeyConnector == true, + // 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, ) }, hasPendingAccountAddition = hasPendingAccountAddition, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt index 5f5591d68..1ff43e8f8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt @@ -5,7 +5,10 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions -private const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill" +/** + * Route name for [SetupAutoFillScreen]. + */ +const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill" /** * Navigate to the setup auto-fill screen. @@ -17,14 +20,10 @@ fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) /** * Add the setup auto-fil screen to the nav graph. */ -fun NavGraphBuilder.setupAutoFillDestination( - onNavigateToCompleteSetup: () -> Unit, -) { +fun NavGraphBuilder.setupAutoFillDestination() { composableWithPushTransitions( route = SETUP_AUTO_FILL_ROUTE, ) { - SetupAutoFillScreen( - onNavigateToCompleteSetup = onNavigateToCompleteSetup, - ) + SetupAutoFillScreen() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt index 40807f45a..574f18fb2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt @@ -72,11 +72,12 @@ class SetupAutoFillViewModel @Inject constructor( } private fun handleTurnOnLaterConfirmClick() { - sendEvent(SetupAutoFillEvent.NavigateToCompleteSetup) + // TODO PM-10631 record user chose to turn on later for settings badging. + // TODO PM-10632 update status to complete setup step. } private fun handleContinueClick() { - sendEvent(SetupAutoFillEvent.NavigateToCompleteSetup) + // TODO PM-10632 update status to complete setup step. } private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) { @@ -115,10 +116,6 @@ sealed class SetupAutoFillDialogState { * UI Events for the Auto-fill setup screen. */ sealed class SetupAutoFillEvent { - /** - * Navigate to the complete setup screen. - */ - data object NavigateToCompleteSetup : SetupAutoFillEvent() /** * Navigate to the autofill settings screen. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt index 961dbde01..2a36c4e69 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreen.kt @@ -57,7 +57,6 @@ import com.x8bit.bitwarden.ui.platform.util.isPortrait @OptIn(ExperimentalMaterial3Api::class) @Composable fun SetupAutoFillScreen( - onNavigateToCompleteSetup: () -> Unit, intentManager: IntentManager = LocalIntentManager.current, viewModel: SetupAutoFillViewModel = hiltViewModel(), ) { @@ -65,7 +64,6 @@ fun SetupAutoFillScreen( val handler = rememberSetupAutoFillHandler(viewModel = viewModel) EventsEffect(viewModel = viewModel) { event -> when (event) { - SetupAutoFillEvent.NavigateToCompleteSetup -> onNavigateToCompleteSetup() SetupAutoFillEvent.NavigateToAutofillSettings -> { val showFallback = !intentManager.startSystemAutofillSettingsActivity() if (showFallback) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt index 51a9ed3d9..94ea34b04 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt @@ -5,7 +5,10 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions -private const val SETUP_UNLOCK_ROUTE = "setup_unlock" +/** + * Route for [SetupUnlockScreen] + */ +const val SETUP_UNLOCK_ROUTE = "setup_unlock" /** * Navigate to the setup unlock screen. @@ -17,14 +20,10 @@ fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) { /** * Add the setup unlock screen to the nav graph. */ -fun NavGraphBuilder.setupUnlockDestination( - onNavigateToSetupAutofill: () -> Unit, -) { +fun NavGraphBuilder.setupUnlockDestination() { composableWithPushTransitions( route = SETUP_UNLOCK_ROUTE, ) { - SetupUnlockScreen( - onNavigateToSetupAutofill = onNavigateToSetupAutofill, - ) + SetupUnlockScreen() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt index 460fd06d2..acc33aac3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreen.kt @@ -62,7 +62,6 @@ import com.x8bit.bitwarden.ui.platform.util.isPortrait @OptIn(ExperimentalMaterial3Api::class) @Composable fun SetupUnlockScreen( - onNavigateToSetupAutofill: () -> Unit, viewModel: SetupUnlockViewModel = hiltViewModel(), biometricsManager: BiometricsManager = LocalBiometricsManager.current, ) { @@ -71,7 +70,6 @@ fun SetupUnlockScreen( var showBiometricsPrompt by rememberSaveable { mutableStateOf(value = false) } EventsEffect(viewModel = viewModel) { event -> when (event) { - SetupUnlockEvent.NavigateToSetupAutofill -> onNavigateToSetupAutofill() is SetupUnlockEvent.ShowBiometricsPrompt -> { showBiometricsPrompt = true biometricsManager.promptBiometrics( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt index f77051653..75eb185ac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository @@ -67,7 +68,7 @@ class SetupUnlockViewModel @Inject constructor( } private fun handleContinueClick() { - sendEvent(SetupUnlockEvent.NavigateToSetupAutofill) + updateOnboardingStatusToNextStep() } private fun handleEnableBiometricsClick() { @@ -94,7 +95,7 @@ class SetupUnlockViewModel @Inject constructor( } private fun handleSetUpLaterClick() { - sendEvent(SetupUnlockEvent.NavigateToSetupAutofill) + updateOnboardingStatusToNextStep() } private fun handleDismissDialog() { @@ -173,6 +174,10 @@ class SetupUnlockViewModel @Inject constructor( } } } + + private fun updateOnboardingStatusToNextStep() { + authRepository.setOnboardingStatus(state.userId, OnboardingStatus.AUTOFILL_SETUP) + } } /** @@ -219,10 +224,6 @@ data class SetupUnlockState( * Models events for the setup unlock screen. */ sealed class SetupUnlockEvent { - /** - * Navigate to autofill setup. - */ - data object NavigateToSetupAutofill : SetupUnlockEvent() /** * Shows the prompt for biometrics using with the given [cipher]. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 374a328ed..36874e7a9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -15,6 +15,12 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_AUTO_FILL_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_UNLOCK_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillScreen +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreen +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestination +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestination import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph @@ -90,6 +96,8 @@ fun RootNavScreen( vaultUnlockDestination() vaultUnlockedGraph(navController) setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() }) + setupUnlockDestination() + setupAutoFillDestination() } val targetRoute = when (state) { @@ -114,6 +122,9 @@ fun RootNavScreen( is RootNavState.VaultUnlockedForFido2Assertion, is RootNavState.VaultUnlockedForFido2GetCredentials, -> VAULT_UNLOCKED_GRAPH_ROUTE + + RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_ROUTE + RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_ROUTE } val currentRoute = navController.currentDestination?.rootLevelRoute() @@ -217,6 +228,14 @@ fun RootNavScreen( navOptions = rootNavOptions, ) } + + RootNavState.OnboardingAccountLockSetup -> { + navController.navigateToSetupUnlockScreen(rootNavOptions) + } + + RootNavState.OnboardingAutoFillSetup -> { + navController.navigateToSetupAutoFillScreen(rootNavOptions) + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 8d2edc9bc..0a7abd97b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import android.os.Parcelable import androidx.lifecycle.viewModelScope +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -89,6 +90,17 @@ class RootNavViewModel @Inject constructor( RootNavState.RemovePassword } + userState.activeAccount.isVaultUnlocked && + userState.activeAccount.onboardingStatus != OnboardingStatus.COMPLETE -> { + when (userState.activeAccount.onboardingStatus) { + OnboardingStatus.NOT_STARTED, + OnboardingStatus.ACCOUNT_LOCK_SETUP, + -> RootNavState.OnboardingAccountLockSetup + OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup + OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.") + } + } + userState.activeAccount.isVaultUnlocked -> { when (specialCircumstance) { is SpecialCircumstance.AutofillSave -> { @@ -320,6 +332,18 @@ sealed class RootNavState : Parcelable { */ @Parcelize data object ExpiredRegistrationLink : RootNavState() + + /** + * App should show the set up account lock onboarding screen. + */ + @Parcelize + data object OnboardingAccountLockSetup : RootNavState() + + /** + * App should show the set up autofill onboarding screen. + */ + @Parcelize + data object OnboardingAutoFillSetup : RootNavState() } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt index 6cb201409..9c1dfbeaa 100644 --- a/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt @@ -5,6 +5,7 @@ import android.content.pm.SigningInfo import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.vault.CipherView +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.EmailTokenResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult @@ -1021,6 +1022,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 e737fe7c0..01db4c0e6 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 @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson @@ -1084,6 +1085,54 @@ class AuthDiskSourceTest { actual, ) } + + @Test + fun `getOnboardingStatus should update SharedPreferences`() { + val onboardingStatusBaseKey = "bwPreferencesStorage:onboardingStatus" + val mockUserId = "mockUserId" + val expectedStatus = OnboardingStatus.AUTOFILL_SETUP + fakeSharedPreferences.edit { + putString( + "${onboardingStatusBaseKey}_$mockUserId", + json.encodeToString(expectedStatus), + ) + } + val actual = authDiskSource.getOnboardingStatus(userId = mockUserId) + assertEquals( + expectedStatus, + actual, + ) + } + + @Test + fun `storeOnboardingStatus should update SharedPreferences`() { + val onboardingStatusBaseKey = "bwPreferencesStorage:onboardingStatus" + val mockUserId = "mockUserId" + val mockOnboardingStatus = OnboardingStatus.AUTOFILL_SETUP + authDiskSource.storeOnboardingStatus(mockUserId, mockOnboardingStatus) + + val actual = fakeSharedPreferences.getString( + "${onboardingStatusBaseKey}_$mockUserId", + null, + ) + assertEquals( + json.encodeToString(mockOnboardingStatus), + actual, + ) + } + + @Test + fun `getOnboardingStatusFlow should react to changes from storeOnboardingStatus`() = runTest { + val userId = "userId" + authDiskSource.getOnboardingStatusFlow(userId).test { + // The initial values of the Flow and the property are in sync + assertNull(awaitItem()) + + // Updating the repository updates shared preferences + authDiskSource.storeOnboardingStatus(userId, OnboardingStatus.AUTOFILL_SETUP) + assertEquals(OnboardingStatus.AUTOFILL_SETUP, awaitItem()) + } + } } 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 e8d79ee8e..36a9513f6 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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.datasource.disk.util import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow @@ -26,6 +27,9 @@ class FakeAuthDiskSource : AuthDiskSource { mutableMapOf?>>() private val mutableAccountTokensFlowMap = mutableMapOf>() + + private val mutableOnboardingStatusFlowMap = + mutableMapOf>() private val mutableUserStateFlow = bufferedMutableSharedFlow(replay = 1) private val storedShouldUseKeyConnector = mutableMapOf() @@ -48,6 +52,7 @@ class FakeAuthDiskSource : AuthDiskSource { private val storedMasterPasswordHashes = mutableMapOf() private val storedAuthenticationSyncKeys = mutableMapOf() private val storedPolicies = mutableMapOf?>() + private val storedOnboardingStatus = mutableMapOf() override var userState: UserStateJson? = null set(value) { @@ -250,6 +255,18 @@ class FakeAuthDiskSource : AuthDiskSource { getMutableAccountTokensFlow(userId = userId).tryEmit(accountTokens) } + override fun getOnboardingStatus(userId: String): OnboardingStatus? = + storedOnboardingStatus[userId] + + override fun storeOnboardingStatus(userId: String, onboardingStatus: OnboardingStatus?) { + storedOnboardingStatus[userId] = onboardingStatus + getMutableOnboardingStatusFlow(userId = userId).tryEmit(onboardingStatus) + } + + override fun getOnboardingStatusFlow(userId: String): Flow = + getMutableOnboardingStatusFlow(userId = userId) + .onSubscription { emit(getOnboardingStatus(userId)) } + /** * Assert the the [isTdeLoginComplete] was stored successfully using the [userId]. */ @@ -423,5 +440,12 @@ class FakeAuthDiskSource : AuthDiskSource { bufferedMutableSharedFlow(replay = 1) } + private fun getMutableOnboardingStatusFlow( + userId: String, + ): MutableSharedFlow = + mutableOnboardingStatusFlowMap.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 640044c5d..21ba7c0a1 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 @@ -16,6 +16,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource @@ -233,7 +234,10 @@ class AuthRepositoryTest { getActivePoliciesFlow(type = PolicyTypeJson.MASTER_PASSWORD) } returns mutableActivePolicyFlow } - private val featureFlagManager: FeatureFlagManager = mockk() + + private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) { + every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false + } private val repository = AuthRepositoryImpl( accountsService = accountsService, @@ -336,6 +340,7 @@ class AuthRepositoryTest { userOrganizationsList = emptyList(), userIsUsingKeyConnectorList = emptyList(), hasPendingAccountAddition = false, + onboardingStatus = null, isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, @@ -364,6 +369,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.PIN }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ), repository.userStateFlow.value, ) @@ -380,6 +386,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.PIN }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ), repository.userStateFlow.value, ) @@ -408,6 +415,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ), repository.userStateFlow.value, ) @@ -636,6 +644,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ) val finalUserState = SINGLE_USER_STATE_2.toUserState( vaultState = VAULT_UNLOCK_DATA, @@ -646,6 +655,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ) val kdf = SINGLE_USER_STATE_1.activeAccount.profile.toSdkParams() coEvery { @@ -5347,6 +5357,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ) fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 assertEquals( @@ -5380,6 +5391,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ) fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 assertEquals( @@ -5411,6 +5423,7 @@ class AuthRepositoryTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = null, ) fakeAuthDiskSource.userState = MULTI_USER_STATE assertEquals( @@ -6095,6 +6108,197 @@ class AuthRepositoryTest { ) } + @Test + fun `setOnboardingStatus should save the onboarding status to disk`() { + val userId = "userId" + repository.setOnboardingStatus(userId = userId, status = OnboardingStatus.NOT_STARTED) + assertEquals(OnboardingStatus.NOT_STARTED, fakeAuthDiskSource.getOnboardingStatus(userId)) + } + + @Test + @Suppress("MaxLineLength") + fun `on successful login a new user should have onboarding status set if feature flag is on and has not previously logged in`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + identityService.preLogin(email = EMAIL) + } returns PRE_LOGIN_SUCCESS.asSuccess() + every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) } returns true + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = PASSWORD, + userKey = successResponse.key!!, + ), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + ) + } returns VaultUnlockResult.Success + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + every { settingsRepository.getUserHasLoggedInValue(USER_ID_1) } returns false + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + coVerify { identityService.preLogin(email = EMAIL) } + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", + ) + fakeAuthDiskSource.assertMasterPasswordHash( + userId = USER_ID_1, + passwordHash = PASSWORD_HASH, + ) + assertEquals( + OnboardingStatus.NOT_STARTED, + fakeAuthDiskSource.getOnboardingStatus(USER_ID_1), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `on successful login does not set onboarding status if feature flag is off`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + identityService.preLogin(email = EMAIL) + } returns PRE_LOGIN_SUCCESS.asSuccess() + every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) } returns false + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = PASSWORD, + userKey = successResponse.key!!, + ), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + ) + } returns VaultUnlockResult.Success + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + coVerify { identityService.preLogin(email = EMAIL) } + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", + ) + fakeAuthDiskSource.assertMasterPasswordHash( + userId = USER_ID_1, + passwordHash = PASSWORD_HASH, + ) + verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } + assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1)) + } + + @Suppress("MaxLineLength") + @Test + fun `on successful login does not set onboarding status if feature flag is on but user has previously logged in`() = + runTest { + val successResponse = GET_TOKEN_RESPONSE_SUCCESS + coEvery { + identityService.preLogin(email = EMAIL) + } returns PRE_LOGIN_SUCCESS.asSuccess() + every { featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow) } returns true + coEvery { + identityService.getToken( + email = EMAIL, + authModel = IdentityTokenAuthModel.MasterPassword( + username = EMAIL, + password = PASSWORD_HASH, + ), + captchaToken = null, + uniqueAppId = UNIQUE_APP_ID, + ) + } returns successResponse.asSuccess() + coEvery { + vaultRepository.unlockVault( + userId = USER_ID_1, + email = EMAIL, + kdf = ACCOUNT_1.profile.toSdkParams(), + initUserCryptoMethod = InitUserCryptoMethod.Password( + password = PASSWORD, + userKey = successResponse.key!!, + ), + privateKey = successResponse.privateKey!!, + organizationKeys = null, + ) + } returns VaultUnlockResult.Success + coEvery { vaultRepository.syncIfNecessary() } just runs + every { + GET_TOKEN_RESPONSE_SUCCESS.toUserState( + previousUserState = null, + environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US, + ) + } returns SINGLE_USER_STATE_1 + every { settingsRepository.getUserHasLoggedInValue(USER_ID_1) } returns true + val result = repository.login(email = EMAIL, password = PASSWORD, captchaToken = null) + assertEquals(LoginResult.Success, result) + assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value) + coVerify { identityService.preLogin(email = EMAIL) } + fakeAuthDiskSource.assertPrivateKey( + userId = USER_ID_1, + privateKey = "privateKey", + ) + fakeAuthDiskSource.assertUserKey( + userId = USER_ID_1, + userKey = "key", + ) + fakeAuthDiskSource.assertMasterPasswordHash( + userId = USER_ID_1, + passwordHash = PASSWORD_HASH, + ) + verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) } + assertNull(fakeAuthDiskSource.getOnboardingStatus(USER_ID_1)) + } + companion object { private const val UNIQUE_APP_ID = "testUniqueAppId" private const val NAME = "Example Name" 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 8b45f2858..f91e231cc 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 @@ -4,6 +4,7 @@ import app.cash.turbine.test import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource import com.x8bit.bitwarden.data.auth.repository.model.Organization @@ -472,6 +473,39 @@ class AuthDiskSourceExtensionsTest { assertNull(awaitItem()) } } + + @Test + fun `userStateChangesFlow should emit changes when user state changes`() = runTest { + authDiskSource.storeOnboardingStatus(MOCK_USER_ID, OnboardingStatus.NOT_STARTED) + authDiskSource.onboardingStatusChangesFlow.test { + assertNull(awaitItem()) + authDiskSource.userState = MOCK_USER_STATE + assertEquals( + + OnboardingStatus.NOT_STARTED, + awaitItem(), + ) + authDiskSource.userState = MOCK_USER_STATE.copy( + accounts = mapOf( + MOCK_USER_ID to MOCK_ACCOUNT, + "mockId-2" to mockk(), + ), + ) + expectNoEvents() + authDiskSource.userState = null + assertNull(awaitItem()) + } + } + + @Test + fun `currentOnboardingStatus should return the current onboarding status`() { + authDiskSource.storeOnboardingStatus(MOCK_USER_ID, OnboardingStatus.COMPLETE) + authDiskSource.userState = MOCK_USER_STATE + assertEquals( + OnboardingStatus.COMPLETE, + authDiskSource.currentOnboardingStatus, + ) + } } 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 3b4d849f0..a40e338e2 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 @@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson @@ -23,9 +24,12 @@ import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +@Suppress("LargeClass") class UserStateJsonExtensionsTest { + + @Suppress("MaxLineLength") @Test - fun `toUpdatedUserStateJn should do nothing for a non-matching account`() { + fun `toUpdatedUserStateJson should do nothing for a non-matching account using toRemovedPasswordUserStateJson`() { val originalUserState = UserStateJson( activeUserId = "activeUserId", accounts = mapOf("activeUserId" to mockk()), @@ -38,7 +42,7 @@ class UserStateJsonExtensionsTest { @Suppress("MaxLineLength") @Test - fun `toUpdatedUserStateJn should create user decryption options without a password if not present`() { + fun `toUpdatedUserStateJson should create user decryption options without a password if not present`() { val originalProfile = AccountJson.Profile( userId = "activeUserId", email = "email", @@ -85,7 +89,7 @@ class UserStateJsonExtensionsTest { } @Test - fun `toUpdatedUserStateJn should update user decryption options to not have a password`() { + fun `toUpdatedUserStateJson should update user decryption options to not have a password`() { val originalProfile = AccountJson.Profile( userId = "activeUserId", email = "email", @@ -353,6 +357,7 @@ class UserStateJsonExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.NOT_STARTED, ), ), ), @@ -421,6 +426,7 @@ class UserStateJsonExtensionsTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.PIN }, isDeviceTrustedProvider = { false }, + onboardingStatus = OnboardingStatus.NOT_STARTED, ), ) } @@ -457,6 +463,7 @@ class UserStateJsonExtensionsTest { trustedDevice = null, hasMasterPassword = false, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.NOT_STARTED, ), ), hasPendingAccountAddition = true, @@ -521,6 +528,7 @@ class UserStateJsonExtensionsTest { isBiometricsEnabledProvider = { true }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { false }, + onboardingStatus = OnboardingStatus.NOT_STARTED, ), ) } @@ -563,6 +571,7 @@ class UserStateJsonExtensionsTest { ), hasMasterPassword = false, isUsingKeyConnector = true, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), hasPendingAccountAddition = true, @@ -630,6 +639,229 @@ class UserStateJsonExtensionsTest { isBiometricsEnabledProvider = { false }, vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, isDeviceTrustedProvider = { true }, + onboardingStatus = null, + ), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toUserState should set the correct onboarding 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, + ), + ), + 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, + ), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toUserState should set the default value of onboarding to COMPLETE when passed value is null`() { + 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.COMPLETE, + ), + ), + 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 = null, ), ) } 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 45a5a3c8e..2e7ac68a4 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 @@ -22,6 +22,7 @@ import androidx.credentials.provider.BeginGetPublicKeyCredentialOption import androidx.credentials.provider.PublicKeyCredentialEntry import com.bitwarden.sdk.Fido2CredentialStore import com.bitwarden.vault.CipherView +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType @@ -532,6 +533,7 @@ private fun createMockAccounts(number: Int): List { vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt index bab5ee18b..b96e69ed4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt @@ -60,15 +60,6 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { verify { settingsRepository.disableAutofill() } } - @Test - fun `handleContinueClick sends NavigateToCompleteSetup event`() = runTest { - val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(SetupAutoFillAction.ContinueClick) - assertEquals(SetupAutoFillEvent.NavigateToCompleteSetup, awaitItem()) - } - } - @Test fun `handleTurnOnLater click sets dialogState to TurnOnLaterDialog`() { val viewModel = createViewModel() @@ -79,15 +70,6 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { ) } - @Test - fun `handleTurnOnLaterConfirmClick sends NavigateToCompleteSetup event`() = runTest { - val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick) - assertEquals(SetupAutoFillEvent.NavigateToCompleteSetup, awaitItem()) - } - } - @Test fun `handleDismissDialog sets dialogState to null`() { val viewModel = createViewModel() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreenTest.kt index 72cb07ada..e53e98de4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutofillScreenTest.kt @@ -17,13 +17,11 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test class SetupAutofillScreenTest : BaseComposeTest() { - private var onNavigateToCompleteSetupCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -38,7 +36,6 @@ class SetupAutofillScreenTest : BaseComposeTest() { fun setup() { composeTestRule.setContent { SetupAutoFillScreen( - onNavigateToCompleteSetup = { onNavigateToCompleteSetupCalled = true }, intentManager = intentManager, viewModel = viewModel, ) @@ -109,12 +106,6 @@ class SetupAutofillScreenTest : BaseComposeTest() { verify { viewModel.trySendAction(SetupAutoFillAction.AutoFillServiceFallback) } } - @Test - fun `NavigateToCompleteSetup should call onNavigateToCompleteSetup`() { - mutableEventFlow.tryEmit(SetupAutoFillEvent.NavigateToCompleteSetup) - assertTrue(onNavigateToCompleteSetupCalled) - } - @Test fun `Show autofill fallback dialog when dialog state is AutoFillFallbackDialog`() { mutableStateFlow.update { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt index 938c03130..e9891b1df 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockScreenTest.kt @@ -26,7 +26,6 @@ import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.robolectric.annotation.Config @@ -34,8 +33,6 @@ import javax.crypto.Cipher class SetupUnlockScreenTest : BaseComposeTest() { - private var onNavigateToSetupAutofillCalled = false - private val captureBiometricsSuccess = slot<(cipher: Cipher?) -> Unit>() private val captureBiometricsCancel = slot<() -> Unit>() private val captureBiometricsLockOut = slot<() -> Unit>() @@ -65,7 +62,6 @@ class SetupUnlockScreenTest : BaseComposeTest() { fun setup() { composeTestRule.setContent { SetupUnlockScreen( - onNavigateToSetupAutofill = { onNavigateToSetupAutofillCalled = true }, viewModel = viewModel, biometricsManager = biometricsManager, ) @@ -91,12 +87,6 @@ class SetupUnlockScreenTest : BaseComposeTest() { .assertIsDisplayed() } - @Test - fun `NavigateToSetupAutofill event should invoke the navigate to autofill lambda`() { - mutableEventFlow.tryEmit(SetupUnlockEvent.NavigateToSetupAutofill) - assertTrue(onNavigateToSetupAutofillCalled) - } - @Test fun `on unlock with biometrics should be toggled on or off according to state`() { composeTestRule.onNodeWithText(text = "Unlock with Biometrics").assertIsOff() 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 c123c1850..655b5206b 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.accountsetup import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager @@ -30,6 +31,7 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) private val authRepository: AuthRepository = mockk { every { userStateFlow } returns mutableUserStateFlow + every { setOnboardingStatus(userId = any(), status = any()) } just runs } private val settingsRepository = mockk { every { isUnlockWithPinEnabled } returns false @@ -50,20 +52,26 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { } @Test - fun `ContinueClick should emit NavigateToSetupAutofill`() = runTest { + fun `ContinueClick should call setOnboardingStatus`() = runTest { val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(SetupUnlockAction.ContinueClick) - assertEquals(SetupUnlockEvent.NavigateToSetupAutofill, awaitItem()) + viewModel.trySendAction(SetupUnlockAction.ContinueClick) + verify { + authRepository.setOnboardingStatus( + userId = DEFAULT_USER_ID, + status = OnboardingStatus.AUTOFILL_SETUP, + ) } } @Test - fun `SetUpLaterClick should emit NavigateToSetupAutofill`() = runTest { + fun `SetUpLaterClick should call setOnboardingStatus`() = runTest { val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) - assertEquals(SetupUnlockEvent.NavigateToSetupAutofill, awaitItem()) + viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) + verify { + authRepository.setOnboardingStatus( + userId = DEFAULT_USER_ID, + status = OnboardingStatus.AUTOFILL_SETUP, + ) } } @@ -302,6 +310,7 @@ private val DEFAULT_USER_STATE: UserState = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 0feb64ec4..994563228 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.landing import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType @@ -86,6 +87,7 @@ class LandingViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -222,6 +224,7 @@ class LandingViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) val userState = UserState( activeUserId = "activeUserId", @@ -277,6 +280,7 @@ class LandingViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) val userState = UserState( activeUserId = "activeUserId", @@ -336,6 +340,7 @@ class LandingViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) val userState = UserState( activeUserId = "activeUserId", @@ -511,6 +516,7 @@ class LandingViewModelTest : BaseViewModelTest() { vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) val userState = UserState( @@ -545,6 +551,7 @@ class LandingViewModelTest : BaseViewModelTest() { vaultUnlockType = VaultUnlockType.MASTER_PASSWORD, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 c284aff3c..f06944ada 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult import com.x8bit.bitwarden.data.auth.repository.model.LoginResult @@ -130,6 +131,7 @@ class LoginViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 a0b065ff1..9533159a6 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.removepassword import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult @@ -165,6 +166,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 f20fed662..a2051517d 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.trusteddevice import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.NewSsoUserResult @@ -274,6 +275,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( trustedDevice = TRUSTED_DEVICE, hasMasterPassword = false, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 eeba7f98e..fe21ed0bd 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus 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 @@ -215,6 +216,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -253,6 +255,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -1056,6 +1059,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) private val DEFAULT_USER_STATE = UserState( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt index 9736a2243..dc58dc69c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt @@ -225,6 +225,16 @@ class RootNavScreenTest : BaseComposeTest() { navOptions = expectedNavOptions, ) } + + // Make sure navigating to onboarding graph works as expected: + rootNavStateFlow.value = + RootNavState.OnboardingAccountLockSetup + composeTestRule.runOnIdle { + fakeNavHostController.assertLastNavigation( + route = "setup_unlock", + navOptions = expectedNavOptions, + ) + } } } 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 873c9947d..7c1ddcc62 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import android.content.pm.SigningInfo +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.AuthState import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson @@ -104,6 +105,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -137,6 +139,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -167,6 +170,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -207,6 +211,7 @@ class RootNavViewModelTest : BaseViewModelTest() { ), hasMasterPassword = false, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -244,6 +249,7 @@ class RootNavViewModelTest : BaseViewModelTest() { ), hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -281,6 +287,7 @@ class RootNavViewModelTest : BaseViewModelTest() { ), hasMasterPassword = false, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -315,6 +322,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), hasPendingAccountAddition = true, @@ -362,6 +370,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -393,6 +402,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -432,6 +442,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -471,6 +482,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -517,6 +529,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -563,6 +576,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -604,6 +618,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -644,6 +659,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -719,6 +735,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -769,6 +786,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -827,6 +845,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -865,6 +884,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -898,6 +918,7 @@ class RootNavViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ), @@ -906,6 +927,216 @@ class RootNavViewModelTest : BaseViewModelTest() { assertEquals(RootNavState.VaultLocked, viewModel.stateFlow.value) } + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of NOT_STARTED the nav state should be OnboardingAccountLockSetup`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.NOT_STARTED, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.OnboardingAccountLockSetup, + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of ACCOUNT_LOCK_SETUP the nav state should be OnboardingAccountLockSetup`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.ACCOUNT_LOCK_SETUP, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.OnboardingAccountLockSetup, + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of AUTOFILL_SETUP the nav state should be OnboardingAutoFillSetup`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.AUTOFILL_SETUP, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.OnboardingAutoFillSetup, + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `when the active user has an locked vault and they have a OnboardingStatus of NOT_STARTED the nav state should be VaultLocked`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = false, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.NOT_STARTED, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultLocked, + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of null the nav state should be VaultUnlocked`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultUnlocked(activeUserId = "activeUserId"), + viewModel.stateFlow.value, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `when the active user has an unlocked vault and they have a OnboardingStatus of COMPLETED the nav state should be VaultUnlocked`() { + mutableUserStateFlow.tryEmit( + UserState( + activeUserId = "activeUserId", + accounts = listOf( + UserState.Account( + userId = "activeUserId", + name = "name", + email = "email", + avatarColorHex = "avatarColorHex", + environment = Environment.Us, + isPremium = true, + isLoggedIn = true, + isVaultUnlocked = true, + needsPasswordReset = false, + isBiometricsEnabled = false, + organizations = emptyList(), + needsMasterPassword = false, + trustedDevice = null, + hasMasterPassword = true, + isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, + ), + ), + ), + ) + val viewModel = createViewModel() + assertEquals( + RootNavState.VaultUnlocked(activeUserId = "activeUserId"), + viewModel.stateFlow.value, + ) + } + private fun createViewModel(): RootNavViewModel = RootNavViewModel( authRepository = authRepository, 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 8591ea0e0..9ab363747 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 @@ -7,6 +7,7 @@ import app.cash.turbine.turbineScope import com.bitwarden.vault.CipherView import com.bitwarden.vault.LoginUriView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult @@ -1424,6 +1425,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 de803e9a0..c8a4a56cf 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult @@ -697,6 +698,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 29d2fecf3..7e758b329 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deletea import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -242,6 +243,7 @@ private val DEFAULT_USER_STATE: UserState = UserState( ), hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 8349c9d2c..30d0e77db 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 @@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.manager.model.AuthRequest import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult @@ -370,6 +371,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 cfecfc57d..74a09f37f 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.exporters.ExportFormat import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult @@ -722,6 +723,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 1605764a8..aa930a4c9 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 @@ -5,6 +5,7 @@ import app.cash.turbine.test import app.cash.turbine.turbineScope import com.bitwarden.generators.PasswordGeneratorRequest import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -2406,6 +2407,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 3d1dd75ee..f73487399 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.send.SendView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -1101,6 +1102,7 @@ class AddSendViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 1f1472f00..d54c7e6ab 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 @@ -10,6 +10,7 @@ import com.bitwarden.vault.CollectionView import com.bitwarden.vault.FolderView import com.bitwarden.vault.UriMatchType import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.Organization @@ -3897,6 +3898,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), 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 4ab3d46b9..2386f8b52 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 @@ -13,6 +13,7 @@ import com.bitwarden.vault.PasswordHistoryView import com.bitwarden.vault.SecureNoteType import com.bitwarden.vault.SecureNoteView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType @@ -446,6 +447,7 @@ class CipherViewExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) } 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 75abe4b69..41ba591ae 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.vault.CipherView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.DataState @@ -562,6 +563,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 2f73e1e81..7d154ad54 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 @@ -6,6 +6,7 @@ import app.cash.turbine.test import app.cash.turbine.turbineScope import com.bitwarden.vault.CipherView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -2579,6 +2580,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 404314759..1fa26ab36 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 @@ -7,6 +7,7 @@ import app.cash.turbine.test import com.bitwarden.vault.CipherRepromptType import com.bitwarden.vault.CipherView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus 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 @@ -3806,6 +3807,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) 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 2798e4ef8..625d63bfd 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 @@ -5,6 +5,7 @@ import app.cash.turbine.test import com.bitwarden.vault.CipherView import com.bitwarden.vault.CollectionView import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState @@ -513,6 +514,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 f6cb313ec..26f756fda 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -128,6 +129,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState = trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 10dd52dc4..97904d533 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 @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault import app.cash.turbine.test import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult @@ -196,6 +197,7 @@ class VaultViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -280,6 +282,7 @@ class VaultViewModelTest : BaseViewModelTest() { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -1529,6 +1532,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), UserState.Account( userId = "lockedUserId", @@ -1546,6 +1550,7 @@ private val DEFAULT_USER_STATE = UserState( trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) 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 4ac0fac7b..45bb6315b 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 @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson +import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.model.Organization import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -85,6 +86,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), UserState.Account( userId = "lockedUserId", @@ -110,6 +112,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), UserState.Account( userId = "unlockedUserId", @@ -139,6 +142,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), UserState.Account( userId = "loggedOutUserId", @@ -168,6 +172,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -212,6 +217,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) .toAccountSummary(isActive = true), ) @@ -254,6 +260,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) .toAccountSummary(isActive = false), ) @@ -300,6 +307,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ), ), ) @@ -326,6 +334,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) .toVaultFilterData(isIndividualVaultDisabled = false), ) @@ -381,6 +390,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) .toVaultFilterData( isIndividualVaultDisabled = false, @@ -437,6 +447,7 @@ class UserStateExtensionsTest { trustedDevice = null, hasMasterPassword = true, isUsingKeyConnector = false, + onboardingStatus = OnboardingStatus.COMPLETE, ) .toVaultFilterData( isIndividualVaultDisabled = true,