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 77abc5776..d9ff55383 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, @@ -181,6 +186,8 @@ fun SpotlightMethodPicker( val anonymous = groups["anonymous"]?.firstOrNull() LazyColumn( + // customMethodPickerLayout now renders as the entire screen, so this composable is + // responsible for its own insets. modifier = Modifier .fillMaxSize() .safeDrawingPadding(), @@ -298,7 +305,7 @@ fun SpotlightMethodPicker( } @Composable -private fun ProviderIconButton( +fun ProviderIconButton( style: AuthUITheme.ProviderStyle, contentDescription: String, onClick: () -> Unit, @@ -335,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 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/HighLevelApiDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt index cfa10b93b..65273b2cb 100644 --- a/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/auth/HighLevelApiDemoActivity.kt @@ -59,9 +59,9 @@ import com.firebase.ui.auth.configuration.theme.AuthUITheme import com.firebase.ui.auth.ui.screens.AuthSuccessUiContext import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen import com.firebase.ui.auth.util.EmailLinkConstants +import com.firebaseui.android.demo.R import com.firebase.ui.auth.util.displayIdentifier import com.firebase.ui.auth.util.getDisplayEmail -import com.firebaseui.android.demo.R import com.google.firebase.auth.actionCodeSettings class HighLevelApiDemoActivity : ComponentActivity() { 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..179a84749 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/FullCustomizationDemoActivity.kt @@ -0,0 +1,147 @@ +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 +import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthMethodPickerUI +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.PhoneSignInUI +import com.firebaseui.android.demo.auth.fullcustomization.theme.FullCustomizationTheme + +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 -> + MainUI( + authUI = authUI, + configuration = configuration, + providers = providers, + onProviderSelected = onProviderSelected, + ) + }, + phoneContent = { state -> PhoneSignInUI(state) }, + ) + } + } + } + } +} + +@Composable +private fun MainUI( + 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 -> + AuthMethodPickerUI( + 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/common/AuthTextFieldStyle.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthTextFieldStyle.kt new file mode 100644 index 000000000..ca66ee7bc --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/AuthTextFieldStyle.kt @@ -0,0 +1,75 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +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, + placeholder: 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, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + shape: Shape = AuthFieldShape, +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + label = label?.let { { Text(it) } }, + placeholder = placeholder?.let { { Text(it) } }, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + isError = isError, + supportingText = supportingText?.let { { Text(it) } }, + singleLine = singleLine, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + 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/common/CtaButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/CtaButton.kt new file mode 100644 index 000000000..94317a9e1 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/CtaButton.kt @@ -0,0 +1,54 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +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 +import com.firebaseui.android.demo.auth.fullcustomization.theme.ButtonShape + +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/common/HardOffsetShadow.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/HardOffsetShadow.kt new file mode 100644 index 000000000..628bffafd --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/HardOffsetShadow.kt @@ -0,0 +1,32 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +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/common/OtherSignInMethodsSheet.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/OtherSignInMethodsSheet.kt new file mode 100644 index 000000000..16e5ca91a --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/OtherSignInMethodsSheet.kt @@ -0,0 +1,70 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +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/common/SheetProviderButton.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/SheetProviderButton.kt new file mode 100644 index 000000000..4b8583543 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/common/SheetProviderButton.kt @@ -0,0 +1,121 @@ +package com.firebaseui.android.demo.auth.fullcustomization.common + +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 +import com.firebaseui.android.demo.auth.fullcustomization.theme.ProviderButtonShape + +@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/screens/AuthMethodPickerUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthMethodPickerUI.kt new file mode 100644 index 000000000..fc316bde5 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/AuthMethodPickerUI.kt @@ -0,0 +1,106 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens + +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.firebaseui.android.demo.auth.fullcustomization.common.OtherSignInMethodsSheet +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.EmailEntryStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.LoginStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages.SignUpStep +import com.google.firebase.auth.FirebaseAuth +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +private enum class FlowStep { EnterEmail, Login, SignUp } + +@Composable +fun AuthMethodPickerUI( + 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("AuthMethodPickerUI", "fetchSignInMethodsForEmail failed for $email", e) + emptyList() + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt new file mode 100644 index 000000000..65a933312 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/EmailEntryStep.kt @@ -0,0 +1,184 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +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 +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow +import com.firebaseui.android.demo.auth.fullcustomization.theme.IntroShape + +@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/screens/email/pages/LoginStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/LoginStep.kt new file mode 100644 index 000000000..f26a83260 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/LoginStep.kt @@ -0,0 +1,193 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +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 +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@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/screens/email/pages/SignUpStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/SignUpStep.kt new file mode 100644 index 000000000..cbab9bff0 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/email/pages/SignUpStep.kt @@ -0,0 +1,229 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.email.pages + +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 +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.EmailFieldIcon +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +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/screens/phone/PhoneSignInUI.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/PhoneSignInUI.kt new file mode 100644 index 000000000..ecf4bc41a --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/PhoneSignInUI.kt @@ -0,0 +1,30 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthStep +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages.PhoneEntryStep +import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages.PhoneVerificationStep + +@Composable +fun PhoneSignInUI(state: PhoneAuthContentState) { + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(id = R.drawable.custom_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> PhoneEntryStep(state) + PhoneAuthStep.EnterVerificationCode -> PhoneVerificationStep(state) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt new file mode 100644 index 000000000..38289f1d1 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneEntryStep.kt @@ -0,0 +1,166 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +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.requiredHeight +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.components.CountrySelector +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.FullCustomizationTextField +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@Composable +fun PhoneEntryStep(state: PhoneAuthContentState) { + val isPhoneValid = remember(state.phoneNumber) { + android.util.Patterns.PHONE.matcher(state.phoneNumber).matches() + } + + // 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 CTA 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_phone_mascot), + contentDescription = "doggo - cute phone sign-in mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Login by phone number", + 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 = "phone - sign in card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Surface( + color = Color.White, + shape = AuthFieldShape, + modifier = Modifier + .requiredHeight(56.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + shape = AuthFieldShape, + ) + .semantics { contentDescription = "country code selector" }, + ) { + CountrySelector( + selectedCountry = state.selectedCountry, + onCountrySelected = state.onCountrySelected, + enabled = !state.isLoading, + ) + } + + FullCustomizationTextField( + value = state.phoneNumber, + onValueChange = state.onPhoneNumberChange, + placeholder = "Phone number", + leadingIcon = { + Icon( + imageVector = Icons.Default.Phone, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + enabled = !state.isLoading, + isError = state.error != null, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + modifier = Modifier + .weight(1f) + .semantics { contentDescription = "text-field - phone number input" }, + ) + } + + Text( + text = state.error + ?: "By signing in with phone number, an SMS may be sent. " + + "Message & data rates may apply.", + style = MaterialTheme.typography.bodyLarge, + color = if (state.error != null) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + CtaButton( + text = "Sign Up", + onClick = state.onSendCodeClick, + enabled = isPhoneValid && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - send verification code" }, + ) + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt new file mode 100644 index 000000000..8e9a15318 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/screens/phone/pages/PhoneVerificationStep.kt @@ -0,0 +1,137 @@ +package com.firebaseui.android.demo.auth.fullcustomization.screens.phone.pages + +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.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.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.style.TextAlign +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.ui.components.VerificationCodeInputField +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebaseui.android.demo.R +import com.firebaseui.android.demo.auth.fullcustomization.common.AuthFieldShape +import com.firebaseui.android.demo.auth.fullcustomization.common.CtaButton +import com.firebaseui.android.demo.auth.fullcustomization.common.HardOffsetShadow + +@Composable +fun PhoneVerificationStep(state: PhoneAuthContentState) { + 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_phone_mascot), + contentDescription = "doggo - cute phone sign-in mascot", + modifier = Modifier.size(72.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Enter your code", + 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 = "phone - verification card" }, + color = MaterialTheme.colorScheme.surface, + shape = AuthFieldShape, + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "We sent a code to ${state.fullPhoneNumber}.", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + VerificationCodeInputField( + modifier = Modifier + .fillMaxWidth() + .semantics { contentDescription = "text-field - verification code input" }, + isError = state.error != null, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange, + ) + + Text( + text = if (state.resendTimer > 0) { + "Resend code in ${state.resendTimer}s" + } else { + "Resend code" + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = state.resendTimer == 0) { + state.onResendCodeClick() + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) + + Column(modifier = Modifier.fillMaxWidth()) { + CtaButton( + text = "Verify", + onClick = state.onVerifyCodeClick, + enabled = state.verificationCode.isNotBlank() && !state.isLoading, + isLoading = state.isLoading, + modifier = Modifier.semantics { contentDescription = "button - verify code" }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + TextButton( + onClick = state.onChangeNumberClick, + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Use a different number") + } + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt new file mode 100644 index 000000000..483b0b741 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationShapes.kt @@ -0,0 +1,8 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +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/theme/FullCustomizationTheme.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTheme.kt new file mode 100644 index 000000000..53b916119 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTheme.kt @@ -0,0 +1,135 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +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/theme/FullCustomizationTypography.kt b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTypography.kt new file mode 100644 index 000000000..da31b8bc2 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/auth/fullcustomization/theme/FullCustomizationTypography.kt @@ -0,0 +1,59 @@ +package com.firebaseui.android.demo.auth.fullcustomization.theme + +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/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-xhdpi/full_customization_phone_mascot.png b/app/src/main/res/drawable-xhdpi/full_customization_phone_mascot.png new file mode 100644 index 000000000..6dee4a852 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/full_customization_phone_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 9d43d2b67..17b7decd4 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 @@ -217,6 +217,8 @@ fun FirebaseAuthScreen( ) { composable(AuthRoute.MethodPicker.route) { 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) } diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts index 7ac13aeba..d3cfc2d40 100644 --- a/storage/build.gradle.kts +++ b/storage/build.gradle.kts @@ -57,4 +57,4 @@ dependencies { testImplementation(libs.junit) testImplementation(libs.mockito.core) -} \ No newline at end of file +}