diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 2eddf8176..aa9de475c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -8,13 +8,25 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: set up JDK 17 - uses: actions/setup-java@v1 + - uses: actions/checkout@v4 + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: 17 + java-version: '17' + distribution: 'temurin' + - name: Build with Gradle run: ./scripts/build.sh + - name: Print Logs if: failure() run: ./scripts/print_build_logs.sh diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt b/auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt new file mode 100644 index 000000000..da6b2bbd9 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/AuthProviderButton.kt @@ -0,0 +1,278 @@ +package com.firebase.ui.auth.compose + +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.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.theme.AuthUIAsset +import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme + +/** + * A customizable button for an authentication provider. + * + * This button displays the icon and name of an authentication provider (e.g., Google, Facebook). + * It is designed to be used within a list of sign-in options. The button's appearance can be + * customized using the [style] parameter, and its text is localized via the [stringProvider]. + * + * **Example usage:** + * ```kotlin + * AuthProviderButton( + * provider = AuthProvider.Facebook(), + * onClick = { /* Handle Facebook sign-in */ }, + * stringProvider = DefaultAuthUIStringProvider(LocalContext.current) + * ) + * ``` + * + * @param modifier A modifier for the button + * @param provider The provider to represent. + * @param onClick A callback when the button is clicked + * @param enabled If the button is enabled. Defaults to true. + * @param style Optional custom styling for the button. + * @param stringProvider The [AuthUIStringProvider] for localized strings + * + * @since 10.0.0 + */ +@Composable +fun AuthProviderButton( + modifier: Modifier = Modifier, + provider: AuthProvider, + onClick: () -> Unit, + enabled: Boolean = true, + style: AuthUITheme.ProviderStyle? = null, + stringProvider: AuthUIStringProvider, +) { + val providerStyle = resolveProviderStyle(provider, style) + val providerText = resolveProviderLabel(provider, stringProvider) + + Button( + modifier = modifier, + colors = ButtonDefaults.buttonColors( + containerColor = providerStyle.backgroundColor, + contentColor = providerStyle.contentColor, + ), + shape = providerStyle.shape, + elevation = ButtonDefaults.buttonElevation( + defaultElevation = providerStyle.elevation + ), + onClick = onClick, + enabled = enabled, + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + val providerIcon = providerStyle.icon + if (providerIcon != null) { + val iconTint = providerStyle.iconTint + if (iconTint != null) { + Icon( + painter = providerIcon.painter, + contentDescription = providerText, + tint = iconTint + ) + } else { + Image( + painter = providerIcon.painter, + contentDescription = providerText + ) + } + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + text = providerText + ) + } + } +} + +internal fun resolveProviderStyle( + provider: AuthProvider, + style: AuthUITheme.ProviderStyle?, +): AuthUITheme.ProviderStyle { + if (style != null) return style + + val defaultStyle = + AuthUITheme.Default.providerStyles[provider.providerId] ?: AuthUITheme.ProviderStyle.Empty + + return if (provider is AuthProvider.GenericOAuth) { + AuthUITheme.ProviderStyle( + icon = provider.buttonIcon ?: defaultStyle.icon, + backgroundColor = provider.buttonColor ?: defaultStyle.backgroundColor, + contentColor = provider.contentColor ?: defaultStyle.contentColor, + ) + } else { + defaultStyle + } +} + +internal fun resolveProviderLabel( + provider: AuthProvider, + stringProvider: AuthUIStringProvider +): String = when (provider) { + is AuthProvider.GenericOAuth -> provider.buttonLabel + else -> when (Provider.fromId(provider.providerId)) { + Provider.GOOGLE -> stringProvider.signInWithGoogle + Provider.FACEBOOK -> stringProvider.signInWithFacebook + Provider.TWITTER -> stringProvider.signInWithTwitter + Provider.GITHUB -> stringProvider.signInWithGithub + Provider.EMAIL -> stringProvider.signInWithEmail + Provider.PHONE -> stringProvider.signInWithPhone + Provider.ANONYMOUS -> stringProvider.signInAnonymously + Provider.MICROSOFT -> stringProvider.signInWithMicrosoft + Provider.YAHOO -> stringProvider.signInWithYahoo + Provider.APPLE -> stringProvider.signInWithApple + null -> "Unknown Provider" + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewAuthProviderButton() { + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AuthProviderButton( + provider = AuthProvider.Email( + actionCodeSettings = null, + passwordValidationRules = emptyList() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null, + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Google( + scopes = emptyList(), + serverClientId = null + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Facebook(), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Twitter( + customParameters = emptyMap() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Github( + customParameters = emptyMap() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Microsoft( + tenant = null, + customParameters = emptyMap() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Yahoo( + customParameters = emptyMap() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Apple( + locale = null, + customParameters = emptyMap() + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.Anonymous, + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Generic Provider", + buttonIcon = AuthUIAsset.Vector(Icons.Default.Star), + buttonColor = Color.Gray, + contentColor = Color.White + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Custom Style", + buttonIcon = AuthUIAsset.Vector(Icons.Default.Star), + buttonColor = Color.Gray, + contentColor = Color.White + ), + onClick = {}, + style = AuthUITheme.ProviderStyle( + icon = AuthUITheme.Default.providerStyles[Provider.MICROSOFT.id]?.icon, + backgroundColor = AuthUITheme.Default.providerStyles[Provider.MICROSOFT.id]!!.backgroundColor, + contentColor = AuthUITheme.Default.providerStyles[Provider.MICROSOFT.id]!!.contentColor, + iconTint = Color.Red, + shape = RoundedCornerShape(24.dp), + elevation = 6.dp + ), + stringProvider = DefaultAuthUIStringProvider(context) + ) + AuthProviderButton( + provider = AuthProvider.GenericOAuth( + providerId = "unknown_provider", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = "Unsupported Provider", + buttonIcon = null, + buttonColor = null, + contentColor = null, + ), + onClick = {}, + stringProvider = DefaultAuthUIStringProvider(context) + ) + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt index d44a9d6d9..87008cd28 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt @@ -15,11 +15,10 @@ package com.firebase.ui.auth.compose.configuration import android.content.Context -import android.graphics.Color +import androidx.compose.ui.graphics.Color import android.util.Log -import androidx.compose.ui.graphics.vector.ImageVector -import com.firebase.ui.auth.AuthUI import com.firebase.ui.auth.R +import com.firebase.ui.auth.compose.configuration.theme.AuthUIAsset import com.firebase.ui.auth.util.Preconditions import com.firebase.ui.auth.util.data.PhoneNumberUtils import com.firebase.ui.auth.util.data.ProviderAvailability @@ -55,7 +54,13 @@ internal enum class Provider(val id: String) { ANONYMOUS("anonymous"), MICROSOFT("microsoft.com"), YAHOO("yahoo.com"), - APPLE("apple.com"), + APPLE("apple.com"); + + companion object { + fun fromId(id: String): Provider? { + return entries.find { it.id == id } + } + } } /** @@ -446,12 +451,17 @@ abstract class AuthProvider(open val providerId: String) { /** * An optional icon for the provider button. */ - val buttonIcon: ImageVector?, + val buttonIcon: AuthUIAsset?, /** * An optional background color for the provider button. */ - val buttonColor: Color? + val buttonColor: Color?, + + /** + * An optional content color for the provider button. + */ + val contentColor: Color? ) : OAuthProvider( providerId = providerId, scopes = scopes, diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUIAsset.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUIAsset.kt new file mode 100644 index 000000000..f54f0ed5b --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUIAsset.kt @@ -0,0 +1,67 @@ +/* + * 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.configuration.theme + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource + +/** + * Represents a visual asset used in the authentication UI. + * + * This sealed class allows specifying icons and images from either Android drawable + * resources ([Resource]) or Jetpack Compose [ImageVector]s ([Vector]). The [painter] + * property provides a unified way to get a [Painter] for the asset within a composable. + * + * **Example usage:** + * ```kotlin + * // To use a drawable resource: + * val asset = AuthUIAsset.Resource(R.drawable.my_logo) + * + * // To use a vector asset: + * val vectorAsset = AuthUIAsset.Vector(Icons.Default.Info) + * ``` + */ +sealed class AuthUIAsset { + /** + * An asset loaded from a drawable resource. + * + * @param resId The resource ID of the drawable (e.g., `R.drawable.my_icon`). + */ + class Resource(@param:DrawableRes val resId: Int) : AuthUIAsset() + + /** + * An asset represented by an [ImageVector]. + * + * @param image The [ImageVector] to be displayed. + */ + class Vector(val image: ImageVector) : AuthUIAsset() + + /** + * A [Painter] that can be used to draw this asset in a composable. + * + * This property automatically resolves the asset type and returns the appropriate + * [Painter] for rendering. + */ + @get:Composable + internal val painter: Painter + get() = when (this) { + is Resource -> painterResource(resId) + is Vector -> rememberVectorPainter(image) + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUITheme.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUITheme.kt index d83cf5923..4af62ffc8 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUITheme.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/AuthUITheme.kt @@ -56,6 +56,11 @@ class AuthUITheme( * provider button, allowing for per-provider branding and customization. */ class ProviderStyle( + /** + * The provider's icon. + */ + val icon: AuthUIAsset?, + /** * The background color of the button. */ @@ -81,7 +86,19 @@ class AuthUITheme( * The shadow elevation for the button. Defaults to 2.dp. */ val elevation: Dp = 2.dp - ) + ) { + internal companion object { + /** + * A fallback style for unknown providers with no icon, white background, + * and black text. + */ + val Empty = ProviderStyle( + icon = null, + backgroundColor = Color.White, + contentColor = Color.Black, + ) + } + } companion object { /** @@ -112,7 +129,6 @@ class AuthUITheme( providerStyles = providerStyles ) } - } } diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/ProviderStyleDefaults.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/ProviderStyleDefaults.kt index 4f063c9a5..7f053fbd3 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/ProviderStyleDefaults.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/theme/ProviderStyleDefaults.kt @@ -15,6 +15,7 @@ package com.firebase.ui.auth.compose.configuration.theme import androidx.compose.ui.graphics.Color +import com.firebase.ui.auth.R import com.firebase.ui.auth.compose.configuration.Provider /** @@ -33,6 +34,7 @@ internal object ProviderStyleDefaults { when (provider) { Provider.GOOGLE -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_googleg_color_24dp), backgroundColor = Color.White, contentColor = Color(0xFF757575) ) @@ -40,6 +42,7 @@ internal object ProviderStyleDefaults { Provider.FACEBOOK -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_facebook_white_22dp), backgroundColor = Color(0xFF3B5998), contentColor = Color.White ) @@ -47,6 +50,7 @@ internal object ProviderStyleDefaults { Provider.TWITTER -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_twitter_bird_white_24dp), backgroundColor = Color(0xFF5BAAF4), contentColor = Color.White ) @@ -54,6 +58,7 @@ internal object ProviderStyleDefaults { Provider.GITHUB -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_github_white_24dp), backgroundColor = Color(0xFF24292E), contentColor = Color.White ) @@ -61,6 +66,7 @@ internal object ProviderStyleDefaults { Provider.EMAIL -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_mail_white_24dp), backgroundColor = Color(0xFFD0021B), contentColor = Color.White ) @@ -68,6 +74,7 @@ internal object ProviderStyleDefaults { Provider.PHONE -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_phone_white_24dp), backgroundColor = Color(0xFF43C5A5), contentColor = Color.White ) @@ -75,6 +82,7 @@ internal object ProviderStyleDefaults { Provider.ANONYMOUS -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_anonymous_white_24dp), backgroundColor = Color(0xFFF4B400), contentColor = Color.White ) @@ -82,6 +90,7 @@ internal object ProviderStyleDefaults { Provider.MICROSOFT -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_microsoft_24dp), backgroundColor = Color(0xFF2F2F2F), contentColor = Color.White ) @@ -89,6 +98,7 @@ internal object ProviderStyleDefaults { Provider.YAHOO -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_yahoo_24dp), backgroundColor = Color(0xFF720E9E), contentColor = Color.White ) @@ -96,6 +106,7 @@ internal object ProviderStyleDefaults { Provider.APPLE -> { provider.id to AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_apple_white_24dp), backgroundColor = Color.Black, contentColor = Color.White ) diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt new file mode 100644 index 000000000..255fd53af --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/AuthProviderButtonTest.kt @@ -0,0 +1,518 @@ +/* + * 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 + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +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.theme.AuthUIAsset +import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme +import com.google.common.truth.Truth.assertThat +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 +import com.firebase.ui.auth.R + +/** + * Unit tests for [AuthProviderButton] covering UI interactions, styling, + * and provider-specific behavior. + * + * @suppress Internal test class + */ +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class AuthProviderButtonTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var context: Context + private lateinit var stringProvider: AuthUIStringProvider + private var clickedProvider: AuthProvider? = null + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + stringProvider = DefaultAuthUIStringProvider(context) + clickedProvider = null + } + + // ============================================================================================= + // Basic UI Tests + // ============================================================================================= + + @Test + fun `AuthProviderButton displays Google provider correctly`() { + val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Facebook provider correctly`() { + val provider = AuthProvider.Facebook() + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_facebook)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Email provider correctly`() { + val provider = AuthProvider.Email( + actionCodeSettings = null, + passwordValidationRules = emptyList() + ) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_email)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Phone provider correctly`() { + val provider = AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_phone)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Anonymous provider correctly`() { + val provider = AuthProvider.Anonymous + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_anonymously)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Twitter provider correctly`() { + val provider = AuthProvider.Twitter(customParameters = emptyMap()) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_twitter)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Github provider correctly`() { + val provider = AuthProvider.Github(customParameters = emptyMap()) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_github)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Microsoft provider correctly`() { + val provider = AuthProvider.Microsoft(tenant = null, customParameters = emptyMap()) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_microsoft)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Yahoo provider correctly`() { + val provider = AuthProvider.Yahoo(customParameters = emptyMap()) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_yahoo)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays Apple provider correctly`() { + val provider = AuthProvider.Apple(locale = null, customParameters = emptyMap()) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_apple)) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + @Test + fun `AuthProviderButton displays GenericOAuth provider with custom label`() { + val customLabel = "Sign in with Custom Provider" + val provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = customLabel, + buttonIcon = AuthUIAsset.Vector(Icons.Default.Star), + buttonColor = Color.Blue, + contentColor = Color.White + ) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(customLabel) + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + // ============================================================================================= + // Click Interaction Tests + // ============================================================================================= + + @Test + fun `AuthProviderButton onClick is called when clicked`() { + val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .performClick() + + assertThat(clickedProvider).isEqualTo(provider) + } + + @Test + fun `AuthProviderButton respects enabled state`() { + val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { clickedProvider = provider }, + enabled = false, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsNotEnabled() + .performClick() + + assertThat(clickedProvider).isNull() + } + + // ============================================================================================= + // Style Resolution Tests + // ============================================================================================= + + @Test + fun `AuthProviderButton uses custom style when provided`() { + val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + val customStyle = AuthUITheme.Default.providerStyles[Provider.FACEBOOK.id] + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { }, + style = customStyle, + stringProvider = stringProvider + ) + } + + composeTestRule + .onNodeWithText(context.getString(R.string.fui_sign_in_with_google)) + .assertIsDisplayed() + + val resolvedStyle = resolveProviderStyle(provider, customStyle) + assertThat(resolvedStyle).isEqualTo(customStyle) + assertThat(resolvedStyle) + .isNotEqualTo(AuthUITheme.Default.providerStyles[Provider.GOOGLE.id]) + } + + @Test + fun `GenericOAuth provider uses custom styling properties`() { + val customLabel = "Custom Provider" + val customColor = Color.Green + val customContentColor = Color.Black + val customIcon = AuthUIAsset.Vector(Icons.Default.Star) + + val provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = customLabel, + buttonIcon = customIcon, + buttonColor = customColor, + contentColor = customContentColor + ) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { }, + stringProvider = stringProvider + ) + } + + composeTestRule.onNodeWithText(customLabel) + .assertIsDisplayed() + + composeTestRule.onNodeWithContentDescription(customLabel) + .assertIsDisplayed() + + val resolvedStyle = resolveProviderStyle(provider, null) + assertThat(resolvedStyle).isNotNull() + assertThat(resolvedStyle.backgroundColor).isEqualTo(customColor) + assertThat(resolvedStyle.contentColor).isEqualTo(customContentColor) + assertThat(resolvedStyle.icon).isEqualTo(customIcon) + + val googleDefaultStyle = AuthUITheme.Default.providerStyles["google.com"] + assertThat(resolvedStyle).isNotEqualTo(googleDefaultStyle) + } + + @Test + fun `GenericOAuth provider falls back to default style when custom properties are null`() { + val customLabel = "Custom Provider" + val provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonLabel = customLabel, + buttonIcon = null, + buttonColor = null, + contentColor = null + ) + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { }, + stringProvider = stringProvider + ) + } + + composeTestRule.onNodeWithText(customLabel) + .assertIsDisplayed() + + val resolvedStyle = resolveProviderStyle(provider, null) + val googleDefaultStyle = AuthUITheme.Default.providerStyles["google.com"] + + assertThat(googleDefaultStyle).isNotNull() + assertThat(resolvedStyle.backgroundColor).isEqualTo(googleDefaultStyle!!.backgroundColor) + assertThat(resolvedStyle.contentColor).isEqualTo(googleDefaultStyle.contentColor) + assertThat(resolvedStyle.icon).isEqualTo(googleDefaultStyle.icon) + } + + // ============================================================================================= + // Provider Style Fallback Tests + // ============================================================================================= + + @Test + fun `AuthProviderButton provides fallback for unknown provider`() { + val provider = object : AuthProvider("unknown.provider") {} + + composeTestRule.setContent { + AuthProviderButton( + provider = provider, + onClick = { }, + stringProvider = stringProvider + ) + } + + composeTestRule.onNodeWithText("Unknown Provider") + .assertIsDisplayed() + .assertHasClickAction() + .assertIsEnabled() + } + + + @Test + fun `resolveProviderStyle applies custom colors for GenericOAuth with icon`() { + val customColor = Color.Red + val customContentColor = Color.White + + val provider = AuthProvider.GenericOAuth( + providerId = "google.com", + scopes = emptyList(), + customParameters = emptyMap(), + buttonIcon = AuthUIAsset.Vector(Icons.Default.Star), + buttonLabel = "Custom", + buttonColor = customColor, + contentColor = customContentColor + ) + + val resolvedStyle = resolveProviderStyle(provider, null) + + assertThat(resolvedStyle).isNotNull() + assertThat(resolvedStyle.backgroundColor).isEqualTo(customColor) + assertThat(resolvedStyle.contentColor).isEqualTo(customContentColor) + } + + @Test + fun `resolveProviderStyle handles GenericOAuth without icon`() { + val provider = AuthProvider.GenericOAuth( + providerId = "custom.provider", + scopes = emptyList(), + customParameters = emptyMap(), + buttonIcon = null, + buttonLabel = "Custom", + buttonColor = Color.Blue, + contentColor = Color.White + ) + + val resolvedStyle = resolveProviderStyle(provider, null) + + assertThat(resolvedStyle).isNotNull() + assertThat(resolvedStyle.icon).isNull() + assertThat(resolvedStyle.backgroundColor).isEqualTo(Color.Blue) + assertThat(resolvedStyle.contentColor).isEqualTo(Color.White) + } + + @Test + fun `resolveProviderStyle provides fallback for unknown provider`() { + val provider = object : AuthProvider("unknown.provider") {} + + val resolvedStyle = resolveProviderStyle(provider, null) + + assertThat(resolvedStyle).isNotNull() + assertThat(resolvedStyle).isEqualTo(AuthUITheme.ProviderStyle.Empty) + } +} \ No newline at end of file diff --git a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthProviderTest.kt b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthProviderTest.kt index 27685f859..c473867c4 100644 --- a/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthProviderTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthProviderTest.kt @@ -353,7 +353,8 @@ class AuthProviderTest { customParameters = mapOf(), buttonLabel = "Sign in with Custom", buttonIcon = null, - buttonColor = null + buttonColor = null, + contentColor = null, ) provider.validate() @@ -367,7 +368,8 @@ class AuthProviderTest { customParameters = mapOf(), buttonLabel = "Sign in with Custom", buttonIcon = null, - buttonColor = null + buttonColor = null, + contentColor = null, ) try { @@ -386,7 +388,8 @@ class AuthProviderTest { customParameters = mapOf(), buttonLabel = "", buttonIcon = null, - buttonColor = null + buttonColor = null, + contentColor = null, ) try { 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 118977c09..e8bf0fd4a 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 @@ -262,7 +262,12 @@ class AuthUIConfigurationTest { authUIConfiguration { context = applicationContext providers { - provider(AuthProvider.Google(scopes = listOf(), serverClientId = "test_client_id")) + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "test_client_id" + ) + ) } } } catch (e: Exception) { @@ -293,7 +298,13 @@ class AuthUIConfigurationTest { provider(AuthProvider.Microsoft(customParameters = mapOf(), tenant = null)) provider(AuthProvider.Yahoo(customParameters = mapOf())) provider(AuthProvider.Apple(customParameters = mapOf(), locale = null)) - provider(AuthProvider.Phone(defaultNumber = null, defaultCountryCode = null, allowedCountries = null)) + provider( + AuthProvider.Phone( + defaultNumber = null, + defaultCountryCode = null, + allowedCountries = null + ) + ) provider( AuthProvider.Email( actionCodeSettings = null, @@ -313,7 +324,8 @@ class AuthUIConfigurationTest { customParameters = mapOf(), buttonLabel = "Test", buttonIcon = null, - buttonColor = null + buttonColor = null, + contentColor = null, ) try { diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 7e9352e85..8b2317698 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -11,7 +11,7 @@ object Config { } object Plugins { - const val android = "com.android.tools.build:gradle:8.8.0" + const val android = "com.android.tools.build:gradle:8.10.0" const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" const val google = "com.google.gms:google-services:4.3.8" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5c40527d4..4eaec4670 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/proguard-tests/build.gradle.kts b/proguard-tests/build.gradle.kts index edda9e6f0..f6aed148d 100644 --- a/proguard-tests/build.gradle.kts +++ b/proguard-tests/build.gradle.kts @@ -53,7 +53,8 @@ android { "InvalidPackage", // Firestore uses GRPC which makes lint mad "NewerVersionAvailable", "GradleDependency", // For reproducible builds "SelectableText", "SyntheticAccessor", // We almost never care about this - "MediaCapabilities" + "MediaCapabilities", + "MissingApplicationIcon" ) checkAllWarnings = true @@ -79,6 +80,7 @@ dependencies { implementation(project(":database")) implementation(project(":storage")) + implementation(platform(Config.Libs.Firebase.bom)) implementation(Config.Libs.Androidx.lifecycleExtensions) } diff --git a/proguard-tests/src/main/AndroidManifest.xml b/proguard-tests/src/main/AndroidManifest.xml index 8072ee00d..ffb7f7034 100644 --- a/proguard-tests/src/main/AndroidManifest.xml +++ b/proguard-tests/src/main/AndroidManifest.xml @@ -1,2 +1,4 @@ - + + + \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index 964ae2352..63e5eaa7a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,6 +9,6 @@ cp library/google-services.json proguard-tests/google-services.json ./gradlew $GRADLE_ARGS clean ./gradlew $GRADLE_ARGS assembleDebug # TODO(thatfiredev): re-enable before release -#./gradlew $GRADLE_ARGS proguard-tests:build +# ./gradlew $GRADLE_ARGS proguard-tests:build ./gradlew $GRADLE_ARGS checkstyle ./gradlew $GRADLE_ARGS testDebugUnitTest