Adding Navigation to the verification code screen and skeleton UI (#697)

This commit is contained in:
Oleg Semenenko 2024-01-20 15:05:16 -06:00 committed by Álison Fernandes
parent 1a53178137
commit cd1d326d45
8 changed files with 296 additions and 3 deletions

View file

@ -6,6 +6,8 @@ import androidx.navigation.NavOptions
import androidx.navigation.navigation import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListing import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListing
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.vaultVerificationCodeDestination
const val VAULT_GRAPH_ROUTE: String = "vault_graph" const val VAULT_GRAPH_ROUTE: String = "vault_graph"
@ -28,6 +30,10 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen, onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultItemListingScreen = { navController.navigateToVaultItemListing(it) }, onNavigateToVaultItemListingScreen = { navController.navigateToVaultItemListing(it) },
onNavigateToVerificationCodeScreen = {
navController.navigateToVerificationCodeScreen()
},
onDimBottomNavBarRequest = onDimBottomNavBarRequest, onDimBottomNavBarRequest = onDimBottomNavBarRequest,
) )
vaultItemListingDestination( vaultItemListingDestination(
@ -35,6 +41,11 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
) )
vaultVerificationCodeDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
)
} }
} }

View file

@ -11,8 +11,10 @@ const val VAULT_ROUTE: String = "vault"
/** /**
* Add vault destination to the nav graph. * Add vault destination to the nav graph.
*/ */
@Suppress("LongParameterList")
fun NavGraphBuilder.vaultDestination( fun NavGraphBuilder.vaultDestination(
onNavigateToVaultAddItemScreen: () -> Unit, onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToVerificationCodeScreen: () -> Unit,
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
@ -26,6 +28,7 @@ fun NavGraphBuilder.vaultDestination(
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen, onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
onNavigateToVaultItemListingScreen = onNavigateToVaultItemListingScreen, onNavigateToVaultItemListingScreen = onNavigateToVaultItemListingScreen,
onNavigateToVerificationCodeScreen = onNavigateToVerificationCodeScreen,
onDimBottomNavBarRequest = onDimBottomNavBarRequest, onDimBottomNavBarRequest = onDimBottomNavBarRequest,
) )
} }

View file

@ -36,7 +36,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
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.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
@ -70,6 +69,7 @@ fun VaultScreen(
onNavigateToVaultAddItemScreen: () -> Unit, onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
onNavigateToVerificationCodeScreen: () -> Unit,
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
@ -96,8 +96,7 @@ fun VaultScreen(
} }
is VaultEvent.NavigateToVerificationCodeScreen -> { is VaultEvent.NavigateToVerificationCodeScreen -> {
// TODO Add Verification codes detail screen (BIT-1338) onNavigateToVerificationCodeScreen()
showNotYetImplementedToast(context = context)
} }
is VaultEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.itemId) is VaultEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.itemId)

View file

@ -0,0 +1,34 @@
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
private const val VERIFICATION_CODE_ROUTE: String = "verification_code"
/**
* Add the verification code screen to the nav graph.
*/
fun NavGraphBuilder.vaultVerificationCodeDestination(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (String) -> Unit,
) {
composableWithPushTransitions(
route = VERIFICATION_CODE_ROUTE,
) {
VerificationCodeScreen(
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
onNavigateBack = onNavigateBack,
)
}
}
/**
* Navigate to the verification code screen.
*/
fun NavController.navigateToVerificationCodeScreen(
navOptions: NavOptions? = null,
) {
this.navigate(VERIFICATION_CODE_ROUTE, navOptions)
}

View file

