diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index f49eefc32..016a3796a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -2,29 +2,39 @@ package com.x8bit.bitwarden.ui.auth.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.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Button +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll 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.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch +import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField /** @@ -45,103 +55,109 @@ fun LandingScreen( } } + val scrollState = rememberScrollState() Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 16.dp), + .fillMaxHeight() + .padding(horizontal = 16.dp) + .verticalScroll(scrollState), ) { Image( - painter = painterResource(id = R.drawable.logo_legacy), + painter = painterResource(id = R.drawable.logo), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), contentDescription = null, modifier = Modifier - .padding(start = 48.dp, top = 48.dp, end = 48.dp) + .padding(top = 40.dp, bottom = 8.dp) + .width(220.dp) + .height(74.dp) .fillMaxWidth(), ) + Spacer(modifier = Modifier.weight(1f)) + Text( text = stringResource(id = R.string.login_or_create_new_account), - color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier - .align(alignment = Alignment.CenterHorizontally) - .padding(horizontal = 24.dp), + .padding(8.dp) + .wrapContentHeight(), ) + Spacer(modifier = Modifier.weight(1f)) + BitwardenTextField( modifier = Modifier - .fillMaxWidth() - .testTag("Email address"), + .padding( + top = 32.dp, + bottom = 10.dp, + ) + .fillMaxWidth(), value = state.emailInput, - onValueChange = { viewModel.trySendAction(LandingAction.EmailInputChanged(it)) }, + onValueChange = remember(viewModel) { + { viewModel.trySendAction(LandingAction.EmailInputChanged(it)) } + }, label = stringResource(id = R.string.email_address), ) - 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( - modifier = Modifier.testTag("Remember me"), - checked = state.isRememberMeEnabled, - onCheckedChange = { - viewModel.trySendAction(LandingAction.RememberMeToggle(it)) - }, - ) - } - - Button( - onClick = { - viewModel.trySendAction(LandingAction.ContinueButtonClick) + BitwardenSwitch( + label = stringResource(id = R.string.remember_me), + isChecked = state.isRememberMeEnabled, + onCheckedChange = remember(viewModel) { + { viewModel.trySendAction(LandingAction.RememberMeToggle(it)) } }, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .testTag("Continue button"), - enabled = state.isContinueButtonEnabled, - ) { - Text( - text = stringResource(id = R.string.continue_text), - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.bodyMedium, - ) - } + .padding(top = 8.dp) + .fillMaxWidth(), + ) + + BitwardenFilledButton( + label = stringResource(id = R.string.continue_text), + onClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.ContinueButtonClick) } + }, + isEnabled = state.isContinueButtonEnabled, + modifier = Modifier + .padding(top = 32.dp) + .fillMaxWidth(), + ) Row( - horizontalArrangement = Arrangement.Start, + horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .padding( + top = 8.dp, + bottom = 58.dp, + ) .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 16.dp), + .wrapContentHeight(), ) { Text( text = stringResource(id = R.string.new_around_here), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, ) - 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, + BitwardenTextButton( + label = stringResource(id = R.string.create_account), + onClick = remember(viewModel) { + { viewModel.trySendAction(LandingAction.CreateAccountClick) } + }, ) } } } + +@Preview +@Composable +private fun LandingScreen_preview() { + LandingScreen( + onNavigateToCreateAccount = {}, + onNavigateToLogin = {}, + viewModel = LandingViewModel(SavedStateHandle()), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenFilledButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenFilledButton.kt new file mode 100644 index 000000000..618640374 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenFilledButton.kt @@ -0,0 +1,62 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +/** + * Represents a Bitwarden-styled filled [Button]. + * + * @param label The label for the button. + * @param onClick The callback when the button is clicked. + * @param modifier The [Modifier] to be applied to the button. + * @param isEnabled Whether or not the button is enabled. + */ +@Composable +fun BitwardenFilledButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = isEnabled, + ) { + Text( + text = label, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding( + vertical = 10.dp, + horizontal = 24.dp, + ), + ) + } +} + +@Preview +@Composable +private fun BitwardenFilledButton_preview_isEnabled() { + BitwardenFilledButton( + label = "Label", + onClick = {}, + isEnabled = true, + ) +} + +@Preview +@Composable +private fun BitwardenFilledButton_preview_isNotEnabled() { + BitwardenFilledButton( + label = "Label", + onClick = {}, + isEnabled = false, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenSwitch.kt new file mode 100644 index 000000000..f2bf9ffb3 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenSwitch.kt @@ -0,0 +1,76 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +/** + * Represents a Bitwarden-styled [Switch]. + * + * @param label The label for the switch. + * @param isChecked Whether or not the switch is currently checked. + * @param onCheckedChange A callback for when the checked state changes. + * @param modifier The [Modifier] to be applied to the button. + */ +@Composable +fun BitwardenSwitch( + label: String, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .semantics(mergeDescendants = true) { } + .wrapContentHeight(), + ) { + Switch( + modifier = Modifier + .height(32.dp) + .width(52.dp), + checked = isChecked, + onCheckedChange = onCheckedChange, + ) + + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(start = 16.dp), + ) + } +} + +@Preview +@Composable +private fun BitwardenSwitch_preview_isChecked() { + BitwardenSwitch( + label = "Label", + isChecked = true, + onCheckedChange = {}, + ) +} + +@Preview +@Composable +private fun BitwardenSwitch_preview_isNotChecked() { + BitwardenSwitch( + label = "Label", + isChecked = false, + onCheckedChange = {}, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextButton.kt new file mode 100644 index 000000000..a408dbae1 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextButton.kt @@ -0,0 +1,49 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +/** + * Represents a Bitwarden-styled [TextButton]. + * + * @param label The label for the button. + * @param onClick The callback when the button is clicked. + * @param modifier The [Modifier] to be applied to the button. + */ +@Composable +fun BitwardenTextButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TextButton( + onClick = onClick, + modifier = modifier, + ) { + Text( + text = label, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .padding( + vertical = 10.dp, + horizontal = 12.dp, + ), + ) + } +} + +@Preview +@Composable +private fun BitwardenTextButton_preview() { + BitwardenTextButton( + label = "Label", + onClick = {}, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Theme.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Theme.kt index a60e36476..e1492adff 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Theme.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/theme/Theme.kt @@ -46,8 +46,8 @@ fun BitwardenTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + window.statusBarColor = colorScheme.surface.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme } } diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml new file mode 100644 index 000000000..835d4e5b3 --- /dev/null +++ b/app/src/main/res/drawable/logo.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt index a401f8575..33a524e7d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt @@ -1,8 +1,11 @@ package com.x8bit.bitwarden.ui.auth.feature.landing -import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import io.mockk.every @@ -35,7 +38,7 @@ class LandingScreenTest : BaseComposeTest() { viewModel = viewModel, ) } - composeTestRule.onNodeWithTag("Continue button").performClick() + composeTestRule.onNodeWithText("Continue").performClick() verify { viewModel.trySendAction(LandingAction.ContinueButtonClick) } @@ -60,7 +63,11 @@ class LandingScreenTest : BaseComposeTest() { viewModel = viewModel, ) } - composeTestRule.onNodeWithTag("Remember me").performClick() + composeTestRule + .onNodeWithText("Remember me") + .onChildren() + .filterToOne(hasClickAction()) + .performClick() verify { viewModel.trySendAction(LandingAction.RememberMeToggle(true)) } @@ -85,7 +92,7 @@ class LandingScreenTest : BaseComposeTest() { viewModel = viewModel, ) } - composeTestRule.onNodeWithText("Create account").performClick() + composeTestRule.onNodeWithText("Create account").performScrollTo().performClick() verify { viewModel.trySendAction(LandingAction.CreateAccountClick) } @@ -111,7 +118,7 @@ class LandingScreenTest : BaseComposeTest() { viewModel = viewModel, ) } - composeTestRule.onNodeWithTag("Email address").performTextInput(input) + composeTestRule.onNodeWithText("Email address").performTextInput(input) verify { viewModel.trySendAction(LandingAction.EmailInputChanged(input)) }