mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
BIT-553: Apply design reskin on current LoginScreen (#90)
This commit is contained in:
parent
c6911be8d8
commit
bc319368ed
10 changed files with 481 additions and 62 deletions
|
@ -3,28 +3,34 @@ package com.x8bit.bitwarden.ui.auth.feature.login
|
|||
import android.widget.Toast
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
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.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenOutlinedButtonWithIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||
|
||||
/**
|
||||
* The top level composable for the Login screen.
|
||||
|
@ -46,73 +52,118 @@ fun LoginScreen(
|
|||
// TODO Show proper error Dialog
|
||||
Toast.makeText(context, event.messageRes, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
is LoginEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(horizontal = 16.dp, vertical = 32.dp),
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
|
||||
BitwardenTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag("Master password"),
|
||||
value = state.passwordInput,
|
||||
onValueChange = { viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) },
|
||||
label = stringResource(id = R.string.master_password),
|
||||
BitwardenOverflowTopAppBar(
|
||||
title = stringResource(id = R.string.app_name),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.CloseButtonClick) }
|
||||
},
|
||||
dropdownMenuItemContent = {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.get_password_hint))
|
||||
},
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.MasterPasswordHintClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.trySendAction(LoginAction.LoginButtonClick) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("Login button"),
|
||||
enabled = state.isLoginButtonEnabled,
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_with_master_password),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.trySendAction(LoginAction.SingleSignOnClick) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.testTag("Single sign-on button"),
|
||||
enabled = state.isLoginButtonEnabled,
|
||||
) {
|
||||
BitwardenPasswordField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
value = state.passwordInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.PasswordInputChanged(it)) }
|
||||
},
|
||||
label = stringResource(id = R.string.master_password),
|
||||
)
|
||||
|
||||
// TODO: Need to figure out better handling for very small clickable text (BIT-724)
|
||||
Text(
|
||||
text = stringResource(id = R.string.log_in_sso),
|
||||
text = stringResource(id = R.string.get_password_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
.clickable {
|
||||
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
|
||||
}
|
||||
.padding(
|
||||
vertical = 4.dp,
|
||||
horizontal = 16.dp,
|
||||
),
|
||||
)
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.log_in_with_master_password),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.LoginButtonClick) }
|
||||
},
|
||||
isEnabled = state.isLoginButtonEnabled,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
)
|
||||
|
||||
BitwardenOutlinedButtonWithIcon(
|
||||
label = stringResource(id = R.string.log_in_sso),
|
||||
icon = painterResource(id = R.drawable.ic_light_bulb),
|
||||
onClick =
|
||||
remember(viewModel) {
|
||||
{ viewModel.trySendAction(LoginAction.SingleSignOnClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
isEnabled = state.isLoginButtonEnabled,
|
||||
)
|
||||
// TODO Get the "login target" from a dropdown (BIT-202)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.log_in_attempt_by_x_on_y,
|
||||
state.emailAddress,
|
||||
"bitwarden.com",
|
||||
),
|
||||
textAlign = TextAlign.Start,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
)
|
||||
|
||||
// TODO: Need to figure out better handling for very small clickable text (BIT-724)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.trySendAction(LoginAction.NotYouButtonClick) },
|
||||
text = stringResource(id = R.string.not_you),
|
||||
textAlign = TextAlign.Start,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
// TODO Get the "login target" from a dropdown (BIT-202)
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.log_in_attempt_by_x_on_y,
|
||||
state.emailAddress,
|
||||
"bitwarden.com",
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.trySendAction(LoginAction.NotYouButtonClick) },
|
||||
text = stringResource(id = R.string.not_you),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,9 @@ class LoginViewModel @Inject constructor(
|
|||
|
||||
override fun handleAction(action: LoginAction) {
|
||||
when (action) {
|
||||
is LoginAction.CloseButtonClick -> handleCloseButtonClicked()
|
||||
LoginAction.LoginButtonClick -> handleLoginButtonClicked()
|
||||
LoginAction.MasterPasswordHintClick -> handleMasterPasswordHintClicked()
|
||||
LoginAction.NotYouButtonClick -> handleNotYouButtonClicked()
|
||||
LoginAction.SingleSignOnClick -> handleSingleSignOnClicked()
|
||||
is LoginAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||
|
@ -75,6 +77,10 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCloseButtonClicked() {
|
||||
sendEvent(LoginEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleLoginButtonClicked() {
|
||||
attemptLogin(captchaToken = null)
|
||||
}
|
||||
|
@ -103,12 +109,18 @@ class LoginViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleMasterPasswordHintClicked() {
|
||||
// TODO: Navigate to master password hint screen (BIT-72)
|
||||
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
|
||||
}
|
||||
|
||||
private fun handleNotYouButtonClicked() {
|
||||
sendEvent(LoginEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleSingleSignOnClicked() {
|
||||
// TODO BIT-204 navigate to single sign on
|
||||
sendEvent(LoginEvent.ShowToast("Not yet implemented."))
|
||||
}
|
||||
|
||||
private fun handlePasswordInputChanged(action: LoginAction.PasswordInputChanged) {
|
||||
|
@ -144,12 +156,22 @@ sealed class LoginEvent {
|
|||
* Shows an error pop up with a given message
|
||||
*/
|
||||
data class ShowErrorDialog(@StringRes val messageRes: Int) : LoginEvent()
|
||||
|
||||
/**
|
||||
* Shows a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: String) : LoginEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the login screen.
|
||||
*/
|
||||
sealed class LoginAction {
|
||||
/**
|
||||
* Indicates that the top-bar close button was clicked.
|
||||
*/
|
||||
data object CloseButtonClick : LoginAction()
|
||||
|
||||
/**
|
||||
* Indicates that the Login button has been clicked.
|
||||
*/
|
||||
|
@ -160,6 +182,11 @@ sealed class LoginAction {
|
|||
*/
|
||||
data object NotYouButtonClick : LoginAction()
|
||||
|
||||
/**
|
||||
* Indicates that the overflow option for getting a master password hint has been clicked.
|
||||
*/
|
||||
data object MasterPasswordHintClick : LoginAction()
|
||||
|
||||
/**
|
||||
* Indicates that the Enterprise single sign-on button has been clicked.
|
||||
*/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -28,15 +28,15 @@ fun BitwardenFilledButton(
|
|||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = isEnabled,
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled filled [OutlinedButton] with an icon.
|
||||
*
|
||||
* @param label The label for the button.
|
||||
* @param icon The icon 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 BitwardenOutlinedButtonWithIcon(
|
||||
label: String,
|
||||
icon: Painter,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.semantics(mergeDescendants = true) { },
|
||||
enabled = isEnabled,
|
||||
contentPadding = PaddingValues(
|
||||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenOutlinedButtonWithIcon_preview_isEnabled() {
|
||||
BitwardenOutlinedButtonWithIcon(
|
||||
label = "Label",
|
||||
icon = painterResource(id = R.drawable.ic_light_bulb),
|
||||
onClick = {},
|
||||
isEnabled = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenOutlinedButtonWithIcon_preview_isNotEnabled() {
|
||||
BitwardenOutlinedButtonWithIcon(
|
||||
label = "Label",
|
||||
icon = painterResource(id = R.drawable.ic_light_bulb),
|
||||
onClick = {},
|
||||
isEnabled = false,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden-styled [TopAppBar] that assumes the following components:
|
||||
*
|
||||
* - a single navigation control in the upper-left defined by [navigationIcon],
|
||||
* [navigationIconContentDescription], and [onNavigationIconClick].
|
||||
* - a [title] in the middle.
|
||||
* - a single overflow menu in the right with contents defined by the [dropdownMenuItemContent]. It
|
||||
* is strongly recommended that this content be a stack of [DropdownMenuItem].
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BitwardenOverflowTopAppBar(
|
||||
title: String,
|
||||
navigationIcon: Painter,
|
||||
navigationIconContentDescription: String,
|
||||
onNavigationIconClick: () -> Unit,
|
||||
dropdownMenuItemContent: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
var isOverflowMenuVisible by remember { mutableStateOf(false) }
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = { onNavigationIconClick() },
|
||||
) {
|
||||
Icon(
|
||||
painter = navigationIcon,
|
||||
contentDescription = navigationIconContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { isOverflowMenuVisible = !isOverflowMenuVisible },
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_more),
|
||||
contentDescription = stringResource(id = R.string.more),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = isOverflowMenuVisible,
|
||||
onDismissRequest = { isOverflowMenuVisible = false },
|
||||
content = dropdownMenuItemContent,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenOverflowTopAppBar_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenOverflowTopAppBar(
|
||||
title = "Title",
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = {},
|
||||
dropdownMenuItemContent = {},
|
||||
)
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/ic_close.xml
Normal file
9
app/src/main/res/drawable/ic_close.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6.192,5.308C5.948,5.064 5.552,5.064 5.308,5.308C5.064,5.552 5.064,5.948 5.308,6.192L11.116,12L5.308,17.808C5.064,18.052 5.064,18.448 5.308,18.692C5.552,18.936 5.948,18.936 6.192,18.692L12,12.884L17.808,18.692C18.052,18.936 18.448,18.936 18.692,18.692C18.936,18.448 18.936,18.052 18.692,17.808L12.884,12L18.692,6.192C18.936,5.948 18.936,5.552 18.692,5.308C18.448,5.064 18.052,5.064 17.808,5.308L12,11.116L6.192,5.308Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_light_bulb.xml
Normal file
10
app/src/main/res/drawable/ic_light_bulb.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,0.625C6.222,0.625 3.125,3.599 3.125,7.308C3.125,9.503 4.213,11.445 5.882,12.66C6.12,12.833 6.487,13.18 6.789,13.621C7.095,14.066 7.292,14.543 7.292,14.985V17.74C7.292,18.661 8.056,19.375 8.958,19.375H11.042C11.944,19.375 12.708,18.661 12.708,17.74V14.985C12.708,14.543 12.905,14.066 13.211,13.621C13.513,13.18 13.88,12.833 14.118,12.66C15.786,11.445 16.875,9.503 16.875,7.308C16.875,3.599 13.778,0.625 10,0.625ZM4.375,7.308C4.375,4.326 6.875,1.875 10,1.875C13.125,1.875 15.625,4.326 15.625,7.308C15.625,9.076 14.75,10.653 13.382,11.649C13.03,11.906 12.563,12.356 12.18,12.914C11.8,13.468 11.458,14.192 11.458,14.985V15H8.542V14.985C8.542,14.192 8.2,13.468 7.82,12.914C7.437,12.356 6.97,11.906 6.618,11.649C5.25,10.653 4.375,9.076 4.375,7.308ZM8.542,16.25V17.74C8.542,17.934 8.71,18.125 8.958,18.125H11.042C11.29,18.125 11.458,17.934 11.458,17.74V16.25H8.542Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
15
app/src/main/res/drawable/ic_more.xml
Normal file
15
app/src/main/res/drawable/ic_more.xml
Normal file
|
@ -0,0 +1,15 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.25,5.75C13.25,6.44 12.69,7 12,7C11.31,7 10.75,6.44 10.75,5.75C10.75,5.06 11.31,4.5 12,4.5C12.69,4.5 13.25,5.06 13.25,5.75Z"
|
||||
android:fillColor="#1B1B1F"/>
|
||||
<path
|
||||
android:pathData="M13.25,12C13.25,12.69 12.69,13.25 12,13.25C11.31,13.25 10.75,12.69 10.75,12C10.75,11.31 11.31,10.75 12,10.75C12.69,10.75 13.25,11.31 13.25,12Z"
|
||||
android:fillColor="#1B1B1F"/>
|
||||
<path
|
||||
android:pathData="M13.25,18.25C13.25,18.94 12.69,19.5 12,19.5C11.31,19.5 10.75,18.94 10.75,18.25C10.75,17.56 11.31,17 12,17C12.69,17 13.25,17.56 13.25,18.25Z"
|
||||
android:fillColor="#1B1B1F"/>
|
||||
</vector>
|
|
@ -1,8 +1,16 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.login
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.filter
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isPopup
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
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 com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
|
@ -17,6 +25,30 @@ import org.junit.Test
|
|||
|
||||
class LoginScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `close button click should send CloseButtonClick action`() {
|
||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LoginScreen(
|
||||
onNavigateBack = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LoginAction.CloseButtonClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Not you text click should send NotYouButtonClick action`() {
|
||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||
|
@ -35,12 +67,71 @@ class LoginScreenTest : BaseComposeTest() {
|
|||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Not you?").performClick()
|
||||
composeTestRule.onNodeWithText("Not you?").performScrollTo().performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LoginAction.NotYouButtonClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `master password hint text click should send MasterPasswordHintClick action`() {
|
||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LoginScreen(
|
||||
onNavigateBack = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Get your master password hint").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `master password hint option menu click should send MasterPasswordHintClick action`() {
|
||||
val viewModel = mockk<LoginViewModel>(relaxed = true) {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
LoginState(
|
||||
emailAddress = "",
|
||||
isLoginButtonEnabled = false,
|
||||
passwordInput = "",
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
LoginScreen(
|
||||
onNavigateBack = {},
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
// Confirm dropdown version of item is absent
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Get your master password hint")
|
||||
.filter(hasAnyAncestor(isPopup()))
|
||||
.assertCountEquals(0)
|
||||
// Open the overflow menu
|
||||
composeTestRule.onNodeWithContentDescription("More").performClick()
|
||||
// Click on the password hint item in the dropdown
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Get your master password hint")
|
||||
.filterToOne(hasAnyAncestor(isPopup()))
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LoginAction.MasterPasswordHintClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `password input change should send PasswordInputChanged action`() {
|
||||
val input = "input"
|
||||
|
|
|
@ -73,6 +73,23 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseButtonClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.CloseButtonClick)
|
||||
assertEquals(
|
||||
LoginEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginButtonClick login returns error should do nothing`() = runTest {
|
||||
// TODO: handle and display errors (BIT-320)
|
||||
|
@ -147,7 +164,25 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `SingleSignOnClick should do nothing`() = runTest {
|
||||
fun `MasterPasswordHintClick should emit ShowToast`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
},
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.MasterPasswordHintClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
LoginEvent.ShowToast("Not yet implemented."),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SingleSignOnClick should emit ShowToast`() = runTest {
|
||||
val viewModel = LoginViewModel(
|
||||
authRepository = mockk {
|
||||
every { captchaTokenResultFlow } returns flowOf()
|
||||
|
@ -157,6 +192,10 @@ class LoginViewModelTest : BaseViewModelTest() {
|
|||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(LoginAction.SingleSignOnClick)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
assertEquals(
|
||||
LoginEvent.ShowToast("Not yet implemented."),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue