PM-27703: Update Authenticator navigation (#6109)

This commit is contained in:
David Perez 2025-11-03 10:28:23 -06:00 committed by GitHub
parent b1195b5f46
commit 845a5dec22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 244 additions and 294 deletions

View File

@ -36,7 +36,7 @@ import com.x8bit.bitwarden.ui.platform.composition.LocalManagerProvider
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.debugMenuDestination
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.x8bit.bitwarden.ui.platform.feature.rootnav.ROOT_ROUTE
import com.x8bit.bitwarden.ui.platform.feature.rootnav.RootNavigationRoute
import com.x8bit.bitwarden.ui.platform.feature.rootnav.rootNavDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.model.AuthTabLaunchers
@ -119,11 +119,11 @@ class MainActivity : AppCompatActivity() {
) {
NavHost(
navController = navController,
startDestination = ROOT_ROUTE,
startDestination = RootNavigationRoute,
) {
// Nothing else should end up at this top level, we just want the ability
// to have the debug menu appear on top of the rest of the app without
// interacting with the state-based navigation used by the RootNavScreen.
// Both root navigation and debug menu exist at this top level.
// The debug menu can appear on top of the rest of the app without
// interacting with the state-based navigation used by RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },

View File

@ -1,12 +1,17 @@
@file:OmitFromCoverage
package com.x8bit.bitwarden.ui.platform.feature.rootnav
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.Serializable
/**
* The route for the root navigation screen.
* The type-safe route for the root navigation screen.
*/
const val ROOT_ROUTE: String = "root"
@Serializable
data object RootNavigationRoute
/**
* Add the root navigation screen to the nav graph.
@ -14,7 +19,7 @@ const val ROOT_ROUTE: String = "root"
fun NavGraphBuilder.rootNavDestination(
onSplashScreenRemoved: () -> Unit,
) {
composable(route = ROOT_ROUTE) {
composable<RootNavigationRoute> {
RootNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
}
}

View File

@ -11,15 +11,12 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navOptions
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.navigateToTabOrRoot
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.scaffold.model.ScaffoldNavigationData
@ -29,21 +26,17 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavContr
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.settings.about.navigateToAbout
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.navigateToAutoFill
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraphRoot
import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.model.VaultUnlockedNavBarTab
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorGraph
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
import com.x8bit.bitwarden.ui.tools.feature.send.navigateToSendGraph
import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.importitems.navigateToImportItemsScreen
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute
import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVaultGraph
import com.x8bit.bitwarden.ui.vault.feature.vault.vaultGraph
import kotlinx.collections.immutable.persistentListOf
@ -81,41 +74,7 @@ fun VaultUnlockedNavBarScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
when (event) {
is VaultUnlockedNavBarEvent.Shortcut.NavigateToVaultScreen,
is VaultUnlockedNavBarEvent.NavigateToVaultScreen,
-> {
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
navigateToVaultGraph(navOptions = it)
}
}
VaultUnlockedNavBarEvent.Shortcut.NavigateToSendScreen,
VaultUnlockedNavBarEvent.NavigateToSendScreen,
-> {
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
navigateToSendGraph(navOptions = it)
}
}
VaultUnlockedNavBarEvent.Shortcut.NavigateToGeneratorScreen,
VaultUnlockedNavBarEvent.NavigateToGeneratorScreen,
-> {
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
navigateToGeneratorGraph(navOptions = it)
}
}
VaultUnlockedNavBarEvent.Shortcut.NavigateToSettingsScreen,
VaultUnlockedNavBarEvent.NavigateToSettingsScreen,
-> {
navigateToTabOrRoot(tabToNavigateTo = event.tab) {
navigateToSettingsGraph(navOptions = it)
}
}
}
}
navController.navigateToTabOrRoot(target = event.tab)
}
VaultUnlockedNavBarScaffold(
@ -279,37 +238,6 @@ private fun VaultUnlockedNavBarScaffold(
}
}
/**
* Helper function to determine how to navigate to a specified [VaultUnlockedNavBarTab].
* If direct navigation is required, the [navigate] lambda will be invoked with the appropriate
* [NavOptions].
*/
@Suppress("MaxLineLength")
private fun NavController.navigateToTabOrRoot(
tabToNavigateTo: VaultUnlockedNavBarTab,
navigate: (NavOptions) -> Unit,
) {
if (tabToNavigateTo.startDestinationRoute.toObjectNavigationRoute() == currentDestination?.route) {
// We are at the start destination already, so nothing to do.
return
} else if (currentDestination?.parent?.route == tabToNavigateTo.graphRoute.toObjectNavigationRoute()) {
// We are not at the start destination but we are in the correct graph,
// so lets pop up to the start destination.
popBackStack(route = tabToNavigateTo.startDestinationRoute, inclusive = false)
} else {
// We are not in correct graph at all, so navigate there.
navigate(
navOptions {
popUpTo(id = graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
},
)
}
}
/**
* Determine if the current destination is the same as the given tab.
*/

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dialogDimBackgroundAmount" format="float">0.75</dimen>
<color name="windowBackground">@android:color/transparent</color>
</resources>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dialogDimBackgroundAmount" format="float">0.55</dimen>
<!-- default -->
<integer name="displayCutoutMode">0</integer>
<color name="windowBackground">@android:color/transparent</color>

View File

@ -10,24 +10,26 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.ui.platform.composition.LocalManagerProvider
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.debugMenuDestination
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.manager.DebugMenuLaunchManager
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.navigateToDebugMenuScreen
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavScreen
import com.bitwarden.authenticator.ui.platform.feature.rootnav.RootNavigationRoute
import com.bitwarden.authenticator.ui.platform.feature.rootnav.rootNavDestination
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.platform.util.setupEdgeToEdge
import com.bitwarden.ui.platform.util.validate
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
/**
@ -61,20 +63,28 @@ class MainActivity : AppCompatActivity() {
AppCompatDelegate.setDefaultNightMode(settingsRepository.appTheme.osValue)
setupEdgeToEdge(appThemeFlow = mainViewModel.stateFlow.map { it.theme })
setContent {
val navController = rememberNavController()
SetupEventsEffect(navController = navController)
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
updateScreenCapture(isScreenCaptureAllowed = state.isScreenCaptureAllowed)
val navController = rememberNavController()
observeViewModelEvents(navController)
LocalManagerProvider {
BitwardenTheme(
theme = state.theme,
dynamicColor = state.isDynamicColorsEnabled,
) {
RootNavScreen(
NavHost(
navController = navController,
onSplashScreenRemoved = { shouldShowSplashScreen = false },
onExitApplication = { finishAffinity() },
)
startDestination = RootNavigationRoute,
) {
// Both root navigation and debug menu exist at this top level.
// The debug menu can appear on top of the rest of the app without
// interacting with the state-based navigation used by RootNavScreen.
rootNavDestination { shouldShowSplashScreen = false }
debugMenuDestination(
onNavigateBack = { navController.popBackStack() },
onSplashScreenRemoved = { shouldShowSplashScreen = false },
)
}
}
}
}
@ -92,18 +102,16 @@ class MainActivity : AppCompatActivity() {
mainViewModel.trySendAction(MainAction.ReceiveNewIntent(intent = newIntent))
}
private fun observeViewModelEvents(navController: NavHostController) {
mainViewModel
.eventFlow
.onEach { event ->
when (event) {
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.UpdateAppTheme -> {
AppCompatDelegate.setDefaultNightMode(event.osTheme)
}
@Composable
private fun SetupEventsEffect(navController: NavHostController) {
EventsEffect(viewModel = mainViewModel) { event ->
when (event) {
MainEvent.NavigateToDebugMenu -> navController.navigateToDebugMenuScreen()
is MainEvent.UpdateAppTheme -> {
AppCompatDelegate.setDefaultNightMode(event.osTheme)
}
}
.launchIn(lifecycleScope)
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean = debugLaunchManager

View File

@ -1,19 +1,24 @@
@file:OmitFromCoverage
package com.bitwarden.authenticator.ui.authenticator.feature.authenticator
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.bitwarden.annotation.OmitFromCoverage
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.editItemDestination
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.navigateToEditItem
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.manualCodeEntryDestination
import com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.navigateToManualCodeEntryScreen
import com.bitwarden.authenticator.ui.authenticator.feature.navbar.AuthenticatorNavbarRoute
import com.bitwarden.authenticator.ui.authenticator.feature.navbar.authenticatorNavBarDestination
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateToQrCodeScanScreen
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination
import com.bitwarden.authenticator.ui.authenticator.feature.search.itemSearchDestination
import com.bitwarden.authenticator.ui.authenticator.feature.search.navigateToSearch
import com.bitwarden.authenticator.ui.platform.feature.settings.export.navigateToExport
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.navigateToImporting
import com.bitwarden.authenticator.ui.platform.feature.tutorial.navigateToSettingsTutorial
import com.bitwarden.authenticator.ui.platform.feature.tutorial.tutorialSettingsDestination
import kotlinx.serialization.Serializable
/**
@ -34,39 +39,41 @@ fun NavController.navigateToAuthenticatorGraph(navOptions: NavOptions? = null) {
*/
fun NavGraphBuilder.authenticatorGraph(
navController: NavController,
onNavigateBack: () -> Unit,
) {
navigation<AuthenticatorGraphRoute>(
startDestination = AuthenticatorNavbarRoute,
) {
authenticatorNavBarDestination(
onNavigateBack = onNavigateBack,
onNavigateBack = { navController.popBackStack() },
onNavigateToSearch = { navController.navigateToSearch() },
onNavigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() },
onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() },
onNavigateToEditItem = { navController.navigateToEditItem(itemId = it) },
onNavigateToExport = { navController.navigateToExport() },
onNavigateToImport = { navController.navigateToImporting() },
onNavigateToTutorial = { navController.navigateToSettingsTutorial() },
)
itemListingGraph(
navController = navController,
navigateBack = onNavigateBack,
navigateToSearch = {
navController.navigateToSearch()
},
navigateToQrCodeScanner = {
navController.navigateToQrCodeScanScreen()
},
navigateToManualKeyEntry = {
editItemDestination(
onNavigateBack = { navController.popBackStack() },
)
itemSearchDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToEdit = { navController.navigateToEditItem(itemId = it) },
)
qrCodeScanDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToManualCodeEntryScreen = {
navController.popBackStack()
navController.navigateToManualCodeEntryScreen()
},
navigateToEditItem = {
navController.navigateToEditItem(itemId = it)
)
manualCodeEntryDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToQrCodeScreen = {
navController.popBackStack()
navController.navigateToQrCodeScanScreen()
},
navigateToExport = { navController.navigateToExport() },
navigateToImport = { navController.navigateToImporting() },
navigateToTutorial = { navController.navigateToSettingsTutorial() },
)
tutorialSettingsDestination(
onTutorialFinished = { navController.popBackStack() },
)
}
}

View File

@ -5,7 +5,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import kotlinx.serialization.Serializable
/**
@ -37,7 +37,7 @@ fun SavedStateHandle.toEditItemArgs(): EditItemArgs {
fun NavGraphBuilder.editItemDestination(
onNavigateBack: () -> Unit = { },
) {
composableWithPushTransitions<EditItemRoute> {
composableWithSlideTransitions<EditItemRoute> {
EditItemScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -4,13 +4,6 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.editItemDestination
import com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.manualCodeEntryDestination
import com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.navigateToManualCodeEntryScreen
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateToQrCodeScanScreen
import com.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination
import com.bitwarden.authenticator.ui.authenticator.feature.search.itemSearchDestination
import com.bitwarden.authenticator.ui.platform.feature.settings.settingsGraph
import kotlinx.serialization.Serializable
/**
@ -22,54 +15,22 @@ data object ItemListingGraphRoute
/**
* Add the item listing graph to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.itemListingGraph(
navController: NavController,
navigateBack: () -> Unit,
navigateToSearch: () -> Unit,
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (String) -> Unit,
navigateToExport: () -> Unit,
navigateToImport: () -> Unit,
navigateToTutorial: () -> Unit,
onNavigateBack: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (String) -> Unit,
) {
navigation<ItemListingGraphRoute>(
startDestination = ItemListingRoute,
) {
itemListingDestination(
onNavigateBack = navigateBack,
onNavigateToSearch = navigateToSearch,
onNavigateToQrCodeScanner = navigateToQrCodeScanner,
onNavigateToManualKeyEntry = navigateToManualKeyEntry,
onNavigateToEditItemScreen = navigateToEditItem,
)
editItemDestination(
onNavigateBack = { navController.popBackStack() },
)
itemSearchDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToEdit = navigateToEditItem,
)
qrCodeScanDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToManualCodeEntryScreen = {
navController.popBackStack()
navController.navigateToManualCodeEntryScreen()
},
)
manualCodeEntryDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToQrCodeScreen = {
navController.popBackStack()
navController.navigateToQrCodeScanScreen()
},
)
settingsGraph(
navController = navController,
onNavigateToExport = navigateToExport,
onNavigateToImport = navigateToImport,
onNavigateToTutorial = navigateToTutorial,
onNavigateBack = onNavigateBack,
onNavigateToSearch = onNavigateToSearch,
onNavigateToQrCodeScanner = onNavigateToQrCodeScanner,
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
onNavigateToEditItemScreen = onNavigateToEditItem,
)
}
}

View File

@ -1,7 +1,7 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting
import androidx.navigation.NavGraphBuilder
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
/**
@ -20,7 +20,7 @@ fun NavGraphBuilder.itemListingDestination(
onNavigateToManualKeyEntry: () -> Unit = { },
onNavigateToEditItemScreen: (id: String) -> Unit = { },
) {
composableWithPushTransitions<ItemListingRoute> {
composableWithRootPushTransitions<ItemListingRoute> {
ItemListingScreen(
onNavigateBack = onNavigateBack,
onNavigateToSearch = onNavigateToSearch,

View File

@ -20,8 +20,6 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToExport: () -> Unit,
onNavigateToImport: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
composableWithStayTransitions<AuthenticatorNavbarRoute> {
@ -31,8 +29,6 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToQrCodeScanner = onNavigateToQrCodeScanner,
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
onNavigateToEditItem = onNavigateToEditItem,
onNavigateToExport = onNavigateToExport,
onNavigateToImport = onNavigateToImport,
onNavigateToTutorial = onNavigateToTutorial,
)
}

View File

@ -2,7 +2,6 @@ package com.bitwarden.authenticator.ui.authenticator.feature.navbar
import android.os.Parcelable
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -11,22 +10,20 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingGraphRoute
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingRoute
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.navigateToItemListGraph
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsGraphRoute
import com.bitwarden.authenticator.ui.platform.feature.settings.SettingsRoute
import com.bitwarden.authenticator.ui.platform.feature.settings.navigateToSettingsGraph
import com.bitwarden.authenticator.ui.platform.feature.settings.export.navigateToExport
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.navigateToImporting
import com.bitwarden.authenticator.ui.platform.feature.settings.settingsGraph
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.navigateToTabOrRoot
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.scaffold.model.ScaffoldNavigationData
@ -52,23 +49,10 @@ fun AuthenticatorNavBarScreen(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToExport: () -> Unit,
onNavigateToImport: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
val navOptions = navController.authenticatorNavBarScreenNavOptions()
when (event) {
AuthenticatorNavBarEvent.NavigateToSettings -> {
navigateToSettingsGraph(navOptions)
}
AuthenticatorNavBarEvent.NavigateToVerificationCodes -> {
navigateToItemListGraph(navOptions)
}
}
}
navController.navigateToTabOrRoot(target = event.tab)
}
LaunchedEffect(Unit) {
@ -93,13 +77,10 @@ fun AuthenticatorNavBarScreen(
navigateToQrCodeScanner = onNavigateToQrCodeScanner,
navigateToManualKeyEntry = onNavigateToManualKeyEntry,
navigateToEditItem = onNavigateToEditItem,
navigateToExport = onNavigateToExport,
navigateToImport = onNavigateToImport,
navigateToTutorial = onNavigateToTutorial,
onNavigateToTutorial = onNavigateToTutorial,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AuthenticatorNavBarScaffold(
navController: NavHostController,
@ -110,9 +91,7 @@ private fun AuthenticatorNavBarScaffold(
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (itemId: String) -> Unit,
navigateToExport: () -> Unit,
navigateToImport: () -> Unit,
navigateToTutorial: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
var shouldDimNavBar by rememberSaveable { mutableStateOf(value = false) }
@ -144,15 +123,17 @@ private fun AuthenticatorNavBarScaffold(
popExitTransition = RootTransitionProviders.Exit.fadeOut,
) {
itemListingGraph(
navController = navController,
navigateBack = navigateBack,
navigateToSearch = navigateToSearch,
navigateToQrCodeScanner = navigateToQrCodeScanner,
navigateToManualKeyEntry = navigateToManualKeyEntry,
navigateToEditItem = navigateToEditItem,
navigateToExport = navigateToExport,
navigateToImport = navigateToImport,
navigateToTutorial = navigateToTutorial,
onNavigateBack = navigateBack,
onNavigateToSearch = navigateToSearch,
onNavigateToQrCodeScanner = navigateToQrCodeScanner,
onNavigateToManualKeyEntry = navigateToManualKeyEntry,
onNavigateToEditItem = navigateToEditItem,
)
settingsGraph(
onNavigateBack = { navController.popBackStack() },
onNavigateToTutorial = onNavigateToTutorial,
onNavigateToExport = { navController.navigateToExport() },
onNavigateToImport = { navController.navigateToImporting() },
)
}
}
@ -171,8 +152,9 @@ private fun AuthenticatorNavBarScaffold(
* @property iconResSelected The resource ID for the icon representing the tab when it's selected.
*/
@Parcelize
private sealed class AuthenticatorNavBarTab : NavigationItem, Parcelable {
sealed class AuthenticatorNavBarTab : NavigationItem, Parcelable {
@Suppress("UndocumentedPublicClass")
companion object {
/**
* The list of navigation tabs available in the authenticator.
@ -214,18 +196,6 @@ private sealed class AuthenticatorNavBarTab : NavigationItem, Parcelable {
}
}
/**
* Helper function to generate [NavOptions] for [AuthenticatorNavBarScreen].
*/
private fun NavController.authenticatorNavBarScreenNavOptions(): NavOptions =
navOptions {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
/**
* Determine if the current destination is the same as the given tab.
*/

View File

@ -46,15 +46,25 @@ class AuthenticatorNavBarViewModel @Inject constructor(
* Models events for the [AuthenticatorNavBarViewModel].
*/
sealed class AuthenticatorNavBarEvent {
/**
* The [AuthenticatorNavBarTab] to be associated with the event.
*/
abstract val tab: AuthenticatorNavBarTab
/**
* Navigate to the verification codes screen.
*/
data object NavigateToVerificationCodes : AuthenticatorNavBarEvent()
data object NavigateToVerificationCodes : AuthenticatorNavBarEvent() {
override val tab: AuthenticatorNavBarTab = AuthenticatorNavBarTab.VerificationCodes
}
/**
* Navigate to the settings screen.
*/
data object NavigateToSettings : AuthenticatorNavBarEvent()
data object NavigateToSettings : AuthenticatorNavBarEvent() {
override val tab: AuthenticatorNavBarTab = AuthenticatorNavBarTab.Settings
}
}
/**

View File

@ -23,10 +23,13 @@ fun NavController.navigateToDebugMenuScreen() {
/**
* Add the setup unlock screen to the nav graph.
*/
fun NavGraphBuilder.setupDebugMenuDestination(
fun NavGraphBuilder.debugMenuDestination(
onNavigateBack: () -> Unit,
onSplashScreenRemoved: () -> Unit,
) {
composableWithPushTransitions<DebugRoute> {
DebugMenuScreen(onNavigateBack = onNavigateBack)
// If we are displaying the debug screen, then we can just hide the splash screen.
onSplashScreenRemoved()
}
}

View File

@ -0,0 +1,25 @@
@file:OmitFromCoverage
package com.bitwarden.authenticator.ui.platform.feature.rootnav
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.bitwarden.annotation.OmitFromCoverage
import kotlinx.serialization.Serializable
/**
* The type-safe route for the root navigation screen.
*/
@Serializable
data object RootNavigationRoute
/**
* Add the root navigation screen to the nav graph.
*/
fun NavGraphBuilder.rootNavDestination(
onSplashScreenRemoved: () -> Unit,
) {
composable<RootNavigationRoute> {
RootNavScreen(onSplashScreenRemoved = onSplashScreenRemoved)
}
}

View File

@ -19,7 +19,6 @@ import com.bitwarden.authenticator.ui.auth.unlock.unlockDestination
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.AuthenticatorGraphRoute
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.authenticatorGraph
import com.bitwarden.authenticator.ui.authenticator.feature.authenticator.navigateToAuthenticatorGraph
import com.bitwarden.authenticator.ui.platform.feature.debugmenu.setupDebugMenuDestination
import com.bitwarden.authenticator.ui.platform.feature.splash.SplashRoute
import com.bitwarden.authenticator.ui.platform.feature.splash.navigateToSplash
import com.bitwarden.authenticator.ui.platform.feature.splash.splashDestination
@ -42,8 +41,7 @@ import java.util.concurrent.atomic.AtomicReference
fun RootNavScreen(
viewModel: RootNavViewModel = hiltViewModel(),
navController: NavHostController = rememberNavController(),
onSplashScreenRemoved: () -> Unit = {},
onExitApplication: () -> Unit,
onSplashScreenRemoved: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsState()
val previousStateReference = remember { AtomicReference(state) }
@ -82,15 +80,7 @@ fun RootNavScreen(
viewModel.trySendAction(RootNavAction.Internal.AppUnlocked)
},
)
setupDebugMenuDestination(
onNavigateBack = {
navController.popBackStack()
},
)
authenticatorGraph(
navController = navController,
onNavigateBack = onExitApplication,
)
authenticatorGraph(navController = navController)
}
val targetRoute = when (state.navState) {

View File

@ -6,7 +6,6 @@ import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.bitwarden.authenticator.ui.platform.feature.settings.export.exportDestination
import com.bitwarden.authenticator.ui.platform.feature.settings.importing.importingDestination
import com.bitwarden.authenticator.ui.platform.feature.tutorial.tutorialSettingsDestination
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import kotlinx.serialization.Serializable
@ -23,32 +22,44 @@ data object SettingsGraphRoute
data object SettingsRoute
/**
* Add settings graph to the nav graph.
* Add settings destination to the nav graph.
*/
fun NavGraphBuilder.settingsGraph(
navController: NavController,
fun NavGraphBuilder.settingsDestination(
onNavigateToExport: () -> Unit,
onNavigateToImport: () -> Unit,
onNavigateToTutorial: () -> Unit,
) {
composableWithRootPushTransitions<SettingsRoute> {
SettingsScreen(
onNavigateToTutorial = onNavigateToTutorial,
onNavigateToExport = onNavigateToExport,
onNavigateToImport = onNavigateToImport,
)
}
}
/**
* Add settings graph to the nav graph.
*/
fun NavGraphBuilder.settingsGraph(
onNavigateBack: () -> Unit,
onNavigateToTutorial: () -> Unit,
onNavigateToExport: () -> Unit,
onNavigateToImport: () -> Unit,
) {
navigation<SettingsGraphRoute>(
startDestination = SettingsRoute,
) {
composableWithRootPushTransitions<SettingsRoute> {
SettingsScreen(
onNavigateToTutorial = onNavigateToTutorial,
onNavigateToExport = onNavigateToExport,
onNavigateToImport = onNavigateToImport,
)
}
tutorialSettingsDestination(
onTutorialFinished = { navController.popBackStack() },
settingsDestination(
onNavigateToTutorial = onNavigateToTutorial,
onNavigateToExport = onNavigateToExport,
onNavigateToImport = onNavigateToImport,
)
exportDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateBack = onNavigateBack,
)
importingDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateBack = onNavigateBack,
)
}
}

View File

@ -57,6 +57,7 @@ import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.bitwarden.ui.platform.components.row.BitwardenPushRow
import com.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@ -333,37 +334,21 @@ private fun ColumnScope.VaultSettings(
label = stringResource(id = BitwardenString.data),
)
Spacer(modifier = Modifier.height(height = 8.dp))
BitwardenTextRow(
BitwardenPushRow(
text = stringResource(id = BitwardenString.import_vault),
onClick = onImportClick,
cardStyle = CardStyle.Top(),
modifier = Modifier
.standardHorizontalMargin()
.testTag("Import"),
cardStyle = CardStyle.Top(),
content = {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
)
},
)
BitwardenTextRow(
BitwardenPushRow(
text = stringResource(id = BitwardenString.export),
onClick = onExportClick,
cardStyle = CardStyle.Middle(),
modifier = Modifier
.standardHorizontalMargin()
.testTag("Export"),
cardStyle = CardStyle.Middle(),
content = {
Icon(
modifier = Modifier.mirrorIfRtl(),
painter = painterResource(id = BitwardenDrawable.ic_chevron_right),
contentDescription = null,
tint = BitwardenTheme.colorScheme.icon.primary,
)
},
)
BitwardenExternalLinkRow(
text = stringResource(BitwardenString.backup),

View File

@ -3,7 +3,7 @@ package com.bitwarden.authenticator.ui.platform.feature.settings.export
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
/**
@ -18,7 +18,7 @@ data object ExportRoute
fun NavGraphBuilder.exportDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions<ExportRoute> {
composableWithPushTransitions<ExportRoute> {
ExportScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -139,8 +139,8 @@ fun ExportScreen(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.export),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
navigationIcon = painterResource(id = BitwardenDrawable.ic_back),
navigationIconContentDescription = stringResource(id = BitwardenString.back),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(ExportAction.CloseButtonClick)

View File

@ -3,7 +3,7 @@ package com.bitwarden.authenticator.ui.platform.feature.settings.importing
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import kotlinx.serialization.Serializable
/**
@ -18,7 +18,7 @@ data object ImportRoute
fun NavGraphBuilder.importingDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions<ImportRoute> {
composableWithPushTransitions<ImportRoute> {
ImportingScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -123,8 +123,8 @@ fun ImportingScreen(
BitwardenTopAppBar(
title = stringResource(id = BitwardenString.import_vault),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(id = BitwardenString.close),
navigationIcon = painterResource(id = BitwardenDrawable.ic_back),
navigationIconContentDescription = stringResource(id = BitwardenString.back),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(ImportAction.CloseButtonClick)

View File

@ -3,7 +3,8 @@ package com.bitwarden.authenticator.ui.platform.feature.tutorial
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.bitwarden.ui.platform.base.util.composableWithStayTransitions
import kotlinx.serialization.Serializable
/**
@ -22,7 +23,7 @@ data object SettingsTutorialRoute
* Add the top level Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
composable<TutorialRoute> {
composableWithStayTransitions<TutorialRoute> {
TutorialScreen(
onTutorialFinished = onTutorialFinished,
)
@ -33,7 +34,7 @@ fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
* Add the Settings Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -> Unit) {
composable<SettingsTutorialRoute> {
composableWithSlideTransitions<SettingsTutorialRoute> {
TutorialScreen(
onTutorialFinished = onTutorialFinished,
)

View File

@ -7,6 +7,8 @@
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionModeOverlay">true</item>
<item name="android:windowLayoutInDisplayCutoutMode">default</item>
<item name="android:backgroundDimAmount">@dimen/dialogDimBackgroundAmount</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<!-- Launch theme (for auto dark/light based on system) -->

View File

@ -0,0 +1,42 @@
package com.bitwarden.ui.platform.base.util
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavOptions
import androidx.navigation.navOptions
import com.bitwarden.ui.platform.components.navigation.model.NavigationItem
import com.bitwarden.ui.platform.util.toObjectNavigationRoute
/**
* A helper function to determine how to navigate to a specified [NavigationItem].
*
* This function intelligently handles navigation based on the current destination:
* - If already at the target start destination, no action is taken.
* - If in the correct graph but not at start, pops back to start destination of the graph.
* - Otherwise, navigates to the target graph with appropriate [NavOptions].
*
* @param target The [NavigationItem] representing the desired navigation target
*/
fun NavController.navigateToTabOrRoot(target: NavigationItem) {
if (target.startDestinationRoute.toObjectNavigationRoute() == currentDestination?.route) {
// We are at the start destination already, so nothing to do.
return
} else if (target.graphRoute.toObjectNavigationRoute() == currentDestination?.parent?.route) {
// We are not at the start destination but we are in the correct graph,
// so lets pop up to the start destination.
popBackStack(route = target.startDestinationRoute, inclusive = false)
return
} else {
// We are not in correct graph at all, so navigate there.
navigate(
route = target.graphRoute,
navOptions = navOptions {
popUpTo(id = graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
},
)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dialogDimBackgroundAmount">0.75</dimen>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="dialogDimBackgroundAmount">0.55</dimen>
</resources>