PM-12076 remaining state based navigation linkup for onboarding (#3923)

This commit is contained in:
Dave Severns 2024-09-17 13:51:06 -04:00 committed by GitHub
parent 503c966177
commit 37f1da1ec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 166 additions and 8 deletions

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
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.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
@ -16,9 +18,13 @@ import javax.inject.Inject
@HiltViewModel
class SetupAutoFillViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val authRepository: AuthRepository,
) :
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
initialState = SetupAutoFillState(dialogState = null, autofillEnabled = false),
initialState = run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
SetupAutoFillState(userId = userId, dialogState = null, autofillEnabled = false)
},
) {
init {
@ -73,11 +79,11 @@ class SetupAutoFillViewModel @Inject constructor(
private fun handleTurnOnLaterConfirmClick() {
// TODO PM-10631 record user chose to turn on later for settings badging.
// TODO PM-10632 update status to complete setup step.
updateOnboardingStatusToNextStep()
}
private fun handleContinueClick() {
// TODO PM-10632 update status to complete setup step.
updateOnboardingStatusToNextStep()
}
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
@ -87,12 +93,20 @@ class SetupAutoFillViewModel @Inject constructor(
settingsRepository.disableAutofill()
}
}
private fun updateOnboardingStatusToNextStep() =
authRepository
.setOnboardingStatus(
userId = state.userId,
status = OnboardingStatus.FINAL_STEP,
)
}
/**
* UI State for the Auto-fill setup screen.
*/
data class SetupAutoFillState(
val userId: String,
val dialogState: SetupAutoFillDialogState?,
val autofillEnabled: Boolean,
)

View file

@ -0,0 +1,29 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
/**
* Route name for [SetupCompleteScreen].
*/
const val SETUP_COMPLETE_ROUTE = "setup_complete"
/**
* Navigate to the setup complete screen.
*/
fun NavController.navigateToSetupCompleteScreen(navOptions: NavOptions? = null) {
this.navigate(SETUP_COMPLETE_ROUTE, navOptions)
}
/**
* Add the setup complete screen to the nav graph.
*/
fun NavGraphBuilder.setupCompleteDestination() {
composableWithPushTransitions(
route = SETUP_COMPLETE_ROUTE,
) {
SetupCompleteScreen()
}
}

View file

@ -16,10 +16,13 @@ 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_COMPLETE_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.navigateToSetupCompleteScreen
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.setupCompleteDestination
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
@ -98,6 +101,7 @@ fun RootNavScreen(
setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() })
setupUnlockDestination()
setupAutoFillDestination()
setupCompleteDestination()
}
val targetRoute = when (state) {
@ -125,6 +129,7 @@ fun RootNavScreen(
RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_ROUTE
RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_ROUTE
RootNavState.OnboardingStepsComplete -> SETUP_COMPLETE_ROUTE
}
val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -236,6 +241,10 @@ fun RootNavScreen(
RootNavState.OnboardingAutoFillSetup -> {
navController.navigateToSetupAutoFillScreen(rootNavOptions)
}
RootNavState.OnboardingStepsComplete -> {
navController.navigateToSetupCompleteScreen(rootNavOptions)
}
}
}
}

View file

@ -97,8 +97,8 @@ class RootNavViewModel @Inject constructor(
OnboardingStatus.ACCOUNT_LOCK_SETUP,
-> RootNavState.OnboardingAccountLockSetup
OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup
OnboardingStatus.FINAL_STEP -> RootNavState.OnboardingStepsComplete
OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.")
OnboardingStatus.FINAL_STEP -> TODO("PM-12076 complete navigation wiring")
}
}
@ -345,6 +345,12 @@ sealed class RootNavState : Parcelable {
*/
@Parcelize
data object OnboardingAutoFillSetup : RootNavState()
/**
* App should show the onboarding steps complete screen.
*/
@Parcelize
data object OnboardingStepsComplete : RootNavState()
}
/**

View file

@ -1,6 +1,9 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import app.cash.turbine.test
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.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
@ -24,6 +27,15 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
every { disableAutofill() } just runs
}
private val mockUserState = mockk<UserState> {
every { activeUserId } returns DEFAULT_USER_ID
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(mockUserState)
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(any(), any()) } just runs
}
@Test
fun `handleAutofillEnabledUpdateReceive updates autofillEnabled state`() {
val viewModel = createViewModel()
@ -92,5 +104,31 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
)
}
private fun createViewModel() = SetupAutoFillViewModel(settingsRepository)
@Test
fun `handleTurnOnLaterConfirmClick sets onboarding status to FINAL_STEP`() {
val viewModel = createViewModel()
viewModel.trySendAction(SetupAutoFillAction.TurnOnLaterConfirmClick)
verify {
authRepository.setOnboardingStatus(
DEFAULT_USER_ID,
OnboardingStatus.FINAL_STEP,
)
}
}
@Test
fun `handleContinueClick sets onboarding status to FINAL_STEP`() {
val viewModel = createViewModel()
viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
verify {
authRepository.setOnboardingStatus(
DEFAULT_USER_ID,
OnboardingStatus.FINAL_STEP,
)
}
}
private fun createViewModel() = SetupAutoFillViewModel(settingsRepository, authRepository)
}
private const val DEFAULT_USER_ID = "userId"

View file

@ -208,4 +208,8 @@ class SetupAutofillScreenTest : BaseComposeTest() {
}
}
private val DEFAULT_STATE = SetupAutoFillState(dialogState = null, autofillEnabled = false)
private val DEFAULT_STATE = SetupAutoFillState(
userId = "userId",
dialogState = null,
autofillEnabled = false,
)

View file

@ -5,7 +5,9 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.assertEquals
@ -18,8 +20,9 @@ class SetupCompleteViewModelTest : BaseViewModelTest() {
every { activeUserId } returns DEFAULT_USER_ID
}
private val mutableUserStateFlow = MutableStateFlow<UserState?>(mockUserState)
private val authRepository: AuthRepository = mockk(relaxed = true) {
private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(any(), any()) } just runs
}
@Test

View file

@ -226,7 +226,7 @@ class RootNavScreenTest : BaseComposeTest() {
)
}
// Make sure navigating to onboarding graph works as expected:
// Make sure navigating to account lock setup works as expected:
rootNavStateFlow.value =
RootNavState.OnboardingAccountLockSetup
composeTestRule.runOnIdle {
@ -235,6 +235,26 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions,
)
}
// Make sure navigating to account autofill setup works as expected:
rootNavStateFlow.value =
RootNavState.OnboardingAutoFillSetup
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "setup_auto_fill",
navOptions = expectedNavOptions,
)
}
// Make sure navigating to account setup complete works as expected:
rootNavStateFlow.value =
RootNavState.OnboardingStepsComplete
composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation(
route = "setup_complete",
navOptions = expectedNavOptions,
)
}
}
}

View file

@ -1032,6 +1032,41 @@ class RootNavViewModelTest : BaseViewModelTest() {
)
}
@Suppress("MaxLineLength")
@Test
fun `when the active user has an unlocked vault and they have a OnboardingStatus of FINAL_STEP 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.FINAL_STEP,
),
),
),
)
val viewModel = createViewModel()
assertEquals(
RootNavState.OnboardingStepsComplete,
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`() {