From a061cbb1d31df6bd627a18503bec8180115484ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Thu, 20 Feb 2025 17:49:00 +0000 Subject: [PATCH] [PM-15873] Add delay to PTR to remove the spinning wheel (#4750) --- .../ui/vault/feature/vault/VaultViewModel.kt | 53 +++++++++++-- .../vault/feature/vault/VaultViewModelTest.kt | 77 ++++++++++++++++++- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index eda930835f..9cfadc9a30 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl @@ -55,6 +56,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType import com.x8bit.bitwarden.ui.vault.util.shortName import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn @@ -84,6 +86,7 @@ class VaultViewModel @Inject constructor( private val reviewPromptManager: ReviewPromptManager, private val featureFlagManager: FeatureFlagManager, private val specialCircumstanceManager: SpecialCircumstanceManager, + private val networkConnectionManager: NetworkConnectionManager, ) : BaseViewModel( initialState = run { val userState = requireNotNull(authRepository.userStateFlow.value) @@ -362,10 +365,21 @@ class VaultViewModel @Inject constructor( } private fun handleSyncClick() { - mutableStateFlow.update { - it.copy(dialog = VaultState.DialogState.Syncing) + if (networkConnectionManager.isNetworkConnected) { + mutableStateFlow.update { + it.copy(dialog = VaultState.DialogState.Syncing) + } + vaultRepository.sync(forced = true) + } else { + mutableStateFlow.update { + it.copy( + dialog = VaultState.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ) + } } - vaultRepository.sync(forced = true) } private fun handleLockClick() { @@ -426,11 +440,17 @@ class VaultViewModel @Inject constructor( } } + @Suppress("MagicNumber") private fun handleRefreshPull() { mutableStateFlow.update { it.copy(isRefreshing = true) } - // The Pull-To-Refresh composable is already in the refreshing state. - // We will reset that state when sendDataStateFlow emits later on. - vaultRepository.sync(forced = false) + viewModelScope.launch { + delay(250) + if (networkConnectionManager.isNetworkConnected) { + vaultRepository.sync(forced = false) + } else { + sendAction(VaultAction.Internal.InternetConnectionErrorReceived) + } + } } private fun handleOverflowOptionClick(action: VaultAction.OverflowOptionClick) { @@ -588,6 +608,22 @@ class VaultViewModel @Inject constructor( } is VaultAction.Internal.SnackbarDataReceive -> handleSnackbarDataReceive(action) + + VaultAction.Internal.InternetConnectionErrorReceived -> { + handleInternetConnectionErrorReceived() + } + } + } + + private fun handleInternetConnectionErrorReceived() { + mutableStateFlow.update { + it.copy( + isRefreshing = false, + dialog = VaultState.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ) } } @@ -1420,6 +1456,11 @@ sealed class VaultAction { val isIconLoadingDisabled: Boolean, ) : Internal() + /** + * Indicates that the there is not internet connection. + */ + data object InternetConnectionErrorReceived : Internal() + /** * Indicates a result for generating a verification code has been received. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 9f79c06f03..d17f520b46 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState import com.x8bit.bitwarden.data.platform.manager.model.FlagKey import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.Environment @@ -54,9 +55,11 @@ import io.mockk.just import io.mockk.mockk import io.mockk.runs import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.update +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse @@ -155,6 +158,10 @@ class VaultViewModelTest : BaseViewModelTest() { every { specialCircumstance } returns null } + private val networkConnectionManager: NetworkConnectionManager = mockk { + every { isNetworkConnected } returns true + } + @Test fun `initial state should be correct and should trigger a syncIfNecessary call`() { val viewModel = createViewModel() @@ -493,6 +500,27 @@ class VaultViewModelTest : BaseViewModelTest() { } } + @Test + fun `on SyncClick should show the no network dialog if not connection is available`() { + val viewModel = createViewModel() + every { + networkConnectionManager.isNetworkConnected + } returns false + viewModel.trySendAction(VaultAction.SyncClick) + assertEquals( + DEFAULT_STATE.copy( + dialog = VaultState.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + verify(exactly = 0) { + vaultRepository.sync(forced = true) + } + } + @Test fun `on LockClick should call lockVaultForCurrentUser on the VaultRepository`() { val viewModel = createViewModel() @@ -1343,13 +1371,38 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `RefreshPull should call vault repository sync`() { + fun `RefreshPull should call vault repository sync`() = runTest { val viewModel = createViewModel() + viewModel.trySendAction(VaultAction.RefreshPull) + advanceTimeBy(300) + verify(exactly = 1) { + vaultRepository.sync(forced = false) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `RefreshPull should show network error if no internet connection`() = runTest { + val viewModel = createViewModel() + every { + networkConnectionManager.isNetworkConnected + } returns false viewModel.trySendAction(VaultAction.RefreshPull) - - verify(exactly = 1) { + advanceTimeBy(300) + assertEquals( + DEFAULT_STATE.copy( + isRefreshing = false, + dialog = VaultState.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + verify(exactly = 0) { vaultRepository.sync(forced = false) } } @@ -1940,6 +1993,23 @@ class VaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") + @Test + fun `InternetConnectionErrorReceived should show network error if no internet connection`() = runTest { + val viewModel = createViewModel() + viewModel.trySendAction(VaultAction.Internal.InternetConnectionErrorReceived) + assertEquals( + DEFAULT_STATE.copy( + isRefreshing = false, + dialog = VaultState.DialogState.Error( + R.string.internet_connection_required_title.asText(), + R.string.internet_connection_required_message.asText(), + ), + ), + viewModel.stateFlow.value, + ) + } + private fun createViewModel(): VaultViewModel = VaultViewModel( authRepository = authRepository, @@ -1954,6 +2024,7 @@ class VaultViewModelTest : BaseViewModelTest() { snackbarRelayManager = snackbarRelayManager, reviewPromptManager = reviewPromptManager, specialCircumstanceManager = specialCircumstanceManager, + networkConnectionManager = networkConnectionManager, ) }