Skip to content

Commit 3cfde41

Browse files
authored
feat: AuthTextField (#2231)
* feat: AuthMethodPicker, logo and provider theme style * chore: organize folder structure * feat: TOS and PP footer, ui tests for AuthMethodPicker * chore: tests folder structure * chore: use version catalog for compose deps * feat: AuthTextField with validation * test: AuthTextField and field validations * chore: update doc comments * refactor: remove libs.versions.toml catalog file
1 parent 4d1aefd commit 3cfde41

File tree

6 files changed

+658
-14
lines changed

6 files changed

+658
-14
lines changed

auth/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
id("com.android.library")
55
id("com.vanniktech.maven.publish")
66
id("org.jetbrains.kotlin.android")
7-
alias(libs.plugins.compose.compiler)
7+
id("org.jetbrains.kotlin.plugin.compose") version Config.kotlinVersion
88
}
99

1010
android {
@@ -84,6 +84,7 @@ dependencies {
8484
implementation(Config.Libs.Androidx.Compose.activityCompose)
8585
implementation(Config.Libs.Androidx.materialDesign)
8686
implementation(Config.Libs.Androidx.activity)
87+
implementation(Config.Libs.Androidx.Compose.materialIconsExtended)
8788
// The new activity result APIs force us to include Fragment 1.3.0
8889
// See https://issuetracker.google.com/issues/152554847
8990
implementation(Config.Libs.Androidx.fragment)
@@ -115,7 +116,7 @@ dependencies {
115116
testImplementation(Config.Libs.Test.robolectric)
116117
testImplementation(Config.Libs.Test.kotlinReflect)
117118
testImplementation(Config.Libs.Provider.facebook)
118-
testImplementation(libs.androidx.ui.test.junit4)
119+
testImplementation(Config.Libs.Test.composeUiTestJunit4)
119120

120121
debugImplementation(project(":internal:lintchecks"))
121122
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.firebase.ui.auth.compose.ui.components
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.height
8+
import androidx.compose.foundation.text.KeyboardActions
9+
import androidx.compose.foundation.text.KeyboardOptions
10+
import androidx.compose.material.icons.Icons
11+
import androidx.compose.material.icons.filled.Email
12+
import androidx.compose.material.icons.filled.Lock
13+
import androidx.compose.material.icons.filled.Visibility
14+
import androidx.compose.material.icons.filled.VisibilityOff
15+
import androidx.compose.material3.Icon
16+
import androidx.compose.material3.IconButton
17+
import androidx.compose.material3.Text
18+
import androidx.compose.material3.TextField
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.runtime.getValue
21+
import androidx.compose.runtime.mutableStateOf
22+
import androidx.compose.runtime.remember
23+
import androidx.compose.runtime.setValue
24+
import androidx.compose.ui.Alignment
25+
import androidx.compose.ui.Modifier
26+
import androidx.compose.ui.platform.LocalContext
27+
import androidx.compose.ui.text.input.PasswordVisualTransformation
28+
import androidx.compose.ui.text.input.VisualTransformation
29+
import androidx.compose.ui.tooling.preview.Preview
30+
import androidx.compose.ui.unit.dp
31+
import com.firebase.ui.auth.compose.configuration.PasswordRule
32+
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
33+
import com.firebase.ui.auth.compose.configuration.validators.EmailValidator
34+
import com.firebase.ui.auth.compose.configuration.validators.FieldValidator
35+
import com.firebase.ui.auth.compose.configuration.validators.PasswordValidator
36+
37+
/**
38+
* A customizable input field with built-in validation display.
39+
*
40+
* **Example usage:**
41+
* ```kotlin
42+
* val emailTextValue = remember { mutableStateOf("") }
43+
*
44+
* val emailValidator = remember {
45+
* EmailValidator(stringProvider = DefaultAuthUIStringProvider(context))
46+
* }
47+
*
48+
* AuthTextField(
49+
* value = emailTextValue,
50+
* onValueChange = { emailTextValue.value = it },
51+
* label = {
52+
* Text("Email")
53+
* },
54+
* validator = emailValidator
55+
* )
56+
* ```
57+
*
58+
* @param modifier A modifier for the field.
59+
* @param value The current value of the text field.
60+
* @param onValueChange A callback when the value changes.
61+
* @param label The label for the text field.
62+
* @param enabled If the field is enabled.
63+
* @param isError Manually set the error state.
64+
* @param errorMessage A custom error message to display.
65+
* @param validator A validator to automatically handle error state and messages.
66+
* @param keyboardOptions Keyboard options for the field.
67+
* @param keyboardActions Keyboard actions for the field.
68+
* @param visualTransformation Visual transformation for the input (e.g., password).
69+
* @param leadingIcon An optional icon to display at the start of the field.
70+
* @param trailingIcon An optional icon to display at the start of the field.
71+
*/
72+
@Composable
73+
fun AuthTextField(
74+
modifier: Modifier = Modifier,
75+
value: String,
76+
onValueChange: (String) -> Unit,
77+
label: @Composable (() -> Unit)? = null,
78+
enabled: Boolean = true,
79+
isError: Boolean? = null,
80+
errorMessage: String? = null,
81+
validator: FieldValidator? = null,
82+
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
83+
keyboardActions: KeyboardActions = KeyboardActions.Default,
84+
visualTransformation: VisualTransformation = VisualTransformation.None,
85+
leadingIcon: @Composable (() -> Unit)? = null,
86+
trailingIcon: @Composable (() -> Unit)? = null,
87+
) {
88+
val isSecureTextField = validator is PasswordValidator
89+
var passwordVisible by remember { mutableStateOf(false) }
90+
91+
TextField(
92+
modifier = modifier,
93+
value = value,
94+
onValueChange = { newValue ->
95+
onValueChange(newValue)
96+
validator?.validate(newValue)
97+
},
98+
label = label,
99+
singleLine = true,
100+
enabled = enabled,
101+
isError = isError ?: validator?.hasError ?: false,
102+
supportingText = {
103+
if (validator?.hasError ?: false) {
104+
Text(text = errorMessage ?: validator.errorMessage)
105+
}
106+
},
107+
keyboardOptions = keyboardOptions,
108+
keyboardActions = keyboardActions,
109+
visualTransformation = if (isSecureTextField && !passwordVisible)
110+
PasswordVisualTransformation() else visualTransformation,
111+
leadingIcon = leadingIcon,
112+
trailingIcon = trailingIcon ?: {
113+
if (isSecureTextField) {
114+
IconButton(
115+
onClick = {
116+
passwordVisible = !passwordVisible
117+
}
118+
) {
119+
Icon(
120+
imageVector = if (passwordVisible)
121+
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
122+
contentDescription = if (passwordVisible) "Hide password" else "Show password"
123+
)
124+
}
125+
}
126+
},
127+
)
128+
}
129+
130+
@Preview(showBackground = true)
131+
@Composable
132+
internal fun PreviewAuthTextField() {
133+
val context = LocalContext.current
134+
val nameTextValue = remember { mutableStateOf("") }
135+
val emailTextValue = remember { mutableStateOf("") }
136+
val passwordTextValue = remember { mutableStateOf("") }
137+
val emailValidator = remember {
138+
EmailValidator(stringProvider = DefaultAuthUIStringProvider(context))
139+
}
140+
val passwordValidator = remember {
141+
PasswordValidator(
142+
stringProvider = DefaultAuthUIStringProvider(context),
143+
rules = listOf(
144+
PasswordRule.MinimumLength(8),
145+
PasswordRule.RequireUppercase,
146+
PasswordRule.RequireLowercase,
147+
)
148+
)
149+
}
150+
151+
Column(
152+
modifier = Modifier
153+
.fillMaxSize(),
154+
verticalArrangement = Arrangement.Center,
155+
horizontalAlignment = Alignment.CenterHorizontally,
156+
) {
157+
AuthTextField(
158+
value = nameTextValue.value,
159+
label = {
160+
Text("Name")
161+
},
162+
onValueChange = { text ->
163+
nameTextValue.value = text
164+
},
165+
)
166+
Spacer(modifier = Modifier.height(16.dp))
167+
AuthTextField(
168+
value = emailTextValue.value,
169+
validator = emailValidator,
170+
label = {
171+
Text("Email")
172+
},
173+
onValueChange = { text ->
174+
emailTextValue.value = text
175+
},
176+
leadingIcon = {
177+
Icon(
178+
imageVector = Icons.Default.Email,
179+
contentDescription = ""
180+
)
181+
}
182+
)
183+
Spacer(modifier = Modifier.height(16.dp))
184+
AuthTextField(
185+
value = passwordTextValue.value,
186+
validator = passwordValidator,
187+
label = {
188+
Text("Password")
189+
},
190+
onValueChange = { text ->
191+
passwordTextValue.value = text
192+
},
193+
leadingIcon = {
194+
Icon(
195+
imageVector = Icons.Default.Lock,
196+
contentDescription = ""
197+
)
198+
}
199+
)
200+
}
201+
}

0 commit comments

Comments
 (0)