diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt index 9e7cde78d7..032330bb3a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModel.kt @@ -298,11 +298,13 @@ class TwoFactorLoginViewModel @Inject constructor( // Display a toast for a successful result. ResendEmailResult.Success -> { - sendEvent( - TwoFactorLoginEvent.ShowToast( - message = R.string.verification_email_sent.asText(), - ), - ) + if (action.isUserInitiated) { + sendEvent( + TwoFactorLoginEvent.ShowToast( + message = R.string.verification_email_sent.asText(), + ), + ) + } } } } @@ -342,6 +344,7 @@ class TwoFactorLoginViewModel @Inject constructor( sendAction( TwoFactorLoginAction.Internal.ReceiveResendEmailResult( resendEmailResult = result, + isUserInitiated = true, ), ) } @@ -351,13 +354,35 @@ class TwoFactorLoginViewModel @Inject constructor( * Update the state with the auth method or opens the url for the recovery code. */ private fun handleSelectAuthMethod(action: TwoFactorLoginAction.SelectAuthMethod) { - if (action.authMethod == TwoFactorAuthMethod.RECOVERY_CODE) { - sendEvent(TwoFactorLoginEvent.NavigateToRecoveryCode) - } else { - mutableStateFlow.update { - it.copy( - authMethod = action.authMethod, - ) + when (action.authMethod) { + TwoFactorAuthMethod.RECOVERY_CODE -> { + sendEvent(TwoFactorLoginEvent.NavigateToRecoveryCode) + } + + TwoFactorAuthMethod.EMAIL -> { + if (state.authMethod != TwoFactorAuthMethod.EMAIL) { + viewModelScope.launch { + val result = authRepository.resendVerificationCodeEmail() + sendAction( + TwoFactorLoginAction.Internal.ReceiveResendEmailResult( + resendEmailResult = result, + isUserInitiated = false, + ), + ) + } + } + mutableStateFlow.update { it.copy(authMethod = action.authMethod) } + } + + TwoFactorAuthMethod.AUTHENTICATOR_APP, + TwoFactorAuthMethod.DUO, + TwoFactorAuthMethod.YUBI_KEY, + TwoFactorAuthMethod.U2F, + TwoFactorAuthMethod.REMEMBER, + TwoFactorAuthMethod.DUO_ORGANIZATION, + TwoFactorAuthMethod.FIDO_2_WEB_APP, + -> { + mutableStateFlow.update { it.copy(authMethod = action.authMethod) } } } } @@ -592,6 +617,7 @@ sealed class TwoFactorLoginAction { */ data class ReceiveResendEmailResult( val resendEmailResult: ResendEmailResult, + val isUserInitiated: Boolean, ) : Internal() } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt index e24a76e78e..f8750acab7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/twofactorlogin/TwoFactorLoginViewModelTest.kt @@ -467,7 +467,11 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { } returns ResendEmailResult.Error(message = null) val viewModel = createViewModel() - viewModel.eventFlow.test { + viewModel.stateFlow.test { + assertEquals( + DEFAULT_STATE, + awaitItem(), + ) viewModel.actionChannel.trySend( TwoFactorLoginAction.SelectAuthMethod( TwoFactorAuthMethod.EMAIL, @@ -475,11 +479,8 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { ) assertEquals( DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.EMAIL), - viewModel.stateFlow.value, + awaitItem(), ) - - viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) - assertEquals( DEFAULT_STATE.copy( authMethod = TwoFactorAuthMethod.EMAIL, @@ -488,7 +489,29 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { message = R.string.verification_email_not_sent.asText(), ), ), - viewModel.stateFlow.value, + awaitItem(), + ) + + viewModel.actionChannel.trySend(TwoFactorLoginAction.ResendEmailClick) + + assertEquals( + DEFAULT_STATE.copy( + authMethod = TwoFactorAuthMethod.EMAIL, + dialogState = TwoFactorLoginState.DialogState.Loading( + message = R.string.submitting.asText(), + ), + ), + awaitItem(), + ) + assertEquals( + DEFAULT_STATE.copy( + authMethod = TwoFactorAuthMethod.EMAIL, + dialogState = TwoFactorLoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.verification_email_not_sent.asText(), + ), + ), + awaitItem(), ) } } @@ -528,6 +551,67 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() { } } + @Test + @Suppress("MaxLineLength") + fun `ReceiveResendEmailResult with ResendEmailResult Success and isUserInitiated true should ShowToast`() = + runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + TwoFactorLoginAction.Internal.ReceiveResendEmailResult( + resendEmailResult = ResendEmailResult.Success, + isUserInitiated = true, + ), + ) + assertEquals( + TwoFactorLoginEvent.ShowToast( + message = R.string.verification_email_sent.asText(), + ), + awaitItem(), + ) + } + } + + @Test + @Suppress("MaxLineLength") + fun `ReceiveResendEmailResult with ResendEmailResult Success and isUserInitiated false should not emit any events`() = + runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.actionChannel.trySend( + TwoFactorLoginAction.Internal.ReceiveResendEmailResult( + resendEmailResult = ResendEmailResult.Success, + isUserInitiated = false, + ), + ) + expectNoEvents() + } + } + + @Test + fun `ReceiveResendEmailResult with ResendEmailResult Error should not emit any events`() = + runTest { + val viewModel = createViewModel() + viewModel.stateFlow.test { + assertEquals(DEFAULT_STATE, awaitItem()) + viewModel.actionChannel.trySend( + TwoFactorLoginAction.Internal.ReceiveResendEmailResult( + resendEmailResult = ResendEmailResult.Error(message = null), + isUserInitiated = true, + ), + ) + assertEquals( + DEFAULT_STATE.copy( + dialogState = TwoFactorLoginState.DialogState.Error( + title = R.string.an_error_has_occurred.asText(), + message = R.string.verification_email_not_sent.asText(), + ), + ), + awaitItem(), + ) + } + } + private fun createViewModel( state: TwoFactorLoginState? = null, ): TwoFactorLoginViewModel =