PM-11464 Add onboarding status to user Account to allow for root navigation to onboarding flow. (#3878)

This commit is contained in:
Dave Severns 2024-09-16 12:40:56 -04:00 committed by GitHub
parent 3ecf1382b2
commit 759e926588
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1074 additions and 81 deletions

View file

@ -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<OnboardingStatus?>
}

View file

@ -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<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
private val mutableAccountTokensFlowMap =
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(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<OnboardingStatus?> {
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<OnboardingStatus?> =
mutableOnboardingStatusFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableShouldUseKeyConnectorFlowMap(
userId: String,
): MutableSharedFlow<Boolean?> =

View file

@ -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,
}

View file

@ -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?)
}

View file

@ -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<UserAccountTokens>
val userOrganizationsList = array[2] as List<UserOrganizations>
val userIsUsingKeyConnectorList = array[3] as List<UserKeyConnectorState>
val vaultState = array[4] as List<VaultUnlockData>
val hasPendingAccountAddition = array[5] as Boolean
val onboardingStatus = array[4] as OnboardingStatus?
val vaultState = array[5] as List<VaultUnlockData>
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

View file

@ -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.

View file

@ -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<String?>
.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<OnboardingStatus?>
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) }

View file

@ -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<UserOrganizations>,
userIsUsingKeyConnectorList: List<UserKeyConnectorState>,
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,

View file

@ -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()
}
}

View file

@ -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.

View file

@ -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) {

View file

@ -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()
}
}

View file

@ -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(

View file

@ -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].

View file

@ -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)
}
}
}
}

View file

@ -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()
}
/**

View file

@ -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(

View file

@ -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 = """

View file

@ -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<String, MutableSharedFlow<List<SyncResponseJson.Policy>?>>()
private val mutableAccountTokensFlowMap =
mutableMapOf<String, MutableSharedFlow<AccountTokensJson?>>()
private val mutableOnboardingStatusFlowMap =
mutableMapOf<String, MutableSharedFlow<OnboardingStatus?>>()
private val mutableUserStateFlow = bufferedMutableSharedFlow<UserStateJson?>(replay = 1)
private val storedShouldUseKeyConnector = mutableMapOf<String, Boolean?>()
@ -48,6 +52,7 @@ class FakeAuthDiskSource : AuthDiskSource {
private val storedMasterPasswordHashes = mutableMapOf<String, String?>()
private val storedAuthenticationSyncKeys = mutableMapOf<String, String?>()
private val storedPolicies = mutableMapOf<String, List<SyncResponseJson.Policy>?>()
private val storedOnboardingStatus = mutableMapOf<String, OnboardingStatus?>()
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<OnboardingStatus?> =
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<OnboardingStatus?> =
mutableOnboardingStatusFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
//endregion Private helper functions
}

View file

@ -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"

View file

@ -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"

View file

@ -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,
),
)
}

View file

@ -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<UserState.Account> {
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
hasMasterPassword = true,
isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.COMPLETE,
),
)
}

View file

@ -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()

View file

@ -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<SetupAutoFillEvent>()
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 {

View file

@ -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()

View file

@ -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<UserState?>(DEFAULT_USER_STATE)
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(userId = any(), status = any()) } just runs
}
private val settingsRepository = mockk<SettingsRepository> {
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())
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())
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,
),
),
)

View file

@ -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(

View file

@ -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,
),
),
)

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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,
)
}
}
}

View file

@ -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,

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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(

View file

@ -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,

View file

@ -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,
)
}

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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(

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,
),
),
)

View file

@ -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,