Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.firebaseui.android.demo.auth.fullcustomization

import androidx.navigation3.runtime.NavKey
import com.firebase.ui.auth.ui.screens.AuthRoute
import com.firebase.ui.auth.ui.screens.email.EmailAuthMode

/** The demo's own navigation over its email back stack; not FirebaseUI API. */
class DemoNavigator(private val backStack: MutableList<NavKey>) {

/**
* Navigates to [target], replacing any existing entry of the same step *type* rather than stacking
* a duplicate — matching [com.firebase.ui.auth.ui.screens.email.navigateToEmailStep], which the
* library keeps `internal` to its own module. Adds before removing, so no single write leaves the
* stack without the chooser at its base.
*/
fun navigate(target: AuthRoute.Email.Step) {
val existing = backStack.indexOfFirst { it is AuthRoute.Email.Step && it::class == target::class }
backStack.add(target)
if (existing >= 0) {
while (backStack.size > existing + 1) backStack.removeAt(existing)
}
}

/** For [com.firebase.ui.auth.ui.screens.email.EmailAuthScreen]'s `onNavigateToMode`. */
fun navigate(mode: EmailAuthMode, email: String?) = navigate(stepFor(mode, email))
}

private fun stepFor(mode: EmailAuthMode, email: String?): AuthRoute.Email.Step = when (mode) {
EmailAuthMode.SignIn -> AuthRoute.Email.SignIn(email)
EmailAuthMode.SignUp -> AuthRoute.Email.SignUp(email)
EmailAuthMode.ResetPassword -> AuthRoute.Email.ResetPassword(email)
EmailAuthMode.EmailLinkSignIn -> AuthRoute.Email.EmailLinkSignIn(email)
}

internal fun AuthRoute.Email.Step.toMode(): EmailAuthMode = when (this) {
is AuthRoute.Email.SignIn -> EmailAuthMode.SignIn
is AuthRoute.Email.SignUp -> EmailAuthMode.SignUp
is AuthRoute.Email.ResetPassword -> EmailAuthMode.ResetPassword
is AuthRoute.Email.EmailLinkSignIn -> EmailAuthMode.EmailLinkSignIn
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@ 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.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.FirebaseAuthUI
import com.firebase.ui.auth.configuration.AuthUIConfiguration
Expand All @@ -25,8 +32,12 @@ import com.firebase.ui.auth.configuration.authUIConfiguration
import com.firebase.ui.auth.configuration.auth_provider.AuthProvider
import com.firebase.ui.auth.configuration.theme.AuthUIAsset
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen
import com.firebase.ui.auth.util.EmailLinkConstants
import com.firebaseui.android.demo.R
import com.firebaseui.android.demo.utils.EMAIL_LINK_ORIGIN_PARAM
import com.firebaseui.android.demo.auth.fullcustomization.common.ThemeBackground
import com.firebaseui.android.demo.auth.fullcustomization.common.ThemeSwitcher
import com.firebaseui.android.demo.auth.fullcustomization.professional.LegalLinks
import com.firebaseui.android.demo.auth.fullcustomization.professional.LocalLegalLinks
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
Expand All @@ -35,13 +46,17 @@ import com.firebaseui.android.demo.auth.fullcustomization.screens.mfa.MfaEnrollm
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.firebaseui.android.demo.auth.fullcustomization.theme.FullCustomizationThemeId
import com.firebaseui.android.demo.utils.EMAIL_LINK_ORIGIN_PARAM
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"

private const val THEME_PREFERENCES = "full_customization"
private const val THEME_KEY = "theme"
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -97,47 +112,71 @@ class FullCustomizationDemoActivity : ComponentActivity() {
}
}

val preferences = getSharedPreferences(THEME_PREFERENCES, MODE_PRIVATE)
val initialTheme = FullCustomizationThemeId.entries.firstOrNull {
it.name == preferences.getString(THEME_KEY, null)
} ?: FullCustomizationThemeId.Doggo

setContent {
FullCustomizationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
var themeId by remember { mutableStateOf(initialTheme) }
FullCustomizationTheme(themeId = themeId) {
CompositionLocalProvider(
LocalLegalLinks provides LegalLinks(configuration.tosUrl, configuration.privacyPolicyUrl),
) {
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,
Box(modifier = Modifier.fillMaxSize()) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
FirebaseAuthScreen(
configuration = configuration,
providers = providers,
onProviderSelected = onProviderSelected,
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)
},
)
},
// 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)
},
)
}
// Over every screen, so the theme can be switched mid-flow.
ThemeSwitcher(
selected = themeId,
onSelect = { selected ->
themeId = selected
preferences.edit { putString(THEME_KEY, selected.name) }
},
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.padding(top = 8.dp),
)
}
}
}
}
Expand All @@ -153,12 +192,7 @@ private fun MainUI(
) {
val context = LocalContext.current
Box(modifier = Modifier.fillMaxSize()) {
Image(
painter = painterResource(id = R.drawable.custom_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
ThemeBackground()
Column(modifier = Modifier.fillMaxSize()) {
// AuthMethodPickerUI builds its own EmailAuthScreen per step, so none is needed here.
AuthMethodPickerUI(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.firebaseui.android.demo.auth.fullcustomization.common

import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
Expand All @@ -14,20 +13,18 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.firebaseui.android.demo.R
import com.firebaseui.android.demo.auth.fullcustomization.professional.ProHeader
import com.firebaseui.android.demo.auth.fullcustomization.professional.ProPage
import com.firebaseui.android.demo.auth.fullcustomization.professional.isProfessional
import com.firebaseui.android.demo.auth.fullcustomization.theme.LocalFullCustomizationStyle

/**
* The page frame shared by the MFA and reauthentication screens: mascot, headline, a single
Expand All @@ -50,14 +47,22 @@ fun AuthPage(
actions: @Composable ColumnScope.() -> Unit,
card: @Composable ColumnScope.() -> Unit,
) {
// Professional lays the same content out as a plain left-aligned form, with no card.
if (isProfessional()) {
ProPage(
modifier = Modifier.semantics { contentDescription = cardContentDescription },
header = { ProHeader(title = title) },
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp), content = card)
Spacer(modifier = Modifier.height(24.dp))
Column(content = actions)
}
return
}

Box(modifier = Modifier.fillMaxSize()) {
// Outside safeDrawingPadding so it runs edge to edge under the system bars.
Image(
painter = painterResource(id = R.drawable.custom_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
ThemeBackground()

BoxWithConstraints(
modifier = Modifier
Expand All @@ -74,16 +79,10 @@ fun AuthPage(
Spacer(modifier = Modifier.weight(1f))

Column(modifier = Modifier.fillMaxWidth()) {
Image(
painter = painterResource(id = mascot),
contentDescription = mascotDescription,
modifier = Modifier.size(72.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary,
ThemeHeader(
title = title,
mascot = mascot,
mascotDescription = mascotDescription,
)
Spacer(modifier = Modifier.height(24.dp))

Expand All @@ -92,7 +91,7 @@ fun AuthPage(
modifier = Modifier
.fillMaxWidth()
.semantics { contentDescription = cardContentDescription },
color = MaterialTheme.colorScheme.surface,
color = LocalFullCustomizationStyle.current.cardFill,
shape = AuthFieldShape,
) {
Column(
Expand Down
Loading
Loading