PM-15036 Show loading spinner and toast as visual feedback for exporting vault

This commit is contained in:
Dave Severns 2024-11-20 16:57:58 -05:00
parent 3092ba1fc6
commit c34ad889d8
2 changed files with 80 additions and 8 deletions

View file

@ -8,6 +8,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength import com.x8bit.bitwarden.data.auth.datasource.sdk.model.PasswordStrength
import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager import com.x8bit.bitwarden.data.platform.manager.PolicyManager
@ -116,9 +117,22 @@ class ExportVaultViewModel @Inject constructor(
is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> { is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> {
handleReceiveVerifyOneTimePasscodeResult(action) handleReceiveVerifyOneTimePasscodeResult(action)
} }
is ExportVaultAction.Internal.OtpCodeResult -> handleOtpCodeResult(action)
} }
} }
private fun handleOtpCodeResult(action: ExportVaultAction.Internal.OtpCodeResult) {
mutableStateFlow.update {
it.copy(dialogState = null)
}
val toastMessage = when (action.result) {
is RequestOtpResult.Error -> R.string.generic_error_message.asText()
RequestOtpResult.Success -> R.string.code_sent.asText()
}
sendEvent(ExportVaultEvent.ShowToast(message = toastMessage))
}
/** /**
* Dismiss the view. * Dismiss the view.
*/ */
@ -267,8 +281,19 @@ class ExportVaultViewModel @Inject constructor(
} }
private fun handleSendCodeClick() { private fun handleSendCodeClick() {
mutableStateFlow.update {
it.copy(
dialogState = ExportVaultState.DialogState.Loading(
R.string.sending.asText(),
),
)
}
viewModelScope.launch { viewModelScope.launch {
authRepository.requestOneTimePasscode() sendAction(
ExportVaultAction.Internal.OtpCodeResult(
result = authRepository.requestOneTimePasscode(),
),
)
} }
} }
@ -572,5 +597,10 @@ sealed class ExportVaultAction {
data class ReceiveVerifyOneTimePasscodeResult( data class ReceiveVerifyOneTimePasscodeResult(
val result: VerifyOtpResult, val result: VerifyOtpResult,
) : Internal() ) : Internal()
/**
* Indicates that a result for requesting the one-time passcode has been received.
*/
data class OtpCodeResult(val result: RequestOtpResult) : Internal()
} }
} }

View file

@ -38,6 +38,7 @@ import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset import java.time.ZoneOffset
@Suppress("LargeClass")
class ExportVaultViewModelTest : BaseViewModelTest() { class ExportVaultViewModelTest : BaseViewModelTest() {
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE) private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
private val authRepository: AuthRepository = mockk { private val authRepository: AuthRepository = mockk {
@ -479,19 +480,60 @@ class ExportVaultViewModelTest : BaseViewModelTest() {
) )
} }
@Suppress("MaxLineLength")
@Test @Test
fun `SendCodeClick should call requestOneTimePasscode`() { fun `SendCodeClick should call requestOneTimePasscode and update dialog state to sending then back to null when request completes`() =
runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success
viewModel.trySendAction(ExportVaultAction.SendCodeClick) viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
assertEquals( viewModel.trySendAction(ExportVaultAction.SendCodeClick)
DEFAULT_STATE, assertEquals(
viewModel.stateFlow.value, DEFAULT_STATE.copy(
) dialogState = ExportVaultState.DialogState.Loading(
message = R.string.sending.asText(),
),
),
awaitItem(),
)
assertEquals(DEFAULT_STATE, awaitItem())
}
coVerify { authRepository.requestOneTimePasscode() } coVerify { authRepository.requestOneTimePasscode() }
} }
@Test
fun `On successful requestOneTimePasscode should send success toast`() = runTest {
val viewModel = createViewModel()
coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success
viewModel.eventFlow.test {
viewModel.trySendAction(ExportVaultAction.SendCodeClick)
assertEquals(
ExportVaultEvent.ShowToast(
message = R.string.code_sent.asText(),
),
awaitItem(),
)
}
}
@Test
fun `On failed requestOneTimePasscode should send success toast`() = runTest {
val viewModel = createViewModel()
coEvery {
authRepository.requestOneTimePasscode()
} returns RequestOtpResult.Error(message = null)
viewModel.eventFlow.test {
viewModel.trySendAction(ExportVaultAction.SendCodeClick)
assertEquals(
ExportVaultEvent.ShowToast(
message = R.string.generic_error_message.asText(),
),
awaitItem(),
)
}
}
@Test @Test
fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() { fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() {
val viewModel = createViewModel() val viewModel = createViewModel()