Add initial UI flow for TDE (#1235)

This commit is contained in:
David Perez
2024-04-08 10:04:31 -05:00
committed by Álison Fernandes
parent bfbb8d47a6
commit a6a4c40693
12 changed files with 300 additions and 49 deletions

View File

@@ -1137,11 +1137,13 @@ class AuthRepositoryImpl(
// Handle the Trusted Device Encryption flow
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions?.let { options ->
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
trustedDeviceDecryptionOptions = options,
userStateJson = userStateJson,
privateKey = requireNotNull(loginResponse.privateKey),
)
loginResponse.privateKey?.let { privateKey ->
handleLoginCommonSuccessTrustedDeviceUserDecryptionOptions(
trustedDeviceDecryptionOptions = options,
userStateJson = userStateJson,
privateKey = privateKey,
)
}
}
// Remove any cached data after successfully logging in.

View File

@@ -23,7 +23,6 @@ import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceDestination
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
@@ -113,7 +112,6 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
masterPasswordHintDestination(
onNavigateBack = { navController.popBackStack() },
)
trustedDeviceDestination()
twoFactorLoginDestination(
onNavigateBack = { navController.popBackStack() },
)

View File

@@ -0,0 +1,60 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestination
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph"
/**
* Add trusted device destinations to the nav graph.
*/
fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) {
navigation(
startDestination = TRUSTED_DEVICE_ROUTE,
route = TRUSTED_DEVICE_GRAPH_ROUTE,
) {
loginWithDeviceDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToTwoFactorLogin = {
navController.navigateToTwoFactorLogin(
emailAddress = it,
password = null,
)
},
)
trustedDeviceDestination(
onNavigateToAdminApproval = {
navController.navigateToLoginWithDevice(
emailAddress = it,
loginType = LoginWithDeviceType.SSO_ADMIN_APPROVAL,
)
},
onNavigateToLoginWithOtherDevice = {
navController.navigateToLoginWithDevice(
emailAddress = it,
loginType = LoginWithDeviceType.SSO_OTHER_DEVICE,
)
},
)
twoFactorLoginDestination(
onNavigateBack = { navController.popBackStack() },
)
}
}
/**
* Navigate to the trusted device graph.
*/
fun NavController.navigateToTrustedDeviceGraph(
navOptions: NavOptions? = null,
) {
navigate(TRUSTED_DEVICE_GRAPH_ROUTE, navOptions)
}

View File

