PM-13068 Navigate from settings to setup autofill screen. (#4034)

This commit is contained in:
Dave Severns 2024-10-08 10:29:40 -04:00 committed by GitHub
parent bc057932a0
commit 641a48fe44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 301 additions and 44 deletions

View file

@ -1,29 +1,79 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
/** /**
* Route name for [SetupAutoFillScreen]. * Route constant for navigating to the [SetupAutoFillScreen].
*/ */
const val SETUP_AUTO_FILL_ROUTE = "setup_auto_fill" private const val SETUP_AUTO_FILL_PREFIX = "setup_auto_fill"
private const val SETUP_AUTO_FILL_AS_ROOT_PREFIX = "${SETUP_AUTO_FILL_PREFIX}_as_root"
private const val SETUP_AUTO_FILL_NAV_ARG = "isInitialSetup"
private const val SETUP_AUTO_FILL_ROUTE = "$SETUP_AUTO_FILL_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
const val SETUP_AUTO_FILL_AS_ROOT_ROUTE =
"$SETUP_AUTO_FILL_AS_ROOT_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}"
/**
* Arguments for the [SetupAutoFillScreen] using [SavedStateHandle].
*/
@OmitFromCoverage
data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) {
constructor(savedStateHandle: SavedStateHandle) : this(
isInitialSetup = requireNotNull(savedStateHandle[SETUP_AUTO_FILL_NAV_ARG]),
)
}
/** /**
* Navigate to the setup auto-fill screen. * Navigate to the setup auto-fill screen.
*/ */
fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) { fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) {
this.navigate(SETUP_AUTO_FILL_ROUTE, navOptions) this.navigate("$SETUP_AUTO_FILL_PREFIX/false", navOptions)
}
/**
* Navigate to the setup auto-fill screen as the root.
*/
fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? = null) {
this.navigate("$SETUP_AUTO_FILL_AS_ROOT_PREFIX/true", navOptions)
} }
/** /**
* Add the setup auto-fil screen to the nav graph. * Add the setup auto-fil screen to the nav graph.
*/ */
fun NavGraphBuilder.setupAutoFillDestination() { fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) {
composableWithPushTransitions( composableWithSlideTransitions(
route = SETUP_AUTO_FILL_ROUTE, route = SETUP_AUTO_FILL_ROUTE,
arguments = setupAutofillNavArgs,
) { ) {
SetupAutoFillScreen() SetupAutoFillScreen(onNavigateBack = onNavigateBack)
} }
} }
/**
* Add the setup autofil screen to the root nav graph.
*/
fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
composableWithPushTransitions(
route = SETUP_AUTO_FILL_AS_ROOT_ROUTE,
arguments = setupAutofillNavArgs,
) {
SetupAutoFillScreen(
onNavigateBack = {
// No-Op
},
)
}
}
private val setupAutofillNavArgs = listOf(
navArgument(SETUP_AUTO_FILL_NAV_ARG) {
type = NavType.BoolType
},
)

View file

