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

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

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

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
Expand Down Expand Up @@ -52,7 +53,6 @@ import com.firebase.ui.auth.configuration.theme.AuthUIAsset
import com.firebase.ui.auth.configuration.theme.AuthUITheme
import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults
import com.firebase.ui.auth.ui.components.AuthProviderButton
import com.firebase.ui.auth.ui.method_picker.MethodPickerTermsConfiguration
import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen

class CustomMethodPickerDemoActivity : ComponentActivity() {
Expand Down Expand Up @@ -142,32 +142,19 @@ 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,
enabled = termsAccepted
enabled = termsAccepted,
termsAccepted = termsAccepted,
onTermsAcceptedChange = { termsAccepted = it }
)
},
customMethodPickerTermsConfiguration = MethodPickerTermsConfiguration(
content = {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = termsAccepted,
onCheckedChange = { termsAccepted = it }
)
Text(
text = "I have read and accept the Terms of Service and Privacy Policy",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 8.dp)
)
}
},
accepted = termsAccepted,
disableProvidersUntilAccepted = true,
),
)
}
}
Expand All @@ -180,6 +167,8 @@ fun SpotlightMethodPicker(
providers: List<AuthProvider>,
onProviderSelected: (AuthProvider) -> Unit,
enabled: Boolean = true,
termsAccepted: Boolean = true,
onTermsAcceptedChange: (Boolean) -> Unit = {},
) {
val stringProvider = LocalAuthUIStringProvider.current

Expand All @@ -197,7 +186,11 @@ fun SpotlightMethodPicker(
val anonymous = groups["anonymous"]?.firstOrNull()

LazyColumn(
modifier = Modifier.fillMaxSize(),
// customMethodPickerLayout now renders as the entire screen, so this composable is
// responsible for its own insets.
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding(),
contentPadding = PaddingValues(vertical = 48.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
Expand Down Expand Up @@ -290,11 +283,29 @@ fun SpotlightMethodPicker(
}
}
}

item {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = termsAccepted,
onCheckedChange = onTermsAcceptedChange
)
Text(
text = "I have read and accept the Terms of Service and Privacy Policy",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}

@Composable
private fun ProviderIconButton(
fun ProviderIconButton(
style: AuthUITheme.ProviderStyle,
contentDescription: String,
onClick: () -> Unit,
Expand Down Expand Up @@ -331,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 All @@ -347,6 +358,7 @@ private fun styleForProvider(provider: AuthProvider): AuthUITheme.ProviderStyle
backgroundColor = provider.buttonColor ?: Color(0xFF666666),
contentColor = provider.contentColor ?: Color.White
)

else -> AuthUITheme.ProviderStyle(
icon = null,
backgroundColor = Color(0xFF666666),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.firebaseui.android.demo.auth.fullcustomization.FullCustomizationDemoActivity

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

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

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

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.firebaseui.android.demo.R

val AuthFieldShape = RoundedCornerShape(24.dp)

@Composable
fun authTextFieldColors(): TextFieldColors = OutlinedTextFieldDefaults.colors(
unfocusedContainerColor = Color.White,
focusedContainerColor = Color.White,
disabledContainerColor = Color.White,
unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant,
focusedBorderColor = MaterialTheme.colorScheme.secondary,
)

@Composable
fun FullCustomizationTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: String? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
isError: Boolean = false,
supportingText: String? = null,
singleLine: Boolean = true,
visualTransformation: VisualTransformation = VisualTransformation.None,
shape: Shape = AuthFieldShape,
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label?.let { { Text(it) } },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
enabled = enabled,
isError = isError,
supportingText = supportingText?.let { { Text(it) } },
singleLine = singleLine,
visualTransformation = visualTransformation,
shape = shape,
colors = authTextFieldColors(),
)
}

@Composable
fun EmailFieldIcon() {
Image(
painter = painterResource(R.drawable.email_at_sign),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.firebaseui.android.demo.auth.fullcustomization

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

private val CtaShadowColor = Color(0xFF5D0B47)

@Composable
fun CtaButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
isLoading: Boolean = false,
colors: ButtonColors = ButtonDefaults.buttonColors(),
) {
HardOffsetShadow(
shape = ButtonShape,
offsetX = 2.dp,
offsetY = 4.dp,
color = if (enabled) CtaShadowColor else Color.Transparent,
modifier = modifier.fillMaxWidth(),
) {
Button(
onClick = onClick,
enabled = enabled,
shape = ButtonShape,
colors = colors,
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 10.dp),
modifier = Modifier
.fillMaxWidth()
.height(80.dp),
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Comment on lines +46 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

By default, CircularProgressIndicator uses the theme's primary color. Since CtaButton also uses the primary color as its default background, the progress indicator will have extremely low contrast or be completely invisible. Setting its color to LocalContentColor.current ensures it matches the button's text color (e.g., onPrimary) and remains clearly visible.

            if (isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(20.dp),
                    color = androidx.compose.material3.LocalContentColor.current
                )
            } else {

Text(text = text, style = MaterialTheme.typography.titleMedium)
}
}
}
}
Loading