Skip to content
Open
17 changes: 9 additions & 8 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,17 @@ android {
}

dependencies {
implementation(platform(Config.Libs.Androidx.Compose.bom))
implementation(Config.Libs.Androidx.Compose.ui)
implementation(Config.Libs.Androidx.Compose.uiGraphics)
implementation(Config.Libs.Androidx.Compose.material3)
implementation(Config.Libs.Androidx.Compose.foundation)
implementation(Config.Libs.Androidx.Compose.tooling)
implementation(Config.Libs.Androidx.Compose.toolingPreview)
implementation(Config.Libs.Androidx.Compose.activityCompose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(Config.Libs.Androidx.materialDesign)
implementation(Config.Libs.Androidx.activity)
implementation(libs.androidx.compose.material.icons.extended)
// The new activity result APIs force us to include Fragment 1.3.0
// See https://issuetracker.google.com/issues/152554847
implementation(Config.Libs.Androidx.fragment)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package com.firebase.ui.auth.compose.ui.components

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.firebase.ui.auth.compose.configuration.PasswordRule
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
import com.firebase.ui.auth.compose.configuration.validators.EmailValidator
import com.firebase.ui.auth.compose.configuration.validators.FieldValidator
import com.firebase.ui.auth.compose.configuration.validators.PasswordValidator

/**
* A customizable input field with built-in validation display.
*
* **Example usage:**
* ```kotlin
* val emailTextValue = remember { mutableStateOf("") }
*
* val emailValidator = remember {
* EmailValidator(stringProvider = DefaultAuthUIStringProvider(context))
* }
*
* AuthTextField(
* value = emailTextValue,
* onValueChange = { emailTextValue.value = it },
* label = {
* Text("Email")
* },
* validator = emailValidator
* )
* ```
*
* @param modifier A modifier for the field.
* @param value The current value of the text field.
* @param onValueChange A callback when the value changes.
* @param label The label for the text field.
* @param enabled If the field is enabled.
* @param isError Manually set the error state.
* @param errorMessage A custom error message to display.
* @param validator A validator to automatically handle error state and messages.
* @param keyboardOptions Keyboard options for the field.
* @param keyboardActions Keyboard actions for the field.
* @param visualTransformation Visual transformation for the input (e.g., password).
* @param leadingIcon An optional icon to display at the start of the field.
* @param trailingIcon An optional icon to display at the start of the field.
*/
@Composable
fun AuthTextField(
modifier: Modifier = Modifier,
value: String,
onValueChange: (String) -> Unit,
label: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
isError: Boolean? = null,
errorMessage: String? = null,
validator: FieldValidator? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
visualTransformation: VisualTransformation = VisualTransformation.None,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
) {
val isSecureTextField = validator is PasswordValidator
var passwordVisible by remember { mutableStateOf(false) }

TextField(
modifier = modifier,
value = value,
onValueChange = { newValue ->
onValueChange(newValue)
validator?.validate(newValue)
},
label = label,
singleLine = true,
enabled = enabled,
isError = isError ?: validator?.hasError ?: false,
supportingText = {
if (validator?.hasError ?: false) {
Text(text = errorMessage ?: validator.errorMessage)
}
},
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
visualTransformation = if (isSecureTextField && !passwordVisible)
PasswordVisualTransformation() else visualTransformation,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon ?: {
if (isSecureTextField) {
IconButton(
onClick = {
passwordVisible = !passwordVisible
}
) {
Icon(
imageVector = if (passwordVisible)
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
contentDescription = if (passwordVisible) "Hide password" else "Show password"
)
}
}
},
)
}

@Preview(showBackground = true)
@Composable
internal fun PreviewAuthTextField() {
val context = LocalContext.current
val nameTextValue = remember { mutableStateOf("") }
val emailTextValue = remember { mutableStateOf("") }
val passwordTextValue = remember { mutableStateOf("") }
val emailValidator = remember {
EmailValidator(stringProvider = DefaultAuthUIStringProvider(context))
}
val passwordValidator = remember {
PasswordValidator(
stringProvider = DefaultAuthUIStringProvider(context),
rules = listOf(
PasswordRule.MinimumLength(8),
PasswordRule.RequireUppercase,
PasswordRule.RequireLowercase,
)
)
}

Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
AuthTextField(
value = nameTextValue.value,
label = {
Text("Name")
},
onValueChange = { text ->
nameTextValue.value = text
},
)
Spacer(modifier = Modifier.height(16.dp))
AuthTextField(
value = emailTextValue.value,
validator = emailValidator,
label = {
Text("Email")
},
onValueChange = { text ->
emailTextValue.value = text
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Email,
contentDescription = ""
)
}
)
Spacer(modifier = Modifier.height(16.dp))
AuthTextField(
value = passwordTextValue.value,
validator = passwordValidator,
label = {
Text("Password")
},
onValueChange = { text ->
passwordTextValue.value = text
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = ""
)
}
)
}
}
Loading