PM-21255: Implement type-safe navigation (#5131)

This commit is contained in:
David Perez 2025-05-06 15:46:53 -05:00 committed by GitHub
parent 1d68c1fdf6
commit 6fec95cb84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
122 changed files with 1941 additions and 1515 deletions

View File

@ -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<SetupAutofillRoute.AsRoot>()
?: this.toObjectRoute<SetupAutofillRoute.Standard>())
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<SetupAutofillRoute.Standard> {
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<SetupAutofillRoute.AsRoot> {
SetupAutoFillScreen(
onNavigateBack = {
// No-Op
@ -72,9 +89,3 @@ fun NavGraphBuilder.setupAutoFillDestinationAsRoot() {
)
}
}
private val setupAutofillNavArgs = listOf(
navArgument(SETUP_AUTO_FILL_NAV_ARG) {
type = NavType.BoolType
},
)

View File

@ -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,

View File

@ -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<SetupCompleteRoute> {
SetupCompleteScreen()
}
}

View File

@ -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<SetupUnlockRoute.AsRoot>()
?: this.toObjectRoute<SetupUnlockRoute.Standard>()
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<SetupUnlockRoute.Standard> {
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<SetupUnlockRoute.AsRoot> {
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
},
),
)

View File

@ -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

View File

@ -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<AuthGraphRoute>(
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)
}

View File

