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 d3bef647..8e0046ae 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt @@ -15,11 +15,13 @@ package com.amplifyframework.ui.authenticator +import android.app.Activity import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.amplifyframework.auth.AuthChannelEventName import com.amplifyframework.auth.AuthException +import com.amplifyframework.auth.AuthUser import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType @@ -50,8 +52,10 @@ import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration import com.amplifyframework.ui.authenticator.auth.toAttributeKey import com.amplifyframework.ui.authenticator.auth.toFieldKey import com.amplifyframework.ui.authenticator.auth.toVerifiedAttributeKey +import com.amplifyframework.ui.authenticator.data.UserInfo import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep +import com.amplifyframework.ui.authenticator.enums.SignInSource import com.amplifyframework.ui.authenticator.forms.FieldError import com.amplifyframework.ui.authenticator.forms.FieldError.ConfirmationCodeIncorrect import com.amplifyframework.ui.authenticator.forms.FieldError.FieldValueExists @@ -81,6 +85,7 @@ import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage import com.amplifyframework.ui.authenticator.util.isConnectivityIssue import com.amplifyframework.ui.authenticator.util.toFieldError +import java.lang.ref.WeakReference import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -152,6 +157,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth } stateFactory = StepStateFactory( + configuration, authConfiguration, buildForm(configuration.signUpForm), ::moveTo @@ -195,22 +201,23 @@ internal class AuthenticatorViewModel(application: Application, private val auth //region SignUp @VisibleForTesting - suspend fun signUp(username: String, password: String, attributes: List) { + suspend fun signUp(username: String, password: String?, attributes: List) { viewModelScope.launch { val options = AuthSignUpOptions.builder().userAttributes(attributes).build() + val info = UserInfo(username = username, password = password, signInSource = SignInSource.SignUp) when (val result = authProvider.signUp(username, password, options)) { is AmplifyResult.Error -> handleSignUpFailure(result.error) - is AmplifyResult.Success -> handleSignUpSuccess(username, password, result.data) + is AmplifyResult.Success -> handleSignUpSuccess(info, result.data) } }.join() } - private suspend fun confirmSignUp(username: String, password: String, code: String) { + private suspend fun confirmSignUp(info: UserInfo, code: String) { viewModelScope.launch { - when (val result = authProvider.confirmSignUp(username, code)) { + when (val result = authProvider.confirmSignUp(info.username, code)) { is AmplifyResult.Error -> handleSignUpConfirmFailure(result.error) - is AmplifyResult.Success -> handleSignUpSuccess(username, password, result.data) + is AmplifyResult.Success -> handleSignUpSuccess(info, result.data) } }.join() } @@ -228,18 +235,18 @@ internal class AuthenticatorViewModel(application: Application, private val auth private suspend fun handleSignUpFailure(error: AuthException) = handleAuthException(error) private suspend fun handleSignUpConfirmFailure(error: AuthException) = handleAuthException(error) - private suspend fun handleSignUpSuccess(username: String, password: String, result: AuthSignUpResult) { + private suspend fun handleSignUpSuccess(info: UserInfo, result: AuthSignUpResult) { when (result.nextStep.signUpStep) { AuthSignUpStep.CONFIRM_SIGN_UP_STEP -> { val newState = stateFactory.newSignUpConfirmState( result.nextStep.codeDeliveryDetails, - onResendCode = { resendSignUpCode(username) }, - onSubmit = { confirmationCode -> confirmSignUp(username, password, confirmationCode) } + onResendCode = { resendSignUpCode(info.username) }, + onSubmit = { confirmationCode -> confirmSignUp(info, confirmationCode) } ) moveTo(newState) } - AuthSignUpStep.DONE -> handleSignedUp(username, password) - AuthSignUpStep.COMPLETE_AUTO_SIGN_IN -> handleAutoSignIn(username, password) + AuthSignUpStep.COMPLETE_AUTO_SIGN_IN -> handleAutoSignIn(info) + AuthSignUpStep.DONE -> handleSignedUp(info) else -> { // Generic error for any other next steps that may be added in the future val exception = AuthException( @@ -252,31 +259,25 @@ internal class AuthenticatorViewModel(application: Application, private val auth } } - private suspend fun handleAutoSignIn(username: String, password: String) { - 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) + private suspend fun handleAutoSignIn(info: UserInfo) = 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(info) } + is AmplifyResult.Success -> handleSignInSuccess(info, result.data) } } - private suspend fun handleSignedUp(username: String, password: String) { - 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) + private suspend fun handleSignedUp(info: UserInfo) = startSignInJob { + when (val result = authProvider.signIn(info.username, info.password)) { + is AmplifyResult.Error -> { + moveTo(AuthenticatorStep.SignIn) + handleSignInFailure(info, result.error) } + is AmplifyResult.Success -> handleSignInSuccess(info, result.data) } } @@ -284,54 +285,60 @@ internal class AuthenticatorViewModel(application: Application, private val auth //region SignIn @VisibleForTesting - suspend fun signIn(username: String, password: String) { - startSignInJob { - when (val result = authProvider.signIn(username, password)) { - is AmplifyResult.Error -> handleSignInFailure(username, password, result.error) - is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) - } + suspend fun signIn(username: String, password: String?) { + val info = UserInfo( + username = username, + password = password, + signInSource = SignInSource.SignIn + ) + startSignIn(info) + } + + private suspend fun startSignIn(info: UserInfo) = startSignInJob { + when (val result = authProvider.signIn(info.username, info.password)) { + is AmplifyResult.Error -> handleSignInFailure(info, result.error) + is AmplifyResult.Success -> handleSignInSuccess(info, result.data) } } - private suspend fun confirmSignIn(username: String, password: String, challengeResponse: String) { - startSignInJob { - when (val result = authProvider.confirmSignIn(challengeResponse)) { - is AmplifyResult.Error -> handleSignInFailure(username, password, result.error) - is AmplifyResult.Success -> handleSignInSuccess(username, password, result.data) - } + private suspend fun confirmSignIn(info: UserInfo, challengeResponse: String) = startSignInJob { + when (val result = authProvider.confirmSignIn(challengeResponse)) { + is AmplifyResult.Error -> handleSignInFailure(info, result.error) + is AmplifyResult.Success -> handleSignInSuccess(info, result.data) } } - private suspend fun setNewSignInPassword(username: String, password: String) { - 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) + private suspend fun setNewSignInPassword(info: UserInfo, newPassword: String) = startSignInJob { + when (val result = authProvider.confirmSignIn(newPassword)) { + // an error here is more similar to a sign up error + is AmplifyResult.Error -> handleSignUpFailure(result.error) + is AmplifyResult.Success -> { + val newUserInfo = info.copy(password = newPassword) + handleSignInSuccess(newUserInfo, result.data) } } } - private suspend fun handleSignInFailure(username: String, password: String, error: AuthException) { + private suspend fun handleSignInFailure(info: UserInfo, error: AuthException) { // UserNotConfirmed and PasswordResetRequired are special cases where we need // to enter different flows when (error) { - is UserNotConfirmedException -> handleUnconfirmedSignIn(username, password) - is PasswordResetRequiredException -> handleResetRequiredSignIn(username) + is UserNotConfirmedException -> handleUnconfirmedSignIn(info) + is PasswordResetRequiredException -> handleResetRequiredSignIn(info.username) is NotAuthorizedException -> sendMessage(InvalidLoginMessage(error)) else -> handleAuthException(error) } } - private suspend fun handleUnconfirmedSignIn(username: String, password: String) { - when (val result = authProvider.resendSignUpCode(username)) { + private suspend fun handleUnconfirmedSignIn(info: UserInfo) { + when (val result = authProvider.resendSignUpCode(info.username)) { is AmplifyResult.Error -> handleAuthException(result.error) is AmplifyResult.Success -> { val details = result.data val newState = stateFactory.newSignUpConfirmState( details, - onResendCode = { resendSignUpCode(username) }, - onSubmit = { confirmationCode -> confirmSignUp(username, password, confirmationCode) } + onResendCode = { resendSignUpCode(info.username) }, + onSubmit = { confirmationCode -> confirmSignUp(info, confirmationCode) } ) moveTo(newState) } @@ -345,11 +352,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth } } - private suspend fun handleTotpSetupRequired( - username: String, - password: String, - totpSetupDetails: TOTPSetupDetails? - ) { + private suspend fun handleTotpSetupRequired(info: UserInfo, totpSetupDetails: TOTPSetupDetails?) { if (totpSetupDetails == null) { val exception = AuthException("Missing TOTPSetupDetails", "Please open a bug with Amplify") handleGeneralFailure(exception) @@ -357,20 +360,16 @@ internal class AuthenticatorViewModel(application: Application, private val auth } val issuer = configuration.totpOptions?.issuer ?: getAppName() - val setupUri = totpSetupDetails.getSetupURI(issuer, username).toString() + val setupUri = totpSetupDetails.getSetupURI(issuer, info.username).toString() val newState = stateFactory.newSignInContinueWithTotpSetupState( sharedSecret = totpSetupDetails.sharedSecret, setupUri = setupUri, - onSubmit = { confirmationCode -> confirmSignIn(username, password, confirmationCode) } + onSubmit = { confirmationCode -> confirmSignIn(info, confirmationCode) } ) moveTo(newState) } - private suspend fun handleMfaSetupSelectionRequired( - username: String, - password: String, - allowedMfaTypes: Set? - ) { + private suspend fun handleMfaSetupSelectionRequired(info: UserInfo, allowedMfaTypes: Set?) { if (allowedMfaTypes.isNullOrEmpty()) { handleGeneralFailure(AuthException("Missing allowedMfaTypes", "Please open a bug with Amplify")) return @@ -379,20 +378,20 @@ internal class AuthenticatorViewModel(application: Application, private val auth moveTo( stateFactory.newSignInContinueWithMfaSetupSelectionState( allowedMfaTypes = allowedMfaTypes, - onSubmit = { mfaType -> confirmSignIn(username, password, mfaType) } + onSubmit = { mfaType -> confirmSignIn(info, mfaType) } ) ) } - private suspend fun handleEmailMfaSetupRequired(username: String, password: String) { + private suspend fun handleEmailMfaSetupRequired(info: UserInfo) { moveTo( stateFactory.newSignInContinueWithEmailSetupState( - onSubmit = { mfaType -> confirmSignIn(username, password, mfaType) } + onSubmit = { mfaType -> confirmSignIn(info, mfaType) } ) ) } - private suspend fun handleMfaSelectionRequired(username: String, password: String, allowedMfaTypes: Set?) { + private suspend fun handleMfaSelectionRequired(info: UserInfo, allowedMfaTypes: Set?) { if (allowedMfaTypes.isNullOrEmpty()) { handleGeneralFailure(AuthException("Missing allowedMfaTypes", "Please open a bug with Amplify")) return @@ -401,48 +400,48 @@ internal class AuthenticatorViewModel(application: Application, private val auth moveTo( stateFactory.newSignInContinueWithMfaSelectionState( allowedMfaTypes = allowedMfaTypes, - onSubmit = { mfaType -> confirmSignIn(username, password, mfaType) } + onSubmit = { mfaType -> confirmSignIn(info, mfaType) } ) ) } - private suspend fun handleSignInSuccess(username: String, password: String, result: AuthSignInResult) { + private suspend fun handleSignInSuccess(info: UserInfo, result: AuthSignInResult) { when (val nextStep = result.nextStep.signInStep) { AuthSignInStep.DONE -> checkVerificationMechanisms() AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE, AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP -> moveTo( stateFactory.newSignInMfaState( - result.nextStep.codeDeliveryDetails - ) { confirmationCode -> confirmSignIn(username, password, confirmationCode) } + codeDeliveryDetails = result.nextStep.codeDeliveryDetails + ) { confirmationCode -> confirmSignIn(info, confirmationCode) } ) AuthSignInStep.CONFIRM_SIGN_IN_WITH_CUSTOM_CHALLENGE -> moveTo( stateFactory.newSignInConfirmCustomState( result.nextStep.codeDeliveryDetails, result.nextStep.additionalInfo ?: emptyMap() - ) { confirmationCode -> confirmSignIn(username, password, confirmationCode) } + ) { confirmationCode -> confirmSignIn(info, confirmationCode) } ) AuthSignInStep.CONFIRM_SIGN_IN_WITH_NEW_PASSWORD -> moveTo( stateFactory.newSignInConfirmNewPasswordState { newPassword -> - setNewSignInPassword(username, newPassword) + setNewSignInPassword(info, newPassword) } ) // This step isn't actually returned, it comes back as a PasswordResetRequiredException. // Handling here for future correctness - AuthSignInStep.RESET_PASSWORD -> handleResetRequiredSignIn(username) + AuthSignInStep.RESET_PASSWORD -> handleResetRequiredSignIn(info.username) // This step isn't actually returned, it comes back as a UserNotConfirmedException. // Handling here for future correctness - AuthSignInStep.CONFIRM_SIGN_UP -> handleUnconfirmedSignIn(username, password) + AuthSignInStep.CONFIRM_SIGN_UP -> handleUnconfirmedSignIn(info) AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SELECTION -> - handleMfaSelectionRequired(username, password, result.nextStep.allowedMFATypes) + handleMfaSelectionRequired(info, result.nextStep.allowedMFATypes) AuthSignInStep.CONTINUE_SIGN_IN_WITH_MFA_SETUP_SELECTION -> - handleMfaSetupSelectionRequired(username, password, result.nextStep.allowedMFATypes) + handleMfaSetupSelectionRequired(info, result.nextStep.allowedMFATypes) AuthSignInStep.CONTINUE_SIGN_IN_WITH_EMAIL_MFA_SETUP -> - handleEmailMfaSetupRequired(username, password) + handleEmailMfaSetupRequired(info) AuthSignInStep.CONTINUE_SIGN_IN_WITH_TOTP_SETUP -> - handleTotpSetupRequired(username, password, result.nextStep.totpSetupDetails) + handleTotpSetupRequired(info, result.nextStep.totpSetupDetails) AuthSignInStep.CONFIRM_SIGN_IN_WITH_TOTP_CODE -> moveTo( stateFactory.newSignInConfirmTotpCodeState { confirmationCode -> - confirmSignIn(username, password, confirmationCode) + confirmSignIn(info, confirmationCode) } ) else -> { @@ -509,7 +508,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth logger.debug("Confirming password reset") when (val result = authProvider.confirmResetPassword(username, password, code)) { is AmplifyResult.Error -> handleResetPasswordError(result.error) - is AmplifyResult.Success -> handlePasswordResetComplete(username, password) + is AmplifyResult.Success -> handlePasswordResetComplete() } }.join() } @@ -536,16 +535,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth private suspend fun handlePasswordResetComplete(username: String? = null, password: String? = null) { logger.debug("Password reset complete") sendMessage(PasswordResetMessage) - if (username != null && password != null) { - 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)) - } + moveTo(stateFactory.newSignInState(this::signIn)) } private suspend fun handleResetPasswordError(error: AuthException) = handleAuthException(error) @@ -644,14 +634,13 @@ internal class AuthenticatorViewModel(application: Application, private val auth is AmplifyResult.Error -> { if (result.error is SessionExpiredException) { logger.error(result.error.toString()) - logger.error("Current signed in user session has expired, signing out.") signOut() } else { handleGeneralFailure(result.error) } } - is AmplifyResult.Success -> moveTo(stateFactory.newSignedInState(result.data, this::signOut)) + is AmplifyResult.Success -> signInComplete(result.data) } } @@ -661,6 +650,10 @@ internal class AuthenticatorViewModel(application: Application, private val auth expectingSignInEvent = false } + private fun signInComplete(user: AuthUser) { + moveTo(stateFactory.newSignedInState(user, this::signOut)) + } + // Amplify has told us the user signed in. private suspend fun handleSignedInEvent() { if (!expectingSignInEvent && !inPostSignInState()) { diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/data/UserInfo.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/data/UserInfo.kt new file mode 100644 index 00000000..f9ed43c5 --- /dev/null +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/data/UserInfo.kt @@ -0,0 +1,5 @@ +package com.amplifyframework.ui.authenticator.data + +import com.amplifyframework.ui.authenticator.enums.SignInSource + +internal data class UserInfo(val username: String, val password: String?, val signInSource: SignInSource) diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/enums/SignInSource.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/enums/SignInSource.kt new file mode 100644 index 00000000..f29b16cd --- /dev/null +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/enums/SignInSource.kt @@ -0,0 +1,12 @@ +package com.amplifyframework.ui.authenticator.enums + +internal enum class SignInSource { + // Standard sign in + SignIn, + + // Automatic sign in after completing sign up + SignUp, + + // Signed in outside of Authenticator + External +} diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FormBuilder.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FormBuilder.kt index f3896119..19594652 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FormBuilder.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/forms/FormBuilder.kt @@ -23,9 +23,7 @@ import com.amplifyframework.ui.authenticator.auth.toFieldKey internal data class FormData(val fields: List) -internal fun buildForm(func: FormBuilderImpl.() -> Unit): FormData { - return FormBuilderImpl().apply(func).build() -} +internal fun buildForm(func: FormBuilderImpl.() -> Unit): FormData = FormBuilderImpl().apply(func).build() /** * Builder API for supplying custom form metadata for the signup form. @@ -39,12 +37,12 @@ interface SignUpFormBuilder { /** * Adds the standard password field. */ - fun password() + fun password(required: Boolean = true) /** * Adds the standard password confirmation field. */ - fun confirmPassword() + fun confirmPassword(required: Boolean = true) /** * Adds the standard email field. @@ -187,18 +185,23 @@ internal class FormBuilderImpl : SignUpFormBuilder { ) } - override fun password() = password(validator = FieldValidators.None) + override fun password(required: Boolean) = password( + required = required, + validator = FieldValidators.None + ) - fun password(validator: FieldValidator) { + fun password(validator: FieldValidator, required: Boolean = true) { this += FieldConfig.Password( key = FieldKey.Password, + required = required, validator = validator ) } - override fun confirmPassword() { + override fun confirmPassword(required: Boolean) { this += FieldConfig.Password( key = FieldKey.ConfirmPassword, + required = required, validator = FieldValidators.confirmPassword() ) } @@ -317,13 +320,7 @@ internal class FormBuilderImpl : SignUpFormBuilder { ) } - override fun date( - key: FieldKey, - label: String, - hint: String?, - required: Boolean, - validator: FieldValidator - ) { + override fun date(key: FieldKey, label: String, hint: String?, required: Boolean, validator: FieldValidator) { this += FieldConfig.Date( key = key, label = label, @@ -333,13 +330,7 @@ internal class FormBuilderImpl : SignUpFormBuilder { ) } - override fun phone( - key: FieldKey, - label: String, - hint: String?, - required: Boolean, - validator: FieldValidator - ) { + override fun phone(key: FieldKey, label: String, hint: String?, required: Boolean, validator: FieldValidator) { this += FieldConfig.PhoneNumber( key = key, label = label, @@ -392,10 +383,7 @@ internal class FormBuilderImpl : SignUpFormBuilder { fields.putAll(map) } - fun markRequiredFields( - signInMethod: SignInMethod, - requiredKeys: List - ) { + fun markRequiredFields(signInMethod: SignInMethod, requiredKeys: List) { fields.replaceAll { fieldKey, config -> if (fieldKey is FieldKey.UserAttributeKey && requiredKeys.contains(fieldKey.attributeKey)) { config.required() diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInSelectAuthFactorStateImpl.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInSelectAuthFactorStateImpl.kt index d0601eb7..efc59283 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInSelectAuthFactorStateImpl.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInSelectAuthFactorStateImpl.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState import com.amplifyframework.ui.authenticator.auth.SignInMethod -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor +import com.amplifyframework.ui.authenticator.data.containsPassword import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep -import com.amplifyframework.ui.authenticator.enums.containsPassword internal class SignInSelectAuthFactorStateImpl( override val username: String, @@ -46,4 +46,4 @@ internal fun SignInSelectAuthFactorState.getPasswordFactor(): AuthFactor = availableAuthFactors.first { it is AuthFactor.Password } internal val SignInSelectAuthFactorState.signInMethod: SignInMethod - get() = (this as SignInSelectAuthFactorStateImpl).signInMethod \ No newline at end of file + get() = (this as SignInSelectAuthFactorStateImpl).signInMethod diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignUpStateImpl.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignUpStateImpl.kt index dfc52cba..945301f1 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignUpStateImpl.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignUpStateImpl.kt @@ -31,18 +31,28 @@ import com.amplifyframework.ui.authenticator.forms.buildForm internal class SignUpStateImpl( private val signInMethod: SignInMethod, private val signUpAttributes: List, + requirePasswordField: Boolean, private val passwordCriteria: PasswordCriteria, private val signUpForm: FormData, - private val onSubmit: suspend (username: String, password: String, attributes: List) -> Unit, + private val onSubmit: suspend (username: String, password: String?, attributes: List) -> Unit, private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit -) : BaseStateImpl(), SignUpState { +) : BaseStateImpl(), + SignUpState { init { val formData = buildForm { // First add all fields required by configuration in the standard order fieldForSignInMethod(signInMethod) - password(validator = FieldValidators.password(passwordCriteria)) - confirmPassword() + if (requirePasswordField) { + password(validator = FieldValidators.password(passwordCriteria)) + + // We don't add confirm password if the customer supplied a form with password and without confirmPassword + if (signUpForm.containsField(FieldKey.ConfirmPassword) || + !signUpForm.containsField(FieldKey.Password) + ) { + confirmPassword() + } + } signUpAttributes.forEach { attribute -> when (attribute) { AuthUserAttributeKey.birthdate() -> birthdate(required = true) @@ -77,8 +87,10 @@ internal class SignUpStateImpl( override suspend fun signUp() = doSubmit { val username = form.getTrimmed(signInMethod.toFieldKey())!! - val password = form.getTrimmed(FieldKey.Password)!! + val password = form.getTrimmed(FieldKey.Password) val attributes = form.getUserAttributes() onSubmit(username, password, attributes) } + + private fun FormData.containsField(key: FieldKey) = fields.any { it.key == key } } diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt index d1df7f5e..4581b2d0 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt @@ -21,27 +21,25 @@ import com.amplifyframework.auth.AuthUserAttribute import com.amplifyframework.auth.AuthUserAttributeKey import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.result.AuthSignOutResult +import com.amplifyframework.ui.authenticator.AuthenticatorConfiguration import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration +import com.amplifyframework.ui.authenticator.data.signUpRequiresPassword import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep import com.amplifyframework.ui.authenticator.forms.FormData internal class StepStateFactory( + private val configuration: AuthenticatorConfiguration, private val authConfiguration: AmplifyAuthConfiguration, private val signUpForm: FormData, private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit ) { - fun newSignedInState( - user: AuthUser, - onSignOut: suspend () -> AuthSignOutResult - ) = SignedInStateImpl( + fun newSignedInState(user: AuthUser, onSignOut: suspend () -> AuthSignOutResult) = SignedInStateImpl( user = user, onSignOut = onSignOut ) - fun newSignInState( - onSubmit: suspend (username: String, password: String) -> Unit - ) = SignInStateImpl( + fun newSignInState(onSubmit: suspend (username: String, password: String) -> Unit) = SignInStateImpl( signInMethod = authConfiguration.signInMethod, onSubmit = onSubmit, onMoveTo = onMoveTo @@ -67,20 +65,18 @@ internal class StepStateFactory( onMoveTo = onMoveTo ) - fun newSignInConfirmNewPasswordState( - onSubmit: suspend (password: String) -> Unit - ) = SignInConfirmNewPasswordStateImpl( - passwordCriteria = authConfiguration.passwordCriteria, - onSubmit = onSubmit, - onMoveTo = onMoveTo - ) + fun newSignInConfirmNewPasswordState(onSubmit: suspend (password: String) -> Unit) = + SignInConfirmNewPasswordStateImpl( + passwordCriteria = authConfiguration.passwordCriteria, + onSubmit = onSubmit, + onMoveTo = onMoveTo + ) - fun newSignInConfirmTotpCodeState( - onSubmit: suspend (confirmationCode: String) -> Unit - ) = SignInConfirmTotpCodeStateImpl( - onSubmit = onSubmit, - onMoveTo = onMoveTo - ) + fun newSignInConfirmTotpCodeState(onSubmit: suspend (confirmationCode: String) -> Unit) = + SignInConfirmTotpCodeStateImpl( + onSubmit = onSubmit, + onMoveTo = onMoveTo + ) fun newSignInContinueWithMfaSetupSelectionState( allowedMfaTypes: Set, @@ -91,12 +87,11 @@ internal class StepStateFactory( onMoveTo = onMoveTo ) - fun newSignInContinueWithEmailSetupState( - onSubmit: suspend (email: String) -> Unit - ) = SignInContinueWithEmailSetupStateImpl( - onSubmit = onSubmit, - onMoveTo = onMoveTo - ) + fun newSignInContinueWithEmailSetupState(onSubmit: suspend (email: String) -> Unit) = + SignInContinueWithEmailSetupStateImpl( + onSubmit = onSubmit, + onMoveTo = onMoveTo + ) fun newSignInContinueWithMfaSelectionState( allowedMfaTypes: Set, @@ -119,10 +114,11 @@ internal class StepStateFactory( ) fun newSignUpState( - onSubmit: suspend (username: String, password: String, attributes: List) -> Unit + onSubmit: suspend (username: String, password: String?, attributes: List) -> Unit ) = SignUpStateImpl( signInMethod = authConfiguration.signInMethod, signUpAttributes = authConfiguration.signUpAttributes, + requirePasswordField = configuration.authenticationFlow.signUpRequiresPassword, passwordCriteria = authConfiguration.passwordCriteria, signUpForm = signUpForm, onSubmit = onSubmit, @@ -140,9 +136,7 @@ internal class StepStateFactory( onMoveTo = onMoveTo ) - fun newResetPasswordState( - onSubmit: suspend (username: String) -> Unit - ) = PasswordResetStateImpl( + fun newResetPasswordState(onSubmit: suspend (username: String) -> Unit) = PasswordResetStateImpl( signInMethod = authConfiguration.signInMethod, onSubmit = onSubmit, onMoveTo = onMoveTo diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactor.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactor.kt index e7baf5e8..c49f8041 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactor.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactor.kt @@ -16,9 +16,9 @@ import androidx.compose.ui.unit.dp import com.amplifyframework.ui.authenticator.R import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState import com.amplifyframework.ui.authenticator.auth.toFieldKey -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor +import com.amplifyframework.ui.authenticator.data.containsPassword import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep -import com.amplifyframework.ui.authenticator.enums.containsPassword import com.amplifyframework.ui.authenticator.forms.FieldKey import com.amplifyframework.ui.authenticator.states.getPasswordFactor import com.amplifyframework.ui.authenticator.states.signInMethod @@ -33,7 +33,7 @@ fun SignInSelectAuthFactor( headerContent: @Composable (SignInSelectAuthFactorState) -> Unit = { AuthenticatorTitle(stringResource(R.string.amplify_ui_authenticator_title_select_factor)) }, - footerContent: @Composable (SignInSelectAuthFactorState) -> Unit = { SignInSelectFactorFooter(it) } + footerContent: @Composable (SignInSelectAuthFactorState) -> Unit = { SignInSelectAuthFactorFooter(it) } ) { Column( modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp) @@ -79,10 +79,11 @@ fun SignInSelectAuthFactor( } @Composable -fun SignInSelectFactorFooter(state: SignInSelectAuthFactorState, modifier: Modifier = Modifier) = BackToSignInFooter( - modifier = modifier, - onClickBackToSignIn = { state.moveTo(AuthenticatorStep.SignIn) } -) +fun SignInSelectAuthFactorFooter(state: SignInSelectAuthFactorState, modifier: Modifier = Modifier) = + BackToSignInFooter( + modifier = modifier, + onClickBackToSignIn = { state.moveTo(AuthenticatorStep.SignIn) } + ) @Composable private fun AuthFactorButton( diff --git a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/TestTags.kt b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/TestTags.kt index 5df6ca97..e99d1775 100644 --- a/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/TestTags.kt +++ b/authenticator/src/main/java/com/amplifyframework/ui/authenticator/ui/TestTags.kt @@ -15,7 +15,7 @@ package com.amplifyframework.ui.authenticator.ui -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor import com.amplifyframework.ui.authenticator.forms.FieldKey @Suppress("ConstPropertyName") 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 6892de62..2cd90140 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,11 +52,11 @@ 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 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 @@ -106,7 +106,7 @@ internal class RealAuthProvider : AuthProvider { cognitoPlugin?.addToUserAgent(AWSCognitoAuthMetadataType.Authenticator, BuildConfig.VERSION_NAME) } - override suspend fun signIn(username: String, password: String) = suspendCoroutine { continuation -> + override suspend fun signIn(username: String, password: String?) = suspendCoroutine { continuation -> Amplify.Auth.signIn( username, password, @@ -123,7 +123,7 @@ internal class RealAuthProvider : AuthProvider { ) } - override suspend fun signUp(username: String, password: String, options: AuthSignUpOptions) = + override suspend fun signUp(username: String, password: String?, options: AuthSignUpOptions) = suspendCoroutine { continuation -> Amplify.Auth.signUp( username, 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 5e464327..f8d406b0 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModelTest.kt @@ -571,7 +571,7 @@ class AuthenticatorViewModelTest { } @Test - fun `Password reset confirmation succeeds, sign in succeeds, state should be signed in`() = runTest { + fun `Password reset confirmation succeeds, state should be sign in`() = runTest { coEvery { authProvider.fetchAuthSession() } returns Success(mockAuthSession(isSignedIn = false)) coEvery { authProvider.resetPassword(any()) } returns Success( AuthResetPasswordResult( @@ -581,13 +581,12 @@ class AuthenticatorViewModelTest { ) coEvery { authProvider.confirmResetPassword(any(), any(), any()) } returns Success(Unit) - coEvery { authProvider.signIn(any(), any()) } returns Success(mockSignInResult()) viewModel.start(mockAuthenticatorConfiguration(initialStep = AuthenticatorStep.PasswordReset)) viewModel.resetPassword("username") viewModel.confirmResetPassword("username", "password", "code") - viewModel.currentStep shouldBe AuthenticatorStep.SignedIn + viewModel.currentStep shouldBe AuthenticatorStep.SignIn } @Test @@ -652,6 +651,7 @@ class AuthenticatorViewModelTest { viewModel.resetPassword("username") } } + //endregion //region helpers private val AuthenticatorViewModel.currentStep: AuthenticatorStep 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 071b06be..c27cec1e 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/MockAuthenticatorData.kt @@ -35,6 +35,7 @@ import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration import com.amplifyframework.ui.authenticator.auth.PasswordCriteria import com.amplifyframework.ui.authenticator.auth.SignInMethod import com.amplifyframework.ui.authenticator.auth.VerificationMechanism +import com.amplifyframework.ui.authenticator.data.AuthenticationFlow import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep import com.amplifyframework.ui.authenticator.forms.SignUpFormBuilder @@ -45,11 +46,13 @@ import io.mockk.mockk internal fun mockAuthenticatorConfiguration( initialStep: AuthenticatorInitialStep = AuthenticatorStep.SignIn, signUpForm: SignUpFormBuilder.() -> Unit = {}, - totpOptions: TotpOptions? = null + totpOptions: TotpOptions? = null, + authenticationFlow: AuthenticationFlow = AuthenticationFlow.Password ) = AuthenticatorConfiguration( initialStep = initialStep, signUpForm = signUpForm, - totpOptions = totpOptions + totpOptions = totpOptions, + authenticationFlow = authenticationFlow ) internal fun mockAmplifyAuthConfiguration( @@ -116,10 +119,7 @@ internal fun mockNextSignInStep( availableFactors ) -internal fun mockSignUpResult( - nextStep: AuthNextSignUpStep, - userId: String = "userId" -) = AuthSignUpResult( +internal fun mockSignUpResult(nextStep: AuthNextSignUpStep, userId: String = "userId") = AuthSignUpResult( nextStep.signUpStep != AuthSignUpStep.CONFIRM_SIGN_UP_STEP, nextStep, userId diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/testUtil/MockStates.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/testUtil/MockStates.kt index 2c2bda98..2422c3b2 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/testUtil/MockStates.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/testUtil/MockStates.kt @@ -21,7 +21,7 @@ import com.amplifyframework.auth.MFAType import com.amplifyframework.auth.result.AuthWebAuthnCredential import com.amplifyframework.ui.authenticator.auth.PasswordCriteria import com.amplifyframework.ui.authenticator.auth.SignInMethod -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep import com.amplifyframework.ui.authenticator.forms.FormData import com.amplifyframework.ui.authenticator.mockAuthCodeDeliveryDetails @@ -46,9 +46,14 @@ internal fun mockSignInState() = SignInStateImpl( onMoveTo = { } ) -internal fun mockSignUpState() = SignUpStateImpl( - signInMethod = SignInMethod.Username, - signUpAttributes = listOf(AuthUserAttributeKey.email()), +internal fun mockSignUpState( + signInMethod: SignInMethod = SignInMethod.Username, + signUpAttributes: List = listOf(AuthUserAttributeKey.email()), + requirePasswordField: Boolean = true +) = SignUpStateImpl( + signInMethod = signInMethod, + signUpAttributes = signUpAttributes, + requirePasswordField = requirePasswordField, passwordCriteria = PasswordCriteria(8, false, false, false, false), signUpForm = FormData(emptyList()), onSubmit = { _, _, _ -> }, diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactorTest.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactorTest.kt index d38a9df3..618b35a1 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactorTest.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignInSelectAuthFactorTest.kt @@ -16,7 +16,7 @@ package com.amplifyframework.ui.authenticator.ui import com.amplifyframework.ui.authenticator.auth.SignInMethod -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor import com.amplifyframework.ui.authenticator.testUtil.AuthenticatorUiTest import com.amplifyframework.ui.authenticator.testUtil.mockSignInSelectAuthFactorState import com.amplifyframework.ui.authenticator.ui.robots.signInSelectAuthFactor diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignUpTest.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignUpTest.kt index a1256705..e0dc01ad 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignUpTest.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/SignUpTest.kt @@ -17,6 +17,8 @@ package com.amplifyframework.ui.authenticator.ui import androidx.compose.ui.autofill.AutofillManager import androidx.compose.ui.autofill.ContentType +import com.amplifyframework.auth.AuthUserAttributeKey +import com.amplifyframework.ui.authenticator.auth.SignInMethod import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep import com.amplifyframework.ui.authenticator.forms.FieldError import com.amplifyframework.ui.authenticator.forms.FieldKey @@ -193,4 +195,26 @@ class SignUpTest : AuthenticatorUiTest() { } state.form.setFieldError(FieldKey.Email, FieldError.InvalidFormat) } + + @Test + @ScreenshotTest + fun `passwordless with email`() { + val state = mockSignUpState(signInMethod = SignInMethod.Email, requirePasswordField = false) + setContent { + SignUp(state = state) + } + } + + @Test + @ScreenshotTest + fun `passwordless with username`() { + val state = mockSignUpState( + signInMethod = SignInMethod.Username, + signUpAttributes = listOf(AuthUserAttributeKey.email()), + requirePasswordField = false + ) + setContent { + SignUp(state = state) + } + } } diff --git a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/robots/SignInSelectAuthFactorRobot.kt b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/robots/SignInSelectAuthFactorRobot.kt index 143851b4..23341a3e 100644 --- a/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/robots/SignInSelectAuthFactorRobot.kt +++ b/authenticator/src/test/java/com/amplifyframework/ui/authenticator/ui/robots/SignInSelectAuthFactorRobot.kt @@ -18,7 +18,7 @@ package com.amplifyframework.ui.authenticator.ui.robots import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule -import com.amplifyframework.ui.authenticator.enums.AuthFactor +import com.amplifyframework.ui.authenticator.data.AuthFactor import com.amplifyframework.ui.authenticator.forms.FieldKey import com.amplifyframework.ui.authenticator.ui.TestTags import com.amplifyframework.ui.authenticator.ui.testTag diff --git a/authenticator/src/test/screenshots/SignUpTest_passwordless-with-email.png b/authenticator/src/test/screenshots/SignUpTest_passwordless-with-email.png new file mode 100644 index 00000000..7b82be8e Binary files /dev/null and b/authenticator/src/test/screenshots/SignUpTest_passwordless-with-email.png differ diff --git a/authenticator/src/test/screenshots/SignUpTest_passwordless-with-username.png b/authenticator/src/test/screenshots/SignUpTest_passwordless-with-username.png new file mode 100644 index 00000000..56bb9b62 Binary files /dev/null and b/authenticator/src/test/screenshots/SignUpTest_passwordless-with-username.png differ diff --git a/samples/authenticator/app/.gitignore b/samples/authenticator/app/.gitignore index 7915f57c..f103701d 100644 --- a/samples/authenticator/app/.gitignore +++ b/samples/authenticator/app/.gitignore @@ -1,4 +1,4 @@ /build /buildNative **/awsconfiguration.json -**/amplifyconfiguration.json \ No newline at end of file +**/amplifyconfiguration**.json \ No newline at end of file