diff --git a/.editorconfig b/.editorconfig index 530cee93..972db077 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,4 @@ [*.{kt,kts}] #this is to match java checkstyle max_line_length=120 +ktlint_code_style=android_studio \ No newline at end of file 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 67673b95..b44710e4 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt @@ -90,10 +90,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.annotations.VisibleForTesting -internal class AuthenticatorViewModel( - application: Application, - private val authProvider: AuthProvider -) : AndroidViewModel(application) { +internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) : + AndroidViewModel(application) { // Constructor for compose viewModels provider constructor(application: Application) : this(application, RealAuthProvider()) @@ -185,7 +183,8 @@ internal class AuthenticatorViewModel( //region SignUp - private suspend fun signUp(username: String, password: String, attributes: List) { + @VisibleForTesting + suspend fun signUp(username: String, password: String, attributes: List) { viewModelScope.launch { val options = AuthSignUpOptions.builder().userAttributes(attributes).build() @@ -229,6 +228,7 @@ internal class AuthenticatorViewModel( moveTo(newState) } AuthSignUpStep.DONE -> handleSignedUp(username, password) + AuthSignUpStep.COMPLETE_AUTO_SIGN_IN -> handleAutoSignIn(username, password) else -> { // Generic error for any other next steps that may be added in the future val exception = AuthException( @@ -241,6 +241,18 @@ internal class AuthenticatorViewModel( } } + 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) + } + 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 -> { @@ -355,10 +367,7 @@ internal class AuthenticatorViewModel( ) } - private suspend fun handleEmailMfaSetupRequired( - username: String, - password: String - ) { + private suspend fun handleEmailMfaSetupRequired(username: String, password: String) { moveTo( stateFactory.newSignInContinueWithEmailSetupState( onSubmit = { mfaType -> confirmSignIn(username, password, mfaType) } @@ -366,11 +375,7 @@ internal class AuthenticatorViewModel( ) } - private suspend fun handleMfaSelectionRequired( - username: String, - password: String, - allowedMfaTypes: Set? - ) { + private suspend fun handleMfaSelectionRequired(username: String, password: String, allowedMfaTypes: Set?) { if (allowedMfaTypes.isNullOrEmpty()) { handleGeneralFailure(AuthException("Missing allowedMfaTypes", "Please open a bug with Amplify")) return @@ -492,10 +497,7 @@ internal class AuthenticatorViewModel( }.join() } - private suspend fun handleResetPasswordSuccess( - username: String, - result: AuthResetPasswordResult - ) { + private suspend fun handleResetPasswordSuccess(username: String, result: AuthResetPasswordResult) { when (result.nextStep.resetPasswordStep) { AuthResetPasswordStep.DONE -> handlePasswordResetComplete() AuthResetPasswordStep.CONFIRM_RESET_PASSWORD_WITH_CODE -> { diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthProvider.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthProvider.kt index ffb7d723..6892de62 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthProvider.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthProvider.kt @@ -52,33 +52,19 @@ import kotlinx.coroutines.flow.callbackFlow * An abstraction of the Amplify.Auth API that allows us to use coroutines with no exceptions */ internal interface AuthProvider { - suspend fun signIn( - username: String, - password: String - ): AmplifyResult + suspend fun signIn(username: String, password: String): AmplifyResult - suspend fun confirmSignIn( - challengeResponse: String - ): AmplifyResult + suspend fun confirmSignIn(challengeResponse: String): AmplifyResult - suspend fun signUp( - username: String, - password: String, - options: AuthSignUpOptions - ): AmplifyResult + suspend fun signUp(username: String, password: String, options: AuthSignUpOptions): AmplifyResult - suspend fun confirmSignUp( - username: String, - code: String - ): AmplifyResult + suspend fun confirmSignUp(username: String, code: String): AmplifyResult - suspend fun resendSignUpCode( - username: String - ): AmplifyResult + suspend fun resendSignUpCode(username: String): AmplifyResult - suspend fun resetPassword( - username: String - ): AmplifyResult + suspend fun autoSignIn(): AmplifyResult + + suspend fun resetPassword(username: String): AmplifyResult suspend fun confirmResetPassword( username: String, @@ -92,10 +78,7 @@ internal interface AuthProvider { suspend fun fetchUserAttributes(): AmplifyResult> - suspend fun confirmUserAttribute( - key: AuthUserAttributeKey, - confirmationCode: String - ): AmplifyResult + suspend fun confirmUserAttribute(key: AuthUserAttributeKey, confirmationCode: String): AmplifyResult suspend fun resendUserAttributeConfirmationCode(key: AuthUserAttributeKey): AmplifyResult @@ -108,7 +91,9 @@ internal interface AuthProvider { internal sealed interface AuthConfigurationResult { data class Valid(val configuration: AmplifyAuthConfiguration) : AuthConfigurationResult + data class Invalid(val message: String, val cause: Exception? = null) : AuthConfigurationResult + object Missing : AuthConfigurationResult } @@ -116,7 +101,6 @@ internal sealed interface AuthConfigurationResult { * The [AuthProvider] implementation that calls through to [Amplify.Auth] */ internal class RealAuthProvider : AuthProvider { - init { val cognitoPlugin = getCognitoPlugin() cognitoPlugin?.addToUserAgent(AWSCognitoAuthMetadataType.Authenticator, BuildConfig.VERSION_NAME) @@ -139,24 +123,18 @@ internal class RealAuthProvider : AuthProvider { ) } - override suspend fun signUp( - username: String, - password: String, - options: AuthSignUpOptions - ) = suspendCoroutine { continuation -> - Amplify.Auth.signUp( - username, - password, - options, - { continuation.resume(AmplifyResult.Success(it)) }, - { continuation.resume(AmplifyResult.Error(it)) } - ) - } + override suspend fun signUp(username: String, password: String, options: AuthSignUpOptions) = + suspendCoroutine { continuation -> + Amplify.Auth.signUp( + username, + password, + options, + { continuation.resume(AmplifyResult.Success(it)) }, + { continuation.resume(AmplifyResult.Error(it)) } + ) + } - override suspend fun confirmSignUp( - username: String, - code: String - ) = suspendCoroutine { continuation -> + override suspend fun confirmSignUp(username: String, code: String) = suspendCoroutine { continuation -> Amplify.Auth.confirmSignUp( username, code, @@ -165,9 +143,7 @@ internal class RealAuthProvider : AuthProvider { ) } - override suspend fun resendSignUpCode( - username: String - ) = suspendCoroutine { continuation -> + override suspend fun resendSignUpCode(username: String) = suspendCoroutine { continuation -> Amplify.Auth.resendSignUpCode( username, { continuation.resume(AmplifyResult.Success(it)) }, @@ -175,30 +151,32 @@ internal class RealAuthProvider : AuthProvider { ) } - override suspend fun resetPassword( - username: String - ) = suspendCoroutine { continuation -> - Amplify.Auth.resetPassword( - username, + override suspend fun autoSignIn() = suspendCoroutine { continuation -> + Amplify.Auth.autoSignIn( { continuation.resume(AmplifyResult.Success(it)) }, { continuation.resume(AmplifyResult.Error(it)) } ) } - override suspend fun confirmResetPassword( - username: String, - newPassword: String, - confirmationCode: String - ) = suspendCoroutine { continuation -> - Amplify.Auth.confirmResetPassword( + override suspend fun resetPassword(username: String) = suspendCoroutine { continuation -> + Amplify.Auth.resetPassword( username, - newPassword, - confirmationCode, - { continuation.resume(AmplifyResult.Success(Unit)) }, + { continuation.resume(AmplifyResult.Success(it)) }, { continuation.resume(AmplifyResult.Error(it)) } ) } + override suspend fun confirmResetPassword(username: String, newPassword: String, confirmationCode: String) = + suspendCoroutine { continuation -> + Amplify.Auth.confirmResetPassword( + username, + newPassword, + confirmationCode, + { continuation.resume(AmplifyResult.Success(Unit)) }, + { continuation.resume(AmplifyResult.Error(it)) } + ) + } + override suspend fun signOut() = suspendCoroutine { continuation -> Amplify.Auth.signOut { continuation.resume(it) } } @@ -217,27 +195,24 @@ internal class RealAuthProvider : AuthProvider { ) } - override suspend fun confirmUserAttribute( - key: AuthUserAttributeKey, - confirmationCode: String - ) = suspendCoroutine { continuation -> - Amplify.Auth.confirmUserAttribute( - key, - confirmationCode, - { continuation.resume(AmplifyResult.Success(Unit)) }, - { continuation.resume(AmplifyResult.Error(it)) } - ) - } + override suspend fun confirmUserAttribute(key: AuthUserAttributeKey, confirmationCode: String) = + suspendCoroutine { continuation -> + Amplify.Auth.confirmUserAttribute( + key, + confirmationCode, + { continuation.resume(AmplifyResult.Success(Unit)) }, + { continuation.resume(AmplifyResult.Error(it)) } + ) + } - override suspend fun resendUserAttributeConfirmationCode( - key: AuthUserAttributeKey - ) = suspendCoroutine { continuation -> - Amplify.Auth.resendUserAttributeConfirmationCode( - key, - { continuation.resume(AmplifyResult.Success(it)) }, - { continuation.resume(AmplifyResult.Error(it)) } - ) - } + override suspend fun resendUserAttributeConfirmationCode(key: AuthUserAttributeKey) = + suspendCoroutine { continuation -> + Amplify.Auth.resendUserAttributeConfirmationCode( + key, + { continuation.resume(AmplifyResult.Success(it)) }, + { continuation.resume(AmplifyResult.Error(it)) } + ) + } override suspend fun getCurrentUser() = suspendCoroutine { continuation -> Amplify.Auth.getCurrentUser( @@ -282,13 +257,10 @@ internal class RealAuthProvider : AuthProvider { return AuthConfigurationResult.Valid(amplifyAuthConfiguration) } - private fun getCognitoPlugin(): AWSCognitoAuthPlugin? { - return try { - Amplify.Auth.getPlugin("awsCognitoAuthPlugin") - as AWSCognitoAuthPlugin - } catch (e: Throwable) { - null - } + private fun getCognitoPlugin(): AWSCognitoAuthPlugin? = try { + Amplify.Auth.getPlugin("awsCognitoAuthPlugin") as AWSCognitoAuthPlugin + } catch (e: Throwable) { + null } private fun getSignInMethod(attributes: List) = when { 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 e77cec91..ce3374bd 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt @@ -27,6 +27,7 @@ import com.amplifyframework.auth.result.AuthResetPasswordResult 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.ui.authenticator.auth.VerificationMechanism import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep import com.amplifyframework.ui.authenticator.util.AmplifyResult @@ -440,6 +441,22 @@ class AuthenticatorViewModelTest { viewModel.currentStep shouldBe AuthenticatorStep.SignIn } +//endregion +//region sign up tests + + @Test + fun `user can autoSignIn after sign up`() = runTest { + val result = mockSignUpResult(nextStep = mockNextSignUpStep(signUpStep = AuthSignUpStep.COMPLETE_AUTO_SIGN_IN)) + coEvery { authProvider.signUp("username", "password", any()) } returns Success(result) + coEvery { authProvider.autoSignIn() } returns Success(mockSignInResult()) + + viewModel.start(mockAuthenticatorConfiguration()) + viewModel.signUp("username", "password", emptyList()) + advanceUntilIdle() + + viewModel.currentStep shouldBe AuthenticatorStep.SignedIn + } + //endregion //region password reset tests diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt index d63f246b..981e2004 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt @@ -25,8 +25,11 @@ import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.TOTPSetupDetails import com.amplifyframework.auth.result.AuthSignInResult +import com.amplifyframework.auth.result.AuthSignUpResult import com.amplifyframework.auth.result.step.AuthNextSignInStep +import com.amplifyframework.auth.result.step.AuthNextSignUpStep import com.amplifyframework.auth.result.step.AuthSignInStep +import com.amplifyframework.auth.result.step.AuthSignUpStep import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration import com.amplifyframework.ui.authenticator.auth.PasswordCriteria import com.amplifyframework.ui.authenticator.auth.SignInMethod @@ -112,6 +115,25 @@ internal fun mockNextSignInStep( availableFactors ) +internal fun mockSignUpResult( + nextStep: AuthNextSignUpStep, + userId: String = "userId" +) = AuthSignUpResult( + nextStep.signUpStep != AuthSignUpStep.CONFIRM_SIGN_UP_STEP, + nextStep, + userId +) + +internal fun mockNextSignUpStep( + signUpStep: AuthSignUpStep = AuthSignUpStep.DONE, + additionalInfo: Map = emptyMap(), + codeDeliveryDetails: AuthCodeDeliveryDetails? = null +) = AuthNextSignUpStep( + signUpStep, + additionalInfo, + codeDeliveryDetails +) + internal fun mockUserAttributes(vararg attribute: Pair) = attribute.map { AuthUserAttribute(it.first, it.second) }