diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index ff32edf9b6..18e2b487f8 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -32,6 +32,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.vault.feature.importitems.importItemsDestination import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -114,6 +115,7 @@ fun NavGraphBuilder.settingsGraph( onNavigateToFlightRecorder: () -> Unit, onNavigateToRecordedLogs: () -> Unit, onNavigateToImportLogins: () -> Unit, + onNavigateToImportItems: () -> Unit, onNavigateToAboutPrivilegedApps: () -> Unit, ) { navigation( @@ -162,6 +164,11 @@ fun NavGraphBuilder.settingsGraph( onNavigateToExportVault = onNavigateToExportVault, onNavigateToFolders = onNavigateToFolders, onNavigateToImportLogins = onNavigateToImportLogins, + onNavigateToImportItems = onNavigateToImportItems, + ) + importItemsDestination( + onNavigateBack = { navController.popBackStack() }, + onNavigateToImportLogins = onNavigateToImportLogins, ) blockAutoFillDestination(onNavigateBack = { navController.popBackStack() }) privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() }) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt index bda60be1d3..ab941e6c9d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsNavigation.kt @@ -20,6 +20,7 @@ fun NavGraphBuilder.vaultSettingsDestination( onNavigateToExportVault: () -> Unit, onNavigateToFolders: () -> Unit, onNavigateToImportLogins: () -> Unit, + onNavigateToImportItems: () -> Unit, ) { composableWithPushTransitions { VaultSettingsScreen( @@ -27,6 +28,7 @@ fun NavGraphBuilder.vaultSettingsDestination( onNavigateToExportVault = onNavigateToExportVault, onNavigateToFolders = onNavigateToFolders, onNavigateToImportLogins = onNavigateToImportLogins, + onNavigateToImportItems = onNavigateToImportItems, ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt index dfff3ee838..f90769f712 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreen.kt @@ -49,6 +49,7 @@ fun VaultSettingsScreen( onNavigateToExportVault: () -> Unit, onNavigateToFolders: () -> Unit, onNavigateToImportLogins: () -> Unit, + onNavigateToImportItems: () -> Unit, viewModel: VaultSettingsViewModel = hiltViewModel(), ) { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -60,6 +61,7 @@ fun VaultSettingsScreen( VaultSettingsEvent.NavigateToExportVault -> onNavigateToExportVault() VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders() is VaultSettingsEvent.NavigateToImportVault -> onNavigateToImportLogins() + is VaultSettingsEvent.NavigateToImportItems -> onNavigateToImportItems() is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt index 5e92a9534e..d20c67123d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModel.kt @@ -1,9 +1,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault import androidx.lifecycle.viewModelScope +import com.bitwarden.core.data.manager.model.FlagKey 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.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager @@ -22,6 +24,7 @@ import javax.inject.Inject class VaultSettingsViewModel @Inject constructor( snackbarRelayManager: SnackbarRelayManager, private val firstTimeActionManager: FirstTimeActionManager, + private val featureFlagManager: FeatureFlagManager, ) : BaseViewModel( initialState = run { val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState @@ -106,7 +109,11 @@ class VaultSettingsViewModel @Inject constructor( } private fun handleImportItemsClicked() { - sendEvent(VaultSettingsEvent.NavigateToImportVault) + if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)) { + sendEvent(VaultSettingsEvent.NavigateToImportItems) + } else { + sendEvent(VaultSettingsEvent.NavigateToImportVault) + } } } @@ -131,6 +138,11 @@ sealed class VaultSettingsEvent { */ data object NavigateToImportVault : VaultSettingsEvent() + /** + * Navigate to the import vault URL. + */ + data object NavigateToImportItems : VaultSettingsEvent() + /** * Navigate to the Export Vault screen. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 3b4de80aec..eb73f2cdbf 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -1,4 +1,5 @@ @file:OmitFromCoverage + package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar import androidx.navigation.NavController diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 59546304d2..0d57001fb0 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -40,6 +40,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.navigateToSendGraph import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs +import com.x8bit.bitwarden.ui.vault.feature.importitems.navigateToImportItemsScreen import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVaultGraph @@ -265,6 +266,7 @@ private fun VaultUnlockedNavBarScaffold( onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen, onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen, onNavigateToImportLogins = onNavigateToImportLogins, + onNavigateToImportItems = { navController.navigateToImportItemsScreen() }, onNavigateToFlightRecorder = onNavigateToFlightRecorder, onNavigateToRecordedLogs = onNavigateToRecordedLogs, onNavigateToAboutPrivilegedApps = onNavigateToAboutPrivilegedApps, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt index 139c81405b..a3d3b2e020 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/manager/snackbar/SnackbarRelay.kt @@ -21,5 +21,4 @@ enum class SnackbarRelay { LOGINS_IMPORTED, SEND_DELETED, SEND_UPDATED, - VAULT_SYNC_FAILED, } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsNavigation.kt new file mode 100644 index 0000000000..e1aad1cd01 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsNavigation.kt @@ -0,0 +1,40 @@ +@file:OmitFromCoverage + +package com.x8bit.bitwarden.ui.vault.feature.importitems + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.ui.platform.base.util.composableWithPushTransitions +import kotlinx.serialization.Serializable + +/** + * The type-safe route for the import items screen. + */ +@Serializable +data object ImportItemsRoute + +/** + * Helper function to navigate to the import items screen. + */ +fun NavController.navigateToImportItemsScreen( + navOptions: NavOptions? = null, +) { + navigate(route = ImportItemsRoute, navOptions = navOptions) +} + +/** + * Add the import items screen to the nav graph. + */ +fun NavGraphBuilder.importItemsDestination( + onNavigateBack: () -> Unit, + onNavigateToImportLogins: () -> Unit, +) { + composableWithPushTransitions { + ImportItemsScreen( + onNavigateBack = onNavigateBack, + onNavigateToImportFromComputer = onNavigateToImportLogins, + ) + } +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt index 5736de2a9b..aec2717803 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt @@ -30,6 +30,9 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.row.BitwardenTextRow import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold +import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost +import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarHostState +import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenString @@ -46,7 +49,6 @@ import kotlinx.coroutines.launch @Composable fun ImportItemsScreen( onNavigateBack: () -> Unit, - onNavigateToVault: () -> Unit, onNavigateToImportFromComputer: () -> Unit, viewModel: ImportItemsViewModel = hiltViewModel(), credentialExchangeImporter: CredentialExchangeImporter = @@ -55,11 +57,11 @@ fun ImportItemsScreen( val state by viewModel.stateFlow.collectAsStateWithLifecycle() val coroutineScope = rememberCoroutineScope() val handler = rememberImportItemsHandler(viewModel = viewModel) + val snackbarHostState = rememberBitwardenSnackbarHostState() EventsEffect(viewModel) { event -> when (event) { ImportItemsEvent.NavigateBack -> onNavigateBack() - ImportItemsEvent.NavigateToVault -> onNavigateToVault() ImportItemsEvent.NavigateToImportFromComputer -> onNavigateToImportFromComputer() is ImportItemsEvent.ShowRegisteredImportSources -> { coroutineScope.launch { @@ -73,6 +75,17 @@ fun ImportItemsScreen( ) } } + + is ImportItemsEvent.ShowBasicSnackbar -> { + snackbarHostState.showSnackbar(event.data) + } + + is ImportItemsEvent.ShowSyncFailedSnackbar -> { + snackbarHostState.showSnackbar( + snackbarData = event.data, + onActionPerformed = handler.onSyncFailedTryAgainClick, + ) + } } } @@ -85,6 +98,7 @@ fun ImportItemsScreen( onNavigateBack = handler.onNavigateBack, onImportFromComputerClick = handler.onImportFromComputerClick, onImportFromAnotherAppClick = handler.onImportFromAnotherAppClick, + snackbarHostState = snackbarHostState, ) } @@ -95,6 +109,7 @@ private fun ImportItemsScaffold( onImportFromComputerClick: () -> Unit, onImportFromAnotherAppClick: () -> Unit, modifier: Modifier = Modifier, + snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(), ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) BitwardenScaffold( @@ -112,6 +127,11 @@ private fun ImportItemsScaffold( scrollBehavior = scrollBehavior, ) }, + snackbarHost = { + BitwardenSnackbarHost( + bitwardenHostState = snackbarHostState, + ) + }, ) { ImportItemsContent( onImportFromComputerClick = onImportFromComputerClick, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModel.kt index 031b1a2f64..a85422ee0a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModel.kt @@ -13,10 +13,9 @@ import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult -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 @@ -33,7 +32,6 @@ private const val KEY_STATE = "state" class ImportItemsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val vaultRepository: VaultRepository, - private val snackbarRelayManager: SnackbarRelayManager, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: ImportItemsState(), ) { @@ -52,14 +50,6 @@ class ImportItemsViewModel @Inject constructor( handleImportCredentialSelectionReceive(action) } - ImportItemsAction.ReturnToVaultClick -> { - handleReturnToVaultClick() - } - - is ImportItemsAction.Internal.ImportCredentialsResultReceive -> { - handleImportCredentialsResultReceive(action) - } - ImportItemsAction.ImportFromComputerClick -> { handleImportFromComputerClick() } @@ -67,11 +57,59 @@ class ImportItemsViewModel @Inject constructor( ImportItemsAction.DismissDialog -> { handleDismissDialog() } + + ImportItemsAction.SyncFailedTryAgainClick -> { + handleSyncFailedTryAgainClick() + } + + is ImportItemsAction.Internal -> { + handleInternalAction(action) + } } } - private fun handleReturnToVaultClick() { - sendEvent(ImportItemsEvent.NavigateToVault) + private fun handleInternalAction(action: ImportItemsAction.Internal) { + when (action) { + is ImportItemsAction.Internal.ImportCredentialsResultReceive -> { + handleImportCredentialsResultReceive(action) + } + + is ImportItemsAction.Internal.RetrySyncResultReceive -> { + handleRetrySyncResultReceive(action) + } + } + } + + private fun handleRetrySyncResultReceive( + action: ImportItemsAction.Internal.RetrySyncResultReceive, + ) { + clearDialogs() + when (action.result) { + is SyncVaultDataResult.Success -> { + sendEvent( + ImportItemsEvent.ShowBasicSnackbar( + data = BitwardenSnackbarData( + message = BitwardenString.syncing_complete.asText(), + ), + ), + ) + } + + is SyncVaultDataResult.Error -> { + showSyncFailedSnackbar() + } + } + } + + private fun handleSyncFailedTryAgainClick() { + showLoadingDialog(message = BitwardenString.syncing.asText()) + viewModelScope.launch { + sendAction( + ImportItemsAction.Internal.RetrySyncResultReceive( + result = vaultRepository.syncForResult(), + ), + ) + } } private fun handleBackClick() { @@ -118,7 +156,7 @@ class ImportItemsViewModel @Inject constructor( } is ImportCredentialsSelectionResult.Success -> { - updateImportProgress(BitwardenString.saving_items.asText()) + showLoadingDialog(BitwardenString.saving_items.asText()) viewModelScope.launch { sendAction( ImportItemsAction.Internal.ImportCredentialsResultReceive( @@ -150,19 +188,19 @@ class ImportItemsViewModel @Inject constructor( is ImportCredentialsResult.Success -> { clearDialogs() - snackbarRelayManager.sendSnackbarData( - data = BitwardenSnackbarData( - messageHeader = BitwardenString.import_successful.asText(), - message = BitwardenPlurals - .x_items_have_been_imported_to_your_vault - .asPluralsText( - quantity = action.result.itemCount, - args = arrayOf(action.result.itemCount), - ), + sendEvent( + ImportItemsEvent.ShowBasicSnackbar( + data = BitwardenSnackbarData( + messageHeader = BitwardenString.import_successful.asText(), + message = BitwardenPlurals + .x_items_have_been_imported_to_your_vault + .asPluralsText( + quantity = action.result.itemCount, + args = arrayOf(action.result.itemCount), + ), + ), ), - relay = SnackbarRelay.LOGINS_IMPORTED, ) - sendEvent(ImportItemsEvent.NavigateToVault) } ImportCredentialsResult.NoItems -> { @@ -175,20 +213,25 @@ class ImportItemsViewModel @Inject constructor( is ImportCredentialsResult.SyncFailed -> { clearDialogs() - snackbarRelayManager.sendSnackbarData( - data = BitwardenSnackbarData( - messageHeader = BitwardenString.vault_sync_failed.asText(), - message = BitwardenString - .your_items_have_been_successfully_imported_but_could_not_sync_vault - .asText(), - actionLabel = BitwardenString.try_again.asText(), - ), - relay = SnackbarRelay.VAULT_SYNC_FAILED, - ) + showSyncFailedSnackbar() } } } + private fun showSyncFailedSnackbar() { + sendEvent( + ImportItemsEvent.ShowSyncFailedSnackbar( + data = BitwardenSnackbarData( + messageHeader = BitwardenString.vault_sync_failed.asText(), + message = BitwardenString + .your_items_have_been_successfully_imported_but_could_not_sync_vault + .asText(), + actionLabel = BitwardenString.try_again.asText(), + ), + ), + ) + } + private fun clearDialogs() { mutableStateFlow.update { it.copy(dialog = null) } } @@ -209,7 +252,7 @@ class ImportItemsViewModel @Inject constructor( } } - private fun updateImportProgress(message: Text) { + private fun showLoadingDialog(message: Text) { mutableStateFlow.update { it.copy( dialog = ImportItemsState.DialogState.Loading(message = message), @@ -272,11 +315,6 @@ sealed class ImportItemsAction { val selectionResult: ImportCredentialsSelectionResult, ) : ImportItemsAction() - /** - * User clicked the Return to vault button. - */ - data object ReturnToVaultClick : ImportItemsAction() - /** * User clicked the back button. */ @@ -287,6 +325,11 @@ sealed class ImportItemsAction { */ data object DismissDialog : ImportItemsAction() + /** + * User clicked the try again button. + */ + data object SyncFailedTryAgainClick : ImportItemsAction() + /** * Internal actions that the [ImportItemsViewModel] may itself send. */ @@ -294,7 +337,16 @@ sealed class ImportItemsAction { /** * Import CXF result received. */ - data class ImportCredentialsResultReceive(val result: ImportCredentialsResult) : Internal() + data class ImportCredentialsResultReceive( + val result: ImportCredentialsResult, + ) : Internal() + + /** + * Retry sync result received. + */ + data class RetrySyncResultReceive( + val result: SyncVaultDataResult, + ) : Internal() } } @@ -313,11 +365,6 @@ sealed class ImportItemsEvent { */ data object NavigateToImportFromComputer : ImportItemsEvent() - /** - * Navigate to the vault. - */ - data object NavigateToVault : ImportItemsEvent() - /** * Show registered import sources. * @@ -326,4 +373,18 @@ sealed class ImportItemsEvent { data class ShowRegisteredImportSources( val credentialTypes: List, ) : ImportItemsEvent(), BackgroundEvent + + /** + * Show a basic snackbar. + */ + data class ShowBasicSnackbar( + val data: BitwardenSnackbarData, + ) : ImportItemsEvent(), BackgroundEvent + + /** + * Show a snackbar indicating that the sync failed, with an option to retry. + */ + data class ShowSyncFailedSnackbar( + val data: BitwardenSnackbarData, + ) : ImportItemsEvent(), BackgroundEvent } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/handlers/ImportItemsHandler.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/handlers/ImportItemsHandler.kt index 82f6331cca..0f2251ddeb 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/handlers/ImportItemsHandler.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/handlers/ImportItemsHandler.kt @@ -18,6 +18,7 @@ data class ImportItemsHandler( val onDismissDialog: () -> Unit, val onImportFromAnotherAppClick: () -> Unit, val onImportFromComputerClick: () -> Unit, + val onSyncFailedTryAgainClick: () -> Unit, ) { @Suppress("UndocumentedPublicClass") @@ -30,14 +31,17 @@ data class ImportItemsHandler( onNavigateBack = { viewModel.trySendAction(ImportItemsAction.BackClick) }, + onDismissDialog = { + viewModel.trySendAction(ImportItemsAction.DismissDialog) + }, onImportFromAnotherAppClick = { viewModel.trySendAction(ImportItemsAction.ImportFromAnotherAppClick) }, onImportFromComputerClick = { viewModel.trySendAction(ImportItemsAction.ImportFromComputerClick) }, - onDismissDialog = { - viewModel.trySendAction(ImportItemsAction.DismissDialog) + onSyncFailedTryAgainClick = { + viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick) }, ) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index ae78c0c54d..e9da859444 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -190,7 +190,6 @@ class VaultViewModel @Inject constructor( SnackbarRelay.CIPHER_RESTORED, SnackbarRelay.CIPHER_UPDATED, SnackbarRelay.LOGINS_IMPORTED, - SnackbarRelay.VAULT_SYNC_FAILED, ), ) .map { VaultAction.Internal.SnackbarDataReceive(it) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt index 952e80b3a5..1b006905bb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsScreenTest.kt @@ -24,6 +24,7 @@ import org.junit.Test class VaultSettingsScreenTest : BitwardenComposeTest() { private var onNavigateToImportLoginsCalled = false + private var onNavigateToImportItemsCalled = false private var onNavigateBackCalled = false private var onNavigateToExportVaultCalled = false private var onNavigateToFoldersCalled = false @@ -48,6 +49,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() { onNavigateToExportVault = { onNavigateToExportVaultCalled = true }, onNavigateToFolders = { onNavigateToFoldersCalled = true }, onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true }, + onNavigateToImportItems = { onNavigateToImportItemsCalled = true }, ) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt index 9c16fdda18..d9fbfd09dd 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/vault/VaultSettingsViewModelTest.kt @@ -1,10 +1,12 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.vault import app.cash.turbine.test +import com.bitwarden.core.data.manager.model.FlagKey import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay @@ -29,6 +31,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() { every { firstTimeStateFlow } returns mutableFirstTimeStateFlow every { storeShowImportLoginsSettingsBadge(any()) } just runs } + private val featureFlagManager = mockk() private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow() private val snackbarRelayManager = mockk { @@ -58,17 +61,39 @@ class VaultSettingsViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `ImportItemsClick should emit send NavigateToImportVault`() = runTest { - val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) - assertEquals( - VaultSettingsEvent.NavigateToImportVault, - awaitItem(), - ) + fun `ImportItemsClick should emit NavigateToImportVault when CredentialExchangeProtocolImport is disabled`() = + runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + every { + featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport) + } returns false + viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) + assertEquals( + VaultSettingsEvent.NavigateToImportVault, + awaitItem(), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() = + runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + every { + featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport) + } returns true + viewModel.trySendAction(VaultSettingsAction.ImportItemsClick) + assertEquals( + VaultSettingsEvent.NavigateToImportItems, + awaitItem(), + ) + } } - } @Test fun `shouldShowImportCard should update when first time state changes`() = runTest { @@ -131,6 +156,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() { private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel( firstTimeActionManager = firstTimeActionManager, snackbarRelayManager = snackbarRelayManager, + featureFlagManager = featureFlagManager, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt index eb8249ed13..4440109b00 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt @@ -31,7 +31,6 @@ import org.junit.Test class ImportItemsScreenTest : BitwardenComposeTest() { private var onNavigateBackCalled = false - private var onNavigateToVaultCalled = false private var onNavigateToImportFromComputerCalled = false private val credentialExchangeImporter = mockk() @@ -51,7 +50,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { ) { ImportItemsScreen( onNavigateBack = { onNavigateBackCalled = true }, - onNavigateToVault = { onNavigateToVaultCalled = true }, onNavigateToImportFromComputer = { onNavigateToImportFromComputerCalled = true }, viewModel = viewModel, ) @@ -117,12 +115,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { } } - @Test - fun `NavigateToVault should call onNavigateToVault`() { - mockEventFlow.tryEmit(ImportItemsEvent.NavigateToVault) - assertTrue(onNavigateToVaultCalled) - } - @Test fun `ShowRegisteredImportSources should call CredentialExchangeImporter`() = runTest { val importCredentialsSelectionResult = ImportCredentialsSelectionResult.Success( @@ -205,4 +197,4 @@ class ImportItemsScreenTest : BitwardenComposeTest() { } } -private val DEFAULT_STATE: ImportItemsState = ImportItemsState(dialog = null) +private val DEFAULT_STATE: ImportItemsState = ImportItemsState() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModelTest.kt index a652ea5e57..d147dbcd99 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsViewModelTest.kt @@ -11,17 +11,14 @@ import com.bitwarden.ui.platform.resource.BitwardenPlurals import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asPluralsText import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult -import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay -import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager import io.mockk.awaits 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 kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -29,9 +26,6 @@ import org.junit.jupiter.api.Test class ImportItemsViewModelTest : BaseViewModelTest() { private val vaultRepository = mockk() - private val snackbarRelayManager = mockk { - every { sendSnackbarData(any(), any()) } just runs - } @Test fun `BackClick sends NavigateBack event`() = runTest { @@ -55,7 +49,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() { fun `DismissDialog updates state`() = runTest { val viewModel = createViewModel() viewModel.trySendAction(ImportItemsAction.DismissDialog) - assertEquals(ImportItemsState(dialog = null), viewModel.stateFlow.value) + assertEquals(ImportItemsState(), viewModel.stateFlow.value) } @Test @@ -158,14 +152,16 @@ class ImportItemsViewModelTest : BaseViewModelTest() { } @Test - fun `ReturnToVaultClick sends NavigateToVault event`() = runTest { + fun `SyncFailedTryAgainClick should update state and trigger sync`() = runTest { val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(ImportItemsAction.ReturnToVaultClick) - assertEquals( - ImportItemsEvent.NavigateToVault, - awaitItem(), - ) + coEvery { + vaultRepository.syncForResult() + } returns SyncVaultDataResult.Success(itemsAvailable = true) + + viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick) + + coVerify(exactly = 1) { + vaultRepository.syncForResult() } } @@ -193,7 +189,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, show snackbar, and navigate to vault`() = + fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, and show snackbar`() = runTest { val viewModel = createViewModel() @@ -203,25 +199,21 @@ class ImportItemsViewModelTest : BaseViewModelTest() { ), ) - val expectedState = ImportItemsState(dialog = null) + val expectedState = ImportItemsState() assertEquals(expectedState, viewModel.stateFlow.value) - coVerify(exactly = 1) { - snackbarRelayManager.sendSnackbarData( - data = BitwardenSnackbarData( - messageHeader = BitwardenString.import_successful.asText(), - message = BitwardenPlurals - .x_items_have_been_imported_to_your_vault - .asPluralsText( - quantity = 2, - args = arrayOf(2), - ), - ), - relay = SnackbarRelay.LOGINS_IMPORTED, - ) - } viewModel.eventFlow.test { assertEquals( - ImportItemsEvent.NavigateToVault, + ImportItemsEvent.ShowBasicSnackbar( + BitwardenSnackbarData( + messageHeader = BitwardenString.import_successful.asText(), + message = BitwardenPlurals + .x_items_have_been_imported_to_your_vault + .asPluralsText( + quantity = 2, + args = arrayOf(2), + ), + ), + ), awaitItem(), ) } @@ -249,7 +241,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs, show snackbar, and navigate to vault`() = + fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs and show snackbar`() = runTest { val viewModel = createViewModel() @@ -261,18 +253,80 @@ class ImportItemsViewModelTest : BaseViewModelTest() { ), ) - val expectedState = ImportItemsState(dialog = null) + val expectedState = ImportItemsState() assertEquals(expectedState, viewModel.stateFlow.value) - coVerify(exactly = 1) { - snackbarRelayManager.sendSnackbarData( - data = BitwardenSnackbarData( - messageHeader = BitwardenString.vault_sync_failed.asText(), - message = BitwardenString - .your_items_have_been_successfully_imported_but_could_not_sync_vault - .asText(), - actionLabel = BitwardenString.try_again.asText(), + viewModel.eventFlow.test { + assertEquals( + ImportItemsEvent.ShowSyncFailedSnackbar( + data = BitwardenSnackbarData( + messageHeader = BitwardenString.vault_sync_failed.asText(), + message = BitwardenString + .your_items_have_been_successfully_imported_but_could_not_sync_vault + .asText(), + actionLabel = BitwardenString.try_again.asText(), + ), ), - relay = SnackbarRelay.VAULT_SYNC_FAILED, + awaitItem(), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `Internal RetrySyncResultReceive with Success should clear dialogs and display success snackbar`() = + runTest { + val viewModel = createViewModel() + + viewModel.trySendAction( + ImportItemsAction.Internal.RetrySyncResultReceive( + result = SyncVaultDataResult.Success(itemsAvailable = true), + ), + ) + + assertEquals( + ImportItemsState(), + viewModel.stateFlow.value, + ) + viewModel.eventFlow.test { + assertEquals( + ImportItemsEvent.ShowBasicSnackbar( + data = BitwardenSnackbarData( + message = BitwardenString.syncing_complete.asText(), + ), + ), + awaitItem(), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `Internal RetrySyncResultReceive with Error should clear dialogs and show SyncFailed snackbar`() = + runTest { + val viewModel = createViewModel() + + viewModel.trySendAction( + ImportItemsAction.Internal.RetrySyncResultReceive( + result = SyncVaultDataResult.Error(throwable = RuntimeException("Error")), + ), + ) + + assertEquals( + ImportItemsState(), + viewModel.stateFlow.value, + ) + viewModel.eventFlow.test { + assertEquals( + ImportItemsEvent.ShowSyncFailedSnackbar( + data = BitwardenSnackbarData( + messageHeader = BitwardenString.vault_sync_failed.asText(), + message = BitwardenString + .your_items_have_been_successfully_imported_but_could_not_sync_vault + .asText(), + actionLabel = BitwardenString.try_again.asText(), + ), + ), + awaitItem(), ) } } @@ -280,6 +334,5 @@ class ImportItemsViewModelTest : BaseViewModelTest() { private fun createViewModel(): ImportItemsViewModel = ImportItemsViewModel( vaultRepository = vaultRepository, savedStateHandle = SavedStateHandle(), - snackbarRelayManager = snackbarRelayManager, ) }