mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 10:48:47 +03:00
Hook up vault timeout action to repo (#530)
This commit is contained in:
parent
e69c4eb29e
commit
185849951f
6 changed files with 191 additions and 100 deletions
|
@ -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 })
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue