mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11188 show snackbar after import success. PM-13943 add relay for snackbar events across screen contexts. (#4152)
This commit is contained in:
parent
a1108889cb
commit
8b16135955
26 changed files with 505 additions and 59 deletions
|
@ -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.vault.navigateToVaultSettings
|
||||
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"
|
||||
private const val SETTINGS_ROUTE: String = "settings"
|
||||
|
@ -35,7 +36,7 @@ fun NavGraphBuilder.settingsGraph(
|
|||
onNavigateToPendingRequests: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
startDestination = SETTINGS_ROUTE,
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.navigation.NavController
|
|||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
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"
|
||||
|
||||
|
@ -14,7 +15,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
|
|||
onNavigateBack: () -> Unit,
|
||||
onNavigateToExportVault: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = VAULT_SETTINGS_ROUTE,
|
||||
|
|
|
@ -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.BitwardenTextRow
|
||||
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.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.
|
||||
|
@ -47,12 +50,13 @@ fun VaultSettingsScreen(
|
|||
onNavigateBack: () -> Unit,
|
||||
onNavigateToExportVault: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
viewModel: VaultSettingsViewModel = hiltViewModel(),
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
|
@ -65,11 +69,13 @@ fun VaultSettingsScreen(
|
|||
|
||||
is VaultSettingsEvent.NavigateToImportVault -> {
|
||||
if (state.isNewImportLoginsFlowEnabled) {
|
||||
onNavigateToImportLogins()
|
||||
onNavigateToImportLogins(SnackbarRelay.VAULT_SETTINGS_RELAY)
|
||||
} else {
|
||||
intentManager.launchUri(event.url.toUri())
|
||||
}
|
||||
}
|
||||
|
||||
is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +95,11 @@ fun VaultSettingsScreen(
|
|||
},
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
BitwardenSnackbarHost(
|
||||
bitwardenHostState = snackbarHostState,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
|
|
|
@ -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.util.toBaseWebVaultImportUrl
|
||||
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 kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -22,6 +26,7 @@ import javax.inject.Inject
|
|||
class VaultSettingsViewModel @Inject constructor(
|
||||
environmentRepository: EnvironmentRepository,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
snackbarRelayManager: SnackbarRelayManager,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
|
||||
initialState = run {
|
||||
|
@ -53,6 +58,14 @@ class VaultSettingsViewModel @Inject constructor(
|
|||
}
|
||||
.onEach(::sendAction)
|
||||
.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) {
|
||||
|
@ -74,9 +87,19 @@ class VaultSettingsViewModel @Inject constructor(
|
|||
is VaultSettingsAction.Internal.UserFirstTimeStateChanged -> {
|
||||
handleUserFirstTimeStateChanged(action)
|
||||
}
|
||||
|
||||
is VaultSettingsAction.Internal.SnackbarDataReceived -> {
|
||||
handleSnackbarDataReceived(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSnackbarDataReceived(
|
||||
action: VaultSettingsAction.Internal.SnackbarDataReceived,
|
||||
) {
|
||||
sendEvent(VaultSettingsEvent.ShowSnackbar(action.data))
|
||||
}
|
||||
|
||||
private fun handleImportLoginsCardDismissClicked() {
|
||||
if (!state.shouldShowImportCard) return
|
||||
firstTimeActionManager.storeShowImportLoginsSettingsBadge(showBadge = false)
|
||||
|
@ -166,6 +189,11 @@ sealed class VaultSettingsEvent {
|
|||
data class ShowToast(
|
||||
val message: String,
|
||||
) : 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(
|
||||
val showImportLoginsCard: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the snackbar data has been received.
|
||||
*/
|
||||
data class SnackbarDataReceived(val data: BitwardenSnackbarData) : Internal()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
},
|
||||
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
|
||||
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
|
||||
onNavigateToImportLogins = { navController.navigateToImportLoginsScreen() },
|
||||
onNavigateToImportLogins = {
|
||||
navController.navigateToImportLoginsScreen(snackbarRelay = it)
|
||||
},
|
||||
)
|
||||
deleteAccountDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
|
|||
import androidx.navigation.NavOptions
|
||||
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.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
|
||||
/**
|
||||
|
@ -38,7 +39,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
|||
onNavigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions(
|
||||
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
||||
|
|
|
@ -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.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.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.generatorGraph
|
||||
|
@ -77,7 +78,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
onNavigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -171,7 +172,7 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
navigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
var shouldDimNavBar by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
|
@ -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.resource.ResourceManager
|
||||
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.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -32,4 +34,8 @@ class PlatformUiManagerModule {
|
|||
@Singleton
|
||||
fun provideResourceManager(@ApplicationContext context: Context): ResourceManager =
|
||||
ResourceManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSnackbarRelayManager(): SnackbarRelayManager = SnackbarRelayManagerImpl()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,17 +1,39 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
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.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.
|
||||
*/
|
||||
fun NavController.navigateToImportLoginsScreen(navOptions: NavOptions? = null) {
|
||||
navigate(route = IMPORT_LOGINS_ROUTE, navOptions = navOptions)
|
||||
fun NavController.navigateToImportLoginsScreen(
|
||||
snackbarRelay: SnackbarRelay,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(route = "$IMPORT_LOGINS_PREFIX/$snackbarRelay", navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -22,6 +44,12 @@ fun NavGraphBuilder.importLoginsScreenDestination(
|
|||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = IMPORT_LOGINS_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(IMPORT_LOGINS_NAV_ARG) {
|
||||
type = NavType.StringType
|
||||
nullable = false
|
||||
},
|
||||
),
|
||||
) {
|
||||
ImportLoginsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
|
|
|
@ -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.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.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.components.ImportLoginsInstructionStep
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
|
||||
|
@ -627,6 +628,7 @@ private class ImportLoginsDialogContentPreviewProvider :
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = "vault.bitwarden.com",
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
|
@ -634,6 +636,7 @@ private class ImportLoginsDialogContentPreviewProvider :
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = "vault.bitwarden.com",
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
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.util.Text
|
||||
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 kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -21,9 +25,11 @@ import javax.inject.Inject
|
|||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class ImportLoginsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val snackbarRelayManager: SnackbarRelayManager,
|
||||
) :
|
||||
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
|
||||
initialState = run {
|
||||
|
@ -36,6 +42,7 @@ class ImportLoginsViewModel @Inject constructor(
|
|||
showBottomSheet = false,
|
||||
// attempt to trim the scheme of the vault url
|
||||
currentWebVaultUrl = vaultUrl.toUriOrNull()?.host ?: vaultUrl,
|
||||
snackbarRelay = ImportLoginsArgs(savedStateHandle).snackBarRelay,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -70,6 +77,13 @@ 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 = snackbarData, relay = state.snackbarRelay)
|
||||
sendEvent(ImportLoginsEvent.NavigateBack)
|
||||
}
|
||||
|
||||
|
@ -213,6 +227,7 @@ data class ImportLoginsState(
|
|||
val isVaultSyncing: Boolean,
|
||||
val showBottomSheet: Boolean,
|
||||
val currentWebVaultUrl: String,
|
||||
val snackbarRelay: SnackbarRelay,
|
||||
) {
|
||||
/**
|
||||
* Dialog states for the [ImportLoginsViewModel].
|
||||
|
|
|
@ -5,6 +5,7 @@ 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.itemlisting.navigateToVaultItemListing
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.verificationcode.navigateToVerificationCodeScreen
|
||||
|
@ -24,7 +25,7 @@ fun NavGraphBuilder.vaultGraph(
|
|||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
route = VAULT_GRAPH_ROUTE,
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder
|
|||
import androidx.navigation.NavOptions
|
||||
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.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
|
||||
const val VAULT_ROUTE: String = "vault"
|
||||
|
@ -21,7 +22,7 @@ fun NavGraphBuilder.vaultDestination(
|
|||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
) {
|
||||
composableWithRootPushTransitions(
|
||||
route = VAULT_ROUTE,
|
||||
|
|
|
@ -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.BitwardenScaffold
|
||||
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.composition.LocalExitManager
|
||||
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.intent.IntentManager
|
||||
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.vault.handlers.VaultHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
|
@ -83,7 +87,7 @@ fun VaultScreen(
|
|||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
|
@ -97,6 +101,7 @@ fun VaultScreen(
|
|||
{ viewModel.trySendAction(VaultAction.RefreshPull) }
|
||||
},
|
||||
)
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen()
|
||||
|
@ -124,7 +129,11 @@ fun VaultScreen(
|
|||
.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) }
|
||||
|
@ -137,6 +146,7 @@ fun VaultScreen(
|
|||
pullToRefreshState = pullToRefreshState,
|
||||
vaultHandlers = vaultHandlers,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -173,6 +183,7 @@ private fun VaultScreenScaffold(
|
|||
pullToRefreshState: BitwardenPullToRefreshState,
|
||||
vaultHandlers: VaultHandlers,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
snackbarHostState: BitwardenSnackbarHostState,
|
||||
) {
|
||||
var accountMenuVisible by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
|
@ -261,6 +272,11 @@ private fun VaultScreenScaffold(
|
|||
},
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
BitwardenSnackbarHost(
|
||||
bitwardenHostState = snackbarHostState,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
AnimatedVisibility(
|
||||
visible = state.viewState.hasFab && !accountMenuVisible,
|
||||
|
|
|
@ -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.VaultData
|
||||
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.asText
|
||||
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.IconData
|
||||
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.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
|
@ -71,8 +75,9 @@ class VaultViewModel @Inject constructor(
|
|||
private val policyManager: PolicyManager,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
featureFlagManager: FeatureFlagManager,
|
||||
snackbarRelayManager: SnackbarRelayManager,
|
||||
) : BaseViewModel<VaultState, VaultEvent, VaultAction>(
|
||||
initialState = run {
|
||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||
|
@ -143,6 +148,14 @@ class VaultViewModel @Inject constructor(
|
|||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
snackbarRelayManager
|
||||
.getSnackbarDataFlow(SnackbarRelay.MY_VAULT_RELAY)
|
||||
.map {
|
||||
VaultAction.Internal.SnackbarDataReceive(it)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultAction) {
|
||||
|
@ -458,9 +471,15 @@ class VaultViewModel @Inject constructor(
|
|||
is VaultAction.Internal.ValidatePasswordResultReceive -> {
|
||||
handleValidatePasswordResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAction.Internal.SnackbarDataReceive -> handleSnackbarDataReceive(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSnackbarDataReceive(action: VaultAction.Internal.SnackbarDataReceive) {
|
||||
sendEvent(VaultEvent.ShowSnackbar(action.data))
|
||||
}
|
||||
|
||||
private fun handleGenerateTotpResultReceive(
|
||||
action: VaultAction.Internal.GenerateTotpResultReceive,
|
||||
) {
|
||||
|
@ -1011,6 +1030,11 @@ sealed class VaultEvent {
|
|||
* Show a toast with the given [message].
|
||||
*/
|
||||
data class ShowToast(val message: Text) : VaultEvent()
|
||||
|
||||
/**
|
||||
* Show a snackbar with the given [data].
|
||||
*/
|
||||
data class ShowSnackbar(val data: BitwardenSnackbarData) : VaultEvent(), BackgroundEvent
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1217,6 +1241,13 @@ sealed class VaultAction {
|
|||
val overflowAction: ListingItemOverflowAction.VaultAction,
|
||||
val result: ValidatePasswordResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that a snackbar data was received.
|
||||
*/
|
||||
data class SnackbarDataReceive(
|
||||
val data: BitwardenSnackbarData,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1073,4 +1073,6 @@ Do you want to switch to this account?</string>
|
|||
<string name="bitwarden_tools">Bitwarden Tools</string>
|
||||
<string name="got_it">Got it</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>
|
||||
|
|
|
@ -11,7 +11,10 @@ import androidx.compose.ui.test.performScrollTo
|
|||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
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.snackbar.SnackbarRelay
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -19,6 +22,7 @@ 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
|
||||
|
@ -56,7 +60,10 @@ class VaultSettingsScreenTest : BaseComposeTest() {
|
|||
onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
|
||||
onNavigateToFolders = { onNavigateToFoldersCalled = true },
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when ShowSnackbar is sent snackbar should be displayed`() {
|
||||
val data = BitwardenSnackbarData("message".asText())
|
||||
mutableEventFlow.tryEmit(VaultSettingsEvent.ShowSnackbar(data))
|
||||
composeTestRule.onNodeWithText("message").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.repository.util.FakeEnvironmentRepository
|
||||
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.just
|
||||
import io.mockk.mockk
|
||||
|
@ -34,6 +38,8 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
|
|||
every { storeShowImportLoginsSettingsBadge(any()) } just runs
|
||||
}
|
||||
|
||||
private val snackbarRelayManager = SnackbarRelayManagerImpl()
|
||||
|
||||
@Test
|
||||
fun `BackClick should emit NavigateBack`() = runTest {
|
||||
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(
|
||||
environmentRepository = environmentRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.util.asText
|
||||
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 io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -491,4 +492,5 @@ private val DEFAULT_STATE = ImportLoginsState(
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = "vault.bitwarden.com",
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.R
|
||||
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.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.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
@ -55,6 +59,10 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
unmockkStatic(Uri::parse)
|
||||
}
|
||||
|
||||
private val snackbarRelayManager: SnackbarRelayManagerImpl = mockk() {
|
||||
coEvery { sendSnackbarData(any(), any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state is correct`() {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -75,6 +83,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -91,6 +100,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -112,6 +122,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -123,6 +134,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -146,6 +158,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
|
@ -157,6 +170,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
|
@ -187,6 +201,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -198,6 +213,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -239,6 +255,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -255,6 +272,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -271,6 +289,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -291,6 +310,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -312,6 +332,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = true,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -340,6 +361,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = true,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -360,6 +382,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = true,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -390,6 +413,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = true,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -400,6 +424,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
|
@ -418,6 +443,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -441,6 +467,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -451,52 +478,76 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `SuccessfulSyncAcknowledged should hide bottom sheet and send NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateEventFlow(backgroundScope = backgroundScope) { stateFlow, eventFlow ->
|
||||
// Initial state
|
||||
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
|
||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = true,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
fun `SuccessfulSyncAcknowledged should hide bottom sheet and send NavigateBack event and send Snackbar data through snackbar manager`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateEventFlow(backgroundScope = backgroundScope) { stateFlow, eventFlow ->
|
||||
// Initial state
|
||||
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
|
||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = true,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = false,
|
||||
showBottomSheet = true,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
),
|
||||
stateFlow.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(),
|
||||
)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = false,
|
||||
showBottomSheet = true,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(ImportLoginsEvent.NavigateBack, eventFlow.awaitItem())
|
||||
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,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
environmentRepository = environmentRepository,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -508,4 +559,5 @@ private val DEFAULT_STATE = ImportLoginsState(
|
|||
isVaultSyncing = false,
|
||||
showBottomSheet = false,
|
||||
currentWebVaultUrl = DEFAULT_VAULT_URL,
|
||||
snackbarRelay = SnackbarRelay.MY_VAULT_RELAY,
|
||||
)
|
||||
|
|
|
@ -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.util.asText
|
||||
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.intent.IntentManager
|
||||
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.assertLogoutConfirmationDialogIsDisplayed
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
|
@ -91,7 +93,10 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
|
||||
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
|
||||
onNavigateToSearchVault = { onNavigateToSearchScreen = true },
|
||||
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
|
||||
onNavigateToImportLogins = {
|
||||
onNavigateToImportLoginsCalled = true
|
||||
assertEquals(SnackbarRelay.MY_VAULT_RELAY, it)
|
||||
},
|
||||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
|
@ -1195,6 +1200,13 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
mutableEventFlow.tryEmit(VaultEvent.NavigateToImportLogins)
|
||||
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(
|
||||
|
|
|
@ -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.util.asText
|
||||
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.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
|
@ -62,6 +65,8 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
private val snackbarRelayManager = SnackbarRelayManagerImpl()
|
||||
|
||||
private val clipboardManager: BitwardenClipboardManager = mockk {
|
||||
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 =
|
||||
VaultViewModel(
|
||||
authRepository = authRepository,
|
||||
|
@ -1630,6 +1648,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
organizationEventManager = organizationEventManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue