mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 18:06:08 +03:00
Adding Navigation to the verification code screen and skeleton UI (#697)
This commit is contained in:
parent
1a53178137
commit
cd1d326d45
8 changed files with 296 additions and 3 deletions
|
@ -6,6 +6,8 @@ import androidx.navigation.NavOptions
|
|||
import androidx.navigation.navigation
|
||||
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.verificationcode.navigateToVerificationCodeScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.vaultVerificationCodeDestination
|
||||
|
||||
const val VAULT_GRAPH_ROUTE: String = "vault_graph"
|
||||
|
||||
|
@ -28,6 +30,10 @@ fun NavGraphBuilder.vaultGraph(
|
|||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToVaultItemListingScreen = { navController.navigateToVaultItemListing(it) },
|
||||
onNavigateToVerificationCodeScreen = {
|
||||
navController.navigateToVerificationCodeScreen()
|
||||
},
|
||||
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
)
|
||||
vaultItemListingDestination(
|
||||
|
@ -35,6 +41,11 @@ fun NavGraphBuilder.vaultGraph(
|
|||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
)
|
||||
|
||||
vaultVerificationCodeDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,8 +11,10 @@ const val VAULT_ROUTE: String = "vault"
|
|||
/**
|
||||
* Add vault destination to the nav graph.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun NavGraphBuilder.vaultDestination(
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToVerificationCodeScreen: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
|
@ -26,6 +28,7 @@ fun NavGraphBuilder.vaultDestination(
|
|||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToVaultItemListingScreen = onNavigateToVaultItemListingScreen,
|
||||
onNavigateToVerificationCodeScreen = onNavigateToVerificationCodeScreen,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import com.x8bit.bitwarden.R
|
||||
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.showNotYetImplementedToast
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
|
||||
|
@ -70,6 +69,7 @@ fun VaultScreen(
|
|||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVerificationCodeScreen: () -> Unit,
|
||||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
|
@ -96,8 +96,7 @@ fun VaultScreen(
|
|||
}
|
||||
|
||||
is VaultEvent.NavigateToVerificationCodeScreen -> {
|
||||
// TODO Add Verification codes detail screen (BIT-1338)
|
||||
showNotYetImplementedToast(context = context)
|
||||
onNavigateToVerificationCodeScreen()
|
||||
}
|
||||
|
||||
is VaultEvent.NavigateToVaultItem -> onNavigateToVaultItemScreen(event.itemId)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -60,6 +60,7 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
private var onNavigateToVaultEditItemId: String? = null
|
||||
private var onNavigateToVaultItemListingType: VaultItemListingType? = null
|
||||
private var onDimBottomNavBarRequestCalled = false
|
||||
private var onNavigateToVerificationCodeScreen = false
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultEvent>()
|
||||
|
@ -79,6 +80,7 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemId = it },
|
||||
onNavigateToVaultItemListingScreen = { onNavigateToVaultItemListingType = it },
|
||||
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
|
||||
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
|
||||
intentManager = intentManager,
|
||||
)
|
||||
}
|
||||
|
@ -533,6 +535,30 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
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
|
||||
fun `search icon click should send SearchIconClick action`() {
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
|
|
|
@ -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,
|
||||
)
|
Loading…
Reference in a new issue