PM-23292: Migrate toasts to snackbars (#5940)

This commit is contained in:
David Perez 2025-09-26 10:09:35 -05:00 committed by GitHub
parent 7bf4acbb28
commit d5d4caea62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 175 additions and 69 deletions

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -26,7 +25,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@ -48,6 +46,9 @@ import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.field.BitwardenTextField import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.text.BitwardenClickableText import com.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.components.util.rememberVectorPainter
@ -74,16 +75,15 @@ fun CompleteRegistrationScreen(
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val handler = rememberCompleteRegistrationHandler(viewModel = viewModel) val handler = rememberCompleteRegistrationHandler(viewModel = viewModel)
val context = LocalContext.current val snackbarHostState = rememberBitwardenSnackbarHostState()
// route OS back actions through the VM to clear the special circumstance // route OS back actions through the VM to clear the special circumstance
BackHandler(onBack = handler.onBackClick) BackHandler(onBack = handler.onBackClick)
EventsEffect(viewModel) { event -> EventsEffect(viewModel) { event ->
when (event) { when (event) {
is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke() is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke()
is CompleteRegistrationEvent.ShowToast -> { is CompleteRegistrationEvent.ShowSnackbar -> {
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show() snackbarHostState.showSnackbar(BitwardenSnackbarData(message = event.message))
} }
CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance() CompleteRegistrationEvent.NavigateToMakePasswordStrong -> onNavigateToPasswordGuidance()
@ -143,6 +143,7 @@ fun CompleteRegistrationScreen(
onNavigationIconClick = handler.onBackClick, onNavigationIconClick = handler.onBackClick,
) )
}, },
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier

View File

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.util.isValidEmail import com.bitwarden.ui.platform.base.util.isValidEmail
import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenPlurals
@ -54,6 +55,7 @@ class CompleteRegistrationViewModel @Inject constructor(
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository, private val environmentRepository: EnvironmentRepository,
private val specialCircumstanceManager: SpecialCircumstanceManager, private val specialCircumstanceManager: SpecialCircumstanceManager,
private val toastManager: ToastManager,
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>( ) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
initialState = savedStateHandle[KEY_STATE] ?: run { initialState = savedStateHandle[KEY_STATE] ?: run {
val args = savedStateHandle.toCompleteRegistrationArgs() val args = savedStateHandle.toCompleteRegistrationArgs()
@ -146,9 +148,7 @@ class CompleteRegistrationViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
sendEvent( sendEvent(
CompleteRegistrationEvent.ShowToast( CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()),
message = BitwardenString.email_verified.asText(),
),
) )
} }
} }
@ -243,11 +243,7 @@ class CompleteRegistrationViewModel @Inject constructor(
private fun handleLoginResult(action: Internal.ReceiveLoginResult) { private fun handleLoginResult(action: Internal.ReceiveLoginResult) {
clearDialogState() clearDialogState()
sendEvent( toastManager.show(messageId = BitwardenString.account_created_success)
CompleteRegistrationEvent.ShowToast(
message = BitwardenString.account_created_success.asText(),
),
)
authRepository.setOnboardingStatus( authRepository.setOnboardingStatus(
status = OnboardingStatus.NOT_STARTED, status = OnboardingStatus.NOT_STARTED,
@ -504,9 +500,9 @@ sealed class CompleteRegistrationEvent {
data object NavigateBack : CompleteRegistrationEvent() data object NavigateBack : CompleteRegistrationEvent()
/** /**
* Show a toast with the given message. * Show a snackbar with the given message.
*/ */
data class ShowToast( data class ShowSnackbar(
val message: Text, val message: Text,
) : CompleteRegistrationEvent() ) : CompleteRegistrationEvent()

View File

@ -33,6 +33,8 @@ import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton import com.bitwarden.ui.platform.components.fab.BitwardenFloatingActionButton
import com.bitwarden.ui.platform.components.row.BitwardenTextRow import com.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
@ -54,6 +56,7 @@ fun FoldersScreen(
viewModel: FoldersViewModel = hiltViewModel(), viewModel: FoldersViewModel = hiltViewModel(),
) { ) {
val state = viewModel.stateFlow.collectAsStateWithLifecycle() val state = viewModel.stateFlow.collectAsStateWithLifecycle()
val snackbarHostState = rememberBitwardenSnackbarHostState()
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
is FoldersEvent.NavigateBack -> onNavigateBack() is FoldersEvent.NavigateBack -> onNavigateBack()
@ -61,6 +64,8 @@ fun FoldersScreen(
is FoldersEvent.NavigateToEditFolderScreen -> { is FoldersEvent.NavigateToEditFolderScreen -> {
onNavigateToEditFolderScreen(event.folderId) onNavigateToEditFolderScreen(event.folderId)
} }
is FoldersEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
} }
} }
@ -92,6 +97,7 @@ fun FoldersScreen(
.navigationBarsPadding(), .navigationBarsPadding(),
) )
}, },
snackbarHost = { BitwardenSnackbarHost(bitwardenHostState = snackbarHostState) },
) { ) {
when (val viewState = state.value.viewState) { when (val viewState = state.value.viewState) {
is FoldersState.ViewState.Content -> { is FoldersState.ViewState.Content -> {

View File

@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.ui.platform.base.BackgroundEvent
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -11,8 +13,11 @@ import com.bitwarden.ui.util.concat
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem
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.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ -25,6 +30,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FoldersViewModel @Inject constructor( class FoldersViewModel @Inject constructor(
vaultRepository: VaultRepository, vaultRepository: VaultRepository,
snackbarRelayManager: SnackbarRelayManager,
) : BaseViewModel<FoldersState, FoldersEvent, FoldersAction>( ) : BaseViewModel<FoldersState, FoldersEvent, FoldersAction>(
initialState = FoldersState(viewState = FoldersState.ViewState.Loading), initialState = FoldersState(viewState = FoldersState.ViewState.Loading),
) { ) {
@ -33,15 +39,31 @@ class FoldersViewModel @Inject constructor(
.foldersStateFlow .foldersStateFlow
.onEach { sendAction(FoldersAction.Internal.VaultDataReceive(it)) } .onEach { sendAction(FoldersAction.Internal.VaultDataReceive(it)) }
.launchIn(viewModelScope) .launchIn(viewModelScope)
snackbarRelayManager
.getSnackbarDataFlow(
SnackbarRelay.FOLDER_CREATED,
SnackbarRelay.FOLDER_DELETED,
SnackbarRelay.FOLDER_UPDATED,
)
.map { FoldersAction.Internal.SnackbarDataReceived(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
} }
override fun handleAction(action: FoldersAction): Unit = when (action) { override fun handleAction(action: FoldersAction): Unit = when (action) {
is FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked() is FoldersAction.AddFolderButtonClick -> handleAddFolderButtonClicked()
is FoldersAction.CloseButtonClick -> handleCloseButtonClicked() is FoldersAction.CloseButtonClick -> handleCloseButtonClicked()
is FoldersAction.Internal.VaultDataReceive -> handleVaultDataReceive(action) is FoldersAction.Internal -> handleInternalAction(action)
is FoldersAction.FolderClick -> handleFolderClick(action) is FoldersAction.FolderClick -> handleFolderClick(action)
} }
private fun handleInternalAction(action: FoldersAction.Internal) {
when (action) {
is FoldersAction.Internal.SnackbarDataReceived -> handleSnackbarDataReceived(action)
is FoldersAction.Internal.VaultDataReceive -> handleVaultDataReceive(action)
}
}
private fun handleFolderClick(action: FoldersAction.FolderClick) { private fun handleFolderClick(action: FoldersAction.FolderClick) {
sendEvent(FoldersEvent.NavigateToEditFolderScreen(action.folderId)) sendEvent(FoldersEvent.NavigateToEditFolderScreen(action.folderId))
} }
@ -54,6 +76,10 @@ class FoldersViewModel @Inject constructor(
sendEvent(FoldersEvent.NavigateBack) sendEvent(FoldersEvent.NavigateBack)
} }
private fun handleSnackbarDataReceived(action: FoldersAction.Internal.SnackbarDataReceived) {
sendEvent(FoldersEvent.ShowSnackbar(action.data))
}
@Suppress("LongMethod") @Suppress("LongMethod")
private fun handleVaultDataReceive(action: FoldersAction.Internal.VaultDataReceive) { private fun handleVaultDataReceive(action: FoldersAction.Internal.VaultDataReceive) {
when (val vaultDataState = action.vaultDataState) { when (val vaultDataState = action.vaultDataState) {
@ -180,6 +206,13 @@ sealed class FoldersEvent {
* Navigates to the screen to edit a folder. * Navigates to the screen to edit a folder.
*/ */
data class NavigateToEditFolderScreen(val folderId: String) : FoldersEvent() data class NavigateToEditFolderScreen(val folderId: String) : FoldersEvent()
/**
* Show a snackbar.
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : FoldersEvent(), BackgroundEvent
} }
/** /**
@ -205,6 +238,12 @@ sealed class FoldersAction {
* Actions for internal use by the ViewModel. * Actions for internal use by the ViewModel.
*/ */
sealed class Internal : FoldersAction() { sealed class Internal : FoldersAction() {
/**
* Indicates that the vault folders data has been received.
*/
data class SnackbarDataReceived(
val data: BitwardenSnackbarData,
) : Internal()
/** /**
* Indicates that the vault folders data has been received. * Indicates that the vault folders data has been received.

View File

@ -1,6 +1,5 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit package com.x8bit.bitwarden.ui.platform.feature.settings.folders.addedit
import android.widget.Toast
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -18,7 +17,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -55,16 +53,10 @@ fun FolderAddEditScreen(
viewModel: FolderAddEditViewModel = hiltViewModel(), viewModel: FolderAddEditViewModel = hiltViewModel(),
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
var shouldShowConfirmationDialog by rememberSaveable { mutableStateOf(false) } var shouldShowConfirmationDialog by rememberSaveable { mutableStateOf(false) }
EventsEffect(viewModel = viewModel) { event -> EventsEffect(viewModel = viewModel) { event ->
when (event) { when (event) {
is FolderAddEditEvent.NavigateBack -> onNavigateBack.invoke() is FolderAddEditEvent.NavigateBack -> onNavigateBack.invoke()
is FolderAddEditEvent.ShowToast -> {
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
}
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
@ -16,6 +17,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType
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.onEach import kotlinx.coroutines.flow.onEach
@ -35,6 +38,7 @@ private const val KEY_STATE = "state"
class FolderAddEditViewModel @Inject constructor( class FolderAddEditViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val vaultRepository: VaultRepository, private val vaultRepository: VaultRepository,
private val relayManager: SnackbarRelayManager,
) : BaseViewModel<FolderAddEditState, FolderAddEditEvent, FolderAddEditAction>( ) : BaseViewModel<FolderAddEditState, FolderAddEditEvent, FolderAddEditAction>(
// We load the state from the savedStateHandle for testing purposes. // We load the state from the savedStateHandle for testing purposes.
initialState = savedStateHandle[KEY_STATE] initialState = savedStateHandle[KEY_STATE]
@ -263,7 +267,10 @@ class FolderAddEditViewModel @Inject constructor(
} }
is UpdateFolderResult.Success -> { is UpdateFolderResult.Success -> {
sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_updated.asText())) relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_updated.asText()),
relay = SnackbarRelay.FOLDER_UPDATED,
)
sendEvent(FolderAddEditEvent.NavigateBack) sendEvent(FolderAddEditEvent.NavigateBack)
} }
} }
@ -289,7 +296,10 @@ class FolderAddEditViewModel @Inject constructor(
} }
is CreateFolderResult.Success -> { is CreateFolderResult.Success -> {
sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_created.asText())) relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_created.asText()),
relay = SnackbarRelay.FOLDER_CREATED,
)
sendEvent(FolderAddEditEvent.NavigateBack) sendEvent(FolderAddEditEvent.NavigateBack)
} }
} }
@ -312,7 +322,10 @@ class FolderAddEditViewModel @Inject constructor(
DeleteFolderResult.Success -> { DeleteFolderResult.Success -> {
mutableStateFlow.update { it.copy(dialog = null) } mutableStateFlow.update { it.copy(dialog = null) }
sendEvent(FolderAddEditEvent.ShowToast(BitwardenString.folder_deleted.asText())) relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()),
relay = SnackbarRelay.FOLDER_DELETED,
)
sendEvent(event = FolderAddEditEvent.NavigateBack) sendEvent(event = FolderAddEditEvent.NavigateBack)
} }
} }
@ -416,11 +429,6 @@ sealed class FolderAddEditEvent {
* Navigate back to previous screen. * Navigate back to previous screen.
*/ */
data object NavigateBack : FolderAddEditEvent() data object NavigateBack : FolderAddEditEvent()
/**
* Shows a toast with the given [message].
*/
data class ShowToast(val message: Text) : FolderAddEditEvent()
} }
/** /**

View File

@ -16,6 +16,9 @@ enum class SnackbarRelay {
CIPHER_RESTORED, CIPHER_RESTORED,
CIPHER_UPDATED, CIPHER_UPDATED,
ENVIRONMENT_SAVED, ENVIRONMENT_SAVED,
FOLDER_CREATED,
FOLDER_DELETED,
FOLDER_UPDATED,
LOGIN_APPROVAL, LOGIN_APPROVAL,
LOGIN_SUCCESS, LOGIN_SUCCESS,
LOGINS_IMPORTED, LOGINS_IMPORTED,

View File

@ -189,6 +189,7 @@ class VaultViewModel @Inject constructor(
SnackbarRelay.CIPHER_DELETED_SOFT, SnackbarRelay.CIPHER_DELETED_SOFT,
SnackbarRelay.CIPHER_RESTORED, SnackbarRelay.CIPHER_RESTORED,
SnackbarRelay.CIPHER_UPDATED, SnackbarRelay.CIPHER_UPDATED,
SnackbarRelay.FOLDER_CREATED,
SnackbarRelay.LOGINS_IMPORTED, SnackbarRelay.LOGINS_IMPORTED,
), ),
) )

View File

@ -70,6 +70,14 @@ class CompleteRegistrationScreenTest : BitwardenComposeTest() {
} }
} }
@Test
fun `on ShowSnackbar should display snackbar content`() {
val message = "message"
composeTestRule.onNodeWithText(text = message).assertDoesNotExist()
mutableEventFlow.tryEmit(CompleteRegistrationEvent.ShowSnackbar(message = message.asText()))
composeTestRule.onNodeWithText(text = message).assertIsDisplayed()
}
@Test @Test
fun `close click should send CloseClick action`() { fun `close click should send CloseClick action`() {
composeTestRule.onNodeWithContentDescription("Back").performClick() composeTestRule.onNodeWithContentDescription("Back").performClick()

View File

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.data.manager.toast.ToastManager
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
@ -38,6 +39,7 @@ import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
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 io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -46,7 +48,6 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -60,13 +61,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(null) private val mutableUserStateFlow = MutableStateFlow<UserState?>(null)
private val mockAuthRepository = mockk<AuthRepository> { private val mockAuthRepository = mockk<AuthRepository> {
every { userStateFlow } returns mutableUserStateFlow every { userStateFlow } returns mutableUserStateFlow
coEvery { coEvery { login(email = any(), password = any()) } returns LoginResult.Success
login(
email = any(),
password = any(),
)
} returns LoginResult.Success
coEvery { coEvery {
register( register(
email = any(), email = any(),
@ -77,10 +72,10 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
isMasterPasswordStrong = any(), isMasterPasswordStrong = any(),
) )
} returns RegisterResult.Success } returns RegisterResult.Success
coEvery { setOnboardingStatus(OnboardingStatus.NOT_STARTED) } just Runs
coEvery { }
setOnboardingStatus(OnboardingStatus.NOT_STARTED) private val toastManager: ToastManager = mockk {
} just Runs every { show(messageId = any(), duration = any()) } just runs
} }
private val fakeEnvironmentRepository = FakeEnvironmentRepository() private val fakeEnvironmentRepository = FakeEnvironmentRepository()
@ -155,21 +150,18 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
) )
} returns RegisterResult.Success } returns RegisterResult.Success
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE)
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow -> viewModel.stateFlow.test {
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem()) assertEquals(VALID_INPUT_STATE, awaitItem())
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
assertEquals( assertEquals(
VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading), VALID_INPUT_STATE.copy(dialog = CompleteRegistrationDialog.Loading),
stateFlow.awaitItem(), awaitItem(),
)
assertEquals(
CompleteRegistrationEvent.ShowToast(
BitwardenString.account_created_success.asText(),
),
eventFlow.awaitItem(),
) )
// Make sure loading dialog is hidden: // Make sure loading dialog is hidden:
assertEquals(VALID_INPUT_STATE, stateFlow.awaitItem()) assertEquals(VALID_INPUT_STATE, awaitItem())
}
verify(exactly = 1) {
toastManager.show(messageId = BitwardenString.account_created_success)
} }
} }
@ -224,7 +216,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast)
expectNoEvents() expectNoEvents()
} }
verify(exactly = 1) { verify(exactly = 1) {
@ -246,7 +237,6 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE) val viewModel = createCompleteRegistrationViewModel(VALID_INPUT_STATE)
viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick) viewModel.trySendAction(CompleteRegistrationAction.CallToActionClick)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertTrue(awaitItem() is CompleteRegistrationEvent.ShowToast)
assertEquals( assertEquals(
CompleteRegistrationEvent.NavigateToLogin(EMAIL), CompleteRegistrationEvent.NavigateToLogin(EMAIL),
awaitItem(), awaitItem(),
@ -404,13 +394,13 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
} }
@Test @Test
fun `On init should show toast if from email is true`() = runTest { fun `On init should show snackbar if from email is true`() = runTest {
val viewModel = createCompleteRegistrationViewModel( val viewModel = createCompleteRegistrationViewModel(
DEFAULT_STATE.copy(fromEmail = true), DEFAULT_STATE.copy(fromEmail = true),
) )
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals( assertEquals(
CompleteRegistrationEvent.ShowToast(BitwardenString.email_verified.asText()), CompleteRegistrationEvent.ShowSnackbar(BitwardenString.email_verified.asText()),
awaitItem(), awaitItem(),
) )
} }
@ -666,6 +656,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
environmentRepository = fakeEnvironmentRepository, environmentRepository = fakeEnvironmentRepository,
specialCircumstanceManager = specialCircumstanceManager, specialCircumstanceManager = specialCircumstanceManager,
generatorRepository = generatorRepository, generatorRepository = generatorRepository,
toastManager = toastManager,
) )
companion object { companion object {

View File

@ -3,13 +3,16 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.folders
import app.cash.turbine.test import app.cash.turbine.test
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat import com.bitwarden.ui.util.concat
import com.bitwarden.vault.FolderView import com.bitwarden.vault.FolderView
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderDisplayItem
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -25,6 +28,26 @@ class FoldersViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk { private val vaultRepository: VaultRepository = mockk {
every { foldersStateFlow } returns mutableFoldersStateFlow every { foldersStateFlow } returns mutableFoldersStateFlow
} }
private val mutableSnackbarDataFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
private val snackbarRelayManager: SnackbarRelayManager = mockk {
every {
getSnackbarDataFlow(relay = any(), relays = anyVararg())
} returns mutableSnackbarDataFlow
}
@Test
fun `on snackbar data received should emit ShowSnackbar`() = runTest {
val viewModel = createViewModel()
val data = BitwardenSnackbarData(message = "Snackbar!".asText())
viewModel.eventFlow.test {
mutableSnackbarDataFlow.emit(data)
assertEquals(
FoldersEvent.ShowSnackbar(data = data),
awaitItem(),
)
}
}
@Test @Test
fun `BackClick should emit NavigateBack`() = runTest { fun `BackClick should emit NavigateBack`() = runTest {
@ -161,6 +184,7 @@ class FoldersViewModelTest : BaseViewModelTest() {
private fun createViewModel(): FoldersViewModel = FoldersViewModel( private fun createViewModel(): FoldersViewModel = FoldersViewModel(
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
snackbarRelayManager = snackbarRelayManager,
) )
private fun createFolderState( private fun createFolderState(

View File

@ -5,6 +5,7 @@ import app.cash.turbine.test
import com.bitwarden.core.DateTime import com.bitwarden.core.DateTime
import com.bitwarden.core.data.repository.model.DataState import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.base.BaseViewModelTest
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText import com.bitwarden.ui.util.asText
import com.bitwarden.ui.util.concat import com.bitwarden.ui.util.concat
@ -14,11 +15,15 @@ import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult import com.x8bit.bitwarden.data.vault.repository.model.DeleteFolderResult
import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult import com.x8bit.bitwarden.data.vault.repository.model.UpdateFolderResult
import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddEditType
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
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.mockk import io.mockk.mockk
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 io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -38,6 +43,9 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
private val vaultRepository: VaultRepository = mockk { private val vaultRepository: VaultRepository = mockk {
every { getVaultFolderStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableFoldersStateFlow every { getVaultFolderStateFlow(DEFAULT_EDIT_ITEM_ID) } returns mutableFoldersStateFlow
} }
private val relayManager: SnackbarRelayManager = mockk {
every { sendSnackbarData(data = any(), relay = any()) } just runs
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
@ -125,15 +133,17 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(FolderAddEditAction.DeleteClick) viewModel.trySendAction(FolderAddEditAction.DeleteClick)
viewModel.eventFlow.test { viewModel.eventFlow.test {
assertEquals(
FolderAddEditEvent.ShowToast(BitwardenString.folder_deleted.asText()),
awaitItem(),
)
assertEquals( assertEquals(
FolderAddEditEvent.NavigateBack, FolderAddEditEvent.NavigateBack,
awaitItem(), awaitItem(),
) )
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()),
relay = SnackbarRelay.FOLDER_DELETED,
)
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -180,6 +190,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithDialog, awaitItem())
assertEquals(stateWithoutDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem())
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_deleted.asText()),
relay = SnackbarRelay.FOLDER_DELETED,
)
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -322,6 +338,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithDialog, awaitItem())
assertEquals(stateWithoutDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem())
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_created.asText()),
relay = SnackbarRelay.FOLDER_CREATED,
)
}
} }
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@ -348,9 +370,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
vaultRepository.createFolder(any()) vaultRepository.createFolder(any())
} returns CreateFolderResult.Success(mockk()) } returns CreateFolderResult.Success(mockk())
viewModel.trySendAction(FolderAddEditAction.SaveClick) viewModel.trySendAction(FolderAddEditAction.SaveClick)
coVerify( coVerify(exactly = 1) {
exactly = 1,
) {
vaultRepository.createFolder( vaultRepository.createFolder(
folderView = FolderView( folderView = FolderView(
name = DEFAULT_FOLDER_NAME, name = DEFAULT_FOLDER_NAME,
@ -359,6 +379,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
), ),
) )
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_created.asText()),
relay = SnackbarRelay.FOLDER_CREATED,
)
}
unmockkStatic(DateTime::class) unmockkStatic(DateTime::class)
} }
@ -387,9 +413,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
vaultRepository.createFolder(any()) vaultRepository.createFolder(any())
} returns CreateFolderResult.Success(mockk()) } returns CreateFolderResult.Success(mockk())
viewModel.trySendAction(FolderAddEditAction.SaveClick) viewModel.trySendAction(FolderAddEditAction.SaveClick)
coVerify( coVerify(exactly = 1) {
exactly = 1,
) {
vaultRepository.createFolder( vaultRepository.createFolder(
folderView = FolderView( folderView = FolderView(
name = "$parentFolderName/$DEFAULT_FOLDER_NAME", name = "$parentFolderName/$DEFAULT_FOLDER_NAME",
@ -398,6 +422,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
), ),
) )
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_created.asText()),
relay = SnackbarRelay.FOLDER_CREATED,
)
}
unmockkStatic(DateTime::class) unmockkStatic(DateTime::class)
} }
@ -483,6 +513,12 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
assertEquals(stateWithDialog, awaitItem()) assertEquals(stateWithDialog, awaitItem())
assertEquals(stateWithoutDialog, awaitItem()) assertEquals(stateWithoutDialog, awaitItem())
} }
verify(exactly = 1) {
relayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.folder_updated.asText()),
relay = SnackbarRelay.FOLDER_UPDATED,
)
}
} }
@Test @Test
@ -758,6 +794,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
): FolderAddEditViewModel = FolderAddEditViewModel( ): FolderAddEditViewModel = FolderAddEditViewModel(
savedStateHandle = savedStateHandle, savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository, vaultRepository = vaultRepository,
relayManager = relayManager,
) )
} }