From a62097d6aa8d62d0ba46e3c4606189d64bbec5fa Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Mon, 15 Sep 2025 15:59:23 +0100 Subject: [PATCH 1/7] feat(AuthUIConfiguration): implement configuration model, DSL builder and tests --- auth/build.gradle.kts | 13 + .../compose/configuration/AuthProvider.kt | 270 ++++++++++++++++++ .../configuration/AuthUIConfiguration.kt | 157 ++++++++++ .../configuration/AuthUIStringProvider.kt | 41 +++ .../auth/compose/configuration/AuthUITheme.kt | 134 +++++++++ .../compose/configuration/PasswordRule.kt | 52 ++++ .../configuration/AuthUIConfigurationTest.kt | 233 +++++++++++++++ buildSrc/src/main/kotlin/Config.kt | 18 +- 8 files changed, 915 insertions(+), 3 deletions(-) create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIStringProvider.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt create mode 100644 auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt create mode 100644 auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 66df263a8..c027e0848 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("com.android.library") id("com.vanniktech.maven.publish") id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" } android { @@ -67,9 +68,20 @@ android { kotlinOptions { jvmTarget = "17" } + buildFeatures { + compose = true + } } dependencies { + implementation(platform(Config.Libs.Androidx.Compose.bom)) + implementation(Config.Libs.Androidx.Compose.ui) + implementation(Config.Libs.Androidx.Compose.uiGraphics) + implementation(Config.Libs.Androidx.Compose.material3) + implementation(Config.Libs.Androidx.Compose.foundation) + implementation(Config.Libs.Androidx.Compose.tooling) + implementation(Config.Libs.Androidx.Compose.toolingPreview) + implementation(Config.Libs.Androidx.Compose.activityCompose) implementation(Config.Libs.Androidx.materialDesign) implementation(Config.Libs.Androidx.activity) // The new activity result APIs force us to include Fragment 1.3.0 @@ -101,6 +113,7 @@ dependencies { testImplementation(Config.Libs.Test.mockito) testImplementation(Config.Libs.Test.core) testImplementation(Config.Libs.Test.robolectric) + testImplementation(Config.Libs.Test.kotlinReflect) testImplementation(Config.Libs.Provider.facebook) debugImplementation(project(":internal:lintchecks")) 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 new file mode 100644 index 000000000..36daa70a9 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthProvider.kt @@ -0,0 +1,270 @@ +/* + * 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 + +import android.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import com.google.firebase.auth.ActionCodeSettings + +@AuthUIConfigurationDsl +class AuthProvidersBuilder { + private val providers = mutableListOf() + + fun provider(provider: AuthProvider) { + providers.add(provider) + } + + internal fun build(): List = providers.toList() +} + +/** + * Base sealed class for authentication providers. + */ +sealed class AuthProvider() { + /** + * Email/Password authentication provider configuration. + */ + data class Email( + /** + * Requires the user to provide a display name. Defaults to true. + */ + val requireDisplayName: Boolean = true, + + /** + * Enables email link sign-in, Defaults to false. + */ + val enableEmailLinkSignIn: Boolean = false, + + /** + * Settings for email link actions. + */ + val actionCodeSettings: ActionCodeSettings?, + + /** + * Allows new accounts to be created. Defaults to true. + */ + val allowNewAccounts: Boolean = true, + + /** + * The minimum length for a password. Defaults to 6. + */ + val minimumPasswordLength: Int = 6, + + /** + * A list of custom password validation rules. + */ + val passwordValidationRules: List + ) : AuthProvider() + + /** + * Phone number authentication provider configuration. + */ + data class Phone( + /** + * The default country code to pre-select. + */ + val defaultCountryCode: String?, + + /** + * A list of allowed country codes. + */ + val allowedCountries: List?, + + /** + * The expected length of the SMS verification code. Defaults to 6. + */ + val smsCodeLength: Int = 6, + + /** + * The timeout in seconds for receiving the SMS. Defaults to 60L. + */ + val timeout: Long = 60L, + + /** + * Enables instant verification of the phone number. Defaults to true. + */ + val enableInstantVerification: Boolean = true, + + /** + * Enables automatic retrieval of the SMS code. Defaults to true. + */ + val enableAutoRetrieval: Boolean = true + ) : AuthProvider() + + /** + * Google Sign-In provider configuration. + */ + data class Google( + /** + * The list of scopes to request. + */ + val scopes: List, + + /** + * The OAuth 2.0 client ID for your server. + */ + val serverClientId: String?, + + /** + * Requests an ID token. Default to true. + */ + val requestIdToken: Boolean = true, + + /** + * Requests the user's profile information. Defaults to true. + */ + val requestProfile: Boolean = true, + + /** + * Requests the user's email address. Defaults to true. + */ + val requestEmail: Boolean = true + ) : AuthProvider() + + /** + * Facebook Login provider configuration. + */ + data class Facebook( + /** + * The list of scopes (permissions) to request. Defaults to email and public_profile. + */ + val scopes: List = listOf("email", "public_profile"), + + /** + * if true, enable limited login mode. Defaults to false. + */ + val limitedLogin: Boolean = false + ) : AuthProvider() + + /** + * Twitter/X authentication provider configuration. + */ + data class Twitter( + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map + ) : AuthProvider() + + /** + * Github authentication provider configuration. + */ + data class Github( + /** + * The list of scopes to request. Defaults to user:email. + */ + val scopes: List = listOf("user:email"), + + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map + ) : AuthProvider() + + /** + * Microsoft authentication provider configuration. + */ + data class Microsoft( + /** + * The list of scopes to request. Defaults to openid, profile, email. + */ + val scopes: List = listOf("openid", "profile", "email"), + + /** + * The tenant ID for Azure Active Directory. + */ + val tenant: String?, + + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map + ) : AuthProvider() + + /** + * Yahoo authentication provider configuration. + */ + data class Yahoo( + /** + * The list of scopes to request. Defaults to openid, profile, email. + */ + val scopes: List = listOf("openid", "profile", "email"), + + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map + ) : AuthProvider() + + /** + * Apple Sign-In provider configuration. + */ + data class Apple( + /** + * The list of scopes to request. Defaults to name and email. + */ + val scopes: List = listOf("name", "email"), + + /** + * The locale for the sign-in page. + */ + val locale: String?, + + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map + ) : AuthProvider() + + /** + * Anonymous authentication provider. It has no configurable properties. + */ + object Anonymous : AuthProvider() + + /** + * A generic OAuth provider for any unsupported provider. + */ + data class GenericOAuth( + /** + * The provider ID as configured in the Firebase console. + */ + val providerId: String, + + /** + * The list of scopes to request. + */ + val scopes: List, + + /** + * A map of custom OAuth parameters. + */ + val customParameters: Map, + + /** + * The text to display on the provider button. + */ + val buttonLabel: String, + + /** + * An optional icon for the provider button. + */ + val buttonIcon: ImageVector?, + + /** + * An optional background color for the provider button. + */ + val buttonColor: Color? + ) : AuthProvider() +} \ No newline at end of file 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 new file mode 100644 index 000000000..e1de6b0ad --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIConfiguration.kt @@ -0,0 +1,157 @@ +/* + * 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 + +import java.util.Locale +import com.google.firebase.auth.ActionCodeSettings +import androidx.compose.ui.graphics.vector.ImageVector + +fun actionCodeSettings( + block: ActionCodeSettings.Builder.() -> Unit +) = ActionCodeSettings.newBuilder().apply(block).build() + +fun authUIConfiguration(block: AuthUIConfigurationBuilder.() -> Unit): AuthUIConfiguration { + val builder = AuthUIConfigurationBuilder() + builder.block() + return builder.build() +} + +@DslMarker +annotation class AuthUIConfigurationDsl + +@AuthUIConfigurationDsl +class AuthUIConfigurationBuilder { + private val providers = mutableListOf() + var theme: AuthUITheme = AuthUITheme.Default + var stringProvider: AuthUIStringProvider? = null + var locale: Locale? = null + var enableCredentialManager: Boolean = true + var enableMfa: Boolean = true + var enableAnonymousUpgrade: Boolean = false + var tosUrl: String? = null + var privacyPolicyUrl: String? = null + var logo: ImageVector? = null + var actionCodeSettings: ActionCodeSettings? = null + var allowNewEmailAccounts: Boolean = true + var requireDisplayName: Boolean = true + var alwaysShowProviderChoice: Boolean = false + + fun providers(block: AuthProvidersBuilder.() -> Unit) { + val builder = AuthProvidersBuilder() + builder.block() + providers.addAll(builder.build()) + } + + internal fun build(): AuthUIConfiguration { + validate() + return AuthUIConfiguration( + providers = providers.toList(), + theme = theme, + stringProvider = stringProvider, + locale = locale, + enableCredentialManager = enableCredentialManager, + enableMfa = enableMfa, + enableAnonymousUpgrade = enableAnonymousUpgrade, + tosUrl = tosUrl, + privacyPolicyUrl = privacyPolicyUrl, + logo = logo, + actionCodeSettings = actionCodeSettings, + allowNewEmailAccounts = allowNewEmailAccounts, + requireDisplayName = requireDisplayName, + alwaysShowProviderChoice = alwaysShowProviderChoice + ) + } + + private fun validate() { + if (providers.isEmpty()) { + throw IllegalArgumentException("At least one provider must be configured") + } + } +} + +/** + * Configuration object for the authentication flow. + */ +data class AuthUIConfiguration( + /** + * The list of enabled authentication providers. + */ + val providers: List = emptyList(), + + /** + * The theming configuration for the UI. Default to [AuthUITheme.Default]. + */ + val theme: AuthUITheme = AuthUITheme.Default, + + /** + * A custom provider for localized strings. + */ + val stringProvider: AuthUIStringProvider? = null, + + /** + * The locale for internationalization. + */ + val locale: Locale? = null, + + /** + * Enables integration with Android's Credential Manager API. Defaults to true. + */ + val enableCredentialManager: Boolean = true, + + /** + * Enables Multi-Factor Authentication support. Defaults to true. + */ + val enableMfa: Boolean = true, + + /** + * Allows upgrading an anonymous user to a new credential. + */ + val enableAnonymousUpgrade: Boolean = false, + + /** + * The URL for the terms of service. + */ + val tosUrl: String? = null, + + /** + * The URL for the privacy policy. + */ + val privacyPolicyUrl: String? = null, + + /** + * The logo to display on the authentication screens. + */ + val logo: ImageVector? = null, + + /** + * Configuration for email link sign-in. + */ + val actionCodeSettings: ActionCodeSettings? = null, + + /** + * Allows new email accounts to be created. Defaults to true. + */ + val allowNewEmailAccounts: Boolean = true, + + /** + * Requires the user to provide a display name on sign-up. Defaults to true. + */ + val requireDisplayName: Boolean = true, + + /** + * Always shows the provider selection screen, even if only one is enabled. + */ + val alwaysShowProviderChoice: Boolean = false, +) diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIStringProvider.kt new file mode 100644 index 000000000..fe5bbf302 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUIStringProvider.kt @@ -0,0 +1,41 @@ +/* + * 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 + +import android.content.Context +import com.firebase.ui.auth.R + +/** + * An interface for providing localized string resources. This interface defines methods for all + * user-facing strings, such as initializing(), signInWithGoogle(), invalidEmail(), + * passwordsDoNotMatch(), etc., allowing for complete localization of the UI. + */ +interface AuthUIStringProvider { + fun initializing(): String + fun signInWithGoogle(): String + fun invalidEmail(): String + fun passwordsDoNotMatch(): String +} + +class DefaultAuthUIStringProvider(private val context: Context) : AuthUIStringProvider { + override fun initializing(): String = "" + + override fun signInWithGoogle(): String = + context.getString(R.string.fui_sign_in_with_google) + + override fun invalidEmail(): String = "" + + override fun passwordsDoNotMatch(): String = "" +} diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt new file mode 100644 index 000000000..0230f4ebc --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt @@ -0,0 +1,134 @@ +/* + * 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 + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +private val LocalAuthUITheme = staticCompositionLocalOf { AuthUITheme.Default } + +/** + * Theming configuration for the entire Auth UI. + */ +data class AuthUITheme( + /** + * The color scheme to use. + */ + val colorScheme: ColorScheme, + + /** + * The typography to use. + */ + val typography: Typography, + + /** + * The shapes to use for UI elements. + */ + val shapes: Shapes, + + /** + * A map of provider IDs to custom styling. + */ + val providerStyles: Map = emptyMap() +) { + + /** + * A data class nested within AuthUITheme that defines the visual appearance of a specific + * provider button, allowing for per-provider branding and customization. + */ + data class ProviderStyle( + /** + * The background color of the button. + */ + val backgroundColor: Color, + + /** + * The color of the text label on the button. + */ + val contentColor: Color, + + /** + * An optional tint color for the provider's icon. If null, + * the icon's intrinsic color is used. + */ + var iconTint: Color? = null, + + /** + * The shape of the button container. Defaults to RoundedCornerShape(4.dp). + */ + val shape: Shape = RoundedCornerShape(4.dp), + + /** + * The shadow elevation for the button. Defaults to 2.dp. + */ + val elevation: Dp = 2.dp + ) + + companion object { + /** + * A standard light theme with Material 3 defaults and + * pre-configured provider styles. + */ + val Default = AuthUITheme( + colorScheme = lightColorScheme(), + typography = Typography(), + shapes = Shapes(), + // TODO(demolaf): do we provide default styles for each provider? + providerStyles = mapOf( + "google.com" to ProviderStyle( + backgroundColor = Color.White, + contentColor = Color.Black + ) + ) + ) + + /** + * Creates a theme inheriting the app's current Material + * Theme settings. + */ + @Composable + fun fromMaterialTheme( + providerStyles: Map = Default.providerStyles + ): AuthUITheme { + return AuthUITheme( + colorScheme = MaterialTheme.colorScheme, + typography = MaterialTheme.typography, + shapes = MaterialTheme.shapes, + providerStyles = providerStyles + ) + } + } +} + +@Composable +fun AuthUITheme( + theme: AuthUITheme = AuthUITheme.Default, + content: @Composable () -> Unit +) { + CompositionLocalProvider(LocalAuthUITheme provides theme) { + content() + } +} 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 new file mode 100644 index 000000000..1fc36475d --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/PasswordRule.kt @@ -0,0 +1,52 @@ +/* + * 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 + +/** + * A sealed class representing a set of validation rules that can be applied to a password field, + * typically within the [AuthProvider.Email] configuration. + */ +sealed class PasswordRule { + /** + * Requires the password to have at least a certain number of characters. + */ + data class MinimumLength(val value: Int) : PasswordRule() + + /** + * Requires the password to contain at least one uppercase letter (A-Z). + */ + object RequireUppercase : PasswordRule() + + /** + * Requires the password to contain at least one lowercase letter (a-z). + */ + object RequireLowercase: PasswordRule() + + /** + * Requires the password to contain at least one numeric digit (0-9). + */ + object RequireDigit: PasswordRule() + + /** + * Requires the password to contain at least one special character (e.g., !@#$%^&*). + */ + object RequireSpecialCharacter: PasswordRule() + + /** + * Defines a custom validation rule using a regular expression and provides a specific error + * message on failure. + */ + data class Custom(val regex: Regex, val errorMessage: String) +} \ No newline at end of file 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 new file mode 100644 index 000000000..9bcf059a5 --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/compose/configuration/AuthUIConfigurationTest.kt @@ -0,0 +1,233 @@ +/* + * 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 + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import com.google.common.truth.Truth.assertThat +import com.google.firebase.auth.actionCodeSettings +import org.junit.Assert.assertThrows +import org.junit.Test +import java.util.Locale +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.memberProperties + +class AuthUIConfigurationTest { + + @Test + fun `authUIConfiguration with minimal setup uses correct defaults`() { + val config = authUIConfiguration { + providers { + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "" + ) + ) + } + } + + assertThat(config.providers).hasSize(1) + assertThat(config.theme).isEqualTo(AuthUITheme.Default) + assertThat(config.stringProvider).isNull() + assertThat(config.locale).isNull() + assertThat(config.enableCredentialManager).isTrue() + assertThat(config.enableMfa).isTrue() + assertThat(config.enableAnonymousUpgrade).isFalse() + assertThat(config.tosUrl).isNull() + assertThat(config.privacyPolicyUrl).isNull() + assertThat(config.logo).isNull() + assertThat(config.actionCodeSettings).isNull() + assertThat(config.allowNewEmailAccounts).isTrue() + assertThat(config.requireDisplayName).isTrue() + assertThat(config.alwaysShowProviderChoice).isFalse() + } + + @Test + fun `authUIConfiguration with all fields overridden uses custom values`() { + val customTheme = AuthUITheme.Default + val customStringProvider = object : AuthUIStringProvider { + override fun initializing(): String = "" + override fun signInWithGoogle(): String = "" + override fun invalidEmail(): String = "" + override fun passwordsDoNotMatch(): String = "" + } + val customLocale = Locale.US + val customActionCodeSettings = actionCodeSettings { + url = "https://example.com/verify" + handleCodeInApp = true + } + + val config = authUIConfiguration { + providers { + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "" + ) + ) + provider( + AuthProvider.Github( + customParameters = mapOf() + ) + ) + } + theme = customTheme + stringProvider = customStringProvider + locale = customLocale + enableCredentialManager = false + enableMfa = false + enableAnonymousUpgrade = true + tosUrl = "https://example.com/tos" + privacyPolicyUrl = "https://example.com/privacy" + logo = Icons.Default.AccountCircle + actionCodeSettings = customActionCodeSettings + allowNewEmailAccounts = false + requireDisplayName = false + alwaysShowProviderChoice = true + } + + assertThat(config.providers).hasSize(2) + assertThat(config.theme).isEqualTo(customTheme) + assertThat(config.stringProvider).isEqualTo(customStringProvider) + assertThat(config.locale).isEqualTo(customLocale) + assertThat(config.enableCredentialManager).isFalse() + assertThat(config.enableMfa).isFalse() + assertThat(config.enableAnonymousUpgrade).isTrue() + assertThat(config.tosUrl).isEqualTo("https://example.com/tos") + assertThat(config.privacyPolicyUrl).isEqualTo("https://example.com/privacy") + assertThat(config.logo).isEqualTo(Icons.Default.AccountCircle) + assertThat(config.actionCodeSettings).isEqualTo(customActionCodeSettings) + assertThat(config.allowNewEmailAccounts).isFalse() + assertThat(config.requireDisplayName).isFalse() + assertThat(config.alwaysShowProviderChoice).isTrue() + } + + // =========================================================================================== + // Validation Tests + // =========================================================================================== + + @Test(expected = IllegalArgumentException::class) + fun `authUIConfiguration throws when no providers configured`() { + authUIConfiguration { } + } + + @Test + fun `authUIConfiguration succeeds with single provider`() { + val config = authUIConfiguration { + providers { + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "" + ) + ) + } + } + + assertThat(config.providers).hasSize(1) + } + + // =========================================================================================== + // Provider Configuration Tests + // =========================================================================================== + + @Test + fun `providers block can be called multiple times and accumulates providers`() { + val config = authUIConfiguration { + providers { + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "" + ) + ) + } + + providers { + provider( + AuthProvider.Github( + customParameters = mapOf() + ) + ) + } + + enableCredentialManager = true + enableCredentialManager = false + } + + assertThat(config.providers).hasSize(2) + } + + // =========================================================================================== + // Builder Immutability Tests + // =========================================================================================== + + @Test + fun `authUIConfiguration providers list is immutable`() { + val config = authUIConfiguration { + providers { + provider( + AuthProvider.Google( + scopes = listOf(), + serverClientId = "" + ) + ) + } + } + + val originalSize = config.providers.size + + assertThrows(UnsupportedOperationException::class.java) { + (config.providers as MutableList).add( + AuthProvider.Twitter(customParameters = mapOf()) + ) + } + + assertThat(config.providers.size).isEqualTo(originalSize) + } + + @Test + fun `authUIConfiguration creates immutable configuration`() { + val kClass = AuthUIConfiguration::class + + val allProperties = kClass.memberProperties + + allProperties.forEach { + assertThat(it).isNotInstanceOf(KMutableProperty::class.java) + } + + val expectedProperties = setOf( + "providers", + "theme", + "stringProvider", + "locale", + "enableCredentialManager", + "enableMfa", + "enableAnonymousUpgrade", + "tosUrl", + "privacyPolicyUrl", + "logo", + "actionCodeSettings", + "allowNewEmailAccounts", + "requireDisplayName", + "alwaysShowProviderChoice" + ) + + val actualProperties = allProperties.map { it.name }.toSet() + + assertThat(actualProperties).containsExactlyElementsIn(expectedProperties) + } +} diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index d5e15edfe..1eed42446 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -5,8 +5,8 @@ object Config { private const val kotlinVersion = "2.1.0" object SdkVersions { - const val compile = 34 - const val target = 34 + const val compile = 35 + const val target = 35 const val min = 23 } @@ -40,8 +40,18 @@ object Config { const val paging = "androidx.paging:paging-runtime:3.0.0" const val pagingRxJava = "androidx.paging:paging-rxjava3:3.0.0" const val recyclerView = "androidx.recyclerview:recyclerview:1.2.1" - const val materialDesign = "com.google.android.material:material:1.4.0" + + object Compose { + const val bom = "androidx.compose:compose-bom:2025.08.00" + const val ui = "androidx.compose.ui:ui" + const val uiGraphics = "androidx.compose.ui:ui-graphics" + const val toolingPreview = "androidx.compose.ui:ui-tooling-preview" + const val tooling = "androidx.compose.ui:ui-tooling" + const val foundation = "androidx.compose.foundation:foundation" + const val material3 = "androidx.compose.material3:material3" + const val activityCompose = "androidx.activity:activity-compose" + } } object Firebase { @@ -83,6 +93,8 @@ object Config { const val archCoreTesting = "androidx.arch.core:core-testing:2.1.0" const val runner = "androidx.test:runner:1.5.0" const val rules = "androidx.test:rules:1.5.0" + + const val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" } object Lint { From 2c563bd3c1d16b2605c67bb4e7fe5635d249546a Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Tue, 16 Sep 2025 11:13:52 +0100 Subject: [PATCH 2/7] refactor: use OAuthProvider base class for common properties --- .../compose/configuration/AuthProvider.kt | 106 +++++++++++++----- 1 file changed, 78 insertions(+), 28 deletions(-) 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 36daa70a9..5bb8c72dd 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 @@ -32,7 +32,16 @@ class AuthProvidersBuilder { /** * Base sealed class for authentication providers. */ -sealed class AuthProvider() { +sealed class AuthProvider(open val providerId: String) { + /** + * Base class for OAuth authentication providers with common properties. + */ + abstract class OAuthProvider( + override val providerId: String, + open val scopes: List = emptyList(), + open val customParameters: Map = emptyMap() + ) : AuthProvider(providerId) + /** * Email/Password authentication provider configuration. */ @@ -66,7 +75,7 @@ sealed class AuthProvider() { * A list of custom password validation rules. */ val passwordValidationRules: List - ) : AuthProvider() + ) : AuthProvider(providerId = if (enableEmailLinkSignIn) "emailLink" else "password") /** * Phone number authentication provider configuration. @@ -101,7 +110,7 @@ sealed class AuthProvider() { * Enables automatic retrieval of the SMS code. Defaults to true. */ val enableAutoRetrieval: Boolean = true - ) : AuthProvider() + ) : AuthProvider(providerId = "phone") /** * Google Sign-In provider configuration. @@ -110,7 +119,7 @@ sealed class AuthProvider() { /** * The list of scopes to request. */ - val scopes: List, + override val scopes: List, /** * The OAuth 2.0 client ID for your server. @@ -130,8 +139,17 @@ sealed class AuthProvider() { /** * Requests the user's email address. Defaults to true. */ - val requestEmail: Boolean = true - ) : AuthProvider() + val requestEmail: Boolean = true, + + /** + * A map of custom OAuth parameters. + */ + override val customParameters: Map = emptyMap() + ) : OAuthProvider( + providerId = "google.com", + scopes = scopes, + customParameters = customParameters + ) /** * Facebook Login provider configuration. @@ -140,13 +158,22 @@ sealed class AuthProvider() { /** * The list of scopes (permissions) to request. Defaults to email and public_profile. */ - val scopes: List = listOf("email", "public_profile"), + override val scopes: List = listOf("email", "public_profile"), /** * if true, enable limited login mode. Defaults to false. */ - val limitedLogin: Boolean = false - ) : AuthProvider() + val limitedLogin: Boolean = false, + + /** + * A map of custom OAuth parameters. + */ + override val customParameters: Map = emptyMap() + ) : OAuthProvider( + providerId = "facebook.com", + scopes = scopes, + customParameters = customParameters + ) /** * Twitter/X authentication provider configuration. @@ -155,8 +182,11 @@ sealed class AuthProvider() { /** * A map of custom OAuth parameters. */ - val customParameters: Map - ) : AuthProvider() + override val customParameters: Map + ) : OAuthProvider( + providerId = "twitter.com", + customParameters = customParameters + ) /** * Github authentication provider configuration. @@ -165,13 +195,17 @@ sealed class AuthProvider() { /** * The list of scopes to request. Defaults to user:email. */ - val scopes: List = listOf("user:email"), + override val scopes: List = listOf("user:email"), /** * A map of custom OAuth parameters. */ - val customParameters: Map - ) : AuthProvider() + override val customParameters: Map + ) : OAuthProvider( + providerId = "github.com", + scopes = scopes, + customParameters = customParameters + ) /** * Microsoft authentication provider configuration. @@ -180,7 +214,7 @@ sealed class AuthProvider() { /** * The list of scopes to request. Defaults to openid, profile, email. */ - val scopes: List = listOf("openid", "profile", "email"), + override val scopes: List = listOf("openid", "profile", "email"), /** * The tenant ID for Azure Active Directory. @@ -190,8 +224,12 @@ sealed class AuthProvider() { /** * A map of custom OAuth parameters. */ - val customParameters: Map - ) : AuthProvider() + override val customParameters: Map + ) : OAuthProvider( + providerId = "microsoft.com", + scopes = scopes, + customParameters = customParameters + ) /** * Yahoo authentication provider configuration. @@ -200,13 +238,17 @@ sealed class AuthProvider() { /** * The list of scopes to request. Defaults to openid, profile, email. */ - val scopes: List = listOf("openid", "profile", "email"), + override val scopes: List = listOf("openid", "profile", "email"), /** * A map of custom OAuth parameters. */ - val customParameters: Map - ) : AuthProvider() + override val customParameters: Map + ) : OAuthProvider( + providerId = "yahoo.com", + scopes = scopes, + customParameters = customParameters + ) /** * Apple Sign-In provider configuration. @@ -215,7 +257,7 @@ sealed class AuthProvider() { /** * The list of scopes to request. Defaults to name and email. */ - val scopes: List = listOf("name", "email"), + override val scopes: List = listOf("name", "email"), /** * The locale for the sign-in page. @@ -225,13 +267,17 @@ sealed class AuthProvider() { /** * A map of custom OAuth parameters. */ - val customParameters: Map - ) : AuthProvider() + override val customParameters: Map + ) : OAuthProvider( + providerId = "apple.com", + scopes = scopes, + customParameters = customParameters + ) /** * Anonymous authentication provider. It has no configurable properties. */ - object Anonymous : AuthProvider() + object Anonymous : AuthProvider(providerId = "anonymous") /** * A generic OAuth provider for any unsupported provider. @@ -240,17 +286,17 @@ sealed class AuthProvider() { /** * The provider ID as configured in the Firebase console. */ - val providerId: String, + override val providerId: String, /** * The list of scopes to request. */ - val scopes: List, + override val scopes: List, /** * A map of custom OAuth parameters. */ - val customParameters: Map, + override val customParameters: Map, /** * The text to display on the provider button. @@ -266,5 +312,9 @@ sealed class AuthProvider() { * An optional background color for the provider button. */ val buttonColor: Color? - ) : AuthProvider() + ) : OAuthProvider( + providerId = providerId, + scopes = scopes, + customParameters = customParameters + ) } \ No newline at end of file From 1ba20aea8245d341ab4b079a9203b7f877571261 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Tue, 16 Sep 2025 12:54:10 +0100 Subject: [PATCH 3/7] feat: add Provider enum class for provider ids --- .../compose/configuration/AuthProvider.kt | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) 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 5bb8c72dd..509da0d03 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 @@ -17,6 +17,12 @@ package com.firebase.ui.auth.compose.configuration import android.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import com.google.firebase.auth.ActionCodeSettings +import com.google.firebase.auth.EmailAuthProvider +import com.google.firebase.auth.FacebookAuthProvider +import com.google.firebase.auth.GithubAuthProvider +import com.google.firebase.auth.GoogleAuthProvider +import com.google.firebase.auth.PhoneAuthProvider +import com.google.firebase.auth.TwitterAuthProvider @AuthUIConfigurationDsl class AuthProvidersBuilder { @@ -29,6 +35,22 @@ class AuthProvidersBuilder { internal fun build(): List = providers.toList() } +/** + * Enum class to represent all possible providers. + */ +internal enum class Provider(val id: String) { + GOOGLE(GoogleAuthProvider.PROVIDER_ID), + FACEBOOK(FacebookAuthProvider.PROVIDER_ID), + TWITTER(TwitterAuthProvider.PROVIDER_ID), + GITHUB(GithubAuthProvider.PROVIDER_ID), + EMAIL(EmailAuthProvider.PROVIDER_ID), + PHONE(PhoneAuthProvider.PROVIDER_ID), + ANONYMOUS("anonymous"), + MICROSOFT("microsoft.com"), + YAHOO("yahoo.com"), + APPLE("apple.com"), +} + /** * Base sealed class for authentication providers. */ @@ -75,7 +97,7 @@ sealed class AuthProvider(open val providerId: String) { * A list of custom password validation rules. */ val passwordValidationRules: List - ) : AuthProvider(providerId = if (enableEmailLinkSignIn) "emailLink" else "password") + ) : AuthProvider(providerId = Provider.EMAIL.id) /** * Phone number authentication provider configuration. @@ -110,7 +132,7 @@ sealed class AuthProvider(open val providerId: String) { * Enables automatic retrieval of the SMS code. Defaults to true. */ val enableAutoRetrieval: Boolean = true - ) : AuthProvider(providerId = "phone") + ) : AuthProvider(providerId = Provider.PHONE.id) /** * Google Sign-In provider configuration. @@ -146,7 +168,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map = emptyMap() ) : OAuthProvider( - providerId = "google.com", + providerId = Provider.GOOGLE.id, scopes = scopes, customParameters = customParameters ) @@ -170,7 +192,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map = emptyMap() ) : OAuthProvider( - providerId = "facebook.com", + providerId = Provider.FACEBOOK.id, scopes = scopes, customParameters = customParameters ) @@ -184,7 +206,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map ) : OAuthProvider( - providerId = "twitter.com", + providerId = Provider.TWITTER.id, customParameters = customParameters ) @@ -202,7 +224,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map ) : OAuthProvider( - providerId = "github.com", + providerId = Provider.GITHUB.id, scopes = scopes, customParameters = customParameters ) @@ -226,7 +248,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map ) : OAuthProvider( - providerId = "microsoft.com", + providerId = Provider.MICROSOFT.id, scopes = scopes, customParameters = customParameters ) @@ -245,7 +267,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map ) : OAuthProvider( - providerId = "yahoo.com", + providerId = Provider.YAHOO.id, scopes = scopes, customParameters = customParameters ) @@ -269,7 +291,7 @@ sealed class AuthProvider(open val providerId: String) { */ override val customParameters: Map ) : OAuthProvider( - providerId = "apple.com", + providerId = Provider.APPLE.id, scopes = scopes, customParameters = customParameters ) @@ -277,7 +299,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Anonymous authentication provider. It has no configurable properties. */ - object Anonymous : AuthProvider(providerId = "anonymous") + object Anonymous : AuthProvider(providerId = Provider.ANONYMOUS.id) /** * A generic OAuth provider for any unsupported provider. From 63c8e1897635d87fd80d458161f089246ce9b3f5 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Tue, 16 Sep 2025 12:55:22 +0100 Subject: [PATCH 4/7] feat: setup default provider styles for each provider --- .../auth/compose/configuration/AuthUITheme.kt | 85 +++++++++++++++++-- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt index 0230f4ebc..dab0b6660 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt @@ -96,13 +96,7 @@ data class AuthUITheme( colorScheme = lightColorScheme(), typography = Typography(), shapes = Shapes(), - // TODO(demolaf): do we provide default styles for each provider? - providerStyles = mapOf( - "google.com" to ProviderStyle( - backgroundColor = Color.White, - contentColor = Color.Black - ) - ) + providerStyles = defaultProviderStyles ) /** @@ -120,6 +114,83 @@ data class AuthUITheme( providerStyles = providerStyles ) } + + internal val defaultProviderStyles + get(): Map { + return Provider.entries.associate { provider -> + when (provider) { + Provider.GOOGLE -> { + provider.id to ProviderStyle( + backgroundColor = Color.White, + contentColor = Color(0xFF757575) + ) + } + + Provider.FACEBOOK -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF3B5998), + contentColor = Color.White + ) + } + + Provider.TWITTER -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF5BAAF4), + contentColor = Color.White + ) + } + + Provider.GITHUB -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF24292E), + contentColor = Color.White + ) + } + + Provider.EMAIL -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFFD0021B), + contentColor = Color.White + ) + } + + Provider.PHONE -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF43C5A5), + contentColor = Color.White + ) + } + + Provider.ANONYMOUS -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFFF4B400), + contentColor = Color.White + ) + } + + Provider.MICROSOFT -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF2F2F2F), + contentColor = Color.White + ) + } + + Provider.YAHOO -> { + provider.id to ProviderStyle( + backgroundColor = Color(0xFF720E9E), + contentColor = Color.White + ) + } + + Provider.APPLE -> { + provider.id to ProviderStyle( + backgroundColor = Color.Black, + contentColor = Color.White + ) + } + } + } + } } } From a5a944ad2f5a4b1d4eb8fc09f68ec01ee2a809c6 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Tue, 16 Sep 2025 21:01:35 +0100 Subject: [PATCH 5/7] test: added builder validation logic from old auth library and tests --- auth/build.gradle.kts | 2 +- .../compose/configuration/AuthProvider.kt | 36 +++++--- .../configuration/AuthUIConfiguration.kt | 35 ++++++++ .../configuration/AuthUIConfigurationTest.kt | 85 +++++++++++++++++-- build.gradle.kts | 1 + buildSrc/src/main/kotlin/Config.kt | 2 +- gradle/libs.versions.toml | 7 ++ 7 files changed, 148 insertions(+), 20 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index c027e0848..8e6d5304e 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("com.android.library") id("com.vanniktech.maven.publish") id("org.jetbrains.kotlin.android") - id("org.jetbrains.kotlin.plugin.compose") version "2.0.0" + alias(libs.plugins.compose.compiler) } android { 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 509da0d03..f79392926 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 @@ -16,6 +16,7 @@ package com.firebase.ui.auth.compose.configuration import android.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import com.firebase.ui.auth.util.data.ProviderAvailability import com.google.firebase.auth.ActionCodeSettings import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FacebookAuthProvider @@ -51,19 +52,19 @@ internal enum class Provider(val id: String) { APPLE("apple.com"), } +/** + * Base class for OAuth authentication providers with common properties. + */ +abstract class OAuthProvider( + override val providerId: String, + open val scopes: List = emptyList(), + open val customParameters: Map = emptyMap() +) : AuthProvider(providerId) + /** * Base sealed class for authentication providers. */ sealed class AuthProvider(open val providerId: String) { - /** - * Base class for OAuth authentication providers with common properties. - */ - abstract class OAuthProvider( - override val providerId: String, - open val scopes: List = emptyList(), - open val customParameters: Map = emptyMap() - ) : AuthProvider(providerId) - /** * Email/Password authentication provider configuration. */ @@ -97,7 +98,22 @@ sealed class AuthProvider(open val providerId: String) { * A list of custom password validation rules. */ val passwordValidationRules: List - ) : AuthProvider(providerId = Provider.EMAIL.id) + ) : AuthProvider(providerId = Provider.EMAIL.id) { + fun validate() { + if (enableEmailLinkSignIn) { + val actionCodeSettings = actionCodeSettings + ?: requireNotNull(actionCodeSettings) { + "ActionCodeSettings cannot be null when using " + + "email link sign in." + } + + check(actionCodeSettings.canHandleCodeInApp()) { + "You must set canHandleCodeInApp in your " + + "ActionCodeSettings to true for Email-Link Sign-in." + } + } + } + } /** * Phone number authentication provider configuration. 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 e1de6b0ad..2133b3506 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 @@ -75,9 +75,44 @@ class AuthUIConfigurationBuilder { } private fun validate() { + // At least one provider if (providers.isEmpty()) { throw IllegalArgumentException("At least one provider must be configured") } + + // No unsupported providers + val supportedProviderIds = Provider.entries.map { it.id }.toSet() + val unknownProviders = providers.filter { it.providerId !in supportedProviderIds } + require(unknownProviders.isEmpty()) { + "Unknown providers: ${unknownProviders.joinToString { it.providerId }}" + } + + // Cannot have only anonymous provider + if (providers.size == 1 && providers.first() is AuthProvider.Anonymous) { + throw IllegalStateException( + "Sign in as guest cannot be the only sign in method. " + + "In this case, sign the user in anonymously your self; no UI is needed." + ) + } + + // Check for duplicate providers + val providerIds = providers.map { it.providerId } + val duplicates = providerIds.groupingBy { it }.eachCount().filter { it.value > 1 } + + require(duplicates.isEmpty()) { + val message = duplicates.keys.joinToString(", ") + throw IllegalArgumentException( + "Each provider can only be set once. Duplicates: $message" + ) + } + + // Provider specific validations + providers.forEach { provider -> + when (provider) { + is AuthProvider.Email -> provider.validate() + else -> null + } + } } } 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 9bcf059a5..13a7935a2 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 @@ -125,19 +125,88 @@ class AuthUIConfigurationTest { } @Test - fun `authUIConfiguration succeeds with single provider`() { + fun `validation accepts all supported providers`() { val config = authUIConfiguration { providers { - provider( - AuthProvider.Google( - scopes = listOf(), - serverClientId = "" - ) - ) + provider(AuthProvider.Google(scopes = listOf(), serverClientId = "")) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Twitter(customParameters = mapOf())) + provider(AuthProvider.Github(customParameters = mapOf())) + provider(AuthProvider.Microsoft(customParameters = mapOf(), tenant = null)) + provider(AuthProvider.Yahoo(customParameters = mapOf())) + provider(AuthProvider.Apple(customParameters = mapOf(), locale = null)) + provider(AuthProvider.Phone(defaultCountryCode = null, allowedCountries = null)) + provider(AuthProvider.Email(actionCodeSettings = null, passwordValidationRules = listOf())) } } + assertThat(config.providers).hasSize(9) + } - assertThat(config.providers).hasSize(1) + @Test(expected = IllegalArgumentException::class) + fun `validation throws for unsupported provider`() { + val mockProvider = AuthProvider.GenericOAuth( + providerId = "unsupported.provider", + scopes = listOf(), + customParameters = mapOf(), + buttonLabel = "Test", + buttonIcon = null, + buttonColor = null + ) + + authUIConfiguration { + providers { + provider(mockProvider) + } + } + } + + @Test(expected = IllegalStateException::class) + fun `validate throws when only anonymous provider is configured`() { + authUIConfiguration { + providers { + provider(AuthProvider.Anonymous) + } + } + } + + @Test(expected = IllegalArgumentException::class) + fun `validate throws for duplicate providers`() { + authUIConfiguration { + providers { + provider(AuthProvider.Google(scopes = listOf(), serverClientId = "")) + provider(AuthProvider.Google(scopes = listOf("email"), serverClientId = "different")) + } + } + } + + @Test(expected = IllegalArgumentException::class) + fun `validate throws for enableEmailLinkSignIn true when actionCodeSettings is null`() { + authUIConfiguration { + providers { + provider(AuthProvider.Email( + enableEmailLinkSignIn = true, + actionCodeSettings = null, + passwordValidationRules = listOf() + )) + } + } + } + + @Test(expected = IllegalStateException::class) + fun `validate throws for enableEmailLinkSignIn true when actionCodeSettings canHandleCodeInApp false`() { + val customActionCodeSettings = actionCodeSettings { + url = "https://example.com" + handleCodeInApp = false + } + authUIConfiguration { + providers { + provider(AuthProvider.Email( + enableEmailLinkSignIn = true, + actionCodeSettings = customActionCodeSettings, + passwordValidationRules = listOf() + )) + } + } } // =========================================================================================== diff --git a/build.gradle.kts b/build.gradle.kts index c87edeee9..105624a8b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ buildscript { plugins { id("com.github.ben-manes.versions") version "0.20.0" + alias(libs.plugins.compose.compiler) apply false } allprojects { diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 1eed42446..7e9352e85 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -2,7 +2,7 @@ object Config { const val version = "10.0.0-SNAPSHOT" val submodules = listOf("auth", "common", "firestore", "database", "storage") - private const val kotlinVersion = "2.1.0" + private const val kotlinVersion = "2.2.0" object SdkVersions { const val compile = 35 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..379fe1b5b --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +kotlin = "2.2.0" + +[libraries] + +[plugins] +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file From 19787dfee18a8417c5a55d3efd8807d1472ac741 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Thu, 18 Sep 2025 09:58:15 +0100 Subject: [PATCH 6/7] refactor: changes in API design docs replaced sealed with abstract class, data with regular class use isXX prefix for booleans --- .../compose/configuration/AuthProvider.kt | 39 ++++++++-------- .../configuration/AuthUIConfiguration.kt | 38 ++++++++-------- .../auth/compose/configuration/AuthUITheme.kt | 6 +-- .../compose/configuration/PasswordRule.kt | 8 ++-- .../configuration/AuthUIConfigurationTest.kt | 44 +++++++++---------- 5 files changed, 66 insertions(+), 69 deletions(-) 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 f79392926..ef8bb0771 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 @@ -16,7 +16,6 @@ package com.firebase.ui.auth.compose.configuration import android.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import com.firebase.ui.auth.util.data.ProviderAvailability import com.google.firebase.auth.ActionCodeSettings import com.google.firebase.auth.EmailAuthProvider import com.google.firebase.auth.FacebookAuthProvider @@ -53,7 +52,7 @@ internal enum class Provider(val id: String) { } /** - * Base class for OAuth authentication providers with common properties. + * Base abstract class for OAuth authentication providers with common properties. */ abstract class OAuthProvider( override val providerId: String, @@ -62,22 +61,22 @@ abstract class OAuthProvider( ) : AuthProvider(providerId) /** - * Base sealed class for authentication providers. + * Base abstract class for authentication providers. */ -sealed class AuthProvider(open val providerId: String) { +abstract class AuthProvider(open val providerId: String) { /** * Email/Password authentication provider configuration. */ - data class Email( + class Email( /** * Requires the user to provide a display name. Defaults to true. */ - val requireDisplayName: Boolean = true, + val isDisplayNameRequired: Boolean = true, /** * Enables email link sign-in, Defaults to false. */ - val enableEmailLinkSignIn: Boolean = false, + val isEmailLinkSignInEnabled: Boolean = false, /** * Settings for email link actions. @@ -87,7 +86,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Allows new accounts to be created. Defaults to true. */ - val allowNewAccounts: Boolean = true, + val isNewAccountsAllowed: Boolean = true, /** * The minimum length for a password. Defaults to 6. @@ -100,7 +99,7 @@ sealed class AuthProvider(open val providerId: String) { val passwordValidationRules: List ) : AuthProvider(providerId = Provider.EMAIL.id) { fun validate() { - if (enableEmailLinkSignIn) { + if (isEmailLinkSignInEnabled) { val actionCodeSettings = actionCodeSettings ?: requireNotNull(actionCodeSettings) { "ActionCodeSettings cannot be null when using " + @@ -118,7 +117,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Phone number authentication provider configuration. */ - data class Phone( + class Phone( /** * The default country code to pre-select. */ @@ -142,18 +141,18 @@ sealed class AuthProvider(open val providerId: String) { /** * Enables instant verification of the phone number. Defaults to true. */ - val enableInstantVerification: Boolean = true, + val isInstantVerificationEnabled: Boolean = true, /** * Enables automatic retrieval of the SMS code. Defaults to true. */ - val enableAutoRetrieval: Boolean = true + val isAutoRetrievalEnabled: Boolean = true ) : AuthProvider(providerId = Provider.PHONE.id) /** * Google Sign-In provider configuration. */ - data class Google( + class Google( /** * The list of scopes to request. */ @@ -192,7 +191,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Facebook Login provider configuration. */ - data class Facebook( + class Facebook( /** * The list of scopes (permissions) to request. Defaults to email and public_profile. */ @@ -216,7 +215,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Twitter/X authentication provider configuration. */ - data class Twitter( + class Twitter( /** * A map of custom OAuth parameters. */ @@ -229,7 +228,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Github authentication provider configuration. */ - data class Github( + class Github( /** * The list of scopes to request. Defaults to user:email. */ @@ -248,7 +247,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Microsoft authentication provider configuration. */ - data class Microsoft( + class Microsoft( /** * The list of scopes to request. Defaults to openid, profile, email. */ @@ -272,7 +271,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Yahoo authentication provider configuration. */ - data class Yahoo( + class Yahoo( /** * The list of scopes to request. Defaults to openid, profile, email. */ @@ -291,7 +290,7 @@ sealed class AuthProvider(open val providerId: String) { /** * Apple Sign-In provider configuration. */ - data class Apple( + class Apple( /** * The list of scopes to request. Defaults to name and email. */ @@ -320,7 +319,7 @@ sealed class AuthProvider(open val providerId: String) { /** * A generic OAuth provider for any unsupported provider. */ - data class GenericOAuth( + class GenericOAuth( /** * The provider ID as configured in the Firebase console. */ 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 2133b3506..aca1ccf9e 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 @@ -37,16 +37,16 @@ class AuthUIConfigurationBuilder { var theme: AuthUITheme = AuthUITheme.Default var stringProvider: AuthUIStringProvider? = null var locale: Locale? = null - var enableCredentialManager: Boolean = true - var enableMfa: Boolean = true - var enableAnonymousUpgrade: Boolean = false + var isCredentialManagerEnabled: Boolean = true + var isMfaEnabled: Boolean = true + var isAnonymousUpgradeEnabled: Boolean = false var tosUrl: String? = null var privacyPolicyUrl: String? = null var logo: ImageVector? = null var actionCodeSettings: ActionCodeSettings? = null - var allowNewEmailAccounts: Boolean = true - var requireDisplayName: Boolean = true - var alwaysShowProviderChoice: Boolean = false + var isNewEmailAccountsAllowed: Boolean = true + var isDisplayNameRequired: Boolean = true + var isProviderChoiceAlwaysShown: Boolean = false fun providers(block: AuthProvidersBuilder.() -> Unit) { val builder = AuthProvidersBuilder() @@ -61,16 +61,16 @@ class AuthUIConfigurationBuilder { theme = theme, stringProvider = stringProvider, locale = locale, - enableCredentialManager = enableCredentialManager, - enableMfa = enableMfa, - enableAnonymousUpgrade = enableAnonymousUpgrade, + isCredentialManagerEnabled = isCredentialManagerEnabled, + isMfaEnabled = isMfaEnabled, + isAnonymousUpgradeEnabled = isAnonymousUpgradeEnabled, tosUrl = tosUrl, privacyPolicyUrl = privacyPolicyUrl, logo = logo, actionCodeSettings = actionCodeSettings, - allowNewEmailAccounts = allowNewEmailAccounts, - requireDisplayName = requireDisplayName, - alwaysShowProviderChoice = alwaysShowProviderChoice + isNewEmailAccountsAllowed = isNewEmailAccountsAllowed, + isDisplayNameRequired = isDisplayNameRequired, + isProviderChoiceAlwaysShown = isProviderChoiceAlwaysShown ) } @@ -119,7 +119,7 @@ class AuthUIConfigurationBuilder { /** * Configuration object for the authentication flow. */ -data class AuthUIConfiguration( +class AuthUIConfiguration( /** * The list of enabled authentication providers. */ @@ -143,17 +143,17 @@ data class AuthUIConfiguration( /** * Enables integration with Android's Credential Manager API. Defaults to true. */ - val enableCredentialManager: Boolean = true, + val isCredentialManagerEnabled: Boolean = true, /** * Enables Multi-Factor Authentication support. Defaults to true. */ - val enableMfa: Boolean = true, + val isMfaEnabled: Boolean = true, /** * Allows upgrading an anonymous user to a new credential. */ - val enableAnonymousUpgrade: Boolean = false, + val isAnonymousUpgradeEnabled: Boolean = false, /** * The URL for the terms of service. @@ -178,15 +178,15 @@ data class AuthUIConfiguration( /** * Allows new email accounts to be created. Defaults to true. */ - val allowNewEmailAccounts: Boolean = true, + val isNewEmailAccountsAllowed: Boolean = true, /** * Requires the user to provide a display name on sign-up. Defaults to true. */ - val requireDisplayName: Boolean = true, + val isDisplayNameRequired: Boolean = true, /** * Always shows the provider selection screen, even if only one is enabled. */ - val alwaysShowProviderChoice: Boolean = false, + val isProviderChoiceAlwaysShown: Boolean = false, ) diff --git a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt index dab0b6660..d2ae7032d 100644 --- a/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt +++ b/auth/src/main/java/com/firebase/ui/auth/compose/configuration/AuthUITheme.kt @@ -33,7 +33,7 @@ private val LocalAuthUITheme = staticCompositionLocalOf { AuthUITheme.Default } /** * Theming configuration for the entire Auth UI. */ -data class AuthUITheme( +class AuthUITheme( /** * The color scheme to use. */ @@ -56,10 +56,10 @@ data class AuthUITheme( ) { /** - * A data class nested within AuthUITheme that defines the visual appearance of a specific + * A class nested within AuthUITheme that defines the visual appearance of a specific * provider button, allowing for per-provider branding and customization. */ - data class ProviderStyle( + class ProviderStyle( /** * The background color of the button. */ 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 1fc36475d..242ea6e83 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 @@ -15,14 +15,14 @@ package com.firebase.ui.auth.compose.configuration /** - * A sealed class representing a set of validation rules that can be applied to a password field, + * An abstract class representing a set of validation rules that can be applied to a password field, * typically within the [AuthProvider.Email] configuration. */ -sealed class PasswordRule { +abstract class PasswordRule { /** * Requires the password to have at least a certain number of characters. */ - data class MinimumLength(val value: Int) : PasswordRule() + class MinimumLength(val value: Int) : PasswordRule() /** * Requires the password to contain at least one uppercase letter (A-Z). @@ -48,5 +48,5 @@ sealed class PasswordRule { * Defines a custom validation rule using a regular expression and provides a specific error * message on failure. */ - data class Custom(val regex: Regex, val errorMessage: String) + class Custom(val regex: Regex, val errorMessage: String) } \ No newline at end of file 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 13a7935a2..53f2a87e5 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 @@ -43,16 +43,16 @@ class AuthUIConfigurationTest { assertThat(config.theme).isEqualTo(AuthUITheme.Default) assertThat(config.stringProvider).isNull() assertThat(config.locale).isNull() - assertThat(config.enableCredentialManager).isTrue() - assertThat(config.enableMfa).isTrue() - assertThat(config.enableAnonymousUpgrade).isFalse() + assertThat(config.isCredentialManagerEnabled).isTrue() + assertThat(config.isMfaEnabled).isTrue() + assertThat(config.isAnonymousUpgradeEnabled).isFalse() assertThat(config.tosUrl).isNull() assertThat(config.privacyPolicyUrl).isNull() assertThat(config.logo).isNull() assertThat(config.actionCodeSettings).isNull() - assertThat(config.allowNewEmailAccounts).isTrue() - assertThat(config.requireDisplayName).isTrue() - assertThat(config.alwaysShowProviderChoice).isFalse() + assertThat(config.isNewEmailAccountsAllowed).isTrue() + assertThat(config.isDisplayNameRequired).isTrue() + assertThat(config.isProviderChoiceAlwaysShown).isFalse() } @Test @@ -87,32 +87,32 @@ class AuthUIConfigurationTest { theme = customTheme stringProvider = customStringProvider locale = customLocale - enableCredentialManager = false - enableMfa = false - enableAnonymousUpgrade = true + isCredentialManagerEnabled = false + isMfaEnabled = false + isAnonymousUpgradeEnabled = true tosUrl = "https://example.com/tos" privacyPolicyUrl = "https://example.com/privacy" logo = Icons.Default.AccountCircle actionCodeSettings = customActionCodeSettings - allowNewEmailAccounts = false - requireDisplayName = false - alwaysShowProviderChoice = true + isNewEmailAccountsAllowed = false + isDisplayNameRequired = false + isProviderChoiceAlwaysShown = true } assertThat(config.providers).hasSize(2) assertThat(config.theme).isEqualTo(customTheme) assertThat(config.stringProvider).isEqualTo(customStringProvider) assertThat(config.locale).isEqualTo(customLocale) - assertThat(config.enableCredentialManager).isFalse() - assertThat(config.enableMfa).isFalse() - assertThat(config.enableAnonymousUpgrade).isTrue() + assertThat(config.isCredentialManagerEnabled).isFalse() + assertThat(config.isMfaEnabled).isFalse() + assertThat(config.isAnonymousUpgradeEnabled).isTrue() assertThat(config.tosUrl).isEqualTo("https://example.com/tos") assertThat(config.privacyPolicyUrl).isEqualTo("https://example.com/privacy") assertThat(config.logo).isEqualTo(Icons.Default.AccountCircle) assertThat(config.actionCodeSettings).isEqualTo(customActionCodeSettings) - assertThat(config.allowNewEmailAccounts).isFalse() - assertThat(config.requireDisplayName).isFalse() - assertThat(config.alwaysShowProviderChoice).isTrue() + assertThat(config.isNewEmailAccountsAllowed).isFalse() + assertThat(config.isDisplayNameRequired).isFalse() + assertThat(config.isProviderChoiceAlwaysShown).isTrue() } // =========================================================================================== @@ -184,7 +184,7 @@ class AuthUIConfigurationTest { authUIConfiguration { providers { provider(AuthProvider.Email( - enableEmailLinkSignIn = true, + isEmailLinkSignInEnabled = true, actionCodeSettings = null, passwordValidationRules = listOf() )) @@ -201,7 +201,7 @@ class AuthUIConfigurationTest { authUIConfiguration { providers { provider(AuthProvider.Email( - enableEmailLinkSignIn = true, + isEmailLinkSignInEnabled = true, actionCodeSettings = customActionCodeSettings, passwordValidationRules = listOf() )) @@ -232,9 +232,7 @@ class AuthUIConfigurationTest { ) ) } - - enableCredentialManager = true - enableCredentialManager = false + isCredentialManagerEnabled = true } assertThat(config.providers).hasSize(2) From 7e010e42294110e567d622ec8cbc5191116bc5e1 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo Date: Thu, 18 Sep 2025 10:02:00 +0100 Subject: [PATCH 7/7] test: fix AuthUIConfiguration constructor test --- .../compose/configuration/AuthUIConfigurationTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 53f2a87e5..c8e627ff5 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 @@ -281,16 +281,16 @@ class AuthUIConfigurationTest { "theme", "stringProvider", "locale", - "enableCredentialManager", - "enableMfa", - "enableAnonymousUpgrade", + "isCredentialManagerEnabled", + "isMfaEnabled", + "isAnonymousUpgradeEnabled", "tosUrl", "privacyPolicyUrl", "logo", "actionCodeSettings", - "allowNewEmailAccounts", - "requireDisplayName", - "alwaysShowProviderChoice" + "isNewEmailAccountsAllowed", + "isDisplayNameRequired", + "isProviderChoiceAlwaysShown" ) val actualProperties = allProperties.map { it.name }.toSet()