From cd1d326d4563fae44ef0dea182a235d645e7a92e Mon Sep 17 00:00:00 2001 From: Oleg Semenenko <146032743+oleg-livefront@users.noreply.github.com> Date: Sat, 20 Jan 2024 15:05:16 -0600 Subject: [PATCH] Adding Navigation to the verification code screen and skeleton UI (#697) --- .../feature/vault/VaultGraphNavigation.kt | 11 ++ .../ui/vault/feature/vault/VaultNavigation.kt | 3 + .../ui/vault/feature/vault/VaultScreen.kt | 5 +- .../VerificationCodeNavigation.kt | 34 ++++++ .../VerificationCodeScreen.kt | 75 +++++++++++++ .../VerificationCodeViewModel.kt | 104 ++++++++++++++++++ .../ui/vault/feature/vault/VaultScreenTest.kt | 26 +++++ .../VerificationCodeViewModelTest.kt | 41 +++++++ 8 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 8ca327c6d..7db970106 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -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, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt index 378f6d31a..16cdd66e3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -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, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 1a1bd16c8..45469ac93 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt new file mode 100644 index 000000000..2e4750bb9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt @@ -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) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt new file mode 100644 index 000000000..dfa056169 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeScreen.kt @@ -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") + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt new file mode 100644 index 000000000..7a5fb2e57 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModel.kt @@ -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( + 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() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 51263e977..186b8710b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -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(relaxed = true) private val mutableEventFlow = bufferedMutableSharedFlow() @@ -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) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt new file mode 100644 index 000000000..efadbfa5b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeViewModelTest.kt @@ -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, +)