diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index 93904a114..be4b50b81 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -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 }, + ), + ), + ) }, ) }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 57c51099c..488a24c24 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenConfirmLogoutDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenConfirmLogoutDialog.kt new file mode 100644 index 000000000..d01a0f325 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenConfirmLogoutDialog.kt @@ -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, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index d46e2f071..715d44a5d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -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, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt index ee37f3e0b..2c6e01d02 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt @@ -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" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 31c18f264..40fe6b5c2 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -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() { every { userStateFlow } returns MutableStateFlow(DEFAULT_USER_STATE) + every { logout() } just runs } private val vaultRepository = mockk() @@ -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()