Add initial Landing screen & Login nav graph (#19)

This commit is contained in:
Caleb Derosier
2023-08-31 08:41:56 -05:00
committed by Álison Fernandes
parent 6212ef8fa9
commit 24c7dade1e
8 changed files with 398 additions and 34 deletions

View File

@@ -0,0 +1,24 @@
package com.x8bit.bitwarden.ui.feature.landing
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
const val LANDING_ROUTE: String = "landing"
/**
* Navigate to the landing screen.
*/
fun NavController.navigateToLanding(navOptions: NavOptions? = null) {
this.navigate(LANDING_ROUTE, navOptions)
}
/**
* Add the Landing screen to the nav graph.
*/
fun NavGraphBuilder.landingDestination(onNavigateToCreateAccount: () -> Unit) {
composable(route = LANDING_ROUTE) {
LandingScreen(onNavigateToCreateAccount = onNavigateToCreateAccount)
}
}

View File

@@ -0,0 +1,135 @@
package com.x8bit.bitwarden.ui.feature.landing
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.base.util.EventsEffect
import com.x8bit.bitwarden.ui.components.BitwardenTextField
/**
* The top level composable for the Landing screen.
*/
@Composable
@Suppress("LongMethod")
fun LandingScreen(
onNavigateToCreateAccount: () -> Unit,
viewModel: LandingViewModel = viewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
LandingEvent.NavigateToCreateAccount -> onNavigateToCreateAccount()
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp),
) {
Image(
painter = painterResource(id = R.drawable.logo_legacy),
contentDescription = null,
modifier = Modifier
.padding(start = 48.dp, top = 48.dp, end = 48.dp)
.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.log_in_or_create_account),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(horizontal = 24.dp),
)
BitwardenTextField(label = state.initialEmailAddress)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.remember_me),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall,
)
Switch(
checked = state.isRememberMeEnabled,
onCheckedChange = {
viewModel.trySendAction(LandingAction.RememberMeToggle(it))
},
)
}
Button(
onClick = { viewModel.trySendAction(LandingAction.ContinueButtonClick) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.testTag("Continue button"),
enabled = state.isContinueButtonEnabled,
) {
Text(
text = stringResource(id = R.string.continue_button),
color = MaterialTheme.colorScheme.onPrimary,
style = MaterialTheme.typography.bodyMedium,
)
}
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.new_around_here),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall,
)
Text(
modifier = Modifier
.clickable {
viewModel.trySendAction(LandingAction.CreateAccountClick)
}
.padding(start = 2.dp),
text = stringResource(id = R.string.create_account),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall,
)
}
}
}

View File

@@ -0,0 +1,84 @@
package com.x8bit.bitwarden.ui.feature.landing
import com.x8bit.bitwarden.ui.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* Manages application state for the initial landing screen.
*/
@HiltViewModel
class LandingViewModel @Inject constructor() :
BaseViewModel<LandingState, LandingEvent, LandingAction>(
initialState = LandingState(
initialEmailAddress = "",
isContinueButtonEnabled = true,
isRememberMeEnabled = false,
),
) {
override fun handleAction(action: LandingAction) {
when (action) {
LandingAction.ContinueButtonClick -> handleContinueButtonClicked()
LandingAction.CreateAccountClick -> handleCreateAccountClicked()
is LandingAction.RememberMeToggle -> handleRememberMeToggled(action)
}
}
private fun handleContinueButtonClicked() {
mutableStateFlow.value = mutableStateFlow.value.copy(
isContinueButtonEnabled = false,
)
}
private fun handleCreateAccountClicked() {
sendEvent(LandingEvent.NavigateToCreateAccount)
}
private fun handleRememberMeToggled(action: LandingAction.RememberMeToggle) {
mutableStateFlow.value = mutableStateFlow.value.copy(
isRememberMeEnabled = action.isChecked,
)
}
}
/**
* Models state of the landing screen.
*/
data class LandingState(
val initialEmailAddress: String,
val isContinueButtonEnabled: Boolean,
val isRememberMeEnabled: Boolean,
)
/**
* Models events for the landing screen.
*/
sealed class LandingEvent {
/**
* Navigates to the Create Account screen.
*/
data object NavigateToCreateAccount : LandingEvent()
}
/**
* Models actions for the landing screen.
*/
sealed class LandingAction {
/**
* Indicates that the continue button has been clicked and the app should navigate to Login.
*/
data object ContinueButtonClick : LandingAction()
/**
* Indicates that the Create Account text was clicked.
*/
data object CreateAccountClick : LandingAction()
/**
* Indicates that the Remember Me switch has been toggled.
*/
data class RememberMeToggle(
val isChecked: Boolean,
) : LandingAction()
}

View File

@@ -0,0 +1,40 @@
package com.x8bit.bitwarden.ui.feature.login
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.feature.createaccount.createAccountDestinations
import com.x8bit.bitwarden.ui.feature.createaccount.navigateToCreateAccount
import com.x8bit.bitwarden.ui.feature.landing.LANDING_ROUTE
import com.x8bit.bitwarden.ui.feature.landing.landingDestination
const val LOGIN_ROUTE: String = "login"
/**
* Add login destinations to the nav graph.
*/
fun NavGraphBuilder.loginDestinations(navController: NavHostController) {
navigation(
startDestination = LANDING_ROUTE,
route = LOGIN_ROUTE,
) {
createAccountDestinations()
landingDestination(
onNavigateToCreateAccount = { navController.navigateToCreateAccount() },
)
}
}
/**
* Navigate to the login screen. Note this will only work if login destination was added
* via [loginDestinations].
*/
fun NavController.navigateToLoginAsRoot() {
navigate(LANDING_ROUTE) {
// When changing root navigation state, pop everything else off the back stack:
popUpTo(graph.id) {
inclusive = true
}
}
}

View File

@@ -10,7 +10,8 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.x8bit.bitwarden.ui.components.PlaceholderComposable
import com.x8bit.bitwarden.ui.feature.createaccount.CreateAccountScreen
import com.x8bit.bitwarden.ui.feature.login.loginDestinations
import com.x8bit.bitwarden.ui.feature.login.navigateToLoginAsRoot
/**
* Controls root level [NavHost] for the app.
@@ -27,7 +28,7 @@ fun RootNavScreen(
startDestination = SplashRoute,
) {
splashDestinations()
loginDestinations()
loginDestinations(navController)
}
// When state changes, navigate to different root navigation state
@@ -75,35 +76,3 @@ private fun NavController.navigateToSplashAsRoot() {
}
}
}
/**
* TODO move to login package(BIT-146)
*/
@Suppress("TopLevelPropertyNaming")
private const val LoginRoute = "login"
/**
* Add login destinations to the nav graph.
*
* TODO: move to login package (BIT-146)
*/
private fun NavGraphBuilder.loginDestinations() {
composable(LoginRoute) {
CreateAccountScreen()
}
}
/**
* Navigate to the splash screen. Note this will only work if login destination was added
* via [loginDestinations].
*
* TODO: move to login package (BIT-146)
*/
private fun NavController.navigateToLoginAsRoot() {
navigate(LoginRoute) {
// When changing root navigation state, pop everything else off the back stack:
popUpTo(graph.id) {
inclusive = true
}
}
}