From 9538b173e60c978a408d6d56d09b7bd235339254 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 20 May 2026 16:22:41 +0100 Subject: [PATCH 1/4] fix(auth): add configurable params for custom UI - AuthMethodPicker - EmailAuth - PhoneAuth - MFA Enrollment & Challenge --- .../ui/auth/ui/method_picker/AuthMethodPicker.kt | 2 +- .../ui/auth/ui/screens/FirebaseAuthScreen.kt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index bf4c3b6a5..c1ed2ac2e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -77,10 +77,10 @@ fun AuthMethodPicker( providers: List, logo: AuthUIAsset? = null, onProviderSelected: (AuthProvider) -> Unit, - customLayout: @Composable ((List, (AuthProvider) -> Unit) -> Unit)? = null, termsOfServiceUrl: String? = null, privacyPolicyUrl: String? = null, lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, + customLayout: @Composable ((List, (AuthProvider) -> Unit) -> Unit)? = null, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current 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 7a67b977e..45a43a4e5 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 @@ -74,8 +74,12 @@ import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvi import com.firebase.ui.auth.configuration.theme.LocalAuthUITheme import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController import com.firebase.ui.auth.ui.components.rememberTopLevelDialogController +import com.firebase.ui.auth.mfa.MfaChallengeContentState +import com.firebase.ui.auth.mfa.MfaEnrollmentContentState import com.firebase.ui.auth.ui.method_picker.AuthMethodPicker +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen import com.firebase.ui.auth.util.EmailLinkPersistenceManager import com.firebase.ui.auth.util.SignInPreferenceManager @@ -108,6 +112,11 @@ fun FirebaseAuthScreen( emailLink: String? = null, mfaConfiguration: MfaConfiguration = MfaConfiguration(), authenticatedContent: (@Composable (state: AuthState, uiContext: AuthSuccessUiContext) -> Unit)? = null, + customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, + emailContent: (@Composable (EmailAuthContentState) -> Unit)? = null, + phoneContent: (@Composable (PhoneAuthContentState) -> Unit)? = null, + mfaEnrollmentContent: (@Composable (MfaEnrollmentContentState) -> Unit)? = null, + mfaChallengeContent: (@Composable (MfaChallengeContentState) -> Unit)? = null, ) { // Set FirebaseUI version LaunchedEffect(authUI.auth) { @@ -267,6 +276,7 @@ fun FirebaseAuthScreen( termsOfServiceUrl = configuration.tosUrl, privacyPolicyUrl = configuration.privacyPolicyUrl, lastSignInPreference = lastSignInPreference.value, + customLayout = customMethodPickerLayout, onProviderSelected = { provider -> when (provider) { is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() @@ -318,6 +328,7 @@ fun FirebaseAuthScreen( authUI = authUI, credentialForLinking = pendingLinkingCredential.value, emailLinkFromDifferentDevice = emailLinkFromDifferentDevice.value, + content = emailContent, onSuccess = { pendingLinkingCredential.value = null }, @@ -343,6 +354,7 @@ fun FirebaseAuthScreen( context = context, configuration = configuration, authUI = authUI, + content = phoneContent, onSuccess = {}, onError = { exception -> onSignInFailure(exception) @@ -450,6 +462,7 @@ fun FirebaseAuthScreen( auth = authUI.auth, configuration = mfaConfiguration, authConfiguration = configuration, + content = mfaEnrollmentContent, onComplete = { navController.popBackStack() }, onSkip = { navController.popBackStack() }, onError = { exception -> @@ -467,6 +480,7 @@ fun FirebaseAuthScreen( MfaChallengeScreen( resolver = resolver, auth = authUI.auth, + content = mfaChallengeContent, onSuccess = { pendingResolver.value = null // Reset auth state to Idle so the firebaseAuthFlow Success state takes over From 128ef886a9215b9e7ecac5d8673e87da924b49e5 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 20 May 2026 16:22:57 +0100 Subject: [PATCH 2/4] fix(auth): add demo activities for email and phone authentication with customizable UI --- app/src/main/AndroidManifest.xml | 24 + .../demo/CustomMethodPickerDemoActivity.kt | 314 +++++ .../demo/CustomSlotsThemingDemoActivity.kt | 1107 +---------------- .../android/demo/EmailAuthSlotDemoActivity.kt | 442 +++++++ .../android/demo/PhoneAuthSlotDemoActivity.kt | 341 +++++ .../demo/ShapeCustomizationDemoActivity.kt | 263 ++++ 6 files changed, 1452 insertions(+), 1039 deletions(-) create mode 100644 app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt create mode 100644 app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt create mode 100644 app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt create mode 100644 app/src/main/java/com/firebaseui/android/demo/ShapeCustomizationDemoActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7403aaf50..72f1b8025 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,30 @@ android:label="Custom Slots & Theming Demo" android:exported="false" android:theme="@style/Theme.FirebaseUIAndroid" /> + + + + + + + + diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt new file mode 100644 index 000000000..151a57949 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -0,0 +1,314 @@ +package com.firebaseui.android.demo + +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.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +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.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebase.ui.auth.ui.components.AuthProviderButton +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen + +class CustomMethodPickerDemoActivity : 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.GenericOAuth( + providerName = "Discord", + providerId = "oidc.discord", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with Discord", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_discord_24dp), + buttonColor = Color(0xFF5865F2), + contentColor = Color.White + ) + ) + provider( + AuthProvider.GenericOAuth( + providerName = "LINE", + providerId = "oidc.line", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Sign in with LINE", + buttonIcon = AuthUIAsset.Resource(R.drawable.ic_line_logo_24dp), + buttonColor = Color(0xFF06C755), + contentColor = Color.White + ) + ) + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + ) + provider(AuthProvider.Anonymous) + } + } + + setContent { + AuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = { result -> + Log.d("CustomMethodPickerDemo", "Auth success: ${result.user?.uid}") + }, + onSignInFailure = { exception: AuthException -> + Log.e("CustomMethodPickerDemo", "Auth failed", exception) + }, + onSignInCancelled = { + Log.d("CustomMethodPickerDemo", "Auth cancelled") + }, + customMethodPickerLayout = { providers, onProviderSelected -> + SpotlightMethodPicker( + providers = providers, + onProviderSelected = onProviderSelected + ) + } + ) + } + } + } + } +} + +@Composable +fun SpotlightMethodPicker( + providers: List, + onProviderSelected: (AuthProvider) -> Unit, +) { + val stringProvider = LocalAuthUIStringProvider.current + + val featured = providers.filter { it is AuthProvider.Google || it is AuthProvider.Apple } + val social = providers.filter { + it !is AuthProvider.Google && it !is AuthProvider.Apple && + it !is AuthProvider.Email && it !is AuthProvider.Phone && + it !is AuthProvider.Anonymous + } + val credential = providers.filter { it is AuthProvider.Email || it is AuthProvider.Phone } + val anonymous = providers.filterIsInstance().firstOrNull() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 48.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Text( + text = "Sign in", + style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Choose how you'd like to continue", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(24.dp)) + } + + items(featured) { provider -> + AuthProviderButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + provider = provider, + onClick = { onProviderSelected(provider) }, + stringProvider = stringProvider + ) + } + + if (social.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + } + item { + LazyRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items(social) { provider -> + val style = styleForProvider(provider) + ProviderIconButton( + style = style, + contentDescription = provider.providerId, + onClick = { onProviderSelected(provider) } + ) + } + } + } + } + + if (credential.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(4.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + } + items(credential) { provider -> + AuthProviderButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + provider = provider, + onClick = { onProviderSelected(provider) }, + stringProvider = stringProvider + ) + } + } + + anonymous?.let { + item { + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { onProviderSelected(it) }) { + Text("Continue as guest") + } + } + } + } +} + +@Composable +private fun ProviderIconButton( + style: AuthUITheme.ProviderStyle, + contentDescription: String, + onClick: () -> Unit, +) { + Button( + onClick = onClick, + modifier = Modifier.size(52.dp), + shape = CircleShape, + colors = ButtonDefaults.buttonColors(containerColor = style.backgroundColor), + contentPadding = PaddingValues(0.dp), + elevation = ButtonDefaults.buttonElevation(defaultElevation = style.elevation) + ) { + style.icon?.let { asset -> + val painter = asset.asPainter() + val tint = style.iconTint + if (tint != null) { + Icon( + painter = painter, + contentDescription = contentDescription, + tint = tint, + modifier = Modifier.size(22.dp) + ) + } else { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = Modifier.size(22.dp) + ) + } + } + } +} + +@Composable +private 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) { + 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.GenericOAuth -> AuthUITheme.ProviderStyle( + icon = provider.buttonIcon, + backgroundColor = provider.buttonColor ?: Color(0xFF666666), + contentColor = provider.contentColor ?: Color.White + ) + else -> AuthUITheme.ProviderStyle( + icon = null, + backgroundColor = Color(0xFF666666), + contentColor = Color.White + ) +} diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt index 00b0054f0..4aa50b05f 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt @@ -1,1105 +1,134 @@ package com.firebaseui.android.demo +import android.content.Intent 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.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -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.PasswordRule -import com.firebase.ui.auth.configuration.authUIConfiguration -import com.firebase.ui.auth.configuration.auth_provider.AuthProvider -import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider -import com.firebase.ui.auth.configuration.theme.AuthUITheme -import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults -import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider -import com.firebase.ui.auth.ui.components.AuthProviderButton -import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState -import com.firebase.ui.auth.ui.screens.email.EmailAuthMode -import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen -import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState -import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen -import com.firebase.ui.auth.ui.screens.phone.PhoneAuthStep -import com.google.firebase.auth.AuthResult -/** - * Demo activity showcasing custom slots and theming capabilities: - * - EmailAuthScreen with custom slot UI - * - PhoneAuthScreen with custom slot UI - * - Provider button shape customization with global and per-provider overrides - * - AuthUITheme.fromMaterialTheme() with custom ProviderStyle overrides - */ class CustomSlotsThemingDemoActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - val authUI = FirebaseAuthUI.getInstance() - val appContext = applicationContext - - // Configuration for email authentication - val emailConfiguration = authUIConfiguration { - context = appContext - providers { - provider( - AuthProvider.Email( - isDisplayNameRequired = true, - isNewAccountsAllowed = true, - isEmailLinkSignInEnabled = false, - emailLinkActionCodeSettings = null, - isEmailLinkForceSameDeviceEnabled = false, - minimumPasswordLength = 8, - passwordValidationRules = listOf( - PasswordRule.MinimumLength(8), - PasswordRule.RequireLowercase, - PasswordRule.RequireUppercase, - PasswordRule.RequireDigit - ) - ) - ) - } - tosUrl = "https://policies.google.com/terms" - privacyPolicyUrl = "https://policies.google.com/privacy" - } - - // Configuration for phone authentication - val phoneConfiguration = authUIConfiguration { - context = appContext - providers { - provider( - AuthProvider.Phone( - defaultNumber = null, - defaultCountryCode = "US", - allowedCountries = emptyList(), - smsCodeLength = 6, - timeout = 60L, - isInstantVerificationEnabled = true - ) - ) - } - } - setContent { - // Custom theme using fromMaterialTheme() with custom provider styles - CustomAuthUITheme { + MaterialTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - var selectedDemo by remember { mutableStateOf(DemoType.Email) } - - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - ) { - // Demo selector tabs - DemoSelector( - selectedDemo = selectedDemo, - onDemoSelected = { selectedDemo = it } - ) - - // Show selected demo - when (selectedDemo) { - DemoType.Email -> EmailAuthDemo( - authUI = authUI, - configuration = emailConfiguration, - context = appContext - ) - DemoType.Phone -> PhoneAuthDemo( - authUI = authUI, - configuration = phoneConfiguration, - context = appContext - ) - DemoType.ShapeCustomization -> ShapeCustomizationDemo() + CustomSlotsDemoChooser( + onEmailAuthSlotClick = { + startActivity(Intent(this, EmailAuthSlotDemoActivity::class.java)) + }, + onPhoneAuthSlotClick = { + startActivity(Intent(this, PhoneAuthSlotDemoActivity::class.java)) + }, + onShapeCustomizationClick = { + startActivity(Intent(this, ShapeCustomizationDemoActivity::class.java)) + }, + onCustomMethodPickerClick = { + startActivity(Intent(this, CustomMethodPickerDemoActivity::class.java)) } - } - } - } - } - } -} - -enum class DemoType { - Email, - Phone, - ShapeCustomization -} - -@Composable -fun CustomAuthUITheme(content: @Composable () -> Unit) { - // Use Material Theme colors - MaterialTheme { - // UPDATED: Now uses ProviderStyleDefaults and the new providerButtonShape API - // Apply custom theme using fromMaterialTheme with global button shape - val authTheme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(12.dp) // Global shape for all buttons - ) - - AuthUITheme(theme = authTheme) { - content() - } - } -} - -@Composable -fun DemoSelector( - selectedDemo: DemoType, - onDemoSelected: (DemoType) -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Custom Slots & Theming Demo", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - Text( - text = "Select a demo to see custom UI implementations using slot APIs", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - FilterChip( - selected = selectedDemo == DemoType.Email, - onClick = { onDemoSelected(DemoType.Email) }, - label = { Text("Email Auth") }, - modifier = Modifier.weight(1f) - ) - FilterChip( - selected = selectedDemo == DemoType.Phone, - onClick = { onDemoSelected(DemoType.Phone) }, - label = { Text("Phone Auth") }, - modifier = Modifier.weight(1f) ) } - FilterChip( - selected = selectedDemo == DemoType.ShapeCustomization, - onClick = { onDemoSelected(DemoType.ShapeCustomization) }, - label = { Text("Shape Customization") }, - modifier = Modifier.fillMaxWidth() - ) } } } } @Composable -fun EmailAuthDemo( - authUI: FirebaseAuthUI, - configuration: AuthUIConfiguration, - context: android.content.Context +fun CustomSlotsDemoChooser( + onEmailAuthSlotClick: () -> Unit, + onPhoneAuthSlotClick: () -> Unit, + onShapeCustomizationClick: () -> Unit, + onCustomMethodPickerClick: () -> Unit, ) { - var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } - - // Monitor auth state changes - LaunchedEffect(Unit) { - authUI.authStateFlow().collect { _ -> - currentUser = authUI.getCurrentUser() - } - } - - if (currentUser != null) { - // Show success screen - val successScrollState = rememberScrollState() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(successScrollState) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "✓", - style = MaterialTheme.typography.displayLarge, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Successfully Authenticated!", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = currentUser?.email ?: "Signed in", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(32.dp)) - Button(onClick = { - authUI.auth.signOut() - }) { - Text("Sign Out") - } - } - } else { - // Show custom email auth UI using slot API - // Provide the string provider required by EmailAuthScreen - CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { - EmailAuthScreen( - context = context, - configuration = configuration, - authUI = authUI, - onSuccess = { result: AuthResult -> - Log.d("CustomSlotsDemo", "Email auth success: ${result.user?.uid}") - }, - onError = { exception: AuthException -> - Log.e("CustomSlotsDemo", "Email auth error", exception) - }, - onCancel = { - Log.d("CustomSlotsDemo", "Email auth cancelled") - } - ) { state: EmailAuthContentState -> - // Custom UI using the slot API - CustomEmailAuthUI(state) - } - } - } -} - -@Composable -fun CustomEmailAuthUI(state: EmailAuthContentState) { - val scrollState = rememberScrollState() - Column( modifier = Modifier .fillMaxSize() - .verticalScroll(scrollState) + .verticalScroll(rememberScrollState()) + .systemBarsPadding() .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Spacer(modifier = Modifier.height(16.dp)) - - // Title based on mode - Text( - text = when (state.mode) { - EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> "📧 Welcome Back" - EmailAuthMode.SignUp -> "📧 Create Account" - EmailAuthMode.ResetPassword -> "📧 Reset Password" - }, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Error display - state.error?.let { errorMessage -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = errorMessage, - modifier = Modifier.padding(12.dp), - color = MaterialTheme.colorScheme.onErrorContainer, - style = MaterialTheme.typography.bodySmall - ) - } - } - - // Render UI based on mode - when (state.mode) { - EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> SignInUI(state) - EmailAuthMode.SignUp -> SignUpUI(state) - EmailAuthMode.ResetPassword -> ResetPasswordUI(state) - } - } -} - -@Composable -fun SignInUI(state: EmailAuthContentState) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = state.email, - onValueChange = state.onEmailChange, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - OutlinedTextField( - value = state.password, - onValueChange = state.onPasswordChange, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - if (state.emailSignInLinkSent) { - Text( - text = "✓ Sign-in link sent! Check your email.", - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.fillMaxWidth() - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = state.onSignInClick, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isLoading - ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Sign In") - } - } - - TextButton( - onClick = state.onGoToResetPassword, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text("Forgot Password?") - } - - HorizontalDivider() - - TextButton( - onClick = state.onGoToSignUp, - modifier = Modifier.fillMaxWidth() - ) { - Text("Don't have an account? Sign Up") - } - } -} - -@Composable -fun SignUpUI(state: EmailAuthContentState) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = state.displayName, - onValueChange = state.onDisplayNameChange, - label = { Text("Display Name") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - OutlinedTextField( - value = state.email, - onValueChange = state.onEmailChange, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - OutlinedTextField( - value = state.password, - onValueChange = state.onPasswordChange, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - OutlinedTextField( - value = state.confirmPassword, - onValueChange = state.onConfirmPasswordChange, - label = { Text("Confirm Password") }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading + Text( + text = "Custom Slots & Theming", + style = MaterialTheme.typography.headlineMedium ) - - Spacer(modifier = Modifier.height(8.dp)) - - Button( - onClick = state.onSignUpClick, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isLoading - ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Create Account") - } - } - - HorizontalDivider() - - TextButton( - onClick = state.onGoToSignIn, - modifier = Modifier.fillMaxWidth() - ) { - Text("Already have an account? Sign In") - } - } -} - -@Composable -fun ResetPasswordUI(state: EmailAuthContentState) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { Text( - text = "Enter your email address and we'll send you a link to reset your password.", + text = "Select a demo to explore slot APIs and theme customization", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = state.email, - onValueChange = state.onEmailChange, - label = { Text("Email") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading + DemoCard( + title = "Email Auth — Custom Slot", + description = "Replace the default email sign-in UI with a fully custom composable using the content slot.", + onClick = onEmailAuthSlotClick ) - if (state.resetLinkSent) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "✓ Password reset link sent! Check your email.", - modifier = Modifier.padding(12.dp), - color = MaterialTheme.colorScheme.onPrimaryContainer, - style = MaterialTheme.typography.bodyMedium - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Button( - onClick = state.onSendResetLinkClick, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isLoading && !state.resetLinkSent - ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Send Reset Link") - } - } - - HorizontalDivider() - - TextButton( - onClick = state.onGoToSignIn, - modifier = Modifier.fillMaxWidth() - ) { - Text("Back to Sign In") - } - } -} - -@Composable -fun PhoneAuthDemo( - authUI: FirebaseAuthUI, - configuration: AuthUIConfiguration, - context: android.content.Context -) { - var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } - - // Monitor auth state changes - LaunchedEffect(Unit) { - authUI.authStateFlow().collect { _ -> - currentUser = authUI.getCurrentUser() - } - } - - if (currentUser != null) { - // Show success screen - val successScrollState = rememberScrollState() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(successScrollState) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "📱", - style = MaterialTheme.typography.displayLarge, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Phone Verified!", - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = currentUser?.phoneNumber ?: "Signed in", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(32.dp)) - Button(onClick = { - authUI.auth.signOut() - }) { - Text("Sign Out") - } - } - } else { - // Show custom phone auth UI using slot API - // Provide the string provider required by PhoneAuthScreen - CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { - PhoneAuthScreen( - context = context, - configuration = configuration, - authUI = authUI, - onSuccess = { result: AuthResult -> - Log.d("CustomSlotsDemo", "Phone auth success: ${result.user?.uid}") - }, - onError = { exception: AuthException -> - Log.e("CustomSlotsDemo", "Phone auth error", exception) - }, - onCancel = { - Log.d("CustomSlotsDemo", "Phone auth cancelled") - } - ) { state: PhoneAuthContentState -> - // Custom UI using the slot API - CustomPhoneAuthUI(state) - } - } - } -} - -@Composable -fun CustomPhoneAuthUI(state: PhoneAuthContentState) { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Spacer(modifier = Modifier.height(16.dp)) - - // Title based on step - Text( - text = when (state.step) { - PhoneAuthStep.EnterPhoneNumber -> "📱 Phone Verification" - PhoneAuthStep.EnterVerificationCode -> "📱 Enter Code" - }, - style = MaterialTheme.typography.headlineMedium, - color = MaterialTheme.colorScheme.onSurface + DemoCard( + title = "Phone Auth — Custom Slot", + description = "Replace the default phone auth UI with a fully custom composable using the content slot.", + onClick = onPhoneAuthSlotClick ) - Spacer(modifier = Modifier.height(8.dp)) - - // Error display - state.error?.let { errorMessage -> - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.errorContainer - ), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = errorMessage, - modifier = Modifier.padding(12.dp), - color = MaterialTheme.colorScheme.onErrorContainer, - style = MaterialTheme.typography.bodySmall - ) - } - } - - // Render UI based on step - when (state.step) { - PhoneAuthStep.EnterPhoneNumber -> EnterPhoneNumberUI(state) - PhoneAuthStep.EnterVerificationCode -> EnterVerificationCodeUI(state) - } - } -} - -@Composable -fun EnterPhoneNumberUI(state: PhoneAuthContentState) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Enter your phone number to receive a verification code", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + DemoCard( + title = "Shape Customization", + description = "Preview provider button shapes using global and per-provider overrides via AuthUITheme.", + onClick = onShapeCustomizationClick ) - Spacer(modifier = Modifier.height(8.dp)) - - // Country selector (simplified for demo) - OutlinedCard( - onClick = { /* In real app, open country selector */ }, - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "${state.selectedCountry.flagEmoji} ${state.selectedCountry.dialCode}", - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = state.selectedCountry.name, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - OutlinedTextField( - value = state.phoneNumber, - onValueChange = state.onPhoneNumberChange, - label = { Text("Phone Number") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading + DemoCard( + title = "Custom Method Picker Layout", + description = "Replace the default vertical provider list with a 2-column grid using customMethodPickerLayout on FirebaseAuthScreen.", + onClick = onCustomMethodPickerClick ) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = state.onSendCodeClick, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isLoading && state.phoneNumber.isNotBlank() - ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Send Code") - } - } } } @Composable -fun EnterVerificationCodeUI(state: PhoneAuthContentState) { - Column( +private fun DemoCard( + title: String, + description: String, + onClick: () -> Unit, +) { + Card( modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) + onClick = onClick ) { - Text( - text = "We sent a verification code to:", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Text( - text = state.fullPhoneNumber, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(8.dp)) - - OutlinedTextField( - value = state.verificationCode, - onValueChange = state.onVerificationCodeChange, - label = { Text("6-Digit Code") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !state.isLoading - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Button( - onClick = state.onVerifyCodeClick, - modifier = Modifier.fillMaxWidth(), - enabled = !state.isLoading && state.verificationCode.length == 6 - ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Verify Code") - } - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - TextButton(onClick = state.onChangeNumberClick) { - Text("Change Number") - } - - TextButton( - onClick = state.onResendCodeClick, - enabled = state.resendTimer == 0 - ) { - Text( - if (state.resendTimer > 0) - "Resend (${state.resendTimer}s)" - else - "Resend Code" - ) - } - } - } -} - -/** - * Demo showcasing provider button shape customization capabilities. - * Demonstrates: - * - Global shape configuration for all buttons - * - Per-provider shape overrides - * - Using ProviderStyleDefaults with .copy() - */ -@Composable -fun ShapeCustomizationDemo() { - val context = androidx.compose.ui.platform.LocalContext.current - val stringProvider = DefaultAuthUIStringProvider(context) - var selectedPreset by remember { mutableStateOf(ShapePreset.DEFAULT) } - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - // Title and description - Text( - text = "Provider Button Shape Customization", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.primary - ) - - Text( - text = "This demo showcases the new shape customization API for provider buttons. " + - "You can set a global shape for all buttons or customize individual providers.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - HorizontalDivider() - - // Preset selector - Text( - text = "Select Shape Preset:", - style = MaterialTheme.typography.titleMedium - ) - - ShapePreset.entries.forEach { preset -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedPreset == preset, - onClick = { selectedPreset = preset } - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = preset.displayName, - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = preset.description, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - HorizontalDivider() - - // Preview section - Text( - text = "Preview:", - style = MaterialTheme.typography.titleMedium - ) - - // Render buttons with the selected preset - when (selectedPreset) { - ShapePreset.DEFAULT -> DefaultShapeButtons(stringProvider) - ShapePreset.DEFAULT_COPY -> DefaultCopyShapeButtons(stringProvider) - ShapePreset.DARK_COPY -> DarkCopyShapeButtons(stringProvider) - ShapePreset.FROM_MATERIAL -> FromMaterialThemeButtons(stringProvider) - ShapePreset.PILL -> PillShapeButtons(stringProvider) - ShapePreset.MIXED -> MixedShapeButtons(stringProvider) - } - - // Code example - HorizontalDivider() - - Text( - text = "Code Example:", - style = MaterialTheme.typography.titleMedium - ) - - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp) + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Text(text = title, style = MaterialTheme.typography.titleMedium) Text( - text = selectedPreset.codeExample, - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace - ), - modifier = Modifier.padding(12.dp) + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } - -enum class ShapePreset( - val displayName: String, - val description: String, - val codeExample: String -) { - DEFAULT( - "Default Shapes", - "Uses the standard 4dp rounded corners", - """ -// No customization needed -val theme = AuthUITheme.Default - """.trimIndent() - ), - DEFAULT_COPY( - "Default.copy()", - "Customize default light theme with .copy()", - """ -val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(12.dp) -) - """.trimIndent() - ), - DARK_COPY( - "DefaultDark.copy()", - "Customize default dark theme with .copy()", - """ -val theme = AuthUITheme.DefaultDark.copy( - providerButtonShape = RoundedCornerShape(16.dp) -) - """.trimIndent() - ), - FROM_MATERIAL( - "fromMaterialTheme()", - "Inherit from Material Theme", - """ -val theme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(12.dp) -) - """.trimIndent() - ), - PILL( - "Pill Shape", - "Creates pill-shaped buttons (Default.copy)", - """ -val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(28.dp) -) - """.trimIndent() - ), - MIXED( - "Mixed Shapes", - "Different shapes per provider (Default.copy)", - """ -val customStyles = mapOf( - "google.com" to ProviderStyleDefaults.Google.copy( - shape = RoundedCornerShape(24.dp) - ), - "facebook.com" to ProviderStyleDefaults.Facebook.copy( - shape = RoundedCornerShape(8.dp) - ) -) - -val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(12.dp), - providerStyles = customStyles -) - """.trimIndent() - ) -} - -@Composable -fun DefaultShapeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Default theme - no customization - AuthUITheme { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun DefaultCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Using AuthUITheme.Default.copy() to customize the light theme - val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(12.dp) - ) - AuthUITheme(theme = theme) { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun DarkCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Using AuthUITheme.DefaultDark.copy() to customize the dark theme - val theme = AuthUITheme.DefaultDark.copy( - providerButtonShape = RoundedCornerShape(16.dp) - ) - AuthUITheme(theme = theme) { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun FromMaterialThemeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Using AuthUITheme.fromMaterialTheme() to inherit from Material Theme - val theme = AuthUITheme.fromMaterialTheme( - providerButtonShape = RoundedCornerShape(12.dp) - ) - AuthUITheme(theme = theme) { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun PillShapeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Pill-shaped buttons using Default.copy() - val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(28.dp) - ) - AuthUITheme(theme = theme) { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun MixedShapeButtons(stringProvider: DefaultAuthUIStringProvider) { - // Mixed shapes per provider using Default.copy() - val customStyles = mapOf( - "google.com" to ProviderStyleDefaults.Google.copy( - shape = RoundedCornerShape(24.dp) // Pill shape for Google - ), - "facebook.com" to ProviderStyleDefaults.Facebook.copy( - shape = RoundedCornerShape(8.dp) // Medium rounded for Facebook - ) - // Email uses global default (12dp) - ) - - val theme = AuthUITheme.Default.copy( - providerButtonShape = RoundedCornerShape(12.dp), - providerStyles = customStyles - ) - - AuthUITheme(theme = theme) { - ButtonPreviewColumn(stringProvider) - } -} - -@Composable -fun ButtonPreviewColumn(stringProvider: DefaultAuthUIStringProvider) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - AuthProviderButton( - provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null), - onClick = { }, - stringProvider = stringProvider, - modifier = Modifier.fillMaxWidth() - ) - - AuthProviderButton( - provider = AuthProvider.Facebook(), - onClick = { }, - stringProvider = stringProvider, - modifier = Modifier.fillMaxWidth() - ) - - AuthProviderButton( - provider = AuthProvider.Email( - emailLinkActionCodeSettings = null, - passwordValidationRules = emptyList() - ), - onClick = { }, - stringProvider = stringProvider, - modifier = Modifier.fillMaxWidth() - ) - } -} diff --git a/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt new file mode 100644 index 000000000..59cc95e8b --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt @@ -0,0 +1,442 @@ +package com.firebaseui.android.demo + +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.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +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.PasswordRule +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState +import com.firebase.ui.auth.ui.screens.email.EmailAuthMode +import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen +import com.google.firebase.auth.AuthResult + +class EmailAuthSlotDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val appContext = applicationContext + + val configuration = authUIConfiguration { + context = appContext + providers { + provider( + AuthProvider.Email( + isDisplayNameRequired = true, + isNewAccountsAllowed = true, + isEmailLinkSignInEnabled = false, + emailLinkActionCodeSettings = null, + isEmailLinkForceSameDeviceEnabled = false, + minimumPasswordLength = 8, + passwordValidationRules = listOf( + PasswordRule.MinimumLength(8), + PasswordRule.RequireLowercase, + PasswordRule.RequireUppercase, + PasswordRule.RequireDigit + ) + ) + ) + } + tosUrl = "https://policies.google.com/terms" + privacyPolicyUrl = "https://policies.google.com/privacy" + } + + setContent { + CustomAuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + EmailAuthDemo( + authUI = authUI, + configuration = configuration, + context = appContext + ) + } + } + } + } + } +} + +@Composable +fun CustomAuthUITheme(content: @Composable () -> Unit) { + MaterialTheme { + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp) + ) + AuthUITheme(theme = authTheme) { + content() + } + } +} + +@Composable +fun EmailAuthDemo( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + context: android.content.Context +) { + var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } + + LaunchedEffect(Unit) { + authUI.authStateFlow().collect { _ -> + currentUser = authUI.getCurrentUser() + } + } + + if (currentUser != null) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Successfully Authenticated!", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = currentUser?.email ?: "Signed in", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(32.dp)) + Button(onClick = { authUI.auth.signOut() }) { + Text("Sign Out") + } + } + } else { + CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { + EmailAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + onSuccess = { result: AuthResult -> + Log.d("EmailAuthSlotDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception: AuthException -> + Log.e("EmailAuthSlotDemo", "Auth error", exception) + }, + onCancel = { + Log.d("EmailAuthSlotDemo", "Auth cancelled") + } + ) { state: EmailAuthContentState -> + CustomEmailAuthUI(state) + } + } + } +} + +@Composable +fun CustomEmailAuthUI(state: EmailAuthContentState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = when (state.mode) { + EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> "Welcome Back" + EmailAuthMode.SignUp -> "Create Account" + EmailAuthMode.ResetPassword -> "Reset Password" + }, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + state.error?.let { errorMessage -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = errorMessage, + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.bodySmall + ) + } + } + + when (state.mode) { + EmailAuthMode.SignIn, EmailAuthMode.EmailLinkSignIn -> SignInUI(state) + EmailAuthMode.SignUp -> SignUpUI(state) + EmailAuthMode.ResetPassword -> ResetPasswordUI(state) + } + } +} + +@Composable +fun SignInUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + if (state.emailSignInLinkSent) { + Text( + text = "Sign-in link sent! Check your email.", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSignInClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Sign In") + } + } + + TextButton( + onClick = state.onGoToResetPassword, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text("Forgot Password?") + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignUp, + modifier = Modifier.fillMaxWidth() + ) { + Text("Don't have an account? Sign Up") + } + } +} + +@Composable +fun SignUpUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedTextField( + value = state.displayName, + onValueChange = state.onDisplayNameChange, + label = { Text("Display Name") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.password, + onValueChange = state.onPasswordChange, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + OutlinedTextField( + value = state.confirmPassword, + onValueChange = state.onConfirmPasswordChange, + label = { Text("Confirm Password") }, + visualTransformation = PasswordVisualTransformation(), + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSignUpClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Create Account") + } + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignIn, + modifier = Modifier.fillMaxWidth() + ) { + Text("Already have an account? Sign In") + } + } +} + +@Composable +fun ResetPasswordUI(state: EmailAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Enter your email address and we'll send you a link to reset your password.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = state.email, + onValueChange = state.onEmailChange, + label = { Text("Email") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + if (state.resetLinkSent) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Password reset link sent! Check your email.", + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onPrimaryContainer, + style = MaterialTheme.typography.bodyMedium + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onSendResetLinkClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && !state.resetLinkSent + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Send Reset Link") + } + } + + HorizontalDivider() + + TextButton( + onClick = state.onGoToSignIn, + modifier = Modifier.fillMaxWidth() + ) { + Text("Back to Sign In") + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt new file mode 100644 index 000000000..a092e177e --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt @@ -0,0 +1,341 @@ +package com.firebaseui.android.demo + +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.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +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.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen +import com.firebase.ui.auth.ui.screens.phone.PhoneAuthStep +import com.google.firebase.auth.AuthResult + +class PhoneAuthSlotDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + val authUI = FirebaseAuthUI.getInstance() + val appContext = applicationContext + + val configuration = authUIConfiguration { + context = appContext + providers { + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = "US", + allowedCountries = emptyList(), + smsCodeLength = 6, + timeout = 60L, + isInstantVerificationEnabled = true + ) + ) + } + } + + setContent { + CustomAuthUITheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + PhoneAuthDemo( + authUI = authUI, + configuration = configuration, + context = appContext + ) + } + } + } + } + } +} + +@Composable +fun PhoneAuthDemo( + authUI: FirebaseAuthUI, + configuration: AuthUIConfiguration, + context: android.content.Context +) { + var currentUser by remember { mutableStateOf(authUI.getCurrentUser()) } + + LaunchedEffect(Unit) { + authUI.authStateFlow().collect { _ -> + currentUser = authUI.getCurrentUser() + } + } + + if (currentUser != null) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Phone Verified!", + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = currentUser?.phoneNumber ?: "Signed in", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(32.dp)) + Button(onClick = { authUI.auth.signOut() }) { + Text("Sign Out") + } + } + } else { + CompositionLocalProvider(LocalAuthUIStringProvider provides configuration.stringProvider) { + PhoneAuthScreen( + context = context, + configuration = configuration, + authUI = authUI, + onSuccess = { result: AuthResult -> + Log.d("PhoneAuthSlotDemo", "Auth success: ${result.user?.uid}") + }, + onError = { exception: AuthException -> + Log.e("PhoneAuthSlotDemo", "Auth error", exception) + }, + onCancel = { + Log.d("PhoneAuthSlotDemo", "Auth cancelled") + } + ) { state: PhoneAuthContentState -> + CustomPhoneAuthUI(state) + } + } + } +} + +@Composable +fun CustomPhoneAuthUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> "Phone Verification" + PhoneAuthStep.EnterVerificationCode -> "Enter Code" + }, + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + state.error?.let { errorMessage -> + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = errorMessage, + modifier = Modifier.padding(12.dp), + color = MaterialTheme.colorScheme.onErrorContainer, + style = MaterialTheme.typography.bodySmall + ) + } + } + + when (state.step) { + PhoneAuthStep.EnterPhoneNumber -> EnterPhoneNumberUI(state) + PhoneAuthStep.EnterVerificationCode -> EnterVerificationCodeUI(state) + } + } +} + +@Composable +fun EnterPhoneNumberUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Enter your phone number to receive a verification code", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedCard( + onClick = { }, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "${state.selectedCountry.flagEmoji} ${state.selectedCountry.dialCode}", + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = state.selectedCountry.name, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + OutlinedTextField( + value = state.phoneNumber, + onValueChange = state.onPhoneNumberChange, + label = { Text("Phone Number") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = state.onSendCodeClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && state.phoneNumber.isNotBlank() + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Send Code") + } + } + } +} + +@Composable +fun EnterVerificationCodeUI(state: PhoneAuthContentState) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "We sent a verification code to:", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + Text( + text = state.fullPhoneNumber, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = state.verificationCode, + onValueChange = state.onVerificationCodeChange, + label = { Text("6-Digit Code") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !state.isLoading + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = state.onVerifyCodeClick, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isLoading && state.verificationCode.length == 6 + ) { + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .height(20.dp) + .padding(horizontal = 4.dp), + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text("Verify Code") + } + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton(onClick = state.onChangeNumberClick) { + Text("Change Number") + } + + TextButton( + onClick = state.onResendCodeClick, + enabled = state.resendTimer == 0 + ) { + Text( + if (state.resendTimer > 0) "Resend (${state.resendTimer}s)" + else "Resend Code" + ) + } + } + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/ShapeCustomizationDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/ShapeCustomizationDemoActivity.kt new file mode 100644 index 000000000..5faba7336 --- /dev/null +++ b/app/src/main/java/com/firebaseui/android/demo/ShapeCustomizationDemoActivity.kt @@ -0,0 +1,263 @@ +package com.firebaseui.android.demo + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebase.ui.auth.ui.components.AuthProviderButton + +class ShapeCustomizationDemoActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + MaterialTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + ShapeCustomizationDemo() + } + } + } + } + } +} + +@Composable +fun ShapeCustomizationDemo() { + val context = LocalContext.current + val stringProvider = DefaultAuthUIStringProvider(context) + var selectedPreset by remember { mutableStateOf(ShapePreset.DEFAULT) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Provider Button Shape Customization", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = "Showcases the shape customization API for provider buttons. " + + "Set a global shape for all buttons or customize individual providers.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider() + + Text(text = "Select Shape Preset:", style = MaterialTheme.typography.titleMedium) + + ShapePreset.entries.forEach { preset -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedPreset == preset, + onClick = { selectedPreset = preset } + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text(text = preset.displayName, style = MaterialTheme.typography.bodyLarge) + Text( + text = preset.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + HorizontalDivider() + + Text(text = "Preview:", style = MaterialTheme.typography.titleMedium) + + when (selectedPreset) { + ShapePreset.DEFAULT -> DefaultShapeButtons(stringProvider) + ShapePreset.DEFAULT_COPY -> DefaultCopyShapeButtons(stringProvider) + ShapePreset.DARK_COPY -> DarkCopyShapeButtons(stringProvider) + ShapePreset.FROM_MATERIAL -> FromMaterialThemeButtons(stringProvider) + ShapePreset.PILL -> PillShapeButtons(stringProvider) + ShapePreset.MIXED -> MixedShapeButtons(stringProvider) + } + + HorizontalDivider() + + Text(text = "Code Example:", style = MaterialTheme.typography.titleMedium) + + androidx.compose.material3.Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = selectedPreset.codeExample, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + modifier = Modifier.padding(12.dp) + ) + } + } +} + +enum class ShapePreset( + val displayName: String, + val description: String, + val codeExample: String +) { + DEFAULT( + "Default Shapes", + "Uses the standard 4dp rounded corners", + "// No customization needed\nval theme = AuthUITheme.Default" + ), + DEFAULT_COPY( + "Default.copy()", + "Customize default light theme with .copy()", + "val theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(12.dp)\n)" + ), + DARK_COPY( + "DefaultDark.copy()", + "Customize default dark theme with .copy()", + "val theme = AuthUITheme.DefaultDark.copy(\n providerButtonShape = RoundedCornerShape(16.dp)\n)" + ), + FROM_MATERIAL( + "fromMaterialTheme()", + "Inherit from Material Theme", + "val theme = AuthUITheme.fromMaterialTheme(\n providerButtonShape = RoundedCornerShape(12.dp)\n)" + ), + PILL( + "Pill Shape", + "Creates pill-shaped buttons (Default.copy)", + "val theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(28.dp)\n)" + ), + MIXED( + "Mixed Shapes", + "Different shapes per provider (Default.copy)", + "val customStyles = mapOf(\n \"google.com\" to ProviderStyleDefaults.Google.copy(\n shape = RoundedCornerShape(24.dp)\n ),\n \"facebook.com\" to ProviderStyleDefaults.Facebook.copy(\n shape = RoundedCornerShape(8.dp)\n )\n)\n\nval theme = AuthUITheme.Default.copy(\n providerButtonShape = RoundedCornerShape(12.dp),\n providerStyles = customStyles\n)" + ) +} + +@Composable +fun DefaultShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme { ButtonPreviewColumn(stringProvider) } +} + +@Composable +fun DefaultCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.Default.copy(providerButtonShape = RoundedCornerShape(12.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun DarkCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.DefaultDark.copy(providerButtonShape = RoundedCornerShape(16.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun FromMaterialThemeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.fromMaterialTheme(providerButtonShape = RoundedCornerShape(12.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun PillShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + AuthUITheme(theme = AuthUITheme.Default.copy(providerButtonShape = RoundedCornerShape(28.dp))) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun MixedShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + val customStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy(shape = RoundedCornerShape(24.dp)), + "facebook.com" to ProviderStyleDefaults.Facebook.copy(shape = RoundedCornerShape(8.dp)) + ) + AuthUITheme( + theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customStyles + ) + ) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun ButtonPreviewColumn(stringProvider: DefaultAuthUIStringProvider) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + AuthProviderButton( + provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + AuthProviderButton( + provider = AuthProvider.Facebook(), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + AuthProviderButton( + provider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + } +} From 2000fa8b0be28275535444b440d2beec302b8b2b Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 20 May 2026 16:56:05 +0100 Subject: [PATCH 3/4] updates --- .../ui/auth/ui/screens/FirebaseAuthScreen.kt | 2 +- .../ui/screens/FirebaseAuthScreenSlotsTest.kt | 199 ++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 auth/src/test/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreenSlotsTest.kt 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 45a43a4e5..db02e02e9 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 @@ -111,12 +111,12 @@ fun FirebaseAuthScreen( authUI: FirebaseAuthUI = FirebaseAuthUI.getInstance(), emailLink: String? = null, mfaConfiguration: MfaConfiguration = MfaConfiguration(), - authenticatedContent: (@Composable (state: AuthState, uiContext: AuthSuccessUiContext) -> Unit)? = null, customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, emailContent: (@Composable (EmailAuthContentState) -> Unit)? = null, phoneContent: (@Composable (PhoneAuthContentState) -> Unit)? = null, mfaEnrollmentContent: (@Composable (MfaEnrollmentContentState) -> Unit)? = null, mfaChallengeContent: (@Composable (MfaChallengeContentState) -> Unit)? = null, + authenticatedContent: (@Composable (state: AuthState, uiContext: AuthSuccessUiContext) -> Unit)? = null, ) { // Set FirebaseUI version LaunchedEffect(authUI.auth) { diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreenSlotsTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreenSlotsTest.kt new file mode 100644 index 000000000..0c3836ace --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreenSlotsTest.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.screens + +import android.content.Context +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Tests that [FirebaseAuthScreen] correctly forwards each customization slot to the + * appropriate sub-screen. + * + * These tests cover the fix for the API gap where slots such as [customMethodPickerLayout], + * [emailContent], and [phoneContent] were accepted by sub-screens but never reachable through + * the high-level [FirebaseAuthScreen] composable. + */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, sdk = [34]) +class FirebaseAuthScreenSlotsTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var context: Context + private lateinit var authUI: FirebaseAuthUI + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + FirebaseAuthUI.clearInstanceCache() + FirebaseApp.getApps(context).forEach { it.delete() } + FirebaseApp.initializeApp( + context, + FirebaseOptions.Builder() + .setApiKey("fake-api-key") + .setApplicationId("fake-app-id") + .setProjectId("fake-project-id") + .build() + ) + authUI = FirebaseAuthUI.getInstance() + } + + @After + fun tearDown() { + FirebaseAuthUI.clearInstanceCache() + FirebaseApp.getApps(context).forEach { + try { it.delete() } catch (_: Exception) {} + } + } + + // ============================================================================================= + // customMethodPickerLayout slot tests + // ============================================================================================= + + @Test + fun `customMethodPickerLayout is rendered when provided`() { + val configuration = authUIConfiguration { + context = this@FirebaseAuthScreenSlotsTest.context + providers { + provider(AuthProvider.Email(emailLinkActionCodeSettings = null, passwordValidationRules = emptyList())) + provider(AuthProvider.Phone(defaultNumber = null, defaultCountryCode = null, allowedCountries = null)) + } + } + + composeTestRule.setContent { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = {}, + onSignInFailure = {}, + onSignInCancelled = {}, + customMethodPickerLayout = { _, _ -> + Text( + text = "Custom Picker", + modifier = Modifier.testTag("custom_method_picker") + ) + } + ) + } + + composeTestRule.onNodeWithTag("custom_method_picker").assertIsDisplayed() + } + + @Test + fun `default method picker renders when customMethodPickerLayout is null`() { + val configuration = authUIConfiguration { + context = this@FirebaseAuthScreenSlotsTest.context + providers { + provider(AuthProvider.Email(emailLinkActionCodeSettings = null, passwordValidationRules = emptyList())) + provider(AuthProvider.Phone(defaultNumber = null, defaultCountryCode = null, allowedCountries = null)) + } + } + + composeTestRule.setContent { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = {}, + onSignInFailure = {}, + onSignInCancelled = {} + ) + } + + composeTestRule.onNodeWithTag("AuthMethodPicker LazyColumn").assertIsDisplayed() + } + + // ============================================================================================= + // emailContent slot tests + // ============================================================================================= + + @Test + fun `emailContent slot is rendered when provided`() { + val configuration = authUIConfiguration { + context = this@FirebaseAuthScreenSlotsTest.context + providers { + provider(AuthProvider.Email(emailLinkActionCodeSettings = null, passwordValidationRules = emptyList())) + } + } + + composeTestRule.setContent { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = {}, + onSignInFailure = {}, + onSignInCancelled = {}, + emailContent = { _ -> + Text( + text = "Custom Email UI", + modifier = Modifier.testTag("custom_email_slot") + ) + } + ) + } + + composeTestRule.onNodeWithTag("custom_email_slot").assertIsDisplayed() + } + + // ============================================================================================= + // phoneContent slot tests + // ============================================================================================= + + @Test + fun `phoneContent slot is rendered when provided`() { + val configuration = authUIConfiguration { + context = this@FirebaseAuthScreenSlotsTest.context + providers { + provider(AuthProvider.Phone(defaultNumber = null, defaultCountryCode = null, allowedCountries = null)) + } + } + + composeTestRule.setContent { + FirebaseAuthScreen( + configuration = configuration, + authUI = authUI, + onSignInSuccess = {}, + onSignInFailure = {}, + onSignInCancelled = {}, + phoneContent = { _ -> + Text( + text = "Custom Phone UI", + modifier = Modifier.testTag("custom_phone_slot") + ) + } + ) + } + + composeTestRule.onNodeWithTag("custom_phone_slot").assertIsDisplayed() + } +} From 89a58385d712b317f6bd9d574afedc1381b11ab2 Mon Sep 17 00:00:00 2001 From: demolaf Date: Wed, 20 May 2026 17:31:48 +0100 Subject: [PATCH 4/4] updates --- .../demo/CustomMethodPickerDemoActivity.kt | 18 +++++++++++------- .../android/demo/EmailAuthSlotDemoActivity.kt | 13 ++++--------- .../android/demo/PhoneAuthSlotDemoActivity.kt | 9 +++------ .../auth/ui/method_picker/AuthMethodPicker.kt | 2 +- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt index 151a57949..cd73ce212 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -151,14 +151,18 @@ fun SpotlightMethodPicker( ) { val stringProvider = LocalAuthUIStringProvider.current - val featured = providers.filter { it is AuthProvider.Google || it is AuthProvider.Apple } - val social = providers.filter { - it !is AuthProvider.Google && it !is AuthProvider.Apple && - it !is AuthProvider.Email && it !is AuthProvider.Phone && - it !is AuthProvider.Anonymous + val groups = providers.groupBy { + when (it) { + is AuthProvider.Google, is AuthProvider.Apple -> "featured" + is AuthProvider.Email, is AuthProvider.Phone -> "credential" + is AuthProvider.Anonymous -> "anonymous" + else -> "social" + } } - val credential = providers.filter { it is AuthProvider.Email || it is AuthProvider.Phone } - val anonymous = providers.filterIsInstance().firstOrNull() + val featured = groups.getOrElse("featured") { emptyList() } + val social = groups.getOrElse("social") { emptyList() } + val credential = groups.getOrElse("credential") { emptyList() } + val anonymous = groups["anonymous"]?.firstOrNull() LazyColumn( modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt index 59cc95e8b..cb9605621 100644 --- a/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -270,9 +271,7 @@ fun SignInUI(state: EmailAuthContentState) { ) { if (state.isLoading) { CircularProgressIndicator( - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), + modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary ) } else { @@ -351,9 +350,7 @@ fun SignUpUI(state: EmailAuthContentState) { ) { if (state.isLoading) { CircularProgressIndicator( - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), + modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary ) } else { @@ -420,9 +417,7 @@ fun ResetPasswordUI(state: EmailAuthContentState) { ) { if (state.isLoading) { CircularProgressIndicator( - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), + modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary ) } else { diff --git a/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt index a092e177e..9639beefb 100644 --- a/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -256,9 +257,7 @@ fun EnterPhoneNumberUI(state: PhoneAuthContentState) { ) { if (state.isLoading) { CircularProgressIndicator( - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), + modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary ) } else { @@ -309,9 +308,7 @@ fun EnterVerificationCodeUI(state: PhoneAuthContentState) { ) { if (state.isLoading) { CircularProgressIndicator( - modifier = Modifier - .height(20.dp) - .padding(horizontal = 4.dp), + modifier = Modifier.size(20.dp), color = MaterialTheme.colorScheme.onPrimary ) } else { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index c1ed2ac2e..083148016 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -80,7 +80,7 @@ fun AuthMethodPicker( termsOfServiceUrl: String? = null, privacyPolicyUrl: String? = null, lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, - customLayout: @Composable ((List, (AuthProvider) -> Unit) -> Unit)? = null, + customLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current