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 package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.viewModelScope 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.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -16,9 +18,13 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SetupAutoFillViewModel @Inject constructor( class SetupAutoFillViewModel @Inject constructor(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val authRepository: AuthRepository,
) : ) :
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>( 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 { init {
@ -73,11 +79,11 @@ class SetupAutoFillViewModel @Inject constructor(
private fun handleTurnOnLaterConfirmClick() { private fun handleTurnOnLaterConfirmClick() {
// TODO PM-10631 record user chose to turn on later for settings badging. // 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() { private fun handleContinueClick() {
// TODO PM-10632 update status to complete setup step. updateOnboardingStatusToNextStep()
} }
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) { private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
@ -87,12 +93,20 @@ class SetupAutoFillViewModel @Inject constructor(
settingsRepository.disableAutofill() settingsRepository.disableAutofill()
} }
} }
private fun updateOnboardingStatusToNextStep() =
authRepository
.setOnboardingStatus(
userId = state.userId,
status = OnboardingStatus.FINAL_STEP,
)
} }
/** /**
* UI State for the Auto-fill setup screen. * UI State for the Auto-fill setup screen.
*/ */
data class SetupAutoFillState( data class SetupAutoFillState(
val userId: String,
val dialogState: SetupAutoFillDialogState?, val dialogState: SetupAutoFillDialogState?,
val autofillEnabled: Boolean, 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.compose.rememberNavController
import androidx.navigation.navOptions 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_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.SETUP_UNLOCK_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillScreen 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.navigateToSetupUnlockScreen
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestination 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.accountsetup.setupUnlockDestination
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE 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.authGraph
@ -98,6 +101,7 @@ fun RootNavScreen(
setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() }) setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() })
setupUnlockDestination() setupUnlockDestination()
setupAutoFillDestination() setupAutoFillDestination()
setupCompleteDestination()
} }
val targetRoute = when (state) { val targetRoute = when (state) {
@ -125,6 +129,7 @@ fun RootNavScreen(
RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_ROUTE RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_ROUTE
RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_ROUTE RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_ROUTE
RootNavState.OnboardingStepsComplete -> SETUP_COMPLETE_ROUTE
} }
val currentRoute = navController.currentDestination?.rootLevelRoute() val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -236,6 +241,10 @@ fun RootNavScreen(
RootNavState.OnboardingAutoFillSetup -> { RootNavState.OnboardingAutoFillSetup -> {
navController.navigateToSetupAutoFillScreen(rootNavOptions) navController.navigateToSetupAutoFillScreen(rootNavOptions)
} }
RootNavState.OnboardingStepsComplete -> {
navController.navigateToSetupCompleteScreen(rootNavOptions)
}
} }
} }
} }

View file

@ -97,8 +97,8 @@ class RootNavViewModel @Inject constructor(
OnboardingStatus.ACCOUNT_LOCK_SETUP, OnboardingStatus.ACCOUNT_LOCK_SETUP,
-> RootNavState.OnboardingAccountLockSetup -> RootNavState.OnboardingAccountLockSetup
OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup OnboardingStatus.AUTOFILL_SETUP -> RootNavState.OnboardingAutoFillSetup
OnboardingStatus.FINAL_STEP -> RootNavState.OnboardingStepsComplete
OnboardingStatus.COMPLETE -> throw IllegalStateException("Should not have entered here.") 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 @Parcelize
data object OnboardingAutoFillSetup : RootNavState() 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 package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import app.cash.turbine.test 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.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every import io.mockk.every
@ -24,6 +27,15 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
every { disableAutofill() } just runs 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 @Test
fun `handleAutofillEnabledUpdateReceive updates autofillEnabled state`() { fun `handleAutofillEnabledUpdateReceive updates autofillEnabled state`() {
val viewModel = createViewModel() 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.data.auth.repository.model.UserState
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@ -18,8 +20,9 @@ class SetupCompleteViewModelTest : BaseViewModelTest() {
every { activeUserId } returns DEFAULT_USER_ID every { activeUserId } returns DEFAULT_USER_ID
} }
private val mutableUserStateFlow = MutableStateFlow<UserState?>(mockUserState) private val mutableUserStateFlow = MutableStateFlow<UserState?>(mockUserState)
private val authRepository: AuthRepository = mockk(relaxed = true) { private val authRepository: AuthRepository = mockk {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
every { setOnboardingStatus(any(), any()) } just runs
} }
@Test @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 = rootNavStateFlow.value =
RootNavState.OnboardingAccountLockSetup RootNavState.OnboardingAccountLockSetup
composeTestRule.runOnIdle { composeTestRule.runOnIdle {
@ -235,6 +235,26 @@ class RootNavScreenTest : BaseComposeTest() {
navOptions = expectedNavOptions, 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") @Suppress("MaxLineLength")
@Test @Test
fun `when the active user has an locked vault and they have a OnboardingStatus of NOT_STARTED the nav state should be VaultLocked`() { fun `when the active user has an locked vault and they have a OnboardingStatus of NOT_STARTED the nav state should be VaultLocked`() {