diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt index b44710e4..5930db31 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt @@ -118,6 +118,9 @@ internal class AuthenticatorViewModel(application: Application, private val auth ) val events = _events.asSharedFlow() + // Is there a current Amplify call in progress that could result in a signed in event? + private var expectingSignInEvent: Boolean = false + fun start(configuration: AuthenticatorConfiguration) { if (::configuration.isInitialized) { return @@ -242,24 +245,30 @@ internal class AuthenticatorViewModel(application: Application, private val auth } private suspend fun handleAutoSignIn(username: String, password: String) { - when (val result = authProvider.autoSignIn()) { - is AmplifyResult.Error -> { - // If auto sign in fails then proceed with manually trying to sign in the user. If this also fails the - // user will end up back on the sign in screen. - logger.warn("Unable to complete auto-signIn") - handleSignedUp(username, password) + startSignInJob { + when (val result = authProvider.autoSignIn()) { + is AmplifyResult.Error -> { + // If auto sign in fails then proceed with manually trying to sign in the user. If this also fails the + // user will end up back on the sign in screen. + logger.warn("Unable to complete auto-signIn") + handleSignedUp(username, password) + } + + is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } - is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } } private suspend fun handleSignedUp(username: String, password: String) { - when (val result = authProvider.signIn(username, password)) { - is AmplifyResult.Error -> { - moveTo(AuthenticatorStep.SignIn) - handleSignInFailure(username, password, result.error) + startSignInJob { + when (val result = authProvider.signIn(username, password)) { + is AmplifyResult.Error -> { + moveTo(AuthenticatorStep.SignIn) + handleSignInFailure(username, password, result.error) + } + + is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } - is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } } @@ -268,31 +277,31 @@ internal class AuthenticatorViewModel(application: Application, private val auth @VisibleForTesting suspend fun signIn(username: String, password: String) { - viewModelScope.launch { + startSignInJob { when (val result = authProvider.signIn(username, password)) { is AmplifyResult.Error -> handleSignInFailure(username, password, result.error) is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } - }.join() + } } private suspend fun confirmSignIn(username: String, password: String, challengeResponse: String) { - viewModelScope.launch { + startSignInJob { when (val result = authProvider.confirmSignIn(challengeResponse)) { is AmplifyResult.Error -> handleSignInFailure(username, password, result.error) is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } - }.join() + } } private suspend fun setNewSignInPassword(username: String, password: String) { - viewModelScope.launch { + startSignInJob { when (val result = authProvider.confirmSignIn(password)) { // an error here is more similar to a sign up error is AmplifyResult.Error -> handleSignUpFailure(result.error) is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) } - }.join() + } } private suspend fun handleSignInFailure(username: String, password: String, error: AuthException) { @@ -520,9 +529,11 @@ internal class AuthenticatorViewModel(application: Application, private val auth logger.debug("Password reset complete") sendMessage(PasswordResetMessage) if (username != null && password != null) { - when (val result = authProvider.signIn(username, password)) { - is AmplifyResult.Error -> moveTo(stateFactory.newSignInState(this::signIn)) - is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) + startSignInJob { + when (val result = authProvider.signIn(username, password)) { + is AmplifyResult.Error -> moveTo(stateFactory.newSignInState(this::signIn)) + is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) + } } } else { moveTo(stateFactory.newSignInState(this::signIn)) @@ -636,9 +647,27 @@ internal class AuthenticatorViewModel(application: Application, private val auth } } + private suspend fun startSignInJob(body: suspend () -> Unit) { + expectingSignInEvent = true + viewModelScope.launch { body() }.join() + expectingSignInEvent = false + } + // Amplify has told us the user signed in. private suspend fun handleSignedInEvent() { - // TODO : move the user to signedInState *if* we are not in the process of signing in or verifying the user + if (!expectingSignInEvent && !inPostSignInState()) { + handleSignedIn() + } + } + + private fun inPostSignInState(): Boolean { + val step = currentState.step + return when (step) { + is AuthenticatorStep.VerifyUser, + is AuthenticatorStep.VerifyUserConfirm, + is AuthenticatorStep.SignedIn -> true + else -> false + } } private fun handleSignedOut() { diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt index ce3374bd..5e464327 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt @@ -16,7 +16,9 @@ package com.amplifyframework.ui.authenticator import android.app.Application +import androidx.lifecycle.viewmodel.compose.viewModel import aws.smithy.kotlin.runtime.http.HttpException +import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthUserAttributeKey.email import com.amplifyframework.auth.AuthUserAttributeKey.emailVerified import com.amplifyframework.auth.MFAType @@ -28,6 +30,7 @@ import com.amplifyframework.auth.result.step.AuthNextResetPasswordStep import com.amplifyframework.auth.result.step.AuthResetPasswordStep import com.amplifyframework.auth.result.step.AuthSignInStep import com.amplifyframework.auth.result.step.AuthSignUpStep +import com.amplifyframework.hub.HubEvent import com.amplifyframework.ui.authenticator.auth.VerificationMechanism import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep import com.amplifyframework.ui.authenticator.util.AmplifyResult @@ -45,7 +48,11 @@ import io.mockk.every import io.mockk.mockk import java.net.UnknownHostException import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -65,10 +72,13 @@ class AuthenticatorViewModelTest { private val viewModel = AuthenticatorViewModel(application, authProvider) + private val hubFlow = MutableSharedFlow>(replay = 0) + @Before fun setup() { coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration() coEvery { authProvider.getCurrentUser() } returns Success(mockUser()) + coEvery { authProvider.authStatusEvents() } returns hubFlow } //region start tests @@ -441,6 +451,60 @@ class AuthenticatorViewModelTest { viewModel.currentStep shouldBe AuthenticatorStep.SignIn } + @Test + fun `moves to SignedInState when receiving SignedIn event`() = runTest { + coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false)) + + viewModel.start(mockAuthenticatorConfiguration()) + runCurrent() + + viewModel.currentStep shouldBe AuthenticatorStep.SignIn + hubFlow.emit(HubEvent.create(AuthChannelEventName.SIGNED_IN.name)) + viewModel.currentStep shouldBe AuthenticatorStep.SignedIn + } + + @Test + fun `does not advance to signed in if sign in is in progress when SignedIn event is received`() = runTest { + coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false)) + coEvery { authProvider.signIn(any(), any()) } coAnswers { + delay(1000) // delay so that the sign in does not complete until the clock is advanced + Success(mockSignInResult()) + } + + viewModel.start(mockAuthenticatorConfiguration()) + runCurrent() + + viewModel.currentStep shouldBe AuthenticatorStep.SignIn + + backgroundScope.launch { viewModel.signIn("username", "password") } + + hubFlow.emit(HubEvent.create(AuthChannelEventName.SIGNED_IN.name)) + + // Since sign in is in progress we should not move to SignedIn until after it completes + viewModel.currentStep shouldBe AuthenticatorStep.SignIn + advanceUntilIdle() // advance the clock to complete sign in + viewModel.currentStep shouldBe AuthenticatorStep.SignedIn + } + + @Test + fun `does not advance to SignedIn when SignedIn event is received in a post-sign-in state`() = runTest { + coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false)) + coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult()) + coEvery { authProvider.getConfiguration() } returns mockAmplifyAuthConfiguration( + verificationMechanisms = setOf(VerificationMechanism.Email) + ) + coEvery { authProvider.fetchUserAttributes() } returns Success( + mockUserAttributes(email() to "email", emailVerified() to "false") + ) + + viewModel.start(mockAuthenticatorConfiguration()) + viewModel.signIn("username", "password") + + viewModel.currentStep shouldBe AuthenticatorStep.VerifyUser + hubFlow.emit(HubEvent.create(AuthChannelEventName.SIGNED_IN.name)) + viewModel.currentStep shouldBe AuthenticatorStep.VerifyUser // stay in current state + } + //endregion //region sign up tests