BIT-143: Add initial bottom navigation screen (#25)

Co-authored-by: Brian Yencho <brian@livefront.com>
This commit is contained in:
Ramsey Smith 2023-09-05 14:44:50 -06:00 committed by Álison Fernandes
parent dc48420820
commit 69feff2dcd
16 changed files with 666 additions and 0 deletions

View file

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.detekt)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
kotlin("kapt")

View file

@ -14,6 +14,8 @@ import androidx.navigation.navOptions
import com.x8bit.bitwarden.ui.auth.feature.auth.authDestinations
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuth
import com.x8bit.bitwarden.ui.platform.components.PlaceholderComposable
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlocked
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations
/**
* Controls root level [NavHost] for the app.
@ -31,6 +33,7 @@ fun RootNavScreen(
) {
splashDestinations()
authDestinations(navController)
vaultUnlockedDestinations()
}
// When state changes, navigate to different root navigation state
@ -43,6 +46,7 @@ fun RootNavScreen(
when (state) {
RootNavState.Auth -> navController.navigateToAuth(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.VaultUnlocked -> navController.navigateToVaultUnlocked(rootNavOptions)
}
}

View file

@ -31,6 +31,11 @@ class RootNavViewModel @Inject constructor() :
* Models state of the root level navigation of the app.
*/
sealed class RootNavState {
/**
* Show the vault unlocked screen.
*/
data object VaultUnlocked : RootNavState()
/**
* Show the auth screens.
*/

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlocked
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination
private const val VAULT_UNLOCKED_ROUTE = "VaultUnlocked"
/**
* Navigate to the vault unlocked screen. Note this will only work if vault unlocked destinations were added
* via [vaultUnlockedDestinations].
*/
fun NavController.navigateToVaultUnlocked(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_ROUTE, navOptions)
}
/**
* Add vault unlocked destinations to the root nav graph.
*/
fun NavGraphBuilder.vaultUnlockedDestinations() {
navigation(
startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE,
route = VAULT_UNLOCKED_ROUTE,
) {
vaultUnlockedNavBarDestination()
}
}

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedDestinations
/**
* The functions below pertain to entry into the [VaultUnlockedNavBarScreen].
*/
const val VAULT_UNLOCKED_NAV_BAR_ROUTE: String = "VaultUnlockedNavBar"
/**
* Navigate to the vault unlocked nav bar screen.
* Note this will only work if vault unlocked nav bar destination was added
* via [vaultUnlockedDestinations].
*/
fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) {
navigate(VAULT_UNLOCKED_NAV_BAR_ROUTE, navOptions)
}
/**
* Add vault unlocked destination to the root nav graph.
*/
fun NavGraphBuilder.vaultUnlockedNavBarDestination() {
composable(VAULT_UNLOCKED_NAV_BAR_ROUTE) {
VaultUnlockedNavBarScreen()
}
}

View file

