From 6fec95cb8459347742664d39266d5376eba0ecf4 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 6 May 2025 15:46:53 -0500 Subject: [PATCH] PM-21255: Implement type-safe navigation (#5131) --- .../accountsetup/SetupAutoFillNavigation.kt | 77 +++--- .../accountsetup/SetupAutoFillViewModel.kt | 2 +- .../accountsetup/SetupCompleteNavigation.kt | 12 +- .../accountsetup/SetupUnlockNavigation.kt | 72 +++--- .../accountsetup/SetupUnlockViewModel.kt | 2 +- .../ui/auth/feature/auth/AuthNavigation.kt | 26 +- .../checkemail/CheckEmailNavigation.kt | 34 +-- .../feature/checkemail/CheckEmailViewModel.kt | 2 +- .../CompleteRegistrationNavigation.kt | 54 ++-- .../CompleteRegistrationViewModel.kt | 2 +- .../createaccount/CreateAccountNavigation.kt | 13 +- .../EnterpriseSignOnNavigation.kt | 38 +-- .../EnterpriseSignOnViewModel.kt | 8 +- .../environment/EnvironmentNavigation.kt | 19 +- .../ExpiredRegistrationLinkNavigation.kt | 13 +- .../auth/feature/landing/LandingNavigation.kt | 13 +- .../ui/auth/feature/login/LoginNavigation.kt | 44 ++-- .../ui/auth/feature/login/LoginViewModel.kt | 31 ++- .../LoginWithDeviceNavigation.kt | 43 ++-- .../LoginWithDeviceViewModel.kt | 6 +- .../MasterPasswordGeneratorNavigation.kt | 13 +- .../MasterPasswordGuidanceNavigation.kt | 13 +- .../MasterPasswordHintNavigation.kt | 37 +-- .../MasterPasswordHintViewModel.kt | 6 +- .../PreventAccountLockoutNavigation.kt | 13 +- .../RemovePasswordNavigation.kt | 12 +- .../resetpassword/ResetPasswordNavigation.kt | 13 +- .../setpassword/SetPasswordNavigation.kt | 13 +- .../StartRegistrationNavigation.kt | 13 +- .../TrustedDeviceEncryptionNavigation.kt | 14 +- .../trusteddevice/TrustedDeviceNavigation.kt | 12 +- .../TwoFactorLoginNavigation.kt | 74 +++--- .../twofactorlogin/TwoFactorLoginViewModel.kt | 2 +- .../vaultunlock/VaultUnlockNavigation.kt | 69 +++-- .../vaultunlock/VaultUnlockViewModel.kt | 2 +- .../auth/feature/welcome/WelcomeNavigation.kt | 13 +- .../base/util/NavGraphBuilderExtensions.kt | 60 ++--- .../feature/debugmenu/DebugMenuNavigation.kt | 13 +- .../platform/feature/rootnav/RootNavScreen.kt | 68 ++--- .../feature/search/SearchNavigation.kt | 157 +++++------ .../feature/search/SearchViewModel.kt | 2 +- .../feature/settings/SettingsNavigation.kt | 86 ++++--- .../feature/settings/SettingsViewModel.kt | 2 +- .../feature/settings/about/AboutNavigation.kt | 53 ++-- .../AccountSecurityNavigation.kt | 13 +- .../deleteaccount/DeleteAccountNavigation.kt | 13 +- .../DeleteAccountConfirmationNavigation.kt | 13 +- .../loginapproval/LoginApprovalNavigation.kt | 39 ++- .../loginapproval/LoginApprovalViewModel.kt | 6 +- .../PendingRequestsNavigation.kt | 13 +- .../appearance/AppearanceNavigation.kt | 45 +++- .../settings/autofill/AutoFillNavigation.kt | 13 +- .../blockautofill/BlockAutoFillNavigation.kt | 13 +- .../exportvault/ExportVaultNavigation.kt | 13 +- .../FlightRecorderNavigation.kt | 47 +++- .../recordedLogs/RecordedLogsNavigation.kt | 53 ++-- .../settings/folders/FoldersNavigation.kt | 13 +- .../addedit/FolderAddEditNavigation.kt | 76 +++--- .../folders/addedit/FolderAddEditViewModel.kt | 2 +- .../feature/settings/other/OtherNavigation.kt | 75 ++++-- .../feature/settings/other/OtherViewModel.kt | 2 +- .../settings/vault/VaultSettingsNavigation.kt | 13 +- .../feature/splash/SplashNavigation.kt | 11 +- .../vaultunlocked/VaultUnlockedNavigation.kt | 16 +- .../VaultUnlockedNavBarNavigation.kt | 12 +- .../VaultUnlockedNavBarScreen.kt | 4 +- .../model/VaultUnlockedNavBarTab.kt | 33 +-- .../manager/snackbar/SnackbarRelay.kt | 2 + .../bitwarden/ui/platform/util/RouteUtil.kt | 18 ++ .../util/SavedStateHandleExtensions.kt | 24 ++ .../generator/GeneratorGraphNavigation.kt | 14 +- .../feature/generator/GeneratorNavigation.kt | 91 ++++--- .../feature/generator/GeneratorViewModel.kt | 2 +- .../PasswordHistoryNavigation.kt | 78 +++--- .../PasswordHistoryViewModel.kt | 2 +- .../tools/feature/send/SendGraphNavigation.kt | 14 +- .../ui/tools/feature/send/SendNavigation.kt | 13 +- .../feature/send/addsend/AddSendNavigation.kt | 82 +++--- .../feature/send/addsend/AddSendViewModel.kt | 2 +- .../feature/addedit/VaultAddEditNavigation.kt | 151 ++++------- .../feature/addedit/VaultAddEditViewModel.kt | 2 +- .../attachments/AttachmentsNavigation.kt | 35 +-- .../attachments/AttachmentsViewModel.kt | 2 +- .../importlogins/ImportLoginsNavigation.kt | 40 ++- .../importlogins/ImportLoginsViewModel.kt | 2 +- .../vault/feature/item/VaultItemNavigation.kt | 72 ++---- .../vault/feature/item/VaultItemViewModel.kt | 2 +- .../itemlisting/VaultItemListingNavigation.kt | 243 +++++++++--------- .../itemlisting/VaultItemListingViewModel.kt | 3 +- .../ManualCodeEntryNavigation.kt | 13 +- .../VaultMoveToOrganizationNavigation.kt | 53 ++-- .../VaultMoveToOrganizationViewModel.kt | 11 +- .../qrcodescan/QrCodeScanNavigation.kt | 13 +- .../feature/vault/VaultGraphNavigation.kt | 14 +- .../ui/vault/feature/vault/VaultNavigation.kt | 13 +- .../VerificationCodeNavigation.kt | 13 +- .../ui/vault/model/VaultItemCipherType.kt | 3 + .../SetupAutoFillViewModelTest.kt | 24 +- .../accountsetup/SetupUnlockViewModelTest.kt | 24 +- .../checkemail/CheckEmailViewModelTest.kt | 39 +-- .../CompleteRegistrationViewModelTest.kt | 23 +- .../EnterpriseSignOnViewModelTest.kt | 30 ++- .../auth/feature/login/LoginViewModelTest.kt | 16 +- .../LoginWithDeviceViewModelTest.kt | 21 +- .../TwoFactorLoginViewModelTest.kt | 29 +-- .../vaultunlock/VaultUnlockViewModelTest.kt | 16 +- .../ui/platform/base/FakeNavHostController.kt | 8 +- .../feature/search/SearchViewModelTest.kt | 66 ++--- .../feature/settings/SettingsViewModelTest.kt | 16 +- .../LoginApprovalViewModelTest.kt | 21 +- .../addedit/FolderAddEditViewModelTest.kt | 30 ++- .../settings/other/OtherViewModelTest.kt | 16 +- .../generator/GeneratorViewModelTest.kt | 28 +- .../send/addsend/AddSendViewModelTest.kt | 11 +- .../addedit/VaultAddEditViewModelTest.kt | 46 ++-- .../attachments/AttachmentsViewModelTest.kt | 14 +- .../importlogins/ImportLoginsViewModelTest.kt | 18 +- .../feature/item/VaultItemViewModelTest.kt | 24 +- .../VaultItemListingViewModelTest.kt | 47 +--- .../VaultMoveToOrganizationViewModelTest.kt | 25 +- detekt-config.yml | 1 + docs/ARCHITECTURE.md | 27 +- 122 files changed, 1941 insertions(+), 1515 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/util/RouteUtil.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/util/SavedStateHandleExtensions.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt index b7f667dfd4..f3797ccf0c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillNavigation.kt @@ -6,65 +6,82 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import com.x8bit.bitwarden.ui.platform.util.toObjectRoute +import kotlinx.serialization.Serializable /** - * Route constant for navigating to the [SetupAutoFillScreen]. + * The type-safe route for the setup autofill screen. */ -private const val SETUP_AUTO_FILL_PREFIX = "setup_auto_fill" -private const val SETUP_AUTO_FILL_AS_ROOT_PREFIX = "${SETUP_AUTO_FILL_PREFIX}_as_root" -private const val SETUP_AUTO_FILL_NAV_ARG = "isInitialSetup" -private const val SETUP_AUTO_FILL_ROUTE = "$SETUP_AUTO_FILL_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}" -const val SETUP_AUTO_FILL_AS_ROOT_ROUTE = - "$SETUP_AUTO_FILL_AS_ROOT_PREFIX/{$SETUP_AUTO_FILL_NAV_ARG}" +sealed class SetupAutofillRoute { + /** + * The [isInitialSetup] value used in the setup autofill screen. + */ + abstract val isInitialSetup: Boolean + + /** + * The type-safe route for the standard setup autofill screen. + */ + @Serializable + data object Standard : SetupAutofillRoute() { + override val isInitialSetup: Boolean get() = false + } + + /** + * The type-safe route for the root setup autofill screen. + */ + @Serializable + data object AsRoot : SetupAutofillRoute() { + override val isInitialSetup: Boolean get() = true + } +} /** * Arguments for the [SetupAutoFillScreen] using [SavedStateHandle]. */ -data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) { - constructor(savedStateHandle: SavedStateHandle) : this( - isInitialSetup = requireNotNull(savedStateHandle[SETUP_AUTO_FILL_NAV_ARG]), - ) +data class SetupAutoFillScreenArgs(val isInitialSetup: Boolean) + +/** + * Constructs a [SetupAutoFillScreenArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toSetupAutoFillArgs(): SetupAutoFillScreenArgs { + val route = (this.toObjectRoute() + ?: this.toObjectRoute()) + return route + ?.let { SetupAutoFillScreenArgs(isInitialSetup = it.isInitialSetup) } + ?: throw IllegalStateException("Missing correct route for SetupAutofillScreen") } /** - * Navigate to the setup auto-fill screen. + * Navigate to the setup autofill screen. */ fun NavController.navigateToSetupAutoFillScreen(navOptions: NavOptions? = null) { - this.navigate("$SETUP_AUTO_FILL_PREFIX/false", navOptions) + this.navigate(route = SetupAutofillRoute.Standard, navOptions = navOptions) } /** - * Navigate to the setup auto-fill screen as the root. + * Navigate to the setup autofill screen as the root. */ fun NavController.navigateToSetupAutoFillAsRootScreen(navOptions: NavOptions? = null) { - this.navigate("$SETUP_AUTO_FILL_AS_ROOT_PREFIX/true", navOptions) + this.navigate(route = SetupAutofillRoute.AsRoot, navOptions = navOptions) } /** - * Add the setup auto-fil screen to the nav graph. + * Add the setup autofill screen to the nav graph. */ fun NavGraphBuilder.setupAutoFillDestination(onNavigateBack: () -> Unit) { - composableWithSlideTransitions( - route = SETUP_AUTO_FILL_ROUTE, - arguments = setupAutofillNavArgs, - ) { + composableWithSlideTransitions { SetupAutoFillScreen(onNavigateBack = onNavigateBack) } } /** - * Add the setup autofil screen to the root nav graph. + * Add the setup autofill screen to the root nav graph. */ fun NavGraphBuilder.setupAutoFillDestinationAsRoot() { - composableWithPushTransitions( - route = SETUP_AUTO_FILL_AS_ROOT_ROUTE, - arguments = setupAutofillNavArgs, - ) { + composableWithPushTransitions { SetupAutoFillScreen( onNavigateBack = { // No-Op @@ -72,9 +89,3 @@ fun NavGraphBuilder.setupAutoFillDestinationAsRoot() { ) } } - -private val setupAutofillNavArgs = listOf( - navArgument(SETUP_AUTO_FILL_NAV_ARG) { - type = NavType.BoolType - }, -) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt index 17dba1285f..367bb92d74 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModel.kt @@ -32,7 +32,7 @@ class SetupAutoFillViewModel @Inject constructor( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId - val isInitialSetup = SetupAutoFillScreenArgs(savedStateHandle).isInitialSetup + val isInitialSetup = savedStateHandle.toSetupAutoFillArgs().isInitialSetup SetupAutoFillState( userId = userId, dialogState = null, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteNavigation.kt index 7501e34867..06f619543e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupCompleteNavigation.kt @@ -7,26 +7,26 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable /** - * Route name for [SetupCompleteScreen]. + * The type-safe route for the setup complete screen. */ -const val SETUP_COMPLETE_ROUTE = "setup_complete" +@Serializable +data object SetupCompleteRoute /** * Navigate to the setup complete screen. */ fun NavController.navigateToSetupCompleteScreen(navOptions: NavOptions? = null) { - this.navigate(SETUP_COMPLETE_ROUTE, navOptions) + this.navigate(route = SetupCompleteRoute, navOptions = navOptions) } /** * Add the setup complete screen to the nav graph. */ fun NavGraphBuilder.setupCompleteDestination() { - composableWithPushTransitions( - route = SETUP_COMPLETE_ROUTE, - ) { + composableWithPushTransitions { SetupCompleteScreen() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt index aadf0fb5a5..b440bdfc16 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockNavigation.kt @@ -6,45 +6,68 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import com.x8bit.bitwarden.ui.platform.util.toObjectRoute +import kotlinx.serialization.Serializable /** - * Route constants for [SetupUnlockScreen] + * The type-safe route for the setup unlock screen. */ -private const val SETUP_UNLOCK_PREFIX = "setup_unlock" -private const val SETUP_UNLOCK_AS_ROOT_PREFIX = "${SETUP_UNLOCK_PREFIX}_as_root" -private const val SETUP_UNLOCK_INITIAL_SETUP_ARG = "isInitialSetup" -const val SETUP_UNLOCK_AS_ROOT_ROUTE = "$SETUP_UNLOCK_AS_ROOT_PREFIX/" + - "{$SETUP_UNLOCK_INITIAL_SETUP_ARG}" -private const val SETUP_UNLOCK_ROUTE = "$SETUP_UNLOCK_PREFIX/{$SETUP_UNLOCK_INITIAL_SETUP_ARG}" +sealed class SetupUnlockRoute { + /** + * The [isInitialSetup] value used in the setup unlock screen. + */ + abstract val isInitialSetup: Boolean + + /** + * The type-safe route for the standard setup unlock screen. + */ + @Serializable + data object Standard : SetupUnlockRoute() { + override val isInitialSetup: Boolean get() = false + } + + /** + * The type-safe route for the root setup unlock screen. + */ + @Serializable + data object AsRoot : SetupUnlockRoute() { + override val isInitialSetup: Boolean get() = true + } +} /** * Class to retrieve setup unlock arguments from the [SavedStateHandle]. */ data class SetupUnlockArgs( val isInitialSetup: Boolean, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - isInitialSetup = requireNotNull(savedStateHandle[SETUP_UNLOCK_INITIAL_SETUP_ARG]), - ) +) + +/** + * Constructs a [SetupUnlockArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toSetupUnlockArgs(): SetupUnlockArgs { + val route = this.toObjectRoute() + ?: this.toObjectRoute() + return route + ?.let { SetupUnlockArgs(isInitialSetup = it.isInitialSetup) } + ?: throw IllegalStateException("Missing correct route for SetupUnlockScreen") } /** * Navigate to the setup unlock screen. */ fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) { - this.navigate("$SETUP_UNLOCK_PREFIX/false", navOptions) + this.navigate(route = SetupUnlockRoute.Standard, navOptions = navOptions) } /** * Navigate to the setup unlock screen as root. */ fun NavController.navigateToSetupUnlockScreenAsRoot(navOptions: NavOptions? = null) { - this.navigate("$SETUP_UNLOCK_AS_ROOT_PREFIX/true", navOptions) + this.navigate(route = SetupUnlockRoute.AsRoot, navOptions = navOptions) } /** @@ -53,10 +76,7 @@ fun NavController.navigateToSetupUnlockScreenAsRoot(navOptions: NavOptions? = nu fun NavGraphBuilder.setupUnlockDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = SETUP_UNLOCK_ROUTE, - arguments = setupUnlockArguments, - ) { + composableWithSlideTransitions { SetupUnlockScreen( onNavigateBack = onNavigateBack, ) @@ -67,10 +87,7 @@ fun NavGraphBuilder.setupUnlockDestination( * Add the setup unlock screen to the root nav graph. */ fun NavGraphBuilder.setupUnlockDestinationAsRoot() { - composableWithPushTransitions( - route = SETUP_UNLOCK_AS_ROOT_ROUTE, - arguments = setupUnlockArguments, - ) { + composableWithPushTransitions { SetupUnlockScreen( onNavigateBack = { // No-Op @@ -78,12 +95,3 @@ fun NavGraphBuilder.setupUnlockDestinationAsRoot() { ) } } - -private val setupUnlockArguments = listOf( - navArgument( - name = SETUP_UNLOCK_INITIAL_SETUP_ARG, - builder = { - type = NavType.BoolType - }, - ), -) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt index 49a9192b6a..c2cb9bbd83 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModel.kt @@ -43,7 +43,7 @@ class SetupUnlockViewModel @Inject constructor( cipher = biometricsEncryptionManager.getOrCreateCipher(userId = userId), ) // whether or not the user has completed the initial setup prior to this. - val isInitialSetup = SetupUnlockArgs(savedStateHandle).isInitialSetup + val isInitialSetup = savedStateHandle.toSetupUnlockArgs().isInitialSetup SetupUnlockState( userId = userId, isUnlockWithPasswordEnabled = authRepository diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt index 80947c19f3..0c605b87ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/auth/AuthNavigation.kt @@ -21,7 +21,7 @@ import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.navigateToEnterprise import com.x8bit.bitwarden.ui.auth.feature.environment.environmentDestination import com.x8bit.bitwarden.ui.auth.feature.environment.navigateToEnvironment import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.expiredRegistrationLinkDestination -import com.x8bit.bitwarden.ui.auth.feature.landing.LANDING_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.landing.LandingRoute import com.x8bit.bitwarden.ui.auth.feature.landing.landingDestination import com.x8bit.bitwarden.ui.auth.feature.landing.navigateToLanding import com.x8bit.bitwarden.ui.auth.feature.login.loginDestination @@ -46,8 +46,13 @@ import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestinat import com.x8bit.bitwarden.ui.auth.feature.welcome.welcomeDestination import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToPreAuthSettings import com.x8bit.bitwarden.ui.platform.feature.settings.preAuthSettingsDestinations +import kotlinx.serialization.Serializable -const val AUTH_GRAPH_ROUTE: String = "auth_graph" +/** + * The type-safe route for the auth graph. + */ +@Serializable +data object AuthGraphRoute /** * Add auth destinations to the nav graph. @@ -56,9 +61,8 @@ const val AUTH_GRAPH_ROUTE: String = "auth_graph" fun NavGraphBuilder.authGraph( navController: NavHostController, ) { - navigation( - startDestination = LANDING_ROUTE, - route = AUTH_GRAPH_ROUTE, + navigation( + startDestination = LandingRoute, ) { createAccountDestination( onNavigateBack = { navController.popBackStack() }, @@ -67,7 +71,7 @@ fun NavGraphBuilder.authGraph( emailAddress = emailAddress, captchaToken = captchaToken, navOptions = navOptions { - popUpTo(LANDING_ROUTE) + popUpTo(route = LandingRoute) }, ) }, @@ -82,7 +86,7 @@ fun NavGraphBuilder.authGraph( ) }, onNavigateToCheckEmail = { emailAddress -> - navController.navigateToCheckEmail(emailAddress) + navController.navigateToCheckEmail(emailAddress = emailAddress) }, onNavigateToEnvironment = { navController.navigateToEnvironment() }, ) @@ -102,7 +106,7 @@ fun NavGraphBuilder.authGraph( emailAddress = emailAddress, captchaToken = captchaToken, navOptions = navOptions { - popUpTo(LANDING_ROUTE) + popUpTo(route = LandingRoute) }, ) }, @@ -203,14 +207,14 @@ fun NavGraphBuilder.authGraph( onNavigateToStartRegistration = { navController.navigateToStartRegistration( navOptions = navOptions { - popUpTo(LANDING_ROUTE) + popUpTo(route = LandingRoute) }, ) }, onNavigateToLogin = { navController.navigateToLanding( navOptions = navOptions { - popUpTo(LANDING_ROUTE) + popUpTo(route = LandingRoute) }, ) }, @@ -226,5 +230,5 @@ fun NavGraphBuilder.authGraph( fun NavController.navigateToAuthGraph( navOptions: NavOptions? = null, ) { - navigate(AUTH_GRAPH_ROUTE, navOptions) + navigate(route = AuthGraphRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt index f93c220d26..8bdedbbc26 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt @@ -6,19 +6,24 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL: String = "email" -private const val CHECK_EMAIL_ROUTE: String = "check_email/{$EMAIL}" +/** + * The type-safe route for the check email screen. + */ +@Serializable +data class CheckEmailRoute( + val emailAddress: String, +) /** * Navigate to the check email screen. */ fun NavController.navigateToCheckEmail(emailAddress: String, navOptions: NavOptions? = null) { - this.navigate("check_email/$emailAddress", navOptions) + this.navigate(route = CheckEmailRoute(emailAddress = emailAddress), navOptions = navOptions) } /** @@ -26,10 +31,14 @@ fun NavController.navigateToCheckEmail(emailAddress: String, navOptions: NavOpti */ data class CheckEmailArgs( val emailAddress: String, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - emailAddress = checkNotNull(savedStateHandle.get(EMAIL)), - ) +) + +/** + * Constructs a [CheckEmailArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toCheckEmailArgs(): CheckEmailArgs { + val route = this.toRoute() + return CheckEmailArgs(emailAddress = route.emailAddress) } /** @@ -38,12 +47,7 @@ data class CheckEmailArgs( fun NavGraphBuilder.checkEmailDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = CHECK_EMAIL_ROUTE, - arguments = listOf( - navArgument(EMAIL) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { CheckEmailScreen( onNavigateBack = onNavigateBack, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModel.kt index 14d3372d17..dd7fe53aa5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModel.kt @@ -21,7 +21,7 @@ class CheckEmailViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: CheckEmailState( - email = CheckEmailArgs(savedStateHandle).emailAddress, + email = savedStateHandle.toCheckEmailArgs().emailAddress, ), ) { init { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt index 31f3a4a2c6..02aa43d88e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt @@ -6,17 +6,20 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL_ADDRESS: String = "email_address" -private const val VERIFICATION_TOKEN: String = "verification_token" -private const val FROM_EMAIL: String = "from_email" -private const val COMPLETE_REGISTRATION_PREFIX = "complete_registration" -private const val COMPLETE_REGISTRATION_ROUTE = - "$COMPLETE_REGISTRATION_PREFIX/{$EMAIL_ADDRESS}/{$VERIFICATION_TOKEN}/{$FROM_EMAIL}" +/** + * The type-safe route for the complete registration screen. + */ +@Serializable +data class CompleteRegistrationRoute( + val emailAddress: String, + val verificationToken: String, + val fromEmail: Boolean, +) /** * Class to retrieve complete registration arguments from the [SavedStateHandle]. @@ -25,11 +28,17 @@ data class CompleteRegistrationArgs( val emailAddress: String, val verificationToken: String, val fromEmail: Boolean, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - emailAddress = checkNotNull(savedStateHandle.get(EMAIL_ADDRESS)), - verificationToken = checkNotNull(savedStateHandle.get(VERIFICATION_TOKEN)), - fromEmail = checkNotNull(savedStateHandle.get(FROM_EMAIL)), +) + +/** + * Constructs a [CompleteRegistrationArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toCompleteRegistrationArgs(): CompleteRegistrationArgs { + val route = this.toRoute() + return CompleteRegistrationArgs( + emailAddress = route.emailAddress, + verificationToken = route.verificationToken, + fromEmail = route.fromEmail, ) } @@ -43,8 +52,12 @@ fun NavController.navigateToCompleteRegistration( navOptions: NavOptions? = null, ) { this.navigate( - "$COMPLETE_REGISTRATION_PREFIX/$emailAddress/$verificationToken/$fromEmail", - navOptions, + route = CompleteRegistrationRoute( + emailAddress = emailAddress, + verificationToken = verificationToken, + fromEmail = fromEmail, + ), + navOptions = navOptions, ) } @@ -57,14 +70,7 @@ fun NavGraphBuilder.completeRegistrationDestination( onNavigateToPreventAccountLockout: () -> Unit, onNavigateToLogin: (email: String, token: String?) -> Unit, ) { - composableWithSlideTransitions( - route = COMPLETE_REGISTRATION_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - navArgument(VERIFICATION_TOKEN) { type = NavType.StringType }, - navArgument(FROM_EMAIL) { type = NavType.BoolType }, - ), - ) { + composableWithSlideTransitions { CompleteRegistrationScreen( onNavigateBack = onNavigateBack, onNavigateToPasswordGuidance = onNavigateToPasswordGuidance, @@ -78,5 +84,5 @@ fun NavGraphBuilder.completeRegistrationDestination( * Pop up to the complete registration screen. */ fun NavController.popUpToCompleteRegistration() { - popBackStack(route = COMPLETE_REGISTRATION_ROUTE, inclusive = false) + this.popBackStack(route = CompleteRegistrationRoute, inclusive = false) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index c3e4660312..1b7f594da9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -57,7 +57,7 @@ class CompleteRegistrationViewModel @Inject constructor( private val specialCircumstanceManager: SpecialCircumstanceManager, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { - val args = CompleteRegistrationArgs(savedStateHandle) + val args = savedStateHandle.toCompleteRegistrationArgs() CompleteRegistrationState( userEmail = args.emailAddress, emailVerificationToken = args.verificationToken, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt index b8935ca4d5..e1c2380fc1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val CREATE_ACCOUNT_ROUTE = "create_account" +/** + * The type-safe route for the create account screen. + */ +@Serializable +data object CreateAccountRoute /** * Navigate to the create account screen. */ fun NavController.navigateToCreateAccount(navOptions: NavOptions? = null) { - this.navigate(CREATE_ACCOUNT_ROUTE, navOptions) + this.navigate(route = CreateAccountRoute, navOptions = navOptions) } /** @@ -24,9 +29,7 @@ fun NavGraphBuilder.createAccountDestination( onNavigateBack: () -> Unit, onNavigateToLogin: (emailAddress: String, captchaToken: String) -> Unit, ) { - composableWithSlideTransitions( - route = CREATE_ACCOUNT_ROUTE, - ) { + composableWithSlideTransitions { CreateAccountScreen( onNavigateBack = onNavigateBack, onNavigateToLogin = onNavigateToLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnNavigation.kt index e55f8e2a84..6e71af027c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnNavigation.kt @@ -6,22 +6,30 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val ENTERPRISE_SIGN_ON_PREFIX = "enterprise_sign_on " -private const val EMAIL_ADDRESS: String = "email_address" -private const val ENTERPRISE_SIGN_ON_ROUTE = "$ENTERPRISE_SIGN_ON_PREFIX/{$EMAIL_ADDRESS}" +/** + * The type-safe route for the enterprise sign-on screen. + */ +@Serializable +data class EnterpriseSignOnRoute( + val emailAddress: String, +) /** * Class to retrieve login arguments from the [SavedStateHandle]. */ -data class EnterpriseSignOnArgs(val emailAddress: String) { - constructor(savedStateHandle: SavedStateHandle) : this( - checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String, - ) +data class EnterpriseSignOnArgs(val emailAddress: String) + +/** + * Constructs a [EnterpriseSignOnArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toEnterpriseSignOnArgs(): EnterpriseSignOnArgs { + val route = this.toRoute() + return EnterpriseSignOnArgs(emailAddress = route.emailAddress) } /** @@ -31,7 +39,10 @@ fun NavController.navigateToEnterpriseSignOn( emailAddress: String, navOptions: NavOptions? = null, ) { - this.navigate("$ENTERPRISE_SIGN_ON_PREFIX/$emailAddress", navOptions) + this.navigate( + route = EnterpriseSignOnRoute(emailAddress = emailAddress), + navOptions = navOptions, + ) } /** @@ -42,12 +53,7 @@ fun NavGraphBuilder.enterpriseSignOnDestination( onNavigateToSetPassword: () -> Unit, onNavigateToTwoFactorLogin: (emailAddress: String, orgIdentifier: String) -> Unit, ) { - composableWithSlideTransitions( - route = ENTERPRISE_SIGN_ON_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { EnterpriseSignOnScreen( onNavigateBack = onNavigateBack, onNavigateToSetPassword = onNavigateToSetPassword, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt index dfe3101a63..1841017d54 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModel.kt @@ -191,7 +191,7 @@ class EnterpriseSignOnViewModel @Inject constructor( mutableStateFlow.update { it.copy(dialogState = null) } sendEvent( EnterpriseSignOnEvent.NavigateToTwoFactorLogin( - emailAddress = EnterpriseSignOnArgs(savedStateHandle).emailAddress, + emailAddress = savedStateHandle.toEnterpriseSignOnArgs().emailAddress, orgIdentifier = state.orgIdentifierInput, ), ) @@ -434,7 +434,7 @@ class EnterpriseSignOnViewModel @Inject constructor( viewModelScope.launch { val result = authRepository .login( - email = EnterpriseSignOnArgs(savedStateHandle).emailAddress, + email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress, ssoCode = ssoCallbackResult.code, ssoCodeVerifier = ssoData.codeVerifier, ssoRedirectUri = SSO_URI, @@ -459,7 +459,7 @@ class EnterpriseSignOnViewModel @Inject constructor( viewModelScope.launch { if (featureFlagManager.getFeatureFlag(key = FlagKey.VerifiedSsoDomainEndpoint)) { val result = authRepository.getVerifiedOrganizationDomainSsoDetails( - email = EnterpriseSignOnArgs(savedStateHandle).emailAddress, + email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress, ) sendAction( EnterpriseSignOnAction.Internal.OnVerifiedOrganizationDomainSsoDetailsReceive( @@ -468,7 +468,7 @@ class EnterpriseSignOnViewModel @Inject constructor( ) } else { val result = authRepository.getOrganizationDomainSsoDetails( - email = EnterpriseSignOnArgs(savedStateHandle).emailAddress, + email = savedStateHandle.toEnterpriseSignOnArgs().emailAddress, ) sendAction( EnterpriseSignOnAction.Internal.OnOrganizationDomainSsoDetailsReceive(result), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentNavigation.kt index a20507ee3e..ee7f96d0ce 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/environment/EnvironmentNavigation.kt @@ -7,25 +7,28 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions - -private const val ENVIRONMENT_ROUTE = "environment" +import kotlinx.serialization.Serializable /** - * Add settings destinations to the nav graph. + * The type-safe route for the environment screen. + */ +@Serializable +data object EnvironmentRoute + +/** + * Add the environment destination to the nav graph. */ fun NavGraphBuilder.environmentDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = ENVIRONMENT_ROUTE, - ) { + composableWithSlideTransitions { EnvironmentScreen(onNavigateBack = onNavigateBack) } } /** - * Navigate to the about screen. + * Navigate to the environment screen. */ fun NavController.navigateToEnvironment(navOptions: NavOptions? = null) { - navigate(ENVIRONMENT_ROUTE, navOptions) + this.navigate(route = EnvironmentRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkNavigation.kt index 5404cf715d..3b8a6ccc97 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/expiredregistrationlink/ExpiredRegistrationLinkNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val EXPIRED_REGISTRATION_LINK_ROUTE = "expired_registration_link" +/** + * The type-safe route for the expired registration link screen. + */ +@Serializable +data object ExpiredRegistrationLinkRoute /** * Navigate to the expired registration link screen. */ fun NavController.navigateToExpiredRegistrationLinkScreen(navOptions: NavOptions? = null) { - this.navigate(route = EXPIRED_REGISTRATION_LINK_ROUTE, navOptions = navOptions) + this.navigate(route = ExpiredRegistrationLinkRoute, navOptions = navOptions) } /** @@ -25,9 +30,7 @@ fun NavGraphBuilder.expiredRegistrationLinkDestination( onNavigateToStartRegistration: () -> Unit, onNavigateToLogin: () -> Unit, ) { - composableWithPushTransitions( - route = EXPIRED_REGISTRATION_LINK_ROUTE, - ) { + composableWithPushTransitions { ExpiredRegistrationLinkScreen( onNavigateBack = onNavigateBack, onNavigateToStartRegistration = onNavigateToStartRegistration, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt index e81122bb74..89a63b92b5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions +import kotlinx.serialization.Serializable -const val LANDING_ROUTE: String = "landing" +/** + * The type-safe route for the landing screen. + */ +@Serializable +data object LandingRoute /** * Navigate to the landing screen. */ fun NavController.navigateToLanding(navOptions: NavOptions? = null) { - this.navigate(LANDING_ROUTE, navOptions) + this.navigate(route = LandingRoute, navOptions = navOptions) } /** @@ -27,9 +32,7 @@ fun NavGraphBuilder.landingDestination( onNavigateToStartRegistration: () -> Unit, onNavigateToPreAuthSettings: () -> Unit, ) { - composableWithStayTransitions( - route = LANDING_ROUTE, - ) { + composableWithStayTransitions { LandingScreen( onNavigateToCreateAccount = onNavigateToCreateAccount, onNavigateToLogin = onNavigateToLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt index 39b92dee4b..359273d272 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginNavigation.kt @@ -6,22 +6,33 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL_ADDRESS: String = "email_address" -private const val CAPTCHA_TOKEN = "captcha_token" -private const val LOGIN_ROUTE: String = "login/{$EMAIL_ADDRESS}?$CAPTCHA_TOKEN={$CAPTCHA_TOKEN}" +/** + * The type-safe route for the login screen. + */ +@Serializable +data class LoginRoute( + val emailAddress: String, + val captchaToken: String?, +) /** * Class to retrieve login arguments from the [SavedStateHandle]. */ -data class LoginArgs(val emailAddress: String, val captchaToken: String?) { - constructor(savedStateHandle: SavedStateHandle) : this( - checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String, - savedStateHandle[CAPTCHA_TOKEN], +data class LoginArgs(val emailAddress: String, val captchaToken: String?) + +/** + * Constructs a [LoginArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toLoginArgs(): LoginArgs { + val route = this.toRoute() + return LoginArgs( + emailAddress = route.emailAddress, + captchaToken = route.captchaToken, ) } @@ -34,8 +45,8 @@ fun NavController.navigateToLogin( navOptions: NavOptions? = null, ) { this.navigate( - "login/$emailAddress?$CAPTCHA_TOKEN=$captchaToken", - navOptions, + route = LoginRoute(emailAddress = emailAddress, captchaToken = captchaToken), + navOptions = navOptions, ) } @@ -53,16 +64,7 @@ fun NavGraphBuilder.loginDestination( isNewDeviceVerification: Boolean, ) -> Unit, ) { - composableWithSlideTransitions( - route = LOGIN_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - navArgument(CAPTCHA_TOKEN) { - type = NavType.StringType - nullable = true - }, - ), - ) { + composableWithSlideTransitions { LoginScreen( onNavigateBack = onNavigateBack, onNavigateToMasterPasswordHint = onNavigateToMasterPasswordHint, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt index d162cb065a..2c755e20f1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModel.kt @@ -6,6 +6,8 @@ import android.net.Uri import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.KnownDeviceResult @@ -16,8 +18,6 @@ import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries import dagger.hilt.android.lifecycle.HiltViewModel @@ -43,16 +43,23 @@ class LoginViewModel @Inject constructor( ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] - ?: LoginState( - emailAddress = LoginArgs(savedStateHandle).emailAddress, - isLoginButtonEnabled = false, - passwordInput = "", - environmentLabel = environmentRepository.environment.label, - dialogState = LoginState.DialogState.Loading(R.string.loading.asText()), - captchaToken = LoginArgs(savedStateHandle).captchaToken, - accountSummaries = authRepository.userStateFlow.value?.toAccountSummaries().orEmpty(), - shouldShowLoginWithDevice = false, - ), + ?: run { + val args = savedStateHandle.toLoginArgs() + LoginState( + emailAddress = args.emailAddress, + isLoginButtonEnabled = false, + passwordInput = "", + environmentLabel = environmentRepository.environment.label, + dialogState = LoginState.DialogState.Loading(R.string.loading.asText()), + captchaToken = args.captchaToken, + accountSummaries = authRepository + .userStateFlow + .value + ?.toAccountSummaries() + .orEmpty(), + shouldShowLoginWithDevice = false, + ) + }, ) { init { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceNavigation.kt index bf90f4351a..4ec67ed626 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceNavigation.kt @@ -6,17 +6,20 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDeviceType import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL_ADDRESS: String = "email_address" -private const val LOGIN_WITH_DEVICE_PREFIX = "login_with_device" -private const val LOGIN_TYPE: String = "login_type" -private const val LOGIN_WITH_DEVICE_ROUTE = - "$LOGIN_WITH_DEVICE_PREFIX/{$EMAIL_ADDRESS}/{$LOGIN_TYPE}" +/** + * The type-safe route for the login with device screen. + */ +@Serializable +data class LoginWithDeviceRoute( + val emailAddress: String, + val loginType: LoginWithDeviceType, +) /** * Class to retrieve login with device arguments from the [SavedStateHandle]. @@ -24,11 +27,14 @@ private const val LOGIN_WITH_DEVICE_ROUTE = data class LoginWithDeviceArgs( val emailAddress: String, val loginType: LoginWithDeviceType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - emailAddress = checkNotNull(savedStateHandle.get(EMAIL_ADDRESS)), - loginType = checkNotNull(savedStateHandle.get(LOGIN_TYPE)), - ) +) + +/** + * Constructs a [LoginWithDeviceArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toLoginWithDeviceArgs(): LoginWithDeviceArgs { + val route = this.toRoute() + return LoginWithDeviceArgs(emailAddress = route.emailAddress, loginType = route.loginType) } /** @@ -40,7 +46,10 @@ fun NavController.navigateToLoginWithDevice( navOptions: NavOptions? = null, ) { this.navigate( - route = "$LOGIN_WITH_DEVICE_PREFIX/$emailAddress/$loginType", + route = LoginWithDeviceRoute( + emailAddress = emailAddress, + loginType = loginType, + ), navOptions = navOptions, ) } @@ -52,13 +61,7 @@ fun NavGraphBuilder.loginWithDeviceDestination( onNavigateBack: () -> Unit, onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit, ) { - composableWithSlideTransitions( - route = LOGIN_WITH_DEVICE_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - navArgument(LOGIN_TYPE) { type = NavType.EnumType(LoginWithDeviceType::class.java) }, - ), - ) { + composableWithSlideTransitions { LoginWithDeviceScreen( onNavigateBack = onNavigateBack, onNavigateToTwoFactorLogin = onNavigateToTwoFactorLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt index 09b57061ae..9c53ab10ad 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt @@ -4,6 +4,8 @@ import android.net.Uri import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.manager.model.CreateAuthRequestResult import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -14,8 +16,6 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.model.LoginWithDevice import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.util.toAuthRequestType import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -39,7 +39,7 @@ class LoginWithDeviceViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { - val args = LoginWithDeviceArgs(savedStateHandle) + val args = savedStateHandle.toLoginWithDeviceArgs() LoginWithDeviceState( loginWithDeviceType = args.loginType, emailAddress = args.emailAddress, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorNavigation.kt index ee2a2c2497..279b3353c6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordgenerator/MasterPasswordGeneratorNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val MASTER_PASSWORD_GENERATOR = "master_password_generator" +/** + * The type-safe route for the master password generator screen. + */ +@Serializable +data object MasterPasswordGeneratorRoute /** * Navigate to master password generator screen. */ fun NavController.navigateToMasterPasswordGenerator(navOptions: NavOptions? = null) { - this.navigate(MASTER_PASSWORD_GENERATOR, navOptions) + this.navigate(route = MasterPasswordGeneratorRoute, navOptions = navOptions) } /** @@ -25,9 +30,7 @@ fun NavGraphBuilder.masterPasswordGeneratorDestination( onNavigateToPreventLockout: () -> Unit, onNavigateBackWithPassword: () -> Unit, ) { - composableWithSlideTransitions( - route = MASTER_PASSWORD_GENERATOR, - ) { + composableWithSlideTransitions { MasterPasswordGeneratorScreen( onNavigateBack = onNavigateBack, onNavigateToPreventLockout = onNavigateToPreventLockout, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceNavigation.kt index 43fd46ae57..f2a0d70f4e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val MASTER_PASSWORD_GUIDANCE = "master_password_guidance" +/** + * The type-safe route for the master password guidance screen. + */ +@Serializable +data object MasterPasswordGuidanceRoute /** * Navigate to the master password guidance screen. */ fun NavController.navigateToMasterPasswordGuidance(navOptions: NavOptions? = null) { - this.navigate(MASTER_PASSWORD_GUIDANCE, navOptions) + this.navigate(route = MasterPasswordGuidanceRoute, navOptions = navOptions) } /** @@ -24,9 +29,7 @@ fun NavGraphBuilder.masterPasswordGuidanceDestination( onNavigateBack: () -> Unit, onNavigateToGeneratePassword: () -> Unit, ) { - composableWithSlideTransitions( - route = MASTER_PASSWORD_GUIDANCE, - ) { + composableWithSlideTransitions { MasterPasswordGuidanceScreen( onNavigateBack = onNavigateBack, onNavigateToGeneratePassword = onNavigateToGeneratePassword, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintNavigation.kt index 48d8134078..930803e336 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintNavigation.kt @@ -6,21 +6,30 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL_ADDRESS: String = "email_address" -private const val MASTER_PASSWORD_HINT_ROUTE: String = "master_password_hint/{$EMAIL_ADDRESS}" +/** + * The type-safe route for the master password hint screen. + */ +@Serializable +data class MasterPasswordHintRoute( + val emailAddress: String, +) /** * Class to retrieve login arguments from the [SavedStateHandle]. */ -data class MasterPasswordHintArgs(val emailAddress: String) { - constructor(savedStateHandle: SavedStateHandle) : this( - checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String, - ) +data class MasterPasswordHintArgs(val emailAddress: String) + +/** + * Constructs a [MasterPasswordHintArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toMasterPasswordHintArgs(): MasterPasswordHintArgs { + val route = this.toRoute() + return MasterPasswordHintArgs(emailAddress = route.emailAddress) } /** @@ -30,7 +39,10 @@ fun NavController.navigateToMasterPasswordHint( emailAddress: String, navOptions: NavOptions? = null, ) { - this.navigate("master_password_hint/$emailAddress", navOptions) + this.navigate( + route = MasterPasswordHintRoute(emailAddress = emailAddress), + navOptions = navOptions, + ) } /** @@ -39,12 +51,7 @@ fun NavController.navigateToMasterPasswordHint( fun NavGraphBuilder.masterPasswordHintDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = MASTER_PASSWORD_HINT_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { MasterPasswordHintScreen(onNavigateBack = onNavigateBack) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintViewModel.kt index f2301b93e6..bf8a834ce6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordhint/MasterPasswordHintViewModel.kt @@ -3,13 +3,13 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PasswordHintResult import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -31,7 +31,7 @@ class MasterPasswordHintViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: MasterPasswordHintState( - emailInput = MasterPasswordHintArgs(savedStateHandle).emailAddress, + emailInput = savedStateHandle.toMasterPasswordHintArgs().emailAddress, ), ) { init { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutNavigation.kt index a4d2ad46bb..5e10c7b585 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/preventaccountlockout/PreventAccountLockoutNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val PREVENT_ACCOUNT_LOCKOUT = "prevent_account_lockout" +/** + * The type-safe route for the prevent account lockout screen. + */ +@Serializable +data object PreventAccountLockoutRoute /** * Navigate to prevent account lockout screen. */ fun NavController.navigateToPreventAccountLockout(navOptions: NavOptions? = null) { - this.navigate(PREVENT_ACCOUNT_LOCKOUT, navOptions) + this.navigate(route = PreventAccountLockoutRoute, navOptions = navOptions) } /** @@ -23,9 +28,7 @@ fun NavController.navigateToPreventAccountLockout(navOptions: NavOptions? = null fun NavGraphBuilder.preventAccountLockoutDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = PREVENT_ACCOUNT_LOCKOUT, - ) { + composableWithSlideTransitions { PreventAccountLockoutScreen( onNavigateBack = onNavigateBack, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordNavigation.kt index 3b6f6fe4d9..3f606158ea 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordNavigation.kt @@ -7,19 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.bitwarden.core.annotation.OmitFromCoverage +import kotlinx.serialization.Serializable /** - * The route for navigating to the [RemovePasswordScreen]. + * The type-safe route for the remove password screen. */ -const val REMOVE_PASSWORD_ROUTE: String = "remove_password" +@Serializable +data object RemovePasswordRoute /** * Add the Remove Password screen to the nav graph. */ fun NavGraphBuilder.removePasswordDestination() { - composable( - route = REMOVE_PASSWORD_ROUTE, - ) { + composable { RemovePasswordScreen() } } @@ -30,5 +30,5 @@ fun NavGraphBuilder.removePasswordDestination() { fun NavController.navigateToRemovePassword( navOptions: NavOptions? = null, ) { - this.navigate(REMOVE_PASSWORD_ROUTE, navOptions) + this.navigate(route = RemovePasswordRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordNavigation.kt index dd4d12f66d..a467c19264 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/resetpassword/ResetPasswordNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.bitwarden.core.annotation.OmitFromCoverage +import kotlinx.serialization.Serializable -const val RESET_PASSWORD_ROUTE: String = "reset_password" +/** + * The type-safe route for the reset password screen. + */ +@Serializable +data object ResetPasswordRoute /** * Add the Reset Password screen to the nav graph. @@ -16,9 +21,7 @@ const val RESET_PASSWORD_ROUTE: String = "reset_password" fun NavGraphBuilder.resetPasswordDestination( onNavigateToPreventAccountLockOut: () -> Unit, ) { - composable( - route = RESET_PASSWORD_ROUTE, - ) { + composable { ResetPasswordScreen(onNavigateToPreventAccountLockOut = onNavigateToPreventAccountLockOut) } } @@ -29,5 +32,5 @@ fun NavGraphBuilder.resetPasswordDestination( fun NavController.navigateToResetPasswordScreen( navOptions: NavOptions? = null, ) { - this.navigate(RESET_PASSWORD_ROUTE, navOptions) + this.navigate(route = ResetPasswordRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordNavigation.kt index 3db354e591..56cbd3808c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/setpassword/SetPasswordNavigation.kt @@ -7,16 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.bitwarden.core.annotation.OmitFromCoverage +import kotlinx.serialization.Serializable -const val SET_PASSWORD_ROUTE: String = "set_password" +/** + * The type-safe route for the set password screen. + */ +@Serializable +data object SetPasswordRoute /** * Add the Set Password screen to the nav graph. */ fun NavGraphBuilder.setPasswordDestination() { - composable( - route = SET_PASSWORD_ROUTE, - ) { + composable { SetPasswordScreen() } } @@ -27,5 +30,5 @@ fun NavGraphBuilder.setPasswordDestination() { fun NavController.navigateToSetPassword( navOptions: NavOptions? = null, ) { - this.navigate(SET_PASSWORD_ROUTE, navOptions) + this.navigate(route = SetPasswordRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationNavigation.kt index a2954f2d20..145c36ec5e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val START_REGISTRATION_ROUTE = "start_registration" +/** + * The type-safe route for the start registration screen. + */ +@Serializable +data object StartRegistrationRoute /** * Navigate to the start registration screen. */ fun NavController.navigateToStartRegistration(navOptions: NavOptions? = null) { - this.navigate(START_REGISTRATION_ROUTE, navOptions) + this.navigate(route = StartRegistrationRoute, navOptions = navOptions) } /** @@ -29,9 +34,7 @@ fun NavGraphBuilder.startRegistrationDestination( onNavigateToCheckEmail: (email: String) -> Unit, onNavigateToEnvironment: () -> Unit, ) { - composableWithSlideTransitions( - route = START_REGISTRATION_ROUTE, - ) { + composableWithSlideTransitions { StartRegistrationScreen( onNavigateBack = onNavigateBack, onNavigateToCompleteRegistration = onNavigateToCompleteRegistration, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt index 90aa501b1b..b0d536d62d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceEncryptionNavigation.kt @@ -15,16 +15,20 @@ import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLog import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToTdeVaultUnlock import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.tdeVaultUnlockDestination +import kotlinx.serialization.Serializable -const val TRUSTED_DEVICE_GRAPH_ROUTE: String = "trusted_device_graph" +/** + * The type-safe route for the trusted device graph. + */ +@Serializable +data object TrustedDeviceGraphRoute /** * Add trusted device destinations to the nav graph. */ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) { - navigation( - startDestination = TRUSTED_DEVICE_ROUTE, - route = TRUSTED_DEVICE_GRAPH_ROUTE, + navigation( + startDestination = TrustedDeviceRoute, ) { loginWithDeviceDestination( onNavigateBack = { navController.popBackStack() }, @@ -66,5 +70,5 @@ fun NavGraphBuilder.trustedDeviceGraph(navController: NavHostController) { fun NavController.navigateToTrustedDeviceGraph( navOptions: NavOptions? = null, ) { - navigate(TRUSTED_DEVICE_GRAPH_ROUTE, navOptions) + navigate(route = TrustedDeviceGraphRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt index 2f8c05b9e0..67f6e22972 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceNavigation.kt @@ -7,11 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable /** - * The route for navigating to the [TrustedDeviceScreen]. + * The type-safe route for the trusted device screen. */ -const val TRUSTED_DEVICE_ROUTE: String = "trusted_device" +@Serializable +data object TrustedDeviceRoute /** * Add the Trusted Device Screen to the nav graph. @@ -21,9 +23,7 @@ fun NavGraphBuilder.trustedDeviceDestination( onNavigateToLoginWithOtherDevice: (emailAddress: String) -> Unit, onNavigateToLock: (emailAddress: String) -> Unit, ) { - composableWithSlideTransitions( - route = TRUSTED_DEVICE_ROUTE, - ) { + composableWithSlideTransitions { TrustedDeviceScreen( onNavigateToAdminApproval = onNavigateToAdminApproval, onNavigateToLoginWithOtherDevice = onNavigateToLoginWithOtherDevice, @@ -38,5 +38,5 @@ fun NavGraphBuilder.trustedDeviceDestination( fun NavController.navigateToTrustedDevice( navOptions: NavOptions? = null, ) { - this.navigate(TRUSTED_DEVICE_ROUTE, navOptions) + this.navigate(route = TrustedDeviceRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt index 7ae0ada9f9..e91af043bb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginNavigation.kt @@ -6,23 +6,21 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage -import com.bitwarden.network.util.base64UrlDecodeOrNull -import com.bitwarden.network.util.base64UrlEncode import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EMAIL_ADDRESS = "email_address" -private const val PASSWORD = "password" -private const val ORG_IDENTIFIER = "org_identifier" -private const val TWO_FACTOR_LOGIN_PREFIX = "two_factor_login" -private const val NEW_DEVICE_VERIFICATION = "new_device_verification" -private const val TWO_FACTOR_LOGIN_ROUTE = - "$TWO_FACTOR_LOGIN_PREFIX/{$EMAIL_ADDRESS}?" + - "$PASSWORD={$PASSWORD}&" + - "$ORG_IDENTIFIER={$ORG_IDENTIFIER}&" + - "$NEW_DEVICE_VERIFICATION={$NEW_DEVICE_VERIFICATION}" +/** + * The type-safe route for the two-factor login screen. + */ +@Serializable +data class TwoFactorLoginRoute( + val emailAddress: String, + val password: String?, + val orgIdentifier: String?, + val isNewDeviceVerification: Boolean, +) /** * Class to retrieve Two-Factor Login arguments from the [SavedStateHandle]. @@ -32,12 +30,18 @@ data class TwoFactorLoginArgs( val password: String?, val orgIdentifier: String?, val isNewDeviceVerification: Boolean, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - emailAddress = checkNotNull(savedStateHandle[EMAIL_ADDRESS]) as String, - password = savedStateHandle.get(PASSWORD)?.base64UrlDecodeOrNull(), - orgIdentifier = savedStateHandle.get(ORG_IDENTIFIER)?.base64UrlDecodeOrNull(), - isNewDeviceVerification = savedStateHandle.get(NEW_DEVICE_VERIFICATION) ?: false, +) + +/** + * Constructs a [TwoFactorLoginArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toTwoFactorLoginArgs(): TwoFactorLoginArgs { + val route = this.toRoute() + return TwoFactorLoginArgs( + emailAddress = route.emailAddress, + password = route.password, + orgIdentifier = route.orgIdentifier, + isNewDeviceVerification = route.isNewDeviceVerification, ) } @@ -48,14 +52,16 @@ fun NavController.navigateToTwoFactorLogin( emailAddress: String, password: String?, orgIdentifier: String?, - navOptions: NavOptions? = null, isNewDeviceVerification: Boolean = false, + navOptions: NavOptions? = null, ) { this.navigate( - route = "$TWO_FACTOR_LOGIN_PREFIX/$emailAddress?" + - "$PASSWORD=${password?.base64UrlEncode()}&" + - "$ORG_IDENTIFIER=${orgIdentifier?.base64UrlEncode()}&" + - "$NEW_DEVICE_VERIFICATION=$isNewDeviceVerification", + route = TwoFactorLoginRoute( + emailAddress = emailAddress, + password = password, + orgIdentifier = orgIdentifier, + isNewDeviceVerification = isNewDeviceVerification, + ), navOptions = navOptions, ) } @@ -66,23 +72,7 @@ fun NavController.navigateToTwoFactorLogin( fun NavGraphBuilder.twoFactorLoginDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = TWO_FACTOR_LOGIN_ROUTE, - arguments = listOf( - navArgument(EMAIL_ADDRESS) { type = NavType.StringType }, - navArgument(PASSWORD) { - type = NavType.StringType - nullable = true - }, - navArgument(ORG_IDENTIFIER) { - type = NavType.StringType - nullable = true - }, - navArgument(NEW_DEVICE_VERIFICATION) { - type = NavType.BoolType - }, - ), - ) { + composableWithSlideTransitions { TwoFactorLoginScreen( onNavigateBack = onNavigateBack, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index 8be2dd8cf1..398918fdae 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt @@ -57,7 +57,7 @@ class TwoFactorLoginViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { - val args = TwoFactorLoginArgs(savedStateHandle) + val args = savedStateHandle.toTwoFactorLoginArgs() TwoFactorLoginState( authMethod = authRepository.twoFactorResponse.preferredAuthMethod, availableAuthMethods = authRepository.twoFactorResponse.availableAuthMethods, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt index 2784dca6cb..9baa966356 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockNavigation.kt @@ -6,28 +6,55 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType +import com.x8bit.bitwarden.ui.platform.util.toObjectRoute +import kotlinx.serialization.Serializable -private const val VAULT_UNLOCK_TYPE: String = "unlock_type" -private const val TDE_VAULT_UNLOCK_ROUTE_PREFIX: String = "tde_vault_unlock" -private const val TDE_VAULT_UNLOCK_ROUTE: String = - "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}" -private const val VAULT_UNLOCK_ROUTE_PREFIX: String = "vault_unlock" -const val VAULT_UNLOCK_ROUTE: String = "$VAULT_UNLOCK_ROUTE_PREFIX/{$VAULT_UNLOCK_TYPE}" +/** + * The type-safe route for the vault unlock screen. + */ +@Serializable +sealed class VaultUnlockRoute { + /** + * The underlying [UnlockType] used in the vault unlock screen. + */ + abstract val unlockType: UnlockType + + /** + * The type-safe route for the standard vault unlock screen. + */ + @Serializable + data object Standard : VaultUnlockRoute() { + override val unlockType: UnlockType get() = UnlockType.STANDARD + } + + /** + * The type-safe route for the TDE vault unlock screen. + */ + @Serializable + data object Tde : VaultUnlockRoute() { + override val unlockType: UnlockType get() = UnlockType.TDE + } +} /** * Class to retrieve vault unlock arguments from the [SavedStateHandle]. */ data class VaultUnlockArgs( val unlockType: UnlockType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - unlockType = checkNotNull(savedStateHandle.get(VAULT_UNLOCK_TYPE)), - ) +) + +/** + * Constructs a [VaultUnlockArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toVaultUnlockArgs(): VaultUnlockArgs { + val route = this.toObjectRoute() + ?: this.toObjectRoute() + return route + ?.let { VaultUnlockArgs(unlockType = it.unlockType) } + ?: throw IllegalStateException("Missing correct route for VaultUnlockScreen") } /** @@ -37,7 +64,7 @@ fun NavController.navigateToVaultUnlock( navOptions: NavOptions? = null, ) { navigate( - route = "$VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.STANDARD}", + route = VaultUnlockRoute.Standard, navOptions = navOptions, ) } @@ -46,12 +73,7 @@ fun NavController.navigateToVaultUnlock( * Add the Vault Unlock screen to the nav graph. */ fun NavGraphBuilder.vaultUnlockDestination() { - composable( - route = VAULT_UNLOCK_ROUTE, - arguments = listOf( - navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) }, - ), - ) { + composable { VaultUnlockScreen() } } @@ -63,7 +85,7 @@ fun NavController.navigateToTdeVaultUnlock( navOptions: NavOptions? = null, ) { navigate( - route = "$TDE_VAULT_UNLOCK_ROUTE_PREFIX/${UnlockType.TDE}", + route = VaultUnlockRoute.Tde, navOptions = navOptions, ) } @@ -72,12 +94,7 @@ fun NavController.navigateToTdeVaultUnlock( * Add the Vault Unlock screen to the TDE nav graph. */ fun NavGraphBuilder.tdeVaultUnlockDestination() { - composable( - route = TDE_VAULT_UNLOCK_ROUTE, - arguments = listOf( - navArgument(VAULT_UNLOCK_TYPE) { type = NavType.EnumType(UnlockType::class.java) }, - ), - ) { + composable { VaultUnlockScreen() } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index c55a73a875..aad98d6c31 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -86,7 +86,7 @@ class VaultUnlockViewModel @Inject constructor( val specialCircumstance = specialCircumstanceManager.specialCircumstance val showAccountMenu = - VaultUnlockArgs(savedStateHandle).unlockType == UnlockType.STANDARD && + savedStateHandle.toVaultUnlockArgs().unlockType == UnlockType.STANDARD && (specialCircumstance !is SpecialCircumstance.Fido2GetCredentials && specialCircumstance !is SpecialCircumstance.Fido2Assertion) VaultUnlockState( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeNavigation.kt index 49002e498a..013bf5f897 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/welcome/WelcomeNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions +import kotlinx.serialization.Serializable -private const val WELCOME_ROUTE: String = "welcome" +/** + * The type-safe route for the welcome screen. + */ +@Serializable +data object WelcomeRoute /** * Navigate to the welcome screen. */ fun NavController.navigateToWelcome(navOptions: NavOptions? = null) { - this.navigate(WELCOME_ROUTE, navOptions) + this.navigate(route = WelcomeRoute, navOptions = navOptions) } /** @@ -25,9 +30,7 @@ fun NavGraphBuilder.welcomeDestination( onNavigateToLogin: () -> Unit, onNavigateToStartRegistration: () -> Unit, ) { - composableWithStayTransitions( - route = WELCOME_ROUTE, - ) { + composableWithStayTransitions { WelcomeScreen( onNavigateToCreateAccount = onNavigateToCreateAccount, onNavigateToLogin = onNavigateToLogin, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/NavGraphBuilderExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/NavGraphBuilderExtensions.kt index 7d2d789687..21240fda38 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/NavGraphBuilderExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/NavGraphBuilderExtensions.kt @@ -1,28 +1,28 @@ +@file:OmitFromCoverage + package com.x8bit.bitwarden.ui.platform.base.util import androidx.compose.animation.AnimatedContentScope import androidx.compose.runtime.Composable -import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavType import androidx.navigation.compose.composable import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders +import kotlin.reflect.KType /** * A wrapper around [NavGraphBuilder.composable] that supplies slide up/down transitions. */ -@OmitFromCoverage -fun NavGraphBuilder.composableWithSlideTransitions( - route: String, - arguments: List = emptyList(), +inline fun NavGraphBuilder.composableWithSlideTransitions( + typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { - this.composable( - route = route, - arguments = arguments, + this.composable( + typeMap = typeMap, deepLinks = deepLinks, enterTransition = TransitionProviders.Enter.slideUp, exitTransition = TransitionProviders.Exit.stay, @@ -35,21 +35,19 @@ fun NavGraphBuilder.composableWithSlideTransitions( /** * A wrapper around [NavGraphBuilder.composable] that supplies "stay" transitions. */ -@OmitFromCoverage -fun NavGraphBuilder.composableWithStayTransitions( - route: String, - arguments: List = emptyList(), +inline fun NavGraphBuilder.composableWithStayTransitions( + typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { - this.composable( - route = route, - arguments = arguments, + this.composable( + typeMap = typeMap, deepLinks = deepLinks, enterTransition = TransitionProviders.Enter.stay, exitTransition = TransitionProviders.Exit.stay, popEnterTransition = TransitionProviders.Enter.stay, popExitTransition = TransitionProviders.Exit.stay, + sizeTransform = null, content = content, ) } @@ -60,21 +58,19 @@ fun NavGraphBuilder.composableWithStayTransitions( * This is suitable for screens deeper within a hierarchy that uses push transitions; the root * screen of such a hierarchy should use [composableWithRootPushTransitions]. */ -@OmitFromCoverage -fun NavGraphBuilder.composableWithPushTransitions( - route: String, - arguments: List = emptyList(), +inline fun NavGraphBuilder.composableWithPushTransitions( + typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { - this.composable( - route = route, - arguments = arguments, + this.composable( + typeMap = typeMap, deepLinks = deepLinks, enterTransition = TransitionProviders.Enter.pushLeft, exitTransition = TransitionProviders.Exit.stay, popEnterTransition = TransitionProviders.Enter.stay, popExitTransition = TransitionProviders.Exit.pushRight, + sizeTransform = null, content = content, ) } @@ -83,21 +79,19 @@ fun NavGraphBuilder.composableWithPushTransitions( * A wrapper around [NavGraphBuilder.composable] that supplies push transitions to the root screen * in a nested graph that uses push transitions. */ -@OmitFromCoverage -fun NavGraphBuilder.composableWithRootPushTransitions( - route: String, - arguments: List = emptyList(), +inline fun NavGraphBuilder.composableWithRootPushTransitions( + typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), - content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, + noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { - this.composable( - route = route, - arguments = arguments, + this.composable( + typeMap = typeMap, deepLinks = deepLinks, enterTransition = TransitionProviders.Enter.stay, exitTransition = TransitionProviders.Exit.pushLeft, popEnterTransition = TransitionProviders.Enter.pushRight, popExitTransition = TransitionProviders.Exit.fadeOut, + sizeTransform = null, content = content, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuNavigation.kt index 26d5e9cb3f..e68b340fbe 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuNavigation.kt @@ -6,14 +6,19 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val DEBUG_MENU = "debug_menu" +/** + * The type-safe route for the debug screen. + */ +@Serializable +data object DebugRoute /** * Navigate to the setup unlock screen. */ fun NavController.navigateToDebugMenuScreen() { - this.navigate(DEBUG_MENU) { + this.navigate(route = DebugRoute) { launchSingleTop = true } } @@ -25,9 +30,7 @@ fun NavGraphBuilder.debugMenuDestination( onNavigateBack: () -> Unit, onSplashScreenRemoved: () -> Unit, ) { - composableWithPushTransitions( - route = DEBUG_MENU, - ) { + composableWithPushTransitions { DebugMenuScreen(onNavigateBack = onNavigateBack) // If we are displaying the debug screen, then we can just hide the splash screen. onSplashScreenRemoved() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 35c1556710..8361b0606e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -17,48 +17,49 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.navOptions import com.bitwarden.core.annotation.OmitFromCoverage -import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_AUTO_FILL_AS_ROOT_ROUTE -import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_COMPLETE_ROUTE -import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SETUP_UNLOCK_AS_ROOT_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupAutofillRoute +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupCompleteRoute +import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockRoute import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupAutoFillAsRootScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupCompleteScreen import com.x8bit.bitwarden.ui.auth.feature.accountsetup.navigateToSetupUnlockScreenAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupAutoFillDestinationAsRoot import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupCompleteDestination import com.x8bit.bitwarden.ui.auth.feature.accountsetup.setupUnlockDestinationAsRoot -import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.auth.AuthGraphRoute import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration import com.x8bit.bitwarden.ui.auth.feature.expiredregistrationlink.navigateToExpiredRegistrationLinkScreen import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.navigateToPreventAccountLockout -import com.x8bit.bitwarden.ui.auth.feature.removepassword.REMOVE_PASSWORD_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.removepassword.RemovePasswordRoute import com.x8bit.bitwarden.ui.auth.feature.removepassword.navigateToRemovePassword import com.x8bit.bitwarden.ui.auth.feature.removepassword.removePasswordDestination -import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.resetpassword.ResetPasswordRoute import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordScreen import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination -import com.x8bit.bitwarden.ui.auth.feature.setpassword.SET_PASSWORD_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.setpassword.SetPasswordRoute import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword -import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TRUSTED_DEVICE_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceGraphRoute import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.navigateToTrustedDeviceGraph import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.trustedDeviceGraph -import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE +import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VaultUnlockRoute import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination import com.x8bit.bitwarden.ui.auth.feature.welcome.navigateToWelcome import com.x8bit.bitwarden.ui.platform.components.util.rememberBitwardenNavController import com.x8bit.bitwarden.ui.platform.feature.rootnav.util.toVaultItemListingType import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval -import com.x8bit.bitwarden.ui.platform.feature.splash.SPLASH_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.splash.SplashRoute import com.x8bit.bitwarden.ui.platform.feature.splash.navigateToSplash import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination -import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VaultUnlockedGraphRoute import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph import com.x8bit.bitwarden.ui.platform.theme.NonNullEnterTransitionProvider import com.x8bit.bitwarden.ui.platform.theme.NonNullExitTransitionProvider import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders +import com.x8bit.bitwarden.ui.platform.util.toObjectNavigationRoute import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs @@ -89,7 +90,7 @@ fun RootNavScreen( NavHost( navController = navController, - startDestination = SPLASH_ROUTE, + startDestination = SplashRoute, enterTransition = { toEnterTransition()(this) }, exitTransition = { toExitTransition()(this) }, popEnterTransition = { toEnterTransition()(this) }, @@ -116,14 +117,14 @@ fun RootNavScreen( is RootNavState.CompleteOngoingRegistration, RootNavState.AuthWithWelcome, RootNavState.ExpiredRegistrationLink, - -> AUTH_GRAPH_ROUTE + -> AuthGraphRoute - RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE - RootNavState.SetPassword -> SET_PASSWORD_ROUTE - RootNavState.RemovePassword -> REMOVE_PASSWORD_ROUTE - RootNavState.Splash -> SPLASH_ROUTE - RootNavState.TrustedDevice -> TRUSTED_DEVICE_GRAPH_ROUTE - RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE + RootNavState.ResetPassword -> ResetPasswordRoute + RootNavState.SetPassword -> SetPasswordRoute + RootNavState.RemovePassword -> RemovePasswordRoute + RootNavState.Splash -> SplashRoute + RootNavState.TrustedDevice -> TrustedDeviceGraphRoute + RootNavState.VaultLocked -> VaultUnlockRoute.Standard is RootNavState.VaultUnlocked, is RootNavState.VaultUnlockedForAutofillSave, is RootNavState.VaultUnlockedForAutofillSelection, @@ -133,11 +134,11 @@ fun RootNavScreen( is RootNavState.VaultUnlockedForFido2Save, is RootNavState.VaultUnlockedForFido2Assertion, is RootNavState.VaultUnlockedForFido2GetCredentials, - -> VAULT_UNLOCKED_GRAPH_ROUTE + -> VaultUnlockedGraphRoute - RootNavState.OnboardingAccountLockSetup -> SETUP_UNLOCK_AS_ROOT_ROUTE - RootNavState.OnboardingAutoFillSetup -> SETUP_AUTO_FILL_AS_ROOT_ROUTE - RootNavState.OnboardingStepsComplete -> SETUP_COMPLETE_ROUTE + RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot + RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot + RootNavState.OnboardingStepsComplete -> SetupCompleteRoute } val currentRoute = navController.currentDestination?.rootLevelRoute() @@ -145,7 +146,9 @@ fun RootNavScreen( // death. In this case, the NavHost already restores state, so we don't have to navigate. // However, if the route is correct but the underlying state is different, we should still // proceed in order to get a fresh version of that route. - if (currentRoute == targetRoute && previousStateReference.get() == state) { + if (currentRoute == targetRoute.toObjectNavigationRoute() && + previousStateReference.get() == state + ) { previousStateReference.set(state) return } @@ -291,13 +294,13 @@ private fun NavDestination?.rootLevelRoute(): String? { @Suppress("MaxLineLength") private fun AnimatedContentTransitionScope.toEnterTransition(): NonNullEnterTransitionProvider = when (targetState.destination.rootLevelRoute()) { - RESET_PASSWORD_ROUTE -> RootTransitionProviders.Enter.slideUp + ResetPasswordRoute.toObjectNavigationRoute() -> RootTransitionProviders.Enter.slideUp else -> when (initialState.destination.rootLevelRoute()) { // Disable transitions when coming from the splash screen - SPLASH_ROUTE -> RootTransitionProviders.Enter.none + SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Enter.none // The RESET_PASSWORD_ROUTE animation should be stay but due to an issue when combining // certain animations, we are just using a fadeIn instead. - RESET_PASSWORD_ROUTE -> RootTransitionProviders.Enter.fadeIn + ResetPasswordRoute.toObjectNavigationRoute() -> RootTransitionProviders.Enter.fadeIn else -> RootTransitionProviders.Enter.fadeIn } } @@ -306,13 +309,14 @@ private fun AnimatedContentTransitionScope.toEnterTransition( * Define the exit transition for each route. */ @Suppress("MaxLineLength") -private fun AnimatedContentTransitionScope.toExitTransition(): NonNullExitTransitionProvider = - when (initialState.destination.rootLevelRoute()) { +private fun AnimatedContentTransitionScope.toExitTransition(): NonNullExitTransitionProvider { + return when (initialState.destination.rootLevelRoute()) { // Disable transitions when coming from the splash screen - SPLASH_ROUTE -> RootTransitionProviders.Exit.none - RESET_PASSWORD_ROUTE -> RootTransitionProviders.Exit.slideDown + SplashRoute.toObjectNavigationRoute() -> RootTransitionProviders.Exit.none + ResetPasswordRoute.toObjectNavigationRoute() -> RootTransitionProviders.Exit.slideDown else -> when (targetState.destination.rootLevelRoute()) { - RESET_PASSWORD_ROUTE -> RootTransitionProviders.Exit.stay + ResetPasswordRoute.toObjectNavigationRoute() -> RootTransitionProviders.Exit.stay else -> RootTransitionProviders.Exit.fadeOut } } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt index 5ff11447c0..be1534e37b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchNavigation.kt @@ -6,46 +6,79 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs +import kotlinx.serialization.Serializable -private const val SEARCH_TYPE: String = "search_type" -private const val SEARCH_TYPE_SEND_ALL: String = "search_type_sends_all" -private const val SEARCH_TYPE_SEND_TEXT: String = "search_type_sends_text" -private const val SEARCH_TYPE_SEND_FILE: String = "search_type_sends_file" -private const val SEARCH_TYPE_VAULT_ALL: String = "search_type_vault_all" -private const val SEARCH_TYPE_VAULT_LOGINS: String = "search_type_vault_logins" -private const val SEARCH_TYPE_VAULT_CARDS: String = "search_type_vault_cards" -private const val SEARCH_TYPE_VAULT_IDENTITIES: String = "search_type_vault_identities" -private const val SEARCH_TYPE_VAULT_SECURE_NOTES: String = "search_type_vault_secure_notes" -private const val SEARCH_TYPE_VAULT_COLLECTION: String = "search_type_vault_collection" -private const val SEARCH_TYPE_VAULT_NO_FOLDER: String = "search_type_vault_no_folder" -private const val SEARCH_TYPE_VAULT_FOLDER: String = "search_type_vault_folder" -private const val SEARCH_TYPE_VAULT_TRASH: String = "search_type_vault_trash" -private const val SEARCH_TYPE_VAULT_VERIFICATION_CODES: String = - "search_type_vault_verification_codes" -private const val SEARCH_TYPE_ID: String = "search_type_id" -private const val SEARCH_TYPE_VAULT_SSH_KEYS: String = "search_type_vault_ssh_keys" +/** + * The type-safe route for the search screen. + */ +@Serializable +data class SearchRoute( + val searchableItemType: SearchableItemType, + val id: String?, +) -private const val SEARCH_ROUTE_PREFIX: String = "search" -private const val SEARCH_ROUTE: String = "$SEARCH_ROUTE_PREFIX/{$SEARCH_TYPE}/{$SEARCH_TYPE_ID}" +/** + * Represents the various types of searchable items. + */ +@Serializable +enum class SearchableItemType { + SENDS_ALL, + SENDS_TEXTS, + SENDS_FILES, + VAULT_ALL, + VAULT_LOGINS, + VAULT_CARDS, + VAULT_IDENTITIES, + VAULT_SECURE_NOTES, + VAULT_SSH_KEYS, + VAULT_COLLECTIONS, + VAULT_NO_FOLDER, + VAULT_FOLDER, + VAULT_TRASH, + VAULT_VERIFICATION_CODES, +} /** * Class to retrieve search arguments from the [SavedStateHandle]. */ data class SearchArgs( val type: SearchType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - type = determineSearchType( - searchTypeString = requireNotNull(savedStateHandle.get(SEARCH_TYPE)), - id = savedStateHandle.get(SEARCH_TYPE_ID), - ), +) + +/** + * Constructs a [SearchArgs] from the [SavedStateHandle] and internal route data. + */ +@Suppress("CyclomaticComplexMethod") +fun SavedStateHandle.toSearchArgs(): SearchArgs { + val route = this.toRoute() + return SearchArgs( + type = when (route.searchableItemType) { + SearchableItemType.SENDS_ALL -> SearchType.Sends.All + SearchableItemType.SENDS_TEXTS -> SearchType.Sends.Texts + SearchableItemType.SENDS_FILES -> SearchType.Sends.Files + SearchableItemType.VAULT_ALL -> SearchType.Vault.All + SearchableItemType.VAULT_LOGINS -> SearchType.Vault.Logins + SearchableItemType.VAULT_CARDS -> SearchType.Vault.Cards + SearchableItemType.VAULT_IDENTITIES -> SearchType.Vault.Identities + SearchableItemType.VAULT_SECURE_NOTES -> SearchType.Vault.SecureNotes + SearchableItemType.VAULT_SSH_KEYS -> SearchType.Vault.SshKeys + SearchableItemType.VAULT_NO_FOLDER -> SearchType.Vault.NoFolder + SearchableItemType.VAULT_TRASH -> SearchType.Vault.Trash + SearchableItemType.VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes + SearchableItemType.VAULT_FOLDER -> SearchType.Vault.Folder( + folderId = requireNotNull(route.id), + ) + + SearchableItemType.VAULT_COLLECTIONS -> SearchType.Vault.Collection( + collectionId = requireNotNull(route.id), + ) + }, ) } @@ -58,16 +91,7 @@ fun NavGraphBuilder.searchDestination( onNavigateToEditCipher: (args: VaultAddEditArgs) -> Unit, onNavigateToViewCipher: (args: VaultItemArgs) -> Unit, ) { - composableWithSlideTransitions( - route = SEARCH_ROUTE, - arguments = listOf( - navArgument(SEARCH_TYPE) { type = NavType.StringType }, - navArgument(SEARCH_TYPE_ID) { - type = NavType.StringType - nullable = true - }, - ), - ) { + composableWithSlideTransitions { SearchScreen( onNavigateBack = onNavigateBack, onNavigateToEditSend = onNavigateToEditSend, @@ -84,50 +108,31 @@ fun NavController.navigateToSearch( searchType: SearchType, navOptions: NavOptions? = null, ) { - navigate( - route = "$SEARCH_ROUTE_PREFIX/${searchType.toTypeString()}/${searchType.toIdOrNull()}", + this.navigate( + route = SearchRoute( + searchableItemType = searchType.toSearchableItemType(), + id = searchType.toIdOrNull(), + ), navOptions = navOptions, ) } -private fun determineSearchType( - searchTypeString: String, - id: String?, -): SearchType = - when (searchTypeString) { - SEARCH_TYPE_SEND_ALL -> SearchType.Sends.All - SEARCH_TYPE_SEND_TEXT -> SearchType.Sends.Texts - SEARCH_TYPE_SEND_FILE -> SearchType.Sends.Files - SEARCH_TYPE_VAULT_ALL -> SearchType.Vault.All - SEARCH_TYPE_VAULT_LOGINS -> SearchType.Vault.Logins - SEARCH_TYPE_VAULT_CARDS -> SearchType.Vault.Cards - SEARCH_TYPE_VAULT_IDENTITIES -> SearchType.Vault.Identities - SEARCH_TYPE_VAULT_SECURE_NOTES -> SearchType.Vault.SecureNotes - SEARCH_TYPE_VAULT_COLLECTION -> SearchType.Vault.Collection(requireNotNull(id)) - SEARCH_TYPE_VAULT_NO_FOLDER -> SearchType.Vault.NoFolder - SEARCH_TYPE_VAULT_FOLDER -> SearchType.Vault.Folder(requireNotNull(id)) - SEARCH_TYPE_VAULT_TRASH -> SearchType.Vault.Trash - SEARCH_TYPE_VAULT_VERIFICATION_CODES -> SearchType.Vault.VerificationCodes - SEARCH_TYPE_VAULT_SSH_KEYS -> SearchType.Vault.SshKeys - else -> throw IllegalArgumentException("Invalid Search Type") - } - -private fun SearchType.toTypeString(): String = +private fun SearchType.toSearchableItemType(): SearchableItemType = when (this) { - SearchType.Sends.All -> SEARCH_TYPE_SEND_ALL - SearchType.Sends.Files -> SEARCH_TYPE_SEND_FILE - SearchType.Sends.Texts -> SEARCH_TYPE_SEND_TEXT - SearchType.Vault.All -> SEARCH_TYPE_VAULT_ALL - SearchType.Vault.Cards -> SEARCH_TYPE_VAULT_CARDS - is SearchType.Vault.Collection -> SEARCH_TYPE_VAULT_COLLECTION - is SearchType.Vault.Folder -> SEARCH_TYPE_VAULT_FOLDER - SearchType.Vault.Identities -> SEARCH_TYPE_VAULT_IDENTITIES - SearchType.Vault.Logins -> SEARCH_TYPE_VAULT_LOGINS - SearchType.Vault.NoFolder -> SEARCH_TYPE_VAULT_NO_FOLDER - SearchType.Vault.SecureNotes -> SEARCH_TYPE_VAULT_SECURE_NOTES - SearchType.Vault.Trash -> SEARCH_TYPE_VAULT_TRASH - SearchType.Vault.VerificationCodes -> SEARCH_TYPE_VAULT_VERIFICATION_CODES - SearchType.Vault.SshKeys -> SEARCH_TYPE_VAULT_SSH_KEYS + SearchType.Sends.All -> SearchableItemType.SENDS_ALL + SearchType.Sends.Files -> SearchableItemType.SENDS_FILES + SearchType.Sends.Texts -> SearchableItemType.SENDS_TEXTS + SearchType.Vault.All -> SearchableItemType.VAULT_ALL + SearchType.Vault.Cards -> SearchableItemType.VAULT_CARDS + is SearchType.Vault.Collection -> SearchableItemType.VAULT_COLLECTIONS + is SearchType.Vault.Folder -> SearchableItemType.VAULT_FOLDER + SearchType.Vault.Identities -> SearchableItemType.VAULT_IDENTITIES + SearchType.Vault.Logins -> SearchableItemType.VAULT_LOGINS + SearchType.Vault.NoFolder -> SearchableItemType.VAULT_NO_FOLDER + SearchType.Vault.SecureNotes -> SearchableItemType.VAULT_SECURE_NOTES + SearchType.Vault.Trash -> SearchableItemType.VAULT_TRASH + SearchType.Vault.VerificationCodes -> SearchableItemType.VAULT_VERIFICATION_CODES + SearchType.Vault.SshKeys -> SearchableItemType.VAULT_SSH_KEYS } private fun SearchType.toIdOrNull(): String? = diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt index 85c12f5b67..4ad3e5b262 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModel.kt @@ -87,7 +87,7 @@ class SearchViewModel @Inject constructor( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { - val searchType = SearchArgs(savedStateHandle).type + val searchType = savedStateHandle.toSearchArgs().type val userState = requireNotNull(authRepo.userStateFlow.value) val specialCircumstance = specialCircumstanceManager.specialCircumstance val searchTerm = (specialCircumstance as? SpecialCircumstance.SearchShortcut) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 340644798d..d1ca25e3c8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -7,8 +7,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument import androidx.navigation.navOptions import androidx.navigation.navigation import com.bitwarden.core.annotation.OmitFromCoverage @@ -33,20 +31,57 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDestination import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import com.x8bit.bitwarden.ui.platform.util.toObjectRoute +import kotlinx.serialization.Serializable -private const val IS_PRE_AUTH: String = "isPreAuth" -private const val PRE_AUTH_SETTINGS_ROUTE = "pre_auth_settings" +/** + * The type-safe route for the settings graph. + */ +@Serializable +data object SettingsGraphRoute -const val SETTINGS_GRAPH_ROUTE: String = "settings_graph" -const val SETTINGS_ROUTE: String = "settings" +/** + * The type-safe route for the settings screen. + */ +@Serializable +sealed class SettingsRoute { + /** + * Indicates that the settings screen should be shown as a pre-authentication. + */ + abstract val isPreAuth: Boolean + + /** + * The type-safe route for the settings screen in the settings graph. + */ + @Serializable + data object Standard : SettingsRoute() { + override val isPreAuth: Boolean get() = false + } + + /** + * The type-safe route for the pre-auth settings screen. + */ + @Serializable + data object PreAuth : SettingsRoute() { + override val isPreAuth: Boolean get() = true + } +} /** * Class to retrieve settings arguments from the [SavedStateHandle]. */ -data class SettingsArgs(val isPreAuth: Boolean) { - constructor(savedStateHandle: SavedStateHandle) : this( - isPreAuth = requireNotNull(savedStateHandle[IS_PRE_AUTH]), - ) +data class SettingsArgs(val isPreAuth: Boolean) + +/** + * Constructs a [SettingsArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toSettingsArgs(): SettingsArgs { + val route = this.toObjectRoute() + ?: this.toObjectRoute() + return route + ?.let { SettingsArgs(isPreAuth = it.isPreAuth) } + ?: this.toObjectRoute()?.let { SettingsArgs(isPreAuth = false) } + ?: throw IllegalStateException("Missing correct route for SettingsScreen") } /** @@ -65,19 +100,10 @@ fun NavGraphBuilder.settingsGraph( onNavigateToRecordedLogs: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit, ) { - navigation( - startDestination = SETTINGS_ROUTE, - route = SETTINGS_GRAPH_ROUTE, + navigation( + startDestination = SettingsRoute.Standard, ) { - composableWithRootPushTransitions( - route = SETTINGS_ROUTE, - arguments = listOf( - navArgument(name = IS_PRE_AUTH) { - type = NavType.BoolType - defaultValue = false - }, - ), - ) { + composableWithRootPushTransitions { SettingsScreen( onNavigateBack = {}, onNavigateToAbout = { navController.navigateToAbout(isPreAuth = false) }, @@ -129,15 +155,7 @@ fun NavGraphBuilder.settingsGraph( fun NavGraphBuilder.preAuthSettingsDestinations( navController: NavController, ) { - composableWithSlideTransitions( - route = PRE_AUTH_SETTINGS_ROUTE, - arguments = listOf( - navArgument(name = IS_PRE_AUTH) { - type = NavType.BoolType - defaultValue = true - }, - ), - ) { + composableWithSlideTransitions { SettingsScreen( onNavigateBack = { navController.popBackStack() }, onNavigateToAbout = { navController.navigateToAbout(isPreAuth = true) }, @@ -176,7 +194,7 @@ fun NavGraphBuilder.preAuthSettingsDestinations( * Navigate to the settings graph. */ fun NavController.navigateToSettingsGraph(navOptions: NavOptions? = null) { - navigate(SETTINGS_GRAPH_ROUTE, navOptions) + this.navigate(route = SettingsGraphRoute, navOptions = navOptions) } /** @@ -194,12 +212,12 @@ fun NavController.navigateToSettingsGraphRoot() { }, ) // Then ensures that we are at the root - popBackStack(route = SETTINGS_ROUTE, inclusive = false) + popBackStack(route = SettingsRoute.Standard, inclusive = false) } /** * Navigate to the pre-auth settings screen. */ fun NavController.navigateToPreAuthSettings(navOptions: NavOptions? = null) { - navigate(PRE_AUTH_SETTINGS_ROUTE, navOptions) + this.navigate(route = SettingsRoute.PreAuth, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt index 80d43e75ed..cba52da8f5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModel.kt @@ -31,7 +31,7 @@ class SettingsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = SettingsState( - isPreAuth = SettingsArgs(savedStateHandle = savedStateHandle).isPreAuth, + isPreAuth = savedStateHandle.toSettingsArgs().isPreAuth, securityCount = firstTimeActionManager.allSecuritySettingsBadgeCountFlow.value, autoFillCount = firstTimeActionManager.allAutofillSettingsBadgeCountFlow.value, vaultCount = firstTimeActionManager.allVaultSettingsBadgeCountFlow.value, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutNavigation.kt index 046434ae80..ea1975da75 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/about/AboutNavigation.kt @@ -7,9 +7,25 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val PRE_AUTH_ABOUT_ROUTE = "pre_auth_settings_about" -private const val ABOUT_ROUTE = "settings_about" +/** + * The type-safe route for the settings about screen. + */ +@Serializable +sealed class SettingsAboutRoute { + /** + * The type-safe route for the settings about screen. + */ + @Serializable + data object Standard : SettingsAboutRoute() + + /** + * The type-safe route for the pre-auth settings about screen. + */ + @Serializable + data object PreAuth : SettingsAboutRoute() +} /** * Add settings destinations to the nav graph. @@ -20,14 +36,22 @@ fun NavGraphBuilder.aboutDestination( onNavigateToFlightRecorder: () -> Unit, onNavigateToRecordedLogs: () -> Unit, ) { - composableWithPushTransitions( - route = getRoute(isPreAuth = isPreAuth), - ) { - AboutScreen( - onNavigateBack = onNavigateBack, - onNavigateToFlightRecorder = onNavigateToFlightRecorder, - onNavigateToRecordedLogs = onNavigateToRecordedLogs, - ) + if (isPreAuth) { + composableWithPushTransitions { + AboutScreen( + onNavigateBack = onNavigateBack, + onNavigateToFlightRecorder = onNavigateToFlightRecorder, + onNavigateToRecordedLogs = onNavigateToRecordedLogs, + ) + } + } else { + composableWithPushTransitions { + AboutScreen( + onNavigateBack = onNavigateBack, + onNavigateToFlightRecorder = onNavigateToFlightRecorder, + onNavigateToRecordedLogs = onNavigateToRecordedLogs, + ) + } } } @@ -38,9 +62,8 @@ fun NavController.navigateToAbout( isPreAuth: Boolean, navOptions: NavOptions? = null, ) { - navigate(route = getRoute(isPreAuth = isPreAuth), navOptions = navOptions) + navigate( + route = if (isPreAuth) SettingsAboutRoute.PreAuth else SettingsAboutRoute.Standard, + navOptions = navOptions, + ) } - -private fun getRoute( - isPreAuth: Boolean, -): String = if (isPreAuth) PRE_AUTH_ABOUT_ROUTE else ABOUT_ROUTE diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt index 45215e0603..fe47f1932f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val ACCOUNT_SECURITY_ROUTE = "settings_account_security" +/** + * The type-safe route for the account security screen. + */ +@Serializable +data object AccountSecurityRoute /** * Add settings destinations to the nav graph. @@ -19,9 +24,7 @@ fun NavGraphBuilder.accountSecurityDestination( onNavigateToPendingRequests: () -> Unit, onNavigateToSetupUnlockScreen: () -> Unit, ) { - composableWithPushTransitions( - route = ACCOUNT_SECURITY_ROUTE, - ) { + composableWithPushTransitions { AccountSecurityScreen( onNavigateBack = onNavigateBack, onNavigateToDeleteAccount = onNavigateToDeleteAccount, @@ -35,5 +38,5 @@ fun NavGraphBuilder.accountSecurityDestination( * Navigate to the account security screen. */ fun NavController.navigateToAccountSecurity(navOptions: NavOptions? = null) { - navigate(ACCOUNT_SECURITY_ROUTE, navOptions) + this.navigate(route = AccountSecurityRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt index 25de18f0ce..fd593e8a27 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val DELETE_ACCOUNT_ROUTE = "delete_account" +/** + * The type-safe route for the delete account screen. + */ +@Serializable +data object DeleteAccountRoute /** * Add delete account destinations to the nav graph. @@ -17,9 +22,7 @@ fun NavGraphBuilder.deleteAccountDestination( onNavigateBack: () -> Unit, onNavigateToDeleteAccountConfirmation: () -> Unit, ) { - composableWithSlideTransitions( - route = DELETE_ACCOUNT_ROUTE, - ) { + composableWithSlideTransitions { DeleteAccountScreen( onNavigateBack = onNavigateBack, onNavigateToDeleteAccountConfirmation = onNavigateToDeleteAccountConfirmation, @@ -31,5 +34,5 @@ fun NavGraphBuilder.deleteAccountDestination( * Navigate to the delete account screen. */ fun NavController.navigateToDeleteAccount(navOptions: NavOptions? = null) { - navigate(DELETE_ACCOUNT_ROUTE, navOptions) + this.navigate(route = DeleteAccountRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationNavigation.kt index ad997bf41a..2ae0fdd54a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccountconfirmation/DeleteAccountConfirmationNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val DELETE_ACCOUNT_CONFIRMATION_ROUTE = "delete_account_confirmation" +/** + * The type-safe route for the delete account confirmation screen. + */ +@Serializable +data object DeleteAccountConfirmationRoute /** * Add delete account confirmation destinations to the nav graph. @@ -16,9 +21,7 @@ private const val DELETE_ACCOUNT_CONFIRMATION_ROUTE = "delete_account_confirmati fun NavGraphBuilder.deleteAccountConfirmationDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = DELETE_ACCOUNT_CONFIRMATION_ROUTE, - ) { + composableWithSlideTransitions { DeleteAccountConfirmationScreen( onNavigateBack = onNavigateBack, ) @@ -29,5 +32,5 @@ fun NavGraphBuilder.deleteAccountConfirmationDestination( * Navigate to the [DeleteAccountConfirmationScreen]. */ fun NavController.navigateToDeleteAccountConfirmation(navOptions: NavOptions? = null) { - navigate(DELETE_ACCOUNT_CONFIRMATION_ROUTE, navOptions) + this.navigate(route = DeleteAccountConfirmationRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalNavigation.kt index 67ab4566b3..8d907cd0f7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalNavigation.kt @@ -6,22 +6,30 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val FINGERPRINT: String = "fingerprint" -private const val LOGIN_APPROVAL_PREFIX = "login_approval" -private const val LOGIN_APPROVAL_ROUTE = "$LOGIN_APPROVAL_PREFIX?$FINGERPRINT={$FINGERPRINT}" +/** + * The type-safe route for the login approval screen. + */ +@Serializable +data class LoginApprovalRoute( + val fingerprint: String?, +) /** * Class to retrieve login approval arguments from the [SavedStateHandle]. */ -data class LoginApprovalArgs(val fingerprint: String?) { - constructor(savedStateHandle: SavedStateHandle) : this( - fingerprint = savedStateHandle.get(FINGERPRINT), - ) +data class LoginApprovalArgs(val fingerprint: String?) + +/** + * Constructs a [LoginApprovalArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toLoginApprovalArgs(): LoginApprovalArgs { + val route = this.toRoute() + return LoginApprovalArgs(fingerprint = route.fingerprint) } /** @@ -30,16 +38,7 @@ data class LoginApprovalArgs(val fingerprint: String?) { fun NavGraphBuilder.loginApprovalDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = LOGIN_APPROVAL_ROUTE, - arguments = listOf( - navArgument(FINGERPRINT) { - type = NavType.StringType - nullable = true - defaultValue = null - }, - ), - ) { + composableWithSlideTransitions { LoginApprovalScreen( onNavigateBack = onNavigateBack, ) @@ -53,5 +52,5 @@ fun NavController.navigateToLoginApproval( fingerprint: String?, navOptions: NavOptions? = null, ) { - navigate("$LOGIN_APPROVAL_PREFIX?$FINGERPRINT=$fingerprint", navOptions) + this.navigate(route = LoginApprovalRoute(fingerprint = fingerprint), navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt index 281d307305..4202ca7e80 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModel.kt @@ -5,6 +5,8 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginap import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestResult import com.x8bit.bitwarden.data.auth.manager.model.AuthRequestUpdatesResult @@ -12,8 +14,6 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance import com.x8bit.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn @@ -45,7 +45,7 @@ class LoginApprovalViewModel @Inject constructor( specialCircumstance = specialCircumstance, fingerprint = specialCircumstance ?.let { "" } - ?: requireNotNull(LoginApprovalArgs(savedStateHandle).fingerprint), + ?: requireNotNull(savedStateHandle.toLoginApprovalArgs().fingerprint), masterPasswordHash = null, publicKey = "", requestId = "", diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsNavigation.kt index 0c7b3cec79..5e12a95857 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/pendingrequests/PendingRequestsNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val PENDING_REQUESTS_ROUTE = "pending_requests" +/** + * The type-safe route for the pending requests screen. + */ +@Serializable +data object PendingRequestsRoute /** * Add pending requests destinations to the nav graph. @@ -17,9 +22,7 @@ fun NavGraphBuilder.pendingRequestsDestination( onNavigateBack: () -> Unit, onNavigateToLoginApproval: (fingerprintPhrase: String) -> Unit, ) { - composableWithSlideTransitions( - route = PENDING_REQUESTS_ROUTE, - ) { + composableWithSlideTransitions { PendingRequestsScreen( onNavigateBack = onNavigateBack, onNavigateToLoginApproval = onNavigateToLoginApproval, @@ -31,5 +34,5 @@ fun NavGraphBuilder.pendingRequestsDestination( * Navigate to the Pending Login Requests screen. */ fun NavController.navigateToPendingRequests(navOptions: NavOptions? = null) { - navigate(PENDING_REQUESTS_ROUTE, navOptions) + this.navigate(route = PendingRequestsRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceNavigation.kt index 6d7658f483..9cce737a46 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/appearance/AppearanceNavigation.kt @@ -7,9 +7,25 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val PRE_AUTH_APPEARANCE_ROUTE = "pre_auth_settings_appearance" -private const val APPEARANCE_ROUTE = "settings_appearance" +/** + * The type-safe route for the settings appearance screen. + */ +@Serializable +sealed class SettingsAppearanceRoute { + /** + * The type-safe route for the settings appearance screen. + */ + @Serializable + data object Standard : SettingsAppearanceRoute() + + /** + * The type-safe route for the pre-auth settings appearance screen. + */ + @Serializable + data object PreAuth : SettingsAppearanceRoute() +} /** * Add settings destinations to the nav graph. @@ -18,10 +34,14 @@ fun NavGraphBuilder.appearanceDestination( isPreAuth: Boolean, onNavigateBack: () -> Unit, ) { - composableWithPushTransitions( - route = getRoute(isPreAuth = isPreAuth), - ) { - AppearanceScreen(onNavigateBack = onNavigateBack) + if (isPreAuth) { + composableWithPushTransitions { + AppearanceScreen(onNavigateBack = onNavigateBack) + } + } else { + composableWithPushTransitions { + AppearanceScreen(onNavigateBack = onNavigateBack) + } } } @@ -32,9 +52,12 @@ fun NavController.navigateToAppearance( isPreAuth: Boolean, navOptions: NavOptions? = null, ) { - navigate(route = getRoute(isPreAuth = isPreAuth), navOptions = navOptions) + this.navigate( + route = if (isPreAuth) { + SettingsAppearanceRoute.PreAuth + } else { + SettingsAppearanceRoute.Standard + }, + navOptions = navOptions, + ) } - -private fun getRoute( - isPreAuth: Boolean, -): String = if (isPreAuth) PRE_AUTH_APPEARANCE_ROUTE else APPEARANCE_ROUTE diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt index 7b1578bfaa..b2e75afd98 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val AUTO_FILL_ROUTE = "settings_auto_fill" +/** + * The type-safe route for the autofill screen. + */ +@Serializable +data object AutofillRoute /** * Add settings destinations to the nav graph. @@ -18,9 +23,7 @@ fun NavGraphBuilder.autoFillDestination( onNavigateToBlockAutoFillScreen: () -> Unit, onNavigateToSetupAutofill: () -> Unit, ) { - composableWithPushTransitions( - route = AUTO_FILL_ROUTE, - ) { + composableWithPushTransitions { AutoFillScreen( onNavigateBack = onNavigateBack, onNavigateToBlockAutoFillScreen = onNavigateToBlockAutoFillScreen, @@ -33,5 +36,5 @@ fun NavGraphBuilder.autoFillDestination( * Navigate to the auto-fill screen. */ fun NavController.navigateToAutoFill(navOptions: NavOptions? = null) { - navigate(AUTO_FILL_ROUTE, navOptions) + this.navigate(route = AutofillRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt index b6eaf84d5b..d1603bd8b0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/blockautofill/BlockAutoFillNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable -private const val BLOCK_AUTO_FILL_ROUTE = "settings_block_auto_fill" +/** + * The type-safe route for the block autofill settings screen. + */ +@Serializable +data object BlockAutofillSettingsRoute /** * Add block auto-fill destination to the nav graph. @@ -16,9 +21,7 @@ private const val BLOCK_AUTO_FILL_ROUTE = "settings_block_auto_fill" fun NavGraphBuilder.blockAutoFillDestination( onNavigateBack: () -> Unit, ) { - composableWithPushTransitions( - route = BLOCK_AUTO_FILL_ROUTE, - ) { + composableWithPushTransitions { BlockAutoFillScreen(onNavigateBack = onNavigateBack) } } @@ -27,5 +30,5 @@ fun NavGraphBuilder.blockAutoFillDestination( * Navigate to the block auto-fill screen. */ fun NavController.navigateToBlockAutoFillScreen(navOptions: NavOptions? = null) { - navigate(BLOCK_AUTO_FILL_ROUTE, navOptions) + this.navigate(route = BlockAutofillSettingsRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultNavigation.kt index 7658b59418..b44b48ba48 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val EXPORT_VAULT_ROUTE = "export_vault" +/** + * The type-safe route for the pending requests screen. + */ +@Serializable +data object ExportVaultRoute /** * Add the Export Vault screen to the nav graph. @@ -16,9 +21,7 @@ private const val EXPORT_VAULT_ROUTE = "export_vault" fun NavGraphBuilder.exportVaultDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = EXPORT_VAULT_ROUTE, - ) { + composableWithSlideTransitions { ExportVaultScreen( onNavigateBack = onNavigateBack, ) @@ -29,5 +32,5 @@ fun NavGraphBuilder.exportVaultDestination( * Navigate to the Export Vault screen. */ fun NavController.navigateToExportVault(navOptions: NavOptions? = null) { - this.navigate(EXPORT_VAULT_ROUTE, navOptions) + this.navigate(route = ExportVaultRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderNavigation.kt index c4faf58dca..445f95a8c3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderNavigation.kt @@ -7,9 +7,25 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val PRE_AUTH_FLIGHT_RECORDER_ROUTE = "pre_auth_flight_recorder_config" -private const val FLIGHT_RECORDER_ROUTE = "flight_recorder_config" +/** + * The type-safe route for the flight recorder screen. + */ +@Serializable +sealed class FlightRecorderRoute { + /** + * The type-safe route for the flight recorder screen. + */ + @Serializable + data object Standard : FlightRecorderRoute() + + /** + * The type-safe route for the pre-auth flight recorder screen. + */ + @Serializable + data object PreAuth : FlightRecorderRoute() +} /** * Add flight recorder destination to the nav graph. @@ -18,12 +34,18 @@ fun NavGraphBuilder.flightRecorderDestination( isPreAuth: Boolean, onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = getRoute(isPreAuth = isPreAuth), - ) { - FlightRecorderScreen( - onNavigateBack = onNavigateBack, - ) + if (isPreAuth) { + composableWithSlideTransitions { + FlightRecorderScreen( + onNavigateBack = onNavigateBack, + ) + } + } else { + composableWithSlideTransitions { + FlightRecorderScreen( + onNavigateBack = onNavigateBack, + ) + } } } @@ -34,9 +56,8 @@ fun NavController.navigateToFlightRecorder( isPreAuth: Boolean, navOptions: NavOptions? = null, ) { - navigate(route = getRoute(isPreAuth = isPreAuth), navOptions = navOptions) + navigate( + route = if (isPreAuth) FlightRecorderRoute.PreAuth else FlightRecorderRoute.Standard, + navOptions = navOptions, + ) } - -private fun getRoute( - isPreAuth: Boolean, -): String = if (isPreAuth) PRE_AUTH_FLIGHT_RECORDER_ROUTE else FLIGHT_RECORDER_ROUTE diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/RecordedLogsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/RecordedLogsNavigation.kt index 68bb622649..75ea754c8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/RecordedLogsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/recordedLogs/RecordedLogsNavigation.kt @@ -7,10 +7,25 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val PRE_AUTH_FLIGHT_RECORDER_RECORDED_LOGS_ROUTE = - "pre_auth_flight_recorder_recorded_logs" -private const val FLIGHT_RECORDER_RECORDED_LOGS_ROUTE = "flight_recorder_recorded_logs" +/** + * The type-safe route for the recorded logs screen. + */ +@Serializable +sealed class RecordedLogsRoute { + /** + * The type-safe route for the recorded logs screen. + */ + @Serializable + data object Standard : RecordedLogsRoute() + + /** + * The type-safe route for the pre-auth recorded logs screen. + */ + @Serializable + data object PreAuth : RecordedLogsRoute() +} /** * Add recorded logs destination to the nav graph. @@ -19,12 +34,18 @@ fun NavGraphBuilder.recordedLogsDestination( isPreAuth: Boolean, onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = getRoute(isPreAuth = isPreAuth), - ) { - RecordedLogsScreen( - onNavigateBack = onNavigateBack, - ) + if (isPreAuth) { + composableWithSlideTransitions { + RecordedLogsScreen( + onNavigateBack = onNavigateBack, + ) + } + } else { + composableWithSlideTransitions { + RecordedLogsScreen( + onNavigateBack = onNavigateBack, + ) + } } } @@ -35,14 +56,8 @@ fun NavController.navigateToRecordedLogs( isPreAuth: Boolean, navOptions: NavOptions? = null, ) { - navigate(route = getRoute(isPreAuth = isPreAuth), navOptions = navOptions) + navigate( + route = if (isPreAuth) RecordedLogsRoute.PreAuth else RecordedLogsRoute.Standard, + navOptions = navOptions, + ) } - -private fun getRoute( - isPreAuth: Boolean, -): String = - if (isPreAuth) { - PRE_AUTH_FLIGHT_RECORDER_RECORDED_LOGS_ROUTE - } else { - FLIGHT_RECORDER_RECORDED_LOGS_ROUTE - } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt index 39567362f0..fe2e592829 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/FoldersNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val FOLDERS_ROUTE = "settings_folders" +/** + * The type-safe route for the folders screen. + */ +@Serializable +data object FoldersRoute /** * Add folders destinations to the nav graph. @@ -18,9 +23,7 @@ fun NavGraphBuilder.foldersDestination( onNavigateToAddFolderScreen: () -> Unit, onNavigateToEditFolderScreen: (folderId: String) -> Unit, ) { - composableWithSlideTransitions( - route = FOLDERS_ROUTE, - ) { + composableWithSlideTransitions { FoldersScreen( onNavigateBack = onNavigateBack, onNavigateToAddFolderScreen = onNavigateToAddFolderScreen, @@ -33,5 +36,5 @@ fun NavGraphBuilder.foldersDestination( * Navigate to the folders screen. */ fun NavController.navigateToFolders(navOptions: NavOptions? = null) { - navigate(FOLDERS_ROUTE, navOptions) + this.navigate(route = FoldersRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt index 92fc8bde76..31adfc0450 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt @@ -6,23 +6,37 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType +import kotlinx.serialization.Serializable private const val ADD_TYPE: String = "add" private const val EDIT_TYPE: String = "edit" private const val EDIT_ITEM_ID: String = "folder_edit_id" private const val PARENT_FOLDER_NAME: String = "parent_folder_name" -private const val ADD_EDIT_ITEM_PREFIX: String = "folder_add_edit_item" private const val ADD_EDIT_ITEM_TYPE: String = "folder_add_edit_type" -private const val ADD_EDIT_ITEM_ROUTE: String = - "$ADD_EDIT_ITEM_PREFIX/{$ADD_EDIT_ITEM_TYPE}" + - "?$EDIT_ITEM_ID={$EDIT_ITEM_ID}&$PARENT_FOLDER_NAME={$PARENT_FOLDER_NAME}" +/** + * The type-safe route for the login approval screen. + */ +@Serializable +data class FolderAddEditRoute( + val actionType: FolderActionType, + val folderId: String?, + val parentFolderName: String?, +) + +/** + * Represents the action being done with a folder. + */ +@Serializable +enum class FolderActionType { + ADD, + EDIT, +} /** * Class to retrieve folder add & edit arguments from the [SavedStateHandle]. @@ -30,14 +44,19 @@ private const val ADD_EDIT_ITEM_ROUTE: String = data class FolderAddEditArgs( val folderAddEditType: FolderAddEditType, val parentFolderName: String?, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - folderAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { - ADD_TYPE -> FolderAddEditType.AddItem - EDIT_TYPE -> FolderAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) - else -> throw IllegalStateException("Unknown FolderAddEditType.") +) + +/** + * Constructs a [FolderAddEditArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toFolderAddEditArgs(): FolderAddEditArgs { + val route = this.toRoute() + return FolderAddEditArgs( + folderAddEditType = when (route.actionType) { + FolderActionType.ADD -> FolderAddEditType.AddItem + FolderActionType.EDIT -> FolderAddEditType.EditItem(requireNotNull(route.folderId)) }, - parentFolderName = savedStateHandle[PARENT_FOLDER_NAME], + parentFolderName = route.parentFolderName, ) } @@ -47,20 +66,7 @@ data class FolderAddEditArgs( fun NavGraphBuilder.folderAddEditDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = ADD_EDIT_ITEM_ROUTE, - arguments = listOf( - navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType }, - navArgument(EDIT_ITEM_ID) { - nullable = true - type = NavType.StringType - }, - navArgument(PARENT_FOLDER_NAME) { - nullable = true - type = NavType.StringType - }, - ), - ) { + composableWithSlideTransitions { FolderAddEditScreen(onNavigateBack = onNavigateBack) } } @@ -73,18 +79,20 @@ fun NavController.navigateToFolderAddEdit( parentFolderName: String? = null, navOptions: NavOptions? = null, ) { - navigate( - route = "$ADD_EDIT_ITEM_PREFIX/${folderAddEditType.toTypeString()}" + - "?$EDIT_ITEM_ID=${folderAddEditType.toIdOrNull()}" + - "&$PARENT_FOLDER_NAME=$parentFolderName", + this.navigate( + route = FolderAddEditRoute( + actionType = folderAddEditType.toFolderActionType(), + folderId = folderAddEditType.toIdOrNull(), + parentFolderName = parentFolderName, + ), navOptions = navOptions, ) } -private fun FolderAddEditType.toTypeString(): String = +private fun FolderAddEditType.toFolderActionType(): FolderActionType = when (this) { - is FolderAddEditType.AddItem -> ADD_TYPE - is FolderAddEditType.EditItem -> EDIT_TYPE + is FolderAddEditType.AddItem -> FolderActionType.ADD + is FolderAddEditType.EditItem -> FolderActionType.EDIT } private fun FolderAddEditType.toIdOrNull(): String? = diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt index 7e4636ad08..09f327487b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt @@ -39,7 +39,7 @@ class FolderAddEditViewModel @Inject constructor( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { - val folderAddEditArgs = FolderAddEditArgs(savedStateHandle) + val folderAddEditArgs = savedStateHandle.toFolderAddEditArgs() FolderAddEditState( folderAddEditType = folderAddEditArgs.folderAddEditType, viewState = when (folderAddEditArgs.folderAddEditType) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherNavigation.kt index 1813d92202..e3fa116fa4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherNavigation.kt @@ -6,22 +6,52 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions +import com.x8bit.bitwarden.ui.platform.util.toObjectRoute +import kotlinx.serialization.Serializable -private const val IS_PRE_AUTH: String = "isPreAuth" -private const val PRE_AUTH_OTHER_ROUTE = "pre_auth_settings_other" -private const val OTHER_ROUTE = "settings_other" +/** + * The type-safe route for the settings other screen. + */ +@Serializable +sealed class SettingsOtherRoute { + /** + * Indicates that the settings other screen should be shown as a pre-authentication. + */ + abstract val isPreAuth: Boolean + + /** + * The type-safe route for the settings other screen. + */ + @Serializable + data object Standard : SettingsOtherRoute() { + override val isPreAuth: Boolean get() = false + } + + /** + * The type-safe route for the pre-auth settings other screen. + */ + @Serializable + data object PreAuth : SettingsOtherRoute() { + override val isPreAuth: Boolean get() = true + } +} /** * Class to retrieve other settings arguments from the [SavedStateHandle]. */ -data class OtherArgs(val isPreAuth: Boolean) { - constructor(savedStateHandle: SavedStateHandle) : this( - isPreAuth = requireNotNull(savedStateHandle[IS_PRE_AUTH]), - ) +data class OtherArgs(val isPreAuth: Boolean) + +/** + * Constructs a [OtherArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toOtherArgs(): OtherArgs { + val route = this.toObjectRoute() + ?: this.toObjectRoute() + return route + ?.let { OtherArgs(isPreAuth = it.isPreAuth) } + ?: throw IllegalStateException("Missing correct route for SettingsOtherScreen") } /** @@ -31,16 +61,14 @@ fun NavGraphBuilder.otherDestination( isPreAuth: Boolean, onNavigateBack: () -> Unit, ) { - composableWithPushTransitions( - route = getRoute(isPreAuth = isPreAuth), - arguments = listOf( - navArgument(name = IS_PRE_AUTH) { - type = NavType.BoolType - defaultValue = isPreAuth - }, - ), - ) { - OtherScreen(onNavigateBack = onNavigateBack) + if (isPreAuth) { + composableWithPushTransitions { + OtherScreen(onNavigateBack = onNavigateBack) + } + } else { + composableWithPushTransitions { + OtherScreen(onNavigateBack = onNavigateBack) + } } } @@ -51,9 +79,8 @@ fun NavController.navigateToOther( isPreAuth: Boolean, navOptions: NavOptions? = null, ) { - navigate(route = getRoute(isPreAuth = isPreAuth), navOptions = navOptions) + this.navigate( + route = if (isPreAuth) SettingsOtherRoute.PreAuth else SettingsOtherRoute.Standard, + navOptions = navOptions, + ) } - -private fun getRoute( - isPreAuth: Boolean, -): String = if (isPreAuth) PRE_AUTH_OTHER_ROUTE else OTHER_ROUTE diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt index fc034d8d9f..75d2e2663e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt @@ -40,7 +40,7 @@ class OtherViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: OtherState( - isPreAuth = OtherArgs(savedStateHandle = savedStateHandle).isPreAuth, + isPreAuth = savedStateHandle.toOtherArgs().isPreAuth, allowScreenCapture = settingsRepo.isScreenCaptureAllowed, allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value, clearClipboardFrequency = settingsRepo.clearClipboardFrequency, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt index 62af9a34f1..6239f63442 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt @@ -8,8 +8,13 @@ import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import kotlinx.serialization.Serializable -private const val VAULT_SETTINGS_ROUTE = "vault_settings" +/** + * The type-safe route for the vault settings screen. + */ +@Serializable +data object VaultSettingsRoute /** * Add Vault Settings destinations to the nav graph. @@ -20,9 +25,7 @@ fun NavGraphBuilder.vaultSettingsDestination( onNavigateToFolders: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit, ) { - composableWithPushTransitions( - route = VAULT_SETTINGS_ROUTE, - ) { + composableWithPushTransitions { VaultSettingsScreen( onNavigateBack = onNavigateBack, onNavigateToExportVault = onNavigateToExportVault, @@ -36,5 +39,5 @@ fun NavGraphBuilder.vaultSettingsDestination( * Navigate to the Vault Settings screen. */ fun NavController.navigateToVaultSettings(navOptions: NavOptions? = null) { - navigate(VAULT_SETTINGS_ROUTE, navOptions) + this.navigate(route = VaultSettingsRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/splash/SplashNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/splash/SplashNavigation.kt index a6d8cdc745..11190e6c7a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/splash/SplashNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/splash/SplashNavigation.kt @@ -7,14 +7,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.bitwarden.core.annotation.OmitFromCoverage +import kotlinx.serialization.Serializable -const val SPLASH_ROUTE: String = "splash" +/** + * The type-safe route for the splash screen. + */ +@Serializable +data object SplashRoute /** * Add splash destinations to the nav graph. */ fun NavGraphBuilder.splashDestination() { - composable(SPLASH_ROUTE) { SplashScreen() } + composable { SplashScreen() } } /** @@ -23,5 +28,5 @@ fun NavGraphBuilder.splashDestination() { fun NavController.navigateToSplash( navOptions: NavOptions? = null, ) { - navigate(SPLASH_ROUTE, navOptions) + navigate(SplashRoute, navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 4d50fbed63..96584db966 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -32,7 +32,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit.navigate import com.x8bit.bitwarden.ui.platform.feature.settings.folders.foldersDestination import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType import com.x8bit.bitwarden.ui.platform.feature.settings.folders.navigateToFolders -import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.VaultUnlockedNavbarRoute import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination import com.x8bit.bitwarden.ui.tools.feature.generator.generatorModalDestination import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode @@ -57,14 +57,19 @@ import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.navigateToVaultMo import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.vaultMoveToOrganizationDestination import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.navigateToQrCodeScanScreen import com.x8bit.bitwarden.ui.vault.feature.qrcodescan.vaultQrCodeScanDestination +import kotlinx.serialization.Serializable -const val VAULT_UNLOCKED_GRAPH_ROUTE: String = "vault_unlocked_graph" +/** + * The type-safe route for the vault unlocked graph. + */ +@Serializable +data object VaultUnlockedGraphRoute /** * Navigate to the vault unlocked screen. */ fun NavController.navigateToVaultUnlockedGraph(navOptions: NavOptions? = null) { - navigate(VAULT_UNLOCKED_GRAPH_ROUTE, navOptions) + navigate(route = VaultUnlockedGraphRoute, navOptions = navOptions) } /** @@ -74,9 +79,8 @@ fun NavController.navigateToVaultUnlockedGraph(navOptions: NavOptions? = null) { fun NavGraphBuilder.vaultUnlockedGraph( navController: NavController, ) { - navigation( - startDestination = VAULT_UNLOCKED_NAV_BAR_ROUTE, - route = VAULT_UNLOCKED_GRAPH_ROUTE, + navigation( + startDestination = VaultUnlockedNavbarRoute, ) { vaultItemListingDestinationAsRoot( onNavigateBack = { navController.popBackStack() }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 1095c5e045..008f766c17 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -11,17 +11,19 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs +import kotlinx.serialization.Serializable /** - * The functions below pertain to entry into the [VaultUnlockedNavBarScreen]. + * The type-safe route for the vault unlocked navbar screen. */ -const val VAULT_UNLOCKED_NAV_BAR_ROUTE: String = "VaultUnlockedNavBar" +@Serializable +data object VaultUnlockedNavbarRoute /** * Navigate to the [VaultUnlockedNavBarScreen]. */ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) { - navigate(VAULT_UNLOCKED_NAV_BAR_ROUTE, navOptions) + navigate(route = VaultUnlockedNavbarRoute, navOptions = navOptions) } /** @@ -48,9 +50,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToImportLogins: (SnackbarRelay) -> Unit, onNavigateToAddFolderScreen: (selectedFolderName: String?) -> Unit, ) { - composableWithStayTransitions( - route = VAULT_UNLOCKED_NAV_BAR_ROUTE, - ) { + composableWithStayTransitions { VaultUnlockedNavBarScreen( onNavigateToVaultAddItem = onNavigateToVaultAddItem, onNavigateToVaultItem = onNavigateToVaultItem, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 2ab0960618..f21b6ca3e4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -42,7 +42,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.navigateToSendGraph import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs -import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_GRAPH_ROUTE +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 @@ -220,7 +220,7 @@ private fun VaultUnlockedNavBarScaffold( // - consume the IME insets. NavHost( navController = navController, - startDestination = VAULT_GRAPH_ROUTE, + startDestination = VaultGraphRoute, enterTransition = RootTransitionProviders.Enter.fadeIn, exitTransition = RootTransitionProviders.Exit.fadeOut, popEnterTransition = RootTransitionProviders.Enter.fadeIn, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt index 28f8d0e6bf..f5cccf1095 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/model/VaultUnlockedNavBarTab.kt @@ -3,14 +3,15 @@ package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.model import android.os.Parcelable import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.components.model.NavigationItem -import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_GRAPH_ROUTE -import com.x8bit.bitwarden.ui.platform.feature.settings.SETTINGS_ROUTE -import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_GRAPH_ROUTE -import com.x8bit.bitwarden.ui.tools.feature.generator.GENERATOR_ROUTE -import com.x8bit.bitwarden.ui.tools.feature.send.SEND_GRAPH_ROUTE -import com.x8bit.bitwarden.ui.tools.feature.send.SEND_ROUTE -import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_GRAPH_ROUTE -import com.x8bit.bitwarden.ui.vault.feature.vault.VAULT_ROUTE +import com.x8bit.bitwarden.ui.platform.feature.settings.SettingsGraphRoute +import com.x8bit.bitwarden.ui.platform.feature.settings.SettingsRoute +import com.x8bit.bitwarden.ui.platform.util.toObjectNavigationRoute +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorGraphRoute +import com.x8bit.bitwarden.ui.tools.feature.generator.GeneratorRoute +import com.x8bit.bitwarden.ui.tools.feature.send.SendGraphRoute +import com.x8bit.bitwarden.ui.tools.feature.send.SendRoute +import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute +import com.x8bit.bitwarden.ui.vault.feature.vault.VaultRoute import kotlinx.parcelize.Parcelize /** @@ -33,8 +34,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_generator override val labelRes get() = R.string.generator override val contentDescriptionRes get() = R.string.generator - override val graphRoute: String get() = GENERATOR_GRAPH_ROUTE - override val startDestinationRoute get() = GENERATOR_ROUTE + override val graphRoute get() = GeneratorGraphRoute.toObjectNavigationRoute() + override val startDestinationRoute get() = GeneratorRoute.Standard.toObjectNavigationRoute() override val testTag get() = "GeneratorTab" override val notificationCount get() = 0 } @@ -48,8 +49,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_send override val labelRes get() = R.string.send override val contentDescriptionRes get() = R.string.send - override val graphRoute: String get() = SEND_GRAPH_ROUTE - override val startDestinationRoute get() = SEND_ROUTE + override val graphRoute get() = SendGraphRoute.toObjectNavigationRoute() + override val startDestinationRoute get() = SendRoute.toObjectNavigationRoute() override val testTag get() = "SendTab" override val notificationCount get() = 0 } @@ -64,8 +65,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { ) : VaultUnlockedNavBarTab() { override val iconResSelected get() = R.drawable.ic_vault_filled override val iconRes get() = R.drawable.ic_vault - override val graphRoute: String get() = VAULT_GRAPH_ROUTE - override val startDestinationRoute get() = VAULT_ROUTE + override val graphRoute get() = VaultGraphRoute.toObjectNavigationRoute() + override val startDestinationRoute get() = VaultRoute.toObjectNavigationRoute() override val testTag get() = "VaultTab" override val notificationCount get() = 0 } @@ -81,8 +82,8 @@ sealed class VaultUnlockedNavBarTab : NavigationItem, Parcelable { override val iconRes get() = R.drawable.ic_settings override val labelRes get() = R.string.settings override val contentDescriptionRes get() = R.string.settings - override val graphRoute: String get() = SETTINGS_GRAPH_ROUTE - override val startDestinationRoute get() = SETTINGS_ROUTE + override val graphRoute get() = SettingsGraphRoute.toObjectNavigationRoute() + override val startDestinationRoute get() = SettingsRoute.Standard.toObjectNavigationRoute() override val testTag get() = "SettingsTab" } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt index a0f788488d..0b9d5862eb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt @@ -1,11 +1,13 @@ package com.x8bit.bitwarden.ui.platform.manager.snackbar import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData +import kotlinx.serialization.Serializable /** * Models a relay key to be mapped to an instance of [BitwardenSnackbarData] being sent * between producers and consumers of the data. */ +@Serializable enum class SnackbarRelay { VAULT_SETTINGS_RELAY, MY_VAULT_RELAY, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/RouteUtil.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/RouteUtil.kt new file mode 100644 index 0000000000..d440c9d2a3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/RouteUtil.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.ui.platform.util + +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializer +import kotlin.reflect.KClass + +/** + * Gets the route string for an object. + */ +@OptIn(InternalSerializationApi::class) +fun T.toObjectNavigationRoute(): String = this::class.toObjectKClassNavigationRoute() + +/** + * Gets the route string for a [KClass] of an object. + */ +@OptIn(InternalSerializationApi::class) +fun KClass.toObjectKClassNavigationRoute(): String = + this.serializer().descriptor.serialName diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/SavedStateHandleExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/SavedStateHandleExtensions.kt new file mode 100644 index 0000000000..7364c0b438 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/util/SavedStateHandleExtensions.kt @@ -0,0 +1,24 @@ +package com.x8bit.bitwarden.ui.platform.util + +import android.content.Intent +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import androidx.navigation.toRoute + +/** + * Determines if the [SavedStateHandle] contains a route for the specified object class. + * + * This will return the object instance if the route is correct, `null` otherwise. + */ +inline fun SavedStateHandle.toObjectRoute(): T? = + this + .get(key = NavController.KEY_DEEP_LINK_INTENT) + ?.data + ?.pathSegments + .orEmpty() + .takeIf { segments -> segments.any { it == T::class.toObjectKClassNavigationRoute() } } + ?.let { _ -> + // This will get the instance for us. We only do this after the checks above as it + // will always return the object instance even if it is not the correct one. + this.toRoute() + } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt index 02e55f73d0..300c72146b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorGraphNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation import com.bitwarden.core.annotation.OmitFromCoverage +import kotlinx.serialization.Serializable -const val GENERATOR_GRAPH_ROUTE: String = "generator_graph" +/** + * The type-safe route for the generator graph. + */ +@Serializable +data object GeneratorGraphRoute /** * Add generator destination to the root nav graph. @@ -17,9 +22,8 @@ fun NavGraphBuilder.generatorGraph( onNavigateToPasswordHistory: () -> Unit, onDimNavBarRequest: (Boolean) -> Unit, ) { - navigation( - route = GENERATOR_GRAPH_ROUTE, - startDestination = GENERATOR_ROUTE, + navigation( + startDestination = GeneratorRoute.Standard, ) { generatorDestination( onNavigateToPasswordHistory = onNavigateToPasswordHistory, @@ -32,5 +36,5 @@ fun NavGraphBuilder.generatorGraph( * Navigate to the generator graph. */ fun NavController.navigateToGeneratorGraph(navOptions: NavOptions? = null) { - navigate(GENERATOR_GRAPH_ROUTE, navOptions) + this.navigate(route = GeneratorGraphRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt index 005cd13e7b..66dd5fde53 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorNavigation.kt @@ -6,44 +6,71 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode +import kotlinx.serialization.Serializable /** - * The functions below pertain to entry into the [GeneratorScreen]. + * The type-safe route for the generator screen. */ -private const val GENERATOR_MODAL_ROUTE_PREFIX: String = "generator_modal" -private const val GENERATOR_MODE_TYPE: String = "generator_mode_type" -private const val GENERATOR_WEBSITE: String = "generator_website" -private const val USERNAME_GENERATOR: String = "username_generator" -private const val PASSWORD_GENERATOR: String = "password_generator" +@Serializable +sealed class GeneratorRoute { + /** + * The type-safe route for the standard generator screen. + */ + @Serializable + data object Standard : GeneratorRoute() -const val GENERATOR_ROUTE: String = "generator" -private const val GENERATOR_MODAL_ROUTE: String = - "$GENERATOR_MODAL_ROUTE_PREFIX/{$GENERATOR_MODE_TYPE}?$GENERATOR_WEBSITE={$GENERATOR_WEBSITE}" + /** + * The type-safe route for the modal generator screen. + */ + @Serializable + data class Modal( + val type: ModalType, + val website: String?, + ) : GeneratorRoute() +} + +/** + * Indicates the type of modal to be displayed. + */ +@Serializable +enum class ModalType { + PASSWORD, + USERNAME, +} /** * Class to retrieve vault item listing arguments from the [SavedStateHandle]. */ data class GeneratorArgs( val type: GeneratorMode, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - type = when (savedStateHandle.get(GENERATOR_MODE_TYPE)) { - USERNAME_GENERATOR -> GeneratorMode.Modal.Username( - website = savedStateHandle[GENERATOR_WEBSITE], - ) +) - PASSWORD_GENERATOR -> GeneratorMode.Modal.Password - else -> GeneratorMode.Default +/** + * Constructs a [GeneratorArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toGeneratorArgs(): GeneratorArgs { + return GeneratorArgs( + type = try { + this.toModalGeneratorMode() + } catch (_: Exception) { + GeneratorMode.Default }, ) } +private fun SavedStateHandle.toModalGeneratorMode(): GeneratorMode.Modal { + val route = this.toRoute() + return when (route.type) { + ModalType.PASSWORD -> GeneratorMode.Modal.Password + ModalType.USERNAME -> GeneratorMode.Modal.Username(website = route.website) + } +} + /** * Add generator destination to the root nav graph. */ @@ -51,7 +78,7 @@ fun NavGraphBuilder.generatorDestination( onNavigateToPasswordHistory: () -> Unit, onDimNavBarRequest: (Boolean) -> Unit, ) { - composable(GENERATOR_ROUTE) { + composable { GeneratorScreen( onNavigateToPasswordHistory = onNavigateToPasswordHistory, onNavigateBack = {}, @@ -66,16 +93,7 @@ fun NavGraphBuilder.generatorDestination( fun NavGraphBuilder.generatorModalDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = GENERATOR_MODAL_ROUTE, - arguments = listOf( - navArgument(GENERATOR_MODE_TYPE) { type = NavType.StringType }, - navArgument(GENERATOR_WEBSITE) { - type = NavType.StringType - nullable = true - }, - ), - ) { + composableWithSlideTransitions { GeneratorScreen( onNavigateToPasswordHistory = {}, onNavigateBack = onNavigateBack, @@ -91,13 +109,14 @@ fun NavController.navigateToGeneratorModal( mode: GeneratorMode.Modal, navOptions: NavOptions? = null, ) { - val generatorModeType = when (mode) { - GeneratorMode.Modal.Password -> PASSWORD_GENERATOR - is GeneratorMode.Modal.Username -> USERNAME_GENERATOR - } - val website = (mode as? GeneratorMode.Modal.Username)?.website navigate( - route = "$GENERATOR_MODAL_ROUTE_PREFIX/$generatorModeType?$GENERATOR_WEBSITE=$website", + route = GeneratorRoute.Modal( + type = when (mode) { + GeneratorMode.Modal.Password -> ModalType.PASSWORD + is GeneratorMode.Modal.Username -> ModalType.USERNAME + }, + website = (mode as? GeneratorMode.Modal.Username)?.website, + ), navOptions = navOptions, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt index 2651122655..b6a4c5d932 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModel.kt @@ -86,7 +86,7 @@ class GeneratorViewModel @Inject constructor( private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { - val generatorMode = GeneratorArgs(savedStateHandle).type + val generatorMode = savedStateHandle.toGeneratorArgs().type GeneratorState( generatedText = NO_GENERATED_TEXT, selectedType = when (generatorMode) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt index db3115363e..80c8a5b8d1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryNavigation.kt @@ -6,38 +6,48 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorPasswordHistoryMode +import kotlinx.serialization.Serializable -private const val DEFAULT_MODE: String = "default" -private const val ITEM_MODE: String = "item" +/** + * The type-safe route for the password history screen. + */ +@Serializable +data class PasswordHistoryRoute( + val passwordHistoryType: PasswordHistoryType, + val itemId: String?, +) -private const val PASSWORD_HISTORY_PREFIX: String = "password_history" -private const val PASSWORD_HISTORY_MODE: String = "password_history_mode" -private const val PASSWORD_HISTORY_ITEM_ID: String = "password_history_id" - -private const val PASSWORD_HISTORY_ROUTE: String = - PASSWORD_HISTORY_PREFIX + - "/{$PASSWORD_HISTORY_MODE}" + - "?$PASSWORD_HISTORY_ITEM_ID={$PASSWORD_HISTORY_ITEM_ID}" +/** + * Indicates the type of password to be displayed. + */ +@Serializable +enum class PasswordHistoryType { + DEFAULT, + ITEM, +} /** * Class to retrieve password history arguments from the [SavedStateHandle]. */ data class PasswordHistoryArgs( val passwordHistoryMode: GeneratorPasswordHistoryMode, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - passwordHistoryMode = when (requireNotNull(savedStateHandle[PASSWORD_HISTORY_MODE])) { - DEFAULT_MODE -> GeneratorPasswordHistoryMode.Default - ITEM_MODE -> GeneratorPasswordHistoryMode.Item( - requireNotNull(savedStateHandle[PASSWORD_HISTORY_ITEM_ID]), - ) +) - else -> throw IllegalStateException("Unknown VaultAddEditType.") +/** + * Constructs a [PasswordHistoryArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toPasswordHistoryArgs(): PasswordHistoryArgs { + val route = this.toRoute() + return PasswordHistoryArgs( + passwordHistoryMode = when (route.passwordHistoryType) { + PasswordHistoryType.DEFAULT -> GeneratorPasswordHistoryMode.Default + PasswordHistoryType.ITEM -> GeneratorPasswordHistoryMode.Item( + itemId = requireNotNull(route.itemId), + ) }, ) } @@ -48,12 +58,7 @@ data class PasswordHistoryArgs( fun NavGraphBuilder.passwordHistoryDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = PASSWORD_HISTORY_ROUTE, - arguments = listOf( - navArgument(PASSWORD_HISTORY_MODE) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { PasswordHistoryScreen( onNavigateBack = onNavigateBack, ) @@ -68,20 +73,13 @@ fun NavController.navigateToPasswordHistory( navOptions: NavOptions? = null, ) { navigate( - route = "$PASSWORD_HISTORY_PREFIX/${passwordHistoryMode.toModeString()}" + - "?$PASSWORD_HISTORY_ITEM_ID=${passwordHistoryMode.toIdOrNull()}", + route = PasswordHistoryRoute( + passwordHistoryType = when (passwordHistoryMode) { + GeneratorPasswordHistoryMode.Default -> PasswordHistoryType.DEFAULT + is GeneratorPasswordHistoryMode.Item -> PasswordHistoryType.ITEM + }, + itemId = (passwordHistoryMode as? GeneratorPasswordHistoryMode.Item)?.itemId, + ), navOptions = navOptions, ) } - -private fun GeneratorPasswordHistoryMode.toModeString(): String = - when (this) { - is GeneratorPasswordHistoryMode.Default -> DEFAULT_MODE - is GeneratorPasswordHistoryMode.Item -> ITEM_MODE - } - -private fun GeneratorPasswordHistoryMode.toIdOrNull(): String? = - when (this) { - is GeneratorPasswordHistoryMode.Default -> null - is GeneratorPasswordHistoryMode.Item -> itemId - } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt index e4e7a9c781..b80e7d9df9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryViewModel.kt @@ -44,7 +44,7 @@ class PasswordHistoryViewModel @Inject constructor( initialState = savedStateHandle[KEY_STATE] ?: run { PasswordHistoryState( - passwordHistoryMode = PasswordHistoryArgs(savedStateHandle).passwordHistoryMode, + passwordHistoryMode = savedStateHandle.toPasswordHistoryArgs().passwordHistoryMode, viewState = PasswordHistoryState.ViewState.Loading, ) }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt index 050e4f330e..ffe306bbdc 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendGraphNavigation.kt @@ -11,8 +11,13 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToSendItemListing import com.x8bit.bitwarden.ui.vault.feature.itemlisting.sendItemListingDestination import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType +import kotlinx.serialization.Serializable -const val SEND_GRAPH_ROUTE: String = "send_graph" +/** + * The type-safe route for the send graph. + */ +@Serializable +data object SendGraphRoute /** * Add send destination to the nav graph. @@ -23,9 +28,8 @@ fun NavGraphBuilder.sendGraph( onNavigateToEditSend: (sendItemId: String) -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { - navigation( - startDestination = SEND_ROUTE, - route = SEND_GRAPH_ROUTE, + navigation( + startDestination = SendRoute, ) { sendDestination( onNavigateToAddSend = onNavigateToAddSend, @@ -52,5 +56,5 @@ fun NavGraphBuilder.sendGraph( * via [sendGraph]. */ fun NavController.navigateToSendGraph(navOptions: NavOptions? = null) { - navigate(SEND_GRAPH_ROUTE, navOptions) + navigate(route = SendGraphRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt index ac5b495e6e..3bac87f569 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/SendNavigation.kt @@ -8,8 +8,13 @@ import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType +import kotlinx.serialization.Serializable -const val SEND_ROUTE: String = "send" +/** + * The type-safe route for the send screen. + */ +@Serializable +data object SendRoute /** * Add send destination to the nav graph. @@ -21,9 +26,7 @@ fun NavGraphBuilder.sendDestination( onNavigateToSendTextList: () -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { - composableWithRootPushTransitions( - route = SEND_ROUTE, - ) { + composableWithRootPushTransitions { SendScreen( onNavigateToAddSend = onNavigateToAddSend, onNavigateToEditSend = onNavigateToEditSend, @@ -39,5 +42,5 @@ fun NavGraphBuilder.sendDestination( * via [sendDestination]. */ fun NavController.navigateToSend(navOptions: NavOptions? = null) { - navigate(SEND_ROUTE, navOptions) + navigate(route = SendRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt index 9d8bd6bead..b11ad98cd3 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendNavigation.kt @@ -6,51 +6,65 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType +import kotlinx.serialization.Serializable -private const val ADD_TYPE: String = "add" -private const val EDIT_TYPE: String = "edit" -private const val EDIT_ITEM_ID: String = "edit_send_id" +/** + * The type-safe route for the add send screen. + */ +@Serializable +data class AddSendRoute( + val type: ModeType, + val editSendId: String?, +) -private const val ADD_SEND_ITEM_PREFIX: String = "add_send_item" -private const val ADD_SEND_ITEM_TYPE: String = "add_send_item_type" - -const val ADD_SEND_ROUTE: String = - "$ADD_SEND_ITEM_PREFIX/{$ADD_SEND_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" +/** + * Indicates the mode of send to be displayed. + */ +@Serializable +enum class ModeType { + ADD, + EDIT, +} /** * Class to retrieve send add & edit arguments from the [SavedStateHandle]. */ data class AddSendArgs( val sendAddType: AddSendType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - sendAddType = when (requireNotNull(savedStateHandle.get(ADD_SEND_ITEM_TYPE))) { - ADD_TYPE -> AddSendType.AddItem - EDIT_TYPE -> AddSendType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID])) - else -> throw IllegalStateException("Unknown VaultAddEditType.") +) + +/** + * Constructs a [AddSendArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toAddSendArgs(): AddSendArgs { + val route = this.toRoute() + return AddSendArgs( + sendAddType = when (route.type) { + ModeType.ADD -> AddSendType.AddItem + ModeType.EDIT -> AddSendType.EditItem(sendItemId = requireNotNull(route.editSendId)) }, ) } +private fun SavedStateHandle.toAddSendType(): AddSendType { + val route = this.toRoute() + return when (route.type) { + ModeType.ADD -> AddSendType.AddItem + ModeType.EDIT -> AddSendType.EditItem(sendItemId = requireNotNull(route.editSendId)) + } +} + /** * Add the new send screen to the nav graph. */ fun NavGraphBuilder.addSendDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = ADD_SEND_ROUTE, - arguments = listOf( - navArgument(ADD_SEND_ITEM_TYPE) { - type = NavType.StringType - }, - ), - ) { + composableWithSlideTransitions { AddSendScreen(onNavigateBack = onNavigateBack) } } @@ -62,18 +76,14 @@ fun NavController.navigateToAddSend( sendAddType: AddSendType, navOptions: NavOptions? = null, ) { - navigate( - route = "$ADD_SEND_ITEM_PREFIX/${sendAddType.toTypeString()}" + - "?${EDIT_ITEM_ID}=${sendAddType.toIdOrNull()}", + this.navigate( + route = AddSendRoute( + type = when (sendAddType) { + AddSendType.AddItem -> ModeType.ADD + is AddSendType.EditItem -> ModeType.EDIT + }, + editSendId = (sendAddType as? AddSendType.EditItem)?.sendItemId, + ), navOptions = navOptions, ) } - -private fun AddSendType.toTypeString(): String = - when (this) { - is AddSendType.AddItem -> ADD_TYPE - is AddSendType.EditItem -> EDIT_TYPE - } - -private fun AddSendType.toIdOrNull(): String? = - (this as? AddSendType.EditItem)?.sendItemId diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt index 94789ecd74..912463507e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModel.kt @@ -78,7 +78,7 @@ class AddSendViewModel @Inject constructor( // Check to see if we are navigating here from an external source val specialCircumstance = specialCircumstanceManager.specialCircumstance val shareSendType = specialCircumstance.toSendType() - val sendAddType = AddSendArgs(savedStateHandle).sendAddType + val sendAddType = savedStateHandle.toAddSendArgs().sendAddType AddSendState( shouldFinishOnComplete = specialCircumstance.shouldFinishOnComplete(), isShared = shareSendType != null, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt index d76db54dc0..2e82da865a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditNavigation.kt @@ -6,38 +6,35 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType +import kotlinx.serialization.Serializable -private const val ADD_TYPE: String = "add" -private const val EDIT_TYPE: String = "edit" -private const val CLONE_TYPE: String = "clone" -private const val EDIT_ITEM_ID: String = "vault_edit_id" +/** + * The type-safe route for the vault add/edit screen. + */ +@Serializable +data class VaultAddEditRoute( + val vaultAddEditMode: VaultAddEditMode, + val vaultItemId: String?, + val vaultItemCipherType: VaultItemCipherType, + val selectedFolderId: String? = null, + val selectedCollectionId: String? = null, +) -private const val LOGIN: String = "login" -private const val CARD: String = "card" -private const val IDENTITY: String = "identity" -private const val SECURE_NOTE: String = "secure_note" -private const val SSH_KEY: String = "ssh_key" -private const val CIPHER_TYPE: String = "vault_item_type" - -private const val ADD_EDIT_ITEM_PREFIX: String = "vault_add_edit_item" -private const val ADD_EDIT_ITEM_TYPE: String = "vault_add_edit_type" -private const val ADD_SELECTED_FOLDER_ID: String = "vault_add_selected_folder_id" -private const val ADD_SELECTED_COLLECTION_ID: String = "vault_add_selected_collection_id" - -private const val ADD_EDIT_ITEM_ROUTE: String = - ADD_EDIT_ITEM_PREFIX + - "/{$ADD_EDIT_ITEM_TYPE}" + - "?$EDIT_ITEM_ID={$EDIT_ITEM_ID}" + - "?$CIPHER_TYPE={$CIPHER_TYPE}" + - "?$ADD_SELECTED_FOLDER_ID={$ADD_SELECTED_FOLDER_ID}" + - "?$ADD_SELECTED_COLLECTION_ID={$ADD_SELECTED_COLLECTION_ID}" +/** + * The mode in which the vault add/edit screen should be displayed. + */ +@Serializable +enum class VaultAddEditMode { + ADD, + EDIT, + CLONE, +} /** * Class to retrieve vault add & edit arguments from the [SavedStateHandle]. @@ -47,24 +44,27 @@ data class VaultAddEditArgs( val vaultItemCipherType: VaultItemCipherType, val selectedFolderId: String? = null, val selectedCollectionId: String? = null, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - vaultAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) { - ADD_TYPE -> VaultAddEditType.AddItem - EDIT_TYPE -> VaultAddEditType.EditItem( - vaultItemId = requireNotNull(savedStateHandle[EDIT_ITEM_ID]), - ) +) - CLONE_TYPE -> VaultAddEditType.CloneItem( - vaultItemId = requireNotNull(savedStateHandle[EDIT_ITEM_ID]), - ) +/** + * Constructs a [VaultAddEditArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toVaultAddEditArgs(): VaultAddEditArgs { + val route = this.toRoute() + return VaultAddEditArgs( + vaultAddEditType = when (route.vaultAddEditMode) { + VaultAddEditMode.ADD -> VaultAddEditType.AddItem + VaultAddEditMode.EDIT -> { + VaultAddEditType.EditItem(vaultItemId = requireNotNull(route.vaultItemId)) + } - else -> throw IllegalStateException("Unknown VaultAddEditType.") + VaultAddEditMode.CLONE -> { + VaultAddEditType.CloneItem(vaultItemId = requireNotNull(route.vaultItemId)) + } }, - vaultItemCipherType = requireNotNull(savedStateHandle.get(CIPHER_TYPE)) - .toVaultItemCipherType(), - selectedFolderId = savedStateHandle[ADD_SELECTED_FOLDER_ID], - selectedCollectionId = savedStateHandle[ADD_SELECTED_COLLECTION_ID], + vaultItemCipherType = route.vaultItemCipherType, + selectedFolderId = route.selectedFolderId, + selectedCollectionId = route.selectedCollectionId, ) } @@ -80,25 +80,7 @@ fun NavGraphBuilder.vaultAddEditDestination( onNavigateToAttachments: (cipherId: String) -> Unit, onNavigateToMoveToOrganization: (cipherId: String, showOnlyCollections: Boolean) -> Unit, ) { - composableWithSlideTransitions( - route = ADD_EDIT_ITEM_ROUTE, - arguments = listOf( - navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType }, - navArgument(CIPHER_TYPE) { type = NavType.StringType }, - navArgument(ADD_SELECTED_FOLDER_ID) { - type = NavType.StringType - nullable = true - }, - navArgument(ADD_SELECTED_COLLECTION_ID) { - type = NavType.StringType - nullable = true - }, - navArgument(ADD_SELECTED_COLLECTION_ID) { - type = NavType.StringType - nullable = true - }, - ), - ) { + composableWithSlideTransitions { VaultAddEditScreen( onNavigateBack = onNavigateBack, onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen, @@ -118,46 +100,17 @@ fun NavController.navigateToVaultAddEdit( navOptions: NavOptions? = null, ) { navigate( - route = "$ADD_EDIT_ITEM_PREFIX/${args.vaultAddEditType.toTypeString()}" + - "?$EDIT_ITEM_ID=${args.vaultAddEditType.toIdOrNull()}" + - "?$CIPHER_TYPE=${args.vaultItemCipherType.toTypeString()}" + - "?$ADD_SELECTED_FOLDER_ID=${args.selectedFolderId}" + - "?$ADD_SELECTED_COLLECTION_ID=${args.selectedCollectionId}", + route = VaultAddEditRoute( + vaultAddEditMode = when (args.vaultAddEditType) { + VaultAddEditType.AddItem -> VaultAddEditMode.ADD + is VaultAddEditType.CloneItem -> VaultAddEditMode.CLONE + is VaultAddEditType.EditItem -> VaultAddEditMode.EDIT + }, + vaultItemId = args.vaultAddEditType.vaultItemId, + vaultItemCipherType = args.vaultItemCipherType, + selectedFolderId = args.selectedFolderId, + selectedCollectionId = args.selectedFolderId, + ), navOptions = navOptions, ) } - -private fun VaultAddEditType.toTypeString(): String = - when (this) { - is VaultAddEditType.AddItem -> ADD_TYPE - is VaultAddEditType.EditItem -> EDIT_TYPE - is VaultAddEditType.CloneItem -> CLONE_TYPE - } - -private fun VaultAddEditType.toIdOrNull(): String? = - when (this) { - is VaultAddEditType.AddItem -> null - is VaultAddEditType.CloneItem -> vaultItemId - is VaultAddEditType.EditItem -> vaultItemId - } - -private fun VaultItemCipherType.toTypeString(): String = - when (this) { - VaultItemCipherType.LOGIN -> LOGIN - VaultItemCipherType.CARD -> CARD - VaultItemCipherType.IDENTITY -> IDENTITY - VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE - VaultItemCipherType.SSH_KEY -> SSH_KEY - } - -private fun String.toVaultItemCipherType(): VaultItemCipherType = - when (this) { - LOGIN -> VaultItemCipherType.LOGIN - CARD -> VaultItemCipherType.CARD - IDENTITY -> VaultItemCipherType.IDENTITY - SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE - SSH_KEY -> VaultItemCipherType.SSH_KEY - else -> throw IllegalStateException( - "Edit Item string arguments for VaultAddEditNavigation must match!", - ) - } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt index f18d65eafc..a6fbfd8c79 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModel.kt @@ -126,7 +126,7 @@ class VaultAddEditViewModel @Inject constructor( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { - val args = VaultAddEditArgs(savedStateHandle = savedStateHandle) + val args = savedStateHandle.toVaultAddEditArgs() val vaultAddEditType = args.vaultAddEditType val vaultCipherType = args.vaultItemCipherType val selectedFolderId = args.selectedFolderId diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt index 5de9281f2d..ee5af4158a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsNavigation.kt @@ -6,22 +6,30 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val ATTACHMENTS_CIPHER_ID = "cipher_id" -private const val ATTACHMENTS_ROUTE_PREFIX = "attachments" -private const val ATTACHMENTS_ROUTE = "$ATTACHMENTS_ROUTE_PREFIX/{$ATTACHMENTS_CIPHER_ID}" +/** + * The type-safe route for the attachments screen. + */ +@Serializable +data class AttachmentsRoute( + val cipherId: String, +) /** * Class to retrieve arguments from the [SavedStateHandle]. */ -data class AttachmentsArgs(val cipherId: String) { - constructor(savedStateHandle: SavedStateHandle) : this( - cipherId = checkNotNull(savedStateHandle.get(ATTACHMENTS_CIPHER_ID)), - ) +data class AttachmentsArgs(val cipherId: String) + +/** + * Constructs a [AttachmentsArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toAttachmentsArgs(): AttachmentsArgs { + val route = this.toRoute() + return AttachmentsArgs(cipherId = route.cipherId) } /** @@ -30,12 +38,7 @@ data class AttachmentsArgs(val cipherId: String) { fun NavGraphBuilder.attachmentDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = ATTACHMENTS_ROUTE, - arguments = listOf( - navArgument(ATTACHMENTS_CIPHER_ID) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { AttachmentsScreen( onNavigateBack = onNavigateBack, ) @@ -50,7 +53,7 @@ fun NavController.navigateToAttachment( navOptions: NavOptions? = null, ) { navigate( - route = "$ATTACHMENTS_ROUTE_PREFIX/$cipherId", + route = AttachmentsRoute(cipherId = cipherId), navOptions = navOptions, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt index 6f92aa2afb..d6333acd44 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModel.kt @@ -50,7 +50,7 @@ class AttachmentsViewModel @Inject constructor( ?: run { val isPremiumUser = authRepo.userStateFlow.value?.activeAccount?.isPremium == true AttachmentsState( - cipherId = AttachmentsArgs(savedStateHandle).cipherId, + cipherId = savedStateHandle.toAttachmentsArgs().cipherId, viewState = AttachmentsState.ViewState.Loading, dialogState = AttachmentsState.DialogState.Error( title = null, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt index 2406dd7bb1..2003fadfd0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt @@ -6,25 +6,31 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay +import kotlinx.serialization.Serializable -private const val IMPORT_LOGINS_PREFIX = "import-logins" -private const val IMPORT_LOGINS_NAV_ARG = "snackbarRelay" -private const val IMPORT_LOGINS_ROUTE = "$IMPORT_LOGINS_PREFIX/{$IMPORT_LOGINS_NAV_ARG}" +/** + * The type-safe route for the import logins screen. + */ +@Serializable +data class ImportLoginsRoute( + val snackbarRelay: SnackbarRelay, +) /** * Arguments for the [ImportLoginsScreen] using [SavedStateHandle]. */ -data class ImportLoginsArgs(val snackBarRelay: SnackbarRelay) { - constructor(savedStateHandle: SavedStateHandle) : this( - snackBarRelay = SnackbarRelay.valueOf( - requireNotNull(savedStateHandle[IMPORT_LOGINS_NAV_ARG]), - ), - ) +data class ImportLoginsArgs(val snackBarRelay: SnackbarRelay) + +/** + * Constructs a [ImportLoginsArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toImportLoginsArgs(): ImportLoginsArgs { + val route = this.toRoute() + return ImportLoginsArgs(snackBarRelay = route.snackbarRelay) } /** @@ -34,7 +40,7 @@ fun NavController.navigateToImportLoginsScreen( snackbarRelay: SnackbarRelay, navOptions: NavOptions? = null, ) { - navigate(route = "$IMPORT_LOGINS_PREFIX/$snackbarRelay", navOptions = navOptions) + navigate(route = ImportLoginsRoute(snackbarRelay = snackbarRelay), navOptions = navOptions) } /** @@ -43,15 +49,7 @@ fun NavController.navigateToImportLoginsScreen( fun NavGraphBuilder.importLoginsScreenDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = IMPORT_LOGINS_ROUTE, - arguments = listOf( - navArgument(IMPORT_LOGINS_NAV_ARG) { - type = NavType.StringType - nullable = false - }, - ), - ) { + composableWithSlideTransitions { ImportLoginsScreen( onNavigateBack = onNavigateBack, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt index d60e8bb22f..27638eed87 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt @@ -41,7 +41,7 @@ class ImportLoginsViewModel @Inject constructor( showBottomSheet = false, // attempt to trim the scheme of the vault url currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl, - snackbarRelay = ImportLoginsArgs(savedStateHandle).snackBarRelay, + snackbarRelay = savedStateHandle.toImportLoginsArgs().snackBarRelay, ) }, ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemNavigation.kt index 88b2a4757d..d0a2b3076c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemNavigation.kt @@ -6,24 +6,21 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType +import kotlinx.serialization.Serializable -private const val LOGIN: String = "login" -private const val CARD: String = "card" -private const val IDENTITY: String = "identity" -private const val SECURE_NOTE: String = "secure_note" -private const val SSH_KEY: String = "ssh_key" -private const val VAULT_ITEM_CIPHER_TYPE: String = "vault_item_cipher_type" - -private const val VAULT_ITEM_PREFIX = "vault_item" -private const val VAULT_ITEM_ID = "vault_item_id" -private const val VAULT_ITEM_ROUTE = "$VAULT_ITEM_PREFIX/{$VAULT_ITEM_ID}" + - "?$VAULT_ITEM_CIPHER_TYPE={$VAULT_ITEM_CIPHER_TYPE}" +/** + * The type-safe route for the vault item screen. + */ +@Serializable +data class VaultItemRoute( + val vaultItemId: String, + val cipherType: VaultItemCipherType, +) /** * Class to retrieve vault item arguments from the [SavedStateHandle]. @@ -31,12 +28,14 @@ private const val VAULT_ITEM_ROUTE = "$VAULT_ITEM_PREFIX/{$VAULT_ITEM_ID}" + data class VaultItemArgs( val vaultItemId: String, val cipherType: VaultItemCipherType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - vaultItemId = checkNotNull(savedStateHandle.get(VAULT_ITEM_ID)), - cipherType = requireNotNull(savedStateHandle.get(VAULT_ITEM_CIPHER_TYPE)) - .toVaultItemCipherType(), - ) +) + +/** + * Constructs a [VaultItemArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toVaultItemArgs(): VaultItemArgs { + val route = this.toRoute() + return VaultItemArgs(vaultItemId = route.vaultItemId, cipherType = route.cipherType) } /** @@ -49,13 +48,7 @@ fun NavGraphBuilder.vaultItemDestination( onNavigateToAttachments: (vaultItemId: String) -> Unit, onNavigateToPasswordHistory: (vaultItemId: String) -> Unit, ) { - composableWithSlideTransitions( - route = VAULT_ITEM_ROUTE, - arguments = listOf( - navArgument(VAULT_ITEM_ID) { type = NavType.StringType }, - navArgument(VAULT_ITEM_CIPHER_TYPE) { type = NavType.StringType }, - ), - ) { + composableWithSlideTransitions { VaultItemScreen( onNavigateBack = onNavigateBack, onNavigateToVaultAddEditItem = onNavigateToVaultEditItem, @@ -74,29 +67,10 @@ fun NavController.navigateToVaultItem( navOptions: NavOptions? = null, ) { navigate( - route = "$VAULT_ITEM_PREFIX/${args.vaultItemId}" + - "?$VAULT_ITEM_CIPHER_TYPE=${args.cipherType.toTypeString()}", + route = VaultItemRoute( + vaultItemId = args.vaultItemId, + cipherType = args.cipherType, + ), navOptions = navOptions, ) } - -private fun VaultItemCipherType.toTypeString(): String = - when (this) { - VaultItemCipherType.LOGIN -> LOGIN - VaultItemCipherType.CARD -> CARD - VaultItemCipherType.IDENTITY -> IDENTITY - VaultItemCipherType.SECURE_NOTE -> SECURE_NOTE - VaultItemCipherType.SSH_KEY -> SSH_KEY - } - -private fun String.toVaultItemCipherType(): VaultItemCipherType = - when (this) { - LOGIN -> VaultItemCipherType.LOGIN - CARD -> VaultItemCipherType.CARD - IDENTITY -> VaultItemCipherType.IDENTITY - SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE - SSH_KEY -> VaultItemCipherType.SSH_KEY - else -> throw IllegalStateException( - "Edit Item string arguments for VaultAddEditNavigation must match!", - ) - } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt index 2476790915..bc2b131555 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModel.kt @@ -76,7 +76,7 @@ class VaultItemViewModel @Inject constructor( ) : BaseViewModel( // We load the state from the savedStateHandle for testing purposes. initialState = savedStateHandle[KEY_STATE] ?: run { - val args = VaultItemArgs(savedStateHandle) + val args = savedStateHandle.toVaultItemArgs() VaultItemState( vaultItemId = args.vaultItemId, cipherType = args.cipherType, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt index 54231283b9..bf5bc409e2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt @@ -6,8 +6,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions @@ -15,45 +14,97 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType +import kotlinx.serialization.Serializable -private const val CARD: String = "card" -private const val COLLECTION: String = "collection" -private const val FOLDER: String = "folder" -private const val IDENTITY: String = "identity" -private const val LOGIN: String = "login" -private const val SSH_KEY: String = "ssh_key" -private const val SECURE_NOTE: String = "secure_note" -private const val SEND_FILE: String = "send_file" -private const val SEND_TEXT: String = "send_text" -private const val TRASH: String = "trash" -private const val VAULT_ITEM_LISTING_PREFIX: String = "vault_item_listing" -private const val VAULT_ITEM_LISTING_AS_ROOT_PREFIX: String = "vault_item_listing_as_root" -private const val VAULT_ITEM_LISTING_TYPE: String = "vault_item_listing_type" -private const val ID: String = "id" -private const val VAULT_ITEM_LISTING_ROUTE: String = - "$VAULT_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + - "?$ID={$ID}" -private const val VAULT_ITEM_LISTING_AS_ROOT_ROUTE: String = - "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + - "?$ID={$ID}" -private const val SEND_ITEM_LISTING_PREFIX: String = "send_item_listing" -private const val SEND_ITEM_LISTING_ROUTE: String = - "$SEND_ITEM_LISTING_PREFIX/{$VAULT_ITEM_LISTING_TYPE}" + - "?$ID={$ID}" +/** + * The type-safe route for the vault item listing screen. + */ +@Serializable +sealed class VaultItemListingRoute { + /** + * The type of item to be displayed. + */ + abstract val type: ItemListingType + + /** + * The optional item ID used for folder and collection types. + */ + abstract val itemId: String? + + /** + * The type-safe route for the cipher specific vault item listing screen. + */ + @Serializable + data class CipherItemListing( + override val type: ItemListingType, + override val itemId: String?, + ) : VaultItemListingRoute() + + /** + * The type-safe route for the send specific vault item listing screen. + */ + @Serializable + data class SendItemListing( + override val type: ItemListingType, + override val itemId: String?, + ) : VaultItemListingRoute() + + /** + * The type-safe route for the root vault item listing screen. + */ + @Serializable + data class AsRoot( + override val type: ItemListingType, + override val itemId: String?, + ) : VaultItemListingRoute() +} + +/** + * The type of items to be displayed. + */ +@Serializable +enum class ItemListingType { + LOGIN, + IDENTITY, + SECURE_NOTE, + CARD, + SSH_KEY, + TRASH, + FOLDER, + COLLECTION, + SEND_FILE, + SEND_TEXT, +} /** * Class to retrieve vault item listing arguments from the [SavedStateHandle]. */ data class VaultItemListingArgs( val vaultItemListingType: VaultItemListingType, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - vaultItemListingType = determineVaultItemListingType( - vaultItemListingTypeString = checkNotNull( - savedStateHandle[VAULT_ITEM_LISTING_TYPE], - ) as String, - id = savedStateHandle[ID], - ), +) + +/** + * Constructs a [VaultItemListingArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toVaultItemListingArgs(): VaultItemListingArgs { + // We just need to pull the serializable data out of the route, since they are always the same + // it does not matter which instance we fetch. + val route = this.toRoute() + return VaultItemListingArgs( + vaultItemListingType = when (route.type) { + ItemListingType.LOGIN -> VaultItemListingType.Login + ItemListingType.CARD -> VaultItemListingType.Card + ItemListingType.IDENTITY -> VaultItemListingType.Identity + ItemListingType.SECURE_NOTE -> VaultItemListingType.SecureNote + ItemListingType.SSH_KEY -> VaultItemListingType.SshKey + ItemListingType.TRASH -> VaultItemListingType.Trash + ItemListingType.SEND_FILE -> VaultItemListingType.SendFile + ItemListingType.SEND_TEXT -> VaultItemListingType.SendText + ItemListingType.FOLDER -> VaultItemListingType.Folder(folderId = route.itemId) + ItemListingType.COLLECTION -> VaultItemListingType.Collection( + collectionId = requireNotNull(route.itemId), + ) + }, ) } @@ -70,8 +121,7 @@ fun NavGraphBuilder.vaultItemListingDestination( onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, ) { - internalVaultItemListingDestination( - route = VAULT_ITEM_LISTING_ROUTE, + internalVaultItemListingDestination( onNavigateBack = onNavigateBack, onNavigateToAddSendItem = { }, onNavigateToEditSendItem = { }, @@ -96,17 +146,7 @@ fun NavGraphBuilder.vaultItemListingDestinationAsRoot( onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, ) { - composableWithStayTransitions( - route = VAULT_ITEM_LISTING_AS_ROOT_ROUTE, - arguments = listOf( - navArgument( - name = VAULT_ITEM_LISTING_TYPE, - builder = { - type = NavType.StringType - }, - ), - ), - ) { + composableWithStayTransitions { VaultItemListingScreen( onNavigateBack = onNavigateBack, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, @@ -130,8 +170,7 @@ fun NavGraphBuilder.sendItemListingDestination( onNavigateToEditSendItem: (sendId: String) -> Unit, onNavigateToSearchSend: (searchType: SearchType.Sends) -> Unit, ) { - internalVaultItemListingDestination( - route = SEND_ITEM_LISTING_ROUTE, + internalVaultItemListingDestination( onNavigateBack = onNavigateBack, onNavigateToAddSendItem = onNavigateToAddSendItem, onNavigateToEditSendItem = onNavigateToEditSendItem, @@ -147,37 +186,19 @@ fun NavGraphBuilder.sendItemListingDestination( /** * Add the [VaultItemListingScreen] to the nav graph. */ -@Suppress("LongParameterList") -private fun NavGraphBuilder.internalVaultItemListingDestination( - route: String, - onNavigateBack: () -> Unit, - onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit, - onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit, - onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit, - onNavigateToVaultAddItemScreen: (args: VaultAddEditArgs) -> Unit, - onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, - onNavigateToAddSendItem: () -> Unit, - onNavigateToEditSendItem: (sendId: String) -> Unit, - onNavigateToSearch: (searchType: SearchType) -> Unit, +@Suppress("LongParameterList", "MaxLineLength") +private inline fun NavGraphBuilder.internalVaultItemListingDestination( + noinline onNavigateBack: () -> Unit, + noinline onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit, + noinline onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit, + noinline onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit, + noinline onNavigateToVaultAddItemScreen: (args: VaultAddEditArgs) -> Unit, + noinline onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, + noinline onNavigateToAddSendItem: () -> Unit, + noinline onNavigateToEditSendItem: (sendId: String) -> Unit, + noinline onNavigateToSearch: (searchType: SearchType) -> Unit, ) { - composableWithPushTransitions( - route = route, - arguments = listOf( - navArgument( - name = VAULT_ITEM_LISTING_TYPE, - builder = { - type = NavType.StringType - }, - ), - navArgument( - name = ID, - builder = { - type = NavType.StringType - nullable = true - }, - ), - ), - ) { + composableWithPushTransitions { VaultItemListingScreen( onNavigateBack = onNavigateBack, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, @@ -199,9 +220,11 @@ fun NavController.navigateToVaultItemListing( vaultItemListingType: VaultItemListingType, navOptions: NavOptions? = null, ) { - navigate( - route = "$VAULT_ITEM_LISTING_PREFIX/${vaultItemListingType.toTypeString()}" + - "?$ID=${vaultItemListingType.toIdOrNull()}", + this.navigate( + route = VaultItemListingRoute.CipherItemListing( + type = vaultItemListingType.toItemListingType(), + itemId = vaultItemListingType.toIdOrNull(), + ), navOptions = navOptions, ) } @@ -214,8 +237,10 @@ fun NavController.navigateToVaultItemListingAsRoot( navOptions: NavOptions? = null, ) { navigate( - route = "$VAULT_ITEM_LISTING_AS_ROOT_PREFIX/${vaultItemListingType.toTypeString()}" + - "?$ID=${vaultItemListingType.toIdOrNull()}", + route = VaultItemListingRoute.AsRoot( + type = vaultItemListingType.toItemListingType(), + itemId = vaultItemListingType.toIdOrNull(), + ), navOptions = navOptions, ) } @@ -227,25 +252,27 @@ fun NavController.navigateToSendItemListing( vaultItemListingType: VaultItemListingType, navOptions: NavOptions? = null, ) { - navigate( - route = "$SEND_ITEM_LISTING_PREFIX/${vaultItemListingType.toTypeString()}" + - "?$ID=${vaultItemListingType.toIdOrNull()}", + this.navigate( + route = VaultItemListingRoute.SendItemListing( + type = vaultItemListingType.toItemListingType(), + itemId = vaultItemListingType.toIdOrNull(), + ), navOptions = navOptions, ) } -private fun VaultItemListingType.toTypeString(): String { +private fun VaultItemListingType.toItemListingType(): ItemListingType { return when (this) { - is VaultItemListingType.Card -> CARD - is VaultItemListingType.Collection -> COLLECTION - is VaultItemListingType.Folder -> FOLDER - is VaultItemListingType.Identity -> IDENTITY - is VaultItemListingType.Login -> LOGIN - is VaultItemListingType.SecureNote -> SECURE_NOTE - is VaultItemListingType.Trash -> TRASH - is VaultItemListingType.SendFile -> SEND_FILE - is VaultItemListingType.SendText -> SEND_TEXT - is VaultItemListingType.SshKey -> SSH_KEY + is VaultItemListingType.Card -> ItemListingType.CARD + is VaultItemListingType.Collection -> ItemListingType.COLLECTION + is VaultItemListingType.Folder -> ItemListingType.FOLDER + is VaultItemListingType.Identity -> ItemListingType.IDENTITY + is VaultItemListingType.Login -> ItemListingType.LOGIN + is VaultItemListingType.SecureNote -> ItemListingType.SECURE_NOTE + is VaultItemListingType.Trash -> ItemListingType.TRASH + is VaultItemListingType.SendFile -> ItemListingType.SEND_FILE + is VaultItemListingType.SendText -> ItemListingType.SEND_TEXT + is VaultItemListingType.SshKey -> ItemListingType.SSH_KEY } } @@ -262,23 +289,3 @@ private fun VaultItemListingType.toIdOrNull(): String? = is VaultItemListingType.SendText -> null is VaultItemListingType.SshKey -> null } - -private fun determineVaultItemListingType( - vaultItemListingTypeString: String, - id: String?, -): VaultItemListingType { - return when (vaultItemListingTypeString) { - LOGIN -> VaultItemListingType.Login - CARD -> VaultItemListingType.Card - IDENTITY -> VaultItemListingType.Identity - SECURE_NOTE -> VaultItemListingType.SecureNote - SSH_KEY -> VaultItemListingType.SshKey - TRASH -> VaultItemListingType.Trash - FOLDER -> VaultItemListingType.Folder(folderId = id) - COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id)) - SEND_FILE -> VaultItemListingType.SendFile - SEND_TEXT -> VaultItemListingType.SendText - // This should never occur, vaultItemListingTypeString must match - else -> throw IllegalStateException() - } -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 1b3e45c40d..b1a33db559 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -133,7 +133,8 @@ class VaultItemListingViewModel @Inject constructor( val fido2GetCredentialsRequest = specialCircumstance?.toFido2GetCredentialsRequestOrNull() val fido2AssertCredentialRequest = specialCircumstance?.toFido2AssertionRequestOrNull() VaultItemListingState( - itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle) + itemListingType = savedStateHandle + .toVaultItemListingArgs() .vaultItemListingType .toItemListingType(), activeAccountSummary = activeAccountSummary, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryNavigation.kt index 161a9fa98e..d5a8806617 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/manualcodeentry/ManualCodeEntryNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val MANUAL_CODE_ENTRY_ROUTE: String = "manual_code_entry" +/** + * The type-safe route for the manual code entry screen. + */ +@Serializable +data object ManualCodeEntryRoute /** * Add the manual code entry screen to the nav graph. @@ -17,9 +22,7 @@ fun NavGraphBuilder.vaultManualCodeEntryDestination( onNavigateBack: () -> Unit, onNavigateToQrCodeScreen: () -> Unit, ) { - composableWithSlideTransitions( - route = MANUAL_CODE_ENTRY_ROUTE, - ) { + composableWithSlideTransitions { ManualCodeEntryScreen( onNavigateBack = onNavigateBack, onNavigateToQrCodeScreen = onNavigateToQrCodeScreen, @@ -33,5 +36,5 @@ fun NavGraphBuilder.vaultManualCodeEntryDestination( fun NavController.navigateToManualCodeEntryScreen( navOptions: NavOptions? = null, ) { - this.navigate(MANUAL_CODE_ENTRY_ROUTE, navOptions) + this.navigate(route = ManualCodeEntryRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationNavigation.kt index fd61efa2c2..8563c5f547 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationNavigation.kt @@ -6,19 +6,19 @@ import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import androidx.navigation.NavType -import androidx.navigation.navArgument +import androidx.navigation.toRoute import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val VAULT_MOVE_TO_ORGANIZATION_PREFIX = "vault_move_to_organization" -private const val VAULT_MOVE_TO_ORGANIZATION_ID = "vault_move_to_organization_id" -private const val VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS = - "vault_move_to_organization_only_collections" -private const val VAULT_MOVE_TO_ORGANIZATION_ROUTE = - VAULT_MOVE_TO_ORGANIZATION_PREFIX + - "/{$VAULT_MOVE_TO_ORGANIZATION_ID}" + - "/{$VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS}" +/** + * The type-safe route for the vault move to organization screen. + */ +@Serializable +data class VaultMoveToOrganizationRoute( + val vaultItemId: String, + val showOnlyCollections: Boolean, +) /** * Class to retrieve vault move to organization arguments from the [SavedStateHandle]. @@ -26,12 +26,16 @@ private const val VAULT_MOVE_TO_ORGANIZATION_ROUTE = data class VaultMoveToOrganizationArgs( val vaultItemId: String, val showOnlyCollections: Boolean, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - vaultItemId = checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ID]) as String, - showOnlyCollections = - (checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS]) as String) - .toBoolean(), +) + +/** + * Constructs a [VaultMoveToOrganizationArgs] from the [SavedStateHandle] and internal route data. + */ +fun SavedStateHandle.toVaultMoveToOrganizationArgs(): VaultMoveToOrganizationArgs { + val route = this.toRoute() + return VaultMoveToOrganizationArgs( + vaultItemId = route.vaultItemId, + showOnlyCollections = route.showOnlyCollections, ) } @@ -41,15 +45,7 @@ data class VaultMoveToOrganizationArgs( fun NavGraphBuilder.vaultMoveToOrganizationDestination( onNavigateBack: () -> Unit, ) { - composableWithSlideTransitions( - route = VAULT_MOVE_TO_ORGANIZATION_ROUTE, - arguments = listOf( - navArgument(VAULT_MOVE_TO_ORGANIZATION_ID) { type = NavType.StringType }, - navArgument(VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS) { - type = NavType.StringType - }, - ), - ) { + composableWithSlideTransitions { VaultMoveToOrganizationScreen( onNavigateBack = onNavigateBack, ) @@ -64,8 +60,11 @@ fun NavController.navigateToVaultMoveToOrganization( showOnlyCollections: Boolean, navOptions: NavOptions? = null, ) { - navigate( - route = "$VAULT_MOVE_TO_ORGANIZATION_PREFIX/$vaultItemId/$showOnlyCollections", + this.navigate( + route = VaultMoveToOrganizationRoute( + vaultItemId = vaultItemId, + showOnlyCollections = showOnlyCollections, + ), navOptions = navOptions, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt index 4a14a58ffe..202dde94a8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModel.kt @@ -5,6 +5,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.util.combineDataStates +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText +import com.bitwarden.ui.util.concat import com.bitwarden.vault.CipherView import com.bitwarden.vault.CollectionView import com.x8bit.bitwarden.R @@ -13,9 +16,6 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ShareCipherResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText -import com.bitwarden.ui.util.concat import com.x8bit.bitwarden.ui.vault.feature.movetoorganization.util.toViewState import com.x8bit.bitwarden.ui.vault.model.VaultCollection import dagger.hilt.android.lifecycle.HiltViewModel @@ -42,9 +42,10 @@ class VaultMoveToOrganizationViewModel @Inject constructor( ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { + val args = savedStateHandle.toVaultMoveToOrganizationArgs() VaultMoveToOrganizationState( - vaultItemId = VaultMoveToOrganizationArgs(savedStateHandle).vaultItemId, - onlyShowCollections = VaultMoveToOrganizationArgs(savedStateHandle).showOnlyCollections, + vaultItemId = args.vaultItemId, + onlyShowCollections = args.showOnlyCollections, viewState = VaultMoveToOrganizationState.ViewState.Loading, dialogState = null, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanNavigation.kt index 0e5228c4d0..f24250e1ea 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/qrcodescan/QrCodeScanNavigation.kt @@ -7,8 +7,13 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions +import kotlinx.serialization.Serializable -private const val QR_CODE_SCAN_ROUTE: String = "qr_code_scan" +/** + * The type-safe route for the QR code scan screen. + */ +@Serializable +data object QrCodeScanRoute /** * Add the QR code scan screen to the nav graph. @@ -17,9 +22,7 @@ fun NavGraphBuilder.vaultQrCodeScanDestination( onNavigateBack: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit, ) { - composableWithSlideTransitions( - route = QR_CODE_SCAN_ROUTE, - ) { + composableWithSlideTransitions { QrCodeScanScreen( onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen, onNavigateBack = onNavigateBack, @@ -33,5 +36,5 @@ fun NavGraphBuilder.vaultQrCodeScanDestination( fun NavController.navigateToQrCodeScanScreen( navOptions: NavOptions? = null, ) { - this.navigate(QR_CODE_SCAN_ROUTE, navOptions) + this.navigate(route = QrCodeScanRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 3143cc9d11..4bfc0e3a4a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -15,8 +15,13 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListi import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen import com.x8bit.bitwarden.ui.vault.feature.verificationcode.vaultVerificationCodeDestination +import kotlinx.serialization.Serializable -const val VAULT_GRAPH_ROUTE: String = "vault_graph" +/** + * The type-safe route for the vault graph. + */ +@Serializable +data object VaultGraphRoute /** * Add vault destinations to the nav graph. @@ -33,9 +38,8 @@ fun NavGraphBuilder.vaultGraph( onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, onNavigateToAboutScreen: () -> Unit, ) { - navigation( - route = VAULT_GRAPH_ROUTE, - startDestination = VAULT_ROUTE, + navigation( + startDestination = VaultRoute, ) { vaultDestination( onNavigateToVaultAddItemScreen = { onNavigateToVaultAddItemScreen(it) }, @@ -75,5 +79,5 @@ fun NavGraphBuilder.vaultGraph( * Navigate to the vault graph. */ fun NavController.navigateToVaultGraph(navOptions: NavOptions? = null) { - navigate(VAULT_GRAPH_ROUTE, navOptions) + this.navigate(route = VaultGraphRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt index 270ccbac7c..18d3cb0731 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -12,8 +12,13 @@ import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType +import kotlinx.serialization.Serializable -const val VAULT_ROUTE: String = "vault" +/** + * The type-safe route for the vault screen. + */ +@Serializable +data object VaultRoute /** * Add vault destination to the nav graph. @@ -31,9 +36,7 @@ fun NavGraphBuilder.vaultDestination( onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit, onNavigateToAboutScreen: () -> Unit, ) { - composableWithRootPushTransitions( - route = VAULT_ROUTE, - ) { + composableWithRootPushTransitions { VaultScreen( onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen, onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, @@ -53,5 +56,5 @@ fun NavGraphBuilder.vaultDestination( * Navigate to the [VaultScreen]. */ fun NavController.navigateToVault(navOptions: NavOptions? = null) { - navigate(VAULT_ROUTE, navOptions) + this.navigate(route = VaultRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt index a9d463ab95..7f1461cbd2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/verificationcode/VerificationCodeNavigation.kt @@ -8,8 +8,13 @@ import androidx.navigation.NavOptions import com.bitwarden.core.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs +import kotlinx.serialization.Serializable -private const val VERIFICATION_CODE_ROUTE: String = "verification_code" +/** + * The type-safe route for the verification code screen. + */ +@Serializable +data object VerificationCodeRoute /** * Add the verification code screen to the nav graph. @@ -19,9 +24,7 @@ fun NavGraphBuilder.vaultVerificationCodeDestination( onNavigateToSearchVault: () -> Unit, onNavigateToVaultItemScreen: (args: VaultItemArgs) -> Unit, ) { - composableWithPushTransitions( - route = VERIFICATION_CODE_ROUTE, - ) { + composableWithPushTransitions { VerificationCodeScreen( onNavigateToVaultItemScreen = onNavigateToVaultItemScreen, onNavigateToSearch = onNavigateToSearchVault, @@ -36,5 +39,5 @@ fun NavGraphBuilder.vaultVerificationCodeDestination( fun NavController.navigateToVerificationCodeScreen( navOptions: NavOptions? = null, ) { - this.navigate(VERIFICATION_CODE_ROUTE, navOptions) + this.navigate(route = VerificationCodeRoute, navOptions = navOptions) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt index 6571a4601b..e5ab22f1da 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/model/VaultItemCipherType.kt @@ -1,8 +1,11 @@ package com.x8bit.bitwarden.ui.vault.model +import kotlinx.serialization.Serializable + /** * Represents different types of ciphers that can be added/viewed. */ +@Serializable enum class VaultItemCipherType { /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt index 89509d9b03..c1725a1f04 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupAutoFillViewModelTest.kt @@ -12,14 +12,18 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class SetupAutoFillViewModelTest : BaseViewModelTest() { @@ -45,6 +49,16 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { every { setOnboardingStatus(any()) } just runs } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toSetupAutoFillArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toSetupAutoFillArgs) + } + @Test fun `handleAutofillEnabledUpdateReceive updates autofillEnabled state`() { val viewModel = createViewModel() @@ -183,12 +197,10 @@ class SetupAutoFillViewModelTest : BaseViewModelTest() { private fun createViewModel( initialState: SetupAutoFillState? = null, ) = SetupAutoFillViewModel( - savedStateHandle = SavedStateHandle( - mapOf( - "state" to initialState, - "isInitialSetup" to true, - ), - ), + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = initialState) + every { toSetupAutoFillArgs() } returns SetupAutoFillScreenArgs(isInitialSetup = true) + }, settingsRepository = settingsRepository, authRepository = authRepository, firstTimeActionManager = firstTimeActionManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt index 9c3ca026c7..8ac746dfb4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupUnlockViewModelTest.kt @@ -20,12 +20,16 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import javax.crypto.Cipher @@ -56,6 +60,16 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { every { createCipherOrNull(DEFAULT_USER_ID) } returns CIPHER } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toSetupUnlockArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toSetupUnlockArgs) + } + @Test fun `initial state should be correct`() { val viewModel = createViewModel() @@ -373,12 +387,10 @@ class SetupUnlockViewModelTest : BaseViewModelTest() { state: SetupUnlockState? = null, ): SetupUnlockViewModel = SetupUnlockViewModel( - savedStateHandle = SavedStateHandle( - mapOf( - "state" to state, - "isInitialSetup" to true, - ), - ), + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = state) + every { toSetupUnlockArgs() } returns SetupUnlockArgs(isInitialSetup = true) + }, authRepository = authRepository, settingsRepository = settingsRepository, biometricsEncryptionManager = biometricsEncryptionManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModelTest.kt index 1aa890f7bb..16bc4d2f65 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailViewModelTest.kt @@ -2,21 +2,26 @@ package com.x8bit.bitwarden.ui.auth.feature.checkemail import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager -import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.MutableStateFlow +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class CheckEmailViewModelTest : BaseViewModelTest() { - private val mutableFeatureFlagFlow = MutableStateFlow(false) - private val featureFlagManager = mockk(relaxed = true) { - every { getFeatureFlag(FlagKey.OnboardingFlow) } returns false - every { getFeatureFlagFlow(FlagKey.OnboardingFlow) } returns mutableFeatureFlagFlow + + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toCheckEmailArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toCheckEmailArgs) } @Test @@ -76,16 +81,14 @@ class CheckEmailViewModelTest : BaseViewModelTest() { private fun createViewModel(state: CheckEmailState? = null): CheckEmailViewModel = CheckEmailViewModel( - savedStateHandle = SavedStateHandle().also { - it["email"] = EMAIL - it["state"] = state + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = state) + every { toCheckEmailArgs() } returns CheckEmailArgs(emailAddress = EMAIL) }, ) - - companion object { - private const val EMAIL = "test@gmail.com" - private val DEFAULT_STATE = CheckEmailState( - email = EMAIL, - ) - } } + +private const val EMAIL = "test@gmail.com" +private val DEFAULT_STATE = CheckEmailState( + email = EMAIL, +) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index 8fdc43ab4a..4dd44c4f5f 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -108,12 +108,18 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { @BeforeEach fun setUp() { specialCircumstanceManager.specialCircumstance = mockCompleteRegistrationCircumstance - mockkStatic(::generateUriForCaptcha) + mockkStatic( + SavedStateHandle::toCompleteRegistrationArgs, + ::generateUriForCaptcha, + ) } @AfterEach fun tearDown() { - unmockkStatic(::generateUriForCaptcha) + unmockkStatic( + SavedStateHandle::toCompleteRegistrationArgs, + ::generateUriForCaptcha, + ) } @Test @@ -668,11 +674,14 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() { private fun createCompleteRegistrationViewModel( completeRegistrationState: CompleteRegistrationState? = DEFAULT_STATE, ): CompleteRegistrationViewModel = CompleteRegistrationViewModel( - savedStateHandle = SavedStateHandle( - mapOf( - "state" to completeRegistrationState, - ), - ), + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = completeRegistrationState) + every { toCompleteRegistrationArgs() } returns CompleteRegistrationArgs( + emailAddress = completeRegistrationState?.userEmail ?: EMAIL, + verificationToken = completeRegistrationState?.emailVerificationToken ?: TOKEN, + fromEmail = completeRegistrationState?.fromEmail ?: false, + ) + }, authRepository = mockAuthRepository, environmentRepository = fakeEnvironmentRepository, specialCircumstanceManager = specialCircumstanceManager, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt index 4aa72c2586..4592984413 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/enterprisesignon/EnterpriseSignOnViewModelTest.kt @@ -68,14 +68,20 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { @BeforeEach fun setUp() { - mockkStatic(::generateUriForSso) - mockkStatic(Uri::parse) + mockkStatic( + SavedStateHandle::toEnterpriseSignOnArgs, + ::generateUriForSso, + Uri::parse, + ) } @AfterEach fun tearDown() { - unmockkStatic(::generateUriForSso) - unmockkStatic(Uri::parse) + unmockkStatic( + SavedStateHandle::toEnterpriseSignOnArgs, + ::generateUriForSso, + Uri::parse, + ) } @Test @@ -1172,14 +1178,14 @@ class EnterpriseSignOnViewModelTest : BaseViewModelTest() { initialState: EnterpriseSignOnState? = null, ssoData: SsoResponseData? = null, ssoCallbackResult: SsoCallbackResult? = null, - savedStateHandle: SavedStateHandle = SavedStateHandle( - initialState = mapOf( - "state" to initialState, - "email_address" to DEFAULT_EMAIL, - "ssoData" to ssoData, - "ssoCallbackResult" to ssoCallbackResult, - ), - ), + savedStateHandle: SavedStateHandle = SavedStateHandle().apply { + set(key = "state", value = initialState) + set(key = "ssoData", value = ssoData) + set(key = "ssoCallbackResult", value = ssoCallbackResult) + every { + toEnterpriseSignOnArgs() + } returns EnterpriseSignOnArgs(emailAddress = DEFAULT_EMAIL) + }, isNetworkConnected: Boolean = true, dismissInitialDialog: Boolean = true, ): EnterpriseSignOnViewModel = EnterpriseSignOnViewModel( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt index b3c090593e..ef9f3fd682 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginViewModelTest.kt @@ -58,12 +58,18 @@ class LoginViewModelTest : BaseViewModelTest() { @BeforeEach fun setUp() { - mockkStatic(::generateUriForCaptcha) + mockkStatic( + ::generateUriForCaptcha, + SavedStateHandle::toLoginArgs, + ) } @AfterEach fun tearDown() { - unmockkStatic(::generateUriForCaptcha) + unmockkStatic( + ::generateUriForCaptcha, + SavedStateHandle::toLoginArgs, + ) } @Test @@ -617,9 +623,9 @@ class LoginViewModelTest : BaseViewModelTest() { authRepository = authRepository, environmentRepository = fakeEnvironmentRepository, vaultRepository = vaultRepository, - savedStateHandle = SavedStateHandle().also { - it["email_address"] = EMAIL - it["state"] = state + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = state) + every { toLoginArgs() } returns LoginArgs(emailAddress = EMAIL, captchaToken = null) }, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt index 406d34bc37..ddcd9a2720 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt @@ -16,11 +16,16 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.awaits import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.ZonedDateTime @@ -38,6 +43,16 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { coEvery { captchaTokenResultFlow } returns mutableCaptchaTokenResultFlow } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toLoginWithDeviceArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toLoginWithDeviceArgs) + } + @Test fun `initial state should be correct`() { val viewModel = createViewModel(state = null) @@ -728,8 +743,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() { authRepository = authRepository, savedStateHandle = SavedStateHandle().apply { set("state", state) - set("email_address", state?.emailAddress ?: EMAIL) - set("login_type", state?.loginWithDeviceType ?: LoginWithDeviceType.OTHER_DEVICE) + every { toLoginWithDeviceArgs() } returns LoginWithDeviceArgs( + emailAddress = state?.emailAddress ?: EMAIL, + loginType = state?.loginWithDeviceType ?: LoginWithDeviceType.OTHER_DEVICE, + ) }, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt index f9f202b995..9334b95e95 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt @@ -10,7 +10,6 @@ import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault import com.bitwarden.network.model.GetTokenResponseJson import com.bitwarden.network.model.TwoFactorAuthMethod import com.bitwarden.network.model.TwoFactorDataModel -import com.bitwarden.network.util.base64UrlDecodeOrNull import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -75,15 +74,9 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { mockkStatic( ::generateUriForCaptcha, ::generateUriForWebAuth, - String::base64UrlDecodeOrNull, + SavedStateHandle::toTwoFactorLoginArgs, ) mockkStatic(Uri::class) - every { - DEFAULT_ENCODED_PASSWORD.base64UrlDecodeOrNull() - } returns DEFAULT_PASSWORD - every { - DEFAULT_ENCODED_ORG_IDENTIFIER.base64UrlDecodeOrNull() - } returns DEFAULT_ORG_IDENTIFIER } @AfterEach @@ -91,7 +84,7 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { unmockkStatic( ::generateUriForCaptcha, ::generateUriForWebAuth, - String::base64UrlDecodeOrNull, + SavedStateHandle::toTwoFactorLoginArgs, ) unmockkStatic(Uri::class) } @@ -100,9 +93,6 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { fun `initial state should be correct`() = runTest { val viewModel = createViewModel() viewModel.stateFlow.test { - verify { - DEFAULT_ENCODED_PASSWORD.base64UrlDecodeOrNull() - } assertEquals(DEFAULT_STATE, awaitItem()) } } @@ -1233,12 +1223,14 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { authRepository = authRepository, environmentRepository = environmentRepository, resourceManager = resourceManager, - savedStateHandle = SavedStateHandle().also { - it["state"] = state - it["email_address"] = DEFAULT_EMAIL_ADDRESS - it["password"] = DEFAULT_ENCODED_PASSWORD - it["org_identifier"] = DEFAULT_ENCODED_ORG_IDENTIFIER - it["new_device_verification"] = false + savedStateHandle = SavedStateHandle().apply { + set(key = "state", value = state) + every { toTwoFactorLoginArgs() } returns TwoFactorLoginArgs( + emailAddress = DEFAULT_EMAIL_ADDRESS, + password = DEFAULT_PASSWORD, + orgIdentifier = DEFAULT_ENCODED_ORG_IDENTIFIER, + isNewDeviceVerification = false, + ) }, ) } @@ -1260,7 +1252,6 @@ private const val DEFAULT_EMAIL_ADDRESS = "example@email.com" private const val DEFAULT_ORG_IDENTIFIER = "org_identifier" private const val DEFAULT_ENCODED_ORG_IDENTIFIER = "org_identifier" private const val DEFAULT_PASSWORD = "password123" -private const val DEFAULT_ENCODED_PASSWORD = "base64EncodedPassword" private val DEFAULT_STATE = TwoFactorLoginState( authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP, availableAuthMethods = listOf( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 4097ba8c36..3b03972094 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -36,14 +36,18 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import javax.crypto.Cipher @@ -97,6 +101,16 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { every { isFromLockFlow } returns false } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toVaultUnlockArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toVaultUnlockArgs) + } + @Test fun `on init with biometrics enabled and valid should emit PromptForBiometrics`() = runTest { val initialState = DEFAULT_STATE.copy( @@ -1347,7 +1361,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { ): VaultUnlockViewModel = VaultUnlockViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state) - set("unlock_type", unlockType) + every { toVaultUnlockArgs() } returns VaultUnlockArgs(unlockType = unlockType) }, authRepository = authRepository, vaultRepo = vaultRepo, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt index c73327888f..171ab02bd6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/FakeNavHostController.kt @@ -79,8 +79,14 @@ class FakeNavHostController : NavHostController(context = mockk()) { every { id } returns graphId every { startDestinationId } returns graphId every { - findNode(graphId) + findNode(resId = graphId) } returns mockk { every { id } returns graphId } + every { + findNodeComprehensive(resId = any(), lastVisited = any(), searchChildren = any()) + } returns mockk { + every { id } returns graphId + every { arguments } returns emptyMap() + } } override var graph: NavGraph diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt index e765fb12a9..b6576f7f89 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/search/SearchViewModelTest.kt @@ -49,6 +49,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForCipher import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize import com.x8bit.bitwarden.ui.platform.feature.search.util.toViewState @@ -130,6 +131,7 @@ class SearchViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { mockkStatic( + SavedStateHandle::toSearchArgs, List::toViewState, List::filterAndOrganize, List::toFilteredList, @@ -139,6 +141,7 @@ class SearchViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { unmockkStatic( + SavedStateHandle::toSearchArgs, List::toViewState, List::filterAndOrganize, List::toFilteredList, @@ -1518,44 +1521,31 @@ class SearchViewModelTest : BaseViewModelTest() { ): SearchViewModel = SearchViewModel( SavedStateHandle().apply { set("state", initialState) - set( - "search_type", - when (initialState?.searchType) { - SearchTypeData.Sends.All -> "search_type_sends_all" - SearchTypeData.Sends.Files -> "search_type_sends_file" - SearchTypeData.Sends.Texts -> "search_type_sends_text" - SearchTypeData.Vault.All -> "search_type_vault_all" - SearchTypeData.Vault.Cards -> "search_type_vault_cards" - SearchTypeData.Vault.SshKeys -> "search_type_vault_ssh_keys" - is SearchTypeData.Vault.Collection -> "search_type_vault_collection" - is SearchTypeData.Vault.Folder -> "search_type_vault_folder" - SearchTypeData.Vault.Identities -> "search_type_vault_identities" - SearchTypeData.Vault.Logins -> "search_type_vault_logins" - SearchTypeData.Vault.NoFolder -> "search_type_vault_no_folder" - SearchTypeData.Vault.SecureNotes -> "search_type_vault_secure_notes" - SearchTypeData.Vault.VerificationCodes -> "search_type_vault_verification_codes" - SearchTypeData.Vault.Trash -> "search_type_vault_trash" - null -> "search_type_vault_all" - }, - ) - set( - "search_type_id", - when (val searchType = initialState?.searchType) { - SearchTypeData.Sends.All -> null - SearchTypeData.Sends.Files -> null - SearchTypeData.Sends.Texts -> null - SearchTypeData.Vault.All -> null - SearchTypeData.Vault.Cards -> null - SearchTypeData.Vault.SshKeys -> null - is SearchTypeData.Vault.Collection -> searchType.collectionId - is SearchTypeData.Vault.Folder -> searchType.folderId - SearchTypeData.Vault.Identities -> null - SearchTypeData.Vault.Logins -> null - SearchTypeData.Vault.NoFolder -> null - SearchTypeData.Vault.SecureNotes -> null - SearchTypeData.Vault.VerificationCodes -> null - SearchTypeData.Vault.Trash -> null - null -> null + every { + toSearchArgs() + } returns SearchArgs( + type = when (val searchType = initialState?.searchType) { + SearchTypeData.Sends.All -> SearchType.Sends.All + SearchTypeData.Sends.Files -> SearchType.Sends.Files + SearchTypeData.Sends.Texts -> SearchType.Sends.Texts + SearchTypeData.Vault.All -> SearchType.Vault.All + SearchTypeData.Vault.Cards -> SearchType.Vault.Cards + is SearchTypeData.Vault.Collection -> SearchType.Vault.Collection( + collectionId = searchType.collectionId, + ) + + is SearchTypeData.Vault.Folder -> SearchType.Vault.Folder( + folderId = searchType.folderId, + ) + + SearchTypeData.Vault.Identities -> SearchType.Vault.Identities + SearchTypeData.Vault.Logins -> SearchType.Vault.Logins + SearchTypeData.Vault.NoFolder -> SearchType.Vault.NoFolder + SearchTypeData.Vault.SecureNotes -> SearchType.Vault.SecureNotes + SearchTypeData.Vault.SshKeys -> SearchType.Vault.SshKeys + SearchTypeData.Vault.Trash -> SearchType.Vault.Trash + SearchTypeData.Vault.VerificationCodes -> SearchType.Vault.VerificationCodes + null -> SearchType.Vault.All }, ) }, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt index 221bad2943..f6c3c30d1d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsViewModelTest.kt @@ -9,12 +9,16 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class SettingsViewModelTest : BaseViewModelTest() { @@ -31,6 +35,16 @@ class SettingsViewModelTest : BaseViewModelTest() { every { specialCircumstance } returns null } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toSettingsArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toSettingsArgs) + } + @Test fun `on CloseClick should emit NavigateBack`() = runTest { val viewModel = createViewModel() @@ -175,7 +189,7 @@ class SettingsViewModelTest : BaseViewModelTest() { firstTimeActionManager = firstTimeManager, specialCircumstanceManager = specialCircumstanceManager, savedStateHandle = SavedStateHandle().apply { - set("isPreAuth", isPreAuth) + every { toSettingsArgs() } returns SettingsArgs(isPreAuth = isPreAuth) }, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt index b5c22e38be..37519cf131 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/loginapproval/LoginApprovalViewModelTest.kt @@ -22,10 +22,14 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Clock import java.time.Instant @@ -52,6 +56,16 @@ class LoginApprovalViewModelTest : BaseViewModelTest() { every { userStateFlow } returns mutableUserStateFlow } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toLoginApprovalArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toLoginApprovalArgs) + } + @Test fun `init should call getAuthRequestById when special circumstance is absent`() { createViewModel(state = null) @@ -415,9 +429,10 @@ class LoginApprovalViewModelTest : BaseViewModelTest() { clock = fixedClock, authRepository = mockAuthRepository, specialCircumstanceManager = mockSpecialCircumstanceManager, - savedStateHandle = SavedStateHandle() - .also { it["fingerprint"] = FINGERPRINT } - .apply { set("state", state) }, + savedStateHandle = SavedStateHandle().apply { + set("state", state) + every { toLoginApprovalArgs() } returns LoginApprovalArgs(fingerprint = FINGERPRINT) + }, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt index 33c9103776..61da7e2950 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.bitwarden.core.DateTime import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.ui.util.asText +import com.bitwarden.ui.util.concat import com.bitwarden.vault.FolderView import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.vault.repository.VaultRepository @@ -11,8 +13,6 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest -import com.bitwarden.ui.util.asText -import com.bitwarden.ui.util.concat import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType import io.mockk.coEvery import io.mockk.coVerify @@ -23,7 +23,9 @@ import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Instant @@ -37,6 +39,16 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { every { getVaultFolderStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableFoldersStateFlow } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toFolderAddEditArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toFolderAddEditArgs) + } + @Test fun `initial add state should be correct`() = runTest { val folderAddEditType = FolderAddEditType.AddItem @@ -733,18 +745,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() { private fun createSavedStateHandleWithState( state: FolderAddEditState? = DEFAULT_STATE, ) = SavedStateHandle().apply { - val folderAddEditType = state?.folderAddEditType - ?: FolderAddEditType.AddItem - + val folderAddEditType = state?.folderAddEditType ?: FolderAddEditType.AddItem set("state", state) - set( - "folder_add_edit_type", - when (folderAddEditType) { - FolderAddEditType.AddItem -> "add" - is FolderAddEditType.EditItem -> "edit" - }, + every { toFolderAddEditArgs() } returns FolderAddEditArgs( + folderAddEditType = folderAddEditType, + parentFolderName = null, ) - set("folder_edit_id", (folderAddEditType as? FolderAddEditType.EditItem)?.folderId) } private fun createViewModel( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt index 83adbee196..e09fe721a5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt @@ -12,11 +12,15 @@ import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.time.Clock import java.time.Instant @@ -47,6 +51,16 @@ class OtherViewModelTest : BaseViewModelTest() { every { isNetworkConnected } returns true } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toOtherArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toOtherArgs) + } + @Test fun `initial state should be correct when not set`() { val viewModel = createViewModel(state = null) @@ -211,7 +225,7 @@ class OtherViewModelTest : BaseViewModelTest() { vaultRepo = vaultRepository, savedStateHandle = SavedStateHandle().apply { set("state", state) - set("isPreAuth", state?.isPreAuth == true) + every { toOtherArgs() } returns OtherArgs(isPreAuth = state?.isPreAuth == true) }, networkConnectionManager = networkConnectionManager, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt index 4432780322..b5dd65222a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorViewModelTest.kt @@ -38,7 +38,9 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode import io.mockk.every import io.mockk.just import io.mockk.mockk +import io.mockk.mockkStatic import io.mockk.runs +import io.mockk.unmockkStatic import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -46,6 +48,7 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -134,6 +137,16 @@ class GeneratorViewModelTest : BaseViewModelTest() { } returns mutableShouldShowSimpleLoginSelfHostFlow } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toGeneratorArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toGeneratorArgs) + } + @Test fun `initial state should be correct when there is no saved state`() { val viewModel = createViewModel(state = null) @@ -161,9 +174,9 @@ class GeneratorViewModelTest : BaseViewModelTest() { includeNumber = false, ), ), - generatorMode = GeneratorMode.Modal.Username(website = ""), + generatorMode = GeneratorMode.Modal.Username(website = null), currentEmailAddress = "currentEmail", - website = "", + website = null, shouldShowCoachMarkTour = true, shouldShowAnonAddySelfHostServerUrlField = true, shouldShowSimpleLoginSelfHostServerField = true, @@ -171,8 +184,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { val viewModel = createViewModel( state = null, - type = "username_generator", - website = "", + type = GeneratorMode.Modal.Username(website = null), ) assertEquals(expected, viewModel.stateFlow.value) } @@ -207,7 +219,7 @@ class GeneratorViewModelTest : BaseViewModelTest() { val viewModel = createViewModel( state = null, - type = "password_generator", + type = GeneratorMode.Modal.Password, ) assertEquals(expected, viewModel.stateFlow.value) } @@ -2636,13 +2648,11 @@ class GeneratorViewModelTest : BaseViewModelTest() { private fun createViewModel( state: GeneratorState? = initialPasscodeState, - type: String? = null, - website: String? = null, + type: GeneratorMode = GeneratorMode.Default, ): GeneratorViewModel = createViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state) - set("generator_mode_type", type) - set("generator_website", website) + every { toGeneratorArgs() } returns GeneratorArgs(type = type) }, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt index 6230ece7fd..378a34a223 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/tools/feature/send/addsend/AddSendViewModelTest.kt @@ -93,6 +93,7 @@ class AddSendViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { mockkStatic( + SavedStateHandle::toAddSendArgs, AddSendState.ViewState.Content::toSendView, SendView::toSendUrl, SendView::toViewState, @@ -102,6 +103,7 @@ class AddSendViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { unmockkStatic( + SavedStateHandle::toAddSendArgs, AddSendState.ViewState.Content::toSendView, SendView::toSendUrl, SendView::toViewState, @@ -1016,15 +1018,8 @@ class AddSendViewModelTest : BaseViewModelTest() { ): AddSendViewModel = AddSendViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state?.copy(addSendType = addSendType)) - set( - "add_send_item_type", - when (addSendType) { - AddSendType.AddItem -> "add" - is AddSendType.EditItem -> "edit" - }, - ) - set("edit_send_id", (addSendType as? AddSendType.EditItem)?.sendItemId) set("activityToken", activityToken) + every { toAddSendArgs() } returns AddSendArgs(sendAddType = addSendType) }, authRepo = authRepository, environmentRepo = environmentRepository, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 572fa8ce86..8a88abe552 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -136,11 +136,12 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { private val loginInitialState = createVaultAddItemState( typeContentViewState = createLoginTypeContentViewState(), ) - private val loginInitialSavedStateHandle = createSavedStateHandleWithState( - state = loginInitialState, - vaultAddEditType = VaultAddEditType.AddItem, - vaultItemCipherType = VaultItemCipherType.LOGIN, - ) + private val loginInitialSavedStateHandle + get() = createSavedStateHandleWithState( + state = loginInitialState, + vaultAddEditType = VaultAddEditType.AddItem, + vaultItemCipherType = VaultItemCipherType.LOGIN, + ) private val totpTestCodeFlow: MutableSharedFlow = bufferedMutableSharedFlow() @@ -199,16 +200,22 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - mockkStatic(CipherView::toViewState) - mockkStatic(UUID::randomUUID) + mockkStatic( + SavedStateHandle::toVaultAddEditArgs, + CipherView::toViewState, + UUID::randomUUID, + ) mockkObject(ProviderCreateCredentialRequest.Companion) every { UUID.randomUUID().toString() } returns TEST_ID } @AfterEach fun tearDown() { - unmockkStatic(CipherView::toViewState) - unmockkStatic(UUID::randomUUID) + unmockkStatic( + SavedStateHandle::toVaultAddEditArgs, + CipherView::toViewState, + UUID::randomUUID, + ) unmockkObject(ProviderCreateCredentialRequest.Companion) } @@ -4691,24 +4698,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { vaultItemCipherType: VaultItemCipherType, ): SavedStateHandle = SavedStateHandle().apply { set("state", state) - set( - "vault_add_edit_type", - when (vaultAddEditType) { - is VaultAddEditType.AddItem -> "add" - is VaultAddEditType.EditItem -> "edit" - is VaultAddEditType.CloneItem -> "clone" - }, - ) - set("vault_edit_id", (vaultAddEditType as? VaultAddEditType.EditItem)?.vaultItemId) - set( - "vault_item_type", - when (vaultItemCipherType) { - VaultItemCipherType.LOGIN -> "login" - VaultItemCipherType.CARD -> "card" - VaultItemCipherType.IDENTITY -> "identity" - VaultItemCipherType.SECURE_NOTE -> "secure_note" - VaultItemCipherType.SSH_KEY -> "ssh_key" - }, + every { toVaultAddEditArgs() } returns VaultAddEditArgs( + vaultAddEditType = vaultAddEditType, + vaultItemCipherType = vaultItemCipherType, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt index 764527ed80..109a5bb32a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/attachments/AttachmentsViewModelTest.kt @@ -47,13 +47,19 @@ class AttachmentsViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - mockkStatic(CipherView::toViewState) + mockkStatic( + SavedStateHandle::toAttachmentsArgs, + CipherView::toViewState, + ) mockkStatic(Uri::class) } @AfterEach fun tearDown() { - unmockkStatic(CipherView::toViewState) + unmockkStatic( + SavedStateHandle::toAttachmentsArgs, + CipherView::toViewState, + ) unmockkStatic(Uri::class) } @@ -624,7 +630,9 @@ class AttachmentsViewModelTest : BaseViewModelTest() { vaultRepo = vaultRepository, savedStateHandle = SavedStateHandle().apply { set("state", initialState) - set("cipher_id", initialState?.cipherId ?: "mockId-1") + every { + toAttachmentsArgs() + } returns AttachmentsArgs(cipherId = initialState?.cipherId ?: "mockId-1") }, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt index 159dc90444..a276145aaf 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt @@ -48,7 +48,10 @@ class ImportLoginsViewModelTest : BaseViewModelTest() { @BeforeEach fun setUp() { - mockkStatic(Uri::parse) + mockkStatic( + SavedStateHandle::toImportLoginsArgs, + Uri::parse, + ) every { Uri.parse(Environment.Us.environmentUrlData.base) } returns mockk { every { host } returns DEFAULT_VAULT_URL } @@ -56,7 +59,10 @@ class ImportLoginsViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { - unmockkStatic(Uri::parse) + unmockkStatic( + SavedStateHandle::toImportLoginsArgs, + Uri::parse, + ) } private val snackbarRelayManager: SnackbarRelayManagerImpl = mockk { @@ -521,11 +527,9 @@ class ImportLoginsViewModelTest : BaseViewModelTest() { private fun createViewModel( snackbarRelay: SnackbarRelay = SnackbarRelay.MY_VAULT_RELAY, ): ImportLoginsViewModel = ImportLoginsViewModel( - savedStateHandle = SavedStateHandle( - mapOf( - "snackbarRelay" to snackbarRelay.name, - ), - ), + savedStateHandle = SavedStateHandle().apply { + every { toImportLoginsArgs() } returns ImportLoginsArgs(snackBarRelay = snackbarRelay) + }, vaultRepository = vaultRepository, firstTimeActionManager = firstTimeActionManager, environmentRepository = environmentRepository, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index fcfadc90cb..383e541f53 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -120,12 +120,18 @@ class VaultItemViewModelTest : BaseViewModelTest() { @BeforeEach fun setup() { - mockkStatic(CipherView::toViewState) + mockkStatic( + SavedStateHandle::toVaultItemArgs, + CipherView::toViewState, + ) } @AfterEach fun tearDown() { - unmockkStatic(CipherView::toViewState) + unmockkStatic( + SavedStateHandle::toVaultItemArgs, + CipherView::toViewState, + ) unmockkStatic(Uri::class) } @@ -3773,18 +3779,10 @@ class VaultItemViewModelTest : BaseViewModelTest() { ): VaultItemViewModel = VaultItemViewModel( savedStateHandle = SavedStateHandle().apply { set("state", state) - set("vault_item_id", vaultItemId) - set( - "vault_item_cipher_type", - when (vaultItemCipherType) { - VaultItemCipherType.LOGIN -> "login" - VaultItemCipherType.CARD -> "card" - VaultItemCipherType.IDENTITY -> "identity" - VaultItemCipherType.SECURE_NOTE -> "secure_note" - VaultItemCipherType.SSH_KEY -> "ssh_key" - }, - ) set("tempAttachmentFile", tempAttachmentFile) + every { + toVaultItemArgs() + } returns VaultItemArgs(vaultItemId = vaultItemId, cipherType = vaultItemCipherType) }, clipboardManager = bitwardenClipboardManager, authRepository = authRepository, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 19c34ee356..4d2f788644 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -213,9 +213,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } private val initialState = createVaultItemListingState() - private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType( - vaultItemListingType = VaultItemListingType.Login, - ) + private val initialSavedStateHandle + get() = createSavedStateHandleWithVaultItemListingType( + vaultItemListingType = VaultItemListingType.Login, + ) private val mockProviderGetCredentialRequest = mockk(relaxed = true) { every { @@ -236,6 +237,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { @BeforeEach fun setUp() { + mockkStatic(SavedStateHandle::toVaultItemListingArgs) mockkObject( ProviderCreateCredentialRequest.Companion, ProviderGetCredentialRequest.Companion, @@ -252,6 +254,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { @AfterEach fun tearDown() { + unmockkStatic(SavedStateHandle::toVaultItemListingArgs) unmockkObject( ProviderCreateCredentialRequest.Companion, ProviderGetCredentialRequest.Companion, @@ -307,7 +310,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { awaitItem(), ) } - } + } @Test fun `on LockAccountClick should call lockVault for the given account`() { @@ -4713,40 +4716,12 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) } - @Suppress("CyclomaticComplexMethod") private fun createSavedStateHandleWithVaultItemListingType( vaultItemListingType: VaultItemListingType, - ) = SavedStateHandle().apply { - set( - "vault_item_listing_type", - when (vaultItemListingType) { - is VaultItemListingType.Card -> "card" - is VaultItemListingType.Collection -> "collection" - is VaultItemListingType.Folder -> "folder" - is VaultItemListingType.Identity -> "identity" - is VaultItemListingType.Login -> "login" - is VaultItemListingType.SecureNote -> "secure_note" - is VaultItemListingType.Trash -> "trash" - is VaultItemListingType.SendFile -> "send_file" - is VaultItemListingType.SendText -> "send_text" - is VaultItemListingType.SshKey -> "ssh_key" - }, - ) - set( - "id", - when (vaultItemListingType) { - is VaultItemListingType.Card -> null - is VaultItemListingType.Collection -> vaultItemListingType.collectionId - is VaultItemListingType.Folder -> vaultItemListingType.folderId - is VaultItemListingType.Identity -> null - is VaultItemListingType.Login -> null - is VaultItemListingType.SecureNote -> null - is VaultItemListingType.Trash -> null - is VaultItemListingType.SendFile -> null - is VaultItemListingType.SendText -> null - is VaultItemListingType.SshKey -> null - }, - ) + ): SavedStateHandle = SavedStateHandle().apply { + every { + toVaultItemListingArgs() + } returns VaultItemListingArgs(vaultItemListingType = vaultItemListingType) } private fun setupMockUri() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt index 58b6b356f5..9f50ad6198 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt @@ -26,17 +26,20 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() { private val initialState = createVaultMoveToOrganizationState() - private val initialSavedStateHandle = createSavedStateHandleWithState( - state = initialState, - ) + private val initialSavedStateHandle + get() = createSavedStateHandleWithState(state = initialState) private val mutableVaultItemFlow = MutableStateFlow>(DataState.Loading) @@ -54,6 +57,16 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() { every { userStateFlow } returns mutableUserStateFlow } + @BeforeEach + fun setup() { + mockkStatic(SavedStateHandle::toVaultMoveToOrganizationArgs) + } + + @AfterEach + fun tearDown() { + unmockkStatic(SavedStateHandle::toVaultMoveToOrganizationArgs) + } + @Test fun `initial state should be correct when state is null`() = runTest { val viewModel = createViewModel( @@ -456,8 +469,10 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() { showOnlyCollections: Boolean = false, ) = SavedStateHandle().apply { set("state", state) - set("vault_move_to_organization_id", vaultItemId) - set("vault_move_to_organization_only_collections", "$showOnlyCollections") + every { toVaultMoveToOrganizationArgs() } returns VaultMoveToOrganizationArgs( + vaultItemId = vaultItemId, + showOnlyCollections = showOnlyCollections, + ) } @Suppress("MaxLineLength") diff --git a/detekt-config.yml b/detekt-config.yml index 5f0b70a520..bf9bdd506d 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -399,6 +399,7 @@ naming: rootPackage: '' MatchingDeclarationName: active: true + excludes: [ '**/ui/**/*Navigation.kt' ] mustBeFirst: true MemberNameEqualsClassName: active: true diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 281f0bbd2c..c58958e774 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -400,27 +400,24 @@ Note in particular how consumers of the above **do not need to know the details Show example ```kotlin -private const val IS_TOGGLE_ENABLED: String = "is_toggle_enabled" -private const val EXAMPLE_ROUTE_PREFIX = "example" -private const val EXAMPLE_ROUTE = "$EXAMPLE_ROUTE_PREFIX/{$IS_TOGGLE_ENABLED}" +@Serializable +data class ExampleRoute( + val isToggleEnabled: Boolean, +) data class ExampleArgs( val isToggleEnabledInitialValue: Boolean, -) { - constructor(savedStateHandle: SavedStateHandle) : this( - isToggleEnabledInitialValue = checkNotNull(savedStateHandle[IS_TOGGLE_ENABLED]) as Boolean, - ) +) + +fun SavedStateHandle.toExampleArgs(): ExampleArgs { + val route = this.toRoute() + return ExampleArgs(isToggleEnabledInitialValue = route.isToggleEnabled) } fun NavGraphBuilder.exampleDestination( onNavigateToNextScreen: (CompletionData) -> Unit, ) { - composableWithSlideTransitions( - route = EXAMPLE_ROUTE, - arguments = listOf( - navArgument(IS_TOGGLE_ENABLED) { type = NavType.BoolType } - ), - ) { + composableWithSlideTransitions { ExampleScreen(onNavigateToNextScreen = onNavigateToNextScreen) } } @@ -430,8 +427,8 @@ fun NavController.navigateToExample( navOptions: NavOptions? = null, ) { this.navigate( - "$EXAMPLE_ROUTE_PREFIX/$isToggleEnabled", - navOptions, + route = ExampleRoute(isToggleEnabled = isToggleEnabled), + navOptions = navOptions, ) } ```