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 1018c04576..f6b1a61557 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,24 @@ 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 (val result = action.result) { + is RequestOtpResult.Error -> { + result.message?.asText() ?: R.string.generic_error_message.asText() + } + RequestOtpResult.Success -> R.string.code_sent.asText() + } + sendEvent(ExportVaultEvent.ShowToast(message = toastMessage)) + } + /** * Dismiss the view. */ @@ -267,8 +283,19 @@ class ExportVaultViewModel @Inject constructor( } private fun handleSendCodeClick() { + mutableStateFlow.update { + it.copy( + dialogState = ExportVaultState.DialogState.Loading( + message = R.string.sending.asText(), + ), + ) + } viewModelScope.launch { - authRepository.requestOneTimePasscode() + sendAction( + ExportVaultAction.Internal.OtpCodeResult( + result = authRepository.requestOneTimePasscode(), + ), + ) } } @@ -572,5 +599,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 c8058517ed..d21f5ed37a 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,18 +480,63 @@ class ExportVaultViewModelTest : BaseViewModelTest() { ) } + @Suppress("MaxLineLength") @Test - fun `SendCodeClick should call requestOneTimePasscode`() { - val viewModel = createViewModel() - coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success - viewModel.trySendAction(ExportVaultAction.SendCodeClick) + fun `SendCodeClick should call requestOneTimePasscode and update dialog state to sending then back to null when request completes and send correct event on success`() = + runTest { + val viewModel = createViewModel() + coEvery { authRepository.requestOneTimePasscode() } returns RequestOtpResult.Success + viewModel.stateEventFlow(backgroundScope) { stateTurbine, eventTurbine -> + assertEquals(DEFAULT_STATE, stateTurbine.awaitItem()) + viewModel.trySendAction(ExportVaultAction.SendCodeClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = ExportVaultState.DialogState.Loading( + message = R.string.sending.asText(), + ), + ), + stateTurbine.awaitItem(), + ) + assertEquals(DEFAULT_STATE, stateTurbine.awaitItem()) + assertEquals( + ExportVaultEvent.ShowToast( + message = R.string.code_sent.asText(), + ), + eventTurbine.awaitItem(), + ) + } + coVerify { authRepository.requestOneTimePasscode() } + } - assertEquals( - DEFAULT_STATE, - viewModel.stateFlow.value, - ) - coVerify { authRepository.requestOneTimePasscode() } - } + @Suppress("MaxLineLength") + @Test + fun `SendCodeClick should call requestOneTimePasscode and update dialog state to sending then back to null when request completes and send correct event on error`() = + runTest { + val viewModel = createViewModel() + coEvery { + authRepository.requestOneTimePasscode() + } returns RequestOtpResult.Error(message = null) + viewModel.stateEventFlow(backgroundScope) { stateTurbine, eventTurbine -> + assertEquals(DEFAULT_STATE, stateTurbine.awaitItem()) + viewModel.trySendAction(ExportVaultAction.SendCodeClick) + assertEquals( + DEFAULT_STATE.copy( + dialogState = ExportVaultState.DialogState.Loading( + message = R.string.sending.asText(), + ), + ), + stateTurbine.awaitItem(), + ) + assertEquals(DEFAULT_STATE, stateTurbine.awaitItem()) + assertEquals( + ExportVaultEvent.ShowToast( + message = R.string.generic_error_message.asText(), + ), + eventTurbine.awaitItem(), + ) + } + coVerify { authRepository.requestOneTimePasscode() } + } @Test fun `ReceiveExportVaultDataToStringResult should update state to error if result is error`() {