diff --git a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt index 79ed7068a..00b0054f0 100644 --- a/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/CustomSlotsThemingDemoActivity.kt @@ -25,6 +25,9 @@ import com.firebase.ui.auth.configuration.authUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.ui.components.AuthProviderButton import com.firebase.ui.auth.ui.screens.email.EmailAuthContentState import com.firebase.ui.auth.ui.screens.email.EmailAuthMode import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen @@ -37,6 +40,7 @@ import com.google.firebase.auth.AuthResult * Demo activity showcasing custom slots and theming capabilities: * - EmailAuthScreen with custom slot UI * - PhoneAuthScreen with custom slot UI + * - Provider button shape customization with global and per-provider overrides * - AuthUITheme.fromMaterialTheme() with custom ProviderStyle overrides */ class CustomSlotsThemingDemoActivity : ComponentActivity() { @@ -121,6 +125,7 @@ class CustomSlotsThemingDemoActivity : ComponentActivity() { configuration = phoneConfiguration, context = appContext ) + DemoType.ShapeCustomization -> ShapeCustomizationDemo() } } } @@ -131,43 +136,20 @@ class CustomSlotsThemingDemoActivity : ComponentActivity() { enum class DemoType { Email, - Phone + Phone, + ShapeCustomization } @Composable fun CustomAuthUITheme(content: @Composable () -> Unit) { // Use Material Theme colors MaterialTheme { - val customProviderStyles = mapOf( - "google.com" to AuthUITheme.ProviderStyle( - icon = null, // Would use actual Google icon in production - backgroundColor = Color(0xFFFFFFFF), - contentColor = Color(0xFF757575), - iconTint = null, - shape = RoundedCornerShape(8.dp), - elevation = 1.dp - ), - "facebook.com" to AuthUITheme.ProviderStyle( - icon = null, // Would use actual Facebook icon in production - backgroundColor = Color(0xFF1877F2), - contentColor = Color.White, - iconTint = null, - shape = RoundedCornerShape(8.dp), - elevation = 2.dp - ), - "password" to AuthUITheme.ProviderStyle( - icon = null, - backgroundColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - iconTint = null, - shape = RoundedCornerShape(12.dp), - elevation = 3.dp - ) + // UPDATED: Now uses ProviderStyleDefaults and the new providerButtonShape API + // Apply custom theme using fromMaterialTheme with global button shape + val authTheme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp) // Global shape for all buttons ) - // Apply custom theme using fromMaterialTheme - val authTheme = AuthUITheme.fromMaterialTheme(providerStyles = customProviderStyles) - AuthUITheme(theme = authTheme) { content() } @@ -202,21 +184,32 @@ fun DemoSelector( color = MaterialTheme.colorScheme.onPrimaryContainer ) - Row( + Column( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + selected = selectedDemo == DemoType.Email, + onClick = { onDemoSelected(DemoType.Email) }, + label = { Text("Email Auth") }, + modifier = Modifier.weight(1f) + ) + FilterChip( + selected = selectedDemo == DemoType.Phone, + onClick = { onDemoSelected(DemoType.Phone) }, + label = { Text("Phone Auth") }, + modifier = Modifier.weight(1f) + ) + } FilterChip( - selected = selectedDemo == DemoType.Email, - onClick = { onDemoSelected(DemoType.Email) }, - label = { Text("Email Auth") }, - modifier = Modifier.weight(1f) - ) - FilterChip( - selected = selectedDemo == DemoType.Phone, - onClick = { onDemoSelected(DemoType.Phone) }, - label = { Text("Phone Auth") }, - modifier = Modifier.weight(1f) + selected = selectedDemo == DemoType.ShapeCustomization, + onClick = { onDemoSelected(DemoType.ShapeCustomization) }, + label = { Text("Shape Customization") }, + modifier = Modifier.fillMaxWidth() ) } } @@ -823,3 +816,290 @@ fun EnterVerificationCodeUI(state: PhoneAuthContentState) { } } } + +/** + * Demo showcasing provider button shape customization capabilities. + * Demonstrates: + * - Global shape configuration for all buttons + * - Per-provider shape overrides + * - Using ProviderStyleDefaults with .copy() + */ +@Composable +fun ShapeCustomizationDemo() { + val context = androidx.compose.ui.platform.LocalContext.current + val stringProvider = DefaultAuthUIStringProvider(context) + var selectedPreset by remember { mutableStateOf(ShapePreset.DEFAULT) } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Title and description + Text( + text = "Provider Button Shape Customization", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.primary + ) + + Text( + text = "This demo showcases the new shape customization API for provider buttons. " + + "You can set a global shape for all buttons or customize individual providers.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider() + + // Preset selector + Text( + text = "Select Shape Preset:", + style = MaterialTheme.typography.titleMedium + ) + + ShapePreset.entries.forEach { preset -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedPreset == preset, + onClick = { selectedPreset = preset } + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = preset.displayName, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = preset.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + HorizontalDivider() + + // Preview section + Text( + text = "Preview:", + style = MaterialTheme.typography.titleMedium + ) + + // Render buttons with the selected preset + when (selectedPreset) { + ShapePreset.DEFAULT -> DefaultShapeButtons(stringProvider) + ShapePreset.DEFAULT_COPY -> DefaultCopyShapeButtons(stringProvider) + ShapePreset.DARK_COPY -> DarkCopyShapeButtons(stringProvider) + ShapePreset.FROM_MATERIAL -> FromMaterialThemeButtons(stringProvider) + ShapePreset.PILL -> PillShapeButtons(stringProvider) + ShapePreset.MIXED -> MixedShapeButtons(stringProvider) + } + + // Code example + HorizontalDivider() + + Text( + text = "Code Example:", + style = MaterialTheme.typography.titleMedium + ) + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = selectedPreset.codeExample, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ), + modifier = Modifier.padding(12.dp) + ) + } + } +} + +enum class ShapePreset( + val displayName: String, + val description: String, + val codeExample: String +) { + DEFAULT( + "Default Shapes", + "Uses the standard 4dp rounded corners", + """ +// No customization needed +val theme = AuthUITheme.Default + """.trimIndent() + ), + DEFAULT_COPY( + "Default.copy()", + "Customize default light theme with .copy()", + """ +val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp) +) + """.trimIndent() + ), + DARK_COPY( + "DefaultDark.copy()", + "Customize default dark theme with .copy()", + """ +val theme = AuthUITheme.DefaultDark.copy( + providerButtonShape = RoundedCornerShape(16.dp) +) + """.trimIndent() + ), + FROM_MATERIAL( + "fromMaterialTheme()", + "Inherit from Material Theme", + """ +val theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp) +) + """.trimIndent() + ), + PILL( + "Pill Shape", + "Creates pill-shaped buttons (Default.copy)", + """ +val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(28.dp) +) + """.trimIndent() + ), + MIXED( + "Mixed Shapes", + "Different shapes per provider (Default.copy)", + """ +val customStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(24.dp) + ), + "facebook.com" to ProviderStyleDefaults.Facebook.copy( + shape = RoundedCornerShape(8.dp) + ) +) + +val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customStyles +) + """.trimIndent() + ) +} + +@Composable +fun DefaultShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Default theme - no customization + AuthUITheme { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun DefaultCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Using AuthUITheme.Default.copy() to customize the light theme + val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp) + ) + AuthUITheme(theme = theme) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun DarkCopyShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Using AuthUITheme.DefaultDark.copy() to customize the dark theme + val theme = AuthUITheme.DefaultDark.copy( + providerButtonShape = RoundedCornerShape(16.dp) + ) + AuthUITheme(theme = theme) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun FromMaterialThemeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Using AuthUITheme.fromMaterialTheme() to inherit from Material Theme + val theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp) + ) + AuthUITheme(theme = theme) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun PillShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Pill-shaped buttons using Default.copy() + val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(28.dp) + ) + AuthUITheme(theme = theme) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun MixedShapeButtons(stringProvider: DefaultAuthUIStringProvider) { + // Mixed shapes per provider using Default.copy() + val customStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(24.dp) // Pill shape for Google + ), + "facebook.com" to ProviderStyleDefaults.Facebook.copy( + shape = RoundedCornerShape(8.dp) // Medium rounded for Facebook + ) + // Email uses global default (12dp) + ) + + val theme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customStyles + ) + + AuthUITheme(theme = theme) { + ButtonPreviewColumn(stringProvider) + } +} + +@Composable +fun ButtonPreviewColumn(stringProvider: DefaultAuthUIStringProvider) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + AuthProviderButton( + provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + + AuthProviderButton( + provider = AuthProvider.Facebook(), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + + AuthProviderButton( + provider = AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ), + onClick = { }, + stringProvider = stringProvider, + modifier = Modifier.fillMaxWidth() + ) + } +} diff --git a/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt index 570f6135f..52427bba9 100644 --- a/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/HighLevelApiDemoActivity.kt @@ -5,6 +5,8 @@ import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,6 +27,7 @@ import androidx.compose.ui.unit.dp import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI +import com.firebase.ui.auth.configuration.AuthUITransitions import com.firebase.ui.auth.configuration.PasswordRule import com.firebase.ui.auth.configuration.authUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider @@ -42,12 +46,23 @@ class HighLevelApiDemoActivity : ComponentActivity() { val authUI = FirebaseAuthUI.getInstance() val emailLink = intent.getStringExtra(EmailLinkConstants.EXTRA_EMAIL_LINK) + val customTheme = AuthUITheme.Default.copy( + providerButtonShape = ShapeDefaults.ExtraLarge + ) + val configuration = authUIConfiguration { context = applicationContext + theme = customTheme logo = AuthUIAsset.Resource(R.drawable.firebase_auth) tosUrl = "https://policies.google.com/terms" privacyPolicyUrl = "https://policies.google.com/privacy" isAnonymousUpgradeEnabled = false + transitions = AuthUITransitions( + enterTransition = { slideInHorizontally { it } }, + exitTransition = { slideOutHorizontally { -it } }, + popEnterTransition = { slideInHorizontally { -it } }, + popExitTransition = { slideOutHorizontally { it } } + ) providers { provider(AuthProvider.Anonymous) provider( diff --git a/auth/README.md b/auth/README.md index e44ac7b6f..0f3bcd222 100644 --- a/auth/README.md +++ b/auth/README.md @@ -16,48 +16,62 @@ Equivalent FirebaseUI libraries are available for [iOS](https://github.com/fireb ## Table of Contents +### Getting Started 1. [Demo](#demo) -1. [Setup](#setup) - 1. [Prerequisites](#prerequisites) - 1. [Installation](#installation) - 1. [Provider Configuration](#provider-configuration) -1. [Quick Start](#quick-start) - 1. [Minimal Example](#minimal-example) - 1. [Check Authentication State](#check-authentication-state) -1. [Core Concepts](#core-concepts) - 1. [FirebaseAuthUI](#firebaseauthui) - 1. [AuthUIConfiguration](#authuiconfiguration) - 1. [AuthFlowController](#authflowcontroller) - 1. [AuthState](#authstate) -1. [Authentication Methods](#authentication-methods) - 1. [Email & Password](#email--password) - 1. [Phone Number](#phone-number) - 1. [Google Sign-In](#google-sign-in) - 1. [Facebook Login](#facebook-login) - 1. [Other OAuth Providers](#other-oauth-providers) - 1. [Anonymous Authentication](#anonymous-authentication) - 1. [Custom OAuth Provider](#custom-oauth-provider) -1. [Usage Patterns](#usage-patterns) - 1. [High-Level API (Recommended)](#high-level-api-recommended) - 1. [Low-Level API (Advanced)](#low-level-api-advanced) - 1. [Custom UI with Slots](#custom-ui-with-slots) -1. [Multi-Factor Authentication](#multi-factor-authentication) - 1. [MFA Configuration](#mfa-configuration) - 1. [MFA Enrollment](#mfa-enrollment) - 1. [MFA Challenge](#mfa-challenge) -1. [Theming & Customization](#theming--customization) - 1. [Material Theme Integration](#material-theme-integration) - 1. [Custom Theme](#custom-theme) - 1. [Provider Button Styling](#provider-button-styling) -1. [Advanced Features](#advanced-features) - 1. [Anonymous User Upgrade](#anonymous-user-upgrade) - 1. [Email Link Sign-In](#email-link-sign-in) - 1. [Password Validation Rules](#password-validation-rules) - 1. [Credential Manager Integration](#credential-manager-integration) - 1. [Sign Out & Account Deletion](#sign-out--account-deletion) -1. [Localization](#localization) -1. [Error Handling](#error-handling) -1. [Migration Guide](#migration-guide) +2. [Setup](#setup) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Provider Configuration](#provider-configuration) +3. [Quick Start](#quick-start) + - [Minimal Example](#minimal-example) + - [Check Authentication State](#check-authentication-state) + +### Core Concepts +4. [Core Concepts](#core-concepts) + - [FirebaseAuthUI](#firebaseauthui) + - [AuthUIConfiguration](#authuiconfiguration) + - [AuthFlowController](#authflowcontroller) + - [AuthState](#authstate) + +### Authentication +5. [Authentication Methods](#authentication-methods) + - [Email & Password](#email--password) + - [Phone Number](#phone-number) + - [Google Sign-In](#google-sign-in) + - [Facebook Login](#facebook-login) + - [Other OAuth Providers](#other-oauth-providers) + - [Anonymous Authentication](#anonymous-authentication) + - [Custom OAuth Provider](#custom-oauth-provider) +6. [Multi-Factor Authentication](#multi-factor-authentication) + - [MFA Configuration](#mfa-configuration) + - [MFA Enrollment](#mfa-enrollment) + - [MFA Challenge](#mfa-challenge) + +### Implementation +7. [Usage Patterns](#usage-patterns) + - [High-Level API (Recommended)](#high-level-api-recommended) + - [Low-Level API (Advanced)](#low-level-api-advanced) + - [Custom UI with Slots](#custom-ui-with-slots) +8. [Theming & Customization](#theming--customization) + - [Using Default Themes](#using-default-themes) + - [Using Adaptive Theme](#using-adaptive-theme-recommended) + - [Customizing Default Theme](#customizing-default-theme) + - [Theme Behavior & Patterns](#theme-behavior--patterns) + - [Inheriting from Material Theme](#inheriting-from-material-theme) + - [Creating a Completely Custom Theme](#creating-a-completely-custom-theme) + - [Provider Button Styling](#provider-button-styling) + - [Screen Transitions](#screen-transitions) + +### Advanced +9. [Advanced Features](#advanced-features) + - [Anonymous User Upgrade](#anonymous-user-upgrade) + - [Email Link Sign-In](#email-link-sign-in) + - [Password Validation Rules](#password-validation-rules) + - [Credential Manager Integration](#credential-manager-integration) + - [Sign Out & Account Deletion](#sign-out--account-deletion) +10. [Localization](#localization) +11. [Error Handling](#error-handling) +12. [Migration Guide](#migration-guide) ## Demo @@ -970,9 +984,156 @@ fun ManualMfaChallenge(resolver: MultiFactorResolver) { ## Theming & Customization -### Material Theme Integration +FirebaseUI Auth provides flexible theming options to match your app's design: -FirebaseUI automatically inherits your app's Material Theme: +- **`AuthUITheme.Default`** / **`AuthUITheme.DefaultDark`** / **`AuthUITheme.Adaptive`** - Pre-configured Material Design 3 themes +- **`.copy()`** - Customize specific properties of the default themes (data class) +- **`fromMaterialTheme()`** - Inherit from your app's existing Material Theme +- **Custom theme** - Full control over colors, typography, shapes, and provider button styles + +### Using Default Themes + +FirebaseUI provides pre-configured themes for light and dark modes: + +```kotlin +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Email(), AuthProvider.Google()) + theme = AuthUITheme.Default // Light theme + // or + theme = AuthUITheme.DefaultDark // Dark theme +} +``` + +### Using Adaptive Theme (Recommended) + +`AuthUITheme.Adaptive` automatically switches between light and dark themes based on the system setting: + +```kotlin +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Email(), AuthProvider.Google()) + theme = AuthUITheme.Adaptive // Adapts to system dark mode +} +``` + +This is the recommended approach for most apps as it provides a seamless experience that respects the user's system preferences. + +**Note:** `Adaptive` is a `@Composable` property that evaluates to `Default` (light) or `DefaultDark` (dark) based on `isSystemInDarkTheme()` at composition time. + +### Customizing Default Theme + +Use `.copy()` to customize specific properties of the default theme: + +```kotlin +@Composable +fun AuthScreen() { + val customTheme = AuthUITheme.Adaptive.copy( + providerButtonShape = MaterialTheme.shapes.extraLarge // Pill-shaped buttons + ) + + val configuration = authUIConfiguration { + context = applicationContext + providers = listOf(AuthProvider.Google(), AuthProvider.Email()) + theme = customTheme + } + + FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { /* ... */ }, + onSignInFailure = { /* ... */ }, + onSignInCancelled = { /* ... */ } + ) +} +``` + +### Theme Behavior & Patterns + +FirebaseUI Auth supports two theming patterns with clear precedence rules: + +#### Pattern 1: Theme in Configuration Only (Recommended) + +The simplest approach is to set the theme only in `authUIConfiguration`: + +```kotlin +val configuration = authUIConfiguration { + context = applicationContext + providers = listOf(AuthProvider.Email()) + theme = AuthUITheme.Adaptive // Set theme here +} + +FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { /* ... */ } +) +``` + +**When to use:** This is the recommended pattern for most use cases. It's simple and explicit. + +#### Pattern 2: Theme in Wrapper (Optional) + +You can also wrap `FirebaseAuthScreen` with `AuthUITheme`: + +```kotlin +val configuration = authUIConfiguration { + context = applicationContext + providers = listOf(AuthProvider.Email()) + theme = AuthUITheme.Adaptive // Theme in configuration +} + +AuthUITheme(theme = AuthUITheme.Adaptive) { // Optional wrapper + Surface(color = MaterialTheme.colorScheme.background) { + FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { /* ... */ } + ) + } +} +``` + +**When to use:** Use this pattern when you have UI elements **outside** of `FirebaseAuthScreen` that need to share the same theme. + +#### Theme Precedence Rules + +Understanding which theme applies is important: + +1. **Configuration theme takes precedence:** + ```kotlin + val configuration = authUIConfiguration { + theme = AuthUITheme.Default // LIGHT theme + } + + AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper + FirebaseAuthScreen(configuration, ...) + } + // Result: FirebaseAuthScreen uses LIGHT theme (from configuration) + ``` + +2. **Wrapper as fallback:** + ```kotlin + val configuration = authUIConfiguration { + // theme not specified (null) + } + + AuthUITheme(theme = AuthUITheme.DefaultDark) { // DARK wrapper + FirebaseAuthScreen(configuration, ...) + } + // Result: FirebaseAuthScreen inherits DARK theme from wrapper + ``` + +3. **Ultimate fallback:** + ```kotlin + val configuration = authUIConfiguration { + // theme not specified (null) + } + + FirebaseAuthScreen(configuration, ...) // No wrapper + // Result: Uses AuthUITheme.Default (light theme) + ``` + +**Best Practice:** For clarity and consistency, always set `theme` in `authUIConfiguration`. Use the wrapper only if you have additional UI outside `FirebaseAuthScreen`. + +### Inheriting from Material Theme + +Use `fromMaterialTheme()` to automatically inherit your app's Material Design theme: ```kotlin @Composable @@ -980,7 +1141,7 @@ fun App() { MyAppTheme { // Your existing Material3 theme val configuration = authUIConfiguration { providers = listOf(AuthProvider.Email()) - theme = AuthUITheme.fromMaterialTheme() // Inherits MyAppTheme + theme = AuthUITheme.fromMaterialTheme() // Inherits colors, typography, shapes } FirebaseAuthScreen( @@ -991,9 +1152,20 @@ fun App() { } ``` -### Custom Theme +You can also customize while inheriting: + +```kotlin +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Google(), AuthProvider.Facebook()) + theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) // Override button shape + ) +} +``` + +### Creating a Completely Custom Theme -Create a completely custom theme: +Build a theme from scratch with full control: ```kotlin val customTheme = AuthUITheme( @@ -1011,7 +1183,8 @@ val customTheme = AuthUITheme( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), large = RoundedCornerShape(16.dp) - ) + ), + providerButtonShape = RoundedCornerShape(12.dp) ) val configuration = authUIConfiguration { @@ -1022,27 +1195,69 @@ val configuration = authUIConfiguration { ### Provider Button Styling -Customize individual provider button styling: +#### Setting shapes for all provider buttons + +**Option 1: Using `.copy()` on default theme:** + +```kotlin +val customTheme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp) // Applies to all provider buttons +) + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Google(), AuthProvider.Facebook(), AuthProvider.Email()) + theme = customTheme +} +``` + +**Option 2: Using `fromMaterialTheme()`:** + +```kotlin +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Google(), AuthProvider.Facebook()) + theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) + ) +} +``` + +**Option 3: Creating custom theme:** + +```kotlin +val customTheme = AuthUITheme( + colorScheme = MaterialTheme.colorScheme, + typography = MaterialTheme.typography, + shapes = MaterialTheme.shapes, + providerButtonShape = RoundedCornerShape(12.dp) +) + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Google(), AuthProvider.Facebook(), AuthProvider.Email()) + theme = customTheme +} +``` + +#### Customizing individual provider buttons + +Customize specific provider buttons using the pre-defined `ProviderStyleDefaults` constants: + +**Using `.copy()` with default theme:** ```kotlin val customProviderStyles = mapOf( - "google.com" to AuthUITheme.ProviderStyle( - backgroundColor = Color.White, - contentColor = Color(0xFF757575), - iconTint = null, // Use original colors + "google.com" to ProviderStyleDefaults.Google.copy( shape = RoundedCornerShape(8.dp), elevation = 4.dp ), - "facebook.com" to AuthUITheme.ProviderStyle( - backgroundColor = Color(0xFF1877F2), - contentColor = Color.White, - shape = RoundedCornerShape(12.dp), + "facebook.com" to ProviderStyleDefaults.Facebook.copy( + shape = RoundedCornerShape(24.dp), elevation = 0.dp ) ) val customTheme = AuthUITheme.Default.copy( - providerStyles = customProviderStyles + providerButtonShape = RoundedCornerShape(12.dp), // Default for all + providerStyles = customProviderStyles // Specific overrides ) val configuration = authUIConfiguration { @@ -1051,6 +1266,137 @@ val configuration = authUIConfiguration { } ``` +**Using `fromMaterialTheme()`:** + +```kotlin +val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(8.dp), + elevation = 4.dp + ) +) + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Google(), AuthProvider.Facebook()) + theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customProviderStyles + ) +} +``` + +#### Complete customization example + +Real-world example combining global and per-provider customizations: + +```kotlin +// Define custom styles for specific providers +val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(24.dp), // Pill-shaped Google button + elevation = 6.dp + ), + "facebook.com" to ProviderStyleDefaults.Facebook.copy( + shape = RoundedCornerShape(8.dp), // Medium rounded Facebook button + elevation = 0.dp // Flat design + ) + // Email provider will use the global providerButtonShape +) + +// Customize default theme with global button shape and per-provider styles +val customTheme = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), // Global default for all buttons + providerStyles = customProviderStyles // Specific overrides +) + +val configuration = authUIConfiguration { + providers = listOf( + AuthProvider.Google(), // Uses custom shape (24.dp) + AuthProvider.Facebook(), // Uses custom shape (8.dp) + AuthProvider.Email() // Uses global shape (12.dp) + ) + theme = customTheme +} +``` + +### Screen Transitions + +Customize the animations when navigating between screens using the `AuthUITransitions` object: + +**Slide animations:** + +```kotlin +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import com.firebase.ui.auth.configuration.AuthUITransitions + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Email(), AuthProvider.Google()) + transitions = AuthUITransitions( + enterTransition = { slideInHorizontally { it } }, // Slide in from right + exitTransition = { slideOutHorizontally { -it } }, // Slide out to left + popEnterTransition = { slideInHorizontally { -it } }, // Slide in from left + popExitTransition = { slideOutHorizontally { it } } // Slide out to right + ) +} +``` + +**Fade animations (default):** + +```kotlin +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import com.firebase.ui.auth.configuration.AuthUITransitions + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Phone()) + transitions = AuthUITransitions( + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() }, + popEnterTransition = { fadeIn() }, + popExitTransition = { fadeOut() } + ) +} +``` + +**Scale animations:** + +```kotlin +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import com.firebase.ui.auth.configuration.AuthUITransitions + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Facebook()) + transitions = AuthUITransitions( + enterTransition = { fadeIn() + scaleIn(initialScale = 0.9f) }, + exitTransition = { fadeOut() + scaleOut(targetScale = 0.9f) }, + popEnterTransition = { fadeIn() + scaleIn(initialScale = 0.9f) }, + popExitTransition = { fadeOut() + scaleOut(targetScale = 0.9f) } + ) +} +``` + +**Vertical slide:** + +```kotlin +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import com.firebase.ui.auth.configuration.AuthUITransitions + +val configuration = authUIConfiguration { + providers = listOf(AuthProvider.Email()) + transitions = AuthUITransitions( + enterTransition = { slideInVertically { it } }, // Slide up + exitTransition = { slideOutVertically { -it } } // Slide down + ) +} +``` + +> **Note:** If not specified, default fade in/out transitions with 700ms duration are used. + ## Advanced Features ### Anonymous User Upgrade diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index 14a0a7f98..f7f16d715 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -104,7 +104,7 @@ dependencies { implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") - implementation("androidx.navigation:navigation-compose:2.8.3") + api("androidx.navigation:navigation-compose:2.8.3") implementation("com.google.zxing:core:3.5.3") annotationProcessor(Config.Libs.Androidx.lifecycleCompiler) diff --git a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt index af71920d7..168670da1 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthActivity.kt @@ -130,7 +130,7 @@ class FirebaseAuthActivity : ComponentActivity() { // Set up Compose UI setContent { - AuthUITheme(theme = configuration.theme) { + AuthUITheme { FirebaseAuthScreen( authUI = authUI, configuration = configuration, diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUIConfiguration.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUIConfiguration.kt index 68087b419..3fa7f394b 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUIConfiguration.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUIConfiguration.kt @@ -36,7 +36,7 @@ annotation class AuthUIConfigurationDsl class AuthUIConfigurationBuilder { var context: Context? = null private val providers = mutableListOf() - var theme: AuthUITheme = AuthUITheme.Default + var theme: AuthUITheme? = null var locale: Locale? = null var stringProvider: AuthUIStringProvider? = null var isCredentialManagerEnabled: Boolean = true @@ -49,6 +49,7 @@ class AuthUIConfigurationBuilder { var isNewEmailAccountsAllowed: Boolean = true var isDisplayNameRequired: Boolean = true var isProviderChoiceAlwaysShown: Boolean = false + var transitions: AuthUITransitions? = null fun providers(block: AuthProvidersBuilder.() -> Unit) = providers.addAll(AuthProvidersBuilder().apply(block).build()) @@ -112,7 +113,8 @@ class AuthUIConfigurationBuilder { passwordResetActionCodeSettings = passwordResetActionCodeSettings, isNewEmailAccountsAllowed = isNewEmailAccountsAllowed, isDisplayNameRequired = isDisplayNameRequired, - isProviderChoiceAlwaysShown = isProviderChoiceAlwaysShown + isProviderChoiceAlwaysShown = isProviderChoiceAlwaysShown, + transitions = transitions ) } } @@ -132,9 +134,10 @@ class AuthUIConfiguration( val providers: List = emptyList(), /** - * The theming configuration for the UI. Default to [AuthUITheme.Default]. + * The theming configuration for the UI. If null, inherits from the outer AuthUITheme wrapper + * or defaults to [AuthUITheme.Default] if no wrapper is present. */ - val theme: AuthUITheme = AuthUITheme.Default, + val theme: AuthUITheme? = null, /** * The locale for internationalization. @@ -195,4 +198,10 @@ class AuthUIConfiguration( * Always shows the provider selection screen, even if only one is enabled. */ val isProviderChoiceAlwaysShown: Boolean = false, + + /** + * Custom screen transition animations. + * If null, uses default fade in/out transitions. + */ + val transitions: AuthUITransitions? = null, ) diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUITransitions.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUITransitions.kt new file mode 100644 index 000000000..b37dc34e1 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/AuthUITransitions.kt @@ -0,0 +1,35 @@ +/* + * 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.configuration + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.navigation.NavBackStackEntry + +/** + * Container for screen transition animations used in Firebase Auth UI. + * + * @property enterTransition Transition when entering a new screen + * @property exitTransition Transition when exiting current screen + * @property popEnterTransition Transition when returning to previous screen (back navigation) + * @property popExitTransition Transition when exiting during back navigation + */ +data class AuthUITransitions( + val enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition)? = null, + val exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition)? = null, + val popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition)? = null, + val popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition)? = null, +) diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/theme/AuthUITheme.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/theme/AuthUITheme.kt index a2e8e143a..79e78f80f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/theme/AuthUITheme.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/theme/AuthUITheme.kt @@ -15,7 +15,6 @@ package com.firebase.ui.auth.configuration.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -25,11 +24,19 @@ import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme 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 +/** + * CompositionLocal providing access to the current AuthUITheme. + * This allows components to access theme configuration including provider styles and shapes. + */ +val LocalAuthUITheme = staticCompositionLocalOf { AuthUITheme.Default } + /** * Theming configuration for the entire Auth UI. */ @@ -45,21 +52,95 @@ class AuthUITheme( val typography: Typography, /** - * The shapes to use for UI elements. + * The shapes to use for UI elements (text fields, cards, etc.). */ val shapes: Shapes, /** - * A map of provider IDs to custom styling. + * A map of provider IDs to custom styling. Use this to customize individual + * provider buttons with specific colors, icons, shapes, and elevation. + * + * Example: + * ```kotlin + * providerStyles = mapOf( + * "google.com" to ProviderStyleDefaults.Google.copy( + * shape = RoundedCornerShape(12.dp) + * ) + * ) + * ``` + */ + val providerStyles: Map = emptyMap(), + + /** + * Default shape for all provider buttons. If not set, defaults to RoundedCornerShape(4.dp). + * Individual provider styles can override this shape. + * + * Example: + * ```kotlin + * providerButtonShape = RoundedCornerShape(12.dp) + * ``` */ - val providerStyles: Map = emptyMap() + val providerButtonShape: Shape? = null, ) { + /** + * Creates a copy of this AuthUITheme, optionally overriding specific properties. + * + * @param colorScheme The color scheme to use. Defaults to this theme's color scheme. + * @param typography The typography to use. Defaults to this theme's typography. + * @param shapes The shapes to use. Defaults to this theme's shapes. + * @param providerStyles Custom styling for individual providers. Defaults to this theme's provider styles. + * @param providerButtonShape Default shape for provider buttons. Defaults to this theme's provider button shape. + * @return A new AuthUITheme instance with the specified properties. + */ + fun copy( + colorScheme: ColorScheme = this.colorScheme, + typography: Typography = this.typography, + shapes: Shapes = this.shapes, + providerStyles: Map = this.providerStyles, + providerButtonShape: Shape? = this.providerButtonShape, + ): AuthUITheme { + return AuthUITheme( + colorScheme = colorScheme, + typography = typography, + shapes = shapes, + providerStyles = providerStyles, + providerButtonShape = providerButtonShape + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is AuthUITheme) return false + + if (colorScheme != other.colorScheme) return false + if (typography != other.typography) return false + if (shapes != other.shapes) return false + if (providerStyles != other.providerStyles) return false + if (providerButtonShape != other.providerButtonShape) return false + + return true + } + + override fun hashCode(): Int { + var result = colorScheme.hashCode() + result = 31 * result + typography.hashCode() + result = 31 * result + shapes.hashCode() + result = 31 * result + providerStyles.hashCode() + result = 31 * result + (providerButtonShape?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "AuthUITheme(colorScheme=$colorScheme, typography=$typography, shapes=$shapes, " + + "providerStyles=$providerStyles, providerButtonShape=$providerButtonShape)" + } + /** * A class nested within AuthUITheme that defines the visual appearance of a specific * provider button, allowing for per-provider branding and customization. */ - class ProviderStyle( + data class ProviderStyle( /** * The provider's icon. */ @@ -79,17 +160,18 @@ class AuthUITheme( * An optional tint color for the provider's icon. If null, * the icon's intrinsic color is used. */ - var iconTint: Color? = null, + val iconTint: Color? = null, /** - * The shape of the button container. Defaults to RoundedCornerShape(4.dp). + * The shape of the button container. If null, uses the theme's providerButtonShape + * or falls back to RoundedCornerShape(4.dp). */ - val shape: Shape = RoundedCornerShape(4.dp), + val shape: Shape? = null, /** * The shadow elevation for the button. Defaults to 2.dp. */ - val elevation: Dp = 2.dp + val elevation: Dp = 2.dp, ) { internal companion object { /** @@ -123,19 +205,26 @@ class AuthUITheme( providerStyles = ProviderStyleDefaults.default ) + val Adaptive: AuthUITheme + @Composable get() = if (isSystemInDarkTheme()) DefaultDark else Default + /** - * Creates a theme inheriting the app's current Material - * Theme settings. + * Creates a theme inheriting the app's current Material Theme settings. + * + * @param providerStyles Custom styling for individual providers. Defaults to standard provider styles. + * @param providerButtonShape Default shape for all provider buttons. If null, uses RoundedCornerShape(4.dp). */ @Composable fun fromMaterialTheme( - providerStyles: Map = ProviderStyleDefaults.default + providerStyles: Map = ProviderStyleDefaults.default, + providerButtonShape: Shape? = null, ): AuthUITheme { return AuthUITheme( colorScheme = MaterialTheme.colorScheme, typography = MaterialTheme.typography, shapes = MaterialTheme.shapes, - providerStyles = providerStyles + providerStyles = providerStyles, + providerButtonShape = providerButtonShape ) } @@ -152,14 +241,17 @@ class AuthUITheme( @Composable fun AuthUITheme( - theme: AuthUITheme = if (isSystemInDarkTheme()) - AuthUITheme.DefaultDark else AuthUITheme.Default, - content: @Composable () -> Unit + theme: AuthUITheme = AuthUITheme.Adaptive, + content: @Composable () -> Unit, ) { - MaterialTheme( - colorScheme = theme.colorScheme, - typography = theme.typography, - shapes = theme.shapes, - content = content - ) + CompositionLocalProvider( + LocalAuthUITheme provides theme + ) { + MaterialTheme( + colorScheme = theme.colorScheme, + typography = theme.typography, + shapes = theme.shapes, + content = content + ) + } } diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/theme/ProviderStyleDefaults.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/theme/ProviderStyleDefaults.kt index 051758528..c4721b395 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/theme/ProviderStyleDefaults.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/theme/ProviderStyleDefaults.kt @@ -27,90 +27,82 @@ import com.firebase.ui.auth.configuration.auth_provider.Provider * * The styles are automatically applied when using [AuthUITheme.Default] or can be * customized by passing a modified map to [AuthUITheme.fromMaterialTheme]. + * + * Individual provider styles can be accessed and customized using the public properties + * (e.g., [Google], [Facebook]) and then modified using the [AuthUITheme.ProviderStyle.copy] method. */ -internal object ProviderStyleDefaults { - val default: Map - get() = Provider.entries.associate { provider -> - 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) - ) - } +object ProviderStyleDefaults { + val Google = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_googleg_color_24dp), + backgroundColor = Color.White, + contentColor = Color(0xFF757575) + ) - Provider.FACEBOOK -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_facebook_white_22dp), - backgroundColor = Color(0xFF1877F2), - contentColor = Color.White - ) - } + val Facebook = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_facebook_white_22dp), + backgroundColor = Color(0xFF1877F2), + contentColor = Color.White + ) - Provider.TWITTER -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_twitter_x_white_24dp), - backgroundColor = Color.Black, - contentColor = Color.White - ) - } + val Twitter = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_twitter_x_white_24dp), + backgroundColor = Color.Black, + contentColor = Color.White + ) - Provider.GITHUB -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_github_white_24dp), - backgroundColor = Color(0xFF24292E), - contentColor = Color.White - ) - } + val Github = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_github_white_24dp), + backgroundColor = Color(0xFF24292E), + contentColor = Color.White + ) - Provider.EMAIL -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_mail_white_24dp), - backgroundColor = Color(0xFFD0021B), - contentColor = Color.White - ) - } + val Email = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_mail_white_24dp), + backgroundColor = Color(0xFFD0021B), + contentColor = Color.White + ) - Provider.PHONE -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_phone_white_24dp), - backgroundColor = Color(0xFF43C5A5), - contentColor = Color.White - ) - } + val Phone = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_phone_white_24dp), + backgroundColor = Color(0xFF43C5A5), + contentColor = Color.White + ) - Provider.ANONYMOUS -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_anonymous_white_24dp), - backgroundColor = Color(0xFFF4B400), - contentColor = Color.White - ) - } + val Anonymous = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_anonymous_white_24dp), + backgroundColor = Color(0xFFF4B400), + contentColor = Color.White + ) - Provider.MICROSOFT -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_microsoft_24dp), - backgroundColor = Color(0xFF2F2F2F), - contentColor = Color.White - ) - } + val Microsoft = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_microsoft_24dp), + backgroundColor = Color(0xFF2F2F2F), + contentColor = Color.White + ) - Provider.YAHOO -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_yahoo_24dp), - backgroundColor = Color(0xFF720E9E), - contentColor = Color.White - ) - } + val Yahoo = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_yahoo_24dp), + backgroundColor = Color(0xFF720E9E), + contentColor = Color.White + ) - Provider.APPLE -> { - provider.id to AuthUITheme.ProviderStyle( - icon = AuthUIAsset.Resource(R.drawable.fui_ic_apple_white_24dp), - backgroundColor = Color.Black, - contentColor = Color.White - ) - } - } - } + val Apple = AuthUITheme.ProviderStyle( + icon = AuthUIAsset.Resource(R.drawable.fui_ic_apple_white_24dp), + backgroundColor = Color.Black, + contentColor = Color.White + ) + + val default: Map + get() = mapOf( + Provider.GOOGLE.id to Google, + Provider.FACEBOOK.id to Facebook, + Provider.TWITTER.id to Twitter, + Provider.GITHUB.id to Github, + Provider.EMAIL.id to Email, + Provider.PHONE.id to Phone, + Provider.ANONYMOUS.id to Anonymous, + Provider.MICROSOFT.id to Microsoft, + Provider.YAHOO.id to Yahoo, + Provider.APPLE.id to Apple + ) } \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/AuthProviderButton.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/AuthProviderButton.kt index 0d2faa206..9a3f5e1a2 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/AuthProviderButton.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/AuthProviderButton.kt @@ -14,6 +14,7 @@ package com.firebase.ui.auth.ui.components +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -35,6 +36,7 @@ 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.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -45,6 +47,8 @@ import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.theme.AuthUIAsset import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.LocalAuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults /** * A customizable button for an authentication provider. @@ -86,9 +90,15 @@ fun AuthProviderButton( showAsContinue: Boolean = false, ) { val context = LocalContext.current - val providerStyle = resolveProviderStyle(provider, style) + val authTheme = LocalAuthUITheme.current val providerLabel = label ?: resolveProviderLabel(provider, stringProvider, context, showAsContinue) + val providerStyle = resolveProviderStyle( + provider = provider, + style = style, + providerStyles = authTheme.providerStyles, + defaultButtonShape = authTheme.providerButtonShape + ) Button( modifier = modifier, @@ -100,7 +110,7 @@ fun AuthProviderButton( containerColor = providerStyle.backgroundColor, contentColor = providerStyle.contentColor, ), - shape = providerStyle.shape, + shape = providerStyle.shape ?: RoundedCornerShape(4.dp), elevation = ButtonDefaults.buttonElevation( defaultElevation = providerStyle.elevation ), @@ -164,27 +174,46 @@ fun AuthProviderButton( internal fun resolveProviderStyle( provider: AuthProvider, style: AuthUITheme.ProviderStyle?, + providerStyles: Map, + defaultButtonShape: Shape?, ): AuthUITheme.ProviderStyle { - if (style != null) return style + // If explicit style is provided, use it but apply default shape if needed + if (style != null) { + return if (style.shape == null) { + style.copy(shape = defaultButtonShape ?: RoundedCornerShape(4.dp)) + } else { + style + } + } - val defaultStyle = - AuthUITheme.Default.providerStyles[provider.providerId] ?: AuthUITheme.ProviderStyle.Empty + // Get the configured style from the theme or fall back to defaults + val configuredStyle = providerStyles[provider.providerId] + ?: ProviderStyleDefaults.default[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, + // Handle GenericOAuth providers with custom properties + val resolvedStyle = if (provider is AuthProvider.GenericOAuth) { + configuredStyle.copy( + icon = provider.buttonIcon ?: configuredStyle.icon, + backgroundColor = provider.buttonColor ?: configuredStyle.backgroundColor, + contentColor = provider.contentColor ?: configuredStyle.contentColor, ) } else { - defaultStyle + configuredStyle + } + + // Apply default button shape if no shape is explicitly set + return if (resolvedStyle.shape == null) { + resolvedStyle.copy(shape = defaultButtonShape ?: RoundedCornerShape(4.dp)) + } else { + resolvedStyle } } internal fun resolveProviderLabel( provider: AuthProvider, stringProvider: AuthUIStringProvider, - context: android.content.Context, + context: Context, showAsContinue: Boolean = false, ): String = when (provider) { is AuthProvider.GenericOAuth -> provider.buttonLabel diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt index 98991258f..6b3bf5d38 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/FirebaseAuthScreen.kt @@ -16,6 +16,9 @@ package com.firebase.ui.auth.ui.screens import android.util.Log import androidx.activity.compose.LocalActivity +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -61,6 +64,7 @@ import com.firebase.ui.auth.configuration.auth_provider.signInWithEmailLink import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.configuration.theme.LocalAuthUITheme import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController import com.firebase.ui.auth.ui.components.rememberTopLevelDialogController import com.firebase.ui.auth.ui.method_picker.AuthMethodPicker @@ -113,7 +117,8 @@ fun FirebaseAuthScreen( val pendingLinkingCredential = remember { mutableStateOf(null) } val pendingResolver = remember { mutableStateOf(null) } val emailLinkFromDifferentDevice = remember { mutableStateOf(null) } - val lastSignInPreference = remember { mutableStateOf(null) } + val lastSignInPreference = + remember { mutableStateOf(null) } // Load last sign-in preference on launch LaunchedEffect(authState) { @@ -216,7 +221,8 @@ fun FirebaseAuthScreen( CompositionLocalProvider( LocalAuthUIStringProvider provides configuration.stringProvider, - LocalTopLevelDialogController provides dialogController + LocalTopLevelDialogController provides dialogController, + LocalAuthUITheme provides (configuration.theme ?: LocalAuthUITheme.current) ) { Surface( modifier = Modifier @@ -224,7 +230,19 @@ fun FirebaseAuthScreen( ) { NavHost( navController = navController, - startDestination = AuthRoute.MethodPicker.route + startDestination = AuthRoute.MethodPicker.route, + enterTransition = configuration.transitions?.enterTransition ?: { + fadeIn(animationSpec = tween(700)) + }, + exitTransition = configuration.transitions?.exitTransition ?: { + fadeOut(animationSpec = tween(700)) + }, + popEnterTransition = configuration.transitions?.popEnterTransition ?: { + fadeIn(animationSpec = tween(700)) + }, + popExitTransition = configuration.transitions?.popExitTransition ?: { + fadeOut(animationSpec = tween(700)) + } ) { composable(AuthRoute.MethodPicker.route) { Scaffold { innerPadding -> @@ -447,7 +465,8 @@ fun FirebaseAuthScreen( if (emailLink != null && emailProvider != null) { try { // Try to retrieve saved email from DataStore (same-device flow) - val savedEmail = EmailLinkPersistenceManager.default.retrieveSessionRecord(context)?.email + val savedEmail = + EmailLinkPersistenceManager.default.retrieveSessionRecord(context)?.email if (savedEmail != null) { // Same device - we have the email, sign in automatically @@ -492,7 +511,8 @@ fun FirebaseAuthScreen( // Reload sign-in preference (may have been updated by provider) coroutineScope.launch { - lastSignInPreference.value = SignInPreferenceManager.getLastSignIn(context) + lastSignInPreference.value = + SignInPreferenceManager.getLastSignIn(context) } } } @@ -580,26 +600,26 @@ fun FirebaseAuthScreen( is AuthException.AccountLinkingRequiredException -> { pendingLinkingCredential.value = exception.credential - navController.navigate(AuthRoute.Email.route) { - launchSingleTop = true + navController.navigate(AuthRoute.Email.route) { + launchSingleTop = true + } } - } - is AuthException.EmailLinkPromptForEmailException -> { - // Cross-device flow: User needs to enter their email - emailLinkFromDifferentDevice.value = exception.emailLink - navController.navigate(AuthRoute.Email.route) { - launchSingleTop = true + is AuthException.EmailLinkPromptForEmailException -> { + // Cross-device flow: User needs to enter their email + emailLinkFromDifferentDevice.value = exception.emailLink + navController.navigate(AuthRoute.Email.route) { + launchSingleTop = true + } } - } - is AuthException.EmailLinkCrossDeviceLinkingException -> { - // Cross-device linking flow: User needs to enter email to link provider - emailLinkFromDifferentDevice.value = exception.emailLink - navController.navigate(AuthRoute.Email.route) { - launchSingleTop = true + is AuthException.EmailLinkCrossDeviceLinkingException -> { + // Cross-device linking flow: User needs to enter email to link provider + emailLinkFromDifferentDevice.value = exception.emailLink + navController.navigate(AuthRoute.Email.route) { + launchSingleTop = true + } } - } else -> Unit } @@ -644,7 +664,7 @@ data class AuthSuccessUiContext( private fun SuccessDestination( authState: AuthState, stringProvider: AuthUIStringProvider, - uiContext: AuthSuccessUiContext + uiContext: AuthSuccessUiContext, ) { when (authState) { is AuthState.Success -> { @@ -689,7 +709,7 @@ private fun AuthSuccessContent( authUI: FirebaseAuthUI, stringProvider: AuthUIStringProvider, onSignOut: () -> Unit, - onManageMfa: () -> Unit + onManageMfa: () -> Unit, ) { val user = authUI.getCurrentUser() val userIdentifier = user?.email ?: user?.phoneNumber ?: user?.uid.orEmpty() @@ -722,7 +742,7 @@ private fun EmailVerificationContent( authUI: FirebaseAuthUI, stringProvider: AuthUIStringProvider, onCheckStatus: () -> Unit, - onSignOut: () -> Unit + onSignOut: () -> Unit, ) { val user = authUI.getCurrentUser() val emailLabel = user?.email ?: stringProvider.emailProvider @@ -754,7 +774,7 @@ private fun EmailVerificationContent( @Composable private fun ProfileCompletionContent( missingFields: List, - stringProvider: AuthUIStringProvider + stringProvider: AuthUIStringProvider, ) { Column( modifier = Modifier.fillMaxSize(), diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt index 0ce99cdc3..2ebc2542f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/EmailAuthScreen.kt @@ -403,7 +403,8 @@ private fun DefaultEmailAuthContent( onPasswordChange = state.onPasswordChange, onConfirmPasswordChange = state.onConfirmPasswordChange, onSignUpClick = state.onSignUpClick, - onGoToSignIn = state.onGoToSignIn + onGoToSignIn = state.onGoToSignIn, + onNavigateBack = onCancel ) } @@ -415,7 +416,8 @@ private fun DefaultEmailAuthContent( resetLinkSent = state.resetLinkSent, onEmailChange = state.onEmailChange, onSendResetLink = state.onSendResetLinkClick, - onGoToSignIn = state.onGoToSignIn + onGoToSignIn = state.onGoToSignIn, + onNavigateBack = onCancel ) } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt index baf9e1485..8b73f2c2d 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/ResetPasswordUI.kt @@ -24,10 +24,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -63,6 +67,7 @@ fun ResetPasswordUI( onEmailChange: (String) -> Unit, onSendResetLink: () -> Unit, onGoToSignIn: () -> Unit, + onNavigateBack: (() -> Unit)? = null, ) { val context = LocalContext.current @@ -115,6 +120,16 @@ fun ResetPasswordUI( title = { Text(stringProvider.recoverPasswordPageTitle) }, + navigationIcon = { + if (onNavigateBack != null) { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringProvider.backAction + ) + } + } + }, colors = AuthUITheme.topAppBarColors ) }, diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt index 152931f13..dfc413bc6 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignUpUI.kt @@ -24,9 +24,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -65,6 +69,7 @@ fun SignUpUI( onConfirmPasswordChange: (String) -> Unit, onGoToSignIn: () -> Unit, onSignUpClick: () -> Unit, + onNavigateBack: (() -> Unit)? = null, ) { val provider = configuration.providers.filterIsInstance().first() val context = LocalContext.current @@ -105,6 +110,16 @@ fun SignUpUI( title = { Text(stringProvider.signupPageTitle) }, + navigationIcon = { + if (onNavigateBack != null) { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringProvider.backAction + ) + } + } + }, colors = AuthUITheme.topAppBarColors ) }, diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt index 9839a7958..122e73a87 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/EnterVerificationCodeUI.kt @@ -24,9 +24,13 @@ import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -66,6 +70,7 @@ fun EnterVerificationCodeUI( onResendCodeClick: () -> Unit, onChangeNumberClick: () -> Unit, title: String? = null, + onNavigateBack: (() -> Unit)? = null, ) { val context = LocalContext.current val stringProvider = LocalAuthUIStringProvider.current @@ -88,6 +93,16 @@ fun EnterVerificationCodeUI( title = { Text(title ?: stringProvider.verifyPhoneNumber) }, + navigationIcon = { + if (onNavigateBack != null) { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringProvider.backAction + ) + } + } + }, colors = AuthUITheme.topAppBarColors ) }, diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt index 605a1a6b9..0fa4c0f0a 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/phone/PhoneAuthScreen.kt @@ -346,7 +346,8 @@ private fun DefaultPhoneAuthContent( onVerificationCodeChange = state.onVerificationCodeChange, onVerifyCodeClick = state.onVerifyCodeClick, onResendCodeClick = state.onResendCodeClick, - onChangeNumberClick = state.onChangeNumberClick + onChangeNumberClick = state.onChangeNumberClick, + onNavigateBack = onCancel ) } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/AuthUIConfigurationTest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/AuthUIConfigurationTest.kt index 304d3c3be..80cf85ad6 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/AuthUIConfigurationTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/AuthUIConfigurationTest.kt @@ -75,7 +75,7 @@ class AuthUIConfigurationTest { assertThat(config.context).isEqualTo(applicationContext) assertThat(config.providers).hasSize(1) - assertThat(config.theme).isEqualTo(AuthUITheme.Default) + assertThat(config.theme).isNull() assertThat(config.stringProvider).isInstanceOf(DefaultAuthUIStringProvider::class.java) assertThat(config.locale).isNull() assertThat(config.isCredentialManagerEnabled).isTrue() @@ -463,7 +463,8 @@ class AuthUIConfigurationTest { "passwordResetActionCodeSettings", "isNewEmailAccountsAllowed", "isDisplayNameRequired", - "isProviderChoiceAlwaysShown" + "isProviderChoiceAlwaysShown", + "transitions" ) val actualProperties = allProperties.map { it.name }.toSet() diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/theme/AuthUIThemeTest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/theme/AuthUIThemeTest.kt index c08702fea..46e6a1dc8 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/theme/AuthUIThemeTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/theme/AuthUIThemeTest.kt @@ -1,18 +1,33 @@ package com.firebase.ui.auth.configuration.theme +import android.content.Context 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.ShapeDefaults +import androidx.compose.material3.Text import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.configuration.authUIConfiguration +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.ui.screens.FirebaseAuthScreen import com.google.common.truth.Truth.assertThat +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -26,6 +41,47 @@ class AuthUIThemeTest { @get:Rule val composeTestRule = createComposeRule() + private lateinit var applicationContext: Context + + @Before + fun setup() { + applicationContext = ApplicationProvider.getApplicationContext() + + // Clear any existing Firebase apps + FirebaseApp.getApps(applicationContext).forEach { app -> + app.delete() + } + + // Initialize default FirebaseApp + FirebaseApp.initializeApp( + applicationContext, + FirebaseOptions.Builder() + .setApiKey("fake-api-key") + .setApplicationId("fake-app-id") + .setProjectId("fake-project-id") + .build() + ) + } + + private fun createTestConfiguration(theme: AuthUITheme? = null): AuthUIConfiguration { + return authUIConfiguration { + this.context = this@AuthUIThemeTest.applicationContext + this.theme = theme + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + } + } + + // ======================================================================== + // Basic Theme Tests + // ======================================================================== + @Test fun `Default AuthUITheme applies to MaterialTheme`() { val theme = AuthUITheme.Default @@ -40,7 +96,229 @@ class AuthUIThemeTest { } @Test - fun `fromMaterialTheme inherits client MaterialTheme values`() { + fun `AuthUITheme synchronizes with MaterialTheme`() { + val theme = AuthUITheme.DefaultDark + + var authUIThemeColors: ColorScheme? = null + var materialThemeColors: ColorScheme? = null + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + authUIThemeColors = LocalAuthUITheme.current.colorScheme + materialThemeColors = MaterialTheme.colorScheme + } + } + + composeTestRule.waitForIdle() + + assertThat(authUIThemeColors).isEqualTo(materialThemeColors) + } + + @Test + fun `AuthUITheme Default uses light color scheme`() { + val expectedLightColors = lightColorScheme() + + composeTestRule.setContent { + AuthUITheme(theme = AuthUITheme.Default) { + val colors = LocalAuthUITheme.current.colorScheme + assertThat(colors.primary).isEqualTo(expectedLightColors.primary) + assertThat(colors.background).isEqualTo(expectedLightColors.background) + assertThat(colors.surface).isEqualTo(expectedLightColors.surface) + } + } + } + + @Test + fun `AuthUITheme DefaultDark uses dark color scheme`() { + val expectedDarkColors = darkColorScheme() + + composeTestRule.setContent { + AuthUITheme(theme = AuthUITheme.DefaultDark) { + val colors = LocalAuthUITheme.current.colorScheme + assertThat(colors.primary).isEqualTo(expectedDarkColors.primary) + assertThat(colors.background).isEqualTo(expectedDarkColors.background) + assertThat(colors.surface).isEqualTo(expectedDarkColors.surface) + } + } + } + + // ======================================================================== + // Theme Inheritance & Precedence Tests + // ======================================================================== + + @Test + fun `Configuration theme takes precedence over wrapper theme`() { + val wrapperTheme = AuthUITheme.DefaultDark + val configurationTheme = AuthUITheme.Default + + var observedTheme: AuthUITheme? = null + + composeTestRule.setContent { + AuthUITheme(theme = wrapperTheme) { + CompositionLocalProvider( + LocalAuthUITheme provides (configurationTheme) + ) { + observedTheme = LocalAuthUITheme.current + } + } + } + + composeTestRule.waitForIdle() + + assertThat(observedTheme?.colorScheme).isEqualTo(configurationTheme.colorScheme) + } + + @Test + fun `Wrapper theme applies when configuration theme is null`() { + val wrapperTheme = AuthUITheme.DefaultDark + + var insideWrapperTheme: AuthUITheme? = null + var insideProviderTheme: AuthUITheme? = null + + composeTestRule.setContent { + AuthUITheme(theme = wrapperTheme) { + insideWrapperTheme = LocalAuthUITheme.current + + // Simulate FirebaseAuthScreen's theme provision with null config.theme + CompositionLocalProvider( + LocalAuthUITheme provides (null ?: LocalAuthUITheme.current) + ) { + insideProviderTheme = LocalAuthUITheme.current + } + } + } + + composeTestRule.waitForIdle() + + assertThat(insideProviderTheme?.colorScheme).isEqualTo(wrapperTheme.colorScheme) + assertThat(insideWrapperTheme?.colorScheme).isEqualTo(insideProviderTheme?.colorScheme) + } + + @Test + fun `Default theme applies when no theme specified`() { + var observedTheme: AuthUITheme? = null + + composeTestRule.setContent { + // Simulate FirebaseAuthScreen's theme provision with null config.theme and no wrapper + CompositionLocalProvider( + LocalAuthUITheme provides (null ?: LocalAuthUITheme.current) + ) { + observedTheme = LocalAuthUITheme.current + } + } + + composeTestRule.waitForIdle() + + assertThat(observedTheme).isEqualTo(AuthUITheme.Default) + } + + // ======================================================================== + // Adaptive Theme Tests + // ======================================================================== + + @Test + fun `Adaptive theme resolves to Default or DefaultDark`() { + var adaptiveTheme: AuthUITheme? = null + + composeTestRule.setContent { + adaptiveTheme = AuthUITheme.Adaptive + } + + composeTestRule.waitForIdle() + + assertThat(adaptiveTheme).isIn(listOf(AuthUITheme.Default, AuthUITheme.DefaultDark)) + } + + @Test + fun `Adaptive theme in configuration applies correctly`() { + var observedTheme: AuthUITheme? = null + var adaptiveThemeResolved: AuthUITheme? = null + + composeTestRule.setContent { + adaptiveThemeResolved = AuthUITheme.Adaptive + + CompositionLocalProvider( + LocalAuthUITheme provides adaptiveThemeResolved!! + ) { + observedTheme = LocalAuthUITheme.current + } + } + + composeTestRule.waitForIdle() + + assertThat(observedTheme?.colorScheme).isEqualTo(adaptiveThemeResolved?.colorScheme) + } + + // ======================================================================== + // Customization Tests + // ======================================================================== + + @Test + fun `Copy with custom provider button shape applies correctly`() { + val customShape = ShapeDefaults.ExtraLarge + val customTheme = AuthUITheme.Default.copy( + providerButtonShape = customShape + ) + + var observedProviderButtonShape: Shape? = null + + composeTestRule.setContent { + CompositionLocalProvider( + LocalAuthUITheme provides customTheme + ) { + observedProviderButtonShape = LocalAuthUITheme.current.providerButtonShape + } + } + + composeTestRule.waitForIdle() + + assertThat(observedProviderButtonShape).isEqualTo(customShape) + } + + @Test + fun `Copy preserves other properties`() { + val customStyles = mapOf( + "google.com" to AuthUITheme.ProviderStyle( + icon = null, + backgroundColor = Color.Red, + contentColor = Color.White + ) + ) + + val original = AuthUITheme.Default.copy( + providerButtonShape = RoundedCornerShape(12.dp), + providerStyles = customStyles + ) + + val copied = original.copy( + providerButtonShape = RoundedCornerShape(20.dp) + ) + + var observedProviderStyles: Map? = null + var observedProviderButtonShape: Shape? = null + + composeTestRule.setContent { + CompositionLocalProvider( + LocalAuthUITheme provides copied + ) { + observedProviderStyles = LocalAuthUITheme.current.providerStyles + observedProviderButtonShape = LocalAuthUITheme.current.providerButtonShape + } + } + + composeTestRule.waitForIdle() + + assertThat(observedProviderButtonShape).isEqualTo(RoundedCornerShape(20.dp)) + assertThat(observedProviderStyles).containsKey("google.com") + assertThat(observedProviderStyles?.get("google.com")?.backgroundColor).isEqualTo(Color.Red) + } + + // ======================================================================== + // fromMaterialTheme Tests + // ======================================================================== + + @Test + fun `fromMaterialTheme inherits MaterialTheme values`() { val appLightColorScheme = lightColorScheme( primary = Color(0xFF6650a4), secondary = Color(0xFF625b71), @@ -68,14 +346,78 @@ class AuthUIThemeTest { AuthUITheme( theme = AuthUITheme.fromMaterialTheme() ) { - assertThat(MaterialTheme.colorScheme) - .isEqualTo(appLightColorScheme) - assertThat(MaterialTheme.typography) - .isEqualTo(appTypography) - assertThat(MaterialTheme.shapes) - .isEqualTo(appShapes) + assertThat(MaterialTheme.colorScheme).isEqualTo(appLightColorScheme) + assertThat(MaterialTheme.typography).isEqualTo(appTypography) + assertThat(MaterialTheme.shapes).isEqualTo(appShapes) + } + } + } + } + + @Test + fun `fromMaterialTheme inherits all properties completely`() { + val customColorScheme = lightColorScheme( + primary = Color(0xFFFF0000), + background = Color(0xFFFFFFFF) + ) + val customTypography = Typography( + bodyLarge = TextStyle(fontSize = 18.sp) + ) + val customShapes = Shapes( + small = RoundedCornerShape(8.dp) + ) + + var observedColorScheme: ColorScheme? = null + var observedTypography: Typography? = null + var observedShapes: Shapes? = null + + composeTestRule.setContent { + MaterialTheme( + colorScheme = customColorScheme, + typography = customTypography, + shapes = customShapes + ) { + val theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = RoundedCornerShape(16.dp) + ) + + CompositionLocalProvider( + LocalAuthUITheme provides theme + ) { + observedColorScheme = LocalAuthUITheme.current.colorScheme + observedTypography = LocalAuthUITheme.current.typography + observedShapes = LocalAuthUITheme.current.shapes } } } + + composeTestRule.waitForIdle() + + assertThat(observedColorScheme?.primary).isEqualTo(Color(0xFFFF0000)) + assertThat(observedTypography).isEqualTo(customTypography) + assertThat(observedShapes).isEqualTo(customShapes) + } + + @Test + fun `fromMaterialTheme with custom provider button shape`() { + val customShape = RoundedCornerShape(16.dp) + + var observedProviderButtonShape: Shape? = null + + composeTestRule.setContent { + MaterialTheme { + val theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = customShape + ) + + AuthUITheme(theme = theme) { + observedProviderButtonShape = LocalAuthUITheme.current.providerButtonShape + } + } + } + + composeTestRule.waitForIdle() + + assertThat(observedProviderButtonShape).isEqualTo(customShape) } } diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/theme/ProviderButtonShapeCustomizationTest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/theme/ProviderButtonShapeCustomizationTest.kt new file mode 100644 index 000000000..6c01a716f --- /dev/null +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/theme/ProviderButtonShapeCustomizationTest.kt @@ -0,0 +1,216 @@ +/* + * 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.configuration.theme + +import android.content.Context +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider +import com.firebase.ui.auth.ui.components.AuthProviderButton +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 + +/** + * Tests for provider button shape customization features. + * + * Verifies that: + * - Custom shapes can be set globally for all provider buttons + * - Individual provider styles can override the global shape + * - Shapes properly inherit through the composition local system + */ +@Config(sdk = [34]) +@RunWith(RobolectricTestRunner::class) +class ProviderButtonShapeCustomizationTest { + + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var context: Context + + @Before + fun setUp() { + context = ApplicationProvider.getApplicationContext() + } + + @Test + fun `providerButtonShape applies to all provider buttons`() { + val customShape = RoundedCornerShape(16.dp) + val theme = AuthUITheme( + colorScheme = lightColorScheme(), + typography = androidx.compose.material3.Typography(), + shapes = androidx.compose.material3.Shapes(), + providerButtonShape = customShape + ) + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + val currentTheme = LocalAuthUITheme.current + assertThat(currentTheme.providerButtonShape).isEqualTo(customShape) + } + } + } + + @Test + fun `individual provider style shape overrides global providerButtonShape`() { + val globalShape = RoundedCornerShape(8.dp) + val googleSpecificShape = RoundedCornerShape(24.dp) + + val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = googleSpecificShape + ) + ) + + val theme = AuthUITheme( + colorScheme = lightColorScheme(), + typography = androidx.compose.material3.Typography(), + shapes = androidx.compose.material3.Shapes(), + providerButtonShape = globalShape, + providerStyles = customProviderStyles + ) + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + val currentTheme = LocalAuthUITheme.current + val googleStyle = currentTheme.providerStyles["google.com"] + assertThat(googleStyle).isNotNull() + assertThat(googleStyle?.shape).isEqualTo(googleSpecificShape) + } + } + } + + @Test + fun `fromMaterialTheme accepts providerButtonShape parameter`() { + val customShape = RoundedCornerShape(12.dp) + + composeTestRule.setContent { + val theme = AuthUITheme.fromMaterialTheme( + providerButtonShape = customShape + ) + + assertThat(theme.providerButtonShape).isEqualTo(customShape) + } + } + + @Test + fun `ProviderStyleDefaults are publicly accessible`() { + // Verify all default provider styles are accessible + assertThat(ProviderStyleDefaults.Google).isNotNull() + assertThat(ProviderStyleDefaults.Facebook).isNotNull() + assertThat(ProviderStyleDefaults.Twitter).isNotNull() + assertThat(ProviderStyleDefaults.Github).isNotNull() + assertThat(ProviderStyleDefaults.Email).isNotNull() + assertThat(ProviderStyleDefaults.Phone).isNotNull() + assertThat(ProviderStyleDefaults.Anonymous).isNotNull() + assertThat(ProviderStyleDefaults.Microsoft).isNotNull() + assertThat(ProviderStyleDefaults.Yahoo).isNotNull() + assertThat(ProviderStyleDefaults.Apple).isNotNull() + } + + @Test + fun `ProviderStyle is a data class with copy method`() { + val original = ProviderStyleDefaults.Google + val customShape = RoundedCornerShape(20.dp) + + val modified = original.copy(shape = customShape) + + assertThat(modified.shape).isEqualTo(customShape) + assertThat(modified.backgroundColor).isEqualTo(original.backgroundColor) + assertThat(modified.contentColor).isEqualTo(original.contentColor) + assertThat(modified.icon).isEqualTo(original.icon) + } + + @Test + fun `AuthProviderButton respects theme providerButtonShape`() { + val customShape = RoundedCornerShape(16.dp) + val theme = AuthUITheme( + colorScheme = lightColorScheme(), + typography = androidx.compose.material3.Typography(), + shapes = androidx.compose.material3.Shapes(), + providerButtonShape = customShape + ) + + val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) + val stringProvider = DefaultAuthUIStringProvider(context) + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + AuthProviderButton( + provider = provider, + onClick = { }, + stringProvider = stringProvider + ) + // Button should use customShape internally + val currentTheme = LocalAuthUITheme.current + assertThat(currentTheme.providerButtonShape).isEqualTo(customShape) + } + } + } + + @Test + fun `default shape is used when no custom shape is provided`() { + val theme = AuthUITheme( + colorScheme = lightColorScheme(), + typography = androidx.compose.material3.Typography(), + shapes = androidx.compose.material3.Shapes(), + providerButtonShape = null + ) + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + val currentTheme = LocalAuthUITheme.current + assertThat(currentTheme.providerButtonShape).isNull() + } + } + } + + @Test + fun `custom provider styles with null shapes use global providerButtonShape`() { + val globalShape = RoundedCornerShape(12.dp) + + val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = null // Explicitly set to null to inherit global shape + ) + ) + + val theme = AuthUITheme( + colorScheme = lightColorScheme(), + typography = androidx.compose.material3.Typography(), + shapes = androidx.compose.material3.Shapes(), + providerButtonShape = globalShape, + providerStyles = customProviderStyles + ) + + composeTestRule.setContent { + AuthUITheme(theme = theme) { + val currentTheme = LocalAuthUITheme.current + val googleStyle = currentTheme.providerStyles["google.com"] + // Shape should be null in the style, but button will use global shape + assertThat(googleStyle?.shape).isNull() + assertThat(currentTheme.providerButtonShape).isEqualTo(globalShape) + } + } + } +} diff --git a/auth/src/test/java/com/firebase/ui/auth/ui/components/AuthProviderButtonTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/components/AuthProviderButtonTest.kt index d45176e94..a91518522 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/components/AuthProviderButtonTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/components/AuthProviderButtonTest.kt @@ -34,6 +34,7 @@ import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider import com.firebase.ui.auth.configuration.theme.AuthUIAsset import com.firebase.ui.auth.configuration.theme.AuthUITheme +import com.firebase.ui.auth.configuration.theme.ProviderStyleDefaults import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Rule @@ -370,7 +371,7 @@ class AuthProviderButtonTest { @Test fun `AuthProviderButton uses custom style when provided`() { val provider = AuthProvider.Google(scopes = emptyList(), serverClientId = null) - val customStyle = AuthUITheme.Default.providerStyles[Provider.FACEBOOK.id] + val customStyle = ProviderStyleDefaults.Facebook composeTestRule.setContent { AuthProviderButton( @@ -385,10 +386,12 @@ class AuthProviderButtonTest { .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]) + val resolvedStyle = resolveProviderStyle(provider, customStyle, ProviderStyleDefaults.default, null) + assertThat(resolvedStyle.backgroundColor).isEqualTo(customStyle.backgroundColor) + assertThat(resolvedStyle.contentColor).isEqualTo(customStyle.contentColor) + assertThat(resolvedStyle.icon).isEqualTo(customStyle.icon) + assertThat(resolvedStyle.backgroundColor) + .isNotEqualTo(ProviderStyleDefaults.Google.backgroundColor) } @Test @@ -423,14 +426,14 @@ class AuthProviderButtonTest { composeTestRule.onNodeWithContentDescription(customLabel) .assertIsDisplayed() - val resolvedStyle = resolveProviderStyle(provider, null) + val resolvedStyle = resolveProviderStyle(provider, null, ProviderStyleDefaults.default, 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) + val googleDefaultStyle = ProviderStyleDefaults.Google + assertThat(resolvedStyle.backgroundColor).isNotEqualTo(googleDefaultStyle.backgroundColor) } @Test @@ -458,11 +461,10 @@ class AuthProviderButtonTest { composeTestRule.onNodeWithText(customLabel) .assertIsDisplayed() - val resolvedStyle = resolveProviderStyle(provider, null) - val googleDefaultStyle = AuthUITheme.Default.providerStyles["google.com"] + val resolvedStyle = resolveProviderStyle(provider, null, ProviderStyleDefaults.default, null) + val googleDefaultStyle = ProviderStyleDefaults.Google - assertThat(googleDefaultStyle).isNotNull() - assertThat(resolvedStyle.backgroundColor).isEqualTo(googleDefaultStyle!!.backgroundColor) + assertThat(resolvedStyle.backgroundColor).isEqualTo(googleDefaultStyle.backgroundColor) assertThat(resolvedStyle.contentColor).isEqualTo(googleDefaultStyle.contentColor) assertThat(resolvedStyle.icon).isEqualTo(googleDefaultStyle.icon) } @@ -506,7 +508,7 @@ class AuthProviderButtonTest { contentColor = customContentColor ) - val resolvedStyle = resolveProviderStyle(provider, null) + val resolvedStyle = resolveProviderStyle(provider, null, ProviderStyleDefaults.default, null) assertThat(resolvedStyle).isNotNull() assertThat(resolvedStyle.backgroundColor).isEqualTo(customColor) @@ -526,7 +528,7 @@ class AuthProviderButtonTest { contentColor = Color.White ) - val resolvedStyle = resolveProviderStyle(provider, null) + val resolvedStyle = resolveProviderStyle(provider, null, ProviderStyleDefaults.default, null) assertThat(resolvedStyle).isNotNull() assertThat(resolvedStyle.icon).isNull() @@ -538,9 +540,10 @@ class AuthProviderButtonTest { fun `resolveProviderStyle provides fallback for unknown provider`() { val provider = object : AuthProvider(providerId = "unknown.provider", providerName = "Generic Provider") {} - val resolvedStyle = resolveProviderStyle(provider, null) + val resolvedStyle = resolveProviderStyle(provider, null, ProviderStyleDefaults.default, null) assertThat(resolvedStyle).isNotNull() - assertThat(resolvedStyle).isEqualTo(AuthUITheme.ProviderStyle.Empty) + assertThat(resolvedStyle.backgroundColor).isEqualTo(AuthUITheme.ProviderStyle.Empty.backgroundColor) + assertThat(resolvedStyle.contentColor).isEqualTo(AuthUITheme.ProviderStyle.Empty.contentColor) } } \ No newline at end of file diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/ui/AccessibilityTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/ui/AccessibilityTest.kt index 34e879b36..f7ca19e25 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/ui/AccessibilityTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/ui/AccessibilityTest.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.LayoutDirection import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.authUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider @@ -28,6 +29,7 @@ import com.firebase.ui.auth.ui.components.AuthTextField import com.firebase.ui.auth.ui.components.CountrySelector import com.firebase.ui.auth.ui.components.QrCodeImage import com.firebase.ui.auth.ui.components.VerificationCodeInputField +import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen import com.firebase.ui.auth.ui.screens.email.SignInUI import com.firebase.ui.auth.ui.screens.phone.EnterPhoneNumberUI import com.firebase.ui.auth.util.CountryUtils diff --git a/flutterfire/.idea/workspace.xml b/flutterfire/.idea/workspace.xml new file mode 100644 index 000000000..faa0522ee --- /dev/null +++ b/flutterfire/.idea/workspace.xml @@ -0,0 +1,79 @@ + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "demolaf" + } +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/firebase/flutterfire.git", + "accountId": "d8965a4c-722f-468c-8af8-e2f8308205d9" + } +} + { + "associatedIndex": 8 +} + + + + 'firebase_auth_example'.executor": "Run", + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true", + "RunOnceActivity.cidr.known.project.marker": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.readMode.enableVisualFormatting": "true", + "cf.first.check.clang-format": "false", + "cidr.known.project.marker": "true", + "dart.analysis.tool.window.visible": "false", + "git-widget-placeholder": "main", + "io.flutter.project.isFirstOpen": "true", + "io.flutter.reload.alreadyRun": "true", + "last_opened_file_path": "/Users/ademolafadumo/Invertase/FirebaseUI-Android/flutterfire", + "settings.editor.selected.configurable": "flutter.settings", + "show.migrate.to.gradle.popup": "false" + } +}]]> + + + + + + + 1757509591247 + + + + + + + + + + \ No newline at end of file