From beb4c533c847c266676aa376ea28499a832e198b Mon Sep 17 00:00:00 2001 From: David Perez Date: Fri, 6 Jun 2025 13:28:08 -0500 Subject: [PATCH] Update the SnackbarRelayManager (#5317) --- .../feature/settings/SettingsNavigation.kt | 3 +- .../settings/vault/VaultSettingsNavigation.kt | 3 +- .../settings/vault/VaultSettingsScreen.kt | 5 +- .../settings/vault/VaultSettingsViewModel.kt | 6 +- .../vaultunlocked/VaultUnlockedNavigation.kt | 4 +- .../VaultUnlockedNavBarNavigation.kt | 3 +- .../VaultUnlockedNavBarScreen.kt | 5 +- .../manager/di/PlatformUiManagerModule.kt | 7 +- .../manager/snackbar/SnackbarRelay.kt | 4 +- .../manager/snackbar/SnackbarRelayManager.kt | 5 - .../snackbar/SnackbarRelayManagerImpl.kt | 78 +++++++++---- .../importlogins/ImportLoginsNavigation.kt | 16 +-- .../importlogins/ImportLoginsScreen.kt | 3 - .../importlogins/ImportLoginsViewModel.kt | 45 ++++---- .../feature/vault/VaultGraphNavigation.kt | 3 +- .../ui/vault/feature/vault/VaultNavigation.kt | 3 +- .../ui/vault/feature/vault/VaultScreen.kt | 8 +- .../ui/vault/feature/vault/VaultViewModel.kt | 9 +- .../settings/vault/VaultSettingsScreenTest.kt | 7 +- .../vault/VaultSettingsViewModelTest.kt | 15 ++- .../snackbar/SnackbarRelayManagerTest.kt | 103 +++++++----------- .../importlogins/ImportLoginsScreenTest.kt | 2 - .../importlogins/ImportLoginsViewModelTest.kt | 43 +------- .../ui/vault/feature/vault/VaultScreenTest.kt | 6 +- .../vault/feature/vault/VaultViewModelTest.kt | 15 +-- .../repository/util/SharedFlowExtensions.kt | 15 +++ 26 files changed, 180 insertions(+), 236 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index 94fad2ee11..52a050a6ac 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -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( startDestination = SettingsRoute.Standard, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt index 2078298d25..bda60be1d3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt @@ -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 { VaultSettingsScreen( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index c1505cb552..86d4ddf59a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -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()) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt index 978a211ede..3465ef9dcc 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt @@ -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) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 9d4ecfe5ce..3cca709671 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -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, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 1fef8a5635..3ec1b16401 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -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 { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 4debcfcbd9..919c4c65cd 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -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) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt index daf276e032..9c3f6ae2ee 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/di/PlatformUiManagerModule.kt @@ -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, + ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt index 0b9d5862eb..cdda9094c9 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt @@ -9,6 +9,6 @@ import kotlinx.serialization.Serializable */ @Serializable enum class SnackbarRelay { - VAULT_SETTINGS_RELAY, - MY_VAULT_RELAY, + LOGINS_IMPORTED, + SEND_DELETED, } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManager.kt index 1b7e809b9e..cf341c3c42 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManager.kt @@ -19,9 +19,4 @@ interface SnackbarRelayManager { * the [relay] to receive the data from. */ fun getSnackbarDataFlow(relay: SnackbarRelay): Flow - - /** - * Clears the buffer for the given [relay]. - */ - fun clearRelayBuffer(relay: SnackbarRelay) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerImpl.kt index d4a8b163cb..3ee0fff1ff 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerImpl.kt @@ -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>() +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 = - 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 = MutableSharedFlow(), +) : MutableSharedFlow by source { + private val mutableRelayUuidMap: MutableMap> = mutableMapOf() + + fun generateFlowFor( + relay: SnackbarRelay, + ): Flow { + 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 = - mutableSnackbarRelayMap.getOrPut(relay) { - bufferedMutableSharedFlow(replay = 1) - } + ): MutableList = mutableRelayUuidMap.getOrPut(key = relay) { mutableListOf() } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt index 236f45b8cb..f870b75148 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsNavigation.kt @@ -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() - 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) } /** diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt index e5a8dc80b2..cee08fa152 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreen.kt @@ -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, ), ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt index 51776092bb..9d29878078 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModel.kt @@ -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( - 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( + 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]. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 7bfbdec311..081378fc3d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -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, ) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt index aacf19b650..b4c8e96351 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -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, ) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 84da90926f..a3004d6da3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -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() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 806a4e7b45..548a5438b1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -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( 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) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt index c59f7d374e..7b4fbdbf17 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt @@ -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 }, ) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt index dbbbbbf2fe..6b44bd631a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt @@ -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() + private val snackbarRelayManager = mockk { + 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()) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerTest.kt index 270b1f56f8..af359755c3 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelayManagerTest.kt @@ -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()) } } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreenTest.kt index 902343e01e..14ceda671f 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsScreenTest.kt @@ -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, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt index f9ccc897ae..3bd636c39a 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/ImportLoginsViewModelTest.kt @@ -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, ) diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 83ead00c0b..9370879136 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -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 diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index a21a0e065a..790b1916b3 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -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(null) + private val mutableSnackbarDataFlow = bufferedMutableSharedFlow() 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") diff --git a/core/src/main/kotlin/com/bitwarden/core/data/repository/util/SharedFlowExtensions.kt b/core/src/main/kotlin/com/bitwarden/core/data/repository/util/SharedFlowExtensions.kt index 3b809fc0b6..8fa5bc6172 100644 --- a/core/src/main/kotlin/com/bitwarden/core/data/repository/util/SharedFlowExtensions.kt +++ b/core/src/main/kotlin/com/bitwarden/core/data/repository/util/SharedFlowExtensions.kt @@ -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 bufferedMutableSharedFlow( replay = replay, extraBufferCapacity = Int.MAX_VALUE, ) + +/** + * Emits a [value] to this shared flow, suspending until there is at least one subscriber. + */ +suspend fun MutableSharedFlow.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) +}