PM-11188 show snackbar after import success. PM-13943 add relay for snackbar events across screen contexts. (#4152)

This commit is contained in:
Dave Severns 2024-10-29 14:23:00 -04:00 committed by GitHub
parent a1108889cb
commit 8b16135955
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 505 additions and 59 deletions

View file

@ -19,6 +19,7 @@ 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.other.otherDestination
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings 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.feature.settings.vault.vaultSettingsDestination
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
const val SETTINGS_GRAPH_ROUTE: String = "settings_graph" const val SETTINGS_GRAPH_ROUTE: String = "settings_graph"
private const val SETTINGS_ROUTE: String = "settings" private const val SETTINGS_ROUTE: String = "settings"
@ -35,7 +36,7 @@ fun NavGraphBuilder.settingsGraph(
onNavigateToPendingRequests: () -> Unit, onNavigateToPendingRequests: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit, onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit, onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
navigation( navigation(
startDestination = SETTINGS_ROUTE, startDestination = SETTINGS_ROUTE,

View file

@ -4,6 +4,7 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
private const val VAULT_SETTINGS_ROUTE = "vault_settings" private const val VAULT_SETTINGS_ROUTE = "vault_settings"
@ -14,7 +15,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit, onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit, onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
composableWithPushTransitions( composableWithPushTransitions(
route = VAULT_SETTINGS_ROUTE, route = VAULT_SETTINGS_ROUTE,

View file

@ -33,9 +33,12 @@ import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
/** /**
* Displays the vault settings screen. * Displays the vault settings screen.
@ -47,12 +50,13 @@ fun VaultSettingsScreen(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToExportVault: () -> Unit, onNavigateToExportVault: () -> Unit,
onNavigateToFolders: () -> Unit, onNavigateToFolders: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
viewModel: VaultSettingsViewModel = hiltViewModel(), viewModel: VaultSettingsViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val snackbarHostState = rememberBitwardenSnackbarHostState()
val context = LocalContext.current val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
@ -65,11 +69,13 @@ fun VaultSettingsScreen(
is VaultSettingsEvent.NavigateToImportVault -> { is VaultSettingsEvent.NavigateToImportVault -> {
if (state.isNewImportLoginsFlowEnabled) { if (state.isNewImportLoginsFlowEnabled) {
onNavigateToImportLogins() onNavigateToImportLogins(SnackbarRelay.VAULT_SETTINGS_RELAY)
} else { } else {
intentManager.launchUri(event.url.toUri()) intentManager.launchUri(event.url.toUri())
} }
} }
is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
} }
} }
@ -89,6 +95,11 @@ fun VaultSettingsScreen(
}, },
) )
}, },
snackbarHost = {
BitwardenSnackbarHost(
bitwardenHostState = snackbarHostState,
)
},
) { innerPadding -> ) { innerPadding ->
Column( Column(
Modifier Modifier

View file

@ -7,6 +7,10 @@ import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.util.toBaseWebVaultImportUrl import com.x8bit.bitwarden.data.platform.repository.util.toBaseWebVaultImportUrl
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -22,6 +26,7 @@ import javax.inject.Inject
class VaultSettingsViewModel @Inject constructor( class VaultSettingsViewModel @Inject constructor(
environmentRepository: EnvironmentRepository, environmentRepository: EnvironmentRepository,
featureFlagManager: FeatureFlagManager, featureFlagManager: FeatureFlagManager,
snackbarRelayManager: SnackbarRelayManager,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>( ) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
initialState = run { initialState = run {
@ -53,6 +58,14 @@ class VaultSettingsViewModel @Inject constructor(
} }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.VAULT_SETTINGS_RELAY)
.map {
VaultSettingsAction.Internal.SnackbarDataReceived(it)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
override fun handleAction(action: VaultSettingsAction): Unit = when (action) { override fun handleAction(action: VaultSettingsAction): Unit = when (action) {
@ -74,8 +87,18 @@ class VaultSettingsViewModel @Inject constructor(
is VaultSettingsAction.Internal.UserFirstTimeStateChanged -> { is VaultSettingsAction.Internal.UserFirstTimeStateChanged -> {
handleUserFirstTimeStateChanged(action) handleUserFirstTimeStateChanged(action)
} }
is VaultSettingsAction.Internal.SnackbarDataReceived -> {
handleSnackbarDataReceived(action)
} }
} }
}
private fun handleSnackbarDataReceived(
action: VaultSettingsAction.Internal.SnackbarDataReceived,
) {
sendEvent(VaultSettingsEvent.ShowSnackbar(action.data))
}
private fun handleImportLoginsCardDismissClicked() { private fun handleImportLoginsCardDismissClicked() {
if (!state.shouldShowImportCard) return if (!state.shouldShowImportCard) return
@ -166,6 +189,11 @@ sealed class VaultSettingsEvent {
data class ShowToast( data class ShowToast(
val message: String, val message: String,
) : VaultSettingsEvent() ) : VaultSettingsEvent()
/**
* Shows a snackbar with the given [data].
*/
data class ShowSnackbar(val data: BitwardenSnackbarData) : VaultSettingsEvent(), BackgroundEvent
} }
/** /**
@ -220,5 +248,10 @@ sealed class VaultSettingsAction {
data class UserFirstTimeStateChanged( data class UserFirstTimeStateChanged(
val showImportLoginsCard: Boolean, val showImportLoginsCard: Boolean,
) : Internal() ) : Internal()
/**
* Indicates that the snackbar data has been received.
*/
data class SnackbarDataReceived(val data: BitwardenSnackbarData) : Internal()
} }
} }

