From c34ad889d80191aab8a0e589bf5aba87ba353f23 Mon Sep 17 00:00:00 2001 From: Dave Severns Date: Wed, 20 Nov 2024 16:57:58 -0500 Subject: [PATCH] PM-15036 Show loading spinner and toast as visual feedback for exporting vault --- .../exportvault/ExportVaultViewModel.kt | 32 ++++++++++- .../exportvault/ExportVaultViewModelTest.kt | 56 ++++++++++++++++--- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModel.kt index 1018c0457..af8692a2a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModel.kt @@ -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.repository.AuthRepository 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.VerifyOtpResult import com.x8bit.bitwarden.data.platform.manager.PolicyManager @@ -116,9 +117,22 @@ class ExportVaultViewModel @Inject constructor( is ExportVaultAction.Internal.ReceiveVerifyOneTimePasscodeResult -> { 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. */ @@ -267,8 +281,19 @@ class ExportVaultViewModel @Inject constructor( } private fun handleSendCodeClick() { + mutableStateFlow.update { + it.copy( + dialogState = ExportVaultState.DialogState.Loading( + R.string.sending.asText(), + ), + ) + } viewModelScope.launch { - authRepository.requestOneTimePasscode() + sendAction( + ExportVaultAction.Internal.OtpCodeResult( + result = authRepository.requestOneTimePasscode(), + ), + ) } } @@ -572,5 +597,10 @@ sealed class ExportVaultAction { data class ReceiveVerifyOneTimePasscodeResult( val result: VerifyOtpResult, ) : Internal() + + /** + * Indicates that a result for requesting the one-time passcode has been received. + */ + data class OtpCodeResult(val result: RequestOtpResult) : Internal() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt index c8058517e..f1616b60c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt @@ -38,6 +38,7 @@ import java.time.Clock import java.time.Instant import java.time.ZoneOffset +@Suppress("LargeClass") class ExportVaultViewModelTest : BaseViewModelTest() { private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE) private val authRepository: AuthRepository = mockk { @@ -479,19 +480,60 @@ class ExportVaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") @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() coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success - viewModel.trySendAction(ExportVaultAction.SendCodeClick) - - assertEquals( - DEFAULT_STATE, - viewModel.stateFlow.value, - ) + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.trySendAction(ExportVaultAction.SendCodeClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = ExportVaultState.DialogState.Loading( + message = R.string.sending.asText(), + ), + ), + awaitItem(), + ) + assertEquals(DEFAULT_STATE, awaitItem()) + } 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 fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() { val viewModel = createViewModel()