mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 08:55:48 +03:00
BIT-143: Add initial bottom navigation screen (#25)
Co-authored-by: Brian Yencho <brian@livefront.com>
This commit is contained in:
parent
dc48420820
commit
69feff2dcd
16 changed files with 666 additions and 0 deletions
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
9
app/src/main/res/drawable/generator_icon.xml
Normal file
9
app/src/main/res/drawable/generator_icon.xml
Normal 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>
|
9
app/src/main/res/drawable/send_icon.xml
Normal file
9
app/src/main/res/drawable/send_icon.xml
Normal 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>
|
9
app/src/main/res/drawable/settings_icon.xml
Normal file
9
app/src/main/res/drawable/settings_icon.xml
Normal 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>
|
9
app/src/main/res/drawable/sheild_icon.xml
Normal file
9
app/src/main/res/drawable/sheild_icon.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in a new issue