@ -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<String>(EMAIL)),
)
)
/**
* Constructs a [CheckEmailArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toCheckEmailArgs(): CheckEmailArgs {
val route = this.toRoute<CheckEmailRoute>()
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<CheckEmailRoute> {
CheckEmailScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -21,7 +21,7 @@ class CheckEmailViewModel @Inject constructor(
) : BaseViewModel<CheckEmailState, CheckEmailEvent, CheckEmailAction>(
initialState = savedStateHandle[KEY_STATE]
?: CheckEmailState(
email = CheckEmailArgs(savedStateHandle).emailAddress,
email = savedStateHandle.toCheckEmailArgs().emailAddress,
),
) {
init {

View File

@ -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<String>(EMAIL_ADDRESS)),
verificationToken = checkNotNull(savedStateHandle.get<String>(VERIFICATION_TOKEN)),
fromEmail = checkNotNull(savedStateHandle.get<Boolean>(FROM_EMAIL)),
)
/**
* Constructs a [CompleteRegistrationArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toCompleteRegistrationArgs(): CompleteRegistrationArgs {
val route = this.toRoute<CompleteRegistrationRoute>()
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<CompleteRegistrationRoute> {
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)
}

View File

@ -57,7 +57,7 @@ class CompleteRegistrationViewModel @Inject constructor(
private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val args = CompleteRegistrationArgs(savedStateHandle)
val args = savedStateHandle.toCompleteRegistrationArgs()
CompleteRegistrationState(
userEmail = args.emailAddress,
emailVerificationToken = args.verificationToken,

View File

@ -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<CreateAccountRoute> {
CreateAccountScreen(
onNavigateBack = onNavigateBack,
onNavigateToLogin = onNavigateToLogin,

View File

@ -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<EnterpriseSignOnRoute>()
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<EnterpriseSignOnRoute> {
EnterpriseSignOnScreen(
onNavigateBack = onNavigateBack,
onNavigateToSetPassword = onNavigateToSetPassword,

View File

@ -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),

View File

@ -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<EnvironmentRoute> {
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)
}

View File

@ -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<ExpiredRegistrationLinkRoute> {
ExpiredRegistrationLinkScreen(
onNavigateBack = onNavigateBack,
onNavigateToStartRegistration = onNavigateToStartRegistration,

View File

@ -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<LandingRoute> {
LandingScreen(
onNavigateToCreateAccount = onNavigateToCreateAccount,
onNavigateToLogin = onNavigateToLogin,

View File

@ -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<LoginRoute>()
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<LoginRoute> {
LoginScreen(
onNavigateBack = onNavigateBack,
onNavigateToMasterPasswordHint = onNavigateToMasterPasswordHint,

View File

@ -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<LoginState, LoginEvent, LoginAction>(
// 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 {

View File

@ -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<String>(EMAIL_ADDRESS)),
loginType = checkNotNull(savedStateHandle.get<LoginWithDeviceType>(LOGIN_TYPE)),
)
)
/**
* Constructs a [LoginWithDeviceArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toLoginWithDeviceArgs(): LoginWithDeviceArgs {
val route = this.toRoute<LoginWithDeviceRoute>()
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<LoginWithDeviceRoute> {
LoginWithDeviceScreen(
onNavigateBack = onNavigateBack,
onNavigateToTwoFactorLogin = onNavigateToTwoFactorLogin,

View File

@ -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<LoginWithDeviceState, LoginWithDeviceEvent, LoginWithDeviceAction>(
initialState = savedStateHandle[KEY_STATE]
?: run {
val args = LoginWithDeviceArgs(savedStateHandle)
val args = savedStateHandle.toLoginWithDeviceArgs()
LoginWithDeviceState(
loginWithDeviceType = args.loginType,
emailAddress = args.emailAddress,

View File

@ -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<MasterPasswordGeneratorRoute> {
MasterPasswordGeneratorScreen(
onNavigateBack = onNavigateBack,
onNavigateToPreventLockout = onNavigateToPreventLockout,

View File

@ -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<MasterPasswordGuidanceRoute> {
MasterPasswordGuidanceScreen(
onNavigateBack = onNavigateBack,
onNavigateToGeneratePassword = onNavigateToGeneratePassword,

View File

@ -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<MasterPasswordHintRoute>()
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<MasterPasswordHintRoute> {
MasterPasswordHintScreen(onNavigateBack = onNavigateBack)
}
}

View File

@ -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<MasterPasswordHintState, MasterPasswordHintEvent, MasterPasswordHintAction>(
initialState = savedStateHandle[KEY_STATE]
?: MasterPasswordHintState(
emailInput = MasterPasswordHintArgs(savedStateHandle).emailAddress,
emailInput = savedStateHandle.toMasterPasswordHintArgs().emailAddress,
),
) {
init {

View File

@ -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<PreventAccountLockoutRoute> {
PreventAccountLockoutScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -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<RemovePasswordRoute> {
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)
}

View File

@ -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<ResetPasswordRoute> {
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)
}

View File

@ -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<SetPasswordRoute> {
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)
}

View File

@ -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<StartRegistrationRoute> {
StartRegistrationScreen(
onNavigateBack = onNavigateBack,
onNavigateToCompleteRegistration = onNavigateToCompleteRegistration,

View File

@ -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<TrustedDeviceGraphRoute>(
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)
}

View File

@ -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<TrustedDeviceRoute> {
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)
}

View File

@ -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<String>(PASSWORD)?.base64UrlDecodeOrNull(),
orgIdentifier = savedStateHandle.get<String>(ORG_IDENTIFIER)?.base64UrlDecodeOrNull(),
isNewDeviceVerification = savedStateHandle.get<Boolean>(NEW_DEVICE_VERIFICATION) ?: false,
)
/**
* Constructs a [TwoFactorLoginArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toTwoFactorLoginArgs(): TwoFactorLoginArgs {
val route = this.toRoute<TwoFactorLoginRoute>()
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<TwoFactorLoginRoute> {
TwoFactorLoginScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -57,7 +57,7 @@ class TwoFactorLoginViewModel @Inject constructor(
) : BaseViewModel<TwoFactorLoginState, TwoFactorLoginEvent, TwoFactorLoginAction>(
initialState = savedStateHandle[KEY_STATE]
?: run {
val args = TwoFactorLoginArgs(savedStateHandle)
val args = savedStateHandle.toTwoFactorLoginArgs()
TwoFactorLoginState(
authMethod = authRepository.twoFactorResponse.preferredAuthMethod,
availableAuthMethods = authRepository.twoFactorResponse.availableAuthMethods,

View File

@ -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<UnlockType>(VAULT_UNLOCK_TYPE)),
)
)
/**
* Constructs a [VaultUnlockArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toVaultUnlockArgs(): VaultUnlockArgs {
val route = this.toObjectRoute<VaultUnlockRoute.Tde>()
?: this.toObjectRoute<VaultUnlockRoute.Standard>()
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<VaultUnlockRoute.Standard> {
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<VaultUnlockRoute.Tde> {
VaultUnlockScreen()
}
}

View File

@ -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(

View File

@ -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<WelcomeRoute> {
WelcomeScreen(
onNavigateToCreateAccount = onNavigateToCreateAccount,
onNavigateToLogin = onNavigateToLogin,

View File

@ -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<NamedNavArgument> = emptyList(),
inline fun <reified T : Any> NavGraphBuilder.composableWithSlideTransitions(
typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
this.composable<T>(
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<NamedNavArgument> = emptyList(),
inline fun <reified T : Any> NavGraphBuilder.composableWithStayTransitions(
typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
this.composable<T>(
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<NamedNavArgument> = emptyList(),
inline fun <reified T : Any> NavGraphBuilder.composableWithPushTransitions(
typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
this.composable<T>(
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<NamedNavArgument> = emptyList(),
inline fun <reified T : Any> NavGraphBuilder.composableWithRootPushTransitions(
typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
) {
this.composable(
route = route,
arguments = arguments,
this.composable<T>(
typeMap = typeMap,
deepLinks = deepLinks,
enterTransition = TransitionProviders.Enter.stay,
exitTransition = TransitionProviders.Exit.pushLeft,
popEnterTransition = TransitionProviders.Enter.pushRight,
popExitTransition = TransitionProviders.Exit.fadeOut,
sizeTransform = null,
content = content,
)
}

View File

@ -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<DebugRoute> {
DebugMenuScreen(onNavigateBack = onNavigateBack)
// If we are displaying the debug screen, then we can just hide the splash screen.
onSplashScreenRemoved()

View File

@ -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<NavBackStackEntry>.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<NavBackStackEntry>.toEnterTransition(
* Define the exit transition for each route.
*/
@Suppress("MaxLineLength")
private fun AnimatedContentTransitionScope<NavBackStackEntry>.toExitTransition(): NonNullExitTransitionProvider =
when (initialState.destination.rootLevelRoute()) {
private fun AnimatedContentTransitionScope<NavBackStackEntry>.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
}
}
}

