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