mirror of
https://github.com/bitwarden/android.git
synced 2026-06-07 23:58:03 -05:00
Add initial Landing screen & Login nav graph (#19)
This commit is contained in:
committed by
Álison Fernandes
parent
6212ef8fa9
commit
24c7dade1e
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user