Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e342ebc
wip: demo
demolaf Jul 28, 2026
58789ab
sample(auth): add MFA enrollment/challenge, reauth, and authenticated…
demolaf Aug 13, 2026
705cf31
fix(sample): address review feedback on full customization demo
demolaf Aug 21, 2026
c804294
sample(auth): drop recovery codes screen following its removal from M…
demolaf Aug 29, 2026
e81ae6a
sample(auth): label the two-factor CTA from the user's enrolled factors
demolaf Aug 30, 2026
d24ba49
sample(auth): migrate full customization reauth screen to ReauthConte…
demolaf Aug 25, 2026
dbd90b2
sample(auth): render the demo's own email UI for library-initiated em…
demolaf Aug 31, 2026
df16326
sample(auth): ask sign in or create account instead of probing for th…
demolaf Sep 1, 2026
c42db43
sample(auth): carry the typed email into the next step and ungate cre…
demolaf Sep 1, 2026
b1c2e3e
sample(auth): host the primary email flow on its own per-mode Navigat…
demolaf Sep 2, 2026
91efdab
sample(auth): match the swipe-back animation to the tapped one in the…
demolaf Sep 2, 2026
675f746
sample(auth): give reauthentication its own email step in the full-cu…
demolaf Sep 2, 2026
f1097e3
sample(auth): route a returning email link to the demo that sent it
demolaf Sep 7, 2026
978f9a1
sample(auth): treat a declined reauthentication as a cancellation, no…
demolaf Sep 9, 2026
0f23c45
sample(auth): trim the demo's inline comments to one line each
demolaf Sep 9, 2026
d93e4f5
sample(auth): let each demo own its email link tag and move the routi…
demolaf Sep 9, 2026
4ee4f80
sample(auth): drop the stale ContinueUrlBuilder rationale from the em…
demolaf Sep 21, 2026
6c162a1
sample(auth): tag the email link origin with a query param now that t…
demolaf Sep 21, 2026
8f0f365
sample(auth): convert the full customization art to webp and stop lin…
demolaf Sep 21, 2026
7223440
sample(auth): file the full customization art as nodpi instead of sup…
demolaf Sep 21, 2026
2c50b3a
sample(auth): restore the emulator flag and label the phone entry CTA…
demolaf Sep 22, 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 @@ -91,6 +90,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 @@ -93,6 +93,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
14 changes: 11 additions & 3 deletions app/src/main/java/com/firebaseui/android/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ 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
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 @@ -67,9 +67,15 @@ import com.firebase.ui.auth.util.EmailLinkConstants
import com.firebase.ui.auth.util.displayIdentifier
import com.firebase.ui.auth.util.getDisplayEmail
import com.firebaseui.android.demo.R
import com.firebaseui.android.demo.utils.EMAIL_LINK_ORIGIN_PARAM
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 +137,9 @@ class HighLevelApiDemoActivity : ComponentActivity() {
isEmailLinkForceSameDeviceEnabled = false,
isEmailLinkSignInEnabled = true,
emailLinkActionCodeSettings = actionCodeSettings {
url = "https://flutterfire-e2e-tests.firebaseapp.com"
// This tag is what MainActivity routes the return trip on.
url = "https://flutterfire-e2e-tests.firebaseapp.com" +
"?$EMAIL_LINK_ORIGIN_PARAM=$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,182 @@
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.utils.EMAIL_LINK_ORIGIN_PARAM
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 {
// This tag is what MainActivity routes the return trip on.
url = "https://flutterfire-e2e-tests.firebaseapp.com" +
"?$EMAIL_LINK_ORIGIN_PARAM=$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