Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c2df8d2
wip: demo
demolaf Jul 28, 2026
d578013
sample(auth): add MFA enrollment/challenge, reauth, and authenticated…
demolaf Aug 13, 2026
6d074df
fix(sample): address review feedback on full customization demo
demolaf Aug 21, 2026
abb8a7f
sample(auth): drop recovery codes screen following its removal from M…
demolaf Aug 29, 2026
7104394
sample(auth): label the two-factor CTA from the user's enrolled factors
demolaf Aug 30, 2026
da19bae
sample(auth): migrate full customization reauth screen to ReauthConte…
demolaf Aug 25, 2026
217527f
sample(auth): render the demo's own email UI for library-initiated em…
demolaf Aug 31, 2026
098dfde
sample(auth): ask sign in or create account instead of probing for th…
demolaf Sep 1, 2026
3d4c67d
sample(auth): carry the typed email into the next step and ungate cre…
demolaf Sep 1, 2026
72c6b3d
sample(auth): host the primary email flow on its own per-mode Navigat…
demolaf Sep 2, 2026
be274a4
sample(auth): match the swipe-back animation to the tapped one in the…
demolaf Sep 2, 2026
0e1a512
sample(auth): give reauthentication its own email step in the full-cu…
demolaf Sep 2, 2026
830429f
sample(auth): route a returning email link to the demo that sent it
demolaf Sep 7, 2026
3bdadc5
sample(auth): treat a declined reauthentication as a cancellation, no…
demolaf Sep 9, 2026
c7ddaee
sample(auth): trim the demo's inline comments to one line each
demolaf Sep 9, 2026
66f8ffe
sample(auth): let each demo own its email link tag and move the routi…
demolaf Sep 9, 2026
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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.gms.google-services")
id("kotlin-kapt")
// The slot demos host the auth screens on their own Navigation 3 back stacks, and a
// rememberNavBackStack key has to be @Serializable to survive process death.
// Nav3 back-stack keys must be @Serializable to survive process death.
alias(libs.plugins.kotlin.serialization)
}

Expand Down Expand Up @@ -71,6 +70,7 @@ dependencies {
implementation(libs.compose.ui.graphics)
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)

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

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

<!-- Database -->
<activity
android:name="com.firebaseui.android.demo.database.DatabaseDemoActivity"
Expand Down
16 changes: 12 additions & 4 deletions app/src/main/java/com/firebaseui/android/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.util.EmailLinkConstants
import com.firebaseui.android.demo.auth.AuthChooserActivity
import com.firebaseui.android.demo.auth.HighLevelApiDemoActivity
import com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity
import com.firebaseui.android.demo.database.DatabaseDemoActivity
import com.firebaseui.android.demo.firestore.FirestoreDemoActivity
import com.firebaseui.android.demo.storage.StorageDemoActivity
import com.firebaseui.android.demo.utils.emailLinkOrigin
import com.google.firebase.FirebaseApp
import com.google.firebase.database.FirebaseDatabase
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 Expand Up @@ -85,8 +87,14 @@ class MainActivity : ComponentActivity() {

Log.d("MainActivity", "Pending email link: $pendingEmailLink")

fun launchHighLevelDemo() {
val demoIntent = Intent(this, HighLevelApiDemoActivity::class.java).apply {
fun launchDemoForEmailLink() {
val target = when (emailLinkOrigin(pendingEmailLink)) {
FullCustomizationDemoActivity.EMAIL_LINK_ORIGIN ->
FullCustomizationDemoActivity::class.java
// Untagged links predate this routing, so they belong to the demo that had them.
else -> HighLevelApiDemoActivity::class.java
}
val demoIntent = Intent(this, target).apply {
pendingEmailLink?.let { link ->
putExtra(EmailLinkConstants.EXTRA_EMAIL_LINK, link)
pendingEmailLink = null
Expand All @@ -96,7 +104,7 @@ class MainActivity : ComponentActivity() {
}

if (savedInstanceState == null && !pendingEmailLink.isNullOrEmpty()) {
launchHighLevelDemo()
launchDemoForEmailLink()
finish()
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class CustomMethodPickerDemoActivity : ComponentActivity() {
Log.d("CustomMethodPickerDemo", "Auth cancelled")
},
customMethodPickerLayout = { providers, onProviderSelected ->
// Owns the whole screen now, so the terms checkbox is inline here.
SpotlightMethodPicker(
providers = providers,
onProviderSelected = onProviderSelected,
Expand Down Expand Up @@ -181,6 +182,7 @@ fun SpotlightMethodPicker(
val anonymous = groups["anonymous"]?.firstOrNull()

LazyColumn(
// Owns the whole screen, so it handles its own insets.
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding(),
Expand Down Expand Up @@ -298,7 +300,7 @@ fun SpotlightMethodPicker(
}

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

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

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ import com.firebaseui.android.demo.R
import com.google.firebase.auth.actionCodeSettings

class HighLevelApiDemoActivity : ComponentActivity() {
companion object {
/** Marks this demo's email links so MainActivity can route the return trip back here. */
const val EMAIL_LINK_ORIGIN = "highlevel"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
Expand Down Expand Up @@ -131,7 +136,9 @@ class HighLevelApiDemoActivity : ComponentActivity() {
isEmailLinkForceSameDeviceEnabled = false,
isEmailLinkSignInEnabled = true,
emailLinkActionCodeSettings = actionCodeSettings {
url = "https://flutterfire-e2e-tests.firebaseapp.com"
// Trailing segment is what MainActivity routes the return trip on.
url = "https://flutterfire-e2e-tests.firebaseapp.com/demo/" +
EMAIL_LINK_ORIGIN
handleCodeInApp = true
setAndroidPackageName(
"com.firebaseui.android.demo",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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
import com.firebase.ui.auth.util.EmailLinkConstants
import com.google.firebase.auth.actionCodeSettings

class FullCustomizationDemoActivity : ComponentActivity() {
companion object {
/** Marks this demo's email links so MainActivity can route the return trip back here. */
const val EMAIL_LINK_ORIGIN = "fullcustomization"
}

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

val authUI = FirebaseAuthUI.getInstance()
// MainActivity owns the deep link and hands it on once it works out which demo sent it.
val emailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK)
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(
isEmailLinkSignInEnabled = true,
emailLinkActionCodeSettings = actionCodeSettings {
// Trailing segment is what MainActivity routes the return trip on.
url = "https://flutterfire-e2e-tests.firebaseapp.com/demo/" +
EMAIL_LINK_ORIGIN
handleCodeInApp = true
setAndroidPackageName(
"com.firebaseui.android.demo",
true,
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,
emailLink = emailLink,
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,
)
},
// Covers the email flows the library navigates to itself.
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()) {
// AuthMethodPickerUI builds its own EmailAuthScreen per step, so none is needed here.
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
Loading