[PM-15873] Add delay to PTR to remove the spinning wheel (#4750)

This commit is contained in:
André Bispo
2025-02-20 17:49:00 +00:00
committed by GitHub
parent ad03f8c996
commit a061cbb1d3
2 changed files with 121 additions and 9 deletions

View File

@@ -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<VaultState, VaultEvent, VaultAction>(
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.
*/

View File

@@ -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,
)
}