mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
BIT-150: Add more comprehensive list of settings rows. (#155)
Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
parent
207bed42ed
commit
8bdda9bffd
5 changed files with 287 additions and 34 deletions
|
@ -3,26 +3,40 @@ package com.x8bit.bitwarden.ui.platform.feature.settings
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Displays the settings screen.
|
||||
|
@ -38,40 +52,98 @@ fun SettingsScreen(
|
|||
SettingsEvent.NavigateAccountSecurity -> onNavigateToAccountSecurity.invoke()
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
|
||||
val scrollBehavior =
|
||||
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
BitwardenMediumTopAppBar(
|
||||
title = stringResource(id = R.string.settings),
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.verticalScroll(state = rememberScrollState()),
|
||||
) {
|
||||
Settings.values().forEach {
|
||||
SettingsRow(
|
||||
text = R.string.account.asText(),
|
||||
text = it.text,
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
|
||||
{ viewModel.trySendAction(SettingsAction.SettingsClick(it)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsRow(
|
||||
text: Text,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Text(
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 56.dp)
|
||||
.padding(start = 16.dp, end = 24.dp, top = 8.dp, bottom = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.weight(1f),
|
||||
text = text(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_navigate_next),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SettingsRows_preview() {
|
||||
BitwardenTheme {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize()
|
||||
.background(color = MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Settings.values().forEach {
|
||||
SettingsRow(
|
||||
text = it.text,
|
||||
onClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -12,12 +16,36 @@ class SettingsViewModel @Inject constructor() : BaseViewModel<Unit, SettingsEven
|
|||
initialState = Unit,
|
||||
) {
|
||||
override fun handleAction(action: SettingsAction): Unit = when (action) {
|
||||
SettingsAction.AccountSecurityClick -> handleAccountSecurityClick()
|
||||
is SettingsAction.SettingsClick -> handleAccountSecurityClick(action)
|
||||
}
|
||||
|
||||
private fun handleAccountSecurityClick() {
|
||||
private fun handleAccountSecurityClick(action: SettingsAction.SettingsClick) {
|
||||
when (action.settings) {
|
||||
Settings.ACCOUNT_SECURITY -> {
|
||||
sendEvent(SettingsEvent.NavigateAccountSecurity)
|
||||
}
|
||||
|
||||
Settings.AUTO_FILL -> {
|
||||
// TODO: BIT-927 Launch auto-fill UI
|
||||
}
|
||||
|
||||
Settings.VAULT -> {
|
||||
// TODO: BIT-928 Launch vault UI
|
||||
}
|
||||
|
||||
Settings.APPEARANCE -> {
|
||||
// TODO: BIT-929 Launch appearance UI
|
||||
}
|
||||
|
||||
Settings.OTHER -> {
|
||||
// TODO: BIT-930 Launch other UI
|
||||
}
|
||||
|
||||
Settings.ABOUT -> {
|
||||
// TODO: BIT-931 Launch about UI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -35,7 +63,24 @@ sealed class SettingsEvent {
|
|||
*/
|
||||
sealed class SettingsAction {
|
||||
/**
|
||||
* User clicked account security.
|
||||
* User clicked a settings row.
|
||||
*/
|
||||
data object AccountSecurityClick : SettingsAction()
|
||||
data class SettingsClick(
|
||||
val settings: Settings,
|
||||
) : SettingsAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing the settings rows, such as "account security" or "vault".
|
||||
*
|
||||
* @property text The [Text] of the string that represents the label of each setting.
|
||||
*/
|
||||
// TODO: BIT-944 Missing correct resources for "Account Security", "Vault", and "Appearance".
|
||||
enum class Settings(val text: Text) {
|
||||
ACCOUNT_SECURITY(R.string.security.asText()),
|
||||
AUTO_FILL(R.string.autofill.asText()),
|
||||
VAULT(R.string.vaults.asText()),
|
||||
APPEARANCE(R.string.language.asText()),
|
||||
OTHER(R.string.other.asText()),
|
||||
ABOUT(R.string.about.asText()),
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_navigate_next.xml
Normal file
9
app/src/main/res/drawable/ic_navigate_next.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportHeight="20"
|
||||
android:viewportWidth="20">
|
||||
<path
|
||||
android:fillColor="#1B1B1F"
|
||||
android:pathData="M14.697,10.431C15.082,10.052 15.014,9.888 14.634,9.519C14.634,9.519 7.04,1.619 6.524,1.062C6.008,0.504 6.764,-0.522 7.536,0.317C8.309,1.157 15.759,8.887 15.759,8.887C16.301,9.553 16.301,10.461 15.759,11.126L7.644,19.589C6.792,20.527 5.843,19.68 6.628,18.859C9.647,15.7 14.697,10.431 14.697,10.431Z" />
|
||||
</vector>
|
|
@ -13,10 +13,10 @@ import org.junit.Test
|
|||
class SettingsScreenTest : BaseComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on account row click should emit AccountSecurityClick`() {
|
||||
fun `on about row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.AccountSecurityClick) } returns Unit
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
|
@ -24,8 +24,90 @@ class SettingsScreenTest : BaseComposeTest() {
|
|||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Account").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.AccountSecurityClick) }
|
||||
composeTestRule.onNodeWithText("About").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on account security row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every {
|
||||
trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
|
||||
} returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Security").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on appearance row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Language").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on auto-fill row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Auto-fill").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on other row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Other").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on vault row click should emit SettingsClick`() {
|
||||
val viewModel = mockk<SettingsViewModel> {
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
SettingsScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateToAccountSecurity = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Vaults").performClick()
|
||||
verify { viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -9,11 +9,56 @@ import org.junit.jupiter.api.Test
|
|||
class SettingsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `on AccountSecurityClick should emit NavigateAccountSecurity`() = runTest {
|
||||
fun `on SettingsClick with ABOUT should emit nothing`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.AccountSecurityClick)
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ABOUT))
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SettingsClick with ACCOUNT_SECURITY should emit NavigateAccountSecurity`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.ACCOUNT_SECURITY))
|
||||
assertEquals(SettingsEvent.NavigateAccountSecurity, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SettingsClick with APPEARANCE should emit nothing`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.APPEARANCE))
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SettingsClick with AUTO_FILL should emit nothing`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.AUTO_FILL))
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SettingsClick with OTHER should emit nothing`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.OTHER))
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on SettingsClick with VAULT should emit nothing`() = runTest {
|
||||
val viewModel = SettingsViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SettingsAction.SettingsClick(Settings.VAULT))
|
||||
expectNoEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue