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..cd73ce212
--- /dev/null
+++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt
@@ -0,0 +1,318 @@
+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 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 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(),
+ 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..cb9605621
--- /dev/null
+++ b/app/src/main/java/com/firebaseui/android/demo/EmailAuthSlotDemoActivity.kt
@@ -0,0 +1,437 @@
+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.size
+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.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
+ )
+
+ 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.",
+ 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.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")
+ }
+ }
+}
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..9639beefb
--- /dev/null
+++ b/app/src/main/java/com/firebaseui/android/demo/PhoneAuthSlotDemoActivity.kt
@@ -0,0 +1,338 @@
+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.size
+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.size(20.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.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"
+ )
+ }
+ }
+ }
+}
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()
+ )
+ }
+}
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..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
@@ -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..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
@@ -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
@@ -107,6 +111,11 @@ fun FirebaseAuthScreen(
authUI: FirebaseAuthUI = FirebaseAuthUI.getInstance(),
emailLink: String? = null,
mfaConfiguration: MfaConfiguration = MfaConfiguration(),
+ 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
@@ -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
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()
+ }
+}