diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bb74b0a75..abcf1afd2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(libs.compose.ui.graphics) implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.material3) + implementation(libs.compose.material.icons.extended) // Facebook implementation(libs.facebook.login) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4bc5abcfe..6006db68b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -88,6 +88,12 @@ android:exported="false" android:theme="@style/Theme.FirebaseUIAndroid" /> + + + // customMethodPickerLayout now renders as the entire screen (no + // built-in logo/ToS footer/inset handling), so the terms checkbox + // that used to live in customMethodPickerTermsConfiguration is + // rendered inline here instead, and this composable owns its own + // insets via Modifier.safeDrawingPadding() in SpotlightMethodPicker. SpotlightMethodPicker( providers = providers, onProviderSelected = onProviderSelected, - enabled = termsAccepted + enabled = termsAccepted, + termsAccepted = termsAccepted, + onTermsAcceptedChange = { termsAccepted = it } ) }, - customMethodPickerTermsConfiguration = MethodPickerTermsConfiguration( - content = { - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = termsAccepted, - onCheckedChange = { termsAccepted = it } - ) - Text( - text = "I have read and accept the Terms of Service and Privacy Policy", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(start = 8.dp) - ) - } - }, - accepted = termsAccepted, - disableProvidersUntilAccepted = true, - ), ) } } @@ -180,6 +167,8 @@ fun SpotlightMethodPicker( providers: List, onProviderSelected: (AuthProvider) -> Unit, enabled: Boolean = true, + termsAccepted: Boolean = true, + onTermsAcceptedChange: (Boolean) -> Unit = {}, ) { val stringProvider = LocalAuthUIStringProvider.current @@ -197,7 +186,11 @@ fun SpotlightMethodPicker( val anonymous = groups["anonymous"]?.firstOrNull() LazyColumn( - modifier = Modifier.fillMaxSize(), + // customMethodPickerLayout now renders as the entire screen, so this composable is + // responsible for its own insets. + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), contentPadding = PaddingValues(vertical = 48.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -290,11 +283,29 @@ fun SpotlightMethodPicker( } } } + + item { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = termsAccepted, + onCheckedChange = onTermsAcceptedChange + ) + Text( + text = "I have read and accept the Terms of Service and Privacy Policy", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 8.dp) + ) + } + } } } @Composable -private fun ProviderIconButton( +fun ProviderIconButton( style: AuthUITheme.ProviderStyle, contentDescription: String, onClick: () -> Unit, @@ -331,12 +342,12 @@ private fun ProviderIconButton( } @Composable -private fun AuthUIAsset.asPainter(): Painter = when (this) { +fun AuthUIAsset.asPainter(): Painter = when (this) { is AuthUIAsset.Resource -> painterResource(resId) is AuthUIAsset.Vector -> rememberVectorPainter(image) } -private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) { +fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) { is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter is AuthProvider.Github -> ProviderStyleDefaults.Github @@ -347,6 +358,7 @@ private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle backgroundColor = provider.buttonColor ?: Color(0xFF666666), contentColor = provider.contentColor ?: Color.White ) + else -> AuthUITheme.ProviderStyle( icon = null, backgroundColor = Color(0xFF666666), diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt index df069091f..d270beac5 100644 --- a/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/auth/CustomSlotsThemingDemoActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity class CustomSlotsThemingDemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -46,6 +47,9 @@ class CustomSlotsThemingDemoActivity : ComponentActivity() { }, onCustomMethodPickerClick = { startActivity(Intent(this, CustomMethodPickerDemoActivity::class.java)) + }, + onFullCustomizationClick = { + startActivity(Intent(this, FullCustomizationDemoActivity::class.java)) } ) } @@ -60,6 +64,7 @@ fun CustomSlotsDemoChooser( onPhoneAuthSlotClick: () -> Unit, onShapeCustomizationClick: () -> Unit, onCustomMethodPickerClick: () -> Unit, + onFullCustomizationClick: () -> Unit, ) { Column( modifier = Modifier @@ -106,6 +111,12 @@ fun CustomSlotsDemoChooser( description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsConfiguration on FirebaseAuthScreen.", onClick = onCustomMethodPickerClick ) + + DemoCard( + title = "Full Customization", + description = "customMethodPickerLayout renders as the entire screen, so this layers a full-bleed background image and scrim behind the custom method picker.", + onClick = onFullCustomizationClick + ) } } diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/AuthTextFieldStyle.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/AuthTextFieldStyle.kt new file mode 100644 index 000000000..998f29472 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/AuthTextFieldStyle.kt @@ -0,0 +1,70 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.firebaseui.android.demo.R + +val AuthFieldShape = RoundedCornerShape(24.dp) + +@Composable +fun authTextFieldColors(): TextFieldColors = OutlinedTextFieldDefaults.colors( + unfocusedContainerColor = Color.White, + focusedContainerColor = Color.White, + disabledContainerColor = Color.White, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + focusedBorderColor = MaterialTheme.colorScheme.secondary, +) + +@Composable +fun FullCustomizationTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + enabled: Boolean = true, + isError: Boolean = false, + supportingText: String? = null, + singleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + shape: Shape = AuthFieldShape, +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label?.let { { Text(it) } }, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + isError = isError, + supportingText = supportingText?.let { { Text(it) } }, + singleLine = singleLine, + visualTransformation = visualTransformation, + shape = shape, + colors = authTextFieldColors(), + ) +} + +@Composable +fun EmailFieldIcon() { + Image( + painter = painterResource(R.drawable.email_at_sign), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/CtaButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/CtaButton.kt new file mode 100644 index 000000000..d3cd3f5b7 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/CtaButton.kt @@ -0,0 +1,53 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +private val CtaShadowColor = Color(0xFF5D0B47) + +@Composable +fun CtaButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false, + colors: ButtonColors = ButtonDefaults.buttonColors(), +) { + HardOffsetShadow( + shape = ButtonShape, + offsetX = 2.dp, + offsetY = 4.dp, + color = if (enabled) CtaShadowColor else Color.Transparent, + modifier = modifier.fillMaxWidth(), + ) { + Button( + onClick = onClick, + enabled = enabled, + shape = ButtonShape, + colors = colors, + contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp), + modifier = Modifier + .fillMaxWidth() + .height(80.dp), + ) { + if (isLoading) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Text(text = text, style = MaterialTheme.typography.titleMedium) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/EmailEntryStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/EmailEntryStep.kt new file mode 100644 index 000000000..32594a588 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/EmailEntryStep.kt @@ -0,0 +1,178 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.firebaseui.android.demo.R + +@Composable +fun EmailEntryStep( + email: String, + onEmailChange: (String) -> Unit, + isLoading: Boolean, + onContinue: () -> Unit, + onShowOtherMethods: () -> Unit, +) { + val isEmailValid = remember(email) { + android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches() + } + val showEmailError = email.isNotBlank() && !isEmailValid + + // verticalScroll measures content with infinite max height, and Column distributes weights + // against the MIN height when max is infinite (RowColumnMeasurePolicy.kt) — so + // heightIn(min = viewport) makes the weighted spacers expand (centering content, anchoring + // the link to the bottom) when everything fits, and collapse to zero (plain scrolling) when + // it doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 48.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier + .size(96.dp) + .offset(y = 12.dp) + .zIndex(1f), + ) + + Surface( + color = MaterialTheme.colorScheme.secondary, + shape = IntroShape, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "intro - welcome headline bubble" }, + ) { + Text( + text = "Hey there,\nWelcome", + style = MaterialTheme.typography.headlineMedium.copy( + textAlign = TextAlign.Center, + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFFF8F8), + Color(0xFFFFDDB4), + Color(0xFFFFD8EB), + ), + ), + ), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 16.dp), + ) + } + + HardOffsetShadow( + shape = AuthFieldShape, + modifier = Modifier.fillMaxWidth(), + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "email - sign in card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "Enter your email address to continue.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + + FullCustomizationTextField( + value = email, + onValueChange = onEmailChange, + label = "Email address", + leadingIcon = { EmailFieldIcon() }, + enabled = !isLoading, + isError = showEmailError, + supportingText = if (showEmailError) "Enter a valid email address" else null, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address input" }, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = "Continue", + onClick = onContinue, + enabled = isEmailValid && !isLoading, + isLoading = isLoading, + modifier = Modifier.semantics { contentDescription = "button - continue to password" }, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + TextButton( + onClick = onShowOtherMethods, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .semantics { contentDescription = "Other sign-in methods button" }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Use other sign-in methods") + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt new file mode 100644 index 000000000..9b78887c5 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt @@ -0,0 +1,143 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import com.firebase.ui.auth.AuthException +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen +import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen +import com.firebaseui.android.demo.R + +class FullCustomizationDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val configuration = authUIConfiguration { + context = applicationContext + logo = AuthUIAsset.Resource(R.drawable.firebase_auth) + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + providers { + provider( + AuthProvider.Google( + scopes = listOf("email"), + serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com", + ) + ) + provider(AuthProvider.Apple(customParameters = emptyMap(), locale = null)) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Twitter(customParameters = emptyMap())) + provider(AuthProvider.Github(customParameters = emptyMap())) + provider(AuthProvider.Microsoft(tenant = null, customParameters = emptyMap())) + provider(AuthProvider.Yahoo(customParameters = emptyMap())) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + ) + provider(AuthProvider.Anonymous) + } + } + + setContent { + FullCustomizationTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> + Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}") + }, + onSignInFailure = { exception: AuthException -> + Log.e("FullCustomizationDemo", "Auth failed", exception) + }, + onSignInCancelled = { + Log.d("FullCustomizationDemo", "Auth cancelled") + }, + customMethodPickerLayout = { providers, onProviderSelected -> + FullCustomizationMethodPicker( + authUI = authUI, + configuration = configuration, + providers = providers, + onProviderSelected = onProviderSelected, + ) + }, + ) + } + } + } + } +} + +@Composable +private fun FullCustomizationMethodPicker( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + providers: List, + onProviderSelected: (AuthProvider) -> Unit, +) { + val context = LocalContext.current + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + Column(modifier = Modifier.fillMaxSize()) { + EmailAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + onSuccess = { result -> + Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception -> + Log.e("FullCustomizationDemo", "Auth failed", exception) + }, + onCancel = { + Log.d("FullCustomizationDemo", "Auth cancelled") + }, + ) { state -> + SingleFieldSignInUI( + state = state, + otherProviders = providers.filterNot { it is AuthProvider.Email }, + onProviderSelected = onProviderSelected, + tosUrl = configuration.tosUrl, + ppUrl = configuration.privacyPolicyUrl, + ) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationShapes.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationShapes.kt new file mode 100644 index 000000000..1c96ea2f2 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationShapes.kt @@ -0,0 +1,8 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.unit.dp + +val IntroShape = RoundedCornerShape(80.dp) +val ButtonShape = RoundedCornerShape(36.dp) +val ProviderButtonShape = RoundedCornerShape(percent = 50) diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTheme.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTheme.kt new file mode 100644 index 000000000..16ade7f50 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTheme.kt @@ -0,0 +1,135 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import kotlin.math.max +import kotlin.math.min + +private val LightPrimary = Color(0xFF864B6F) +private val LightOnPrimary = Color(0xFFFFFFFF) +private val LightPrimaryContainer = Color(0xFFFFD8EB) +private val LightOnPrimaryContainer = Color(0xFF7B3B73) +private val LightInversePrimary = Color(0xFFFAB1DA) +private val LightSecondary = Color(0xFF4C8BFF) +private val LightOnSecondary = Color(0xFFFFFFFF) +private val LightSecondaryContainer = Color(0xFFCCE5FF) +private val LightTertiaryContainer = Color(0xFFFFDDB4) +private val LightSurface = Color(0xFFFFF8F8) +private val LightSurfaceBright = Color(0xFFFFF8F8) +private val LightOnSurface = Color(0xFF211A1D) +private val LightOnSurfaceVariant = Color(0xFFA08B95) +private val LightSurfaceContainer = Color(0xFFF9EAEF) +private val LightSurfaceContainerLow = Color(0xFFFDF0F6) +private val LightOutline = Color(0xFF81737A) +private val LightOutlineVariant = Color(0xFFD3C2C9) +private val LightInverseSurface = Color(0xFF322F35) +private val LightInverseOnSurface = Color(0xFFF5EFF7) + +val FullCustomizationLightColorScheme = lightColorScheme( + primary = LightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = LightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, + inversePrimary = LightInversePrimary, + secondary = LightSecondary, + onSecondary = LightOnSecondary, + secondaryContainer = LightSecondaryContainer, + tertiaryContainer = LightTertiaryContainer, + surface = LightSurface, + surfaceBright = LightSurfaceBright, + onSurface = LightOnSurface, + onSurfaceVariant = LightOnSurfaceVariant, + surfaceContainer = LightSurfaceContainer, + surfaceContainerLow = LightSurfaceContainerLow, + outline = LightOutline, + outlineVariant = LightOutlineVariant, + inverseSurface = LightInverseSurface, + inverseOnSurface = LightInverseOnSurface, +) + +val FullCustomizationDarkColorScheme = darkColorScheme( + primary = LightPrimary.withLightness(0.78f), + onPrimary = LightOnPrimary.withLightness(0.18f), + primaryContainer = LightPrimaryContainer.withLightness(0.28f), + onPrimaryContainer = LightOnPrimaryContainer.withLightness(0.88f), + inversePrimary = LightPrimary, + secondary = LightSecondary.withLightness(0.78f), + onSecondary = LightOnSecondary.withLightness(0.18f), + secondaryContainer = LightSecondaryContainer.withLightness(0.28f), + tertiaryContainer = LightTertiaryContainer.withLightness(0.28f), + surface = LightSurface.withLightness(0.10f), + surfaceBright = LightSurfaceBright.withLightness(0.20f), + onSurface = LightOnSurface.withLightness(0.88f), + onSurfaceVariant = LightOnSurfaceVariant.withLightness(0.75f), + surfaceContainer = LightSurfaceContainer.withLightness(0.13f), + surfaceContainerLow = LightSurfaceContainerLow.withLightness(0.11f), + outline = LightOutline.withLightness(0.55f), + outlineVariant = LightOutlineVariant.withLightness(0.30f), + inverseSurface = LightSurface.withLightness(0.90f), + inverseOnSurface = LightOnSurface.withLightness(0.15f), +) + +@Composable +fun FullCustomizationTheme(content: @Composable () -> Unit) { + val colorScheme = if (isSystemInDarkTheme()) { + FullCustomizationDarkColorScheme + } else { + FullCustomizationLightColorScheme + } + AuthUITheme( + theme = AuthUITheme.Default.copy( + colorScheme = colorScheme, + typography = FullCustomizationTypography, + providerButtonShape = ProviderButtonShape, + ), + content = content, + ) +} + +private fun Color.withLightness(newLightness: Float): Color { + val (h, s, _) = toHsl() + return hslToColor(h, s, newLightness.coerceIn(0f, 1f), alpha) +} + +private fun Color.toHsl(): Triple { + val r = red + val g = green + val b = blue + val maxC = max(r, max(g, b)) + val minC = min(r, min(g, b)) + val l = (maxC + minC) / 2f + if (maxC == minC) return Triple(0f, 0f, l) + val d = maxC - minC + val s = if (l > 0.5f) d / (2f - maxC - minC) else d / (maxC + minC) + val h = when (maxC) { + r -> ((g - b) / d + (if (g < b) 6f else 0f)) + g -> ((b - r) / d + 2f) + else -> ((r - g) / d + 4f) + } / 6f + return Triple(h, s, l) +} + +private fun hslToColor(h: Float, s: Float, l: Float, alpha: Float): Color { + if (s == 0f) return Color(l, l, l, alpha) + fun hueToRgb(p: Float, q: Float, tIn: Float): Float { + var t = tIn + if (t < 0f) t += 1f + if (t > 1f) t -= 1f + return when { + t < 1f / 6f -> p + (q - p) * 6f * t + t < 1f / 2f -> q + t < 2f / 3f -> p + (q - p) * (2f / 3f - t) * 6f + else -> p + } + } + val q = if (l < 0.5f) l * (1f + s) else l + s - l * s + val p = 2f * l - q + val r = hueToRgb(p, q, h + 1f / 3f) + val g = hueToRgb(p, q, h) + val b = hueToRgb(p, q, h - 1f / 3f) + return Color(r, g, b, alpha) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTypography.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTypography.kt new file mode 100644 index 000000000..501916e3a --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationTypography.kt @@ -0,0 +1,59 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import com.firebaseui.android.demo.R + +val BagelFatOne = FontFamily(Font(R.font.bagel_fat_one_regular, FontWeight.Normal)) + +val Onest = FontFamily( + Font(R.font.onest_regular, FontWeight.Normal), + Font(R.font.onest_medium, FontWeight.Medium), + Font(R.font.onest_semibold, FontWeight.SemiBold), + Font(R.font.onest_bold, FontWeight.Bold), +) + +val Roboto = FontFamily( + Font(R.font.roboto_regular, FontWeight.Normal), + Font(R.font.roboto_medium, FontWeight.Medium), + Font(R.font.roboto_semibold, FontWeight.SemiBold), + Font(R.font.roboto_bold, FontWeight.Bold), +) + +val FullCustomizationTypography = Typography( + headlineSmall = TextStyle( + fontFamily = BagelFatOne, + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + ), + headlineMedium = TextStyle( + fontFamily = BagelFatOne, + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + ), + bodyLarge = TextStyle( + fontFamily = Onest, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + ), + labelLarge = TextStyle( + fontFamily = Roboto, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + titleMedium = TextStyle( + fontFamily = Onest, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 20.sp, + ), +) diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/HardOffsetShadow.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/HardOffsetShadow.kt new file mode 100644 index 000000000..9fb9b8a7c --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/HardOffsetShadow.kt @@ -0,0 +1,32 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun HardOffsetShadow( + shape: Shape, + modifier: Modifier = Modifier, + offsetX: Dp = 3.dp, + offsetY: Dp = 6.dp, + color: Color = MaterialTheme.colorScheme.primaryContainer, + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .offset(x = offsetX, y = offsetY) + .background(color = color, shape = shape), + ) + content() + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/LoginStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/LoginStep.kt new file mode 100644 index 000000000..b3ffa0acf --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/LoginStep.kt @@ -0,0 +1,188 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebaseui.android.demo.R + +@Composable +fun LoginStep( + state: EmailAuthContentState, + onUseDifferentEmail: () -> Unit, +) { + var passwordVisible by remember { mutableStateOf(false) } + + // verticalScroll measures content with infinite max height, and Column distributes weights + // against the MIN height when max is infinite (RowColumnMeasurePolicy.kt) — so + // heightIn(min = viewport) makes the weighted spacers expand (centering content, anchoring + // CTAs to the bottom) when everything fits, and collapse to zero (plain scrolling) when it + // doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Login", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "email - login card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + FullCustomizationTextField( + value = state.email, + onValueChange = {}, + enabled = false, + leadingIcon = { EmailFieldIcon() }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address display" }, + ) + + FullCustomizationTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = "Password", + enabled = !state.isLoading, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) { + Icons.Default.VisibilityOff + } else { + Icons.Default.Visibility + }, + contentDescription = null, + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - password secure input" }, + ) + + Text( + text = if (state.resetLinkSent) "Reset link sent!" else "Forgot password?", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = !state.resetLinkSent) { + state.onSendResetLinkClick() + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Login", + onClick = state.onSignInClick, + enabled = state.password.isNotBlank() && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - login" }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + CtaButton( + text = if (state.emailSignInLinkSent) "Login link sent!" else "Send login link", + onClick = state.onSignInEmailLinkClick, + enabled = !state.isLoading, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.semantics { contentDescription = "button - send login link" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onUseDifferentEmail, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different email") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/OtherSignInMethodsSheet.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/OtherSignInMethodsSheet.kt new file mode 100644 index 000000000..0cfa4aaf6 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/OtherSignInMethodsSheet.kt @@ -0,0 +1,70 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.components.TermsAndPrivacyForm + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OtherSignInMethodsSheet( + otherProviders: List, + onProviderSelected: (AuthProvider) -> Unit, + onDismissRequest: () -> Unit, + tosUrl: String?, + ppUrl: String?, +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + containerColor = MaterialTheme.colorScheme.primaryContainer, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 64.dp), + ) { + Text( + text = "Other sign in methods", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + .semantics { contentDescription = "Other sign-in methods sheet title" }, + ) + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth(), + ) { + otherProviders.forEach { provider -> + SheetProviderButton( + provider = provider, + onClick = { + onDismissRequest() + onProviderSelected(provider) + }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + TermsAndPrivacyForm(tosUrl = tosUrl, ppUrl = ppUrl) + Spacer(modifier = Modifier.height(24.dp)) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SheetProviderButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SheetProviderButton.kt new file mode 100644 index 000000000..4b0f4b243 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SheetProviderButton.kt @@ -0,0 +1,120 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults + +@Composable +fun SheetProviderButton( + provider: AuthProvider, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val label = providerSheetLabel(provider) + val style = when (provider) { + is AuthProvider.Google -> ProviderStyleDefaults.Google + is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook + is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter + is AuthProvider.Github -> ProviderStyleDefaults.Github + is AuthProvider.Microsoft -> ProviderStyleDefaults.Microsoft + is AuthProvider.Yahoo -> ProviderStyleDefaults.Yahoo + is AuthProvider.Apple -> ProviderStyleDefaults.Apple + is AuthProvider.Anonymous -> ProviderStyleDefaults.Anonymous + else -> ProviderStyleDefaults.Email + } + val backgroundColor = if (provider is AuthProvider.Phone) { + MaterialTheme.colorScheme.primary + } else { + style.backgroundColor + } + val contentColor = if (provider is AuthProvider.Google) Color.Black else style.contentColor + val hasWhiteBackground = backgroundColor == Color.White + + Button( + onClick = onClick, + shape = ProviderButtonShape, + colors = ButtonDefaults.buttonColors( + containerColor = backgroundColor, + contentColor = contentColor, + ), + border = if (hasWhiteBackground) BorderStroke(1.dp, Color.Black) else null, + contentPadding = PaddingValues(horizontal = 36.dp, vertical = 12.dp), + modifier = modifier, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + if (provider is AuthProvider.Phone) { + Icon( + imageVector = Icons.Default.Phone, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } else { + style.icon?.let { icon -> + Image( + painter = icon.asPainter(), + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + } + } + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = label, + modifier = Modifier + .weight(1f) + .padding(end = 8.dp), + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + style = MaterialTheme.typography.labelLarge, + ) + } + } +} + +private fun providerSheetLabel(provider: AuthProvider): String = when (provider) { + is AuthProvider.Google -> "Sign in with Google" + is AuthProvider.Facebook -> "Sign in with Facebook" + is AuthProvider.Twitter -> "Sign in with X" + is AuthProvider.Github -> "Sign in with GitHub" + is AuthProvider.Microsoft -> "Sign in with Microsoft" + is AuthProvider.Yahoo -> "Sign in with Yahoo" + is AuthProvider.Apple -> "Sign in with Apple" + is AuthProvider.Phone -> "Sign in with phone" + is AuthProvider.Anonymous -> "Continue as guest" + else -> "Continue" +} + +@Composable +private fun AuthUIAsset.asPainter() = when (this) { + is AuthUIAsset.Resource -> painterResource(resId) + is AuthUIAsset.Vector -> rememberVectorPainter(image) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SignUpStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SignUpStep.kt new file mode 100644 index 000000000..a80c134da --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SignUpStep.kt @@ -0,0 +1,224 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebaseui.android.demo.R + +private val NameFieldStartShape = RoundedCornerShape( + topStart = 16.dp, + bottomStart = 16.dp, + topEnd = 0.dp, + bottomEnd = 0.dp, +) +private val NameFieldEndShape = RoundedCornerShape( + topStart = 0.dp, + bottomStart = 0.dp, + topEnd = 16.dp, + bottomEnd = 16.dp, +) + +@Composable +fun SignUpStep( + state: EmailAuthContentState, + onUseDifferentEmail: () -> Unit, +) { + var firstName by remember { mutableStateOf("") } + var lastName by remember { mutableStateOf("") } + var confirmEmail by remember { mutableStateOf("") } + + val emailsMatch = confirmEmail.isNotBlank() && confirmEmail == state.email + val passwordsMatch = state.confirmPassword.isNotBlank() && state.confirmPassword == state.password + val canSignUp = firstName.isNotBlank() && + lastName.isNotBlank() && + emailsMatch && + state.password.isNotBlank() && + passwordsMatch && + !state.isLoading + + // verticalScroll measures content with infinite max height, and Column distributes weights + // against the MIN height when max is infinite (RowColumnMeasurePolicy.kt) — so + // heightIn(min = viewport) makes the weighted spacers expand (centering content, anchoring + // CTAs to the bottom) when everything fits, and collapse to zero (plain scrolling) when it + // doesn't. + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .heightIn(min = maxHeight) + .padding(horizontal = 40.dp, vertical = 24.dp), + ) { + Spacer(modifier = Modifier.weight(1f)) + + Column(modifier = Modifier.fillMaxWidth()) { + Image( + painter = painterResource(id = R.drawable.full_customization_mascot), + contentDescription = "doggo - cute welcome mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Sign up", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(24.dp)) + + HardOffsetShadow(shape = AuthFieldShape, modifier = Modifier.fillMaxWidth()) { + Surface( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "sign up card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { + FullCustomizationTextField( + value = firstName, + onValueChange = { firstName = it }, + label = "First name", + enabled = !state.isLoading, + shape = NameFieldStartShape, + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - first name" }, + ) + FullCustomizationTextField( + value = lastName, + onValueChange = { lastName = it }, + label = "Last name", + enabled = !state.isLoading, + shape = NameFieldEndShape, + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - last name" }, + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + FullCustomizationTextField( + value = state.email, + onValueChange = {}, + enabled = false, + leadingIcon = { EmailFieldIcon() }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - email address display" }, + ) + FullCustomizationTextField( + value = confirmEmail, + onValueChange = { confirmEmail = it }, + label = "Confirm Email", + enabled = !state.isLoading, + isError = confirmEmail.isNotBlank() && !emailsMatch, + supportingText = if (confirmEmail.isNotBlank() && !emailsMatch) { + "Emails don't match" + } else { + null + }, + leadingIcon = { EmailFieldIcon() }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - confirm email" }, + ) + } + + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + FullCustomizationTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = "Password", + enabled = !state.isLoading, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - password" }, + ) + FullCustomizationTextField( + value = state.confirmPassword, + onValueChange = state.onConfirmPasswordChange, + label = "Confirm Password", + enabled = !state.isLoading, + visualTransformation = PasswordVisualTransformation(), + isError = state.confirmPassword.isNotBlank() && !passwordsMatch, + supportingText = if (state.confirmPassword.isNotBlank() && !passwordsMatch) { + "Passwords don't match" + } else { + null + }, + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - confirm password" }, + ) + } + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Sign up", + onClick = { + state.onDisplayNameChange("$firstName $lastName".trim()) + state.onSignUpClick() + }, + enabled = canSignUp, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - sign up" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = onUseDifferentEmail, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different email") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SingleFieldSignInUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SingleFieldSignInUI.kt new file mode 100644 index 000000000..a2d21c5ea --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/SingleFieldSignInUI.kt @@ -0,0 +1,102 @@ +package com.firebaseui.android.demo.auth.fullcustomization + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.google.firebase.auth.FirebaseAuth +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +private enum class FlowStep { EnterEmail, Login, SignUp } + +@Composable +fun SingleFieldSignInUI( + state: EmailAuthContentState, + otherProviders: List, + onProviderSelected: (AuthProvider) -> Unit, + tosUrl: String?, + ppUrl: String?, +) { + var flowStep by remember { mutableStateOf(FlowStep.EnterEmail) } + var showOtherMethods by remember { mutableStateOf(false) } + var isCheckingEmail by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + // Password/confirmPassword are hoisted in EmailAuthContentState, not local to LoginStep/ + // SignUpStep — they survive a round trip back to EnterEmail, so a stale password typed for + // one email could carry over if a different email also routes to the same step. Clear them + // whenever the user backs out via "Use a different email". + val onUseDifferentEmail: () -> Unit = { + state.onPasswordChange("") + state.onConfirmPasswordChange("") + flowStep = FlowStep.EnterEmail + } + + Box(modifier = Modifier.fillMaxSize()) { + when (flowStep) { + FlowStep.EnterEmail -> EmailEntryStep( + email = state.email, + onEmailChange = state.onEmailChange, + isLoading = state.isLoading || isCheckingEmail, + onContinue = { + isCheckingEmail = true + coroutineScope.launch { + val signInMethods = fetchLegacySignInMethods(state.email) + flowStep = if (signInMethods.isEmpty()) FlowStep.SignUp else FlowStep.Login + isCheckingEmail = false + } + }, + onShowOtherMethods = { showOtherMethods = true }, + ) + + FlowStep.Login -> LoginStep( + state = state, + onUseDifferentEmail = onUseDifferentEmail, + ) + + FlowStep.SignUp -> SignUpStep( + state = state, + onUseDifferentEmail = onUseDifferentEmail, + ) + } + } + + if (showOtherMethods) { + OtherSignInMethodsSheet( + otherProviders = otherProviders, + onProviderSelected = onProviderSelected, + onDismissRequest = { showOtherMethods = false }, + tosUrl = tosUrl, + ppUrl = ppUrl, + ) + } +} + +/** + * Whether [email] is already registered, via Firebase Auth's `fetchSignInMethodsForEmail` — + * deprecated by Firebase ("legacy") and, depending on the project's Email Enumeration Protection + * setting, may always return an empty list regardless of whether the email exists. + */ +private suspend fun fetchLegacySignInMethods(email: String): List { + return try { + @Suppress("DEPRECATION") + FirebaseAuth.getInstance() + .fetchSignInMethodsForEmail(email) + .await() + .signInMethods + ?.filter { it.isNotBlank() } + ?: emptyList() + } catch (e: Exception) { + Log.w("SingleFieldSignInUI", "fetchSignInMethodsForEmail failed for $email", e) + emptyList() + } +} diff --git a/app/src/main/res/drawable-xhdpi/email_at_sign.png b/app/src/main/res/drawable-xhdpi/email_at_sign.png new file mode 100644 index 000000000..f7082d7ad Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/email_at_sign.png differ diff --git a/app/src/main/res/drawable-xhdpi/full_customization_mascot.png b/app/src/main/res/drawable-xhdpi/full_customization_mascot.png new file mode 100644 index 000000000..0a5c3afa3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/full_customization_mascot.png differ diff --git a/app/src/main/res/drawable/custom_background.png b/app/src/main/res/drawable/custom_background.png new file mode 100644 index 000000000..ce5dfe236 Binary files /dev/null and b/app/src/main/res/drawable/custom_background.png differ diff --git a/app/src/main/res/font/bagel_fat_one_regular.ttf b/app/src/main/res/font/bagel_fat_one_regular.ttf new file mode 100644 index 000000000..9de4a2f78 Binary files /dev/null and b/app/src/main/res/font/bagel_fat_one_regular.ttf differ diff --git a/app/src/main/res/font/onest_bold.ttf b/app/src/main/res/font/onest_bold.ttf new file mode 100644 index 000000000..b0a3dd939 Binary files /dev/null and b/app/src/main/res/font/onest_bold.ttf differ diff --git a/app/src/main/res/font/onest_medium.ttf b/app/src/main/res/font/onest_medium.ttf new file mode 100644 index 000000000..2ff600481 Binary files /dev/null and b/app/src/main/res/font/onest_medium.ttf differ diff --git a/app/src/main/res/font/onest_regular.ttf b/app/src/main/res/font/onest_regular.ttf new file mode 100644 index 000000000..dec9f7a23 Binary files /dev/null and b/app/src/main/res/font/onest_regular.ttf differ diff --git a/app/src/main/res/font/onest_semibold.ttf b/app/src/main/res/font/onest_semibold.ttf new file mode 100644 index 000000000..c7e8a3d2e Binary files /dev/null and b/app/src/main/res/font/onest_semibold.ttf differ diff --git a/app/src/main/res/font/roboto_bold.ttf b/app/src/main/res/font/roboto_bold.ttf new file mode 100644 index 000000000..651618564 Binary files /dev/null and b/app/src/main/res/font/roboto_bold.ttf differ diff --git a/app/src/main/res/font/roboto_medium.ttf b/app/src/main/res/font/roboto_medium.ttf new file mode 100644 index 000000000..bc5b17026 Binary files /dev/null and b/app/src/main/res/font/roboto_medium.ttf differ diff --git a/app/src/main/res/font/roboto_regular.ttf b/app/src/main/res/font/roboto_regular.ttf new file mode 100644 index 000000000..3db0d1fb0 Binary files /dev/null and b/app/src/main/res/font/roboto_regular.ttf differ diff --git a/app/src/main/res/font/roboto_semibold.ttf b/app/src/main/res/font/roboto_semibold.ttf new file mode 100644 index 000000000..7a8ef87d5 Binary files /dev/null and b/app/src/main/res/font/roboto_semibold.ttf differ diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt index fffcf5905..748ccca34 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -104,6 +105,14 @@ import kotlinx.coroutines.tasks.await * @param authenticatedContent Optional slot that allows callers to render the authenticated * state themselves. When provided, it receives the current [AuthState] alongside an * [AuthSuccessUiContext] containing common callbacks (sign out, manage MFA, reload user). + * @param customMethodPickerLayout Optional slot that fully replaces the method-picker screen. + * When provided, it renders as the *entire* screen content — edge-to-edge, with no logo, no + * Terms of Service/Privacy Policy footer, and no automatic system-inset handling. The caller is + * responsible for its own insets (e.g. `Modifier.safeDrawingPadding()`) and for displaying any + * required legal disclosures. [customMethodPickerTermsConfiguration] is ignored when this is set. + * @param customMethodPickerTermsConfiguration Optional custom Terms of Service/Privacy Policy + * footer for the *default* method-picker layout. Ignored when [customMethodPickerLayout] is + * provided, since that slot takes over the whole screen. * * @since 10.0.0 */ @@ -207,19 +216,26 @@ fun FirebaseAuthScreen( } ) { composable(AuthRoute.MethodPicker.route) { - Scaffold { innerPadding -> - AuthMethodPicker( - modifier = modifier - .padding(innerPadding), - providers = configuration.providers, - logo = logoAsset, - termsOfServiceUrl = configuration.tosUrl, - privacyPolicyUrl = configuration.privacyPolicyUrl, - lastSignInPreference = lastSignInPreference.value, - customLayout = customMethodPickerLayout, - termsConfiguration = customMethodPickerTermsConfiguration, - onProviderSelected = onProviderSelected, - ) + if (customMethodPickerLayout != null) { + // Takes over the entire screen — no logo, no ToS/Privacy footer, and no + // automatic inset handling. See the KDoc on customMethodPickerLayout. + Box(modifier = modifier.fillMaxSize()) { + customMethodPickerLayout(configuration.providers, onProviderSelected) + } + } else { + Scaffold { innerPadding -> + AuthMethodPicker( + modifier = modifier + .padding(innerPadding), + providers = configuration.providers, + logo = logoAsset, + termsOfServiceUrl = configuration.tosUrl, + privacyPolicyUrl = configuration.privacyPolicyUrl, + lastSignInPreference = lastSignInPreference.value, + termsConfiguration = customMethodPickerTermsConfiguration, + onProviderSelected = onProviderSelected, + ) + } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt index bca4bc4c5..c3b5f5b62 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt @@ -27,17 +27,22 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme import com.firebase.ui.auth.configuration.validators.VerificationCodeValidator import com.firebase.ui.auth.mfa.MfaChallengeContentState import com.firebase.ui.auth.ui.components.VerificationCodeInputField @@ -50,108 +55,136 @@ internal fun DefaultMfaChallengeContent(state: MfaChallengeContentState) { VerificationCodeValidator(stringProvider) } - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = if (isSms) { - val phoneLabel = state.maskedPhoneNumber ?: "" - stringProvider.enterVerificationCodeTitle(phoneLabel) - } else { - stringProvider.mfaStepVerifyFactorTitle - }, - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - - if (isSms && state.maskedPhoneNumber != null) { - Text( - text = stringProvider.mfaStepVerifyFactorSmsHelper, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (state.error != null) { + Scaffold { innerPadding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { Text( - text = state.error, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, + text = if (isSms) { + val phoneLabel = state.maskedPhoneNumber ?: "" + stringProvider.enterVerificationCodeTitle(phoneLabel) + } else { + stringProvider.mfaStepVerifyFactorTitle + }, + style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) - } - Spacer(modifier = Modifier.height(8.dp)) + if (isSms && state.maskedPhoneNumber != null) { + Text( + text = stringProvider.mfaStepVerifyFactorSmsHelper, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } - VerificationCodeInputField( - modifier = Modifier.align(Alignment.CenterHorizontally), - codeLength = 6, - validator = verificationCodeValidator, - isError = state.error != null, - errorMessage = state.error, - onCodeChange = state.onVerificationCodeChange - ) + if (state.error != null) { + Text( + text = state.error, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center + ) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - if (isSms) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - TextButton( - onClick = { state.onResendCodeClick?.invoke() }, - enabled = state.onResendCodeClick != null && !state.isLoading && state.resendTimer == 0 + VerificationCodeInputField( + modifier = Modifier.align(Alignment.CenterHorizontally), + codeLength = 6, + validator = verificationCodeValidator, + isError = state.error != null, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (isSms) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = if (state.resendTimer > 0) { - val minutes = state.resendTimer / 60 - val seconds = state.resendTimer % 60 - val formatted = "$minutes:${String.format(java.util.Locale.ROOT, "%02d", seconds)}" - stringProvider.resendCodeTimer(formatted) - } else { - stringProvider.resendCode - } - ) - } + TextButton( + onClick = { state.onResendCodeClick?.invoke() }, + enabled = state.onResendCodeClick != null && !state.isLoading && state.resendTimer == 0 + ) { + Text( + text = if (state.resendTimer > 0) { + val minutes = state.resendTimer / 60 + val seconds = state.resendTimer % 60 + val formatted = "$minutes:${String.format(java.util.Locale.ROOT, "%02d", seconds)}" + stringProvider.resendCodeTimer(formatted) + } else { + stringProvider.resendCode + } + ) + } - TextButton( + TextButton( + onClick = state.onCancelClick, + enabled = !state.isLoading + ) { + Text(stringProvider.useDifferentMethodAction) + } + } + } else { + OutlinedButton( onClick = state.onCancelClick, - enabled = !state.isLoading + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth() ) { - Text(stringProvider.useDifferentMethodAction) + Text(stringProvider.dismissAction) } } - } else { - OutlinedButton( - onClick = state.onCancelClick, - enabled = !state.isLoading, + + Button( + onClick = state.onVerifyClick, + enabled = state.isValid && !state.isLoading, modifier = Modifier.fillMaxWidth() ) { - Text(stringProvider.dismissAction) + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.padding(end = 8.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + } + Text(stringProvider.verifyAction) } } + } +} - Button( - onClick = state.onVerifyClick, - enabled = state.isValid && !state.isLoading, - modifier = Modifier.fillMaxWidth() +/** + * Renders with a simulated status/nav bar (see CP-240) so the lack of edge-to-edge insets + * handling above is visible in the IDE. A plain `@Preview` draws no system chrome at all, so + * this bug would be invisible there. + */ +@Preview(showSystemUi = true) +@Composable +private fun PreviewDefaultMfaChallengeContentEdgeToEdge() { + val applicationContext = LocalContext.current + val stringProvider = DefaultAuthUIStringProvider(applicationContext) + + AuthUITheme { + CompositionLocalProvider( + LocalAuthUIStringProvider provides stringProvider ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.padding(end = 8.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary + DefaultMfaChallengeContent( + state = MfaChallengeContentState( + factorType = MfaFactor.Sms, + maskedPhoneNumber = "+1••••••890" ) - } - Text(stringProvider.verifyAction) + ) } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt index 8b73f2c2d..201b5a5f8 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons @@ -137,8 +136,7 @@ fun ResetPasswordUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt index 14e045827..94891e029 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -148,8 +147,7 @@ fun SignInEmailLinkUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt index eabde87a1..b776c7e49 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -177,8 +176,7 @@ fun SignInUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt index 6ff0d19f4..da9c9dab7 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -127,8 +126,7 @@ fun SignUpUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { if (provider.isDisplayNameRequired) { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt index ef8bfe545..5e71f4a0a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions @@ -106,8 +105,7 @@ fun EnterPhoneNumberUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { Text(stringProvider.enterPhoneNumberTitle) diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt index 122e73a87..b1813e6c0 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -110,8 +109,7 @@ fun EnterVerificationCodeUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { Text(