diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt index 54eadccc3..c9bcd5b84 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -14,6 +14,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 @@ -51,7 +52,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() { @@ -141,32 +141,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, - ), ) } } @@ -179,6 +166,8 @@ fun SpotlightMethodPicker( providers: List, onProviderSelected: (AuthProvider) -> Unit, enabled: Boolean = true, + termsAccepted: Boolean = true, + onTermsAcceptedChange: (Boolean) -> Unit = {}, ) { val stringProvider = LocalAuthUIStringProvider.current @@ -196,7 +185,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, @@ -289,11 +282,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, @@ -330,12 +341,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 @@ -346,6 +357,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), diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt index fffcf5905..748ccca34 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -104,6 +105,14 @@ import kotlinx.coroutines.tasks.await * @param authenticatedContent Optional slot that allows callers to render the authenticated * state themselves. When provided, it receives the current [AuthState] alongside an * [AuthSuccessUiContext] containing common callbacks (sign out, manage MFA, reload user). + * @param customMethodPickerLayout Optional slot that fully replaces the method-picker screen. + * When provided, it renders as the *entire* screen content — edge-to-edge, with no logo, no + * Terms of Service/Privacy Policy footer, and no automatic system-inset handling. The caller is + * responsible for its own insets (e.g. `Modifier.safeDrawingPadding()`) and for displaying any + * required legal disclosures. [customMethodPickerTermsConfiguration] is ignored when this is set. + * @param customMethodPickerTermsConfiguration Optional custom Terms of Service/Privacy Policy + * footer for the *default* method-picker layout. Ignored when [customMethodPickerLayout] is + * provided, since that slot takes over the whole screen. * * @since 10.0.0 */ @@ -207,19 +216,26 @@ fun FirebaseAuthScreen( } ) { composable(AuthRoute.MethodPicker.route) { - Scaffold { innerPadding -> - AuthMethodPicker( - modifier = modifier - .padding(innerPadding), - providers = configuration.providers, - logo = logoAsset, - termsOfServiceUrl = configuration.tosUrl, - privacyPolicyUrl = configuration.privacyPolicyUrl, - lastSignInPreference = lastSignInPreference.value, - customLayout = customMethodPickerLayout, - termsConfiguration = customMethodPickerTermsConfiguration, - onProviderSelected = onProviderSelected, - ) + if (customMethodPickerLayout != null) { + // Takes over the entire screen — no logo, no ToS/Privacy footer, and no + // automatic inset handling. See the KDoc on customMethodPickerLayout. + Box(modifier = modifier.fillMaxSize()) { + customMethodPickerLayout(configuration.providers, onProviderSelected) + } + } else { + Scaffold { innerPadding -> + AuthMethodPicker( + modifier = modifier + .padding(innerPadding), + providers = configuration.providers, + logo = logoAsset, + termsOfServiceUrl = configuration.tosUrl, + privacyPolicyUrl = configuration.privacyPolicyUrl, + lastSignInPreference = lastSignInPreference.value, + termsConfiguration = customMethodPickerTermsConfiguration, + onProviderSelected = onProviderSelected, + ) + } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt index bca4bc4c5..c3b5f5b62 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/MfaChallengeDefaults.kt @@ -27,17 +27,22 @@ import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.firebase.ui.auth.configuration.MfaFactor +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.AuthUITheme import com.firebase.ui.auth.configuration.validators.VerificationCodeValidator import com.firebase.ui.auth.mfa.MfaChallengeContentState import com.firebase.ui.auth.ui.components.VerificationCodeInputField @@ -50,108 +55,136 @@ internal fun DefaultMfaChallengeContent(state: MfaChallengeContentState) { VerificationCodeValidator(stringProvider) } - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = if (isSms) { - val phoneLabel = state.maskedPhoneNumber ?: "" - stringProvider.enterVerificationCodeTitle(phoneLabel) - } else { - stringProvider.mfaStepVerifyFactorTitle - }, - style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center - ) - - if (isSms && state.maskedPhoneNumber != null) { - Text( - text = stringProvider.mfaStepVerifyFactorSmsHelper, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (state.error != null) { + Scaffold { innerPadding -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { Text( - text = state.error, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, + text = if (isSms) { + val phoneLabel = state.maskedPhoneNumber ?: "" + stringProvider.enterVerificationCodeTitle(phoneLabel) + } else { + stringProvider.mfaStepVerifyFactorTitle + }, + style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center ) - } - Spacer(modifier = Modifier.height(8.dp)) + if (isSms && state.maskedPhoneNumber != null) { + Text( + text = stringProvider.mfaStepVerifyFactorSmsHelper, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } - VerificationCodeInputField( - modifier = Modifier.align(Alignment.CenterHorizontally), - codeLength = 6, - validator = verificationCodeValidator, - isError = state.error != null, - errorMessage = state.error, - onCodeChange = state.onVerificationCodeChange - ) + if (state.error != null) { + Text( + text = state.error, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center + ) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(8.dp)) - if (isSms) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - TextButton( - onClick = { state.onResendCodeClick?.invoke() }, - enabled = state.onResendCodeClick != null && !state.isLoading && state.resendTimer == 0 + VerificationCodeInputField( + modifier = Modifier.align(Alignment.CenterHorizontally), + codeLength = 6, + validator = verificationCodeValidator, + isError = state.error != null, + errorMessage = state.error, + onCodeChange = state.onVerificationCodeChange + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (isSms) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = if (state.resendTimer > 0) { - val minutes = state.resendTimer / 60 - val seconds = state.resendTimer % 60 - val formatted = "$minutes:${String.format(java.util.Locale.ROOT, "%02d", seconds)}" - stringProvider.resendCodeTimer(formatted) - } else { - stringProvider.resendCode - } - ) - } + TextButton( + onClick = { state.onResendCodeClick?.invoke() }, + enabled = state.onResendCodeClick != null && !state.isLoading && state.resendTimer == 0 + ) { + Text( + text = if (state.resendTimer > 0) { + val minutes = state.resendTimer / 60 + val seconds = state.resendTimer % 60 + val formatted = "$minutes:${String.format(java.util.Locale.ROOT, "%02d", seconds)}" + stringProvider.resendCodeTimer(formatted) + } else { + stringProvider.resendCode + } + ) + } - TextButton( + TextButton( + onClick = state.onCancelClick, + enabled = !state.isLoading + ) { + Text(stringProvider.useDifferentMethodAction) + } + } + } else { + OutlinedButton( onClick = state.onCancelClick, - enabled = !state.isLoading + enabled = !state.isLoading, + modifier = Modifier.fillMaxWidth() ) { - Text(stringProvider.useDifferentMethodAction) + Text(stringProvider.dismissAction) } } - } else { - OutlinedButton( - onClick = state.onCancelClick, - enabled = !state.isLoading, + + Button( + onClick = state.onVerifyClick, + enabled = state.isValid && !state.isLoading, modifier = Modifier.fillMaxWidth() ) { - Text(stringProvider.dismissAction) + if (state.isLoading) { + CircularProgressIndicator( + modifier = Modifier.padding(end = 8.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + } + Text(stringProvider.verifyAction) } } + } +} - Button( - onClick = state.onVerifyClick, - enabled = state.isValid && !state.isLoading, - modifier = Modifier.fillMaxWidth() +/** + * Renders with a simulated status/nav bar (see CP-240) so the lack of edge-to-edge insets + * handling above is visible in the IDE. A plain `@Preview` draws no system chrome at all, so + * this bug would be invisible there. + */ +@Preview(showSystemUi = true) +@Composable +private fun PreviewDefaultMfaChallengeContentEdgeToEdge() { + val applicationContext = LocalContext.current + val stringProvider = DefaultAuthUIStringProvider(applicationContext) + + AuthUITheme { + CompositionLocalProvider( + LocalAuthUIStringProvider provides stringProvider ) { - if (state.isLoading) { - CircularProgressIndicator( - modifier = Modifier.padding(end = 8.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary + DefaultMfaChallengeContent( + state = MfaChallengeContentState( + factorType = MfaFactor.Sms, + maskedPhoneNumber = "+1••••••890" ) - } - Text(stringProvider.verifyAction) + ) } } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt index 8b73f2c2d..201b5a5f8 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons @@ -137,8 +136,7 @@ fun ResetPasswordUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt index 14e045827..94891e029 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInEmailLinkUI.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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 @@ -148,8 +147,7 @@ fun SignInEmailLinkUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt index eabde87a1..b776c7e49 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -177,8 +176,7 @@ fun SignInUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { AuthTextField( diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt index 6ff0d19f4..da9c9dab7 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -127,8 +126,7 @@ fun SignUpUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { if (provider.isDisplayNameRequired) { diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt index ef8bfe545..5e71f4a0a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterPhoneNumberUI.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height 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.text.KeyboardOptions @@ -106,8 +105,7 @@ fun EnterPhoneNumberUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { Text(stringProvider.enterPhoneNumberTitle) diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt index 122e73a87..b1813e6c0 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height 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 @@ -110,8 +109,7 @@ fun EnterVerificationCodeUI( Column( modifier = Modifier .padding(innerPadding) - .safeDrawingPadding() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), ) { Text(