View file

@ -106,7 +106,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
}, },
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() }, onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() }, onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
onNavigateToImportLogins = { navController.navigateToImportLoginsScreen() }, onNavigateToImportLogins = {
navController.navigateToImportLoginsScreen(snackbarRelay = it)
},
) )
deleteAccountDestination( deleteAccountDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },

View file

@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithStayTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType 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.model.VaultItemCipherType import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
/** /**
@ -38,7 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
onNavigateToPasswordHistory: () -> Unit, onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit, onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit, onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
composableWithStayTransitions( composableWithStayTransitions(
route = VAULT_UNLOCKED_NAV_BAR_ROUTE, route = VAULT_UNLOCKED_NAV_BAR_ROUTE,

View file

@ -44,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph import com.x8bit.bitwarden.ui.platform.feature.settings.navigateToSettingsGraph
import com.x8bit.bitwarden.ui.platform.feature.settings.settingsGraph 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.feature.vaultunlockednavbar.model.VaultUnlockedNavBarTab
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph
@ -77,7 +78,7 @@ fun VaultUnlockedNavBarScreen(
onNavigateToPasswordHistory: () -> Unit, onNavigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit, onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit, onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -171,7 +172,7 @@ private fun VaultUnlockedNavBarScaffold(
navigateToPasswordHistory: () -> Unit, navigateToPasswordHistory: () -> Unit,
onNavigateToSetupUnlockScreen: () -> Unit, onNavigateToSetupUnlockScreen: () -> Unit,
onNavigateToSetupAutoFillScreen: () -> Unit, onNavigateToSetupAutoFillScreen: () -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
var shouldDimNavBar by remember { mutableStateOf(false) } var shouldDimNavBar by remember { mutableStateOf(false) }

View file

@ -5,6 +5,8 @@ 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.intent.IntentManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManager
import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManagerImpl import com.x8bit.bitwarden.ui.platform.manager.resource.ResourceManagerImpl
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManagerImpl
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -32,4 +34,8 @@ class PlatformUiManagerModule {
@Singleton @Singleton
fun provideResourceManager(@ApplicationContext context: Context): ResourceManager = fun provideResourceManager(@ApplicationContext context: Context): ResourceManager =
ResourceManagerImpl(context = context) ResourceManagerImpl(context = context)
@Provides
@Singleton
fun provideSnackbarRelayManager(): SnackbarRelayManager = SnackbarRelayManagerImpl()
} }

View file

@ -0,0 +1,12 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
/**
* Models a relay key to be mapped to an instance of [BitwardenSnackbarData] being sent
* between producers and consumers of the data.
*/
enum class SnackbarRelay {
VAULT_SETTINGS_RELAY,
MY_VAULT_RELAY,
}

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.flow.Flow
/**
* Manager responsible for relaying snackbar data between a producer and consumer who may
* communicate with reference to a specific [SnackbarRelay].
*/
interface SnackbarRelayManager {
/**
* Called from a producer to send snackbar data to a consumer, the producer must
* specify the [relay] to send the data to.
*/
fun sendSnackbarData(data: BitwardenSnackbarData, relay: SnackbarRelay)
/**
* Called from a consumer to receive snackbar data from a producer, the consumer must specify
* the [relay] to receive the data from.
*/
fun getSnackbarDataFlow(relay: SnackbarRelay): Flow<BitwardenSnackbarData>
}

