Skip to content

Commit 8ef7b8a

Browse files
authored
feat(authenticator): Add support for Compose autofill (#252)
1 parent 6abe43e commit 8ef7b8a

File tree

48 files changed

+413
-60
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+413
-60
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.amplifyframework.ui.authenticator.locals
2+
3+
import androidx.compose.runtime.compositionLocalOf
4+
import com.amplifyframework.ui.authenticator.enums.AuthenticatorStep
5+
6+
/**
7+
* This composition local supplies the current AuthenticatorStep. This allows descendant composables to tailor
8+
* their content to specific steps.
9+
*/
10+
internal val LocalAuthenticatorStep = compositionLocalOf<AuthenticatorStep> { AuthenticatorStep.Loading }

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import androidx.compose.foundation.verticalScroll
3131
import androidx.compose.material3.SnackbarHost
3232
import androidx.compose.material3.SnackbarHostState
3333
import androidx.compose.runtime.Composable
34+
import androidx.compose.runtime.CompositionLocalProvider
3435
import androidx.compose.runtime.LaunchedEffect
3536
import androidx.compose.runtime.remember
3637
import androidx.compose.ui.Alignment
@@ -56,6 +57,7 @@ import com.amplifyframework.ui.authenticator.SignUpState
5657
import com.amplifyframework.ui.authenticator.SignedInState
5758
import com.amplifyframework.ui.authenticator.VerifyUserConfirmState
5859
import com.amplifyframework.ui.authenticator.VerifyUserState
60+
import com.amplifyframework.ui.authenticator.locals.LocalAuthenticatorStep
5961
import com.amplifyframework.ui.authenticator.rememberAuthenticatorState
6062
import com.amplifyframework.ui.authenticator.util.AuthenticatorMessage
6163

@@ -140,31 +142,35 @@ fun Authenticator(
140142
Column(
141143
modifier = Modifier.verticalScroll(rememberScrollState())
142144
) {
143-
headerContent()
144-
when (targetState) {
145-
is LoadingState -> loadingContent()
146-
is SignInState -> signInContent(targetState)
147-
is SignInConfirmMfaState -> signInConfirmMfaContent(targetState)
148-
is SignInConfirmCustomState -> signInConfirmCustomContent(targetState)
149-
is SignInConfirmNewPasswordState -> signInConfirmNewPasswordContent(
150-
targetState
151-
)
152-
is SignInConfirmTotpCodeState -> signInConfirmTotpCodeContent(targetState)
153-
is SignInContinueWithTotpSetupState -> signInContinueWithTotpSetupContent(targetState)
154-
is SignInContinueWithEmailSetupState -> signInContinueWithEmailSetupContent(targetState)
155-
is SignInContinueWithMfaSetupSelectionState ->
156-
signInContinueWithMfaSetupSelectionContent(targetState)
157-
is SignInContinueWithMfaSelectionState -> signInContinueWithMfaSelectionContent(targetState)
158-
is SignUpState -> signUpContent(targetState)
159-
is PasswordResetState -> passwordResetContent(targetState)
160-
is PasswordResetConfirmState -> passwordResetConfirmContent(targetState)
161-
is ErrorState -> errorContent(targetState)
162-
is SignUpConfirmState -> signUpConfirmContent(targetState)
163-
is VerifyUserState -> verifyUserContent(targetState)
164-
is VerifyUserConfirmState -> verifyUserConfirmContent(targetState)
165-
else -> Unit
145+
CompositionLocalProvider(LocalAuthenticatorStep provides targetState.step) {
146+
headerContent()
147+
when (targetState) {
148+
is LoadingState -> loadingContent()
149+
is SignInState -> signInContent(targetState)
150+
is SignInConfirmMfaState -> signInConfirmMfaContent(targetState)
151+
is SignInConfirmCustomState -> signInConfirmCustomContent(targetState)
152+
is SignInConfirmNewPasswordState -> signInConfirmNewPasswordContent(
153+
targetState
154+
)
155+
156+
is SignInConfirmTotpCodeState -> signInConfirmTotpCodeContent(targetState)
157+
is SignInContinueWithTotpSetupState -> signInContinueWithTotpSetupContent(targetState)
158+
is SignInContinueWithEmailSetupState -> signInContinueWithEmailSetupContent(targetState)
159+
is SignInContinueWithMfaSetupSelectionState ->
160+
signInContinueWithMfaSetupSelectionContent(targetState)
161+
162+
is SignInContinueWithMfaSelectionState -> signInContinueWithMfaSelectionContent(targetState)
163+
is SignUpState -> signUpContent(targetState)
164+
is PasswordResetState -> passwordResetContent(targetState)
165+
is PasswordResetConfirmState -> passwordResetConfirmContent(targetState)
166+
is ErrorState -> errorContent(targetState)
167+
is SignUpConfirmState -> signUpConfirmContent(targetState)
168+
is VerifyUserState -> verifyUserContent(targetState)
169+
is VerifyUserConfirmState -> verifyUserConfirmContent(targetState)
170+
else -> Unit
171+
}
172+
footerContent()
166173
}
167-
footerContent()
168174
}
169175
}
170176
SnackbarHost(hostState = snackbarState, modifier = Modifier.align(Alignment.BottomCenter))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal fun AuthenticatorForm(
4848
) {
4949
state.fields.values.forEach { field ->
5050
AuthenticatorField(
51-
modifier = Modifier.fillMaxWidth().testTag(field.config.key.toString()),
51+
modifier = Modifier.fillMaxWidth().testTag(field.config.key.testTag),
5252
fieldConfig = field.config,
5353
fieldState = field.state,
5454
formState = state

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
4040
import androidx.compose.ui.draw.clip
4141
import androidx.compose.ui.graphics.Color
4242
import androidx.compose.ui.unit.Dp
43+
import androidx.compose.ui.unit.IntOffset
4344
import androidx.compose.ui.unit.dp
4445

4546
/**
@@ -58,10 +59,7 @@ fun AuthenticatorLoading(modifier: Modifier = Modifier) {
5859
}
5960

6061
@Composable
61-
private fun LoadingDot(
62-
modifier: Modifier = Modifier,
63-
color: Color
64-
) {
62+
private fun LoadingDot(modifier: Modifier = Modifier, color: Color) {
6563
Box(
6664
modifier = modifier
6765
.clip(shape = CircleShape)
@@ -101,7 +99,7 @@ internal fun LoadingIndicator(
10199
modifier = Modifier
102100
.size(dotSize)
103101
.aspectRatio(1f)
104-
.offset(y = offset.dp)
102+
.offset { IntOffset(x = 0, y = offset.dp.roundToPx()) }
105103
)
106104
}
107105
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.amplifyframework.ui.authenticator.R
3636
import com.amplifyframework.ui.authenticator.forms.FieldConfig
3737
import com.amplifyframework.ui.authenticator.forms.MutablePasswordFieldState
3838
import com.amplifyframework.ui.authenticator.strings.StringResolver
39+
import com.amplifyframework.ui.authenticator.util.contentTypeForKey
3940

4041
@Composable
4142
internal fun PasswordInputField(
@@ -55,7 +56,7 @@ internal fun PasswordInputField(
5556
)
5657

5758
OutlinedTextField(
58-
modifier = modifier,
59+
modifier = modifier.contentTypeForKey(fieldConfig.key),
5960
enabled = enabled,
6061
value = fieldState.content,
6162
onValueChange = { fieldState.content = it },

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Composable
2424
import androidx.compose.runtime.rememberCoroutineScope
2525
import androidx.compose.ui.Alignment
2626
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.platform.LocalAutofillManager
28+
import androidx.compose.ui.platform.testTag
2729
import androidx.compose.ui.res.stringResource
2830
import androidx.compose.ui.unit.dp
2931
import com.amplifyframework.auth.AuthCodeDeliveryDetails
@@ -62,12 +64,18 @@ fun PasswordResetConfirm(
6264
}
6365

6466
@Composable
65-
fun PasswordResetConfirmFooter(
66-
state: PasswordResetConfirmState,
67-
modifier: Modifier = Modifier
68-
) {
67+
fun PasswordResetConfirmFooter(state: PasswordResetConfirmState, modifier: Modifier = Modifier) {
68+
// If we navigate away from the screen via some reason other that submitting the form then cancel any changes
69+
// in autofill manager so that user won't be prompted to update their saved password
70+
val autofillManager = LocalAutofillManager.current
6971
Column(modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
70-
TextButton(onClick = { state.moveTo(AuthenticatorStep.SignIn) }) {
72+
TextButton(
73+
modifier = Modifier.testTag(TestTags.BackToSignInButton),
74+
onClick = {
75+
autofillManager?.cancel()
76+
state.moveTo(AuthenticatorStep.SignIn)
77+
}
78+
) {
7179
Text(stringResource(R.string.amplify_ui_authenticator_button_back_to_signin))
7280
}
7381
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import com.amplifyframework.ui.authenticator.forms.FieldConfig
6464
import com.amplifyframework.ui.authenticator.forms.MutableFieldState
6565
import com.amplifyframework.ui.authenticator.strings.StringResolver
6666
import com.amplifyframework.ui.authenticator.util.Region
67+
import com.amplifyframework.ui.authenticator.util.contentTypeForKey
6768
import com.amplifyframework.ui.authenticator.util.regionList
6869
import com.amplifyframework.ui.authenticator.util.regionMap
6970
import java.util.Locale
@@ -102,7 +103,7 @@ internal fun PhoneInputField(
102103
}
103104

104105
OutlinedTextField(
105-
modifier = modifier,
106+
modifier = modifier.contentTypeForKey(fieldConfig.key),
106107
enabled = enabled,
107108
value = state.number,
108109
onValueChange = { state.number = it },

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.material3.TextButton
2525
import androidx.compose.runtime.Composable
2626
import androidx.compose.runtime.rememberCoroutineScope
2727
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.platform.LocalAutofillManager
2829
import androidx.compose.ui.platform.testTag
2930
import androidx.compose.ui.res.stringResource
3031
import androidx.compose.ui.unit.dp
@@ -61,20 +62,31 @@ fun SignIn(
6162
}
6263

6364
@Composable
64-
fun SignInFooter(
65-
state: SignInState,
66-
modifier: Modifier = Modifier,
67-
hideSignUp: Boolean = false
68-
) {
65+
fun SignInFooter(state: SignInState, modifier: Modifier = Modifier, hideSignUp: Boolean = false) {
66+
// If we navigate away from the screen via some reason other that submitting the form then cancel any changes
67+
// in autofill manager so that user won't be prompted to update their saved password
68+
val autofillManager = LocalAutofillManager.current
6969
Row(
7070
modifier = modifier.fillMaxWidth(),
7171
horizontalArrangement = Arrangement.SpaceEvenly
7272
) {
73-
TextButton(onClick = { state.moveTo(AuthenticatorStep.PasswordReset) }) {
73+
TextButton(
74+
modifier = Modifier.testTag(TestTags.ForgotPasswordButton),
75+
onClick = {
76+
autofillManager?.cancel()
77+
state.moveTo(AuthenticatorStep.PasswordReset)
78+
}
79+
) {
7480
Text(stringResource(R.string.amplify_ui_authenticator_button_forgot_password))
7581
}
7682
if (!hideSignUp) {
77-
TextButton(onClick = { state.moveTo(AuthenticatorStep.SignUp) }) {
83+
TextButton(
84+
modifier = Modifier.testTag(TestTags.CreateAccountButton),
85+
onClick = {
86+
autofillManager?.cancel()
87+
state.moveTo(AuthenticatorStep.SignUp)
88+
}
89+
) {
7890
Text(stringResource(R.string.amplify_ui_authenticator_button_signup))
7991
}
8092
}

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable
2525
import androidx.compose.runtime.rememberCoroutineScope
2626
import androidx.compose.ui.Alignment
2727
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.platform.LocalAutofillManager
2829
import androidx.compose.ui.platform.testTag
2930
import androidx.compose.ui.res.stringResource
3031
import androidx.compose.ui.unit.dp
@@ -59,12 +60,18 @@ fun SignUp(
5960
}
6061

6162
@Composable
62-
fun SignUpFooter(
63-
state: SignUpState,
64-
modifier: Modifier = Modifier
65-
) {
63+
fun SignUpFooter(state: SignUpState, modifier: Modifier = Modifier) {
64+
// If we navigate away from the screen via some reason other that submitting the form then cancel any changes
65+
// in autofill manager so that user won't be prompted to update their saved password
66+
val autofillManager = LocalAutofillManager.current
6667
Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
67-
TextButton(onClick = { state.moveTo(AuthenticatorStep.SignIn) }) {
68+
TextButton(
69+
modifier = Modifier.testTag(TestTags.BackToSignInButton),
70+
onClick = {
71+
autofillManager?.cancel()
72+
state.moveTo(AuthenticatorStep.SignIn)
73+
}
74+
) {
6875
Text(stringResource(R.string.amplify_ui_authenticator_button_back_to_signin))
6976
}
7077
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,22 @@
1515

1616
package com.amplifyframework.ui.authenticator.ui
1717

18+
import com.amplifyframework.ui.authenticator.forms.FieldKey
19+
20+
@Suppress("ConstPropertyName")
1821
internal object TestTags {
1922
const val SignInConfirmButton = "SignInConfirmButton"
2023
const val BackToSignInButton = "BackToSignInButton"
2124
const val CopyKeyButton = "CopyKeyButton"
2225
const val SignInButton = "SignInButton"
2326
const val SignUpButton = "SignUpButton"
27+
const val ForgotPasswordButton = "ForgotPasswordButton"
28+
const val CreateAccountButton = "CreateAccountButton"
2429
const val PasswordResetButton = "PasswordResetButton"
2530
const val AuthenticatorTitle = "AuthenticatorTitle"
2631

2732
const val ShowPasswordIcon = "ShowPasswordIcon"
2833
}
34+
35+
internal val FieldKey.testTag: String
36+
get() = this.toString()

0 commit comments

Comments
 (0)