diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt index 4d9bc280d..6fb0202de 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt @@ -18,8 +18,8 @@ import android.content.Context import java.util.Locale import com.google.firebase.auth.ActionCodeSettings import androidx.compose.ui.graphics.vector.ImageVector -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme fun actionCodeSettings(block: ActionCodeSettings.Builder.() -> Unit) = diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt index 5c5d5b125..7073fbe6e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt @@ -14,7 +14,7 @@ package com.firebase.ui.auth.compose.configuration -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider /** * An abstract class representing a set of validation rules that can be applied to a password field, diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt similarity index 99% rename from auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProvider.kt rename to auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt index ea88485e3..f81ead323 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProvider.kt @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.ui.auth.compose.configuration.stringprovider +package com.firebase.ui.auth.compose.configuration.string_provider /** * An interface for providing localized string resources. This interface defines methods for all diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProviderSample.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProviderSample.kt similarity index 97% rename from auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProviderSample.kt rename to auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProviderSample.kt index 7ddf64522..af0c830cc 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProviderSample.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/AuthUIStringProviderSample.kt @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.ui.auth.compose.configuration.stringprovider +package com.firebase.ui.auth.compose.configuration.string_provider import android.content.Context import com.firebase.ui.auth.compose.configuration.AuthProvider diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/DefaultAuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt similarity index 99% rename from auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/DefaultAuthUIStringProvider.kt rename to auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt index 7df16d9ca..5eba036af 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/DefaultAuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/string_provider/DefaultAuthUIStringProvider.kt @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.ui.auth.compose.configuration.stringprovider +package com.firebase.ui.auth.compose.configuration.string_provider import android.content.Context import android.content.res.Configuration diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidator.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidator.kt index 7acfc8bc1..30582a309 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidator.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidator.kt @@ -14,7 +14,7 @@ package com.firebase.ui.auth.compose.configuration.validators -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider internal class EmailValidator(override val stringProvider: AuthUIStringProvider) : FieldValidator { private var _validationStatus = FieldValidationStatus(hasError = false, errorMessage = null) diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/FieldValidator.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/FieldValidator.kt index efa72188f..88cf98875 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/FieldValidator.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/FieldValidator.kt @@ -14,7 +14,7 @@ package com.firebase.ui.auth.compose.configuration.validators -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider /** * An interface for validating input fields. diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidator.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidator.kt index 67cb7d376..2d9efafc1 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidator.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidator.kt @@ -14,7 +14,7 @@ package com.firebase.ui.auth.compose.configuration.validators -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.compose.configuration.PasswordRule internal class PasswordValidator( diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButton.kt similarity index 90% rename from auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt rename to auth/src/main/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButton.kt index da6b2bbd9..255d6c59e 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButton.kt @@ -1,12 +1,14 @@ -package com.firebase.ui.auth.compose +package com.firebase.ui.auth.compose.ui.components import androidx.compose.foundation.Image import androidx.compose.material3.Icon import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -19,12 +21,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.firebase.ui.auth.compose.configuration.AuthProvider import com.firebase.ui.auth.compose.configuration.Provider -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme @@ -63,10 +67,11 @@ fun AuthProviderButton( stringProvider: AuthUIStringProvider, ) { val providerStyle = resolveProviderStyle(provider, style) - val providerText = resolveProviderLabel(provider, stringProvider) + val providerLabel = resolveProviderLabel(provider, stringProvider) Button( modifier = modifier, + contentPadding = PaddingValues(horizontal = 12.dp), colors = ButtonDefaults.buttonColors( containerColor = providerStyle.backgroundColor, contentColor = providerStyle.contentColor, @@ -79,7 +84,9 @@ fun AuthProviderButton( enabled = enabled, ) { Row( - verticalAlignment = Alignment.CenterVertically + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start ) { val providerIcon = providerStyle.icon if (providerIcon != null) { @@ -87,19 +94,21 @@ fun AuthProviderButton( if (iconTint != null) { Icon( painter = providerIcon.painter, - contentDescription = providerText, + contentDescription = providerLabel, tint = iconTint ) } else { Image( painter = providerIcon.painter, - contentDescription = providerText + contentDescription = providerLabel ) } - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(12.dp)) } Text( - text = providerText + text = providerLabel, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } } diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/ErrorRecoveryDialog.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialog.kt similarity index 97% rename from auth/src/main/java/com/firebase/ui/auth/compose/ErrorRecoveryDialog.kt rename to auth/src/main/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialog.kt index 3b9cc0b57..732a48662 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/ErrorRecoveryDialog.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialog.kt @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.ui.auth.compose +package com.firebase.ui.auth.compose.ui.components import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme @@ -22,7 +22,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.window.DialogProperties -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider +import com.firebase.ui.auth.compose.AuthException +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider /** * A composable dialog for displaying authentication errors with recovery options. diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AnnotatedStringResource.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AnnotatedStringResource.kt new file mode 100644 index 000000000..4c98be9ac --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AnnotatedStringResource.kt @@ -0,0 +1,76 @@ +package com.firebase.ui.auth.compose.ui.method_picker + +import android.content.Context +import android.content.Intent +import androidx.annotation.StringRes +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.core.net.toUri + +@Composable +internal fun AnnotatedStringResource( + context: Context, + modifier: Modifier = Modifier, + @StringRes id: Int, + vararg links: Pair, + inPreview: Boolean = false, + previewText: String? = null, +) { + val labels = links.map { it.first }.toTypedArray() + + val template = if (inPreview && previewText != null) { + previewText + } else { + stringResource(id = id, *labels) + } + + val annotated = buildAnnotatedString { + var currentIndex = 0 + + links.forEach { (label, url) -> + val start = template.indexOf(label, currentIndex).takeIf { it >= 0 } ?: return@forEach + + append(template.substring(currentIndex, start)) + + withLink( + LinkAnnotation.Url( + url, + styles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline, + ) + ) + ) { + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + context.startActivity(intent) + } + ) { + append(label) + } + + currentIndex = start + label.length + } + + if (currentIndex < template.length) { + append(template.substring(currentIndex)) + } + } + + Text( + modifier = modifier, + text = annotated, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt new file mode 100644 index 000000000..855e3d6b3 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPicker.kt @@ -0,0 +1,174 @@ +package com.firebase.ui.auth.compose.ui.method_picker + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.firebase.ui.auth.R +import com.firebase.ui.auth.compose.configuration.AuthProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset +import com.firebase.ui.auth.compose.ui.components.AuthProviderButton + +/** + * Renders the provider selection screen. + * + * **Example usage:** + * ```kotlin + * AuthMethodPicker( + * providers = listOf( + * AuthProvider.Google(), + * AuthProvider.Email(), + * ), + * onProviderSelected = { provider -> /* ... */ } + * ) + * ``` + * + * @param modifier A modifier for the screen layout. + * @param providers The list of providers to display. + * @param logo An optional logo to display. + * @param onProviderSelected A callback when a provider is selected. + * @param customLayout An optional custom layout composable for the provider buttons. + * @param termsOfServiceUrl The URL for the Terms of Service. + * @param privacyPolicyUrl The URL for the Privacy Policy. + * + * @since 10.0.0 + */ +@Composable +fun AuthMethodPicker( + modifier: Modifier = Modifier, + providers: List, + logo: AuthUIAsset? = null, + onProviderSelected: (AuthProvider) -> Unit, + customLayout: @Composable ((List, (AuthProvider) -> Unit) -> Unit)? = null, + termsOfServiceUrl: String? = null, + privacyPolicyUrl: String? = null, +) { + val context = LocalContext.current + val inPreview = LocalInspectionMode.current + + Column( + modifier = modifier + ) { + logo?.let { + Image( + modifier = Modifier + .weight(0.4f) + .align(Alignment.CenterHorizontally), + painter = it.painter, + contentDescription = if (inPreview) "" + else stringResource(R.string.fui_auth_method_picker_logo) + ) + } + if (customLayout != null) { + customLayout(providers, onProviderSelected) + } else { + BoxWithConstraints( + modifier = Modifier + .weight(1f), + ) { + val paddingWidth = maxWidth.value * 0.23 + LazyColumn( + modifier = Modifier + .padding(horizontal = paddingWidth.dp) + .testTag("AuthMethodPicker LazyColumn"), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + itemsIndexed(providers) { index, provider -> + Box( + modifier = Modifier + .padding(bottom = if (index < providers.lastIndex) 16.dp else 0.dp) + ) { + AuthProviderButton( + modifier = Modifier + .fillMaxWidth(), + onClick = { + onProviderSelected(provider) + }, + provider = provider, + stringProvider = DefaultAuthUIStringProvider(context) + ) + } + } + } + } + } + AnnotatedStringResource( + context = context, + inPreview = inPreview, + previewText = "By continuing, you accept our Terms of Service and Privacy Policy.", + modifier = Modifier.padding(vertical = 16.dp), + id = R.string.fui_tos_and_pp, + links = arrayOf( + "Terms of Service" to (termsOfServiceUrl ?: ""), + "Privacy Policy" to (privacyPolicyUrl ?: "") + ) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewAuthMethodPicker() { + Column( + modifier = Modifier + .fillMaxSize() + ) { + AuthMethodPicker( + providers = listOf( + AuthProvider.Email( + actionCodeSettings = null, + passwordValidationRules = emptyList() + ), + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null, + ), + AuthProvider.Google( + scopes = emptyList(), + serverClientId = null + ), + AuthProvider.Facebook(), + AuthProvider.Twitter( + customParameters = emptyMap() + ), + AuthProvider.Github( + customParameters = emptyMap() + ), + AuthProvider.Microsoft( + tenant = null, + customParameters = emptyMap() + ), + AuthProvider.Yahoo( + customParameters = emptyMap() + ), + AuthProvider.Apple( + locale = null, + customParameters = emptyMap() + ), + AuthProvider.Anonymous, + ), + logo = AuthUIAsset.Resource(R.drawable.fui_ic_check_circle_black_128dp), + onProviderSelected = { provider -> + + }, + termsOfServiceUrl = "https://example.com/terms", + privacyPolicyUrl = "https://example.com/privacy" + ) + } +} \ No newline at end of file diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml index 75060012d..5790ca8d2 100644 --- a/auth/src/main/res/values/strings.xml +++ b/auth/src/main/res/values/strings.xml @@ -19,6 +19,7 @@ Email + "Auth method picker logo" Sign in with Google Sign in with Facebook Sign in with Twitter diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt index e8bf0fd4a..f08be227f 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt @@ -20,8 +20,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.R -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme import com.google.common.truth.Truth.assertThat import com.google.firebase.auth.actionCodeSettings diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/PasswordRuleTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/PasswordRuleTest.kt index a4d5139a6..dbaccbcfb 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/PasswordRuleTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/PasswordRuleTest.kt @@ -17,8 +17,8 @@ package com.firebase.ui.auth.compose.configuration import android.content.Context import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.R -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidatorTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidatorTest.kt index 3253cfb1c..3715d63ec 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidatorTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/EmailValidatorTest.kt @@ -17,8 +17,8 @@ package com.firebase.ui.auth.compose.configuration.validators import android.content.Context import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.R -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidatorTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidatorTest.kt index a3993bd36..27d34b6a6 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidatorTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/validators/PasswordValidatorTest.kt @@ -17,8 +17,8 @@ package com.firebase.ui.auth.compose.configuration.validators import android.content.Context import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.R -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.compose.configuration.PasswordRule import com.google.common.truth.Truth.assertThat import org.junit.Before diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButtonTest.kt similarity index 98% rename from auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt rename to auth/src/test/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButtonTest.kt index 255fd53af..faae2cf48 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/ui/components/AuthProviderButtonTest.kt @@ -12,7 +12,7 @@ * limitations under the License. */ -package com.firebase.ui.auth.compose +package com.firebase.ui.auth.compose.ui.components import android.content.Context import androidx.compose.material.icons.Icons @@ -29,8 +29,8 @@ import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.compose.configuration.AuthProvider import com.firebase.ui.auth.compose.configuration.Provider -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.firebase.ui.auth.compose.configuration.stringprovider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme import com.google.common.truth.Truth.assertThat diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/ErrorRecoveryDialogLogicTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialogLogicTest.kt similarity index 72% rename from auth/src/test/java/com/firebase/ui/auth/compose/ErrorRecoveryDialogLogicTest.kt rename to auth/src/test/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialogLogicTest.kt index f3c730b05..6a4f5df2f 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/ErrorRecoveryDialogLogicTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/ui/components/ErrorRecoveryDialogLogicTest.kt @@ -1,25 +1,11 @@ -/* - * 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.compose +package com.firebase.ui.auth.compose.ui.components -import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider -import com.google.common.truth.Truth.assertThat +import com.firebase.ui.auth.compose.AuthException +import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider +import com.google.common.truth.Truth import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` +import org.mockito.Mockito import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -30,20 +16,20 @@ import org.robolectric.annotation.Config @Config(manifest = Config.NONE) class ErrorRecoveryDialogLogicTest { - private val mockStringProvider = mock(AuthUIStringProvider::class.java).apply { - `when`(retryAction).thenReturn("Try again") - `when`(continueText).thenReturn("Continue") - `when`(signInDefault).thenReturn("Sign in") - `when`(networkErrorRecoveryMessage).thenReturn("Network error, check your internet connection.") - `when`(invalidCredentialsRecoveryMessage).thenReturn("Incorrect password.") - `when`(userNotFoundRecoveryMessage).thenReturn("That email address doesn't match an existing account") - `when`(weakPasswordRecoveryMessage).thenReturn("Password not strong enough. Use at least 6 characters and a mix of letters and numbers") - `when`(emailAlreadyInUseRecoveryMessage).thenReturn("Email account registration unsuccessful") - `when`(tooManyRequestsRecoveryMessage).thenReturn("This phone number has been used too many times") - `when`(mfaRequiredRecoveryMessage).thenReturn("Additional verification required. Please complete multi-factor authentication.") - `when`(accountLinkingRequiredRecoveryMessage).thenReturn("Account needs to be linked. Please try a different sign-in method.") - `when`(authCancelledRecoveryMessage).thenReturn("Authentication was cancelled. Please try again when ready.") - `when`(unknownErrorRecoveryMessage).thenReturn("An unknown error occurred.") + private val mockStringProvider = Mockito.mock(AuthUIStringProvider::class.java).apply { + Mockito.`when`(retryAction).thenReturn("Try again") + Mockito.`when`(continueText).thenReturn("Continue") + Mockito.`when`(signInDefault).thenReturn("Sign in") + Mockito.`when`(networkErrorRecoveryMessage).thenReturn("Network error, check your internet connection.") + Mockito.`when`(invalidCredentialsRecoveryMessage).thenReturn("Incorrect password.") + Mockito.`when`(userNotFoundRecoveryMessage).thenReturn("That email address doesn't match an existing account") + Mockito.`when`(weakPasswordRecoveryMessage).thenReturn("Password not strong enough. Use at least 6 characters and a mix of letters and numbers") + Mockito.`when`(emailAlreadyInUseRecoveryMessage).thenReturn("Email account registration unsuccessful") + Mockito.`when`(tooManyRequestsRecoveryMessage).thenReturn("This phone number has been used too many times") + Mockito.`when`(mfaRequiredRecoveryMessage).thenReturn("Additional verification required. Please complete multi-factor authentication.") + Mockito.`when`(accountLinkingRequiredRecoveryMessage).thenReturn("Account needs to be linked. Please try a different sign-in method.") + Mockito.`when`(authCancelledRecoveryMessage).thenReturn("Authentication was cancelled. Please try again when ready.") + Mockito.`when`(unknownErrorRecoveryMessage).thenReturn("An unknown error occurred.") } // ============================================================================================= @@ -59,7 +45,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Network error, check your internet connection.") + Truth.assertThat(message).isEqualTo("Network error, check your internet connection.") } @Test @@ -71,7 +57,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Incorrect password.") + Truth.assertThat(message).isEqualTo("Incorrect password.") } @Test @@ -83,7 +69,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("That email address doesn't match an existing account") + Truth.assertThat(message).isEqualTo("That email address doesn't match an existing account") } @Test @@ -99,7 +85,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers\n\nReason: Password should be at least 8 characters") + Truth.assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers\n\nReason: Password should be at least 8 characters") } @Test @@ -111,7 +97,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers") + Truth.assertThat(message).isEqualTo("Password not strong enough. Use at least 6 characters and a mix of letters and numbers") } @Test @@ -127,7 +113,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Email account registration unsuccessful (test@example.com)") + Truth.assertThat(message).isEqualTo("Email account registration unsuccessful (test@example.com)") } @Test @@ -139,7 +125,7 @@ class ErrorRecoveryDialogLogicTest { val message = getRecoveryMessage(error, mockStringProvider) // Assert - assertThat(message).isEqualTo("Email account registration unsuccessful") + Truth.assertThat(message).isEqualTo("Email account registration unsuccessful") } // ============================================================================================= @@ -155,7 +141,7 @@ class ErrorRecoveryDialogLogicTest { val actionText = getRecoveryActionText(error, mockStringProvider) // Assert - assertThat(actionText).isEqualTo("Try again") + Truth.assertThat(actionText).isEqualTo("Try again") } @Test @@ -167,7 +153,7 @@ class ErrorRecoveryDialogLogicTest { val actionText = getRecoveryActionText(error, mockStringProvider) // Assert - assertThat(actionText).isEqualTo("Continue") + Truth.assertThat(actionText).isEqualTo("Continue") } @Test @@ -179,7 +165,7 @@ class ErrorRecoveryDialogLogicTest { val actionText = getRecoveryActionText(error, mockStringProvider) // Assert - assertThat(actionText).isEqualTo("Sign in") + Truth.assertThat(actionText).isEqualTo("Sign in") } @Test @@ -191,7 +177,7 @@ class ErrorRecoveryDialogLogicTest { val actionText = getRecoveryActionText(error, mockStringProvider) // Assert - assertThat(actionText).isEqualTo("Continue") + Truth.assertThat(actionText).isEqualTo("Continue") } @Test @@ -203,7 +189,7 @@ class ErrorRecoveryDialogLogicTest { val actionText = getRecoveryActionText(error, mockStringProvider) // Assert - assertThat(actionText).isEqualTo("Continue") + Truth.assertThat(actionText).isEqualTo("Continue") } // ============================================================================================= @@ -216,7 +202,7 @@ class ErrorRecoveryDialogLogicTest { val error = AuthException.NetworkException("Network error") // Act & Assert - assertThat(isRecoverable(error)).isTrue() + Truth.assertThat(isRecoverable(error)).isTrue() } @Test @@ -225,7 +211,7 @@ class ErrorRecoveryDialogLogicTest { val error = AuthException.InvalidCredentialsException("Invalid credentials") // Act & Assert - assertThat(isRecoverable(error)).isTrue() + Truth.assertThat(isRecoverable(error)).isTrue() } @Test @@ -234,7 +220,7 @@ class ErrorRecoveryDialogLogicTest { val error = AuthException.TooManyRequestsException("Too many requests") // Act & Assert - assertThat(isRecoverable(error)).isFalse() + Truth.assertThat(isRecoverable(error)).isFalse() } @Test @@ -243,7 +229,7 @@ class ErrorRecoveryDialogLogicTest { val error = AuthException.MfaRequiredException("MFA required") // Act & Assert - assertThat(isRecoverable(error)).isTrue() + Truth.assertThat(isRecoverable(error)).isTrue() } @Test @@ -252,7 +238,7 @@ class ErrorRecoveryDialogLogicTest { val error = AuthException.UnknownException("Unknown error") // Act & Assert - assertThat(isRecoverable(error)).isTrue() + Truth.assertThat(isRecoverable(error)).isTrue() } // Helper functions to test the private functions - we need to make them internal for testing diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPickerTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPickerTest.kt new file mode 100644 index 000000000..17d736ca7 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/ui/method_picker/AuthMethodPickerTest.kt @@ -0,0 +1,308 @@ +package com.firebase.ui.auth.compose.ui.method_picker + +import android.content.Context +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.R +import com.firebase.ui.auth.compose.configuration.AuthProvider +import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset +import com.google.common.truth.Truth +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 [AuthMethodPicker] covering UI interactions, provider selection, + * scroll tests, logo display, and custom layouts. + * + * @suppress Internal test class + */ +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class AuthMethodPickerTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var context: Context + private var selectedProvider: AuthProvider? = null + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + selectedProvider = null + } + + // ============================================================================================= + // Basic UI Tests + // ============================================================================================= + + @Test + fun `AuthMethodPicker displays all providers`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null), + AuthProvider.Facebook(), + AuthProvider.Email(actionCodeSettings = null, passwordValidationRules = emptyList()) + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_facebook)) + .assertIsDisplayed() + .assertHasClickAction() + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_email)) + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun `AuthMethodPicker displays terms of service text`() { + val context = ApplicationProvider.getApplicationContext() + 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) + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_tos_and_pp, *labels)) + .assertIsDisplayed() + } + + @Test + fun `AuthMethodPicker displays logo when provided`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + logo = AuthUIAsset.Resource(R.drawable.fui_ic_check_circle_black_128dp), + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithContentDescription(context.getString(R.string.fui_auth_method_picker_logo)) + .assertIsDisplayed() + } + + @Test + fun `AuthMethodPicker does not display logo when null`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + logo = null, + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithContentDescription(context.getString(R.string.fui_auth_method_picker_logo)) + .assertIsNotDisplayed() + } + + @Test + fun `AuthMethodPicker displays logo and providers together`() { + val context = ApplicationProvider.getApplicationContext() + 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) + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + logo = AuthUIAsset.Resource(R.drawable.fui_ic_check_circle_black_128dp), + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithContentDescription(context.getString(R.string.fui_auth_method_picker_logo)) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_tos_and_pp, *labels)) + .assertIsDisplayed() + } + + @Test + fun `AuthMethodPicker calls onProviderSelected when Provider is clicked`() { + val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + val providers = listOf(googleProvider) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .performClick() + + Truth.assertThat(selectedProvider).isEqualTo(googleProvider) + } + + // ============================================================================================= + // Custom Layout Tests + // ============================================================================================= + + @Test + fun `AuthMethodPicker uses custom layout when provided`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null) + ) + var customLayoutCalled = false + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + customLayout = { _, _ -> + customLayoutCalled = true + Text("Custom Layout") + } + ) + } + + Truth.assertThat(customLayoutCalled).isTrue() + composeTestRule + .onNodeWithText("Custom Layout") + .assertIsDisplayed() + } + + @Test + fun `AuthMethodPicker custom layout receives providers list`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null), + AuthProvider.Facebook() + ) + var receivedProviders: List? = null + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + customLayout = { providersList, _ -> + receivedProviders = providersList + } + ) + } + + Truth.assertThat(receivedProviders).isEqualTo(providers) + } + + @Test + fun `AuthMethodPicker custom layout can trigger provider selection`() { + val googleProvider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + val providers = listOf(googleProvider) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it }, + customLayout = { providersList, onSelected -> + Button(onClick = { onSelected(providersList[0]) }) { + Text("Custom Button") + } + } + ) + } + + composeTestRule + .onNodeWithText("Custom Button") + .performClick() + + Truth.assertThat(selectedProvider).isEqualTo(googleProvider) + } + + // ============================================================================================= + // Scrolling Tests + // ============================================================================================= + + @Test + fun `AuthMethodPicker allows scrolling through many providers`() { + val providers = listOf( + AuthProvider.Google(scopes = emptyList(), serverClientId = null), + AuthProvider.Facebook(), + AuthProvider.Twitter(customParameters = emptyMap()), + AuthProvider.Github(customParameters = emptyMap()), + AuthProvider.Microsoft(tenant = null, customParameters = emptyMap()), + AuthProvider.Yahoo(customParameters = emptyMap()), + AuthProvider.Apple(locale = null, customParameters = emptyMap()), + AuthProvider.Email(actionCodeSettings = null, passwordValidationRules = emptyList()), + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ), + AuthProvider.Anonymous + ) + + composeTestRule.setContent { + AuthMethodPicker( + providers = providers, + onProviderSelected = { selectedProvider = it } + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + + composeTestRule + .onNodeWithTag("AuthMethodPicker LazyColumn") + .performScrollToNode(hasText(context.getString(R.string.fui_sign_in_anonymously))) + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_anonymously)) + .assertIsDisplayed() + } +} \ No newline at end of file