PM-22551: Update toasts to snackbars for Sends (#5339)

This commit is contained in:
David Perez
2025-06-11 16:33:59 +00:00
committed by GitHub
parent 2a8a16ab3f
commit 2dc6c170f5
25 changed files with 369 additions and 66 deletions
@@ -84,6 +84,6 @@ data class BitwardenSnackbarData(
fun rememberBitwardenSnackbarHostState(
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
scope: CoroutineScope = rememberCoroutineScope(),
) = remember {
): BitwardenSnackbarHostState = remember {
BitwardenSnackbarHostState(snackbarHostState = snackbarHostState, scope = scope)
}
@@ -31,6 +31,8 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingConten
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.search.handlers.SearchHandlers
@@ -72,6 +74,7 @@ fun SearchScreen(
)
}
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
SearchEvent.NavigateBack -> onNavigateBack()
@@ -111,6 +114,7 @@ fun SearchScreen(
is SearchEvent.NavigateToUrl -> intentManager.launchUri(event.url.toUri())
is SearchEvent.ShowShareSheet -> intentManager.shareText(event.content)
is SearchEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
is SearchEvent.ShowToast -> {
Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
@@ -159,6 +163,7 @@ fun SearchScreen(
)
}
},
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
@@ -9,6 +9,7 @@ import com.bitwarden.data.repository.util.baseIconUrl
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.send.SendType
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
@@ -39,12 +40,15 @@ import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.model.AutofillSelectionOption
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
import com.x8bit.bitwarden.ui.platform.feature.search.util.toSearchTypeData
import com.x8bit.bitwarden.ui.platform.feature.search.util.toViewState
import com.x8bit.bitwarden.ui.platform.feature.search.util.updateWithAdditionalDataIfNecessary
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendItemType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
@@ -81,6 +85,7 @@ class SearchViewModel @Inject constructor(
private val accessibilitySelectionManager: AccessibilitySelectionManager,
private val autofillSelectionManager: AutofillSelectionManager,
private val organizationEventManager: OrganizationEventManager,
private val snackbarRelayManager: SnackbarRelayManager,
private val vaultRepo: VaultRepository,
private val authRepo: AuthRepository,
environmentRepo: EnvironmentRepository,
@@ -135,6 +140,11 @@ class SearchViewModel @Inject constructor(
.map { SearchAction.Internal.VaultDataReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.SEND_DELETED, SnackbarRelay.SEND_UPDATED)
.map { SearchAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: SearchAction) {
@@ -473,6 +483,8 @@ class SearchViewModel @Inject constructor(
handleRemovePasswordSendResultReceive(action)
}
is SearchAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action)
is SearchAction.Internal.UpdateCipherResultReceive -> {
handleUpdateCipherResultReceive(action)
}
@@ -554,6 +566,10 @@ class SearchViewModel @Inject constructor(
}
}
private fun handleSnackbarDataReceived(action: SearchAction.Internal.SnackbarDataReceived) {
sendEvent(SearchEvent.ShowSnackbar(action.data))
}
private fun handleUpdateCipherResultReceive(
action: SearchAction.Internal.UpdateCipherResultReceive,
) {
@@ -1184,6 +1200,13 @@ sealed class SearchAction {
val result: RemovePasswordSendResult,
) : Internal()
/**
* Indicates that snackbar data has been received.
*/
data class SnackbarDataReceived(
val data: BitwardenSnackbarData,
) : Internal()
/**
* Indicates a result for updating a cipher during the autofill-and-save process.
*/
@@ -1265,6 +1288,13 @@ sealed class SearchEvent {
val content: String,
) : SearchEvent()
/**
* Show a snackbar to the user.
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : SearchEvent(), BackgroundEvent
/**
* Show a toast with the given [message].
*/
@@ -11,4 +11,5 @@ import kotlinx.serialization.Serializable
enum class SnackbarRelay {
LOGINS_IMPORTED,
SEND_DELETED,
SEND_UPDATED,
}
@@ -16,7 +16,10 @@ interface SnackbarRelayManager {
/**
* Called from a consumer to receive snackbar data from a producer, the consumer must specify
* the [relay] to receive the data from.
* the [relay] or [relays] to receive the data from.
*/
fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData>
fun getSnackbarDataFlow(
relay: SnackbarRelay,
vararg relays: SnackbarRelay,
): Flow<BitwardenSnackbarData>
}
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.launch
@@ -34,9 +35,14 @@ class SnackbarRelayManagerImpl(
}
}
override fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData> =
snackbarSharedFlow
.generateFlowFor(relay = relay)
override fun getSnackbarDataFlow(
relay: SnackbarRelay,
vararg relays: SnackbarRelay,
): Flow<BitwardenSnackbarData> =
merge(
snackbarSharedFlow.generateFlowFor(relay = relay),
*relays.map { snackbarSharedFlow.generateFlowFor(relay = it) }.toTypedArray(),
)
.map { it.data }
}
@@ -38,6 +38,8 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialo
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.model.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalAppResumeStateManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
@@ -82,6 +84,7 @@ fun SendScreen(
AppResumeScreenData.SendScreen
}
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is SendEvent.NavigateToSearch -> onNavigateToSearchSend(SearchType.Sends.All)
@@ -116,6 +119,8 @@ fun SendScreen(
intentManager.shareText(event.url)
}
is SendEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
is SendEvent.ShowToast -> {
Toast
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
@@ -197,6 +202,7 @@ fun SendScreen(
}
},
pullToRefreshState = pullToRefreshState,
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) {
val modifier = Modifier
.fillMaxSize()
@@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
@@ -23,6 +24,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemScreen
@@ -49,6 +53,7 @@ class SendViewModel @Inject constructor(
authRepo: AuthRepository,
settingsRepo: SettingsRepository,
policyManager: PolicyManager,
snackbarRelayManager: SnackbarRelayManager,
private val clipboardManager: BitwardenClipboardManager,
private val environmentRepo: EnvironmentRepository,
private val vaultRepo: VaultRepository,
@@ -89,6 +94,11 @@ class SendViewModel @Inject constructor(
.map { SendAction.Internal.UserStateReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.SEND_DELETED, SnackbarRelay.SEND_UPDATED)
.map { SendAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: SendAction): Unit = when (action) {
@@ -132,6 +142,7 @@ class SendViewModel @Inject constructor(
}
is SendAction.Internal.UserStateReceive -> handleUserStateReceive(action)
is SendAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action)
}
private fun handleInternetConnectionErrorReceived() {
@@ -152,6 +163,10 @@ class SendViewModel @Inject constructor(
}
}
private fun handleSnackbarDataReceived(action: SendAction.Internal.SnackbarDataReceived) {
sendEvent(SendEvent.ShowSnackbar(action.data))
}
private fun handlePullToRefreshEnableReceive(
action: SendAction.Internal.PullToRefreshEnableReceive,
) {
@@ -701,6 +716,13 @@ sealed class SendAction {
val result: RemovePasswordSendResult,
) : Internal()
/**
* Indicates that snackbar data has been received from a relay to be displayed.
*/
data class SnackbarDataReceived(
val data: BitwardenSnackbarData,
) : Internal()
/**
* Indicates what the current [userState] is.
*/
@@ -781,6 +803,13 @@ sealed class SendEvent {
*/
data class ShowShareSheet(val url: String) : SendEvent()
/**
* Show a snackbar to the user.
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : SendEvent(), BackgroundEvent
/**
* Show a toast to the user.
*/
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.tools.feature.send.addedit
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -11,7 +10,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.hilt.navigation.compose.hiltViewModel
@@ -31,6 +29,8 @@ import com.x8bit.bitwarden.ui.platform.components.content.BitwardenLoadingConten
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.composition.LocalPermissionsManager
@@ -57,15 +57,13 @@ fun AddEditSendScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val addSendHandlers = remember(viewModel) { AddEditSendHandlers.create(viewModel) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val resources = context.resources
val fileChooserLauncher = intentManager.getActivityResultLauncher { activityResult ->
intentManager.getFileDataFromActivityResult(activityResult)?.let {
addSendHandlers.onFileChoose(it)
}
}
val snackbarHostState = rememberBitwardenSnackbarHostState()
BackHandler(
onBack = remember(viewModel) {
{ viewModel.trySendAction(AddEditSendAction.CloseClick) }
@@ -89,9 +87,7 @@ fun AddEditSendScreen(
intentManager.shareText(event.message)
}
is AddEditSendEvent.ShowToast -> {
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
}
is AddEditSendEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}
@@ -166,6 +162,7 @@ fun AddEditSendScreen(
},
)
},
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) {
val modifier = Modifier
.fillMaxSize()
@@ -27,7 +27,10 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AddEditSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.shouldFinishOnComplete
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendName
@@ -70,6 +73,7 @@ class AddEditSendViewModel @Inject constructor(
private val vaultRepo: VaultRepository,
private val policyManager: PolicyManager,
private val networkConnectionManager: NetworkConnectionManager,
private val snackbarRelayManager: SnackbarRelayManager,
) : BaseViewModel<AddEditSendState, AddEditSendEvent, AddEditSendAction>(
// We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] ?: run {
@@ -250,6 +254,10 @@ class AddEditSendViewModel @Inject constructor(
message = result.sendView.toSendUrl(state.baseWebSendUrl),
),
)
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(message = R.string.send_updated.asText()),
relay = SnackbarRelay.SEND_UPDATED,
)
}
}
}
@@ -273,7 +281,10 @@ class AddEditSendViewModel @Inject constructor(
is DeleteSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
navigateBack(isDeleted = true)
sendEvent(AddEditSendEvent.ShowToast(message = R.string.send_deleted.asText()))
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(message = R.string.send_deleted.asText()),
relay = SnackbarRelay.SEND_DELETED,
)
}
}
}
@@ -301,7 +312,11 @@ class AddEditSendViewModel @Inject constructor(
updateCommonContent { it.copy(hasPassword = false) }
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(
AddEditSendEvent.ShowToast(message = R.string.send_password_removed.asText()),
AddEditSendEvent.ShowSnackbar(
data = BitwardenSnackbarData(
message = R.string.send_password_removed.asText(),
),
),
)
}
}
@@ -845,9 +860,11 @@ sealed class AddEditSendEvent {
) : BackgroundEvent, AddEditSendEvent()
/**
* Show Toast.
* Show a snackbar.
*/
data class ShowToast(val message: Text) : AddEditSendEvent()
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : AddEditSendEvent()
}
/**
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.tools.feature.send.viewsend
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@@ -68,6 +67,8 @@ import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@@ -89,6 +90,7 @@ fun ViewSendScreen(
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
val resources = context.resources
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is ViewSendEvent.NavigateBack -> onNavigateBack()
@@ -106,9 +108,7 @@ fun ViewSendScreen(
intentManager.shareText(text = event.text(resources).toString())
}
is ViewSendEvent.ShowToast -> {
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
}
is ViewSendEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}
@@ -153,6 +153,7 @@ fun ViewSendScreen(
)
}
},
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) {
ViewSendScreenContent(
state = state,
@@ -16,6 +16,9 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.util.toViewSendViewStateContent
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -37,6 +40,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class ViewSendViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager,
private val snackbarRelayManager: SnackbarRelayManager,
private val clock: Clock,
private val vaultRepository: VaultRepository,
environmentRepository: EnvironmentRepository,
@@ -60,6 +64,11 @@ class ViewSendViewModel @Inject constructor(
.map { ViewSendAction.Internal.SendDataReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.SEND_UPDATED)
.map { ViewSendAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: ViewSendAction) {
@@ -79,6 +88,7 @@ class ViewSendViewModel @Inject constructor(
when (action) {
is ViewSendAction.Internal.SendDataReceive -> handleSendDataReceive(action)
is ViewSendAction.Internal.DeleteResultReceive -> handleDeleteResultReceive(action)
is ViewSendAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action)
}
}
@@ -134,12 +144,19 @@ class ViewSendViewModel @Inject constructor(
is DeleteSendResult.Success -> {
mutableStateFlow.update { it.copy(dialogState = null) }
sendEvent(ViewSendEvent.ShowToast(message = R.string.send_deleted.asText()))
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(message = R.string.send_deleted.asText()),
relay = SnackbarRelay.SEND_DELETED,
)
sendEvent(ViewSendEvent.NavigateBack)
}
}
}
private fun handleSnackbarDataReceived(action: ViewSendAction.Internal.SnackbarDataReceived) {
sendEvent(ViewSendEvent.ShowSnackbar(action.data))
}
private fun handleSendDataReceive(action: ViewSendAction.Internal.SendDataReceive) {
when (val dataState = action.sendDataState) {
is DataState.Error -> sendErrorReceive(dataState = dataState)
@@ -340,10 +357,10 @@ sealed class ViewSendEvent {
) : ViewSendEvent()
/**
* Shows the [message] via a toast.
* Show a snackbar to the user.
*/
data class ShowToast(
val message: Text,
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : ViewSendEvent(), BackgroundEvent
}
@@ -399,5 +416,12 @@ sealed class ViewSendAction {
* Indicates that the send item data has been received.
*/
data class SendDataReceive(val sendDataState: DataState<SendView?>) : Internal()
/**
* Indicates that snackbar data has been received.
*/
data class SnackbarDataReceived(
val data: BitwardenSnackbarData,
) : Internal()
}
}
@@ -45,6 +45,9 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialo
import com.x8bit.bitwarden.ui.platform.components.model.BitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.model.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
import com.x8bit.bitwarden.ui.platform.composition.LocalCredentialProviderCompletionManager
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
@@ -105,6 +108,7 @@ fun VaultItemListingScreen(
{ viewModel.trySendAction(VaultItemListingsAction.RefreshPull) }
},
)
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event ->
when (event) {
is VaultItemListingEvent.NavigateBack -> onNavigateBack()
@@ -217,6 +221,8 @@ fun VaultItemListingScreen(
is VaultItemListingEvent.NavigateToAddFolder -> {
onNavigateToAddFolder(event.parentFolderName)
}
is VaultItemListingEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
}
}
@@ -325,6 +331,7 @@ fun VaultItemListingScreen(
BackHandler(onBack = vaultItemListingHandlers.backClick)
VaultItemListingScaffold(
state = state,
snackbarHostState = snackbarHostState,
pullToRefreshState = pullToRefreshState,
vaultItemListingHandlers = vaultItemListingHandlers,
)
@@ -478,6 +485,7 @@ private fun VaultItemListingDialogs(
private fun VaultItemListingScaffold(
state: VaultItemListingState,
pullToRefreshState: BitwardenPullToRefreshState,
snackbarHostState: BitwardenSnackbarHostState,
vaultItemListingHandlers: VaultItemListingHandlers,
) {
var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) }
@@ -553,6 +561,7 @@ private fun VaultItemListingScaffold(
)
},
pullToRefreshState = pullToRefreshState,
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) {
when (state.viewState) {
is VaultItemListingState.ViewState.Content -> {
@@ -72,9 +72,12 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.util.toSendItemType
@@ -132,6 +135,7 @@ class VaultItemListingViewModel @Inject constructor(
private val bitwardenCredentialManager: BitwardenCredentialManager,
private val organizationEventManager: OrganizationEventManager,
private val networkConnectionManager: NetworkConnectionManager,
private val snackbarRelayManager: SnackbarRelayManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@@ -189,6 +193,12 @@ class VaultItemListingViewModel @Inject constructor(
.onEach(::sendAction)
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.SEND_DELETED, SnackbarRelay.SEND_UPDATED)
.map { VaultItemListingsAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
specialCircumstanceManager.specialCircumstance
?.toCreateCredentialRequestOrNull()
?.let { request ->
@@ -1403,6 +1413,10 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.Internal.GetCredentialEntriesResultReceive -> {
handleGetCredentialEntriesResultReceive(action)
}
is VaultItemListingsAction.Internal.SnackbarDataReceived -> {
handleSnackbarDataReceived(action)
}
}
}
@@ -2038,6 +2052,12 @@ class VaultItemListingViewModel @Inject constructor(
}
}
private fun handleSnackbarDataReceived(
action: VaultItemListingsAction.Internal.SnackbarDataReceived,
) {
sendEvent(VaultItemListingEvent.ShowSnackbar(action.data))
}
private fun updateStateWithVaultData(vaultData: VaultData, clearDialogState: Boolean) {
mutableStateFlow.update { currentState ->
currentState.copy(
@@ -2769,6 +2789,13 @@ sealed class VaultItemListingEvent {
*/
data class ShowToast(val text: Text) : VaultItemListingEvent()
/**
* Show a snackbar to the user.
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : VaultItemListingEvent(), BackgroundEvent
/**
* Complete the current FIDO 2 credential registration process.
*
@@ -3138,6 +3165,13 @@ sealed class VaultItemListingsAction {
val userId: String,
val result: Result<List<CredentialEntry>>,
) : Internal()
/**
* Indicates that snackbar data has been received.
*/
data class SnackbarDataReceived(
val data: BitwardenSnackbarData,
) : Internal()
}
}
+1
View File
@@ -466,6 +466,7 @@ Scanning will happen automatically.</string>
<string name="add_text_send">New text Send</string>
<string name="are_you_sure_delete_send">Are you sure you want to delete this Send?</string>
<string name="send_deleted">Send deleted</string>
<string name="send_updated">Send updated</string>
<string name="one_day">1 day</string>
<string name="two_days">2 days</string>
<string name="three_days">3 days</string>
@@ -26,6 +26,7 @@ import com.bitwarden.ui.util.isProgressBar
import com.bitwarden.vault.CipherType
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.model.AutofillSelectionOption
import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForCipher
import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForSend
@@ -151,6 +152,16 @@ class SearchScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `on ShowSnackbar event should display the snackbar`() {
val message = "message"
val data = BitwardenSnackbarData(message = message.asText())
mutableEventFlow.tryEmit(SearchEvent.ShowSnackbar(data = data))
composeTestRule
.onNodeWithText(text = message)
.assertIsDisplayed()
}
@Test
fun `ShowShareSheet should call onNavigateBack`() {
val sendUrl = "www.test.com"
@@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.network.model.PolicyTypeJson
@@ -50,10 +51,12 @@ import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.search.util.createMockDisplayItemForCipher
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
import com.x8bit.bitwarden.ui.platform.feature.search.util.toViewState
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@@ -69,6 +72,7 @@ import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
@@ -129,6 +133,13 @@ class SearchViewModelTest : BaseViewModelTest() {
private val organizationEventManager = mockk<OrganizationEventManager> {
every { trackEvent(event = any()) } just runs
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every {
getSnackbarDataFlow(relay = any(), relays = anyVararg())
} returns mutableSnackbarDataFlow
}
@BeforeEach
fun setup() {
@@ -1554,6 +1565,16 @@ class SearchViewModelTest : BaseViewModelTest() {
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
@Test
fun `SnackbarDataReceive should update emit ShowSnackbar`() = runTest {
val viewModel = createViewModel()
val snackbarData = BitwardenSnackbarData(message = "Test".asText())
viewModel.eventFlow.test {
mutableSnackbarDataFlow.tryEmit(snackbarData)
assertEquals(SearchEvent.ShowSnackbar(data = snackbarData), awaitItem())
}
}
@Suppress("CyclomaticComplexMethod")
private fun createViewModel(
initialState: SearchState? = null,
@@ -1599,6 +1620,7 @@ class SearchViewModelTest : BaseViewModelTest() {
accessibilitySelectionManager = accessibilitySelectionManager,
autofillSelectionManager = autofillSelectionManager,
organizationEventManager = organizationEventManager,
snackbarRelayManager = snackbarRelayManager,
)
/**
@@ -28,6 +28,7 @@ import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.data.platform.manager.util.AppResumeStateManager
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.ModeType
@@ -139,6 +140,16 @@ class SendScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `on ShowSnackbar event should display the snackbar`() {
val message = "message"
val data = BitwardenSnackbarData(message = message.asText())
mutableEventFlow.tryEmit(SendEvent.ShowSnackbar(data = data))
composeTestRule
.onNodeWithText(text = message)
.assertIsDisplayed()
}
@Test
fun `on ShowShareSheet should call shareText on IntentManager`() {
val text = "sharable stuff"
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.data.repository.util.baseWebSendUrl
import com.bitwarden.network.model.PolicyTypeJson
@@ -23,6 +24,8 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.SendData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.util.toViewState
import io.mockk.coEvery
@@ -34,6 +37,7 @@ import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.advanceTimeBy
@@ -72,6 +76,13 @@ class SendViewModelTest : BaseViewModelTest() {
private val networkConnectionManager: NetworkConnectionManager = mockk {
every { isNetworkConnected } returns true
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every {
getSnackbarDataFlow(relay = any(), relays = anyVararg())
} returns mutableSnackbarDataFlow
}
@BeforeEach
fun setup() {
@@ -629,6 +640,16 @@ class SendViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `SnackbarDataReceive should update emit ShowSnackbar`() = runTest {
val viewModel = createViewModel()
val snackbarData = BitwardenSnackbarData(message = "Test".asText())
viewModel.eventFlow.test {
mutableSnackbarDataFlow.tryEmit(snackbarData)
assertEquals(SendEvent.ShowSnackbar(data = snackbarData), awaitItem())
}
}
@Suppress("LongParameterList")
private fun createViewModel(
state: SendState? = null,
@@ -649,6 +670,7 @@ class SendViewModelTest : BaseViewModelTest() {
vaultRepo = vaultRepository,
policyManager = policyManager,
networkConnectionManager = networkConnectionManager,
snackbarRelayManager = snackbarRelayManager,
)
}
@@ -27,6 +27,7 @@ import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.isEditableText
import com.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
@@ -100,6 +101,16 @@ class AddEditSendScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `on ShowSnackbar event should display the snackbar`() {
val message = "message"
val data = BitwardenSnackbarData(message = message.asText())
mutableEventFlow.tryEmit(AddEditSendEvent.ShowSnackbar(data = data))
composeTestRule
.onNodeWithText(text = message)
.assertIsDisplayed()
}
@Test
fun `on ShowShareSheet should call shareText on IntentManager`() {
val text = "sharable stuff"
@@ -24,7 +24,10 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.data.vault.repository.model.RemovePasswordSendResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.model.AddEditSendType
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toSendView
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.util.toViewState
@@ -81,6 +84,9 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
private val networkConnectionManager = mockk<NetworkConnectionManager> {
every { isNetworkConnected } returns true
}
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every { sendSnackbarData(data = any(), relay = any()) } just runs
}
@BeforeEach
fun setup() {
@@ -594,7 +600,11 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
viewModel.eventFlow.test {
viewModel.trySendAction(AddEditSendAction.RemovePasswordClick)
assertEquals(
AddEditSendEvent.ShowToast(R.string.send_password_removed.asText()),
AddEditSendEvent.ShowSnackbar(
data = BitwardenSnackbarData(
message = R.string.send_password_removed.asText(),
),
),
awaitItem(),
)
}
@@ -649,22 +659,28 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
}
@Test
fun `DeleteClick vaultRepository deleteSend Success should show toast`() = runTest {
val sendId = "mockId-1"
coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Success
val viewModel = createViewModel(
state = DEFAULT_STATE.copy(
fun `DeleteClick vaultRepository deleteSend Success should emit NavigateUpToSearchOrRoot`() =
runTest {
val sendId = "mockId-1"
coEvery { vaultRepository.deleteSend(sendId) } returns DeleteSendResult.Success
val viewModel = createViewModel(
state = DEFAULT_STATE.copy(
addEditSendType = AddEditSendType.EditItem(sendItemId = sendId),
),
addEditSendType = AddEditSendType.EditItem(sendItemId = sendId),
),
addEditSendType = AddEditSendType.EditItem(sendItemId = sendId),
)
)
viewModel.eventFlow.test {
viewModel.trySendAction(AddEditSendAction.DeleteClick)
assertEquals(AddEditSendEvent.NavigateUpToSearchOrRoot, awaitItem())
assertEquals(AddEditSendEvent.ShowToast(R.string.send_deleted.asText()), awaitItem())
viewModel.eventFlow.test {
viewModel.trySendAction(AddEditSendAction.DeleteClick)
assertEquals(AddEditSendEvent.NavigateUpToSearchOrRoot, awaitItem())
}
verify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(message = R.string.send_deleted.asText()),
relay = SnackbarRelay.SEND_DELETED,
)
}
}
}
@Test
fun `ShareLinkClick with nonnull sendUrl should launch share sheet`() = runTest {
@@ -959,6 +975,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
vaultRepo = vaultRepository,
policyManager = policyManager,
networkConnectionManager = networkConnectionManager,
snackbarRelayManager = snackbarRelayManager,
)
}
@@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.tools.feature.send.viewsend
import android.widget.Toast
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
@@ -16,6 +15,7 @@ import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.assertNoDialogExists
import com.bitwarden.ui.util.isProgressBar
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.ModeType
@@ -23,13 +23,10 @@ import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -52,7 +49,6 @@ class ViewSendScreenTest : BitwardenComposeTest() {
@Before
fun setup() {
mockkStatic(Toast::class)
setContent(
intentManager = intentManager,
) {
@@ -64,11 +60,6 @@ class ViewSendScreenTest : BitwardenComposeTest() {
}
}
@After
fun tearDown() {
unmockkStatic(Toast::class)
}
@Test
fun `on NavigateBack event should call onNavigateBack`() {
mutableEventFlow.tryEmit(ViewSendEvent.NavigateBack)
@@ -106,16 +97,13 @@ class ViewSendScreenTest : BitwardenComposeTest() {
}
@Test
fun `on ShowToast event should call onNavigateToEdit`() {
fun `on ShowSnackbar event should display the snackbar`() {
val message = "message"
val toast = mockk<Toast> {
every { show() } just runs
}
every { Toast.makeText(any(), message, Toast.LENGTH_SHORT) } returns toast
mutableEventFlow.tryEmit(ViewSendEvent.ShowToast(message = message.asText()))
verify(exactly = 1) {
toast.show()
}
val data = BitwardenSnackbarData(message = message.asText())
mutableEventFlow.tryEmit(ViewSendEvent.ShowSnackbar(data = data))
composeTestRule
.onNodeWithText(text = message)
.assertIsDisplayed()
}
@Test
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.tools.feature.send.viewsend
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.send.SendView
import com.bitwarden.ui.platform.base.BaseViewModelTest
@@ -14,6 +15,9 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DeleteSendResult
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.util.toViewSendViewStateContent
import io.mockk.coEvery
@@ -24,6 +28,7 @@ import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
@@ -46,6 +51,14 @@ class ViewSendViewModelTest : BaseViewModelTest() {
private val environmentRepository = mockk<EnvironmentRepository> {
every { environment } returns Environment.Us
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every { sendSnackbarData(data = any(), relay = any()) } just runs
every {
getSnackbarDataFlow(relay = any(), relays = anyVararg())
} returns mutableSnackbarDataFlow
}
@BeforeEach
fun setup() {
@@ -149,7 +162,7 @@ class ViewSendViewModelTest : BaseViewModelTest() {
}
@Test
fun `on DeleteClick with success should display toast`() = runTest {
fun `on DeleteClick with success should navigate back`() = runTest {
val initialState = DEFAULT_STATE.copy(viewState = DEFAULT_CONTENT_VIEW_STATE)
val sendView = createMockSendView(number = 1)
every {
@@ -175,15 +188,17 @@ class ViewSendViewModelTest : BaseViewModelTest() {
initialState.copy(dialogState = null),
stateFlow.awaitItem(),
)
assertEquals(
ViewSendEvent.ShowToast(message = R.string.send_deleted.asText()),
eventFLow.awaitItem(),
)
assertEquals(
ViewSendEvent.NavigateBack,
eventFLow.awaitItem(),
)
}
verify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(message = R.string.send_deleted.asText()),
relay = SnackbarRelay.SEND_DELETED,
)
}
}
@Test
@@ -390,6 +405,16 @@ class ViewSendViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `SnackbarDataReceive should update emit ShowSnackbar`() = runTest {
val viewModel = createViewModel()
val snackbarData = BitwardenSnackbarData(message = "Test".asText())
viewModel.eventFlow.test {
mutableSnackbarDataFlow.tryEmit(snackbarData)
assertEquals(ViewSendEvent.ShowSnackbar(data = snackbarData), awaitItem())
}
}
private fun createViewModel(
state: ViewSendState? = null,
): ViewSendViewModel = ViewSendViewModel(
@@ -397,8 +422,8 @@ class ViewSendViewModelTest : BaseViewModelTest() {
clock = FIXED_CLOCK,
vaultRepository = vaultRepository,
environmentRepository = environmentRepository,
savedStateHandle = SavedStateHandle().apply
{
snackbarRelayManager = snackbarRelayManager,
savedStateHandle = SavedStateHandle().apply {
set(key = "state", value = state)
every { toViewSendArgs() } returns ViewSendArgs(
sendId = (state ?: DEFAULT_STATE).sendId,
@@ -37,6 +37,7 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialR
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
@@ -368,6 +369,16 @@ class VaultItemListingScreenTest : BitwardenComposeTest() {
}
}
@Test
fun `on ShowSnackbar event should display the snackbar`() {
val message = "message"
val data = BitwardenSnackbarData(message = message.asText())
mutableEventFlow.tryEmit(VaultItemListingEvent.ShowSnackbar(data = data))
composeTestRule
.onNodeWithText(text = message)
.assertIsDisplayed()
}
@Test
fun `back gesture should send BackClick action`() {
backDispatcher?.onBackPressed()
@@ -15,6 +15,7 @@ import androidx.credentials.provider.PublicKeyCredentialEntry
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
@@ -86,7 +87,9 @@ import com.x8bit.bitwarden.ui.credentials.manager.model.GetCredentialsResult
import com.x8bit.bitwarden.ui.credentials.manager.model.RegisterFido2CredentialResult
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
@@ -112,6 +115,7 @@ import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.advanceTimeBy
@@ -244,6 +248,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { callingRequest } returns mockk<CreatePublicKeyCredentialRequest>(relaxed = true)
every { callingAppInfo } returns mockCallingAppInfo
}
private val mutableSnackbarDataFlow: MutableSharedFlow<BitwardenSnackbarData> =
bufferedMutableSharedFlow()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every {
getSnackbarDataFlow(relay = any(), relays = anyVararg())
} returns mutableSnackbarDataFlow
}
@BeforeEach
fun setUp() {
@@ -4949,6 +4960,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `SnackbarDataReceive should update emit ShowSnackbar`() = runTest {
val viewModel = createVaultItemListingViewModel()
val snackbarData = BitwardenSnackbarData(message = "Test".asText())
viewModel.eventFlow.test {
mutableSnackbarDataFlow.tryEmit(snackbarData)
assertEquals(VaultItemListingEvent.ShowSnackbar(data = snackbarData), awaitItem())
}
}
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
): SavedStateHandle = SavedStateHandle().apply {
@@ -4985,6 +5006,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
originManager = originManager,
networkConnectionManager = networkConnectionManager,
privilegedAppRepository = privilegedAppRepository,
snackbarRelayManager = snackbarRelayManager,
)
@Suppress("MaxLineLength")