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..29a6f0ee2 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() { @@ -144,29 +144,11 @@ class CustomMethodPickerDemoActivity : ComponentActivity() { 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 +161,8 @@ fun SpotlightMethodPicker( providers: List, onProviderSelected: (AuthProvider) -> Unit, enabled: Boolean = true, + termsAccepted: Boolean = true, + onTermsAcceptedChange: (Boolean) -> Unit = {}, ) { val stringProvider = LocalAuthUIStringProvider.current @@ -196,7 +180,9 @@ fun SpotlightMethodPicker( val anonymous = groups["anonymous"]?.firstOrNull() LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding(), contentPadding = PaddingValues(vertical = 48.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -289,6 +275,24 @@ 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) + ) + } + } } } @@ -346,6 +350,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..d664f01c8 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,24 @@ 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) { + 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..0780348ee 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 correct edge-to-edge inset handling + * can be verified in the IDE preview. A plain `@Preview` draws no system chrome at all, so + * inset issues 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 dfc413bc6..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 @@ -95,7 +94,7 @@ fun SignUpUI( val isFormValid = remember(displayName, email, password, confirmPassword) { derivedStateOf { listOf( - displayNameValidator.validate(displayName), + !provider.isDisplayNameRequired || displayNameValidator.validate(displayName), emailValidator.validate(email), passwordValidator.validate(password), confirmPasswordValidator.validate(confirmPassword) @@ -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( diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignUpUITest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignUpUITest.kt new file mode 100644 index 000000000..ac642adb7 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/ui/screens/email/SignUpUITest.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2025 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.firebase.ui.auth.ui.screens.email + +import android.content.Context +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.test.assertIsEnabled +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performTextInput +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Unit tests for [SignUpUI], covering form validity logic. + * + * @suppress Internal test class + */ +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class SignUpUITest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var applicationContext: Context + private lateinit var stringProvider: AuthUIStringProvider + + @Before + fun setUp() { + applicationContext = ApplicationProvider.getApplicationContext() + stringProvider = DefaultAuthUIStringProvider(applicationContext) + } + + @Test + fun `sign up button becomes enabled when display name is not required and hidden`() { + val provider = AuthProvider.Email( + isDisplayNameRequired = false, + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + val configuration = authUIConfiguration { + context = applicationContext + providers { provider(provider) } + } + + composeTestRule.setContent { + CompositionLocalProvider(LocalAuthUIStringProvider provides stringProvider) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var confirmPassword by remember { mutableStateOf("") } + + SignUpUI( + configuration = configuration, + isLoading = false, + displayName = "", + email = email, + password = password, + confirmPassword = confirmPassword, + onDisplayNameChange = { }, + onEmailChange = { email = it }, + onPasswordChange = { password = it }, + onConfirmPasswordChange = { confirmPassword = it }, + onGoToSignIn = { }, + onSignUpClick = { } + ) + } + } + + // Name field should not be rendered since it isn't required. + composeTestRule.onNodeWithText(stringProvider.nameHint).assertDoesNotExist() + + composeTestRule.onNodeWithText(stringProvider.emailHint) + .performTextInput("test@example.com") + composeTestRule.onNodeWithText(stringProvider.passwordHint) + .performTextInput("Password123") + composeTestRule.onNodeWithText(stringProvider.confirmPasswordHint) + .performTextInput("Password123") + + composeTestRule.waitForIdle() + + // With email/password/confirmPassword all valid and no display name required, + // the sign up button should be enabled even though displayName is still "". + composeTestRule.onNode(hasText(stringProvider.signupPageTitle.uppercase()) and hasClickAction()) + .assertIsEnabled() + } +}