@ -1,5 +1,7 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus 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.AuthRepository
@ -10,20 +12,31 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state"
/** /**
* View model for the Auto-fill setup screen. * View model for the Auto-fill setup screen.
*/ */
@HiltViewModel @HiltViewModel
class SetupAutoFillViewModel @Inject constructor( class SetupAutoFillViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
) : ) :
BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>( BaseViewModel<SetupAutoFillState, SetupAutoFillEvent, SetupAutoFillAction>(
initialState = run { // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
SetupAutoFillState(userId = userId, dialogState = null, autofillEnabled = false) val isInitialSetup = SetupAutoFillScreenArgs(savedStateHandle).isInitialSetup
SetupAutoFillState(
userId = userId,
dialogState = null,
autofillEnabled = false,
isInitialSetup = isInitialSetup,
)
}, },
) { ) {
@ -48,9 +61,15 @@ class SetupAutoFillViewModel @Inject constructor(
is SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive -> { is SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive -> {
handleAutofillEnabledUpdateReceive(action) handleAutofillEnabledUpdateReceive(action)
} }
SetupAutoFillAction.CloseClick -> handleCloseClick()
} }
} }
private fun handleCloseClick() {
sendEvent(SetupAutoFillEvent.NavigateBack)
}
private fun handleAutofillEnabledUpdateReceive( private fun handleAutofillEnabledUpdateReceive(
action: SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive, action: SetupAutoFillAction.Internal.AutofillEnabledUpdateReceive,
) { ) {
@ -83,7 +102,11 @@ class SetupAutoFillViewModel @Inject constructor(
} }
private fun handleContinueClick() { private fun handleContinueClick() {
if (state.isInitialSetup) {
updateOnboardingStatusToNextStep() updateOnboardingStatusToNextStep()
} else {
sendEvent(SetupAutoFillEvent.NavigateBack)
}
} }
private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) { private fun handleAutofillServiceChanged(action: SetupAutoFillAction.AutofillServiceChanged) {
@ -105,24 +128,28 @@ class SetupAutoFillViewModel @Inject constructor(
/** /**
* UI State for the Auto-fill setup screen. * UI State for the Auto-fill setup screen.
*/ */
@Parcelize
data class SetupAutoFillState( data class SetupAutoFillState(
val userId: String, val userId: String,
val dialogState: SetupAutoFillDialogState?, val dialogState: SetupAutoFillDialogState?,
val autofillEnabled: Boolean, val autofillEnabled: Boolean,
) val isInitialSetup: Boolean,
) : Parcelable
/** /**
* Dialog states for the Auto-fill setup screen. * Dialog states for the Auto-fill setup screen.
*/ */
sealed class SetupAutoFillDialogState { sealed class SetupAutoFillDialogState : Parcelable {
/** /**
* Represents the turn on later dialog. * Represents the turn on later dialog.
*/ */
@Parcelize
data object TurnOnLaterDialog : SetupAutoFillDialogState() data object TurnOnLaterDialog : SetupAutoFillDialogState()
/** /**
* Represents the autofill fallback dialog. * Represents the autofill fallback dialog.
*/ */
@Parcelize
data object AutoFillFallbackDialog : SetupAutoFillDialogState() data object AutoFillFallbackDialog : SetupAutoFillDialogState()
} }
@ -135,6 +162,11 @@ sealed class SetupAutoFillEvent {
* Navigate to the autofill settings screen. * Navigate to the autofill settings screen.
*/ */
data object NavigateToAutofillSettings : SetupAutoFillEvent() data object NavigateToAutofillSettings : SetupAutoFillEvent()
/**
* Navigate back.
*/
data object NavigateBack : SetupAutoFillEvent()
} }
/** /**
@ -173,6 +205,11 @@ sealed class SetupAutoFillAction {
*/ */
data object AutoFillServiceFallback : SetupAutoFillAction() data object AutoFillServiceFallback : SetupAutoFillAction()
/**
* The user has clicked the close button.
*/
data object CloseClick : SetupAutoFillAction()
/** /**
* Internal actions not send through UI. * Internal actions not send through UI.
*/ */

View file

@ -19,6 +19,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -37,6 +38,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
@ -45,6 +47,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage import com.x8bit.bitwarden.ui.platform.components.image.BitwardenGifImage
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@ -57,6 +60,7 @@ import com.x8bit.bitwarden.ui.platform.util.isPortrait
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SetupAutoFillScreen( fun SetupAutoFillScreen(
onNavigateBack: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
viewModel: SetupAutoFillViewModel = hiltViewModel(), viewModel: SetupAutoFillViewModel = hiltViewModel(),
) { ) {
@ -70,6 +74,8 @@ fun SetupAutoFillScreen(
handler.sendAutoFillServiceFallback.invoke() handler.sendAutoFillServiceFallback.invoke()
} }
} }
SetupAutoFillEvent.NavigateBack -> onNavigateBack()
} }
} }
when (state.dialogState) { when (state.dialogState) {
@ -105,14 +111,32 @@ fun SetupAutoFillScreen(
.nestedScroll(scrollBehavior.nestedScrollConnection), .nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = { topBar = {
BitwardenTopAppBar( BitwardenTopAppBar(
title = stringResource(id = R.string.account_setup), title = stringResource(
id = if (state.isInitialSetup) {
R.string.account_setup
} else {
R.string.turn_on_autofill
},
),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
navigationIcon = null, navigationIcon = if (state.isInitialSetup) {
null
} else {
NavigationIcon(
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(SetupAutoFillAction.CloseClick)
}
},
)
},
) )
}, },
) { innerPadding -> ) { innerPadding ->
SetupAutoFillContent( SetupAutoFillContent(
autofillEnabled = state.autofillEnabled, state = state,
onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) }, onAutofillServiceChanged = { handler.onAutofillServiceChanged(it) },
onContinueClick = handler.onContinueClick, onContinueClick = handler.onContinueClick,
onTurnOnLaterClick = handler.onTurnOnLaterClick, onTurnOnLaterClick = handler.onTurnOnLaterClick,
@ -127,7 +151,7 @@ fun SetupAutoFillScreen(
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
private fun SetupAutoFillContent( private fun SetupAutoFillContent(
autofillEnabled: Boolean, state: SetupAutoFillState,
onAutofillServiceChanged: (Boolean) -> Unit, onAutofillServiceChanged: (Boolean) -> Unit,
onContinueClick: () -> Unit, onContinueClick: () -> Unit,
onTurnOnLaterClick: () -> Unit, onTurnOnLaterClick: () -> Unit,
@ -147,7 +171,7 @@ private fun SetupAutoFillContent(
label = stringResource( label = stringResource(
R.string.autofill_services, R.string.autofill_services,
), ),
isChecked = autofillEnabled, isChecked = state.autofillEnabled,
onCheckedChange = onAutofillServiceChanged, onCheckedChange = onAutofillServiceChanged,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -162,6 +186,7 @@ private fun SetupAutoFillContent(
.standardHorizontalMargin(), .standardHorizontalMargin(),
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
if (state.isInitialSetup) {
BitwardenTextButton( BitwardenTextButton(
label = stringResource(R.string.turn_on_later), label = stringResource(R.string.turn_on_later),
onClick = onTurnOnLaterClick, onClick = onTurnOnLaterClick,
@ -169,6 +194,7 @@ private fun SetupAutoFillContent(
.fillMaxWidth() .fillMaxWidth()
.standardHorizontalMargin(), .standardHorizontalMargin(),
) )
}
Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.navigationBarsPadding())
} }
} }
@ -240,7 +266,12 @@ private fun OrderedHeaderContent() {
private fun SetupAutoFillContentDisabled_preview() { private fun SetupAutoFillContentDisabled_preview() {
BitwardenTheme { BitwardenTheme {
SetupAutoFillContent( SetupAutoFillContent(
state = SetupAutoFillState(
userId = "disputationi",
dialogState = null,
autofillEnabled = false, autofillEnabled = false,
isInitialSetup = true,
),
onAutofillServiceChanged = {}, onAutofillServiceChanged = {},
onContinueClick = {}, onContinueClick = {},
onTurnOnLaterClick = {}, onTurnOnLaterClick = {},
@ -253,7 +284,12 @@ private fun SetupAutoFillContentDisabled_preview() {
private fun SetupAutoFillContentEnabled_preview() { private fun SetupAutoFillContentEnabled_preview() {
BitwardenTheme { BitwardenTheme {
SetupAutoFillContent( SetupAutoFillContent(
state = SetupAutoFillState(
userId = "disputationi",
dialogState = null,
autofillEnabled = true, autofillEnabled = true,
isInitialSetup = true,
),
onAutofillServiceChanged = {}, onAutofillServiceChanged = {},
onContinueClick = {}, onContinueClick = {},
onTurnOnLaterClick = {}, onTurnOnLaterClick = {},

View file

@ -33,6 +33,7 @@ class SetupUnlockViewModel @Inject constructor(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val biometricsEncryptionManager: BiometricsEncryptionManager, private val biometricsEncryptionManager: BiometricsEncryptionManager,
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>( ) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run { initialState = savedStateHandle[KEY_STATE] ?: run {
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid( val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(

View file

@ -15,13 +15,13 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost 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_AS_ROOT_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_COMPLETE_ROUTE import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_COMPLETE_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_UNLOCK_AS_ROOT_ROUTE import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_UNLOCK_AS_ROOT_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillAsRootScreen
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupCompleteScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupCompleteScreen
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreenAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreenAsRoot
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestination import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestinationAsRoot
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupCompleteDestination import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupCompleteDestination
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
@ -100,7 +100,7 @@ fun RootNavScreen(
vaultUnlockedGraph(navController) vaultUnlockedGraph(navController)
setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() }) setupDebugMenuDestination(onNavigateBack = { navController.popBackStack() })
setupUnlockDestinationAsRoot() setupUnlockDestinationAsRoot()
setupAutoFillDestination() setupAutoFillDestinationAsRoot()
setupCompleteDestination() setupCompleteDestination()
} }
@ -129,7 +129,7 @@ fun RootNavScreen(
-> VAULT_UNLOCKED_GRAPH_ROUTE -> VAULT_UNLOCKED_GRAPH_ROUTE
RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_AS_ROOT_ROUTE RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_AS_ROOT_ROUTE
RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_ROUTE RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_AS_ROOT_ROUTE
RootNavState.OnboardingStepsComplete -> SETUP_COMPLETE_ROUTE RootNavState.OnboardingStepsComplete -> SETUP_COMPLETE_ROUTE
} }
val currentRoute = navController.currentDestination?.rootLevelRoute() val currentRoute = navController.currentDestination?.rootLevelRoute()
@ -248,7 +248,7 @@ fun RootNavScreen(
} }
RootNavState.OnboardingAutoFillSetup -> { RootNavState.OnboardingAutoFillSetup -> {
navController.navigateToSetupAutoFillScreen(rootNavOptions) navController.navigateToSetupAutoFillAsRootScreen(rootNavOptions)
} }
RootNavState.OnboardingStepsComplete -> { RootNavState.OnboardingStepsComplete -> {

View file

@ -4,7 +4,6 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.navigation import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreen
import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import com.x8bit.bitwarden.ui.platform.feature.settings.about.aboutDestination import com.x8bit.bitwarden.ui.platform.feature.settings.about.aboutDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.about.navigateToAbout import com.x8bit.bitwarden.ui.platform.feature.settings.about.navigateToAbout
@ -34,6 +33,8 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToExportVault: () -> Unit, onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit, onNavigateToFolders: () -> Unit,
onNavigateToPendingRequests: () -> Unit, onNavigateToPendingRequests: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
) { ) {
navigation( navigation(
startDestination = SETTINGS_ROUTE, startDestination = SETTINGS_ROUTE,
@ -56,12 +57,13 @@ fun NavGraphBuilder.settingsGraph(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToDeleteAccount = onNavigateToDeleteAccount, onNavigateToDeleteAccount = onNavigateToDeleteAccount,
onNavigateToPendingRequests = onNavigateToPendingRequests, onNavigateToPendingRequests = onNavigateToPendingRequests,
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() }, onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
) )
appearanceDestination(onNavigateBack = { navController.popBackStack() }) appearanceDestination(onNavigateBack = { navController.popBackStack() })
autoFillDestination( autoFillDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() }, onNavigateToBlockAutoFillScreen = { navController.navigateToBlockAutoFillScreen() },
onNavigateToSetupAutofill = onNavigateToSetupAutoFillScreen,
) )
otherDestination(onNavigateBack = { navController.popBackStack() }) otherDestination(onNavigateBack = { navController.popBackStack() })
vaultSettingsDestination( vaultSettingsDestination(

View file

@ -13,6 +13,7 @@ private const val AUTO_FILL_ROUTE = "settings_auto_fill"
fun NavGraphBuilder.autoFillDestination( fun NavGraphBuilder.autoFillDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToBlockAutoFillScreen: () -> Unit,
onNavigateToSetupAutofill: () -> Unit,
) { ) {
composableWithPushTransitions( composableWithPushTransitions(
route = AUTO_FILL_ROUTE, route = AUTO_FILL_ROUTE,
@ -20,6 +21,7 @@ fun NavGraphBuilder.autoFillDestination(
AutoFillScreen( AutoFillScreen(
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen, onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen,
onNavigateToSetupAutofill = onNavigateToSetupAutofill,
) )
} }
} }

View file

@ -64,6 +64,7 @@ fun AutoFillScreen(
viewModel: AutoFillViewModel = hiltViewModel(), viewModel: AutoFillViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToBlockAutoFillScreen: () -> Unit,
onNavigateToSetupAutofill: () -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -94,6 +95,8 @@ fun AutoFillScreen(
AutoFillEvent.NavigateToSettings -> { AutoFillEvent.NavigateToSettings -> {
intentManager.startCredentialManagerSettings(context) intentManager.startCredentialManagerSettings(context)
} }
AutoFillEvent.NavigateToSetupAutofill -> onNavigateToSetupAutofill()
} }
} }

View file

@ -118,7 +118,7 @@ class AutoFillViewModel @Inject constructor(
private fun handleAutoFillActionCardCtClick() { private fun handleAutoFillActionCardCtClick() {
dismissShowAutofillActionCard() dismissShowAutofillActionCard()
// TODO PM-13068 navigate to auto fill setup screen sendEvent(AutoFillEvent.NavigateToSetupAutofill)
} }
private fun handleUpdateShowAutofillActionCard( private fun handleUpdateShowAutofillActionCard(
@ -261,6 +261,11 @@ sealed class AutoFillEvent {
data class ShowToast( data class ShowToast(
val text: Text, val text: Text,
) : AutoFillEvent() ) : AutoFillEvent()
/**
* Navigates to the setup autofill screen.
*/
data object NavigateToSetupAutofill : AutoFillEvent()
} }
/** /**

View file

@ -4,6 +4,10 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.navigation import androidx.navigation.navigation
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.platform.feature.search.navigateToSearch import com.x8bit.bitwarden.ui.platform.feature.search.navigateToSearch
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
@ -98,6 +102,8 @@ fun NavGraphBuilder.vaultUnlockedGraph(
passwordHistoryMode = GeneratorPasswordHistoryMode.Default, passwordHistoryMode = GeneratorPasswordHistoryMode.Default,
) )
}, },
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
) )
deleteAccountDestination( deleteAccountDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
@ -200,5 +206,15 @@ fun NavGraphBuilder.vaultUnlockedGraph(
attachmentDestination( attachmentDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
) )
setupUnlockDestination(
onNavigateBack = {
navController.popBackStack()
},
)
setupAutoFillDestination(
onNavigateBack = {
navController.popBackStack()
},
)
} }
} }

View file

@ -36,6 +36,8 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToFolders: () -> Unit, onNavigateToFolders: () -> Unit,
onNavigateToPendingRequests: () -> Unit, onNavigateToPendingRequests: () -> Unit,
onNavigateToPasswordHistory: () -> Unit, onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
) { ) {
composableWithStayTransitions( composableWithStayTransitions(
route = VAULT_UNLOCKED_NAV_BAR_ROUTE, route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
@ -53,6 +55,8 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToFolders = onNavigateToFolders, onNavigateToFolders = onNavigateToFolders,
onNavigateToPendingRequests = onNavigateToPendingRequests, onNavigateToPendingRequests = onNavigateToPendingRequests,
onNavigateToPasswordHistory = onNavigateToPasswordHistory, onNavigateToPasswordHistory = onNavigateToPasswordHistory,
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
) )
} }
} }

View file

@ -34,7 +34,6 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
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.setupUnlockDestination
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.max import com.x8bit.bitwarden.ui.platform.base.util.max
import com.x8bit.bitwarden.ui.platform.base.util.toDp import com.x8bit.bitwarden.ui.platform.base.util.toDp
@ -76,6 +75,8 @@ fun VaultUnlockedNavBarScreen(
onNavigateToFolders: () -> Unit, onNavigateToFolders: () -> Unit,
onNavigateToPendingRequests: () -> Unit, onNavigateToPendingRequests: () -> Unit,
onNavigateToPasswordHistory: () -> Unit, onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -133,6 +134,8 @@ fun VaultUnlockedNavBarScreen(
settingsTabClickedAction = remember(viewModel) { settingsTabClickedAction = remember(viewModel) {
{ viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) } { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) }
}, },
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
) )
} }
@ -160,6 +163,8 @@ private fun VaultUnlockedNavBarScaffold(
navigateToFolders: () -> Unit, navigateToFolders: () -> Unit,
navigateToPendingRequests: () -> Unit, navigateToPendingRequests: () -> Unit,
navigateToPasswordHistory: () -> Unit, navigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit,
) { ) {
var shouldDimNavBar by remember { mutableStateOf(false) } var shouldDimNavBar by remember { mutableStateOf(false) }
@ -235,11 +240,8 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToExportVault = navigateToExportVault, onNavigateToExportVault = navigateToExportVault,
onNavigateToFolders = navigateToFolders, onNavigateToFolders = navigateToFolders,
onNavigateToPendingRequests = navigateToPendingRequests, onNavigateToPendingRequests = navigateToPendingRequests,
) onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
setupUnlockDestination( onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
onNavigateBack = {
navController.popBackStack()
},
) )
} }
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.accountsetup package com.x8bit.bitwarden.ui.auth.feature.accountsetup
import androidx.lifecycle.SavedStateHandle
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.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
@ -128,6 +129,25 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
) )
} }
} }
@Test
fun `handleContinueClick send NavigateBack event when not initial setup`() = runTest {
val viewModel = createViewModel(initialState = DEFAULT_STATE.copy(isInitialSetup = false))
viewModel.eventFlow.test {
viewModel.trySendAction(SetupAutoFillAction.ContinueClick)
assertEquals(
SetupAutoFillEvent.NavigateBack,
awaitItem(),
)
}
verify(exactly = 0) {
authRepository.setOnboardingStatus(
DEFAULT_USER_ID,
OnboardingStatus.FINAL_STEP,
)
}
}
@Test @Test
fun `handleTurnOnLaterConfirmClick sets showAutoFillSettingBadge to true`() { fun `handleTurnOnLaterConfirmClick sets showAutoFillSettingBadge to true`() {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -140,10 +160,34 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() {
} }
} }
private fun createViewModel() = SetupAutoFillViewModel( @Test
fun `handleClose click sends NavigateBack event`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SetupAutoFillAction.CloseClick)
assertEquals(SetupAutoFillEvent.NavigateBack, awaitItem())
}
}
private fun createViewModel(
initialState: SetupAutoFillState? = null,
) = SetupAutoFillViewModel(
savedStateHandle = SavedStateHandle(
mapOf(
"state" to initialState,
"isInitialSetup" to true,
),
),
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
authRepository = authRepository, authRepository = authRepository,
) )
} }
private const val DEFAULT_USER_ID = "userId" private const val DEFAULT_USER_ID = "userId"
private val DEFAULT_STATE = SetupAutoFillState(
userId = DEFAULT_USER_ID,
dialogState = null,
autofillEnabled = false,
isInitialSetup = true,
)

View file

@ -5,6 +5,7 @@ import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
@ -15,12 +16,14 @@ import com.x8bit.bitwarden.ui.util.assertNoDialogExists
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
class SetupAutofillScreenTest : BaseComposeTest() { class SetupAutofillScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private val mutableEventFlow = bufferedMutableSharedFlow<SetupAutoFillEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<SetupAutoFillEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -38,6 +41,7 @@ class SetupAutofillScreenTest : BaseComposeTest() {
SetupAutoFillScreen( SetupAutoFillScreen(
intentManager = intentManager, intentManager = intentManager,
viewModel = viewModel, viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
) )
} }
} }
@ -90,6 +94,15 @@ class SetupAutofillScreenTest : BaseComposeTest() {
} }
} }
@Test
fun `Turn on later component should not be displayed when not in initial setup`() {
mutableStateFlow.update { it.copy(isInitialSetup = false) }
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText(text = "Turn on later")
.assertDoesNotExist()
}
@Test @Test
fun `NavigateToAutoFillSettings should start system autofill settings activity`() { fun `NavigateToAutoFillSettings should start system autofill settings activity`() {
every { intentManager.startSystemAutofillSettingsActivity() } returns true every { intentManager.startSystemAutofillSettingsActivity() } returns true
@ -207,10 +220,35 @@ class SetupAutofillScreenTest : BaseComposeTest() {
} }
composeTestRule.assertNoDialogExists() composeTestRule.assertNoDialogExists()
} }
@Test
fun `on NavigateBack event should invoke onNavigateBack`() {
mutableEventFlow.tryEmit(SetupAutoFillEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
@Test
fun `close icon should not show when in initial setup`() {
composeTestRule
.onNodeWithContentDescription(label = "Close")
.assertDoesNotExist()
}
@Test
fun `close icon should show when not initial setup and send action when clicked`() {
mutableStateFlow.update { it.copy(isInitialSetup = false) }
composeTestRule
.onNodeWithContentDescription(label = "Close")
.assertIsDisplayed()
.performClick()
verify { viewModel.trySendAction(SetupAutoFillAction.CloseClick) }
}
} }
private val DEFAULT_STATE = SetupAutoFillState( private val DEFAULT_STATE = SetupAutoFillState(
userId = "userId", userId = "userId",
dialogState = null, dialogState = null,
autofillEnabled = false, autofillEnabled = false,
isInitialSetup = true,
) )

View file

@ -250,7 +250,7 @@ class RootNavScreenTest : BaseComposeTest() {
RootNavState.OnboardingAutoFillSetup RootNavState.OnboardingAutoFillSetup
composeTestRule.runOnIdle { composeTestRule.runOnIdle {
fakeNavHostController.assertLastNavigation( fakeNavHostController.assertLastNavigation(
route = "setup_auto_fill", route = "setup_auto_fill_as_root/true",
navOptions = expectedNavOptions, navOptions = expectedNavOptions,
) )
} }

View file

@ -35,6 +35,7 @@ class AutoFillScreenTest : BaseComposeTest() {
private var isSystemSettingsRequestSuccess = false private var isSystemSettingsRequestSuccess = false
private var onNavigateBackCalled = false private var onNavigateBackCalled = false
private var onNavigateToBlockAutoFillScreenCalled = false private var onNavigateToBlockAutoFillScreenCalled = false
private var onNavigateToSetupAutoFillScreenCalled = false
private val mutableEventFlow = bufferedMutableSharedFlow<AutoFillEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<AutoFillEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
@ -56,6 +57,7 @@ class AutoFillScreenTest : BaseComposeTest() {
onNavigateToBlockAutoFillScreen = { onNavigateToBlockAutoFillScreenCalled = true }, onNavigateToBlockAutoFillScreen = { onNavigateToBlockAutoFillScreenCalled = true },
viewModel = viewModel, viewModel = viewModel,
intentManager = intentManager, intentManager = intentManager,
onNavigateToSetupAutofill = { onNavigateToSetupAutoFillScreenCalled = true },
) )
} }
} }
@ -498,6 +500,12 @@ class AutoFillScreenTest : BaseComposeTest() {
.performClick() .performClick()
verify { viewModel.trySendAction(AutoFillAction.DismissShowAutofillActionCard) } verify { viewModel.trySendAction(AutoFillAction.DismissShowAutofillActionCard) }
} }
@Test
fun `when NavigateToSetupAutofill event is sent should call onNavigateToSetupAutofill`() {
mutableEventFlow.tryEmit(AutoFillEvent.NavigateToSetupAutofill)
assertTrue(onNavigateToSetupAutoFillScreenCalled)
}
} }
private val DEFAULT_STATE: AutoFillState = AutoFillState( private val DEFAULT_STATE: AutoFillState = AutoFillState(

View file

@ -321,10 +321,17 @@ class AutoFillViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@Test @Test
fun `when AutoFillActionCardCtaClick action is sent should update show autofill in repository`() { fun `when AutoFillActionCardCtaClick action is sent should update show autofill in repository and send NavigateToSetupAutofill event`() =
runTest {
mutableShowAutofillActionCardFlow.update { true } mutableShowAutofillActionCardFlow.update { true }
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AutoFillAction.AutoFillActionCardCtaClick) viewModel.trySendAction(AutoFillAction.AutoFillActionCardCtaClick)
assertEquals(
AutoFillEvent.NavigateToSetupAutofill,
awaitItem(),
)
}
verify { verify {
settingsRepository.storeShowAutoFillSettingBadge( settingsRepository.storeShowAutoFillSettingBadge(
DEFAULT_STATE.activeUserId, DEFAULT_STATE.activeUserId,

View file

@ -53,6 +53,8 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
onNavigateToPendingRequests = {}, onNavigateToPendingRequests = {},
onNavigateToSearchVault = {}, onNavigateToSearchVault = {},
onNavigateToSearchSend = {}, onNavigateToSearchSend = {},
onNavigateToSetupAutoFillScreen = {},
onNavigateToSetupUnlockScreen = {},
) )
} }
} }