@ -0,0 +1,327 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import android.os.Parcelable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.base.util.EventsEffect
import com.x8bit.bitwarden.ui.components.PlaceholderComposable
import kotlinx.parcelize.Parcelize
/**
* Top level composable for the Vault Unlocked Screen.
*/
@Composable
fun VaultUnlockedNavBarScreen(
viewModel: VaultUnlockedNavBarViewModel = viewModel(),
navController: NavHostController = rememberNavController(),
) {
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
val navOptions = vaultUnlockedNavBarScreenNavOptions()
when (event) {
VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar -> navigateToVault(navOptions)
VaultUnlockedNavBarEvent.NavigateToSendScreen -> navigateToSend(navOptions)
VaultUnlockedNavBarEvent.NavigateToGeneratorScreen -> navigateToGenerator(navOptions)
VaultUnlockedNavBarEvent.NavigateToSettingsScreen -> navigateToSettings(navOptions)
}
}
}
VaultUnlockedNavBarScaffold(
navController = navController,
generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) },
sendTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) },
vaultTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) },
settingsTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) },
)
}
/**
* Scaffold that contains the bottom nav bar for the [VaultUnlockedNavBarScreen]
*/
@Composable
private fun VaultUnlockedNavBarScaffold(
navController: NavHostController,
vaultTabClickedAction: () -> Unit,
sendTabClickedAction: () -> Unit,
generatorTabClickedAction: () -> Unit,
settingsTabClickedAction: () -> Unit,
) {
var state by rememberSaveable {
mutableStateOf<VaultUnlockedNavBarTab>(VaultUnlockedNavBarTab.Vault)
}
Scaffold(
bottomBar = {
BottomAppBar {
val destinations = listOf(
VaultUnlockedNavBarTab.Vault,
VaultUnlockedNavBarTab.Send,
VaultUnlockedNavBarTab.Generator,
VaultUnlockedNavBarTab.Settings,
)
destinations.forEach { destination ->
NavigationBarItem(
modifier = Modifier.testTag(destination.route),
icon = {
Icon(
painter = painterResource(id = destination.iconRes),
contentDescription = stringResource(id = destination.contentDescriptionRes),
)
},
label = {
Text(text = stringResource(id = destination.labelRes))
},
selected = destination == state,
onClick = {
state = destination
when (destination) {
VaultUnlockedNavBarTab.Vault -> vaultTabClickedAction()
VaultUnlockedNavBarTab.Send -> sendTabClickedAction()
VaultUnlockedNavBarTab.Generator -> generatorTabClickedAction()
VaultUnlockedNavBarTab.Settings -> settingsTabClickedAction()
}
},
)
}
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = state.route,
modifier = Modifier.padding(innerPadding),
) {
vaultDestination()
sendDestination()
generatorDestination()
settingsDestination()
}
}
}
/**
* Models tabs for the nav bar of the vault unlocked portion of the app.
*/
@Parcelize
private sealed class VaultUnlockedNavBarTab : Parcelable {
/**
* Resource id for the icon representing the tab.
*/
abstract val iconRes: Int
/**
* Resource id for the label describing the tab.
*/
abstract val labelRes: Int
/**
* Resource id for the content description describing the tab.
*/
abstract val contentDescriptionRes: Int
/**
* Route of the tab.
*/
abstract val route: String
/**
* Show the Generator screen.
*/
@Parcelize
data object Generator : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.generator_icon
override val labelRes get() = R.string.generator_label
override val contentDescriptionRes get() = R.string.generator_tab_content_description
override val route get() = GENERATOR_ROUTE
}
/**
* Show the Send screen.
*/
@Parcelize
data object Send : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.send_icon
override val labelRes get() = R.string.send_label
override val contentDescriptionRes get() = R.string.send_tab_content_description
override val route get() = SEND_ROUTE
}
/**
* Show the Vault screen.
*/
@Parcelize
data object Vault : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.sheild_icon
override val labelRes get() = R.string.vault_label
override val contentDescriptionRes get() = R.string.vault_tab_content_description
override val route get() = VAULT_ROUTE
}
/**
* Show the Settings screen.
*/
@Parcelize
data object Settings : VaultUnlockedNavBarTab() {
override val iconRes get() = R.drawable.settings_icon
override val labelRes get() = R.string.settings_label
override val contentDescriptionRes get() = R.string.settings_tab_content_description
override val route get() = SETTINGS_ROUTE
}
}
/**
* Helper function to generate [NavOptions] for [VaultUnlockedNavBarScreen].
*/
private fun NavController.vaultUnlockedNavBarScreenNavOptions(): NavOptions =
navOptions {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
/**
* The functions below should be moved to their respective feature packages once they exist.
*
* For an example of how to setup these nav extensions, see NIA project.
*/
// #region Generator
/**
* TODO: move to generator package (BIT-148)
*/
private const val GENERATOR_ROUTE = "generator"
/**
* Add generator destination to the nav graph.
*
* TODO: move to generator package (BIT-148)
*/
private fun NavGraphBuilder.generatorDestination() {
composable(GENERATOR_ROUTE) {
PlaceholderComposable(text = "Generator")
}
}
/**
* Navigate to the generator screen. Note this will only work if generator screen was added
* via [generatorDestination].
*
* TODO: move to generator package (BIT-148)
*
*/
private fun NavController.navigateToGenerator(navOptions: NavOptions? = null) {
navigate(GENERATOR_ROUTE, navOptions)
}
// #endregion Generator
// #region Send
/**
* TODO: move to send package (BIT-149)
*/
private const val SEND_ROUTE = "send"
/**
* Add send destination to the nav graph.
*
* TODO: move to send package (BIT-149)
*/
private fun NavGraphBuilder.sendDestination() {
composable(SEND_ROUTE) {
PlaceholderComposable(text = "Send")
}
}
/**
* Navigate to the send screen. Note this will only work if send screen was added
* via [sendDestination].
*
* TODO: move to send package (BIT-149)
*
*/
private fun NavController.navigateToSend(navOptions: NavOptions? = null) {
navigate(SEND_ROUTE, navOptions)
}
// #endregion Send
// #region Settings
/**
* TODO: move to settings package (BIT-147)
*/
private const val SETTINGS_ROUTE = "settings"
/**
* Add settings destination to the nav graph.
*
* TODO: move to settings package (BIT-147)
*/
private fun NavGraphBuilder.settingsDestination() {
composable(SETTINGS_ROUTE) {
PlaceholderComposable(text = "Settings")
}
}
/**
* Navigate to the generator screen. Note this will only work if generator screen was added
* via [settingsDestination].
*
* TODO: move to settings package (BIT-147)
*
*/
private fun NavController.navigateToSettings(navOptions: NavOptions? = null) {
navigate(SETTINGS_ROUTE, navOptions)
}
// #endregion Settings
// #region Vault
/**
* TODO: move to vault package (BIT-178)
*/
private const val VAULT_ROUTE = "vault"
/**
* Add vault destination to the nav graph.
*
* TODO: move to vault package (BIT-178)
*/
private fun NavGraphBuilder.vaultDestination() {
composable(VAULT_ROUTE) {
PlaceholderComposable(text = "Vault")
}
}
/**
* Navigate to the vault screen. Note this will only work if vault screen was added
* via [vaultDestination].
*
* TODO: move to vault package (BIT-178)
*
*/
private fun NavController.navigateToVault(navOptions: NavOptions? = null) {
navigate(VAULT_ROUTE, navOptions)
}
// #endregion Vault

View file

@ -0,0 +1,103 @@
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
import com.x8bit.bitwarden.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* Manages bottom tab navigation of the application.
*/
@HiltViewModel
class VaultUnlockedNavBarViewModel @Inject constructor() :
BaseViewModel<Unit, VaultUnlockedNavBarEvent, VaultUnlockedNavBarAction>(
initialState = Unit,
) {
override fun handleAction(action: VaultUnlockedNavBarAction) {
when (action) {
VaultUnlockedNavBarAction.GeneratorTabClick -> handleGeneratorTabClicked()
VaultUnlockedNavBarAction.SendTabClick -> handleSendTabClicked()
VaultUnlockedNavBarAction.SettingsTabClick -> handleSettingsTabClicked()
VaultUnlockedNavBarAction.VaultTabClick -> handleVaultTabClicked()
}
}
// #region BottomTabViewModel Action Handlers
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToGeneratorScreen] event
*/
private fun handleGeneratorTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToSendScreen] event
*/
private fun handleSendTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToSendScreen)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar] event
*/
private fun handleVaultTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar)
}
/**
* Attempts to send [VaultUnlockedNavBarEvent.NavigateToSettingsScreen] event
*/
private fun handleSettingsTabClicked() {
sendEvent(VaultUnlockedNavBarEvent.NavigateToSettingsScreen)
}
// #endregion BottomTabViewModel Action Handlers
}
/**
* Models actions for the bottom tab of the vault unlocked portion of the app.
*/
sealed class VaultUnlockedNavBarAction {
/**
* click Generator tab.
*/
data object GeneratorTabClick : VaultUnlockedNavBarAction()
/**
* click Send tab.
*/
data object SendTabClick : VaultUnlockedNavBarAction()
/**
* click Vault tab.
*/
data object VaultTabClick : VaultUnlockedNavBarAction()
/**
* click Settings tab.
*/
data object SettingsTabClick : VaultUnlockedNavBarAction()
}
/**
* Models events for the bottom tab of the vault unlocked portion of the app.
*/
sealed class VaultUnlockedNavBarEvent {
/**
* Navigate to the Generator screen.
*/
data object NavigateToGeneratorScreen : VaultUnlockedNavBarEvent()
/**
* Navigate to the Send screen.
*/
data object NavigateToSendScreen : VaultUnlockedNavBarEvent()
/**
* Navigate to the Vault screen.
*/
data object NavigateToVaultScreenNavBar : VaultUnlockedNavBarEvent()
/**
* Navigate to the Settings screen.
*/
data object NavigateToSettingsScreen : VaultUnlockedNavBarEvent()
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M320.64 512.32c141.385 0 256-114.615 256-256s-114.615-256-256-256c-141.385 0-256 114.615-256 256s114.615 256 256 256zM488 278.402c3.315 0.796 6.199 2.832 8.055 5.69 1.859 2.858 2.547 6.318 1.93 9.671-8.887 36.99-28.976 70.334-57.529 95.474s-64.173 40.845-101.99 44.976c-6.4 0.64-12.8 0.928-19.2 0.928-34.070-0.307-67.349-10.301-95.948-28.819s-51.337-44.797-65.555-75.757c-0.339-0.387-0.768-0.688-1.248-0.877-0.48-0.185-0.998-0.253-1.511-0.195-0.512 0.055-1.003 0.233-1.43 0.522s-0.78 0.672-1.027 1.127l-8.032 16.576c-1.536 3.181-4.274 5.623-7.611 6.787s-6.999 0.957-10.181-0.579c-3.183-1.536-5.624-4.275-6.789-7.613-1.164-3.334-0.955-6.998 0.581-10.179l24.96-51.49c1.492-3.055 4.099-5.423 7.284-6.614s6.706-1.114 9.836 0.214l53.12 22.4c1.613 0.675 3.078 1.66 4.311 2.901s2.209 2.712 2.874 4.33c0.664 1.618 1.003 3.351 0.998 5.1s-0.356 3.479-1.030 5.094c-0.674 1.614-1.66 3.079-2.901 4.312s-2.712 2.208-4.33 2.873c-1.618 0.663-3.351 1.002-5.1 0.998-1.749-0.007-3.48-0.358-5.093-1.031l-20.832-8.738c-2.624-0.288-3.68 1.6-3.2 2.912 13.19 29.202 35.259 53.495 63.062 69.422s59.924 22.675 91.786 19.283c32.304-3.2 62.791-16.445 87.175-37.872s41.44-49.962 48.762-81.585c0.365-1.711 1.063-3.333 2.055-4.774s2.259-2.671 3.728-3.623c1.469-0.951 3.111-1.603 4.829-1.92 1.721-0.317 3.488-0.291 5.197 0.076zM502.947 157.296c3.331-1.163 6.989-0.962 10.173 0.56 1.622 0.746 3.075 1.811 4.275 3.131 1.203 1.32 2.125 2.867 2.717 4.551s0.838 3.469 0.726 5.25c-0.115 1.781-0.582 3.521-1.383 5.117l-24.928 51.488c-1.498 3.050-4.106 5.412-7.286 6.602-3.184 1.19-6.701 1.118-9.834-0.202l-53.152-22.4c-1.613-0.677-3.079-1.665-4.31-2.907s-2.205-2.715-2.87-4.335c-0.663-1.619-0.998-3.353-0.992-5.103 0.010-1.75 0.361-3.481 1.037-5.095 0.678-1.613 1.664-3.078 2.909-4.31 1.241-1.232 2.714-2.207 4.333-2.87s3.353-1 5.104-0.993c1.75 0.007 3.481 0.359 5.094 1.036l20.832 8.704c2.592 0.288 3.68-1.6 3.2-2.912-13.216-29.171-35.293-53.431-63.094-69.329s-59.911-22.621-91.753-19.216c-32.303 3.198-62.791 16.443-87.175 37.87s-41.437 49.961-48.761 81.585c-0.73 3.458-2.804 6.486-5.765 8.415s-6.569 2.603-10.027 1.874c-3.458-0.73-6.485-2.804-8.415-5.765s-2.603-6.569-1.874-10.027c8.919-36.975 29.038-70.293 57.608-95.403s64.194-40.785 102.008-44.886c6.4-0.64 12.8-0.928 19.2-0.928 34.055 0.283 67.325 10.252 95.923 28.742s51.347 44.737 65.581 75.674c0.339 0.394 0.768 0.7 1.255 0.891 0.483 0.19 1.005 0.261 1.523 0.204 0.515-0.056 1.011-0.238 1.443-0.53 0.429-0.291 0.784-0.683 1.027-1.142l8.032-16.576c1.549-3.173 4.285-5.606 7.619-6.768z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M418.883 157.792c0 0 9.28-7.552 15.36-0.512 5.344 6.144-1.472 14.112-1.472 14.112l-176.226 194.976c-3.936 4.57-6.075 10.416-6.016 16.448l0.768 112.32c0.046 3.571 1.213 7.040 3.338 9.911s5.099 5.002 8.502 6.089c1.692 0.567 3.464 0.861 5.248 0.864 2.682 0.003 5.327-0.624 7.724-1.827 2.396-1.207 4.474-2.96 6.069-5.117l49.666-67.2c2.72-3.664 6.576-6.329 10.969-7.581 4.39-1.248 9.072-1.014 13.318 0.669l108.64 42.88c2.333 0.921 4.839 1.325 7.341 1.175 2.505-0.151 4.947-0.845 7.155-2.038 2.195-1.184 4.106-2.839 5.587-4.848 1.481-2.007 2.502-4.32 2.989-6.768l87.84-441.056c0.634-3.203 0.33-6.521-0.88-9.553s-3.27-5.65-5.936-7.535c-2.665-1.909-5.824-3.012-9.097-3.176s-6.525 0.616-9.366 2.248l-477.858 273.376c-2.722 1.565-4.957 3.854-6.457 6.613s-2.206 5.879-2.041 9.014c0.166 3.135 1.197 6.164 2.98 8.75s4.246 4.625 7.118 5.895l106.080 48.256c5.627 3.837 12.391 5.645 19.181 5.133 6.791-0.509 13.206-3.312 18.195-7.949z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M532.989 289.887l-3.872-2.528c-3.197-1.866-5.744-4.667-7.299-8.026-1.558-3.358-2.048-7.113-1.405-10.759v-24.64c-0.682-3.57-0.202-7.266 1.37-10.542s4.154-5.964 7.366-7.666l4.768-2.4c12.013-7.054 20.768-18.547 24.384-32 3.421-13.333 1.661-27.466-4.928-39.552l-25.056-43.872c-7.011-11.727-18.259-20.313-31.418-23.987-13.161-3.674-27.229-2.155-39.303 4.243l-4.384 2.208c-3.286 1.769-6.983 2.63-10.711 2.496-3.731-0.135-7.357-1.261-10.505-3.263-7.082-4.796-14.579-8.951-22.4-12.416-3.28-1.636-6.038-4.157-7.962-7.278s-2.935-6.719-2.918-10.386v-6.528c0.099-6.965-1.197-13.879-3.808-20.335s-6.49-12.326-11.401-17.264c-4.915-4.937-10.765-8.842-17.209-11.486s-13.351-3.972-20.317-3.907h-51.2c-6.952-0.043-13.842 1.301-20.267 3.954s-12.257 6.561-17.154 11.496c-4.896 4.935-8.758 10.797-11.361 17.243s-3.892 13.347-3.794 20.298v5.472c0.032 3.614-0.938 7.165-2.802 10.261s-4.55 5.614-7.758 7.275c-5.691 2.572-11.197 5.533-16.48 8.864l-6.080 3.584c-3.102 2.221-6.788 3.481-10.6 3.623s-7.582-0.839-10.84-2.823l-3.968-1.952c-5.856-3.516-12.377-5.778-19.153-6.642s-13.656-0.314-20.208 1.618c-13.446 3.716-24.885 12.58-31.84 24.672l-24.96 43.68c-3.566 6.048-5.867 12.757-6.763 19.721s-0.37 14.037 1.547 20.791c1.743 6.495 4.779 12.571 8.925 17.866s9.317 9.699 15.203 12.95l2.88 2.848 1.312 0.928c3.197 1.867 5.744 4.667 7.3 8.026s2.046 7.113 1.403 10.758v24.704c0.326 3.533-0.314 7.087-1.853 10.283s-3.918 5.913-6.883 7.861l-4.768 2.4c-11.724 7.217-20.258 18.63-23.866 31.917s-2.020 27.447 4.442 39.603l25.088 43.872c6.806 11.955 18.058 20.739 31.308 24.445 13.25 3.702 27.425 2.026 39.445-4.669l4.352-2.176c3.287-1.792 6.994-2.669 10.736-2.547 3.742 0.125 7.382 1.248 10.544 3.251 7.082 4.797 14.578 8.954 22.4 12.416 3.281 1.635 6.038 4.157 7.962 7.28 1.923 3.12 2.934 6.717 2.918 10.384v5.472c-0.102 6.954 1.185 13.859 3.788 20.31s6.468 12.317 11.368 17.251c4.901 4.938 10.738 8.845 17.169 11.495s13.327 3.987 20.282 3.936h51.2c6.957 0.051 13.856-1.286 20.288-3.936s12.272-6.557 17.175-11.491c4.902-4.938 8.771-10.8 11.379-17.251 2.605-6.451 3.897-13.357 3.798-20.313v-5.472c-0.032-3.613 0.938-7.165 2.803-10.259 1.863-3.098 4.547-5.616 7.757-7.277 5.683-2.567 11.181-5.526 16.448-8.864l1.376-0.8 4.704-2.784c3.111-2.211 6.803-3.466 10.618-3.606 3.815-0.144 7.587 0.832 10.854 2.807l3.968 1.952c5.993 3.568 12.653 5.878 19.565 6.791 6.915 0.912 13.945 0.409 20.659-1.478 6.599-1.805 12.755-4.95 18.080-9.248 5.325-4.295 9.706-9.645 12.864-15.712l24.96-43.68c3.504-5.907 5.757-12.474 6.615-19.289 0.861-6.816 0.307-13.735-1.622-20.327-3.584-13.397-12.298-24.846-24.256-31.873zM319.997 346.752c-17.949 0-35.495-5.322-50.419-15.296-14.924-9.971-26.556-24.144-33.424-40.727s-8.666-34.83-5.165-52.434c3.502-17.604 12.145-33.775 24.837-46.466s28.862-21.335 46.466-24.837c17.604-3.502 35.852-1.704 52.434 5.164s30.755 18.501 40.73 33.425c9.971 14.924 15.293 32.47 15.293 50.419 0 24.069-9.562 47.153-26.579 64.17-17.021 17.021-40.103 26.582-64.173 26.582z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="24dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M526.976 6.401c-1.929-2.029-4.253-3.644-6.826-4.745-2.576-1.101-5.351-1.663-8.151-1.655h-383.998c-2.802-0.016-5.576 0.543-8.153 1.645s-4.898 2.719-6.823 4.755c-2.031 1.928-3.647 4.252-4.748 6.827s-1.663 5.348-1.653 8.149v256c0.075 19.489 3.855 38.786 11.136 56.863 6.93 17.843 16.24 34.669 27.68 50.016 11.758 15.37 24.924 29.606 39.328 42.528 13.368 12.256 27.44 23.721 42.144 34.336 12.8 9.088 26.24 17.696 40.32 25.824s24.021 13.623 29.824 16.48c5.856 2.88 10.592 5.152 14.112 6.656 2.752 1.325 5.778 1.984 8.83 1.92 3.011 0.042 5.987-0.653 8.672-2.016 3.584-1.568 8.256-3.776 14.176-6.656s16-8.384 29.824-16.48c13.824-8.096 27.424-16.736 40.32-25.824 14.723-10.618 28.816-22.083 42.208-34.336 14.419-12.906 27.587-27.146 39.328-42.528 11.43-15.353 20.739-32.176 27.68-50.016 7.293-18.074 11.072-37.373 11.136-56.863v-256c0.013-2.784-0.544-5.541-1.641-8.101-1.095-2.559-2.704-4.867-4.726-6.779v0zM477.472 279.712c0 92.799-157.472 172.512-157.472 172.512v-397.375h157.472v224.864z" />
</vector>

View file

@ -17,4 +17,14 @@
<string name="log_in_or_create_account">Log in or create a new account to access your secure vault.</string>
<string name="new_around_here">New around here?</string>
<string name="remember_me">Remember me</string>
<!--Bottom Navigation-->
<string name="generator_tab_content_description">Press to navigate to the generator screen.</string>
<string name="send_tab_content_description">Press to navigate to the send screen.</string>
<string name="vault_tab_content_description">Press to navigate to the vault screen.</string>
<string name="settings_tab_content_description">Press to navigate to the settings screen.</string>
<string name="vault_label">Vaults</string>
<string name="send_label">Send</string>
<string name="generator_label">Generator</string>
<string name="settings_label">Settings</string>
</resources>

View file

@ -0,0 +1,70 @@
package com.x8bit.bitwarden.example.ui.feature.vaultunlockednavbar
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.x8bit.bitwarden.example.ui.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarAction
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarScreen
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarViewModel
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
@Test
fun `vault tab click should send VaultTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
)
}
onNodeWithTag("vault").performClick()
}
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick) }
}
@Test
fun `send tab click should send SendTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
)
}
onNodeWithTag("send").performClick()
}
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick) }
}
@Test
fun `generator tab click should send GeneratorTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
)
}
onNodeWithTag("generator").performClick()
}
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }
}
@Test
fun `settings tab click should send SendTabClick action`() {
val viewModel = mockk<VaultUnlockedNavBarViewModel>(relaxed = true)
composeTestRule.apply {
setContent {
VaultUnlockedNavBarScreen(
viewModel = viewModel,
)
}
onNodeWithTag("settings").performClick()
}
verify { viewModel.trySendAction(VaultUnlockedNavBarAction.SettingsTabClick) }
}
}

View file

@ -0,0 +1,48 @@
package com.x8bit.bitwarden.example.ui.feature.vaultunlockednavbar
import app.cash.turbine.test
import com.x8bit.bitwarden.example.ui.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarAction
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarEvent
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavBarViewModel
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultUnlockedNavBarViewModelTest : BaseViewModelTest() {
@Test
fun `VaultTabClick should navigate to the vault screen`() = runTest {
val viewModel = VaultUnlockedNavBarViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultUnlockedNavBarAction.VaultTabClick)
assertEquals(VaultUnlockedNavBarEvent.NavigateToVaultScreenNavBar, awaitItem())
}
}
@Test
fun `SendTabClick should navigate to the send screen`() = runTest {
val viewModel = VaultUnlockedNavBarViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(VaultUnlockedNavBarAction.SendTabClick)
assertEquals(VaultUnlockedNavBarEvent.NavigateToSendScreen, awaitItem())
}
}
@Test
fun `GeneratorTabClick should navigate to the generator screen`() = runTest {
val viewModel = VaultUnlockedNavBarViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.GeneratorTabClick)
assertEquals(VaultUnlockedNavBarEvent.NavigateToGeneratorScreen, awaitItem())
}
}
@Test
fun `SettingsTabClick should navigate to the settings screen`() = runTest {
val viewModel = VaultUnlockedNavBarViewModel()
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultUnlockedNavBarAction.SettingsTabClick)
assertEquals(VaultUnlockedNavBarEvent.NavigateToSettingsScreen, awaitItem())
}
}
}

View file

@ -2,5 +2,6 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.ksp) apply false
}

View file

@ -91,5 +91,6 @@ android-application = { id = "com.android.application", version.ref = "androidGr
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }