diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt index f695109787..5a431e6e15 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt @@ -86,6 +86,7 @@ private const val MAXIMUM_ACCOUNT_LIMIT = 5 * sync with the associated app bar. */ @OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongMethod") @Composable fun BitwardenAccountSwitcher( isVisible: Boolean, @@ -104,19 +105,37 @@ fun BitwardenAccountSwitcher( var isVisibleActual by remember { mutableStateOf(isVisible) } var lockOrLogoutAccount by remember { mutableStateOf(null) } - if (lockOrLogoutAccount != null && !isVisibleActual) { - LockOrLogoutDialog( - accountSummary = requireNotNull(lockOrLogoutAccount), - onDismissRequest = { lockOrLogoutAccount = null }, - onLockAccountClick = { - onLockAccountClick(it) - lockOrLogoutAccount = null - }, - onLogoutAccountClick = { - onLogoutAccountClick(it) - lockOrLogoutAccount = null - }, - ) + var logoutConfirmationAccount by remember { mutableStateOf(null) } + when { + isVisibleActual -> { + // Can not show dialogs when the switcher itself is visible + } + + lockOrLogoutAccount != null -> { + LockOrLogoutDialog( + accountSummary = requireNotNull(lockOrLogoutAccount), + onDismissRequest = { lockOrLogoutAccount = null }, + onLockAccountClick = { + onLockAccountClick(it) + lockOrLogoutAccount = null + }, + onLogoutAccountClick = { + lockOrLogoutAccount = null + logoutConfirmationAccount = it + }, + ) + } + + logoutConfirmationAccount != null -> { + BitwardenLogoutConfirmationDialog( + accountSummary = requireNotNull(logoutConfirmationAccount), + onDismissRequest = { logoutConfirmationAccount = null }, + onConfirmClick = { + onLogoutAccountClick(requireNotNull(logoutConfirmationAccount)) + logoutConfirmationAccount = null + }, + ) + } } Box(modifier = modifier) { 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 index d01a0f3254..48552bb2b9 100644 --- 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 @@ -3,21 +3,29 @@ package com.x8bit.bitwarden.ui.platform.components import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary /** * 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. + * @param accountSummary Optional account information that may be used to provide additional + * information. */ @Composable fun BitwardenLogoutConfirmationDialog( onDismissRequest: () -> Unit, onConfirmClick: () -> Unit, + accountSummary: AccountSummary? = null, ) { + val baseConfirmationMessage = stringResource(id = R.string.logout_confirmation) + val message = accountSummary + ?.let { "$baseConfirmationMessage\n\n${it.email}\n${it.environmentLabel}" } + ?: baseConfirmationMessage BitwardenTwoButtonDialog( title = stringResource(id = R.string.log_out), - message = stringResource(id = R.string.logout_confirmation), + message = message, confirmButtonText = stringResource(id = R.string.yes), onConfirmClick = onConfirmClick, dismissButtonText = stringResource(id = R.string.cancel), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 431383b564..c6043a163a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -136,6 +136,9 @@ class VaultViewModel @Inject constructor( private fun handleLogoutAccountClick(action: VaultAction.LogoutAccountClick) { authRepository.logout(userId = action.accountSummary.userId) + mutableStateFlow.update { + it.copy(isSwitchingAccounts = action.accountSummary.isActive) + } } private fun handleSwitchAccountClick(action: VaultAction.SwitchAccountClick) { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt index 314e03a2d1..741c409900 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreenTest.kt @@ -23,6 +23,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed +import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertSwitcherIsDisplayed import com.x8bit.bitwarden.ui.util.assertSwitcherIsNotDisplayed @@ -31,6 +32,7 @@ import com.x8bit.bitwarden.ui.util.performAccountIconClick import com.x8bit.bitwarden.ui.util.performAccountLongClick import com.x8bit.bitwarden.ui.util.performLockAccountClick import com.x8bit.bitwarden.ui.util.performLogoutAccountClick +import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -158,7 +160,7 @@ class LandingScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `logout button click in the lock-or-logout dialog should send LogoutAccountClick action and close the dialog`() { + fun `logout button click in the lock-or-logout dialog should show the logout confirmation dialog and hide the lock-or-logout dialog`() { // Show the lock-or-logout dialog val accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY) mutableStateFlow.update { @@ -169,6 +171,25 @@ class LandingScreenTest : BaseComposeTest() { composeTestRule.performLogoutAccountClick() + composeTestRule.assertLogoutConfirmationDialogIsDisplayed( + accountSummary = ACTIVE_ACCOUNT_SUMMARY, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `logout button click in the logout confirmation dialog should send LogoutAccountClick action and close the dialog`() { + // Show the logout confirmation dialog + val accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY) + mutableStateFlow.update { + it.copy(accountSummaries = accountSummaries) + } + composeTestRule.performAccountIconClick() + composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) + composeTestRule.performLogoutAccountClick() + + composeTestRule.performLogoutAccountConfirmationClick() + verify { viewModel.trySendAction(LandingAction.LogoutAccountClick(ACTIVE_ACCOUNT_SUMMARY)) } composeTestRule.assertNoDialogExists() } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt index ab1c465b1f..13f6db35f4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreenTest.kt @@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed +import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertSwitcherIsDisplayed import com.x8bit.bitwarden.ui.util.assertSwitcherIsNotDisplayed @@ -27,6 +28,7 @@ import com.x8bit.bitwarden.ui.util.performAccountIconClick import com.x8bit.bitwarden.ui.util.performAccountLongClick import com.x8bit.bitwarden.ui.util.performLockAccountClick import com.x8bit.bitwarden.ui.util.performLogoutAccountClick +import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -147,7 +149,7 @@ class LoginScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `logout button click in the lock-or-logout dialog should send LogoutAccountClick action and close the dialog`() { + fun `logout button click in the lock-or-logout dialog should show the logout confirmation dialog and hide the lock-or-logout dialog`() { // Show the lock-or-logout dialog val accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY) mutableStateFlow.update { @@ -158,6 +160,25 @@ class LoginScreenTest : BaseComposeTest() { composeTestRule.performLogoutAccountClick() + composeTestRule.assertLogoutConfirmationDialogIsDisplayed( + accountSummary = ACTIVE_ACCOUNT_SUMMARY, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `logout button click in the logout confirmation dialog should send LogoutAccountClick action and close the dialog`() { + // Show the logout confirmation dialog + val accountSummaries = listOf(ACTIVE_ACCOUNT_SUMMARY) + mutableStateFlow.update { + it.copy(accountSummaries = accountSummaries) + } + composeTestRule.performAccountIconClick() + composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) + composeTestRule.performLogoutAccountClick() + + composeTestRule.performLogoutAccountConfirmationClick() + verify { viewModel.trySendAction(LoginAction.LogoutAccountClick(ACTIVE_ACCOUNT_SUMMARY)) } composeTestRule.assertNoDialogExists() } 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 644ee6963d..e9606d6efe 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.test.performTextInput import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed +import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertSwitcherIsDisplayed import com.x8bit.bitwarden.ui.util.assertSwitcherIsNotDisplayed @@ -25,6 +26,7 @@ import com.x8bit.bitwarden.ui.util.performAccountLongClick import com.x8bit.bitwarden.ui.util.performAddAccountClick import com.x8bit.bitwarden.ui.util.performLockAccountClick import com.x8bit.bitwarden.ui.util.performLogoutAccountClick +import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -130,13 +132,28 @@ class VaultUnlockScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `logout button click in the lock-or-logout dialog should send LogoutAccountClick action and close the dialog`() { + fun `logout button click in the lock-or-logout dialog should show the logout confirmation dialog and hide the lock-or-logout dialog`() { // Show the lock-or-logout dialog composeTestRule.performAccountIconClick() composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) composeTestRule.performLogoutAccountClick() + composeTestRule.assertLogoutConfirmationDialogIsDisplayed( + accountSummary = ACTIVE_ACCOUNT_SUMMARY, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `logout button click in the logout confirmation dialog should send LogoutAccountClick action and close the dialog`() { + // Show the logout confirmation dialog + composeTestRule.performAccountIconClick() + composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) + composeTestRule.performLogoutAccountClick() + + composeTestRule.performLogoutAccountConfirmationClick() + verify { viewModel.trySendAction(VaultUnlockAction.LogoutAccountClick(ACTIVE_ACCOUNT_SUMMARY)) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/util/BitwardenAccountSwitcherTestHelpers.kt b/app/src/test/java/com/x8bit/bitwarden/ui/util/BitwardenAccountSwitcherTestHelpers.kt index b6e715dd9b..2c4560f138 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/util/BitwardenAccountSwitcherTestHelpers.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/util/BitwardenAccountSwitcherTestHelpers.kt @@ -78,6 +78,35 @@ fun ComposeContentTestRule.assertLockOrLogoutDialogIsDisplayed( .assertIsDisplayed() } +/** + * Asserts the logoung confirmation dialog is currently displayed with information from the given + * [accountSummary]. + */ +fun ComposeContentTestRule.assertLogoutConfirmationDialogIsDisplayed( + accountSummary: AccountSummary, +) { + this.waitForIdle() + this + .onNode(isDialog()) + .assertIsDisplayed() + this + .onAllNodesWithText("Log out") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + this + .onAllNodesWithText("Are you sure you want to log out?", substring = true) + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + this + .onAllNodesWithText(accountSummary.email, substring = true) + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + this + .onAllNodesWithText(accountSummary.environmentLabel, substring = true) + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() +} + /** * Clicks on the given [accountSummary] in the account switcher. */ @@ -118,6 +147,16 @@ fun ComposeContentTestRule.performLogoutAccountClick() { .performClick() } +/** + * Clicks the "Yes" button in the logout confirmation dialog to confirm the logout. + */ +fun ComposeContentTestRule.performLogoutAccountConfirmationClick() { + this + .onAllNodesWithText("Yes") + .filterToOne(hasAnyAncestor(isDialog())) + .performClick() +} + /** * Opens the account switcher. * diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index cbd95212b0..cbbc6ea338 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed +import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertSwitcherIsDisplayed import com.x8bit.bitwarden.ui.util.assertSwitcherIsNotDisplayed @@ -22,6 +23,7 @@ import com.x8bit.bitwarden.ui.util.performAccountLongClick import com.x8bit.bitwarden.ui.util.performAddAccountClick import com.x8bit.bitwarden.ui.util.performLockAccountClick import com.x8bit.bitwarden.ui.util.performLogoutAccountClick +import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import io.mockk.every import io.mockk.mockk @@ -145,13 +147,28 @@ class VaultScreenTest : BaseComposeTest() { @Suppress("MaxLineLength") @Test - fun `logout button click in the lock-or-logout dialog should send LogoutAccountClick action and close the dialog`() { + fun `logout button click in the lock-or-logout dialog should show the logout confirmation dialog and hide the lock-or-logout dialog`() { // Show the lock-or-logout dialog composeTestRule.performAccountIconClick() composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) composeTestRule.performLogoutAccountClick() + composeTestRule.assertLogoutConfirmationDialogIsDisplayed( + accountSummary = ACTIVE_ACCOUNT_SUMMARY, + ) + } + + @Suppress("MaxLineLength") + @Test + fun `logout button click in the logout confirmation dialog should send LogoutAccountClick action and close the dialog`() { + // Show the logout confirmation dialog + composeTestRule.performAccountIconClick() + composeTestRule.performAccountLongClick(ACTIVE_ACCOUNT_SUMMARY) + composeTestRule.performLogoutAccountClick() + + composeTestRule.performLogoutAccountConfirmationClick() + verify { viewModel.trySendAction(VaultAction.LogoutAccountClick(ACTIVE_ACCOUNT_SUMMARY)) } composeTestRule.assertNoDialogExists() } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 3611d6b7ec..d445dc5bc3 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -151,6 +151,7 @@ class VaultViewModelTest : BaseViewModelTest() { val accountUserId = "userId" val accountSummary = mockk { every { userId } returns accountUserId + every { isActive } returns false } val viewModel = createViewModel() @@ -159,16 +160,45 @@ class VaultViewModelTest : BaseViewModelTest() { verify { vaultRepository.lockVaultIfNecessary(userId = accountUserId) } } + @Suppress("MaxLineLength") @Test - fun `on LogoutAccountClick should call logout for the given account`() { + fun `on LogoutAccountClick for an active account should call logout for the given account and set isSwitchingAccounts to true`() { val accountUserId = "userId" val accountSummary = mockk { every { userId } returns accountUserId + every { isActive } returns true } val viewModel = createViewModel() viewModel.trySendAction(VaultAction.LogoutAccountClick(accountSummary)) + assertEquals( + DEFAULT_STATE.copy( + isSwitchingAccounts = true, + ), + viewModel.stateFlow.value, + ) + verify { authRepository.logout(userId = accountUserId) } + } + + @Suppress("MaxLineLength") + @Test + fun `on LogoutAccountClick for an inactive account should call logout for the given account and set isSwitchingAccounts to false`() { + val accountUserId = "userId" + val accountSummary = mockk { + every { userId } returns accountUserId + every { isActive } returns false + } + val viewModel = createViewModel() + + viewModel.trySendAction(VaultAction.LogoutAccountClick(accountSummary)) + + assertEquals( + DEFAULT_STATE.copy( + isSwitchingAccounts = false, + ), + viewModel.stateFlow.value, + ) verify { authRepository.logout(userId = accountUserId) } }