Hook up vault timeout action to repo (#530)

This commit is contained in:
Brian Yencho 2024-01-08 09:56:03 -06:00 committed by Álison Fernandes
parent e69c4eb29e
commit 185849951f
6 changed files with 191 additions and 100 deletions

View file

@ -31,6 +31,7 @@ import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
import com.x8bit.bitwarden.ui.platform.base.util.Text
@ -100,16 +101,6 @@ fun AccountSecurityScreen(
},
)
AccountSecurityDialog.SessionTimeoutAction -> SessionTimeoutActionDialog(
selectedSessionTimeoutAction = state.sessionTimeoutAction,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.DismissDialog) }
},
onActionSelect = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionSelect(it)) }
},
)
null -> Unit
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@ -210,19 +201,13 @@ fun AccountSecurityScreen(
},
modifier = Modifier.fillMaxWidth(),
)
BitwardenTextRow(
text = stringResource(id = R.string.session_timeout_action),
onClick = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionClick) }
SessionTimeoutActionRow(
selectedVaultTimeoutAction = state.vaultTimeoutAction,
onVaultTimeoutActionSelect = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.VaultTimeoutActionSelect(it)) }
},
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = state.sessionTimeoutAction.text(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
)
Spacer(Modifier.height(16.dp))
BitwardenListHeaderText(
@ -349,6 +334,47 @@ private fun SessionTimeoutRow(
}
}
@Suppress("LongMethod")
@Composable
private fun SessionTimeoutActionRow(
selectedVaultTimeoutAction: VaultTimeoutAction,
onVaultTimeoutActionSelect: (VaultTimeoutAction) -> Unit,
modifier: Modifier = Modifier,
) {
var shouldShowSelectionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.session_timeout_action),
onClick = { shouldShowSelectionDialog = true },
modifier = modifier,
) {
Text(
text = selectedVaultTimeoutAction.displayLabel(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (shouldShowSelectionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.vault_timeout_action),
onDismissRequest = { shouldShowSelectionDialog = false },
) {
val vaultTimeoutActionOptions = VaultTimeoutAction.entries
vaultTimeoutActionOptions.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == selectedVaultTimeoutAction,
onClick = {
shouldShowSelectionDialog = false
onVaultTimeoutActionSelect(
vaultTimeoutActionOptions.first { it == option },
)
},
)
}
}
}
}
@Composable
private fun FingerPrintPhraseDialog(
fingerprintPhrase: Text,
@ -397,26 +423,3 @@ private fun FingerPrintPhraseDialog(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
)
}
@Composable
private fun SessionTimeoutActionDialog(
selectedSessionTimeoutAction: SessionTimeoutAction,
onDismissRequest: () -> Unit,
onActionSelect: (SessionTimeoutAction) -> Unit,
) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.vault_timeout_action),
onDismissRequest = onDismissRequest,
) {
SessionTimeoutAction.values().forEach { option ->
BitwardenSelectionRow(
text = option.text,
isSelected = option == selectedSessionTimeoutAction,
onClick = {
onActionSelect(
SessionTimeoutAction.values().first { it == option })
},
)
}
}
}

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
@ -39,7 +40,7 @@ class AccountSecurityViewModel @Inject constructor(
isUnlockWithBiometricsEnabled = false,
isUnlockWithPinEnabled = false,
vaultTimeoutType = settingsRepository.vaultTimeout.type,
sessionTimeoutAction = SessionTimeoutAction.LOCK,
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
),
) {
@ -62,11 +63,10 @@ class AccountSecurityViewModel @Inject constructor(
AccountSecurityAction.LogoutClick -> handleLogoutClick()
AccountSecurityAction.PendingLoginRequestsClick -> handlePendingLoginRequestsClick()
is AccountSecurityAction.VaultTimeoutTypeSelect -> handleVaultTimeoutTypeSelect(action)
is AccountSecurityAction.SessionTimeoutActionSelect -> {
handleSessionTimeoutActionSelect(action)
is AccountSecurityAction.VaultTimeoutActionSelect -> {
handleVaultTimeoutActionSelect(action)
}
AccountSecurityAction.SessionTimeoutActionClick -> handleSessionTimeoutActionClick()
AccountSecurityAction.TwoStepLoginClick -> handleTwoStepLoginClick()
is AccountSecurityAction.UnlockWithBiometricToggle -> {
handleUnlockWithBiometricToggled(action)
@ -146,21 +146,19 @@ class AccountSecurityViewModel @Inject constructor(
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
}
private fun handleSessionTimeoutActionSelect(
action: AccountSecurityAction.SessionTimeoutActionSelect,
private fun handleVaultTimeoutActionSelect(
action: AccountSecurityAction.VaultTimeoutActionSelect,
) {
// TODO BIT-746: Implement session timeout action
val vaultTimeoutAction = action.vaultTimeoutAction
mutableStateFlow.update {
it.copy(
dialog = null,
sessionTimeoutAction = action.sessionTimeoutAction,
vaultTimeoutAction = action.vaultTimeoutAction,
)
}
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
}
settingsRepository.vaultTimeoutAction = vaultTimeoutAction
private fun handleSessionTimeoutActionClick() {
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.SessionTimeoutAction) }
// TODO BIT-746: Finish implementing session timeout action
sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText()))
}
private fun handleTwoStepLoginClick() {
@ -194,7 +192,7 @@ data class AccountSecurityState(
val isUnlockWithBiometricsEnabled: Boolean,
val isUnlockWithPinEnabled: Boolean,
val vaultTimeoutType: VaultTimeout.Type,
val sessionTimeoutAction: SessionTimeoutAction,
val vaultTimeoutAction: VaultTimeoutAction,
) : Parcelable
/**
@ -212,12 +210,6 @@ sealed class AccountSecurityDialog : Parcelable {
*/
@Parcelize
data object FingerprintPhrase : AccountSecurityDialog()
/**
* Allows the user to select a session timeout action.
*/
@Parcelize
data object SessionTimeoutAction : AccountSecurityDialog()
}
/**
@ -325,17 +317,12 @@ sealed class AccountSecurityAction {
) : AccountSecurityAction()
/**
* User selected a [SessionTimeoutAction].
* User selected a [VaultTimeoutAction].
*/
data class SessionTimeoutActionSelect(
val sessionTimeoutAction: SessionTimeoutAction,
data class VaultTimeoutActionSelect(
val vaultTimeoutAction: VaultTimeoutAction,
) : AccountSecurityAction()
/**
* User clicked session timeout action.
*/
data object SessionTimeoutActionClick : AccountSecurityAction()
/**
* User clicked two-step login.
*/

View file

@ -0,0 +1,16 @@
package com.x8bit.bitwarden.ui.platform.util
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* Provides a human-readable display label for the given [VaultTimeoutAction].
*/
val VaultTimeoutAction.displayLabel: Text
get() = when (this) {
VaultTimeoutAction.LOCK -> R.string.lock
VaultTimeoutAction.LOGOUT -> R.string.log_out
}
.asText()

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
@ -340,34 +341,99 @@ class AccountSecurityScreenTest : BaseComposeTest() {
}
@Test
fun `on session timeout action click should send SessionTimeoutActionClick`() {
fun `on session timeout action click should show a selection dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.performScrollTo()
.performClick()
verify { viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionClick) }
composeTestRule
.onAllNodesWithText("Vault timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Lock")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Log out")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `on session timeout action dialog option click should close the dialog and send VaultTimeoutActionSelect`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Vault timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Log out")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify {
viewModel.trySendAction(
AccountSecurityAction.VaultTimeoutActionSelect(
VaultTimeoutAction.LOGOUT,
),
)
}
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `on session timeout action dialog cancel click should close the dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Vault timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
.performClick()
verify(exactly = 0) { viewModel.trySendAction(any()) }
composeTestRule.assertNoDialogExists()
}
@Test
fun `session timeout action should be updated on or off according to state`() {
fun `session timeout action should be updated according to state`() {
composeTestRule
.onNodeWithText("Session timeout action")
.performScrollTo()
.assertTextEquals("Session timeout action", "Lock")
mutableStateFlow.update { it.copy(sessionTimeoutAction = SessionTimeoutAction.LOG_OUT) }
mutableStateFlow.update { it.copy(vaultTimeoutAction = VaultTimeoutAction.LOGOUT) }
composeTestRule
.onNodeWithText("Session timeout action")
.performScrollTo()
.assertTextEquals("Session timeout action", "Log out")
}
@Test
fun `session timeout action dialog should be displayed to state`() {
composeTestRule.onNodeWithText("Vault timeout action").assertDoesNotExist()
mutableStateFlow.update { it.copy(dialog = AccountSecurityDialog.SessionTimeoutAction) }
composeTestRule.onNodeWithText("Vault timeout action").assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `on two-step login click should display confirmation dialog and confirm click should send TwoStepLoginClick`() {
@ -528,7 +594,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
isUnlockWithBiometricsEnabled = false,
isUnlockWithPinEnabled = false,
vaultTimeoutType = VaultTimeout.Type.THIRTY_MINUTES,
sessionTimeoutAction = SessionTimeoutAction.LOCK,
vaultTimeoutAction = VaultTimeoutAction.LOCK,
)
}
}

View file

@ -5,6 +5,7 @@ import app.cash.turbine.test
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeout
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
@ -146,11 +147,14 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
}
@Test
fun `on SessionTimeoutActionSelect should update session timeout action`() = runTest {
val viewModel = createViewModel()
fun `on VaultTimeoutActionSelect should update vault timeout action`() = runTest {
val settingsRepository = mockk<SettingsRepository>() {
every { vaultTimeoutAction = any() } just runs
}
val viewModel = createViewModel(settingsRepository = settingsRepository)
viewModel.eventFlow.test {
viewModel.trySendAction(
AccountSecurityAction.SessionTimeoutActionSelect(SessionTimeoutAction.LOG_OUT),
AccountSecurityAction.VaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT),
)
assertEquals(
AccountSecurityEvent.ShowToast("Not yet implemented.".asText()),
@ -158,22 +162,14 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
)
}
assertEquals(
DEFAULT_STATE.copy(dialog = null, sessionTimeoutAction = SessionTimeoutAction.LOG_OUT),
DEFAULT_STATE.copy(
vaultTimeoutAction = VaultTimeoutAction.LOGOUT,
),
viewModel.stateFlow.value,
)
verify { settingsRepository.vaultTimeoutAction = VaultTimeoutAction.LOGOUT }
}
@Test
fun `on SessionTimeoutActionClick should update shouldShowSessionTimeoutActionDialog`() =
runTest {
val viewModel = createViewModel()
viewModel.trySendAction(AccountSecurityAction.SessionTimeoutActionClick)
assertEquals(
DEFAULT_STATE.copy(dialog = AccountSecurityDialog.SessionTimeoutAction),
viewModel.stateFlow.value,
)
}
@Test
fun `on TwoStepLoginClick should emit NavigateToTwoStepLogin`() = runTest {
val viewModel = createViewModel()
@ -268,7 +264,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
isUnlockWithBiometricsEnabled = false,
isUnlockWithPinEnabled = false,
vaultTimeoutType = VaultTimeout.Type.THIRTY_MINUTES,
sessionTimeoutAction = SessionTimeoutAction.LOCK,
vaultTimeoutAction = VaultTimeoutAction.LOCK,
)
}
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.platform.util
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.base.util.asText
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class VaultTimeoutActionExtensionsTest {
@Test
fun `displayLabel should return the correct value for each type`() {
mapOf(
VaultTimeoutAction.LOCK to R.string.lock.asText(),
VaultTimeoutAction.LOGOUT to R.string.log_out.asText(),
)
.forEach { (type, label) ->
assertEquals(
label,
type.displayLabel,
)
}
}
}