Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -785,7 +785,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
}

@Test
fun `FileChose should emit ShowToast`() = runTest {
fun `FileChose should update the state accordingly`() = runTest {
val initialState = DEFAULT_STATE.copy(
viewState = DEFAULT_VIEW_STATE.copy(
selectedType = AddEditSendState.ViewState.Content.SendType.File(
Expand Down Expand Up @@ -824,7 +824,7 @@ class AddEditSendViewModelTest : BaseViewModelTest() {
}

@Test
fun `ChooseFileClick should emit ShowToast`() = runTest {
fun `ChooseFileClick should emit ShowChooserSheet`() = runTest {
val arePermissionsGranted = true
val viewModel = createViewModel()
viewModel.eventFlow.test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {

@Test
@Suppress("MaxLineLength")
fun `ConfirmDeleteClick with DeleteCipherResult Success should emit ShowToast and NavigateBack`() =
fun `ConfirmDeleteClick with DeleteCipherResult Success should emit send snackbar event and NavigateBack`() =
runTest {
val cipherListView = createMockCipherListView(number = 1)
val cipherView = createMockCipherView(number = 1)
Expand Down Expand Up @@ -1326,7 +1326,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}

@Test
fun `in add mode, createCipherInOrganization success should ShowToast and NavigateBack`() =
@Suppress("MaxLineLength")
fun `in add mode, createCipherInOrganization success should send snackbar event and NavigateBack`() =
runTest {
val stateWithName = createVaultAddItemState(
commonContentViewState = createCommonContentViewState(
Expand Down Expand Up @@ -1656,39 +1657,41 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
}

@Test
fun `in edit mode, updateCipher success should ShowToast and NavigateBack`() = runTest {
val cipherView = createMockCipherListView(1)
val stateWithName = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)

mutableVaultDataFlow.value = DataState.Loaded(createVaultData(cipherListView = cipherView))
fun `in edit mode, updateCipher success should send snackbar event and NavigateBack`() =
runTest {
val cipherView = createMockCipherListView(1)
val stateWithName = createVaultAddItemState(
vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID),
commonContentViewState = createCommonContentViewState(
name = "mockName-1",
),
)

val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
)
mutableVaultDataFlow.value =
DataState.Loaded(createVaultData(cipherListView = cipherView))

coEvery {
vaultRepository.updateCipher(any(), any())
} returns UpdateCipherResult.Success
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(VaultAddEditEvent.NavigateBack, awaitItem())
}
verify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.item_updated.asText()),
relay = SnackbarRelay.CIPHER_UPDATED,
val viewModel = createAddVaultItemViewModel(
createSavedStateHandleWithState(
state = stateWithName,
vaultAddEditType = VaultAddEditType.AddItem,
vaultItemCipherType = VaultItemCipherType.LOGIN,
),
)

coEvery {
vaultRepository.updateCipher(any(), any())
} returns UpdateCipherResult.Success
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAddEditAction.Common.SaveClick)
assertEquals(VaultAddEditEvent.NavigateBack, awaitItem())
}
verify(exactly = 1) {
snackbarRelayManager.sendSnackbarData(
data = BitwardenSnackbarData(BitwardenString.item_updated.asText()),
relay = SnackbarRelay.CIPHER_UPDATED,
)
}
}
}

@Test
fun `in add mode, SaveClick with no network connection error should show error dialog`() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ class AttachmentsViewModelTest : BaseViewModelTest() {
}

@Test
fun `ChooseFileClick should emit ShowToast`() = runTest {
fun `ChooseFileClick should emit ShowChooserSheet`() = runTest {
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(AttachmentsAction.ChooseFileClick)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class QrCodeScanViewModelTest : BaseViewModelTest() {
}

@Test
fun `ManualEntryTextClick should emit ShowToast`() = runTest {
fun `ManualEntryTextClick should emit NavigateToManualCodeEntry`() = runTest {
val viewModel = createViewModel()

viewModel.eventFlow.test {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.bitwarden.authenticator.ui.authenticator.feature.edititem

import android.widget.Toast
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -18,8 +17,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
Expand Down Expand Up @@ -67,24 +64,9 @@ fun EditItemScreen(
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val resources = LocalResources.current

EventsEffect(viewModel = viewModel) { event ->
when (event) {
EditItemEvent.NavigateBack -> {
onNavigateBack()
}

is EditItemEvent.ShowToast -> {
Toast
.makeText(
context,
event.message(resources),
Toast.LENGTH_LONG,
)
.show()
}
EditItemEvent.NavigateBack -> onNavigateBack()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.CreateIte
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.EditItemState.Companion.MAX_ALLOWED_CODE_DIGITS
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.EditItemState.Companion.MIN_ALLOWED_CODE_DIGITS
import com.bitwarden.authenticator.ui.authenticator.feature.edititem.model.EditItemData
import com.bitwarden.authenticator.ui.platform.model.SnackbarRelay
import com.bitwarden.core.data.repository.model.DataState
import com.bitwarden.core.data.repository.util.takeUntilLoaded
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.base.util.isBase32
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
Expand All @@ -39,6 +42,7 @@ private const val KEY_STATE = "state"
@HiltViewModel
class EditItemViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
private val snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
savedStateHandle: SavedStateHandle,
) : BaseViewModel<EditItemState, EditItemEvent, EditItemAction>(
initialState = savedStateHandle[KEY_STATE] ?: EditItemState(
Expand Down Expand Up @@ -233,7 +237,10 @@ class EditItemViewModel @Inject constructor(
}

CreateItemResult.Success -> {
sendEvent(EditItemEvent.ShowToast(BitwardenString.item_saved.asText()))
snackbarRelayManager.sendSnackbarData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ Correct pattern: Using sendSnackbarData before navigation

The pattern of sending snackbar data via relay manager before emitting NavigateBack event (lines 240-244) is correct. This ensures the destination screen can receive and display the snackbar upon arrival.

data = BitwardenSnackbarData(message = BitwardenString.item_saved.asText()),
relay = SnackbarRelay.ITEM_SAVED,
)
sendEvent(EditItemEvent.NavigateBack)
}
}
Expand Down Expand Up @@ -446,11 +453,6 @@ sealed class EditItemEvent {
* Navigates back.
*/
data object NavigateBack : EditItemEvent()

/**
* Show a toast with the given [message].
*/
data class ShowToast(val message: Text) : EditItemEvent()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting

import android.Manifest
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -30,8 +29,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
Expand Down Expand Up @@ -96,8 +93,6 @@ fun ItemListingScreen(
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val context = LocalContext.current
val resources = LocalResources.current
val launcher = permissionsManager.getLauncher { isGranted ->
if (isGranted) {
viewModel.trySendAction(ItemListingAction.ScanQrCodeClick)
Expand All @@ -112,8 +107,8 @@ fun ItemListingScreen(
is ItemListingEvent.NavigateToSearch -> onNavigateToSearch()
is ItemListingEvent.NavigateToQrCodeScanner -> onNavigateToQrCodeScanner()
is ItemListingEvent.NavigateToManualAddItem -> onNavigateToManualKeyEntry()
is ItemListingEvent.ShowToast -> {
Toast.makeText(context, event.message(resources), Toast.LENGTH_LONG).show()
is ItemListingEvent.ShowSnackbar -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ Good refactoring: Removed Toast imports and Android framework dependencies

The removal of android.widget.Toast, LocalContext, and related Android framework dependencies from the Screen composables aligns with best practices for keeping UI layer testable and framework-agnostic. Snackbars are now properly integrated with Compose's state management.

snackbarHostState.showSnackbar(snackbarData = event.data)
}

is ItemListingEvent.NavigateToEditItem -> onNavigateToEditItemScreen(event.id)
Expand All @@ -134,10 +129,6 @@ fun ItemListingScreen(
ItemListingEvent.NavigateToBitwardenSettings -> {
intentManager.startBitwardenAccountSettings()
}

is ItemListingEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(snackbarData = event.data)
}
}
}

Expand Down Expand Up @@ -429,7 +420,7 @@ private fun ItemListingContent(

when (state.sharedItems) {
is SharedCodesDisplayState.Codes -> {
state.sharedItems.sections.forEachIndexed { index, section ->
state.sharedItems.sections.forEachIndexed { _, section ->
item(key = "sharedSection_${section.id}") {
AuthenticatorExpandingHeader(
label = section.label(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import com.bitwarden.authenticator.ui.authenticator.feature.util.toSharedCodesDi
import com.bitwarden.authenticator.ui.platform.components.listitem.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.platform.components.listitem.model.VerificationCodeDisplayItem
import com.bitwarden.authenticator.ui.platform.model.SnackbarRelay
import com.bitwarden.authenticatorbridge.manager.AuthenticatorBridgeManager
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.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
Expand Down Expand Up @@ -56,6 +59,7 @@ class ItemListingViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager,
private val encodingManager: BitwardenEncodingManager,
private val settingsRepository: SettingsRepository,
snackbarRelayManager: SnackbarRelayManager<SnackbarRelay>,
) : BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
initialState = ItemListingState(
alertThresholdSeconds = settingsRepository.authenticatorAlertThresholdSeconds,
Expand Down Expand Up @@ -90,6 +94,12 @@ class ItemListingViewModel @Inject constructor(
.map { ItemListingAction.Internal.FirstTimeUserSyncReceive }
.onEach(::sendAction)
.launchIn(viewModelScope)

snackbarRelayManager
.getSnackbarDataFlow(SnackbarRelay.ITEM_SAVED, SnackbarRelay.ITEM_ADDED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ Exemplary implementation: Correct usage of SnackbarRelayManager pattern

This demonstrates proper usage of the relay pattern:

  1. Subscribes to multiple relay keys in a single flow
  2. Maps to event with BackgroundEvent marker
  3. Uses onEach(::sendEvent) for clean event emission
  4. Properly scoped with launchIn(viewModelScope)

This pattern is consistent with the Password Manager app's implementation.

.map(ItemListingEvent::ShowSnackbar)
.onEach(::sendEvent)
.launchIn(viewModelScope)
}

override fun handleAction(action: ItemListingAction) {
Expand Down Expand Up @@ -277,11 +287,7 @@ class ItemListingViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(dialog = null)
}
sendEvent(
ItemListingEvent.ShowToast(
message = BitwardenString.item_deleted.asText(),
),
)
sendEvent(ItemListingEvent.ShowSnackbar(BitwardenString.item_deleted.asText()))
}
}
}
Expand All @@ -305,7 +311,7 @@ class ItemListingViewModel @Inject constructor(

CreateItemResult.Success -> {
sendEvent(
event = ItemListingEvent.ShowToast(
event = ItemListingEvent.ShowSnackbar(
message = BitwardenString.verification_code_added.asText(),
),
)
Expand Down Expand Up @@ -847,19 +853,12 @@ sealed class ItemListingEvent {
*/
data object NavigateToBitwardenSettings : ItemListingEvent()

/**
* Show a Toast with [message].
*/
data class ShowToast(
val message: Text,
) : ItemListingEvent()

/**
* Show a Snackbar with the given [data].
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : ItemListingEvent() {
) : ItemListingEvent(), BackgroundEvent {
constructor(
message: Text,
messageHeader: Text? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry

import android.Manifest
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -23,7 +22,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand Down Expand Up @@ -75,20 +73,12 @@ fun ManualCodeEntryScreen(
}
}

val context = LocalContext.current

EventsEffect(viewModel = viewModel) { event ->
when (event) {
is ManualCodeEntryEvent.NavigateToAppSettings -> {
intentManager.startAppSettingsActivity()
}

is ManualCodeEntryEvent.ShowToast -> {
Toast
.makeText(context, event.message.invoke(context.resources), Toast.LENGTH_SHORT)
.show()
}

is ManualCodeEntryEvent.NavigateToQrCodeScreen -> {
onNavigateToQrCodeScreen.invoke()
}
Expand Down
Loading
Loading