Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,10 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The full customization demo hosts its own Navigation 3 back stack for the email flow (see
// AuthMethodPickerUI.kt), and its custom NavKey needs the same @Serializable support the auth
// module already applies for its own Navigation 3 keys.
alias(libs.plugins.kotlin.serialization)
}

android {
Expand DownExpand Up@@ -68,6 +72,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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand Up@@ -298,7 +305,7 @@ fun SpotlightMethodPicker(
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
package com.firebaseui.android.demo.auth.fullcustomization

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
import com.firebase.ui.auth.configuration.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.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.email.EmailAuthUI
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

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,
),
customMethodPickerLayout = { providers, onProviderSelected ->
MainUI(
authUI = authUI,
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
)
},
// The picker hosts its own email entry; this slot covers the email
// flows the library navigates to itself (reauth, linking, recovery), which
// would otherwise render its stock screen.
emailContent = { state -> EmailAuthUI(state) },
phoneContent = { state -> PhoneSignInUI(state) },
mfaEnrollmentContent = { state -> MfaEnrollmentUI(state) },
mfaChallengeContent = { state -> MfaChallengeUI(state) },
reauthContent = { state -> ReauthUI(state) },
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()) {
// Hosts its own per-mode navigation for the email path; no wrapping EmailAuthScreen
// call needed here any more — AuthMethodPickerUI builds one instance per step itself.
AuthMethodPickerUI(
context = context,
configuration = configuration,
authUI = authUI,
otherProviders = providers.filterNot { it is AuthProvider.Email },
onProviderSelected = onProviderSelected,
onSuccess = { result ->
Log.d("FullCustomizationDemo", "Auth success: ${result.user?.uid}")
},
onError = { exception ->
Log.e("FullCustomizationDemo", "Auth failed", exception)
},
onCancel = {
Log.d("FullCustomizationDemo", "Auth cancelled")
},
)
}
}
}
Loading