Skip to content

Commit c751aec

Browse files
committed
Hook up passkey prompt
1 parent 73f7de9 commit c751aec

27 files changed

+256
-119
lines changed

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorState.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import androidx.compose.runtime.mutableStateOf
2222
import androidx.compose.runtime.remember
2323
import androidx.compose.runtime.rememberCoroutineScope
2424
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.platform.LocalContext
2526
import androidx.lifecycle.viewmodel.compose.viewModel
2627
import com.amplifyframework.ui.authenticator.data.AuthenticationFlow
2728
import com.amplifyframework.ui.authenticator.enums.AuthenticatorInitialStep
2829
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
2930
import com.amplifyframework.ui.authenticator.forms.SignUpFormBuilder
3031
import com.amplifyframework.ui.authenticator.options.TotpOptions
3132
import com.amplifyframework.ui.authenticator.util.AuthenticatorMessage
33+
import com.amplifyframework.ui.authenticator.util.findActivity
3234
import kotlinx.coroutines.flow.Flow
3335
import kotlinx.coroutines.flow.launchIn
3436
import kotlinx.coroutines.flow.onEach
@@ -52,6 +54,8 @@ fun rememberAuthenticatorState(
5254
): AuthenticatorState {
5355
val viewModel = viewModel<AuthenticatorViewModel>()
5456
val scope = rememberCoroutineScope()
57+
val context = LocalContext.current
58+
5559
return remember {
5660
val configuration = AuthenticatorConfiguration(
5761
initialStep = initialStep,
@@ -60,7 +64,7 @@ fun rememberAuthenticatorState(
6064
authenticationFlow = authenticationFlow
6165
)
6266

63-
viewModel.start(configuration)
67+
viewModel.start(configuration, context.findActivity())
6468
AuthenticatorStateImpl(viewModel).also { state ->
6569
viewModel.stepState.onEach { state.stepState = it }.launchIn(scope)
6670
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorStepState.kt

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ interface AuthenticatorStepState {
4040
val step: AuthenticatorStep
4141
}
4242

43+
/**
44+
* A state holder for the UI that has multiple possible actions that may be in progress.
45+
*/
46+
@Stable
47+
interface AuthenticatorActionState<T> {
48+
/**
49+
* The action in progress, or null if state is idle
50+
*/
51+
val action: T?
52+
}
53+
4354
/**
4455
* The Authenticator is loading the current state of the user's Auth session.
4556
*/
@@ -99,7 +110,17 @@ interface SignInState : AuthenticatorStepState {
99110
* The user has entered their username and must select the authentication factor they'd like to use to sign in
100111
*/
101112
@Stable
102-
interface SignInSelectAuthFactorState : AuthenticatorStepState {
113+
interface SignInSelectAuthFactorState :
114+
AuthenticatorStepState,
115+
AuthenticatorActionState<SignInSelectAuthFactorState.Action> {
116+
117+
sealed interface Action {
118+
/**
119+
* User has selected an auth factor
120+
*/
121+
data class SelectFactor(val factor: AuthFactor) : Action
122+
}
123+
103124
/**
104125
* The input form state holder for this step.
105126
*/
@@ -115,11 +136,6 @@ interface SignInSelectAuthFactorState : AuthenticatorStepState {
115136
*/
116137
val availableAuthFactors: Set<AuthFactor>
117138

118-
/**
119-
* The factor the user selected and is currently being processed
120-
*/
121-
val selectedFactor: AuthFactor?
122-
123139
/**
124140
* Move the user to a different [AuthenticatorInitialStep].
125141
*/
@@ -530,7 +546,21 @@ interface VerifyUserConfirmState : AuthenticatorStepState {
530546
* via biometrics
531547
*/
532548
@Stable
533-
interface PasskeyCreationPromptState : AuthenticatorStepState {
549+
interface PasskeyCreationPromptState :
550+
AuthenticatorStepState,
551+
AuthenticatorActionState<PasskeyCreationPromptState.Action> {
552+
sealed interface Action {
553+
/**
554+
* User is creating a passkey
555+
*/
556+
class CreatePasskey : Action
557+
558+
/**
559+
* User has selected the Skip button
560+
*/
561+
class Skip : Action
562+
}
563+
534564
/**
535565
* Create a passkey
536566
*/
@@ -546,7 +576,16 @@ interface PasskeyCreationPromptState : AuthenticatorStepState {
546576
* The user is being shown a confirmation screen after creating a passkey
547577
*/
548578
@Stable
549-
interface PasskeyCreatedState : AuthenticatorStepState {
579+
interface PasskeyCreatedState :
580+
AuthenticatorStepState,
581+
AuthenticatorActionState<PasskeyCreatedState.Action> {
582+
sealed interface Action {
583+
/**
584+
* User has selected the Done button
585+
*/
586+
class Done : Action
587+
}
588+
550589
/**
551590
* A list of existing passkeys for this user, including the one they've just created
552591
*/

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import com.amplifyframework.auth.cognito.exceptions.service.InvalidParameterExce
3434
import com.amplifyframework.auth.cognito.exceptions.service.InvalidPasswordException
3535
import com.amplifyframework.auth.cognito.exceptions.service.LimitExceededException
3636
import com.amplifyframework.auth.cognito.exceptions.service.PasswordResetRequiredException
37+
import com.amplifyframework.auth.cognito.exceptions.service.UserCancelledException
3738
import com.amplifyframework.auth.cognito.exceptions.service.UserNotConfirmedException
3839
import com.amplifyframework.auth.cognito.exceptions.service.UserNotFoundException
3940
import com.amplifyframework.auth.cognito.exceptions.service.UsernameExistsException
@@ -87,12 +88,15 @@ import com.amplifyframework.ui.authenticator.util.InvalidLoginMessage
8788
import com.amplifyframework.ui.authenticator.util.LimitExceededMessage
8889
import com.amplifyframework.ui.authenticator.util.MissingConfigurationException
8990
import com.amplifyframework.ui.authenticator.util.NetworkErrorMessage
91+
import com.amplifyframework.ui.authenticator.util.PasskeyCreationFailedMessage
92+
import com.amplifyframework.ui.authenticator.util.PasskeyPromptCheck
9093
import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
9194
import com.amplifyframework.ui.authenticator.util.RealAuthProvider
9295
import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
9396
import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
9497
import com.amplifyframework.ui.authenticator.util.authFlow
9598
import com.amplifyframework.ui.authenticator.util.callingActivity
99+
import com.amplifyframework.ui.authenticator.util.getOrDefault
96100
import com.amplifyframework.ui.authenticator.util.isAuthFlowSessionExpiredError
97101
import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
98102
import com.amplifyframework.ui.authenticator.util.preferredFirstFactor
@@ -107,8 +111,11 @@ import kotlinx.coroutines.launch
107111
import kotlinx.coroutines.withContext
108112
import org.jetbrains.annotations.VisibleForTesting
109113

110-
internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) :
111-
AndroidViewModel(application) {
114+
internal class AuthenticatorViewModel(
115+
application: Application,
116+
private val authProvider: AuthProvider,
117+
private val passkeyCheck: PasskeyPromptCheck = PasskeyPromptCheck(authProvider)
118+
) : AndroidViewModel(application) {
112119

113120
// Constructor for compose viewModels provider
114121
constructor(application: Application) : this(application, RealAuthProvider())
@@ -140,13 +147,14 @@ internal class AuthenticatorViewModel(application: Application, private val auth
140147

141148
// The current activity is used for WebAuthn sign-in when using passwordless functionality
142149
private var activityReference: WeakReference<Activity> = WeakReference(null)
143-
var activity: Activity?
150+
private var activity: Activity?
144151
get() = activityReference.get()
145152
set(value) {
146153
activityReference = WeakReference(value)
147154
}
148155

149-
fun start(configuration: AuthenticatorConfiguration) {
156+
fun start(configuration: AuthenticatorConfiguration, activity: Activity?) {
157+
this.activity = activity
150158
if (::configuration.isInitialized) {
151159
return
152160
}
@@ -216,7 +224,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
216224
suspend fun signUp(username: String, password: String?, attributes: List<AuthUserAttribute>) {
217225
viewModelScope.launch {
218226
val options = AuthSignUpOptions.builder().userAttributes(attributes).build()
219-
val info = UserInfo(username = username, password = password, signInSource = SignInSource.SignUp)
227+
val info = UserInfo(username = username, password = password, signInSource = SignInSource.AutoSignIn)
220228

221229
when (val result = authProvider.signUp(username, password, options)) {
222230
is AmplifyResult.Error -> handleSignUpFailure(result.error)
@@ -471,7 +479,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
471479

472480
private suspend fun handleSignInSuccess(info: UserInfo, result: AuthSignInResult) {
473481
when (val nextStep = result.nextStep.signInStep) {
474-
AuthSignInStep.DONE -> checkVerificationMechanisms()
482+
AuthSignInStep.DONE -> checkForPasskeyPrompt(info)
475483
AuthSignInStep.CONFIRM_SIGN_IN_WITH_SMS_MFA_CODE,
476484
AuthSignInStep.CONFIRM_SIGN_IN_WITH_OTP -> moveTo(
477485
stateFactory.newSignInMfaState(
@@ -537,6 +545,38 @@ internal class AuthenticatorViewModel(application: Application, private val auth
537545
}
538546
}
539547

548+
private suspend fun checkForPasskeyPrompt(info: UserInfo) {
549+
val activityRef = activity
550+
if (activityRef != null && passkeyCheck.shouldPromptForPasskey(userInfo = info, config = configuration)) {
551+
moveTo(
552+
stateFactory.newPasskeyPromptState(
553+
onSubmit = { createPasskey(activityRef) },
554+
onSkip = ::checkVerificationMechanisms
555+
)
556+
)
557+
} else {
558+
checkVerificationMechanisms()
559+
}
560+
}
561+
562+
private suspend fun createPasskey(activityRef: Activity) {
563+
when (val result = authProvider.createPasskey(activityRef)) {
564+
is AmplifyResult.Error -> when (result.error) {
565+
is UserCancelledException -> Unit // This is expected, user can retry or skip
566+
else -> sendMessage(PasskeyCreationFailedMessage(result.error)) // User can retry/skip
567+
}
568+
is AmplifyResult.Success -> {
569+
val passkeys = authProvider.getPasskeys().getOrDefault { emptyList() }
570+
moveTo(
571+
stateFactory.newPasskeyCreatedState(
572+
passkeys = passkeys,
573+
onDone = ::checkVerificationMechanisms
574+
)
575+
)
576+
}
577+
}
578+
}
579+
540580
private suspend fun checkVerificationMechanisms() {
541581
val mechanisms = authConfiguration.verificationMechanisms
542582
if (mechanisms.isEmpty()) {

authenticator/src/main/java/com/amplifyframework/ui/authenticator/enums/SignInSource.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,5 @@ internal enum class SignInSource {
55
SignIn,
66

77
// Automatic sign in after completing sign up
8-
SignUp,
9-
10-
// Signed in outside of Authenticator
11-
External
8+
AutoSignIn
129
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.amplifyframework.ui.authenticator.states
2+
3+
import com.amplifyframework.ui.authenticator.AuthenticatorActionState
4+
5+
internal interface MutableActionState<T> : AuthenticatorActionState<T> {
6+
override var action: T?
7+
}
8+
9+
internal inline fun <T> MutableActionState<T>.withAction(action: T, func: () -> Unit) {
10+
this.action = action
11+
func()
12+
this.action = null
13+
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package com.amplifyframework.ui.authenticator.states
22

3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
36
import com.amplifyframework.auth.result.AuthWebAuthnCredential
47
import com.amplifyframework.ui.authenticator.PasskeyCreatedState
58
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
69

710
internal class PasskeyCreatedStateImpl(
811
override val passkeys: List<AuthWebAuthnCredential>,
912
private val onDone: suspend () -> Unit
10-
) : PasskeyCreatedState {
13+
) : PasskeyCreatedState,
14+
MutableActionState<PasskeyCreatedState.Action> {
1115
override val step: AuthenticatorStep = AuthenticatorStep.PasskeyCreated
1216

13-
override suspend fun done() = onDone()
17+
override var action: PasskeyCreatedState.Action? by mutableStateOf(null)
18+
19+
override suspend fun done() = withAction(PasskeyCreatedState.Action.Done()) {
20+
onDone()
21+
}
1422
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
package com.amplifyframework.ui.authenticator.states
22

3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
36
import com.amplifyframework.ui.authenticator.PasskeyCreationPromptState
7+
import com.amplifyframework.ui.authenticator.PasskeyCreationPromptState.Action
48
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
59
import kotlinx.coroutines.sync.Mutex
610
import kotlinx.coroutines.sync.withLock
711

812
class PasskeyCreationPromptStateImpl(private val onSubmit: suspend () -> Unit, private val onSkip: suspend () -> Unit) :
9-
PasskeyCreationPromptState {
13+
PasskeyCreationPromptState,
14+
MutableActionState<Action> {
1015
private val mutex = Mutex()
1116

12-
override suspend fun createPasskey() {
17+
override val step = AuthenticatorStep.PasskeyCreationPrompt
18+
19+
override var action: Action? by mutableStateOf(null)
20+
21+
override suspend fun createPasskey() = withAction(Action.CreatePasskey()) {
1322
mutex.withLock {
1423
onSubmit()
1524
}
1625
}
1726

18-
override suspend fun skip() = onSkip()
19-
20-
override val step = AuthenticatorStep.PasskeyCreationPrompt
27+
override suspend fun skip() = withAction(Action.Skip()) { onSkip() }
2128
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/SignInSelectAuthFactorStateImpl.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue
44
import androidx.compose.runtime.mutableStateOf
55
import androidx.compose.runtime.setValue
66
import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState
7+
import com.amplifyframework.ui.authenticator.SignInSelectAuthFactorState.Action
78
import com.amplifyframework.ui.authenticator.auth.SignInMethod
89
import com.amplifyframework.ui.authenticator.data.AuthFactor
910
import com.amplifyframework.ui.authenticator.data.containsPassword
@@ -17,10 +18,11 @@ internal class SignInSelectAuthFactorStateImpl(
1718
private val onSubmit: suspend (authFactor: AuthFactor) -> Unit,
1819
private val onMoveTo: (step: AuthenticatorInitialStep) -> Unit
1920
) : BaseStateImpl(),
20-
SignInSelectAuthFactorState {
21+
SignInSelectAuthFactorState,
22+
MutableActionState<Action> {
2123
override val step: AuthenticatorStep = AuthenticatorStep.SignInSelectAuthFactor
2224

23-
override var selectedFactor: AuthFactor? by mutableStateOf(null)
25+
override var action: Action? by mutableStateOf(null)
2426

2527
init {
2628
if (availableAuthFactors.containsPassword()) {
@@ -30,15 +32,13 @@ internal class SignInSelectAuthFactorStateImpl(
3032

3133
override fun moveTo(step: AuthenticatorInitialStep) = onMoveTo(step)
3234

33-
override suspend fun select(authFactor: AuthFactor) {
35+
override suspend fun select(authFactor: AuthFactor) = withAction(Action.SelectFactor(authFactor)) {
3436
// Clear errors
3537
form.fields.values.forEach { it.state.error = null }
3638

37-
selectedFactor = authFactor
3839
form.enabled = false
3940
onSubmit(authFactor)
4041
form.enabled = true
41-
selectedFactor = null
4242
}
4343
}
4444

authenticator/src/main/java/com/amplifyframework/ui/authenticator/states/StepStateFactory.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.amplifyframework.auth.AuthUserAttribute
2121
import com.amplifyframework.auth.AuthUserAttributeKey
2222
import com.amplifyframework.auth.MFAType
2323
import com.amplifyframework.auth.result.AuthSignOutResult
24+
import com.amplifyframework.auth.result.AuthWebAuthnCredential
2425
import com.amplifyframework.ui.authenticator.AuthenticatorConfiguration
2526
import com.amplifyframework.ui.authenticator.auth.AmplifyAuthConfiguration
2627
import com.amplifyframework.ui.authenticator.data.AuthFactor
@@ -196,4 +197,16 @@ internal class StepStateFactory(
196197
onResendCode = onResendCode,
197198
onSkip = onSkip
198199
)
200+
201+
fun newPasskeyPromptState(onSubmit: suspend () -> Unit, onSkip: suspend () -> Unit) =
202+
PasskeyCreationPromptStateImpl(
203+
onSubmit = onSubmit,
204+
onSkip = onSkip
205+
)
206+
207+
fun newPasskeyCreatedState(passkeys: List<AuthWebAuthnCredential>, onDone: suspend () -> Unit) =
208+
PasskeyCreatedStateImpl(
209+
passkeys = passkeys,
210+
onDone = onDone
211+
)
199212
}

0 commit comments

Comments
 (0)