Add skeleton for trusted device UI (#1132)

This commit is contained in:
David Perez 2024-03-13 11:35:53 -05:00 committed by Álison Fernandes
parent 05079f2a32
commit 509ef72546
7 changed files with 248 additions and 0 deletions

View file

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.loginWithDeviceDestin
import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDevice
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHintDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
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
@ -106,6 +107,9 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
masterPasswordHintDestination(
onNavigateBack = { navController.popBackStack() },
)
trustedDeviceDestination(
onNavigateBack = { navController.popBackStack() },
)
twoFactorLoginDestination(
onNavigateBack = { navController.popBackStack() },
)

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val TRUSTED_DEVICE_ROUTE: String = "trusted_device"
/**
* Add the Trusted Device Screen to the nav graph.
*/
fun NavGraphBuilder.trustedDeviceDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = TRUSTED_DEVICE_ROUTE,
) {
TrustedDeviceScreen(
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the Trusted Device Screen.
*/
fun NavController.navigateToTrustedDevice(navOptions: NavOptions? = null) {
this.navigate(TRUSTED_DEVICE_ROUTE, navOptions)
}

View file

@ -0,0 +1,70 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.handlers.TrustedDeviceHandlers
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
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.scaffold.BitwardenScaffold
/**
* The top level composable for the Reset Password screen.
*/
@Composable
fun TrustedDeviceScreen(
viewModel: TrustedDeviceViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handlers = remember(viewModel) { TrustedDeviceHandlers.create(viewModel = viewModel) }
EventsEffect(viewModel = viewModel) { event ->
when (event) {
TrustedDeviceEvent.NavigateBack -> onNavigateBack()
}
}
TrustedDeviceScaffold(
state = state,
handlers = handlers,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TrustedDeviceScaffold(
state: TrustedDeviceState,
handlers: TrustedDeviceHandlers,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.log_in_initiated),
scrollBehavior = scrollBehavior,
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = handlers.onBackClick,
),
)
},
) { innerPadding ->
}
}

View file

@ -0,0 +1,53 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.lifecycle.SavedStateHandle
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Manages application state for the Trusted Device screen.
*/
@HiltViewModel
class TrustedDeviceViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<TrustedDeviceState, TrustedDeviceEvent, TrustedDeviceAction>(
initialState = savedStateHandle[KEY_STATE] ?: TrustedDeviceState,
) {
override fun handleAction(action: TrustedDeviceAction) {
when (action) {
TrustedDeviceAction.BackClick -> handleBackClick()
}
}
private fun handleBackClick() {
sendEvent(TrustedDeviceEvent.NavigateBack)
}
}
/**
* Models the state for the Trusted Device screen.
*/
data object TrustedDeviceState
/**
* Models events for the Trusted Device screen.
*/
sealed class TrustedDeviceEvent {
/**
* Navigates back.
*/
data object NavigateBack : TrustedDeviceEvent()
}
/**
* Models actions for the Trusted Device screen.
*/
sealed class TrustedDeviceAction {
/**
* User clicked back button.
*/
data object BackClick : TrustedDeviceAction()
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice.handlers
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceAction
import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceViewModel
/**
* A collection of handler functions for managing actions within the context of the Trusted Device
* Screen.
*/
data class TrustedDeviceHandlers(
val onBackClick: () -> Unit,
) {
companion object {
/**
* Creates an instance of [TrustedDeviceHandlers] by binding actions to the provided
* [TrustedDeviceViewModel].
*/
fun create(viewModel: TrustedDeviceViewModel): TrustedDeviceHandlers =
TrustedDeviceHandlers(
onBackClick = { viewModel.trySendAction(TrustedDeviceAction.BackClick) },
)
}
}

View file

@ -0,0 +1,40 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertTrue
class TrustedDeviceScreenTest : BaseComposeTest() {
private var onNavigateBackCalled: Boolean = false
private val mutableEventFlow = bufferedMutableSharedFlow<TrustedDeviceEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
val viewModel = mockk<TrustedDeviceViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
every { stateFlow } returns mutableStateFlow
}
@Before
fun setUp() {
composeTestRule.setContent {
TrustedDeviceScreen(
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
)
}
}
@Test
fun `on NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(TrustedDeviceEvent.NavigateBack)
assertTrue(onNavigateBackCalled)
}
}
private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState

View file

@ -0,0 +1,28 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class TrustedDeviceViewModelTest : BaseViewModelTest() {
@Test
fun `on BackClick emits NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(TrustedDeviceAction.BackClick)
assertEquals(TrustedDeviceEvent.NavigateBack, awaitItem())
}
}
private fun createViewModel(
state: TrustedDeviceState? = null,
): TrustedDeviceViewModel =
TrustedDeviceViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
)
}