View File

@ -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<String>(SEARCH_TYPE)),
id = savedStateHandle.get<String>(SEARCH_TYPE_ID),
),
)
/**
* Constructs a [SearchArgs] from the [SavedStateHandle] and internal route data.
*/
@Suppress("CyclomaticComplexMethod")
fun SavedStateHandle.toSearchArgs(): SearchArgs {
val route = this.toRoute<SearchRoute>()
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<SearchRoute> {
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? =

View File

@ -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)

View File

@ -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<SettingsRoute.PreAuth>()
?: this.toObjectRoute<SettingsRoute.Standard>()
return route
?.let { SettingsArgs(isPreAuth = it.isPreAuth) }
?: this.toObjectRoute<SettingsGraphRoute>()?.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<SettingsGraphRoute>(
startDestination = SettingsRoute.Standard,
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE,
arguments = listOf(
navArgument(name = IS_PRE_AUTH) {
type = NavType.BoolType
defaultValue = false
},
),
) {
composableWithRootPushTransitions<SettingsRoute.Standard> {
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<SettingsRoute.PreAuth> {
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)
}

View File

@ -31,7 +31,7 @@ class SettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<SettingsState, SettingsEvent, SettingsAction>(
initialState = SettingsState(
isPreAuth = SettingsArgs(savedStateHandle = savedStateHandle).isPreAuth,
isPreAuth = savedStateHandle.toSettingsArgs().isPreAuth,
securityCount = firstTimeActionManager.allSecuritySettingsBadgeCountFlow.value,
autoFillCount = firstTimeActionManager.allAutofillSettingsBadgeCountFlow.value,
vaultCount = firstTimeActionManager.allVaultSettingsBadgeCountFlow.value,

View File

@ -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<SettingsAboutRoute.PreAuth> {
AboutScreen(
onNavigateBack = onNavigateBack,
onNavigateToFlightRecorder = onNavigateToFlightRecorder,
onNavigateToRecordedLogs = onNavigateToRecordedLogs,
)
}
} else {
composableWithPushTransitions<SettingsAboutRoute.Standard> {
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

View File

@ -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<AccountSecurityRoute> {
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)
}

View File

@ -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<DeleteAccountRoute> {
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)
}

View File

@ -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<DeleteAccountConfirmationRoute> {
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)
}

View File

@ -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<String>(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<LoginApprovalRoute>()
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<LoginApprovalRoute> {
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)
}

View File

@ -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 = "",

View File

@ -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<PendingRequestsRoute> {
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)
}

View File

@ -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<SettingsAppearanceRoute.PreAuth> {
AppearanceScreen(onNavigateBack = onNavigateBack)
}
} else {
composableWithPushTransitions<SettingsAppearanceRoute.Standard> {
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

View File

@ -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<AutofillRoute> {
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)
}

View File

@ -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<BlockAutofillSettingsRoute> {
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)
}

