mirror of
https://github.com/bitwarden/android.git
synced 2026-03-15 23:58:32 -05:00
[PM-22399] Send 2FA email when view appears (#5498)
Co-authored-by: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com>
This commit is contained in:
@@ -122,6 +122,13 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
.map { TwoFactorLoginAction.Internal.ReceiveWebAuthResult(webAuthResult = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
// If the auth method is email and it is not to verify the device, call resendEmail.
|
||||
if (state.authMethod == TwoFactorAuthMethod.EMAIL && !state.isNewDeviceVerification) {
|
||||
sendAction(TwoFactorLoginAction.Internal.SendVerificationCodeEmail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleAction(action: TwoFactorLoginAction) {
|
||||
@@ -159,6 +166,10 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
is TwoFactorLoginAction.Internal.ReceiveResendEmailResult -> {
|
||||
handleReceiveResendEmailResult(action)
|
||||
}
|
||||
|
||||
TwoFactorLoginAction.Internal.SendVerificationCodeEmail -> {
|
||||
handleSendVerificationCodeEmail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,6 +487,20 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
* Resend the verification code email.
|
||||
*/
|
||||
private fun handleResendEmailClick() {
|
||||
sendVerificationCodeEmail(isUserInitiated = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* send the verification code email without user interaction.
|
||||
*/
|
||||
private fun handleSendVerificationCodeEmail() {
|
||||
sendVerificationCodeEmail(isUserInitiated = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the verification code email.
|
||||
*/
|
||||
private fun sendVerificationCodeEmail(isUserInitiated: Boolean) {
|
||||
// Ensure that the user is in fact verifying with email.
|
||||
if (state.authMethod != TwoFactorAuthMethod.EMAIL) {
|
||||
return
|
||||
@@ -500,7 +525,7 @@ class TwoFactorLoginViewModel @Inject constructor(
|
||||
sendAction(
|
||||
TwoFactorLoginAction.Internal.ReceiveResendEmailResult(
|
||||
resendEmailResult = result,
|
||||
isUserInitiated = true,
|
||||
isUserInitiated = isUserInitiated,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -821,5 +846,10 @@ sealed class TwoFactorLoginAction {
|
||||
data class ReceiveWebAuthResult(
|
||||
val webAuthResult: WebAuthResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Indicates that the verification code email should be sent.
|
||||
*/
|
||||
data object SendVerificationCodeEmail : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,64 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `init with email auth method and not new device verification should call resendEmail`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.EMAIL,
|
||||
isNewDeviceVerification = false,
|
||||
)
|
||||
coEvery { authRepository.resendVerificationCodeEmail() } returns ResendEmailResult.Success
|
||||
|
||||
createViewModel(state = initialState)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
authRepository.resendVerificationCodeEmail()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `init with email auth method and new device verification should not call resendEmail`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.EMAIL,
|
||||
isNewDeviceVerification = true,
|
||||
)
|
||||
coEvery { authRepository.resendVerificationCodeEmail() } returns ResendEmailResult.Success
|
||||
|
||||
createViewModel(state = initialState)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
authRepository.resendVerificationCodeEmail()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init with non-email auth method and not new device verification should not call resendEmail`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
|
||||
isNewDeviceVerification = false,
|
||||
)
|
||||
coEvery { authRepository.resendVerificationCodeEmail() } returns ResendEmailResult.Success
|
||||
|
||||
createViewModel(state = initialState)
|
||||
|
||||
coVerify(exactly = 0) {
|
||||
authRepository.resendVerificationCodeEmail()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `init with non-email auth method and new device verification should not call resendEmail`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
authMethod = TwoFactorAuthMethod.AUTHENTICATOR_APP,
|
||||
isNewDeviceVerification = true,
|
||||
)
|
||||
createViewModel(state = initialState)
|
||||
|
||||
coVerify(exactly = 0) { authRepository.resendVerificationCodeEmail() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `yubiKeyResultFlow update should populate the input field and attempt login`() {
|
||||
val initialState = DEFAULT_STATE.copy(authMethod = TwoFactorAuthMethod.YUBI_KEY)
|
||||
@@ -899,6 +957,27 @@ class TwoFactorLoginViewModelTest : BaseViewModelTest() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("MaxLineLength")
|
||||
fun `sendVerificationCodeEmail with isUserInitiated false should not show loading and snackbar on success`() =
|
||||
runTest {
|
||||
coEvery { authRepository.resendVerificationCodeEmail() } returns ResendEmailResult.Success
|
||||
val viewModel = createViewModel()
|
||||
// Simulate initial email send (not user initiated)
|
||||
viewModel.trySendAction(
|
||||
TwoFactorLoginAction.Internal.ReceiveResendEmailResult(
|
||||
ResendEmailResult.Success,
|
||||
isUserInitiated = false,
|
||||
),
|
||||
)
|
||||
viewModel.stateEventFlow(backgroundScope) { stateFlow, eventFlow ->
|
||||
// No loading dialog
|
||||
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
|
||||
// No snackbar
|
||||
eventFlow.expectNoEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ResendEmailClick returns success should emit ShowSnackbar`() = runTest {
|
||||
coEvery {
|
||||
|
||||
Reference in New Issue
Block a user