@ -0,0 +1,75 @@
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
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.Alignment
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.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
/**
* Displays the verification codes to the user.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VerificationCodeScreen(
onNavigateBack: () -> Unit,
onNavigateToVaultItemScreen: (String) -> Unit,
viewModel: VerificationCodeViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is VerificationCodeEvent.NavigateBack -> onNavigateBack.invoke()
is VerificationCodeEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.id)
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.verification_codes),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(VerificationCodeAction.BackClick) }
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "Not yet implemented")
}
}
}

View file

@ -0,0 +1,104 @@
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Handles [VerificationCodeAction],
* and launches [VerificationCodeEvent] for the [VerificationCodeScreen].
*/
@HiltViewModel
class VerificationCodeViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<VerificationCodeState, VerificationCodeEvent, VerificationCodeAction>(
initialState = savedStateHandle[KEY_STATE]
?: VerificationCodeState(
viewState = VerificationCodeState.ViewState.Empty,
),
) {
override fun handleAction(action: VerificationCodeAction) {
when (action) {
is VerificationCodeAction.BackClick -> handleBackClick()
is VerificationCodeAction.ItemClick -> handleItemClick(action)
}
}
private fun handleBackClick() {
sendEvent(
event = VerificationCodeEvent.NavigateBack,
)
}
private fun handleItemClick(action: VerificationCodeAction.ItemClick) {
sendEvent(
VerificationCodeEvent.NavigateToVaultItem(action.id),
)
}
}
/**
* Models state of the verification code screen.
*
* @property viewState indicates what view state the screen is in.
*/
@Parcelize
data class VerificationCodeState(
val viewState: ViewState,
) : Parcelable {
/**
* Represents the specific view states for the [VerificationCodeScreen].
*/
@Parcelize
sealed class ViewState : Parcelable {
/**
* Represents an empty content state for the [VerificationCodeScreen].
*/
@Parcelize
data object Empty : ViewState()
}
}
/**
* Models events for the [VerificationCodeScreen].
*/
sealed class VerificationCodeEvent {
/**
* Navigate back.
*/
data object NavigateBack : VerificationCodeEvent()
/**
* Navigates to the VaultItemScreen.
*
* @property id the id of the item to navigate to.
*/
data class NavigateToVaultItem(val id: String) : VerificationCodeEvent()
}
/**
* Models actions for the [VerificationCodeScreen].
*/
sealed class VerificationCodeAction {
/**
* Click the back button.
*/
data object BackClick : VerificationCodeAction()
/**
* Navigates to an item.
*
* @property id the id of the item to navigate to.
*/
data class ItemClick(val id: String) : VerificationCodeAction()
}

View file

@ -60,6 +60,7 @@ class VaultScreenTest : BaseComposeTest() {
private var onNavigateToVaultEditItemId: String? = null private var onNavigateToVaultEditItemId: String? = null
private var onNavigateToVaultItemListingType: VaultItemListingType? = null private var onNavigateToVaultItemListingType: VaultItemListingType? = null
private var onDimBottomNavBarRequestCalled = false private var onDimBottomNavBarRequestCalled = false
private var onNavigateToVerificationCodeScreen = false
private val intentManager = mockk<IntentManager>(relaxed = true) private val intentManager = mockk<IntentManager>(relaxed = true)
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>() private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
@ -79,6 +80,7 @@ class VaultScreenTest : BaseComposeTest() {
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemId = it }, onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemId = it },
onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it }, onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it },
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true }, onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
intentManager = intentManager, intentManager = intentManager,
) )
} }
@ -533,6 +535,30 @@ class VaultScreenTest : BaseComposeTest() {
verify { viewModel.trySendAction(VaultAction.TryAgainClick) } verify { viewModel.trySendAction(VaultAction.TryAgainClick) }
} }
@Test
fun `verification code click should call VerificationCodesClick `() {
mutableStateFlow.update {
it.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
totpItemsCount = 3,
),
isPremium = true,
)
}
composeTestRule
.onNodeWithText("Verification codes")
.performClick()
verify { viewModel.trySendAction(VaultAction.VerificationCodesClick) }
}
@Test
fun `NavigateToVerificationCodeScreen event should call onNavigateToVerificationCodeScreen`() {
mutableEventFlow.tryEmit(VaultEvent.NavigateToVerificationCodeScreen)
assertTrue(onNavigateToVerificationCodeScreen)
}
@Test @Test
fun `search icon click should send SearchIconClick action`() { fun `search icon click should send SearchIconClick action`() {
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) } mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }

View file

@ -0,0 +1,41 @@
package com.x8bit.bitwarden.ui.vault.feature.verificationcode
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 VerificationCodeViewModelTest : BaseViewModelTest() {
@Test
fun `on BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VerificationCodeAction.BackClick)
assertEquals(VerificationCodeEvent.NavigateBack, awaitItem())
}
}
@Test
fun `on ItemClick should emit ItemClick`() = runTest {
val viewModel = createViewModel()
val testId = "testId"
viewModel.eventFlow.test {
viewModel.trySendAction(VerificationCodeAction.ItemClick(testId))
assertEquals(VerificationCodeEvent.NavigateToVaultItem(testId), awaitItem())
}
}
private fun createViewModel(
state: VerificationCodeState? = DEFAULT_STATE,
): VerificationCodeViewModel = VerificationCodeViewModel(
savedStateHandle = SavedStateHandle().apply { set("state", state) },
)
}
private val DEFAULT_STATE: VerificationCodeState = VerificationCodeState(
VerificationCodeState.ViewState.Empty,
)