View file

@ -0,0 +1,35 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.onCompletion
/**
* The default implementation of the [SnackbarRelayManager] interface.
*/
class SnackbarRelayManagerImpl : SnackbarRelayManager {
private val mutableSnackbarRelayMap =
mutableMapOf<SnackbarRelay, MutableSharedFlow<BitwardenSnackbarData?>>()
override fun sendSnackbarData(data: BitwardenSnackbarData, relay: SnackbarRelay) {
getSnackbarDataFlowInternal(relay).tryEmit(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()
private fun getSnackbarDataFlowInternal(
relay: SnackbarRelay,
): MutableSharedFlow<BitwardenSnackbarData?> =
mutableSnackbarRelayMap.getOrPut(relay) {
bufferedMutableSharedFlow(replay = 1)
}
}

View file

@ -1,17 +1,39 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
private const val IMPORT_LOGINS_ROUTE = "import-logins" private const val IMPORT_LOGINS_PREFIX = "import-logins"
private const val IMPORT_LOGINS_NAV_ARG = "snackbarRelay"
private const val IMPORT_LOGINS_ROUTE = "$IMPORT_LOGINS_PREFIX/{$IMPORT_LOGINS_NAV_ARG}"
/**
* Arguments for the [ImportLoginsScreen] using [SavedStateHandle].
*/
@OmitFromCoverage
data class ImportLoginsArgs(val snackBarRelay: SnackbarRelay) {
constructor(savedStateHandle: SavedStateHandle) : this(
snackBarRelay = SnackbarRelay.valueOf(
requireNotNull(savedStateHandle[IMPORT_LOGINS_NAV_ARG]),
),
)
}
/** /**
* Helper function to navigate to the import logins screen. * Helper function to navigate to the import logins screen.
*/ */
fun NavController.navigateToImportLoginsScreen(navOptions: NavOptions? = null) { fun NavController.navigateToImportLoginsScreen(
navigate(route = IMPORT_LOGINS_ROUTE, navOptions = navOptions) snackbarRelay: SnackbarRelay,
navOptions: NavOptions? = null,
) {
navigate(route = "$IMPORT_LOGINS_PREFIX/$snackbarRelay", navOptions = navOptions)
} }
/** /**
@ -22,6 +44,12 @@ fun NavGraphBuilder.importLoginsScreenDestination(
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(
route = IMPORT_LOGINS_ROUTE, route = IMPORT_LOGINS_ROUTE,
arguments = listOf(
navArgument(IMPORT_LOGINS_NAV_ARG) {
type = NavType.StringType
nullable = false
},
),
) { ) {
ImportLoginsScreen( ImportLoginsScreen(
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,

View file

@ -58,6 +58,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.importlogins.components.ImportLoginsInstructionStep 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.ImportLoginHandler
@ -627,6 +628,7 @@ private class ImportLoginsDialogContentPreviewProvider :
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = "vault.bitwarden.com", currentWebVaultUrl = "vault.bitwarden.com",
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
ImportLoginsState( ImportLoginsState(
dialogState = ImportLoginsState.DialogState.ImportLater, dialogState = ImportLoginsState.DialogState.ImportLater,
@ -634,6 +636,7 @@ private class ImportLoginsDialogContentPreviewProvider :
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = "vault.bitwarden.com", currentWebVaultUrl = "vault.bitwarden.com",
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
) )
} }

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins package com.x8bit.bitwarden.ui.vault.feature.importlogins
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
@ -10,6 +11,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -21,9 +25,11 @@ import javax.inject.Inject
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
@HiltViewModel @HiltViewModel
class ImportLoginsViewModel @Inject constructor( class ImportLoginsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
private val environmentRepository: EnvironmentRepository, private val environmentRepository: EnvironmentRepository,
private val snackbarRelayManager: SnackbarRelayManager,
) : ) :
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>( BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
initialState = run { initialState = run {
@ -36,6 +42,7 @@ class ImportLoginsViewModel @Inject constructor(
showBottomSheet = false, showBottomSheet = false,
// attempt to trim the scheme of the vault url // attempt to trim the scheme of the vault url
currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl, currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl,
snackbarRelay = ImportLoginsArgs(savedStateHandle).snackBarRelay,
) )
}, },
) { ) {
@ -70,6 +77,13 @@ class ImportLoginsViewModel @Inject constructor(
showBottomSheet = false, 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 = snackbarData, relay = state.snackbarRelay)
sendEvent(ImportLoginsEvent.NavigateBack) sendEvent(ImportLoginsEvent.NavigateBack)
} }
@ -213,6 +227,7 @@ data class ImportLoginsState(
val isVaultSyncing: Boolean, val isVaultSyncing: Boolean,
val showBottomSheet: Boolean, val showBottomSheet: Boolean,
val currentWebVaultUrl: String, val currentWebVaultUrl: String,
val snackbarRelay: SnackbarRelay,
) { ) {
/** /**
* Dialog states for the [ImportLoginsViewModel]. * Dialog states for the [ImportLoginsViewModel].

View file

@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.navigation import androidx.navigation.navigation
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType 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.itemlisting.navigateToVaultItemListing import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListing
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen
@ -24,7 +25,7 @@ fun NavGraphBuilder.vaultGraph(
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit, onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
navigation( navigation(
route = VAULT_GRAPH_ROUTE, route = VAULT_GRAPH_ROUTE,

View file

@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType 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.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
const val VAULT_ROUTE: String = "vault" const val VAULT_ROUTE: String = "vault"
@ -21,7 +22,7 @@ fun NavGraphBuilder.vaultDestination(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
) { ) {
composableWithRootPushTransitions( composableWithRootPushTransitions(
route = VAULT_ROUTE, route = VAULT_ROUTE,

View file

@ -55,6 +55,9 @@ import com.x8bit.bitwarden.ui.platform.components.model.TopAppBarDividerStyle
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenPullToRefreshState import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState import com.x8bit.bitwarden.ui.platform.components.scaffold.rememberBitwardenPullToRefreshState
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
@ -63,6 +66,7 @@ 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.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@ -83,7 +87,7 @@ fun VaultScreen(
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit, onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit, onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
onNavigateToImportLogins: () -> Unit, onNavigateToImportLogins: (SnackbarRelay) -> Unit,
exitManager: ExitManager = LocalExitManager.current, exitManager: ExitManager = LocalExitManager.current,
intentManager: IntentManager = LocalIntentManager.current, intentManager: IntentManager = LocalIntentManager.current,
permissionsManager: PermissionsManager = LocalPermissionsManager.current, permissionsManager: PermissionsManager = LocalPermissionsManager.current,
@ -97,6 +101,7 @@ fun VaultScreen(
{ viewModel.trySendAction(VaultAction.RefreshPull) } { viewModel.trySendAction(VaultAction.RefreshPull) }
}, },
) )
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen() VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen()
@ -124,7 +129,11 @@ fun VaultScreen(
.show() .show()
} }
VaultEvent.NavigateToImportLogins -> onNavigateToImportLogins() VaultEvent.NavigateToImportLogins -> {
onNavigateToImportLogins(SnackbarRelay.MY_VAULT_RELAY)
}
is VaultEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
} }
} }
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) } val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
@ -137,6 +146,7 @@ fun VaultScreen(
pullToRefreshState = pullToRefreshState, pullToRefreshState = pullToRefreshState,
vaultHandlers = vaultHandlers, vaultHandlers = vaultHandlers,
onDimBottomNavBarRequest = onDimBottomNavBarRequest, onDimBottomNavBarRequest = onDimBottomNavBarRequest,
snackbarHostState = snackbarHostState,
) )
} }
@ -173,6 +183,7 @@ private fun VaultScreenScaffold(
pullToRefreshState: BitwardenPullToRefreshState, pullToRefreshState: BitwardenPullToRefreshState,
vaultHandlers: VaultHandlers, vaultHandlers: VaultHandlers,
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit, onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
snackbarHostState: BitwardenSnackbarHostState,
) { ) {
var accountMenuVisible by rememberSaveable { var accountMenuVisible by rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
@ -261,6 +272,11 @@ private fun VaultScreenScaffold(
}, },
) )
}, },
snackbarHost = {
BitwardenSnackbarHost(
bitwardenHostState = snackbarHostState,
)
},
floatingActionButton = { floatingActionButton = {
AnimatedVisibility( AnimatedVisibility(
visible = state.viewState.hasFab && !accountMenuVisible, visible = state.viewState.hasFab && !accountMenuVisible,

View file

@ -26,6 +26,7 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat import com.x8bit.bitwarden.ui.platform.base.util.concat
@ -33,6 +34,9 @@ import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -71,8 +75,9 @@ class VaultViewModel @Inject constructor(
private val policyManager: PolicyManager, private val policyManager: PolicyManager,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val featureFlagManager: FeatureFlagManager,
private val firstTimeActionManager: FirstTimeActionManager, private val firstTimeActionManager: FirstTimeActionManager,
featureFlagManager: FeatureFlagManager,
snackbarRelayManager: SnackbarRelayManager,
) : BaseViewModel<VaultState, VaultEvent, VaultAction>( ) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
initialState = run { initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value) val userState = requireNotNull(authRepository.userStateFlow.value)
@ -143,6 +148,14 @@ class VaultViewModel @Inject constructor(
} }
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.MY_VAULT_RELAY)
.map {
VaultAction.Internal.SnackbarDataReceive(it)
}
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
override fun handleAction(action: VaultAction) { override fun handleAction(action: VaultAction) {
@ -458,9 +471,15 @@ class VaultViewModel @Inject constructor(
is VaultAction.Internal.ValidatePasswordResultReceive -> { is VaultAction.Internal.ValidatePasswordResultReceive -> {
handleValidatePasswordResultReceive(action) handleValidatePasswordResultReceive(action)
} }
is VaultAction.Internal.SnackbarDataReceive -> handleSnackbarDataReceive(action)
} }
} }
private fun handleSnackbarDataReceive(action: VaultAction.Internal.SnackbarDataReceive) {
sendEvent(VaultEvent.ShowSnackbar(action.data))
}
private fun handleGenerateTotpResultReceive( private fun handleGenerateTotpResultReceive(
action: VaultAction.Internal.GenerateTotpResultReceive, action: VaultAction.Internal.GenerateTotpResultReceive,
) { ) {
@ -1011,6 +1030,11 @@ sealed class VaultEvent {
* Show a toast with the given [message]. * Show a toast with the given [message].
*/ */
data class ShowToast(val message: Text) : VaultEvent() data class ShowToast(val message: Text) : VaultEvent()
/**
* Show a snackbar with the given [data].
*/
data class ShowSnackbar(val data: BitwardenSnackbarData) : VaultEvent(), BackgroundEvent
} }
/** /**
@ -1217,6 +1241,13 @@ sealed class VaultAction {
val overflowAction: ListingItemOverflowAction.VaultAction, val overflowAction: ListingItemOverflowAction.VaultAction,
val result: ValidatePasswordResult, val result: ValidatePasswordResult,
) : Internal() ) : Internal()
/**
* Indicates that a snackbar data was received.
*/
data class SnackbarDataReceive(
val data: BitwardenSnackbarData,
) : Internal()
} }
} }

