@@ -19,6 +19,7 @@ import android.app.Activity
1919import android.app.Application
2020import androidx.lifecycle.AndroidViewModel
2121import androidx.lifecycle.viewModelScope
22+ import com.amplifyframework.AmplifyException
2223import com.amplifyframework.auth.AuthChannelEventName
2324import com.amplifyframework.auth.AuthException
2425import com.amplifyframework.auth.AuthUser
@@ -34,6 +35,7 @@ import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterExce
3435import com.amplifyframework.auth.cognito.exceptions.service.InvalidPasswordException
3536import com.amplifyframework.auth.cognito.exceptions.service.LimitExceededException
3637import com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException
38+ import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException
3739import com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException
3840import com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException
3941import com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException
@@ -87,12 +89,15 @@ import com.amplifyframework.ui.authenticator.util.InvalidLoginMessage
8789import com.amplifyframework.ui.authenticator.util.LimitExceededMessage
8890import com.amplifyframework.ui.authenticator.util.MissingConfigurationException
8991import com.amplifyframework.ui.authenticator.util.NetworkErrorMessage
92+ import com.amplifyframework.ui.authenticator.util.PasskeyCreationFailedMessage
93+ import com.amplifyframework.ui.authenticator.util.PasskeyPromptCheck
9094import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
9195import com.amplifyframework.ui.authenticator.util.RealAuthProvider
9296import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
9397import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
9498import com.amplifyframework.ui.authenticator.util.authFlow
9599import com.amplifyframework.ui.authenticator.util.callingActivity
100+ import com.amplifyframework.ui.authenticator.util.getOrDefault
96101import com.amplifyframework.ui.authenticator.util.isAuthFlowSessionExpiredError
97102import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
98103import com.amplifyframework.ui.authenticator.util.preferredFirstFactor
@@ -107,8 +112,11 @@ import kotlinx.coroutines.launch
107112import kotlinx.coroutines.withContext
108113import org.jetbrains.annotations.VisibleForTesting
109114
110- internal class AuthenticatorViewModel (application : Application , private val authProvider : AuthProvider ) :
111- AndroidViewModel (application) {
115+ internal class AuthenticatorViewModel (
116+ application : Application ,
117+ private val authProvider : AuthProvider ,
118+ private val passkeyCheck : PasskeyPromptCheck = PasskeyPromptCheck (authProvider)
119+ ) : AndroidViewModel(application) {
112120
113121 // Constructor for compose viewModels provider
114122 constructor (application: Application ) : this (application, RealAuthProvider ())
@@ -140,13 +148,14 @@ internal class AuthenticatorViewModel(application: Application, private val auth
140148
141149 // The current activity is used for WebAuthn sign-in when using passwordless functionality
142150 private var activityReference: WeakReference <Activity > = WeakReference (null )
143- var activity: Activity ?
151+ private var activity: Activity ?
144152 get() = activityReference.get()
145153 set(value) {
146154 activityReference = WeakReference (value)
147155 }
148156
149- fun start (configuration : AuthenticatorConfiguration ) {
157+ fun start (configuration : AuthenticatorConfiguration , activity : Activity ? ) {
158+ this .activity = activity
150159 if (::configuration.isInitialized) {
151160 return
152161 }
@@ -216,7 +225,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
216225 suspend fun signUp (username : String , password : String? , attributes : List <AuthUserAttribute >) {
217226 viewModelScope.launch {
218227 val options = AuthSignUpOptions .builder().userAttributes(attributes).build()
219- val info = UserInfo (username = username, password = password, signInSource = SignInSource .SignUp )
228+ val info = UserInfo (username = username, password = password, signInSource = SignInSource .AutoSignIn )
220229
221230 when (val result = authProvider.signUp(username, password, options)) {
222231 is AmplifyResult .Error -> handleSignUpFailure(result.error)
@@ -350,6 +359,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
350359 // UserNotConfirmed and PasswordResetRequired are special cases where we need
351360 // to enter different flows
352361 when (error) {
362+ is UserCancelledException -> Unit // This is an expected error, user can simply retry
353363 is UserNotConfirmedException -> handleUnconfirmedSignIn(info)
354364 is PasswordResetRequiredException -> handleResetRequiredSignIn(info.username)
355365 is NotAuthorizedException -> sendMessage(InvalidLoginMessage (error))
@@ -471,7 +481,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
471481
472482 private suspend fun handleSignInSuccess (info : UserInfo , result : AuthSignInResult ) {
473483 when (val nextStep = result.nextStep.signInStep) {
474- AuthSignInStep .DONE -> checkVerificationMechanisms( )
484+ AuthSignInStep .DONE -> checkForPasskeyPrompt(info )
475485 AuthSignInStep .CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE ,
476486 AuthSignInStep .CONFIRM_SIGN_IN_WITH_OTP -> moveTo(
477487 stateFactory.newSignInMfaState(
@@ -537,6 +547,53 @@ internal class AuthenticatorViewModel(application: Application, private val auth
537547 }
538548 }
539549
550+ private suspend fun checkForPasskeyPrompt (info : UserInfo ) {
551+ if (passkeyCheck.shouldPromptForPasskey(userInfo = info, config = configuration)) {
552+ moveTo(
553+ stateFactory.newPasskeyPromptState(
554+ onSubmit = {
555+ val activityRef = activity
556+ if (activityRef == null ) {
557+ // This shouldn't happen, it indicates a bug. If it does the user can retry or choose to
558+ // skip
559+ sendMessage(
560+ UnknownErrorMessage (
561+ AuthException (
562+ message = " Missing activity reference" ,
563+ recoverySuggestion = AmplifyException .REPORT_BUG_TO_AWS_SUGGESTION
564+ )
565+ )
566+ )
567+ } else {
568+ createPasskey(activityRef)
569+ }
570+ },
571+ onSkip = ::checkVerificationMechanisms
572+ )
573+ )
574+ } else {
575+ checkVerificationMechanisms()
576+ }
577+ }
578+
579+ private suspend fun createPasskey (activityRef : Activity ) {
580+ when (val result = authProvider.createPasskey(activityRef)) {
581+ is AmplifyResult .Error -> when (result.error) {
582+ is UserCancelledException -> Unit // This is expected, user can retry or skip
583+ else -> sendMessage(PasskeyCreationFailedMessage (result.error)) // User can retry/skip
584+ }
585+ is AmplifyResult .Success -> {
586+ val passkeys = authProvider.getPasskeys().getOrDefault { emptyList() }
587+ moveTo(
588+ stateFactory.newPasskeyCreatedState(
589+ passkeys = passkeys,
590+ onDone = ::checkVerificationMechanisms
591+ )
592+ )
593+ }
594+ }
595+ }
596+
540597 private suspend fun checkVerificationMechanisms () {
541598 val mechanisms = authConfiguration.verificationMechanisms
542599 if (mechanisms.isEmpty()) {
0 commit comments