View File

@ -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<ExportVaultRoute> {
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)
}

View File

@ -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<FlightRecorderRoute.PreAuth> {
FlightRecorderScreen(
onNavigateBack = onNavigateBack,
)
}
} else {
composableWithSlideTransitions<FlightRecorderRoute.Standard> {
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

View File

@ -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<RecordedLogsRoute.PreAuth> {
RecordedLogsScreen(
onNavigateBack = onNavigateBack,
)
}
} else {
composableWithSlideTransitions<RecordedLogsRoute.Standard> {
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
}

View File

@ -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<FoldersRoute> {
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)
}

View File

@ -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<FolderAddEditRoute>()
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<FolderAddEditRoute> {
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? =

View File

@ -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) {

View File

@ -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<SettingsOtherRoute.PreAuth>()
?: this.toObjectRoute<SettingsOtherRoute.Standard>()
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<SettingsOtherRoute.PreAuth> {
OtherScreen(onNavigateBack = onNavigateBack)
}
} else {
composableWithPushTransitions<SettingsOtherRoute.Standard> {
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

View File

@ -40,7 +40,7 @@ class OtherViewModel @Inject constructor(
) : BaseViewModel<OtherState, OtherEvent, OtherAction>(
initialState = savedStateHandle[KEY_STATE]
?: OtherState(
isPreAuth = OtherArgs(savedStateHandle = savedStateHandle).isPreAuth,
isPreAuth = savedStateHandle.toOtherArgs().isPreAuth,
allowScreenCapture = settingsRepo.isScreenCaptureAllowed,
allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value,
clearClipboardFrequency = settingsRepo.clearClipboardFrequency,

View File

@ -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<VaultSettingsRoute> {
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)
}

View File

@ -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<SplashRoute> { SplashScreen() }
}
/**
@ -23,5 +28,5 @@ fun NavGraphBuilder.splashDestination() {
fun NavController.navigateToSplash(
navOptions: NavOptions? = null,
) {
navigate(SPLASH_ROUTE, navOptions)
navigate(SplashRoute, navOptions)
}

View File

@ -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<VaultUnlockedGraphRoute>(
startDestination = VaultUnlockedNavbarRoute,
) {
vaultItemListingDestinationAsRoot(
onNavigateBack = { navController.popBackStack() },

View File

@ -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<VaultUnlockedNavbarRoute> {
VaultUnlockedNavBarScreen(
onNavigateToVaultAddItem = onNavigateToVaultAddItem,
onNavigateToVaultItem = onNavigateToVaultItem,

View File

@ -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,

View File

@ -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"
}
}

View File

@ -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,

View File

@ -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 : Any> T.toObjectNavigationRoute(): String = this::class.toObjectKClassNavigationRoute()
/**
* Gets the route string for a [KClass] of an object.
*/
@OptIn(InternalSerializationApi::class)
fun <T : Any> KClass<T>.toObjectKClassNavigationRoute(): String =
this.serializer().descriptor.serialName

View File

@ -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 <reified T : Any> SavedStateHandle.toObjectRoute(): T? =
this
.get<Intent>(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<T>()
}

View File

@ -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<GeneratorGraphRoute>(
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)
}

View File

@ -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<String>(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<GeneratorRoute.Modal>()
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<GeneratorRoute.Standard> {
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<GeneratorRoute.Modal> {
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,
)
}

View File

@ -86,7 +86,7 @@ class GeneratorViewModel @Inject constructor(
private val featureFlagManager: FeatureFlagManager,
) : BaseViewModel<GeneratorState, GeneratorEvent, GeneratorAction>(
initialState = savedStateHandle[KEY_STATE] ?: run {
val generatorMode = GeneratorArgs(savedStateHandle).type
val generatorMode = savedStateHandle.toGeneratorArgs().type
GeneratorState(
generatedText = NO_GENERATED_TEXT,
selectedType = when (generatorMode) {

View File

@ -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<PasswordHistoryRoute>()
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<PasswordHistoryRoute> {
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
}

View File

@ -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,
)
},

View File

@ -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<SendGraphRoute>(
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)
}

View File

@ -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<SendRoute> {
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)
}

View File

@ -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<String>(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<AddSendRoute>()
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<AddSendRoute>()
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<AddSendRoute> {
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

View File

@ -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,

View File

@ -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<VaultAddEditRoute>()
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<String>(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<VaultAddEditRoute> {
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!",
)
}

View File

@ -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

View File

@ -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<String>(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<AttachmentsRoute>()
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<AttachmentsRoute> {
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,
)
}

View File

@ -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,

View File

@ -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<ImportLoginsRoute>()
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<ImportLoginsRoute> {
ImportLoginsScreen(
onNavigateBack = onNavigateBack,
)

View File

@ -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,
)
},
) {

View File

@ -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<String>(VAULT_ITEM_ID)),
cipherType = requireNotNull(savedStateHandle.get<String>(VAULT_ITEM_CIPHER_TYPE))
.toVaultItemCipherType(),
)
)
/**
* Constructs a [VaultItemArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toVaultItemArgs(): VaultItemArgs {
val route = this.toRoute<VaultItemRoute>()
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<VaultItemRoute> {
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!",
)
}

View File

@ -76,7 +76,7 @@ class VaultItemViewModel @Inject constructor(
) : BaseViewModel<VaultItemState, VaultItemEvent, VaultItemAction>(
// 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,

View File

@ -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<VaultItemListingRoute.SendItemListing>()
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<VaultItemListingRoute.CipherItemListing>(
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<VaultItemListingRoute.AsRoot> {
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<VaultItemListingRoute.SendItemListing>(
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 <reified T : VaultItemListingRoute> 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<T> {
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()
}
}

View File

@ -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,

View File

@ -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<ManualCodeEntryRoute> {
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)
}

View File

@ -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<VaultMoveToOrganizationRoute>()
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<VaultMoveToOrganizationRoute> {
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,
)
}

View File

@ -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<VaultMoveToOrganizationState, VaultMoveToOrganizationEvent, VaultMoveToOrganizationAction>(
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,
)

View File

@ -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<QrCodeScanRoute> {
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)
}

View File

@ -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<VaultGraphRoute>(
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)
}

View File

@ -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<VaultRoute> {
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)
}

View File

@ -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<VerificationCodeRoute> {
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)
}

View File

@ -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 {
/**

View File

@ -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,

View File

@ -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,

View File

@ -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<FeatureFlagManager>(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,
)

Some files were not shown because too many files have changed in this diff Show More