View file

@ -1073,4 +1073,6 @@ Do you want to switch to this account?</string>
<string name="bitwarden_tools">Bitwarden Tools</string> <string name="bitwarden_tools">Bitwarden Tools</string>
<string name="got_it">Got it</string> <string name="got_it">Got it</string>
<string name="no_logins_were_imported">No logins were imported</string> <string name="no_logins_were_imported">No logins were imported</string>
<string name="logins_imported">Logins imported</string>
<string name="remember_to_delete_your_imported_password_file_from_your_computer">Remember to delete your imported password file from your computer</string>
</resources> </resources>

View file

@ -11,7 +11,10 @@ import androidx.compose.ui.test.performScrollTo
import androidx.core.net.toUri import androidx.core.net.toUri
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
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.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -19,6 +22,7 @@ import io.mockk.runs
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
@ -56,7 +60,10 @@ class VaultSettingsScreenTest : BaseComposeTest() {
onNavigateToExportVault = { onNavigateToExportVaultCalled = true }, onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
onNavigateToFolders = { onNavigateToFoldersCalled = true }, onNavigateToFolders = { onNavigateToFoldersCalled = true },
intentManager = intentManager, intentManager = intentManager,
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true }, onNavigateToImportLogins = {
onNavigateToImportLoginsCalled = true
assertEquals(SnackbarRelay.VAULT_SETTINGS_RELAY, it)
},
) )
} }
} }
@ -213,4 +220,11 @@ class VaultSettingsScreenTest : BaseComposeTest() {
viewModel.trySendAction(VaultSettingsAction.ImportLoginsCardCtaClick) viewModel.trySendAction(VaultSettingsAction.ImportLoginsCardCtaClick)
} }
} }
@Test
fun `when ShowSnackbar is sent snackbar should be displayed`() {
val data = BitwardenSnackbarData("message".asText())
mutableEventFlow.tryEmit(VaultSettingsEvent.ShowSnackbar(data))
composeTestRule.onNodeWithText("message").assertIsDisplayed()
}
} }

