Update the SnackbarRelayManager (#5317)

This commit is contained in:
David Perez 2025-06-06 13:28:08 -05:00 committed by GitHub
parent e1cd813445
commit beb4c533c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 180 additions and 236 deletions

View File

@ -27,7 +27,6 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther
import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDestination
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.util.toObjectRoute
import kotlinx.serialization.Serializable
@ -95,7 +94,7 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
navigation<SettingsGraphRoute>(
startDestination = SettingsRoute.Standard,

View File

@ -4,7 +4,6 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import kotlinx.serialization.Serializable
/**
@ -20,7 +19,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
) {
composableWithPushTransitions<VaultSettingsRoute> {
VaultSettingsScreen(

View File

@ -42,7 +42,6 @@ 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.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
/**
* Displays the vault settings screen.
@ -54,7 +53,7 @@ fun VaultSettingsScreen(
onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
@ -73,7 +72,7 @@ fun VaultSettingsScreen(
is VaultSettingsEvent.NavigateToImportVault -> {
if (state.isNewImportLoginsFlowEnabled) {
onNavigateToImportLogins(SnackbarRelay.VAULT_SETTINGS_RELAY)
onNavigateToImportLogins()
} else {
intentManager.launchUri(event.url.toUri())
}

View File

@ -60,10 +60,8 @@ class VaultSettingsViewModel @Inject constructor(
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.VAULT_SETTINGS_RELAY)
.map {
VaultSettingsAction.Internal.SnackbarDataReceived(it)
}
.getSnackbarDataFlow(SnackbarRelay.LOGINS_IMPORTED)
.map { VaultSettingsAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}

View File

@ -113,9 +113,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
},
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
onNavigateToImportLogins = {
navController.navigateToImportLoginsScreen(snackbarRelay = it)
},
onNavigateToImportLogins = { navController.navigateToImportLoginsScreen() },
onNavigateToAddFolderScreen = {
navController.navigateToFolderAddEdit(
folderAddEditType = FolderAddEditType.AddItem,

View File

@ -5,7 +5,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithStayTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
@ -46,7 +45,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderName: String?) -> Unit,
) {
composableWithStayTransitions<VaultUnlockedNavbarRoute> {

View File

@ -31,7 +31,6 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraphRoot
import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph
import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.model.VaultUnlockedNavBarTab
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph
import com.x8bit.bitwarden.ui.tools.feature.generator.navigateToGeneratorGraph
import com.x8bit.bitwarden.ui.tools.feature.send.addedit.AddEditSendRoute
@ -71,7 +70,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -178,7 +177,7 @@ private fun VaultUnlockedNavBarScaffold(
onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToFlightRecorder: () -> Unit,
onNavigateToRecordedLogs: () -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
) {
var shouldDimNavBar by rememberSaveable { mutableStateOf(value = false) }

View File

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.manager.di
import android.content.Context
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
@ -40,5 +41,9 @@ class PlatformUiManagerModule {
@Provides
@Singleton
fun provideSnackbarRelayManager(): SnackbarRelayManager = SnackbarRelayManagerImpl()
fun provideSnackbarRelayManager(
dispatcherManager: DispatcherManager,
): SnackbarRelayManager = SnackbarRelayManagerImpl(
dispatcherManager = dispatcherManager,
)
}

View File

@ -9,6 +9,6 @@ import kotlinx.serialization.Serializable
*/
@Serializable
enum class SnackbarRelay {
VAULT_SETTINGS_RELAY,
MY_VAULT_RELAY,
LOGINS_IMPORTED,
SEND_DELETED,
}

View File

@ -19,9 +19,4 @@ interface SnackbarRelayManager {
* the [relay] to receive the data from.
*/
fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData>
/**
* Clears the buffer for the given [relay].
*/
fun clearRelayBuffer(relay: SnackbarRelay)
}

View File

@ -1,41 +1,77 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.repository.util.emitWhenSubscribedTo
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.launch
import java.util.UUID
/**
* The default implementation of the [SnackbarRelayManager] interface.
*/
class SnackbarRelayManagerImpl : SnackbarRelayManager {
private val mutableSnackbarRelayMap =
mutableMapOf<SnackbarRelay, MutableSharedFlow<BitwardenSnackbarData?>>()
class SnackbarRelayManagerImpl(
dispatcherManager: DispatcherManager,
) : SnackbarRelayManager {
private val unconfinedScope = CoroutineScope(context = dispatcherManager.unconfined)
private val snackbarSharedFlow = SnackbarLastSubscriberMutableSharedFlow()
override fun sendSnackbarData(data: BitwardenSnackbarData, relay: SnackbarRelay) {
getSnackbarDataFlowInternal(relay).tryEmit(data)
unconfinedScope.launch {
snackbarSharedFlow.emitWhenSubscribedTo(
value = SnackbarDataAndRelay(
relay = relay,
data = data,
),
)
}
}
override fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData> =
getSnackbarDataFlowInternal(relay)
.onCompletion {
// when the subscription is ended, remove the relay from the map.
mutableSnackbarRelayMap.remove(relay)
}
.filterNotNull()
snackbarSharedFlow
.generateFlowFor(relay = relay)
.map { it.data }
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun clearRelayBuffer(relay: SnackbarRelay) {
getSnackbarDataFlowInternal(relay).resetReplayCache()
/**
* A wrapper for the [BitwardenSnackbarData] payload and [SnackbarRelay] associated with it.
*/
private data class SnackbarDataAndRelay(
val relay: SnackbarRelay,
val data: BitwardenSnackbarData,
)
/**
* Helper class that ensures that only the last subscriber to a specific relay gets the Snackbar
* data.
*/
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
private class SnackbarLastSubscriberMutableSharedFlow(
private val source: MutableSharedFlow<SnackbarDataAndRelay> = MutableSharedFlow(),
) : MutableSharedFlow<SnackbarDataAndRelay> by source {
private val mutableRelayUuidMap: MutableMap<SnackbarRelay, MutableList<UUID>> = mutableMapOf()
fun generateFlowFor(
relay: SnackbarRelay,
): Flow<SnackbarDataAndRelay> {
lateinit var uuid: UUID
return source
.onSubscription {
uuid = UUID.randomUUID().also { getUuidStack(relay = relay).add(element = it) }
}
.onCompletion { getUuidStack(relay = relay).remove(element = uuid) }
.filter { it.relay == relay }
.filter { getUuidStack(relay = relay).last() == uuid }
}
private fun getSnackbarDataFlowInternal(
private fun getUuidStack(
relay: SnackbarRelay,
): MutableSharedFlow<BitwardenSnackbarData?> =
mutableSnackbarRelayMap.getOrPut(relay) {
bufferedMutableSharedFlow(replay = 1)
}
): MutableList<UUID> = mutableRelayUuidMap.getOrPut(key = relay) { mutableListOf() }
}

View File

@ -4,7 +4,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.toRoute
import com.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import kotlinx.serialization.Serializable
@ -13,31 +12,20 @@ import kotlinx.serialization.Serializable
* The type-safe route for the import logins screen.
*/
@Serializable
data class ImportLoginsRoute(
val snackbarRelay: SnackbarRelay,
)
data object ImportLoginsRoute
/**
* Arguments for the [ImportLoginsScreen] using [SavedStateHandle].
*/
data class ImportLoginsArgs(val snackBarRelay: SnackbarRelay)
/**
* Constructs a [ImportLoginsArgs] from the [SavedStateHandle] and internal route data.
*/
fun SavedStateHandle.toImportLoginsArgs(): ImportLoginsArgs {
val route = this.toRoute<ImportLoginsRoute>()
return ImportLoginsArgs(snackBarRelay = route.snackbarRelay)
}
/**
* Helper function to navigate to the import logins screen.
*/
fun NavController.navigateToImportLoginsScreen(
snackbarRelay: SnackbarRelay,
navOptions: NavOptions? = null,
) {
navigate(route = ImportLoginsRoute(snackbarRelay = snackbarRelay), navOptions = navOptions)
navigate(route = ImportLoginsRoute, navOptions = navOptions)
}
/**

View File

@ -59,7 +59,6 @@ import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.importlogins.components.ImportLoginsInstructionStep
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
@ -561,14 +560,12 @@ private class ImportLoginsDialogContentPreviewProvider :
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = "vault.bitwarden.com",
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
ImportLoginsState(
dialogState = ImportLoginsState.DialogState.ImportLater,
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = "vault.bitwarden.com",
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
)
}

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.util.Text
@ -25,26 +24,23 @@ import javax.inject.Inject
@Suppress("TooManyFunctions")
@HiltViewModel
class ImportLoginsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository,
private val firstTimeActionManager: FirstTimeActionManager,
private val environmentRepository: EnvironmentRepository,
private val snackbarRelayManager: SnackbarRelayManager,
) :
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
initialState = run {
val vaultUrl = environmentRepository.environment.environmentUrlData.webVault
?: environmentRepository.environment.environmentUrlData.base
ImportLoginsState(
dialogState = null,
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
// attempt to trim the scheme of the vault url
currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl,
snackbarRelay = savedStateHandle.toImportLoginsArgs().snackBarRelay,
)
},
) {
) : BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
initialState = run {
val vaultUrl = environmentRepository.environment.environmentUrlData.webVault
?: environmentRepository.environment.environmentUrlData.base
ImportLoginsState(
dialogState = null,
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
// attempt to trim the scheme of the vault url
currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl,
)
},
) {
override fun handleAction(action: ImportLoginsAction) {
when (action) {
ImportLoginsAction.ConfirmGetStarted -> handleConfirmGetStarted()
@ -76,13 +72,15 @@ class ImportLoginsViewModel @Inject constructor(
showBottomSheet = false,
)
}
// instead of doing inline, this approach to avoid "MaxLineLength" suppression.
val snackbarData = BitwardenSnackbarData(
messageHeader = R.string.logins_imported.asText(),
message = R.string.remember_to_delete_your_imported_password_file_from_your_computer
.asText(),
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(
messageHeader = R.string.logins_imported.asText(),
message = R.string
.remember_to_delete_your_imported_password_file_from_your_computer
.asText(),
),
relay = SnackbarRelay.LOGINS_IMPORTED,
)
snackbarRelayManager.sendSnackbarData(data = snackbarData, relay = state.snackbarRelay)
sendEvent(ImportLoginsEvent.NavigateBack)
}
@ -221,7 +219,6 @@ data class ImportLoginsState(
val viewState: ViewState,
val showBottomSheet: Boolean,
val currentWebVaultUrl: String,
val snackbarRelay: SnackbarRelay,
) {
/**
* Dialog states for the [ImportLoginsViewModel].

View File

@ -5,7 +5,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListing
@ -31,7 +30,7 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultEditItemScreen: (args: VaultAddEditArgs) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToAboutScreen: () -> Unit,
) {

View File

@ -5,7 +5,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@ -29,7 +28,7 @@ fun NavGraphBuilder.vaultDestination(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToAboutScreen: () -> Unit,
) {

View File

@ -63,7 +63,6 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
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.review.AppReviewManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.components.VaultItemSelectionDialog
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
@ -92,7 +91,7 @@ fun VaultScreen(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
onNavigateToImportLogins: () -> Unit,
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
onNavigateToAboutScreen: () -> Unit,
exitManager: ExitManager = LocalExitManager.current,
@ -170,10 +169,7 @@ fun VaultScreen(
.show()
}
VaultEvent.NavigateToImportLogins -> {
onNavigateToImportLogins(SnackbarRelay.MY_VAULT_RELAY)
}
VaultEvent.NavigateToImportLogins -> onNavigateToImportLogins()
is VaultEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
VaultEvent.PromptForAppReview -> {
launchPrompt.invoke()

View File

@ -86,11 +86,11 @@ class VaultViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
private val firstTimeActionManager: FirstTimeActionManager,
private val snackbarRelayManager: SnackbarRelayManager,
private val reviewPromptManager: ReviewPromptManager,
private val featureFlagManager: FeatureFlagManager,
private val specialCircumstanceManager: SpecialCircumstanceManager,
private val networkConnectionManager: NetworkConnectionManager,
snackbarRelayManager: SnackbarRelayManager,
featureFlagManager: FeatureFlagManager,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@ -166,7 +166,7 @@ class VaultViewModel @Inject constructor(
.launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.MY_VAULT_RELAY)
.getSnackbarDataFlow(SnackbarRelay.LOGINS_IMPORTED)
.map { VaultAction.Internal.SnackbarDataReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
@ -368,9 +368,6 @@ class VaultViewModel @Inject constructor(
SwitchAccountResult.AccountSwitched -> true
SwitchAccountResult.NoChange -> false
}
if (isSwitchingAccounts) {
snackbarRelayManager.clearRelayBuffer(SnackbarRelay.MY_VAULT_RELAY)
}
mutableStateFlow.update {
it.copy(isSwitchingAccounts = isSwitchingAccounts)
}

View File

@ -15,7 +15,6 @@ import com.bitwarden.ui.util.asText
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.platform.manager.snackbar.SnackbarRelay
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -23,7 +22,6 @@ import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@ -62,10 +60,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
onNavigateToFolders = { onNavigateToFoldersCalled = true },
onNavigateToImportLogins = {
onNavigateToImportLoginsCalled = true
assertEquals(SnackbarRelay.VAULT_SETTINGS_RELAY, it)
},
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
)
}
}

View File

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
import app.cash.turbine.test
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
@ -10,7 +11,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
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.SnackbarRelayManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -38,7 +39,12 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
every { storeShowImportLoginsSettingsBadge(any()) } just runs
}
private val snackbarRelayManager = SnackbarRelayManagerImpl()
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
every {
getSnackbarDataFlow(SnackbarRelay.LOGINS_IMPORTED)
} returns mutableSnackbarSharedFlow
}
@Test
fun `BackClick should emit NavigateBack`() = runTest {
@ -147,10 +153,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val expectedSnackbarData = BitwardenSnackbarData(message = "test message".asText())
viewModel.eventFlow.test {
snackbarRelayManager.sendSnackbarData(
data = expectedSnackbarData,
relay = SnackbarRelay.VAULT_SETTINGS_RELAY,
)
mutableSnackbarSharedFlow.tryEmit(expectedSnackbarData)
assertEquals(VaultSettingsEvent.ShowSnackbar(expectedSnackbarData), awaitItem())
}
}

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.manager.snackbar
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.test.runTest
@ -10,93 +11,72 @@ import org.junit.jupiter.api.Assertions.assertEquals
class SnackbarRelayManagerTest {
@Test
fun `Relay is completed successfully when consumer registers first and event is sent`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
val consumer = relayManager.getSnackbarDataFlow(relay)
private val relayManager: SnackbarRelayManager = SnackbarRelayManagerImpl(
dispatcherManager = FakeDispatcherManager(),
)
consumer.test {
@Test
fun `when relay is completed successfully when consumer registers first and event is sent`() =
runTest {
val relay = SnackbarRelay.LOGINS_IMPORTED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
relayManager.getSnackbarDataFlow(relay).test {
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(
expectedData,
awaitItem(),
)
assertEquals(expectedData, awaitItem())
}
}
@Test
fun `Relay is completed successfully when consumer registers second and event is sent`() =
fun `when relay is completed successfully when consumer registers second and event is sent`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val relay = SnackbarRelay.LOGINS_IMPORTED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
// producer code
relayManager.sendSnackbarData(data = expectedData, relay = relay)
relayManager.getSnackbarDataFlow(relay).test {
assertEquals(
expectedData,
awaitItem(),
)
assertEquals(expectedData, awaitItem())
}
}
@Test
fun `When relay is specified by producer only send data to that relay`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay1 = SnackbarRelay.MY_VAULT_RELAY
val relay2 = SnackbarRelay.VAULT_SETTINGS_RELAY
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
val consumer2 = relayManager.getSnackbarDataFlow(relay2).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay1)
consumer2.expectNoEvents()
assertEquals(
expectedData,
consumer1.awaitItem(),
)
}
fun `when relay is specified by producer only send data to that relay`() = runTest {
val relay1 = SnackbarRelay.LOGINS_IMPORTED
val relay2 = SnackbarRelay.SEND_DELETED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
val consumer2 = relayManager.getSnackbarDataFlow(relay2).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay1)
consumer2.expectNoEvents()
assertEquals(expectedData, consumer1.awaitItem())
}
}
@Test
fun `When multiple consumers are registered to the same relay, send data to all consumers`() =
fun `when multiple consumers are registered to the same relay, send data to last consumers`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val relay = SnackbarRelay.LOGINS_IMPORTED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(
expectedData,
consumer1.awaitItem(),
)
val consumer2 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
assertEquals(
expectedData,
consumer2.awaitItem(),
)
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(expectedData, consumer2.awaitItem())
consumer1.expectNoEvents()
}
}
@Suppress("MaxLineLength")
@Test
fun `When multiple consumers are registered to the same relay, and one is completed before the other the second consumer registers should not receive any emissions`() =
fun `when multiple consumers are registered to the same relay, and one is completed before the other the second consumer registers should not receive any emissions`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val relay = SnackbarRelay.LOGINS_IMPORTED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(
expectedData,
consumer1.awaitItem(),
)
assertEquals(expectedData, consumer1.awaitItem())
consumer1.cancel()
val consumer2 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
consumer2.expectNoEvents()
@ -105,21 +85,16 @@ class SnackbarRelayManagerTest {
@Suppress("MaxLineLength")
@Test
fun `When multiple consumers register to the same relay, and clearRelayBuffer is called, the second consumer should not receive any emissions`() =
fun `when multiple consumers are registered to the same relay, and the last one is cancelled, the other most recent consumer should receive the emissions`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val relay = SnackbarRelay.LOGINS_IMPORTED
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(
expectedData,
consumer1.awaitItem(),
)
relayManager.clearRelayBuffer(relay)
val consumer2 = relayManager.getSnackbarDataFlow(relay).testIn(backgroundScope)
consumer2.expectNoEvents()
consumer2.cancel()
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(expectedData, consumer1.awaitItem())
}
}
}

View File

@ -23,7 +23,6 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
import com.x8bit.bitwarden.ui.platform.base.BitwardenComposeTest
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
@ -492,5 +491,4 @@ private val DEFAULT_STATE = ImportLoginsState(
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = "vault.bitwarden.com",
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
)

View File

@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.ui.platform.base.BaseViewModelTest
@ -48,10 +47,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setUp() {
mockkStatic(
SavedStateHandle::toImportLoginsArgs,
Uri::parse,
)
mockkStatic(Uri::parse)
every { Uri.parse(Environment.Us.environmentUrlData.base) } returns mockk {
every { host } returns DEFAULT_VAULT_URL
}
@ -59,10 +55,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
@AfterEach
fun tearDown() {
unmockkStatic(
SavedStateHandle::toImportLoginsArgs,
Uri::parse,
)
unmockkStatic(Uri::parse)
}
private val snackbarRelayManager: SnackbarRelayManagerImpl = mockk {
@ -88,7 +81,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -104,7 +96,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -125,7 +116,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -136,7 +126,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -160,7 +149,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
stateFlow.awaitItem(),
)
@ -171,7 +159,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
stateFlow.awaitItem(),
)
@ -201,7 +188,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -212,7 +198,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.ImportStepOne,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -253,7 +238,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.ImportStepOne,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -269,7 +253,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.ImportStepTwo,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -285,7 +268,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.ImportStepThree,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -305,7 +287,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -326,7 +307,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -354,7 +334,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -374,7 +353,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = true,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -404,7 +382,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -417,7 +394,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
awaitItem(),
)
@ -435,7 +411,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -458,7 +433,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
viewModel.stateFlow.value,
)
@ -484,7 +458,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
stateFlow.awaitItem(),
)
@ -494,7 +467,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = true,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
stateFlow.awaitItem(),
)
@ -505,7 +477,6 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
),
stateFlow.awaitItem(),
)
@ -519,17 +490,12 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
verify {
snackbarRelayManager.sendSnackbarData(
data = expectedSnackbarData,
relay = SnackbarRelay.MY_VAULT_RELAY,
relay = SnackbarRelay.LOGINS_IMPORTED,
)
}
}
private fun createViewModel(
snackbarRelay: SnackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
): ImportLoginsViewModel = ImportLoginsViewModel(
savedStateHandle = SavedStateHandle().apply {
every { toImportLoginsArgs() } returns ImportLoginsArgs(snackBarRelay = snackbarRelay)
},
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel(
vaultRepository = vaultRepository,
firstTimeActionManager = firstTimeActionManager,
environmentRepository = environmentRepository,
@ -544,5 +510,4 @@ private val DEFAULT_STATE = ImportLoginsState(
viewState = ImportLoginsState.ViewState.InitialContent,
showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
)

View File

@ -38,7 +38,6 @@ 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.review.AppReviewManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertRemovalConfirmationDialogIsDisplayed
@ -118,10 +117,7 @@ class VaultScreenTest : BitwardenComposeTest() {
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
onNavigateToSearchVault = { onNavigateToSearchScreen = true },
onNavigateToImportLogins = {
onNavigateToImportLoginsCalled = true
assertEquals(SnackbarRelay.MY_VAULT_RELAY, it)
},
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
onNavigateToAddFolderScreen = { folderName ->
onNavigateToAddFolderCalled = true
onNavigateToAddFolderParentFolderName = folderName

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
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.baseIconUrl
import com.bitwarden.network.model.OrganizationType
@ -63,7 +64,6 @@ import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@ -83,11 +83,11 @@ class VaultViewModelTest : BaseViewModelTest() {
ZoneOffset.UTC,
)
private val mutableSnackbarDataFlow = MutableStateFlow<BitwardenSnackbarData?>(null)
private val mutableSnackbarDataFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every { getSnackbarDataFlow(SnackbarRelay.MY_VAULT_RELAY) } returns mutableSnackbarDataFlow
.filterNotNull()
every { clearRelayBuffer(SnackbarRelay.MY_VAULT_RELAY) } just runs
every {
getSnackbarDataFlow(SnackbarRelay.LOGINS_IMPORTED)
} returns mutableSnackbarDataFlow
}
private val clipboardManager: BitwardenClipboardManager = mockk {
@ -2050,8 +2050,8 @@ class VaultViewModelTest : BaseViewModelTest() {
fun `when SnackbarRelay flow updates, snackbar is shown`() = runTest {
val viewModel = createViewModel()
val expectedSnackbarData = BitwardenSnackbarData(message = "test message".asText())
mutableSnackbarDataFlow.update { expectedSnackbarData }
viewModel.eventFlow.test {
mutableSnackbarDataFlow.tryEmit(expectedSnackbarData)
assertEquals(VaultEvent.ShowSnackbar(expectedSnackbarData), awaitItem())
}
}
@ -2068,9 +2068,6 @@ class VaultViewModelTest : BaseViewModelTest() {
},
),
)
verify(exactly = 1) {
snackbarRelayManager.clearRelayBuffer(SnackbarRelay.MY_VAULT_RELAY)
}
}
@Suppress("MaxLineLength")

View File

@ -1,6 +1,7 @@
package com.bitwarden.core.data.repository.util
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
/**
* Creates a [MutableSharedFlow] with a buffer of [Int.MAX_VALUE] and the given [replay] count.
@ -12,3 +13,17 @@ fun <T> bufferedMutableSharedFlow(
replay = replay,
extraBufferCapacity = Int.MAX_VALUE,
)
/**
* Emits a [value] to this shared flow, suspending until there is at least one subscriber.
*/
suspend fun <T> MutableSharedFlow<T>.emitWhenSubscribedTo(value: T) {
// We have subscribers, so emit now.
if (subscriptionCount.value > 0) {
emit(value = value)
return
}
// We are going to wait until there is at least one subscriber, then emit.
subscriptionCount.first { it > 0 }
emit(value = value)
}