@@ -1,39 +1,29 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
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.composableWithSlideTransitions
private const val EMAIL_ADDRESS: String = "email_address"
private const val TRUSTED_DEVICE_PREFIX: String = "trusted_device"
private const val TRUSTED_DEVICE_ROUTE: String = "$TRUSTED_DEVICE_PREFIX/{${EMAIL_ADDRESS}}"
/**
* Class to retrieve trusted device arguments from the [SavedStateHandle].
* The route for navigating to the [TrustedDeviceScreen].
*/
@OmitFromCoverage
data class TrustedDeviceArgs(val emailAddress: String) {
constructor(savedStateHandle: SavedStateHandle) : this(
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL_ADDRESS)),
)
}
const val TRUSTED_DEVICE_ROUTE: String = "trusted_device"
/**
* Add the Trusted Device Screen to the nav graph.
*/
fun NavGraphBuilder.trustedDeviceDestination() {
fun NavGraphBuilder.trustedDeviceDestination(
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
) {
composableWithSlideTransitions(
route = TRUSTED_DEVICE_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
),
) {
TrustedDeviceScreen()
TrustedDeviceScreen(
onNavigateToAdminApproval = onNavigateToAdminApproval,
onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice,
)
}
}
@@ -41,8 +31,7 @@ fun NavGraphBuilder.trustedDeviceDestination() {
* Navigate to the Trusted Device Screen.
*/
fun NavController.navigateToTrustedDevice(
emailAddress: String,
navOptions: NavOptions? = null,
) {
this.navigate("$TRUSTED_DEVICE_PREFIX/$emailAddress", navOptions)
this.navigate(TRUSTED_DEVICE_ROUTE, navOptions)
}

View File

@@ -46,6 +46,8 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
*/
@Composable
fun TrustedDeviceScreen(
onNavigateToAdminApproval: (emailAddress: String) -> Unit,
onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit,
viewModel: TrustedDeviceViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@@ -54,6 +56,14 @@ fun TrustedDeviceScreen(
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is TrustedDeviceEvent.NavigateToApproveWithAdmin -> {
onNavigateToAdminApproval(event.email)
}
is TrustedDeviceEvent.NavigateToApproveWithDevice -> {
onNavigateToLoginWithOtherDevice(event.email)
}
is TrustedDeviceEvent.ShowToast -> {
Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)

View File

@@ -25,14 +25,19 @@ class TrustedDeviceViewModel @Inject constructor(
) : BaseViewModel<TrustedDeviceState, TrustedDeviceEvent, TrustedDeviceAction>(
initialState = savedStateHandle[KEY_STATE]
?: run {
val account = authRepository.userStateFlow.value?.activeAccount
val trustedDevice = account?.trustedDevice
if (trustedDevice == null) authRepository.logout()
TrustedDeviceState(
emailAddress = TrustedDeviceArgs(savedStateHandle).emailAddress,
emailAddress = account?.email.orEmpty(),
environmentLabel = environmentRepository.environment.label,
isRemembered = false,
showContinueButton = false,
showOtherDeviceButton = false,
showRequestAdminButton = false,
showMasterPasswordButton = false,
isRemembered = true,
showContinueButton = trustedDevice
?.let { !it.hasAdminApproval && !it.hasMasterPassword }
?: false,
showOtherDeviceButton = trustedDevice?.hasLoginApprovingDevice ?: false,
showRequestAdminButton = trustedDevice?.hasAdminApproval ?: false,
showMasterPasswordButton = trustedDevice?.hasMasterPassword ?: false,
)
},
) {
@@ -61,11 +66,13 @@ class TrustedDeviceViewModel @Inject constructor(
}
private fun handleApproveWithAdminClick() {
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
authRepository.shouldTrustDevice = state.isRemembered
sendEvent(TrustedDeviceEvent.NavigateToApproveWithAdmin(state.emailAddress))
}
private fun handleApproveWithDeviceClick() {
sendEvent(TrustedDeviceEvent.ShowToast("Not yet implemented".asText()))
authRepository.shouldTrustDevice = state.isRemembered
sendEvent(TrustedDeviceEvent.NavigateToApproveWithDevice(state.emailAddress))
}
private fun handleApproveWithPasswordClick() {
@@ -95,6 +102,20 @@ data class TrustedDeviceState(
* Models events for the Trusted Device screen.
*/
sealed class TrustedDeviceEvent {
/**
* Navigates to the approve with admin screen.
*/
data class NavigateToApproveWithAdmin(
val email: String,
) : TrustedDeviceEvent()
/**
* Navigates to the approve with device screen.
*/
data class NavigateToApproveWithDevice(
val email: String,
) : TrustedDeviceEvent()
/**
* Displays the [message] as a toast.
*/

View File

@@ -21,6 +21,9 @@ import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPassword
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.setpassword.SET_PASSWORD_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TRUSTED_DEVICE_GRAPH_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.navigateToTrustedDeviceGraph
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceGraph
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
@@ -83,6 +86,7 @@ fun RootNavScreen(
splashDestination()
authGraph(navController)
resetPasswordDestination()
trustedDeviceGraph(navController)
vaultUnlockDestination()
vaultUnlockedGraph(navController)
}
@@ -90,8 +94,9 @@ fun RootNavScreen(
val targetRoute = when (state) {
RootNavState.Auth -> AUTH_GRAPH_ROUTE
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
is RootNavState.SetPassword -> SET_PASSWORD_ROUTE
RootNavState.SetPassword -> SET_PASSWORD_ROUTE
RootNavState.Splash -> SPLASH_ROUTE
RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
is RootNavState.VaultUnlocked,
is RootNavState.VaultUnlockedForAutofillSave,
@@ -130,8 +135,9 @@ fun RootNavScreen(
when (val currentState = state) {
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
is RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
RootNavState.SetPassword -> navController.navigateToSetPassword(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.TrustedDevice -> navController.navigateToTrustedDeviceGraph(rootNavOptions)
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(
rootNavOptions,

View File

@@ -54,6 +54,7 @@ class RootNavViewModel @Inject constructor(
authRepository.updateLastActiveTime()
}
@Suppress("CyclomaticComplexMethod")
private fun handleUserStateUpdateReceive(
action: RootNavAction.Internal.UserStateUpdateReceive,
) {
@@ -64,6 +65,9 @@ class RootNavViewModel @Inject constructor(
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
userState?.activeAccount?.trustedDevice?.isDeviceTrusted == false &&
!userState.activeAccount.isVaultUnlocked -> RootNavState.TrustedDevice
userState == null ||
!userState.activeAccount.isLoggedIn ||
userState.hasPendingAccountAddition -> RootNavState.Auth
@@ -131,6 +135,12 @@ sealed class RootNavState : Parcelable {
@Parcelize
data object Splash : RootNavState()
/**
* App should show the trusted device destination.
*/
@Parcelize
data object TrustedDevice : RootNavState()
/**
* App should show vault locked nav graph.
*/