Skip to content
Draft
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)

// Facebook
implementation(libs.facebook.login)
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
android:exported="false"
android:theme="@style/Theme.FirebaseUIAndroid" />

<activity
android:name="com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity"
android:label="Full Customization"
android:exported="false"
android:theme="@style/Theme.FirebaseUIAndroid" />

<!-- Database -->
<activity
android:name="com.firebaseui.android.demo.database.DatabaseDemoActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import com.google.firebase.firestore.FirebaseFirestore

class MainActivity : ComponentActivity() {
companion object {
internal const val USE_AUTH_EMULATOR = true
internal const val USE_AUTH_EMULATOR = false
private const val AUTH_EMULATOR_HOST = "10.0.2.2"
private const val AUTH_EMULATOR_PORT = 9099

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ class CustomMethodPickerDemoActivity : ComponentActivity() {
Log.d("CustomMethodPickerDemo", "Auth cancelled")
},
customMethodPickerLayout = { providers, onProviderSelected ->
// customMethodPickerLayout now renders as the entire screen (no
// built-in logo/ToS footer/inset handling), so the terms checkbox
// that used to live in customMethodPickerTermsConfiguration is
// rendered inline here instead, and this composable owns its own
// insets via Modifier.safeDrawingPadding() in SpotlightMethodPicker.
SpotlightMethodPicker(
providers = providers,
onProviderSelected = onProviderSelected,
Expand Down Expand Up @@ -181,6 +186,8 @@ fun SpotlightMethodPicker(
val anonymous = groups["anonymous"]?.firstOrNull()

LazyColumn(
// customMethodPickerLayout now renders as the entire screen, so this composable is
// responsible for its own insets.
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding(),
Expand Down Expand Up @@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand Down Expand Up @@ -335,12 +342,12 @@ private fun ProviderIconButton(
}

@Composable
private fun AuthUIAsset.asPainter(): Painter = when (this) {
fun AuthUIAsset.asPainter(): Painter = when (this) {
is AuthUIAsset.Resource -> painterResource(resId)
is AuthUIAsset.Vector -> rememberVectorPainter(image)
}

private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) {
fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle = when (provider) {
is AuthProvider.Facebook -> ProviderStyleDefaults.Facebook
is AuthProvider.Twitter -> ProviderStyleDefaults.Twitter
is AuthProvider.Github -> ProviderStyleDefaults.Github
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity

class CustomSlotsThemingDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -46,6 +47,9 @@ class CustomSlotsThemingDemoActivity : ComponentActivity() {
},
onCustomMethodPickerClick = {
startActivity(Intent(this, CustomMethodPickerDemoActivity::class.java))
},
onFullCustomizationClick = {
startActivity(Intent(this, FullCustomizationDemoActivity::class.java))
}
)
}
Expand All @@ -60,6 +64,7 @@ fun CustomSlotsDemoChooser(
onPhoneAuthSlotClick: () -> Unit,
onShapeCustomizationClick: () -> Unit,
onCustomMethodPickerClick: () -> Unit,
onFullCustomizationClick: () -> Unit,
) {
Column(
modifier = Modifier
Expand Down Expand Up @@ -106,6 +111,12 @@ fun CustomSlotsDemoChooser(
description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsConfiguration on FirebaseAuthScreen.",
onClick = onCustomMethodPickerClick
)

DemoCard(
title = "Full Customization",
description = "customMethodPickerLayout renders as the entire screen, so this layers a full-bleed background image and scrim behind the custom method picker.",
onClick = onFullCustomizationClick
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.lifecycleScope
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.MfaConfiguration
import com.firebase.ui.auth.configuration.MfaFactor
import com.firebase.ui.auth.configuration.authUIConfiguration
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider
import com.firebase.ui.auth.configuration.theme.AuthUIAsset
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen
import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen
import com.firebaseui.android.demo.R
import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthMethodPickerUI
import com.firebaseui.android.demo.auth.fullcustomization.screens.AuthenticatedUI
import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaChallengeUI
import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaEnrollmentUI
import com.firebaseui.android.demo.auth.fullcustomization.screens.phone.PhoneSignInUI
import com.firebaseui.android.demo.auth.fullcustomization.screens.reauth.ReauthUI
import com.firebaseui.android.demo.auth.fullcustomization.theme.FullCustomizationTheme
import kotlinx.coroutines.launch

class FullCustomizationDemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

val authUI = FirebaseAuthUI.getInstance()
val configuration = authUIConfiguration {
context = applicationContext
logo = AuthUIAsset.Resource(R.drawable.firebase_auth)
tosUrl = "https://policies.google.com/terms"
privacyPolicyUrl = "https://policies.google.com/privacy"
providers {
provider(
AuthProvider.Google(
scopes = listOf("email"),
serverClientId = "406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com",
)
)
provider(AuthProvider.Apple(customParameters = emptyMap(), locale = null))
provider(AuthProvider.Facebook())
provider(AuthProvider.Twitter(customParameters = emptyMap()))
provider(AuthProvider.Github(customParameters = emptyMap()))
provider(AuthProvider.Microsoft(tenant = null, customParameters = emptyMap()))
provider(AuthProvider.Yahoo(customParameters = emptyMap()))
provider(
AuthProvider.Email(
emailLinkActionCodeSettings = null,
passwordValidationRules = emptyList()
)
)
provider(
AuthProvider.Phone(
defaultNumber = null,
defaultCountryCode = null,
allowedCountries = null
)
)
provider(AuthProvider.Anonymous)
}
}

setContent {
FullCustomizationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
FirebaseAuthScreen(
configuration = configuration,
authUI = authUI,
onSignInSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onSignInFailure = { exception: AuthException ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onSignInCancelled = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
mfaConfiguration = MfaConfiguration(
allowedFactors = listOf(MfaFactor.Sms, MfaFactor.Totp),
requireEnrollment = false,
enableRecoveryCodes = true,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { reauthState, onDismiss ->
ReauthUI(
state = reauthState,
onDismiss = onDismiss,
onVerified = {
val retry = reauthState.retryOperation
// Dismiss first: onDismiss resets the auth state to Idle, so
// running the retry afterwards keeps the Success it reports
// from being clobbered. Neither call suspends, so the
// composable's cancellation can't interleave between them —
// and the retry itself runs on lifecycleScope, which outlives
// this UI.
onDismiss()
retry?.let {
lifecycleScope.launch { it(applicationContext) }
}
},
)
},
authenticatedContent = { state, uiContext ->
AuthenticatedUI(state = state, uiContext = uiContext)
},
)
}
}
}
}
}

@Composable
private fun MainUI(
authUI: FirebaseAuthUI,
configuration: AuthUIConfiguration,
providers: List<AuthProvider>,
onProviderSelected: (AuthProvider) -> Unit,
) {
val context = LocalContext.current
Box(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.drawable.custom_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
Column(modifier = Modifier.fillMaxSize()) {
EmailAuthScreen(
context = context,
configuration = configuration,
authUI = authUI,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
) { state ->
AuthMethodPickerUI(
state = state,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
tosUrl = configuration.tosUrl,
ppUrl = configuration.privacyPolicyUrl,
)
}
}
}
}
Loading