BIT-1081: Add log out ability to Vault Unlock screen (#285)

This commit is contained in:
Brian Yencho 2023-11-28 09:13:16 -06:00 committed by Álison Fernandes
parent 107ee1c08c
commit 04609b9a86
6 changed files with 138 additions and 20 deletions

View file

@ -38,11 +38,14 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenLogoutConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
@ -76,6 +79,7 @@ fun VaultUnlockScreen(
canScroll = { !accountMenuVisible },
)
// Dynamic dialogs
when (val dialog = state.dialog) {
is VaultUnlockState.VaultUnlockDialog.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
@ -94,6 +98,23 @@ fun VaultUnlockScreen(
null -> Unit
}
// Static dialogs
var showLogoutConfirmationDialog by remember { mutableStateOf(false) }
if (showLogoutConfirmationDialog) {
BitwardenLogoutConfirmationDialog(
onDismissRequest = { showLogoutConfirmationDialog = false },
onConfirmClick = remember(viewModel) {
{
showLogoutConfirmationDialog = false
viewModel.trySendAction(
VaultUnlockAction.ConfirmLogoutClick,
)
}
},
)
}
// Content
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
@ -109,7 +130,14 @@ fun VaultUnlockScreen(
color = state.avatarColor,
onClick = { accountMenuVisible = !accountMenuVisible },
)
BitwardenOverflowActionItem()
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
text = stringResource(id = R.string.log_out),
onClick = { showLogoutConfirmationDialog = true },
),
),
)
},
)
},

View file

@ -71,6 +71,7 @@ class VaultUnlockViewModel @Inject constructor(
when (action) {
VaultUnlockAction.AddAccountClick -> handleAddAccountClick()
VaultUnlockAction.DismissDialog -> handleDismissDialog()
VaultUnlockAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
is VaultUnlockAction.PasswordInputChanged -> handlePasswordInputChanged(action)
is VaultUnlockAction.SwitchAccountClick -> handleSwitchAccountClick(action)
VaultUnlockAction.UnlockClick -> handleUnlockClick()
@ -88,6 +89,10 @@ class VaultUnlockViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) }
}
private fun handleConfirmLogoutClick() {
authRepository.logout()
}
private fun handlePasswordInputChanged(action: VaultUnlockAction.PasswordInputChanged) {
mutableStateFlow.update {
it.copy(passwordInput = action.passwordInput)
@ -213,6 +218,11 @@ sealed class VaultUnlockAction {
*/
data object DismissDialog : VaultUnlockAction()
/**
* The user has clicked on the logout confirmation button.
*/
data object ConfirmLogoutClick : VaultUnlockAction()
/**
* The user has modified the password input.
*/

View file

@ -0,0 +1,27 @@
package com.x8bit.bitwarden.ui.platform.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.x8bit.bitwarden.R
/**
* A reusable dialog for confirming whether or not the user wants to log out.
*
* @param onDismissRequest A callback for when the dialog is requesting dismissal.
* @param onConfirmClick A callback for when the log out confirmation button is clicked.
*/
@Composable
fun BitwardenLogoutConfirmationDialog(
onDismissRequest: () -> Unit,
onConfirmClick: () -> Unit,
) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.log_out),
message = stringResource(id = R.string.logout_confirmation),
confirmButtonText = stringResource(id = R.string.yes),
onConfirmClick = onConfirmClick,
dismissButtonText = stringResource(id = R.string.cancel),
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest,
)
}

View file

@ -31,6 +31,7 @@ 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.base.util.Text
import com.x8bit.bitwarden.ui.platform.components.BitwardenLogoutConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
@ -39,7 +40,6 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenWideSwitch
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
@ -76,8 +76,8 @@ fun AccountSecurityScreen(
}
when (state.dialog) {
AccountSecurityDialog.ConfirmLogout -> ConfirmLogoutDialog(
onDismiss = remember(viewModel) {
AccountSecurityDialog.ConfirmLogout -> BitwardenLogoutConfirmationDialog(
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(AccountSecurityAction.DismissDialog) }
},
onConfirmClick = remember(viewModel) {
@ -286,22 +286,6 @@ fun AccountSecurityScreen(
}
}
@Composable
private fun ConfirmLogoutDialog(
onDismiss: () -> Unit,
onConfirmClick: () -> Unit,
) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.log_out),
message = stringResource(id = R.string.logout_confirmation),
confirmButtonText = stringResource(id = R.string.yes),
onConfirmClick = onConfirmClick,
dismissButtonText = stringResource(id = R.string.cancel),
onDismissClick = onDismiss,
onDismissRequest = onDismiss,
)
}
@Composable
private fun FingerPrintPhraseDialog(
fingerprintPhrase: Text,

View file

@ -3,6 +3,12 @@ package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
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
@ -77,6 +83,58 @@ class VaultUnlockScreenTest : BaseComposeTest() {
composeTestRule.onNodeWithText("Add account").assertDoesNotExist()
}
@Test
fun `logout click in the overflow menu should show the logout confirmation dialog`() {
// Confirm neither the popup nor the dialog are showing
composeTestRule.onNode(isPopup()).assertDoesNotExist()
composeTestRule.onNode(isDialog()).assertDoesNotExist()
// Expand the overflow menu
composeTestRule.onNodeWithContentDescription("More").performClick()
composeTestRule.onNode(isPopup()).assertIsDisplayed()
composeTestRule.onNode(isDialog()).assertDoesNotExist()
// Click on the logout item
composeTestRule
.onAllNodesWithText("Log out")
.filterToOne(hasAnyAncestor(isPopup()))
.performClick()
// Check for the dialog
composeTestRule
.onNode(isDialog())
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Log out")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Are you sure you want to log out?")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `Yes click in the logout confirmation dialog should send the ConfirmLogoutClick action`() {
// Expand the overflow menu
composeTestRule.onNodeWithContentDescription("More").performClick()
// Click on the logout item to display the dialog
composeTestRule
.onAllNodesWithText("Log out")
.filterToOne(hasAnyAncestor(isPopup()))
.performClick()
composeTestRule.onNode(isDialog()).assertIsDisplayed()
// Click on the Yes button in the dialog
composeTestRule
.onAllNodesWithText("Yes")
.filterToOne(hasAnyAncestor(isDialog()))
.performClick()
verify { viewModel.trySendAction(VaultUnlockAction.ConfirmLogoutClick) }
}
@Test
fun `email state change should update logged in as text`() {
val newEmail = "david@bitwarden.com"

View file

@ -17,7 +17,10 @@ import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
@ -28,6 +31,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
private val environmentRepository = FakeEnvironmentRepository()
private val authRepository = mockk<AuthRepository>() {
every { userStateFlow } returns MutableStateFlow(DEFAULT_USER_STATE)
every { logout() } just runs
}
private val vaultRepository = mockk<VaultRepository>()
@ -80,6 +84,13 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `on ConfirmLogoutClick should call logout on the AuthRepository`() {
val viewModel = createViewModel()
viewModel.trySendAction(VaultUnlockAction.ConfirmLogoutClick)
verify { authRepository.logout() }
}
@Test
fun `on PasswordInputChanged should update the password input state`() = runTest {
val viewModel = createViewModel()