From f608852dc7cbc2208f2eb1e32a956efeb3bdba3d Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 2 Jul 2025 14:13:58 -0500 Subject: [PATCH] PM-23305: Replace Vault Screen Toasts with Snackbars (#5468) --- .../ui/vault/feature/vault/VaultScreen.kt | 9 ---- .../ui/vault/feature/vault/VaultViewModel.kt | 47 ++++++++++--------- .../vault/feature/vault/VaultViewModelTest.kt | 8 ++-- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 6973a79165..8eb45f7b8d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -1,6 +1,5 @@ package com.x8bit.bitwarden.ui.vault.feature.vault -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut @@ -20,7 +19,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -100,7 +98,6 @@ fun VaultScreen( appReviewManager: AppReviewManager = LocalAppReviewManager.current, ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val context = LocalContext.current val pullToRefreshState = rememberBitwardenPullToRefreshState( isEnabled = state.isPullToRefreshEnabled, isRefreshing = state.isRefreshing, @@ -164,12 +161,6 @@ fun VaultScreen( is VaultEvent.NavigateToUrl -> intentManager.launchUri(event.url.toUri()) VaultEvent.NavigateOutOfApp -> exitManager.exitApplication() - is VaultEvent.ShowToast -> { - Toast - .makeText(context, event.message(context.resources), Toast.LENGTH_SHORT) - .show() - } - VaultEvent.NavigateToImportLogins -> onNavigateToImportLogins() is VaultEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data) VaultEvent.PromptForAppReview -> { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index baf155744a..615b5d9d32 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -4,6 +4,7 @@ import android.os.Parcelable import androidx.compose.ui.graphics.Color import androidx.lifecycle.viewModelScope import com.bitwarden.core.data.repository.model.DataState +import com.bitwarden.core.util.persistentListOfNotNull import com.bitwarden.data.repository.util.baseIconUrl import com.bitwarden.network.model.PolicyTypeJson import com.bitwarden.ui.platform.base.BackgroundEvent @@ -72,8 +73,6 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import java.time.Clock import javax.inject.Inject -import kotlin.collections.emptyList -import kotlin.collections.map /** * Manages [VaultState], handles [VaultAction], and launches [VaultEvent] for the [VaultScreen]. @@ -252,15 +251,12 @@ class VaultViewModel @Inject constructor( private fun handleSelectAddItemType() { // If policy is enable for any organization, exclude the card option - var excludedOptions = - if (!state.restrictItemTypesPolicyOrgIds.isNullOrEmpty()) { - persistentListOf( - CreateVaultItemType.SSH_KEY, - CreateVaultItemType.CARD, - ) - } else { - persistentListOf(CreateVaultItemType.SSH_KEY) - } + val excludedOptions = persistentListOfNotNull( + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.CARD.takeUnless { + state.restrictItemTypesPolicyOrgIds.isNullOrEmpty() + }, + ) mutableStateFlow.update { it.copy( @@ -816,11 +812,7 @@ class VaultViewModel @Inject constructor( private fun vaultLoadedReceive(vaultData: DataState.Loaded) { if (state.dialog == VaultState.DialogState.Syncing) { - sendEvent( - VaultEvent.ShowToast( - message = R.string.syncing_complete.asText(), - ), - ) + sendEvent(VaultEvent.ShowSnackbar(message = R.string.syncing_complete.asText())) } updateVaultState(vaultData.data) } @@ -1377,15 +1369,26 @@ sealed class VaultEvent { */ data object PromptForAppReview : VaultEvent() - /** - * Show a toast with the given [message]. - */ - data class ShowToast(val message: Text) : VaultEvent() - /** * Show a snackbar with the given [data]. */ - data class ShowSnackbar(val data: BitwardenSnackbarData) : VaultEvent(), BackgroundEvent + data class ShowSnackbar( + val data: BitwardenSnackbarData, + ) : VaultEvent(), BackgroundEvent { + constructor( + message: Text, + messageHeader: Text? = null, + actionLabel: Text? = null, + withDismissAction: Boolean = false, + ) : this( + data = BitwardenSnackbarData( + message = message, + messageHeader = messageHeader, + actionLabel = actionLabel, + withDismissAction = withDismissAction, + ), + ) + } /** * Navigate to the add folder screen diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 29ada594cb..ff8785a940 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -862,7 +862,7 @@ class VaultViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `vaultDataStateFlow Loaded with items when manually syncing with the sync button should update state to Content, show a success Toast, and dismiss pull to refresh`() = + fun `vaultDataStateFlow Loaded with items when manually syncing with the sync button should update state to Content, show a success Snackbar, and dismiss pull to refresh`() = runTest { val expectedState = createMockVaultState( viewState = VaultState.ViewState.Content( @@ -898,7 +898,7 @@ class VaultViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) assertEquals( - VaultEvent.ShowToast(R.string.syncing_complete.asText()), + VaultEvent.ShowSnackbar(R.string.syncing_complete.asText()), awaitItem(), ) } @@ -925,7 +925,7 @@ class VaultViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `vaultDataStateFlow Loaded with empty items when manually syncing with the sync button should update state to NoItems, show a success Toast, and dismiss pull to refresh`() = + fun `vaultDataStateFlow Loaded with empty items when manually syncing with the sync button should update state to NoItems, show a success Snackbar, and dismiss pull to refresh`() = runTest { val expectedState = createMockVaultState( viewState = VaultState.ViewState.NoItems, @@ -945,7 +945,7 @@ class VaultViewModelTest : BaseViewModelTest() { assertEquals(expectedState, viewModel.stateFlow.value) assertEquals( - VaultEvent.ShowToast(R.string.syncing_complete.asText()), + VaultEvent.ShowSnackbar(R.string.syncing_complete.asText()), awaitItem(), ) }