View file

@ -7,6 +7,10 @@ import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
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 io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -34,6 +38,8 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
every { storeShowImportLoginsSettingsBadge(any()) } just runs every { storeShowImportLoginsSettingsBadge(any()) } just runs
} }
private val snackbarRelayManager = SnackbarRelayManagerImpl()
@Test @Test
fun `BackClick should emit NavigateBack`() = runTest { fun `BackClick should emit NavigateBack`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -136,10 +142,24 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `SnackbarDataReceived action should send snackbar event`() = runTest {
val viewModel = createViewModel()
val expectedSnackbarData = BitwardenSnackbarData(message = "test message".asText())
viewModel.eventFlow.test {
snackbarRelayManager.sendSnackbarData(
data = expectedSnackbarData,
relay = SnackbarRelay.VAULT_SETTINGS_RELAY,
)
assertEquals(VaultSettingsEvent.ShowSnackbar(expectedSnackbarData), awaitItem())
}
}
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel( private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel(
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
snackbarRelayManager = snackbarRelayManager,
) )
} }

View file

@ -0,0 +1,105 @@
package com.x8bit.bitwarden.ui.platform.manager.snackbar
import app.cash.turbine.test
import app.cash.turbine.turbineScope
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
import kotlinx.coroutines.test.runTest
import org.junit.Test
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)
consumer.test {
relayManager.sendSnackbarData(data = expectedData, relay = relay)
assertEquals(
expectedData,
awaitItem(),
)
}
}
@Test
fun `Relay is completed successfully when consumer registers second and event is sent`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay = SnackbarRelay.MY_VAULT_RELAY
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
// producer code
relayManager.sendSnackbarData(data = expectedData, relay = relay)
relayManager.getSnackbarDataFlow(relay).test {
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(),
)
}
}
@Test
fun `When multiple consumers are registered to the same relay, send data to all consumers`() =
runTest {
val relayManager = SnackbarRelayManagerImpl()
val relay1 = SnackbarRelay.MY_VAULT_RELAY
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay1)
assertEquals(
expectedData,
consumer1.awaitItem(),
)
val consumer2 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
assertEquals(
expectedData,
consumer2.awaitItem(),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `When multiple consumers are register 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 relay1 = SnackbarRelay.MY_VAULT_RELAY
val expectedData = BitwardenSnackbarData(message = "Test message".asText())
turbineScope {
val consumer1 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
relayManager.sendSnackbarData(data = expectedData, relay = relay1)
assertEquals(
expectedData,
consumer1.awaitItem(),
)
consumer1.cancel()
val consumer2 = relayManager.getSnackbarDataFlow(relay1).testIn(backgroundScope)
consumer2.expectNoEvents()
}
}
}

View file

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

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.ui.vault.feature.importlogins package com.x8bit.bitwarden.ui.vault.feature.importlogins
import android.net.Uri import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
@ -10,15 +11,18 @@ import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.repository.model.SyncVaultDataResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
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 io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
@ -55,6 +59,10 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
unmockkStatic(Uri::parse) unmockkStatic(Uri::parse)
} }
private val snackbarRelayManager: SnackbarRelayManagerImpl = mockk() {
coEvery { sendSnackbarData(any(), any()) } just runs
}
@Test @Test
fun `initial state is correct`() { fun `initial state is correct`() {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -75,6 +83,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -91,6 +100,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -112,6 +122,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -123,6 +134,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -146,6 +158,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
stateFlow.awaitItem(), stateFlow.awaitItem(),
) )
@ -157,6 +170,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
stateFlow.awaitItem(), stateFlow.awaitItem(),
) )
@ -187,6 +201,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -198,6 +213,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -239,6 +255,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -255,6 +272,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -271,6 +289,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -291,6 +310,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -312,6 +332,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = true, isVaultSyncing = true,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -340,6 +361,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = true, isVaultSyncing = true,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -360,6 +382,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = true, showBottomSheet = true,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -390,6 +413,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = true, isVaultSyncing = true,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -400,6 +424,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
awaitItem(), awaitItem(),
) )
@ -418,6 +443,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -441,6 +467,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
) )
@ -451,8 +478,10 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
} }
} }
@Suppress("MaxLineLength")
@Test @Test
fun `SuccessfulSyncAcknowledged should hide bottom sheet and send NavigateBack`() = runTest { fun `SuccessfulSyncAcknowledged should hide bottom sheet and send NavigateBack event and send Snackbar data through snackbar manager`() =
runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.stateEventFlow(backgroundScope = backgroundScope) { stateFlow, eventFlow -> viewModel.stateEventFlow(backgroundScope = backgroundScope) { stateFlow, eventFlow ->
// Initial state // Initial state
@ -465,6 +494,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = true, isVaultSyncing = true,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
stateFlow.awaitItem(), stateFlow.awaitItem(),
) )
@ -475,6 +505,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = true, showBottomSheet = true,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
stateFlow.awaitItem(), stateFlow.awaitItem(),
) )
@ -486,17 +517,37 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
), ),
stateFlow.awaitItem(), stateFlow.awaitItem(),
) )
assertEquals(ImportLoginsEvent.NavigateBack, eventFlow.awaitItem()) assertEquals(ImportLoginsEvent.NavigateBack, eventFlow.awaitItem())
} }
val expectedSnackbarData = BitwardenSnackbarData(
messageHeader = R.string.logins_imported.asText(),
message = R.string.remember_to_delete_your_imported_password_file_from_your_computer
.asText(),
)
verify {
snackbarRelayManager.sendSnackbarData(
data = expectedSnackbarData,
relay = SnackbarRelay.MY_VAULT_RELAY,
)
}
} }
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel( private fun createViewModel(
snackbarRelay: SnackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
): ImportLoginsViewModel = ImportLoginsViewModel(
savedStateHandle = SavedStateHandle(
mapOf(
"snackbarRelay" to snackbarRelay.name,
),
),
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
environmentRepository = environmentRepository, environmentRepository = environmentRepository,
snackbarRelayManager = snackbarRelayManager,
) )
} }
@ -508,4 +559,5 @@ private val DEFAULT_STATE = ImportLoginsState(
isVaultSyncing = false, isVaultSyncing = false,
showBottomSheet = false, showBottomSheet = false,
currentWebVaultUrl = DEFAULT_VAULT_URL, currentWebVaultUrl = DEFAULT_VAULT_URL,
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
) )

View file

@ -24,9 +24,11 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
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.exit.ExitManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLockOrLogoutDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed import com.x8bit.bitwarden.ui.util.assertLogoutConfirmationDialogIsDisplayed
import com.x8bit.bitwarden.ui.util.assertNoDialogExists import com.x8bit.bitwarden.ui.util.assertNoDialogExists
@ -91,7 +93,10 @@ class VaultScreenTest : BaseComposeTest() {
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true }, onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true }, onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
onNavigateToSearchVault = { onNavigateToSearchScreen = true }, onNavigateToSearchVault = { onNavigateToSearchScreen = true },
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true }, onNavigateToImportLogins = {
onNavigateToImportLoginsCalled = true
assertEquals(SnackbarRelay.MY_VAULT_RELAY, it)
},
exitManager = exitManager, exitManager = exitManager,
intentManager = intentManager, intentManager = intentManager,
permissionsManager = permissionsManager, permissionsManager = permissionsManager,
@ -1195,6 +1200,13 @@ class VaultScreenTest : BaseComposeTest() {
mutableEventFlow.tryEmit(VaultEvent.NavigateToImportLogins) mutableEventFlow.tryEmit(VaultEvent.NavigateToImportLogins)
assertTrue(onNavigateToImportLoginsCalled) assertTrue(onNavigateToImportLoginsCalled)
} }
@Test
fun `when ShowSnackbar is sent snackbar should be displayed`() {
val data = BitwardenSnackbarData("message".asText())
mutableEventFlow.tryEmit(VaultEvent.ShowSnackbar(data))
composeTestRule.onNodeWithText("message").assertIsDisplayed()
}
} }
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary( private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(

View file

@ -33,6 +33,9 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
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.vault.feature.itemlisting.model.ListingItemOverflowAction import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -62,6 +65,8 @@ class VaultViewModelTest : BaseViewModelTest() {
ZoneOffset.UTC, ZoneOffset.UTC,
) )
private val snackbarRelayManager = SnackbarRelayManagerImpl()
private val clipboardManager: BitwardenClipboardManager = mockk { private val clipboardManager: BitwardenClipboardManager = mockk {
every { setText(any<String>()) } just runs every { setText(any<String>()) } just runs
} }
@ -1619,6 +1624,19 @@ class VaultViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `when SnackbarRelay flow updates, snackbar is shown`() = runTest {
val viewModel = createViewModel()
val expectedSnackbarData = BitwardenSnackbarData(message = "test message".asText())
viewModel.eventFlow.test {
snackbarRelayManager.sendSnackbarData(
data = expectedSnackbarData,
relay = SnackbarRelay.MY_VAULT_RELAY,
)
assertEquals(VaultEvent.ShowSnackbar(expectedSnackbarData), awaitItem())
}
}
private fun createViewModel(): VaultViewModel = private fun createViewModel(): VaultViewModel =
VaultViewModel( VaultViewModel(
authRepository = authRepository, authRepository = authRepository,
@ -1630,6 +1648,7 @@ class VaultViewModelTest : BaseViewModelTest() {
organizationEventManager = organizationEventManager, organizationEventManager = organizationEventManager,
featureFlagManager = featureFlagManager, featureFlagManager = featureFlagManager,
firstTimeActionManager = firstTimeActionManager, firstTimeActionManager = firstTimeActionManager,
snackbarRelayManager = snackbarRelayManager,
) )
} }