From 38da16d6ff0c71d6a097bd0940631126c6231c8a Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 4 Jun 2026 16:10:26 +0100 Subject: [PATCH 1/6] feat(auth): add termsContent slot to AuthMethodPicker for custom ToS UI --- app/src/main/AndroidManifest.xml | 1 + .../demo/CustomMethodPickerDemoActivity.kt | 24 +++++++ .../demo/CustomSlotsThemingDemoActivity.kt | 4 +- .../auth/ui/method_picker/AuthMethodPicker.kt | 38 ++++++---- .../ui/auth/ui/screens/FirebaseAuthScreen.kt | 2 + .../ui/method_picker/AuthMethodPickerTest.kt | 69 +++++++++++++++++++ 6 files changed, 122 insertions(+), 16 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72f1b8025..991e55b60 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -79,6 +79,7 @@ android:label="Custom Method Picker Layout" android:exported="false" android:theme="@style/Theme.FirebaseUIAndroid" /> + 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 cd73ce212..e9113e285 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -8,6 +8,7 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.fillMaxSize @@ -20,6 +21,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -27,6 +29,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +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.graphics.Color @@ -119,6 +125,8 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { + var termsAccepted by remember { mutableStateOf(false) } + FirebaseAuthScreen( configuration = configuration, authUI = authUI, @@ -136,6 +144,22 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { providers = providers, onProviderSelected = onProviderSelected ) + }, + customMethodPickerTermsContent = { + 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) + ) + } } ) } diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt index 4aa50b05f..6b75980ef 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt @@ -102,8 +102,8 @@ fun CustomSlotsDemoChooser( ) DemoCard( - title = "Custom Method Picker Layout", - description = "Replace the default vertical provider list with a 2-column grid using customMethodPickerLayout on FirebaseAuthScreen.", + title = "Custom Method Picker Layout & Terms", + description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsContent on FirebaseAuthScreen.", onClick = onCustomMethodPickerClick ) } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index 083148016..a3ecc318e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -68,6 +68,9 @@ import com.firebase.ui.auth.util.SignInPreferenceManager * @param termsOfServiceUrl The URL for the Terms of Service. * @param privacyPolicyUrl The URL for the Privacy Policy. * @param lastSignInPreference The last sign-in preference to show a "Continue as..." button. + * @param termsContent An optional composable to replace the default ToS/Privacy Policy footer. + * When provided, the default "By continuing..." text is not rendered. Use this to supply a + * checkbox or custom consent UI without having to reimplement the full provider list. * * @since 10.0.0 */ @@ -81,6 +84,7 @@ fun AuthMethodPicker( privacyPolicyUrl: String? = null, lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, customLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, + termsContent: (@Composable () -> Unit)? = null, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current @@ -100,7 +104,9 @@ fun AuthMethodPicker( ) } if (customLayout != null) { - customLayout(providers, onProviderSelected) + Box(modifier = Modifier.weight(1f)) { + customLayout(providers, onProviderSelected) + } } else { BoxWithConstraints( modifier = Modifier @@ -163,20 +169,24 @@ fun AuthMethodPicker( } } } - AnnotatedStringResource( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), - context = context, - inPreview = inPreview, - previewText = "By continuing, you accept our Terms of Service and Privacy Policy.", - text = stringProvider.tosAndPrivacyPolicy( - termsOfServiceLabel = stringProvider.termsOfService, - privacyPolicyLabel = stringProvider.privacyPolicy - ), - links = arrayOf( - stringProvider.termsOfService to (termsOfServiceUrl ?: ""), - stringProvider.privacyPolicy to (privacyPolicyUrl ?: "") + if (termsContent != null) { + termsContent() + } else if (customLayout == null) { + AnnotatedStringResource( + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), + context = context, + inPreview = inPreview, + previewText = "By continuing, you accept our Terms of Service and Privacy Policy.", + text = stringProvider.tosAndPrivacyPolicy( + termsOfServiceLabel = stringProvider.termsOfService, + privacyPolicyLabel = stringProvider.privacyPolicy + ), + links = arrayOf( + stringProvider.termsOfService to (termsOfServiceUrl ?: ""), + stringProvider.privacyPolicy to (privacyPolicyUrl ?: "") + ) ) - ) + } } } 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 a919223ad..08a089e78 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 @@ -112,6 +112,7 @@ fun FirebaseAuthScreen( emailLink: String? = null, mfaConfiguration: MfaConfiguration = MfaConfiguration(), customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, + customMethodPickerTermsContent: (@Composable () -> Unit)? = null, emailContent: (@Composable (EmailAuthContentState) -> Unit)? = null, phoneContent: (@Composable (PhoneAuthContentState) -> Unit)? = null, mfaEnrollmentContent: (@Composable (MfaEnrollmentContentState) -> Unit)? = null, @@ -277,6 +278,7 @@ fun FirebaseAuthScreen( privacyPolicyUrl = configuration.privacyPolicyUrl, lastSignInPreference = lastSignInPreference.value, customLayout = customMethodPickerLayout, + termsContent = customMethodPickerTermsContent, onProviderSelected = { provider -> when (provider) { is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt index b40917d5b..cac21fcbf 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt @@ -280,6 +280,75 @@ class AuthMethodPickerTest { Truth.assertThat(selectedProvider).isEqualTo(googleProvider) } + @Test + fun `AuthMethodPicker does not render default ToS text when customLayout is provided`() { + val links = arrayOf("Terms of Service" to "", "Privacy Policy" to "") + val labels = links.map { it.first }.toTypedArray() + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + + setContentWithStringProvider { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + customLayout = { _, _ -> Text("Custom Layout") } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_tos_and_pp, *labels)) + .assertDoesNotExist() + } + + // ============================================================================================= + // Custom Terms Content Tests + // ============================================================================================= + + @Test + fun `AuthMethodPicker renders termsContent instead of default ToS when provided`() { + val links = arrayOf("Terms of Service" to "", "Privacy Policy" to "") + val labels = links.map { it.first }.toTypedArray() + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + + setContentWithStringProvider { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + termsContent = { Text("Custom ToS checkbox") } + ) + } + + composeTestRule + .onNodeWithText("Custom ToS checkbox") + .assertIsDisplayed() + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_tos_and_pp, *labels)) + .assertDoesNotExist() + } + + @Test + fun `AuthMethodPicker still renders providers when termsContent is provided`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + + setContentWithStringProvider { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + termsContent = { Text("Custom ToS checkbox") } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + } + // ============================================================================================= // Scrolling Tests // ============================================================================================= From 32cd6f9821133d9d9b60a9acc6bd91fa553d2488 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 4 Jun 2026 17:03:25 +0100 Subject: [PATCH 2/6] feat(auth): add termsAccepted parameter to AuthMethodPicker for consent handling --- .../demo/CustomMethodPickerDemoActivity.kt | 9 ++- .../auth/ui/method_picker/AuthMethodPicker.kt | 11 +++- .../ui/auth/ui/screens/FirebaseAuthScreen.kt | 2 + .../ui/method_picker/AuthMethodPickerTest.kt | 62 ++++++++++++++++++- 4 files changed, 79 insertions(+), 5 deletions(-) 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 e9113e285..edae07848 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -142,7 +142,8 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { customMethodPickerLayout = { providers, onProviderSelected -> SpotlightMethodPicker( providers = providers, - onProviderSelected = onProviderSelected + onProviderSelected = onProviderSelected, + enabled = termsAccepted ) }, customMethodPickerTermsContent = { @@ -160,7 +161,8 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { modifier = Modifier.padding(start = 8.dp) ) } - } + }, + customMethodPickerTermsAccepted = termsAccepted, ) } } @@ -172,6 +174,7 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { fun SpotlightMethodPicker( providers: List, onProviderSelected: (AuthProvider) -> Unit, + enabled: Boolean = true, ) { val stringProvider = LocalAuthUIStringProvider.current @@ -219,6 +222,7 @@ fun SpotlightMethodPicker( .padding(horizontal = 32.dp), provider = provider, onClick = { onProviderSelected(provider) }, + enabled = enabled, stringProvider = stringProvider ) } @@ -266,6 +270,7 @@ fun SpotlightMethodPicker( .padding(horizontal = 32.dp), provider = provider, onClick = { onProviderSelected(provider) }, + enabled = enabled, stringProvider = stringProvider ) } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index a3ecc318e..978cb0671 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -71,6 +71,9 @@ import com.firebase.ui.auth.util.SignInPreferenceManager * @param termsContent An optional composable to replace the default ToS/Privacy Policy footer. * When provided, the default "By continuing..." text is not rendered. Use this to supply a * checkbox or custom consent UI without having to reimplement the full provider list. + * @param termsAccepted Controls whether provider buttons are enabled. Set to false when + * [termsContent] contains an acceptance checkbox that the user has not yet checked. Defaults + * to true so existing callers are unaffected. * * @since 10.0.0 */ @@ -85,10 +88,12 @@ fun AuthMethodPicker( lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, customLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, termsContent: (@Composable () -> Unit)? = null, + termsAccepted: Boolean = true, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current val stringProvider = LocalAuthUIStringProvider.current + val providerButtonsEnabled = termsContent == null || termsAccepted Column( modifier = modifier @@ -127,6 +132,7 @@ fun AuthMethodPicker( ContinueAsButton( provider = lastProvider, identifier = preference.identifier, + enabled = providerButtonsEnabled, onClick = { onProviderSelected(lastProvider) } ) Spacer(modifier = Modifier.height(24.dp)) @@ -161,6 +167,7 @@ fun AuthMethodPicker( onClick = { onProviderSelected(provider) }, + enabled = providerButtonsEnabled, provider = provider, stringProvider = LocalAuthUIStringProvider.current ) @@ -171,7 +178,7 @@ fun AuthMethodPicker( } if (termsContent != null) { termsContent() - } else if (customLayout == null) { + } else { AnnotatedStringResource( modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), context = context, @@ -201,6 +208,7 @@ fun AuthMethodPicker( private fun ContinueAsButton( provider: AuthProvider, identifier: String?, + enabled: Boolean = true, onClick: () -> Unit ) { val stringProvider = LocalAuthUIStringProvider.current @@ -210,6 +218,7 @@ private fun ContinueAsButton( .fillMaxWidth() .testTag("ContinueAsButton"), onClick = onClick, + enabled = enabled, provider = provider, stringProvider = stringProvider, subtitle = identifier, 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 08a089e78..62fef9cd3 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 @@ -113,6 +113,7 @@ fun FirebaseAuthScreen( mfaConfiguration: MfaConfiguration = MfaConfiguration(), customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, customMethodPickerTermsContent: (@Composable () -> Unit)? = null, + customMethodPickerTermsAccepted: Boolean = true, emailContent: (@Composable (EmailAuthContentState) -> Unit)? = null, phoneContent: (@Composable (PhoneAuthContentState) -> Unit)? = null, mfaEnrollmentContent: (@Composable (MfaEnrollmentContentState) -> Unit)? = null, @@ -279,6 +280,7 @@ fun FirebaseAuthScreen( lastSignInPreference = lastSignInPreference.value, customLayout = customMethodPickerLayout, termsContent = customMethodPickerTermsContent, + termsAccepted = customMethodPickerTermsAccepted, onProviderSelected = { provider -> when (provider) { is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt index cac21fcbf..a29de6c75 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt @@ -7,7 +7,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.assertHasClickAction import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription @@ -281,7 +283,7 @@ class AuthMethodPickerTest { } @Test - fun `AuthMethodPicker does not render default ToS text when customLayout is provided`() { + fun `AuthMethodPicker still renders default ToS text when customLayout is provided`() { val links = arrayOf("Terms of Service" to "", "Privacy Policy" to "") val labels = links.map { it.first }.toTypedArray() val providers = listOf( @@ -298,7 +300,7 @@ class AuthMethodPickerTest { composeTestRule .onNodeWithText(context.getString(R.string.fui_tos_and_pp, *labels)) - .assertDoesNotExist() + .assertIsDisplayed() } // ============================================================================================= @@ -349,6 +351,62 @@ class AuthMethodPickerTest { .assertIsDisplayed() } + // ============================================================================================= + // Terms Accepted / Gating Tests + // ============================================================================================= + + @Test + fun `AuthMethodPicker disables provider buttons when termsContent provided and termsAccepted is false`() { + val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + setContentWithStringProvider { + AuthMethodPicker( + providers = listOf(googleProvider), + onProviderSelected = { selectedProvider = it }, + termsContent = { Text("Checkbox") }, + termsAccepted = false + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsNotEnabled() + } + + @Test + fun `AuthMethodPicker ignores termsAccepted when no termsContent is provided`() { + val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + setContentWithStringProvider { + AuthMethodPicker( + providers = listOf(googleProvider), + onProviderSelected = { selectedProvider = it }, + termsAccepted = false // should have no effect without termsContent + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsEnabled() + } + + @Test + fun `AuthMethodPicker enables provider buttons when termsAccepted is true`() { + val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + setContentWithStringProvider { + AuthMethodPicker( + providers = listOf(googleProvider), + onProviderSelected = { selectedProvider = it }, + termsAccepted = true + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsEnabled() + } + // ============================================================================================= // Scrolling Tests // ============================================================================================= From f147d73b774380e5a9102aac234d345ab801b1f0 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 4 Jun 2026 17:08:59 +0100 Subject: [PATCH 3/6] update example --- .../firebaseui/android/demo/CustomMethodPickerDemoActivity.kt | 3 +++ 1 file changed, 3 insertions(+) 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 edae07848..c17dc2029 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -247,6 +247,7 @@ fun SpotlightMethodPicker( ProviderIconButton( style = style, contentDescription = provider.providerId, + enabled = enabled, onClick = { onProviderSelected(provider) } ) } @@ -292,9 +293,11 @@ private fun ProviderIconButton( style: AuthUITheme.ProviderStyle, contentDescription: String, onClick: () -> Unit, + enabled: Boolean = true, ) { Button( onClick = onClick, + enabled = enabled, modifier = Modifier.size(52.dp), shape = CircleShape, colors = ButtonDefaults.buttonColors(containerColor = style.backgroundColor), From 3728e0c57d3afd08e70eb4fb37e606fb45009e66 Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 4 Jun 2026 17:54:21 +0100 Subject: [PATCH 4/6] updates --- .../demo/CustomMethodPickerDemoActivity.kt | 38 ++++++++++--------- .../auth/ui/method_picker/AuthMethodPicker.kt | 19 ++++------ .../ui/auth/ui/screens/FirebaseAuthScreen.kt | 7 ++-- .../ui/method_picker/AuthMethodPickerTest.kt | 38 +++++++++++++------ 4 files changed, 59 insertions(+), 43 deletions(-) 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 c17dc2029..69b20d240 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -51,6 +51,7 @@ 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() { @@ -146,23 +147,26 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { enabled = termsAccepted ) }, - customMethodPickerTermsContent = { - 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) - ) - } - }, - customMethodPickerTermsAccepted = termsAccepted, + 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, + ), ) } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index 978cb0671..c6ad72c88 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -68,12 +68,8 @@ import com.firebase.ui.auth.util.SignInPreferenceManager * @param termsOfServiceUrl The URL for the Terms of Service. * @param privacyPolicyUrl The URL for the Privacy Policy. * @param lastSignInPreference The last sign-in preference to show a "Continue as..." button. - * @param termsContent An optional composable to replace the default ToS/Privacy Policy footer. - * When provided, the default "By continuing..." text is not rendered. Use this to supply a - * checkbox or custom consent UI without having to reimplement the full provider list. - * @param termsAccepted Controls whether provider buttons are enabled. Set to false when - * [termsContent] contains an acceptance checkbox that the user has not yet checked. Defaults - * to true so existing callers are unaffected. + * @param termsConfiguration Optional configuration for a custom ToS/Privacy Policy footer. + * When provided, replaces the default "By continuing..." text. See [MethodPickerTermsConfiguration]. * * @since 10.0.0 */ @@ -87,13 +83,14 @@ fun AuthMethodPicker( privacyPolicyUrl: String? = null, lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, customLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, - termsContent: (@Composable () -> Unit)? = null, - termsAccepted: Boolean = true, + termsConfiguration: MethodPickerTermsConfiguration? = null, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current val stringProvider = LocalAuthUIStringProvider.current - val providerButtonsEnabled = termsContent == null || termsAccepted + val providerButtonsEnabled = termsConfiguration == null || + !termsConfiguration.disableProvidersUntilAccepted || + termsConfiguration.accepted Column( modifier = modifier @@ -176,8 +173,8 @@ fun AuthMethodPicker( } } } - if (termsContent != null) { - termsContent() + if (termsConfiguration != null) { + termsConfiguration.content() } else { AnnotatedStringResource( modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), 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 62fef9cd3..e82caf88b 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 @@ -77,6 +77,7 @@ import com.firebase.ui.auth.ui.components.rememberTopLevelDialogController import com.firebase.ui.auth.mfa.MfaChallengeContentState import com.firebase.ui.auth.mfa.MfaEnrollmentContentState import com.firebase.ui.auth.ui.method_picker.AuthMethodPicker +import com.firebase.ui.auth.ui.method_picker.MethodPickerTermsConfiguration import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen import com.firebase.ui.auth.ui.screens.phone.PhoneAuthContentState @@ -112,8 +113,7 @@ fun FirebaseAuthScreen( emailLink: String? = null, mfaConfiguration: MfaConfiguration = MfaConfiguration(), customMethodPickerLayout: (@Composable (List, (AuthProvider) -> Unit) -> Unit)? = null, - customMethodPickerTermsContent: (@Composable () -> Unit)? = null, - customMethodPickerTermsAccepted: Boolean = true, + customMethodPickerTermsConfiguration: MethodPickerTermsConfiguration? = null, emailContent: (@Composable (EmailAuthContentState) -> Unit)? = null, phoneContent: (@Composable (PhoneAuthContentState) -> Unit)? = null, mfaEnrollmentContent: (@Composable (MfaEnrollmentContentState) -> Unit)? = null, @@ -279,8 +279,7 @@ fun FirebaseAuthScreen( privacyPolicyUrl = configuration.privacyPolicyUrl, lastSignInPreference = lastSignInPreference.value, customLayout = customMethodPickerLayout, - termsContent = customMethodPickerTermsContent, - termsAccepted = customMethodPickerTermsAccepted, + termsConfiguration = customMethodPickerTermsConfiguration, onProviderSelected = { provider -> when (provider) { is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt index a29de6c75..c4029b198 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPickerTest.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.test.performScrollToNode import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.R import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.method_picker.MethodPickerTermsConfiguration 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.AuthUIAsset @@ -308,7 +309,7 @@ class AuthMethodPickerTest { // ============================================================================================= @Test - fun `AuthMethodPicker renders termsContent instead of default ToS when provided`() { + fun `AuthMethodPicker renders termsConfiguration content instead of default ToS when provided`() { val links = arrayOf("Terms of Service" to "", "Privacy Policy" to "") val labels = links.map { it.first }.toTypedArray() val providers = listOf( @@ -319,7 +320,9 @@ class AuthMethodPickerTest { AuthMethodPicker( providers = providers, onProviderSelected = { selectedProvider = it }, - termsContent = { Text("Custom ToS checkbox") } + termsConfiguration = MethodPickerTermsConfiguration( + content = { Text("Custom ToS checkbox") } + ) ) } @@ -333,7 +336,7 @@ class AuthMethodPickerTest { } @Test - fun `AuthMethodPicker still renders providers when termsContent is provided`() { + fun `AuthMethodPicker still renders providers when termsConfiguration is provided`() { val providers = listOf( AuthProvider.Google(scopes = emptyList(), serverClientId = null) ) @@ -342,7 +345,9 @@ class AuthMethodPickerTest { AuthMethodPicker( providers = providers, onProviderSelected = { selectedProvider = it }, - termsContent = { Text("Custom ToS checkbox") } + termsConfiguration = MethodPickerTermsConfiguration( + content = { Text("Custom ToS checkbox") } + ) ) } @@ -356,15 +361,18 @@ class AuthMethodPickerTest { // ============================================================================================= @Test - fun `AuthMethodPicker disables provider buttons when termsContent provided and termsAccepted is false`() { + fun `AuthMethodPicker disables provider buttons when disableProvidersUntilAccepted is true and accepted is false`() { val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) setContentWithStringProvider { AuthMethodPicker( providers = listOf(googleProvider), onProviderSelected = { selectedProvider = it }, - termsContent = { Text("Checkbox") }, - termsAccepted = false + termsConfiguration = MethodPickerTermsConfiguration( + content = { Text("Checkbox") }, + accepted = false, + disableProvidersUntilAccepted = true + ) ) } @@ -374,14 +382,18 @@ class AuthMethodPickerTest { } @Test - fun `AuthMethodPicker ignores termsAccepted when no termsContent is provided`() { + fun `AuthMethodPicker enables provider buttons when disableProvidersUntilAccepted is true and accepted is true`() { val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) setContentWithStringProvider { AuthMethodPicker( providers = listOf(googleProvider), onProviderSelected = { selectedProvider = it }, - termsAccepted = false // should have no effect without termsContent + termsConfiguration = MethodPickerTermsConfiguration( + content = { Text("Checkbox") }, + accepted = true, + disableProvidersUntilAccepted = true + ) ) } @@ -391,14 +403,18 @@ class AuthMethodPickerTest { } @Test - fun `AuthMethodPicker enables provider buttons when termsAccepted is true`() { + fun `AuthMethodPicker ignores accepted when disableProvidersUntilAccepted is false`() { val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) setContentWithStringProvider { AuthMethodPicker( providers = listOf(googleProvider), onProviderSelected = { selectedProvider = it }, - termsAccepted = true + termsConfiguration = MethodPickerTermsConfiguration( + content = { Text("Checkbox") }, + accepted = false, + disableProvidersUntilAccepted = false + ) ) } From 28e0eeaf311a1e85597734f17b012f936fd7067e Mon Sep 17 00:00:00 2001 From: demolaf Date: Thu, 4 Jun 2026 18:18:17 +0100 Subject: [PATCH 5/6] updates --- .../auth/ui/method_picker/AuthMethodPicker.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index c6ad72c88..feb04fb7c 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -46,6 +46,24 @@ import com.firebase.ui.auth.configuration.theme.AuthUIAsset import com.firebase.ui.auth.ui.components.AuthProviderButton import com.firebase.ui.auth.util.SignInPreferenceManager +/** + * Configuration for a custom Terms of Service/Privacy Policy footer in [AuthMethodPicker]. + * + * @param content A composable that replaces the default "By continuing..." footer. Use this to + * supply a checkbox or any custom consent UI. + * @param accepted The current acceptance state. Only used when [disableProvidersUntilAccepted] + * is true. + * @param disableProvidersUntilAccepted When true, provider buttons are disabled until [accepted] + * is true. Defaults to false — buttons remain enabled unless explicitly opted in. + * + * @since 10.0.0 + */ +class MethodPickerTermsConfiguration( + val content: @Composable () -> Unit, + val accepted: Boolean = true, + val disableProvidersUntilAccepted: Boolean = false, +) + /** * Renders the provider selection screen. * From 3c04f74db5565a0d78c9ae7f5330c29e962f61b5 Mon Sep 17 00:00:00 2001 From: demolaf Date: Fri, 5 Jun 2026 12:55:20 +0100 Subject: [PATCH 6/6] updates --- .../firebaseui/android/demo/CustomMethodPickerDemoActivity.kt | 2 +- .../firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 69b20d240..54eadccc3 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomMethodPickerDemoActivity.kt @@ -284,7 +284,7 @@ fun SpotlightMethodPicker( anonymous?.let { item { Spacer(modifier = Modifier.height(8.dp)) - TextButton(onClick = { onProviderSelected(it) }) { + TextButton(onClick = { onProviderSelected(it) }, enabled = enabled) { Text("Continue as guest") } } diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt index 6b75980ef..4b824eed5 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt @@ -103,7 +103,7 @@ fun CustomSlotsDemoChooser( DemoCard( title = "Custom Method Picker Layout & Terms", - description = "Replace the default provider list with a custom layout, and swap the 'By continuing...' footer with a checkbox using customMethodPickerLayout and customMethodPickerTermsContent on FirebaseAuthScreen.", + 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 ) }