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/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt b/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt index a22e6028a..5a19beac4 100644 --- a/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/MainActivity.kt @@ -36,7 +36,7 @@ import com.google.firebase.FirebaseApp */ class MainActivity : ComponentActivity() { companion object { - private const val USE_AUTH_EMULATOR = false + private const val USE_AUTH_EMULATOR = true private const val AUTH_EMULATOR_HOST = "10.0.2.2" private const val AUTH_EMULATOR_PORT = 9099 } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 515bb8e83..b0b680c7f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,10 +1,10 @@ FirebaseUI Demo - flutterfire-e2e-tests.firebaseapp.com + CHANGE-HERE - 128693022464535 - fb128693022464535 - 16dbbdf0cfb309034a6ad98ac2a21688 + APP-ID + fbAPP-ID + CHANGE-HERE \ No newline at end of file diff --git a/auth/README.md b/auth/README.md index e44ac7b6f..ecc3208aa 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 @@ -147,10 +161,10 @@ class MainActivity : ComponentActivity() { setContent { MyAppTheme { val configuration = authUIConfiguration { - providers = listOf( - AuthProvider.Email(), - AuthProvider.Google() - ) + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } } FirebaseAuthScreen( @@ -263,12 +277,12 @@ val authUI = FirebaseAuthUI.create(auth = customAuth) ```kotlin val configuration = authUIConfiguration { - // Required: List of authentication providers - providers = listOf( - AuthProvider.Email(), - AuthProvider.Google(), - AuthProvider.Phone() - ) + // Required: Authentication providers + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + provider(AuthProvider.Phone()) + } // Optional: Theme configuration theme = AuthUITheme.fromMaterialTheme() @@ -353,12 +367,19 @@ override fun onDestroy() { sealed class AuthState { object Idle : AuthState() data class Loading(val message: String?) : AuthState() - data class Success(val result: AuthResult, val isNewUser: Boolean) : AuthState() + data class Success(val result: AuthResult?, val user: FirebaseUser, val isNewUser: Boolean = false) : AuthState() data class Error(val exception: AuthException, val isRecoverable: Boolean) : AuthState() - data class RequiresMfa(val resolver: MultiFactorResolver) : AuthState() - data class RequiresEmailVerification(val user: FirebaseUser) : AuthState() - data class RequiresProfileCompletion(val user: FirebaseUser) : AuthState() + data class RequiresMfa(val resolver: MultiFactorResolver, val hint: String? = null) : AuthState() + data class RequiresEmailVerification(val user: FirebaseUser, val email: String) : AuthState() + data class RequiresProfileCompletion(val user: FirebaseUser, val missingFields: List = emptyList()) : AuthState() object Cancelled : AuthState() + object PasswordResetLinkSent : AuthState() + object EmailSignInLinkSent : AuthState() + data class SMSAutoVerified(val credential: PhoneAuthCredential) : AuthState() + data class PhoneNumberVerificationRequired( + val verificationId: String, + val forceResendingToken: PhoneAuthProvider.ForceResendingToken + ) : AuthState() } ``` @@ -433,7 +454,9 @@ val phoneProvider = AuthProvider.Phone( ) val configuration = authUIConfiguration { - providers = listOf(phoneProvider) + providers { + provider(phoneProvider) + } } ``` @@ -454,7 +477,9 @@ val googleProvider = AuthProvider.Google( ) val configuration = authUIConfiguration { - providers = listOf(googleProvider) + providers { + provider(googleProvider) + } } ``` @@ -475,7 +500,9 @@ val facebookProvider = AuthProvider.Facebook( ) val configuration = authUIConfiguration { - providers = listOf(facebookProvider) + providers { + provider(facebookProvider) + } } ``` @@ -533,13 +560,13 @@ val appleProvider = AuthProvider.Apple( ) val configuration = authUIConfiguration { - providers = listOf( - twitterProvider, - githubProvider, - microsoftProvider, - yahooProvider, - appleProvider - ) + providers { + provider(twitterProvider) + provider(githubProvider) + provider(microsoftProvider) + provider(yahooProvider) + provider(appleProvider) + } } ``` @@ -549,9 +576,9 @@ Enable anonymous authentication to let users use your app without signing in: ```kotlin val configuration = authUIConfiguration { - providers = listOf( - AuthProvider.Anonymous() - ) + providers { + provider(AuthProvider.Anonymous()) + } // Enable anonymous user upgrade isAnonymousUpgradeEnabled = true @@ -590,7 +617,9 @@ val lineProvider = AuthProvider.GenericOAuth( ) val configuration = authUIConfiguration { - providers = listOf(lineProvider) + providers { + provider(lineProvider) + } } ``` @@ -604,12 +633,12 @@ The high-level API provides a complete, opinionated authentication experience wi @Composable fun AuthenticationScreen() { val configuration = authUIConfiguration { - providers = listOf( - AuthProvider.Email(), - AuthProvider.Google(), - AuthProvider.Facebook(), - AuthProvider.Phone() - ) + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + provider(AuthProvider.Phone()) + } tosUrl = "https://example.com/terms" privacyPolicyUrl = "https://example.com/privacy" logo = Icons.Default.Lock @@ -649,6 +678,54 @@ fun AuthenticationScreen() { } ``` +**FirebaseAuthScreen Parameters:** + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `configuration` | `AuthUIConfiguration` | *Required* | Authentication configuration (providers, theme, etc.) | +| `onSignInSuccess` | `(AuthResult) -> Unit` | *Required* | Callback when sign-in succeeds | +| `onSignInFailure` | `(AuthException) -> Unit` | *Required* | Callback when sign-in fails | +| `onSignInCancelled` | `() -> Unit` | *Required* | Callback when user cancels authentication | +| `modifier` | `Modifier` | `Modifier` | Modifier for the composable | +| `authUI` | `FirebaseAuthUI` | `FirebaseAuthUI.getInstance()` | Custom FirebaseAuthUI instance (for multi-app support) | +| `emailLink` | `String?` | `null` | Email link for passwordless sign-in (see [Email Link Sign-In](#email-link-sign-in)) | +| `mfaConfiguration` | `MfaConfiguration` | `MfaConfiguration()` | MFA settings (see [Multi-Factor Authentication](#multi-factor-authentication)) | +| `authenticatedContent` | `@Composable ((AuthState, AuthSuccessUiContext) -> Unit)?` | `null` | Optional content to show after successful authentication | + +**Using authenticatedContent:** + +Show custom UI after authentication completes, before navigating away: + +```kotlin +FirebaseAuthScreen( + configuration = configuration, + onSignInSuccess = { result -> + // Called after authenticatedContent is dismissed + navigateToHome() + }, + onSignInFailure = { exception -> + showError(exception) + }, + onSignInCancelled = { + finish() + }, + authenticatedContent = { state, uiContext -> + // Show a welcome screen or profile completion UI + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Welcome, ${(state as? AuthState.Success)?.user?.displayName}!") + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { uiContext.onContinue() }) { + Text("Continue to App") + } + } + } +) +``` + ### Low-Level API (Advanced) For maximum control, use the `AuthFlowController`: @@ -662,7 +739,10 @@ class AuthActivity : ComponentActivity() { val authUI = FirebaseAuthUI.getInstance() val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email(), AuthProvider.Google()) + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } } controller = authUI.createAuthFlow(configuration) @@ -860,7 +940,9 @@ val mfaConfig = MfaConfiguration( ) val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email()) + providers { + provider(AuthProvider.Email()) + } isMfaEnabled = true } ``` @@ -970,17 +1052,179 @@ fun ManualMfaChallenge(resolver: MultiFactorResolver) { ## Theming & Customization -### Material Theme Integration +FirebaseUI Auth provides flexible theming options to match your app's design: + +- **`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 { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } + theme = AuthUITheme.Default // Light theme + // or + theme = AuthUITheme.DefaultDark // Dark theme +} +``` + +### Using Adaptive Theme (Recommended) -FirebaseUI automatically inherits your app's Material Theme: +`AuthUITheme.Adaptive` automatically switches between light and dark themes based on the system setting: + +```kotlin +val configuration = authUIConfiguration { + providers { + provider(AuthProvider.Email()) + provider(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 { + provider(AuthProvider.Google()) + provider(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 { + provider(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 { + provider(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 fun App() { MyAppTheme { // Your existing Material3 theme val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email()) - theme = AuthUITheme.fromMaterialTheme() // Inherits MyAppTheme + providers { + provider(AuthProvider.Email()) + } + theme = AuthUITheme.fromMaterialTheme() // Inherits colors, typography, shapes } FirebaseAuthScreen( @@ -991,9 +1235,23 @@ fun App() { } ``` -### Custom Theme +You can also customize while inheriting: + +```kotlin +val configuration = authUIConfiguration { + providers { + provider(AuthProvider.Google()) + provider(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,46 +1269,244 @@ val customTheme = AuthUITheme( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(8.dp), large = RoundedCornerShape(16.dp) - ) + ), + providerButtonShape = RoundedCornerShape(12.dp) ) val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email()) + providers { + provider(AuthProvider.Email()) + } theme = customTheme } ``` ### 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 { + provider(AuthProvider.Google()) + provider(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 { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + provider(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 { - providers = listOf(AuthProvider.Google(), AuthProvider.Facebook()) + providers { + provider(AuthProvider.Google()) + provider(AuthProvider.Facebook()) + } theme = customTheme } ``` +**Using `fromMaterialTheme()`:** + +```kotlin +val customProviderStyles = mapOf( + "google.com" to ProviderStyleDefaults.Google.copy( + shape = RoundedCornerShape(8.dp), + elevation = 4.dp + ) +) + +val configuration = authUIConfiguration { + providers { + provider(AuthProvider.Google()) + provider(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 { + provider(AuthProvider.Google()) // Uses custom shape (24.dp) + provider(AuthProvider.Facebook()) // Uses custom shape (8.dp) + provider(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 { + provider(AuthProvider.Email()) + provider(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 { + provider(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 { + provider(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 { + provider(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 @@ -1060,11 +1516,11 @@ Seamlessly upgrade anonymous users to permanent accounts: ```kotlin // 1. Configure anonymous authentication with upgrade enabled val configuration = authUIConfiguration { - providers = listOf( - AuthProvider.Anonymous(), - AuthProvider.Email(), - AuthProvider.Google() - ) + providers { + provider(AuthProvider.Anonymous()) + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } isAnonymousUpgradeEnabled = true } @@ -1095,7 +1551,9 @@ val emailProvider = AuthProvider.Email( ) val configuration = authUIConfiguration { - providers = listOf(emailProvider) + providers { + provider(emailProvider) + } } ``` @@ -1221,7 +1679,9 @@ Credential Manager is enabled by default. To disable: ```kotlin val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email()) + providers { + provider(AuthProvider.Email()) + } isCredentialManagerEnabled = false } ``` @@ -1293,7 +1753,9 @@ class SpanishStringProvider(context: Context) : AuthUIStringProvider { } val configuration = authUIConfiguration { - providers = listOf(AuthProvider.Email()) + providers { + provider(AuthProvider.Email()) + } stringProvider = SpanishStringProvider(context) locale = Locale("es", "ES") } @@ -1414,10 +1876,10 @@ signInLauncher.launch(signInIntent); ```kotlin // New approach with Composable val configuration = authUIConfiguration { - providers = listOf( - AuthProvider.Email(), - AuthProvider.Google() - ) + providers { + provider(AuthProvider.Email()) + provider(AuthProvider.Google()) + } theme = AuthUITheme.fromMaterialTheme() } 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/FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt index df5a49173..9f829a37f 100644 --- a/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/FirebaseAuthUI.kt @@ -19,13 +19,14 @@ import android.content.Intent import androidx.annotation.RestrictTo import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.auth_provider.signOutFromFacebook import com.firebase.ui.auth.configuration.auth_provider.signOutFromGoogle +import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth.AuthStateListener import com.google.firebase.auth.FirebaseUser -import com.google.firebase.auth.ktx.auth -import com.google.firebase.ktx.Firebase +import com.google.firebase.auth.auth import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -77,6 +78,9 @@ class FirebaseAuthUI private constructor( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) var testCredentialManagerProvider: AuthProvider.Google.CredentialManagerProvider? = null + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + var testLoginManagerProvider: AuthProvider.Facebook.LoginManagerProvider? = null + /** * Checks whether a user is currently signed in. * @@ -367,6 +371,7 @@ class FirebaseAuthUI private constructor( auth.signOut() .also { signOutFromGoogle(context) + signOutFromFacebook() } // Update state to idle (user signed out) 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/auth_provider/AuthProvider.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AuthProvider.kt index fb8e55775..1b659a8fc 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AuthProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/AuthProvider.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.annotation.RestrictTo import androidx.compose.ui.graphics.Color import androidx.core.net.toUri +import androidx.credentials.ClearCredentialStateRequest import androidx.credentials.CredentialManager import androidx.credentials.GetCredentialRequest import androidx.datastore.preferences.core.stringPreferencesKey @@ -568,6 +569,11 @@ abstract class AuthProvider(open val providerId: String, open val providerName: filterByAuthorizedAccounts: Boolean, autoSelectEnabled: Boolean ): GoogleSignInResult + + suspend fun clearCredentialState( + context: Context, + credentialManager: CredentialManager, + ) } /** @@ -604,6 +610,13 @@ abstract class AuthProvider(open val providerId: String, open val providerName: photoUrl = googleIdTokenCredential.profilePictureUri, ) } + + override suspend fun clearCredentialState( + context: Context, + credentialManager: CredentialManager, + ) { + credentialManager.clearCredentialState(ClearCredentialStateRequest()) + } } } @@ -655,21 +668,28 @@ abstract class AuthProvider(open val providerId: String, open val providerName: } /** - * An interface to wrap the static `FacebookAuthProvider.getCredential` method to make it testable. + * An interface to wrap Facebook LoginManager and credential operations to make them testable. * @suppress */ - internal interface CredentialProvider { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + interface LoginManagerProvider { fun getCredential(token: String): AuthCredential + fun logOut() } /** - * The default implementation of [CredentialProvider] that calls the static method. + * The default implementation of [LoginManagerProvider]. * @suppress */ - internal class DefaultCredentialProvider : CredentialProvider { + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + class DefaultLoginManagerProvider : LoginManagerProvider { override fun getCredential(token: String): AuthCredential { return FacebookAuthProvider.getCredential(token) } + + override fun logOut() { + com.facebook.login.LoginManager.getInstance().logOut() + } } /** diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt index 878fd03a5..8d4bae6d1 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/EmailAuthProvider+FirebaseAuthUI.kt @@ -16,6 +16,7 @@ package com.firebase.ui.auth.configuration.auth_provider import android.content.Context import android.net.Uri +import android.util.Log import com.firebase.ui.auth.R import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState @@ -23,10 +24,14 @@ import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.canUpgradeAnonymous import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.mergeProfile +import com.firebase.ui.auth.credentialmanager.PasswordCredentialCancelledException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialHandler import com.firebase.ui.auth.util.EmailLinkPersistenceManager import com.firebase.ui.auth.util.EmailLinkParser import com.firebase.ui.auth.util.PersistenceManager import com.firebase.ui.auth.util.SessionUtils +import com.firebase.ui.auth.util.SignInPreferenceManager import com.google.firebase.FirebaseApp import com.google.firebase.auth.ActionCodeSettings import com.google.firebase.auth.AuthCredential @@ -38,6 +43,7 @@ import com.google.firebase.auth.FirebaseAuthUserCollisionException import kotlinx.coroutines.CancellationException import kotlinx.coroutines.tasks.await +private const val TAG = "EmailAuthProvider" /** * Creates an email/password account or links the credential to an anonymous user. @@ -160,6 +166,37 @@ internal suspend fun FirebaseAuthUI.createOrLinkUserWithEmailAndPassword( mergeProfile(auth, name, null) } } + + // Save credentials to Credential Manager if enabled + if (config.isCredentialManagerEnabled) { + try { + val credentialHandler = PasswordCredentialHandler(context) + credentialHandler.savePassword(email, password) + Log.d(TAG, "Password credential saved successfully for: $email") + } catch (e: PasswordCredentialCancelledException) { + // User cancelled - this is fine, don't break the auth flow + Log.d(TAG, "User cancelled credential save for: $email") + } catch (e: PasswordCredentialException) { + // Failed to save - log but don't break the auth flow + Log.w(TAG, "Failed to save password credential for: $email", e) + } + } + + // Save sign-in preference for "Continue as..." feature + if (result != null) { + try { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = "password", + identifier = email + ) + Log.d(TAG, "Sign-in preference saved for: $email") + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + Log.w(TAG, "Failed to save sign-in preference for: $email", e) + } + } + updateAuthState(AuthState.Idle) return result } catch (e: FirebaseAuthUserCollisionException) { @@ -281,6 +318,7 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword( email: String, password: String, credentialForLinking: AuthCredential? = null, + skipCredentialSave: Boolean = false, ): AuthResult? { try { updateAuthState(AuthState.Loading("Signing in...")) @@ -361,7 +399,38 @@ internal suspend fun FirebaseAuthUI.signInWithEmailAndPassword( result } } - }.also { + }.also { result -> + // Save credentials to Credential Manager if enabled + // Skip if user signed in with a retrieved credential (already saved) + if (config.isCredentialManagerEnabled && result != null && !skipCredentialSave) { + try { + val credentialHandler = PasswordCredentialHandler(context) + credentialHandler.savePassword(email, password) + Log.d(TAG, "Password credential saved successfully for: $email") + } catch (e: PasswordCredentialCancelledException) { + // User cancelled - this is fine, don't break the auth flow + Log.d(TAG, "User cancelled credential save for: $email") + } catch (e: PasswordCredentialException) { + // Failed to save - log but don't break the auth flow + Log.w(TAG, "Failed to save password credential for: $email", e) + } + } + + // Save sign-in preference for "Continue as..." feature + if (result != null) { + try { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = "password", + identifier = email + ) + Log.d(TAG, "Sign-in preference saved for: $email") + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + Log.w(TAG, "Failed to save sign-in preference for: $email", e) + } + } + updateAuthState(AuthState.Idle) } } catch (e: FirebaseAuthMultiFactorException) { diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt index 9939a4b47..28ef45636 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProvider+FirebaseAuthUI.kt @@ -32,6 +32,7 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.util.EmailLinkPersistenceManager +import com.firebase.ui.auth.util.SignInPreferenceManager import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch @@ -143,7 +144,7 @@ internal suspend fun FirebaseAuthUI.signInWithFacebook( config: AuthUIConfiguration, provider: AuthProvider.Facebook, accessToken: AccessToken, - credentialProvider: AuthProvider.Facebook.CredentialProvider = AuthProvider.Facebook.DefaultCredentialProvider(), + credentialProvider: AuthProvider.Facebook.LoginManagerProvider = AuthProvider.Facebook.DefaultLoginManagerProvider(), ) { try { updateAuthState( @@ -158,6 +159,23 @@ internal suspend fun FirebaseAuthUI.signInWithFacebook( displayName = profileData?.displayName, photoUrl = profileData?.photoUrl, ) + + // Save sign-in preference for "Continue as..." feature + try { + val user = auth.currentUser + val identifier = user?.email + if (identifier != null) { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = provider.providerId, + identifier = identifier + ) + android.util.Log.d("FacebookAuthProvider", "Sign-in preference saved for: $identifier") + } + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + android.util.Log.w("FacebookAuthProvider", "Failed to save sign-in preference", e) + } } catch (e: AuthException.AccountLinkingRequiredException) { // Account collision occurred - save Facebook credential for linking after email link sign-in // This happens when a user tries to sign in with Facebook but an email link account exists @@ -192,3 +210,23 @@ internal suspend fun FirebaseAuthUI.signInWithFacebook( } } +/** + * Signs out the current user from Facebook. + * + * Invokes Facebook's LoginManager to log out the user from their Facebook session. + * This method silently catches and ignores any exceptions that may occur during the + * logout process to ensure the sign-out flow continues even if Facebook logout fails. + * + * This is typically called as part of the overall sign-out flow when a user signs out + * from Firebase Authentication. + */ +internal fun FirebaseAuthUI.signOutFromFacebook( + loginManagerProvider: AuthProvider.Facebook.LoginManagerProvider = AuthProvider.Facebook.DefaultLoginManagerProvider(), +) { + try { + if (Provider.fromId(getCurrentUser()?.providerId) != Provider.FACEBOOK) return + (testLoginManagerProvider ?: loginManagerProvider).logOut() + } catch (e: Exception) { + Log.e("FacebookAuthProvider", "Error during Facebook sign out", e) + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt index d42362692..2afdb3599 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProvider+FirebaseAuthUI.kt @@ -1,10 +1,10 @@ package com.firebase.ui.auth.configuration.auth_provider import android.content.Context +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.credentials.ClearCredentialStateRequest import androidx.credentials.CredentialManager import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException @@ -13,6 +13,7 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.util.EmailLinkPersistenceManager +import com.firebase.ui.auth.util.SignInPreferenceManager import com.google.android.gms.common.api.Scope import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException import kotlinx.coroutines.CancellationException @@ -149,6 +150,23 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle( displayName = result.displayName, photoUrl = result.photoUrl, ) + + // Save sign-in preference for "Continue as..." feature + try { + val user = auth.currentUser + val identifier = user?.email + if (identifier != null) { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = provider.providerId, + identifier = identifier + ) + Log.d("GoogleAuthProvider", "Sign-in preference saved for: $identifier") + } + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + android.util.Log.w("GoogleAuthProvider", "Failed to save sign-in preference", e) + } } catch (e: AuthException.AccountLinkingRequiredException) { // Account collision occurred - save Facebook credential for linking after email link sign-in // This happens when a user tries to sign in with Facebook but an email link account exists @@ -198,13 +216,17 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle( * * @param context Android context for Credential Manager */ -internal suspend fun signOutFromGoogle(context: Context) { +internal suspend fun FirebaseAuthUI.signOutFromGoogle( + context: Context, + credentialManagerProvider: AuthProvider.Google.CredentialManagerProvider = AuthProvider.Google.DefaultCredentialManagerProvider(), +) { try { - val credentialManager = CredentialManager.create(context) - credentialManager.clearCredentialState( - ClearCredentialStateRequest() + if (Provider.fromId(getCurrentUser()?.providerId) != Provider.GOOGLE) return + (testCredentialManagerProvider ?: credentialManagerProvider).clearCredentialState( + context = context, + credentialManager = CredentialManager.create(context) ) - } catch (_: Exception) { - + } catch (e: Exception) { + Log.e("GoogleAuthProvider", "Error during Google sign out", e) } } \ No newline at end of file diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt index 615aa6982..485065746 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProvider+FirebaseAuthUI.kt @@ -1,6 +1,7 @@ package com.firebase.ui.auth.configuration.auth_provider import android.app.Activity +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -9,6 +10,7 @@ import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration import com.firebase.ui.auth.configuration.auth_provider.AuthProvider.Companion.canUpgradeAnonymous +import com.firebase.ui.auth.util.SignInPreferenceManager import com.google.firebase.auth.FirebaseAuthUserCollisionException import com.google.firebase.auth.OAuthCredential import com.google.firebase.auth.OAuthProvider @@ -48,6 +50,7 @@ import kotlinx.coroutines.tasks.await */ @Composable internal fun FirebaseAuthUI.rememberOAuthSignInHandler( + context: Context, activity: Activity?, config: AuthUIConfiguration, provider: AuthProvider.OAuth, @@ -63,6 +66,7 @@ internal fun FirebaseAuthUI.rememberOAuthSignInHandler( coroutineScope.launch { try { signInWithProvider( + context = context, config = config, activity = activity, provider = provider @@ -119,6 +123,7 @@ internal fun FirebaseAuthUI.rememberOAuthSignInHandler( * @see signInAndLinkWithCredential */ internal suspend fun FirebaseAuthUI.signInWithProvider( + context: Context, config: AuthUIConfiguration, activity: Activity, provider: AuthProvider.OAuth, @@ -172,6 +177,24 @@ internal suspend fun FirebaseAuthUI.signInWithProvider( val credential = authResult?.credential as? OAuthCredential if (credential != null) { // The user is already signed in via startActivityForSignInWithProvider/startActivityForLinkWithProvider + + // Save sign-in preference for "Continue as..." feature + try { + val user = auth.currentUser + val identifier = user?.email + if (identifier != null) { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = provider.providerId, + identifier = identifier + ) + android.util.Log.d("OAuthProvider", "Sign-in preference saved for: $identifier (${provider.providerId})") + } + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + android.util.Log.w("OAuthProvider", "Failed to save sign-in preference", e) + } + // Just update state to Idle updateAuthState(AuthState.Idle) } else { diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt index 95dbdcf78..0be8ee8fa 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProvider+FirebaseAuthUI.kt @@ -1,10 +1,12 @@ package com.firebase.ui.auth.configuration.auth_provider import android.app.Activity +import android.content.Context import com.firebase.ui.auth.AuthException import com.firebase.ui.auth.AuthState import com.firebase.ui.auth.FirebaseAuthUI import com.firebase.ui.auth.configuration.AuthUIConfiguration +import com.firebase.ui.auth.util.SignInPreferenceManager import com.google.firebase.auth.AuthResult import com.google.firebase.auth.MultiFactorSession import com.google.firebase.auth.PhoneAuthCredential @@ -197,6 +199,7 @@ internal suspend fun FirebaseAuthUI.verifyPhoneNumber( * @throws AuthException.NetworkException if a network error occurs */ internal suspend fun FirebaseAuthUI.submitVerificationCode( + context: Context, config: AuthUIConfiguration, verificationId: String, code: String, @@ -206,6 +209,7 @@ internal suspend fun FirebaseAuthUI.submitVerificationCode( updateAuthState(AuthState.Loading("Submitting verification code...")) val credential = credentialProvider.getCredential(verificationId, code) return signInWithPhoneAuthCredential( + context = context, config = config, credential = credential ) @@ -288,15 +292,37 @@ internal suspend fun FirebaseAuthUI.submitVerificationCode( * @throws AuthException.NetworkException if a network error occurs */ internal suspend fun FirebaseAuthUI.signInWithPhoneAuthCredential( + context: Context, config: AuthUIConfiguration, credential: PhoneAuthCredential, ): AuthResult? { try { updateAuthState(AuthState.Loading("Signing in with phone...")) - return signInAndLinkWithCredential( + val result = signInAndLinkWithCredential( config = config, credential = credential, ) + + // Save sign-in preference for "Continue as..." feature + if (result != null) { + try { + val user = auth.currentUser + val identifier = user?.phoneNumber + if (identifier != null) { + SignInPreferenceManager.saveLastSignIn( + context = context, + providerId = "phone", + identifier = identifier + ) + android.util.Log.d("PhoneAuthProvider", "Sign-in preference saved for: $identifier") + } + } catch (e: Exception) { + // Failed to save preference - log but don't break auth flow + android.util.Log.w("PhoneAuthProvider", "Failed to save sign-in preference", e) + } + } + + return result } catch (e: CancellationException) { val cancelledException = AuthException.AuthCancelledException( message = "Sign in with phone was cancelled", diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt index f00cffee6..df71966ab 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/AuthUIStringProvider.kt @@ -97,6 +97,33 @@ interface AuthUIStringProvider { /** Button text for Yahoo sign-in option */ val signInWithYahoo: String + /** Button text for Google continue option */ + val continueWithGoogle: String + + /** Button text for Facebook continue option */ + val continueWithFacebook: String + + /** Button text for Twitter continue option */ + val continueWithTwitter: String + + /** Button text for Github continue option */ + val continueWithGithub: String + + /** Button text for Email continue option */ + val continueWithEmail: String + + /** Button text for Phone continue option */ + val continueWithPhone: String + + /** Button text for Apple continue option */ + val continueWithApple: String + + /** Button text for Microsoft continue option */ + val continueWithMicrosoft: String + + /** Button text for Yahoo continue option */ + val continueWithYahoo: String + /** Error message when email address field is empty */ val missingEmailAddress: String diff --git a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt index f6c3f03ad..a0e53e2a9 100644 --- a/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt +++ b/auth/src/main/java/com/firebase/ui/auth/configuration/string_provider/DefaultAuthUIStringProvider.kt @@ -80,6 +80,28 @@ class DefaultAuthUIStringProvider( override val signInWithYahoo: String get() = localizedContext.getString(R.string.fui_sign_in_with_yahoo) + /** + * Auth Provider "Continue With" Button Strings + */ + override val continueWithGoogle: String + get() = localizedContext.getString(R.string.fui_continue_with_google) + override val continueWithFacebook: String + get() = localizedContext.getString(R.string.fui_continue_with_facebook) + override val continueWithTwitter: String + get() = localizedContext.getString(R.string.fui_continue_with_twitter) + override val continueWithGithub: String + get() = localizedContext.getString(R.string.fui_continue_with_github) + override val continueWithEmail: String + get() = localizedContext.getString(R.string.fui_continue_with_email) + override val continueWithPhone: String + get() = localizedContext.getString(R.string.fui_continue_with_phone) + override val continueWithApple: String + get() = localizedContext.getString(R.string.fui_continue_with_apple) + override val continueWithMicrosoft: String + get() = localizedContext.getString(R.string.fui_continue_with_microsoft) + override val continueWithYahoo: String + get() = localizedContext.getString(R.string.fui_continue_with_yahoo) + /** * Email Validator Strings */ 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/credentialmanager/PasswordCredentialHandler.kt b/auth/src/main/java/com/firebase/ui/auth/credentialmanager/PasswordCredentialHandler.kt index 65e0ac0c0..c83f9a280 100644 --- a/auth/src/main/java/com/firebase/ui/auth/credentialmanager/PasswordCredentialHandler.kt +++ b/auth/src/main/java/com/firebase/ui/auth/credentialmanager/PasswordCredentialHandler.kt @@ -25,6 +25,24 @@ import androidx.credentials.exceptions.CreateCredentialException import androidx.credentials.exceptions.GetCredentialCancellationException import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException +import com.firebase.ui.auth.util.CredentialPersistenceManager + +/** + * Provider interface for obtaining CredentialManager instances. + * This allows test code to inject mock CredentialManager instances. + */ +interface CredentialManagerProvider { + fun getCredentialManager(context: Context): CredentialManager +} + +/** + * Default implementation that creates a real CredentialManager instance. + */ +class DefaultCredentialManagerProvider : CredentialManagerProvider { + override fun getCredentialManager(context: Context): CredentialManager { + return CredentialManager.create(context) + } +} /** * Handler for password credential operations using Android's Credential Manager. @@ -33,11 +51,53 @@ import androidx.credentials.exceptions.NoCredentialException * the system credential manager, which displays native UI prompts to the user. * * @property context The Android context used for credential operations + * @property provider Optional provider for testing purposes */ class PasswordCredentialHandler( - private val context: Context + private val context: Context, + provider: CredentialManagerProvider? = null ) { - private val credentialManager: CredentialManager = CredentialManager.create(context) + companion object { + /** + * Test-only provider for injecting mock CredentialManager instances. + * Set this in your test setup to override the default CredentialManager. + * + * Example: + * ``` + * PasswordCredentialHandler.testCredentialManagerProvider = object : CredentialManagerProvider { + * override fun getCredentialManager(context: Context) = mockCredentialManager + * } + * ``` + */ + @Volatile + var testCredentialManagerProvider: CredentialManagerProvider? = null + + /** + * Checks if credentials have been saved at least once. + * This prevents unnecessary credential retrieval attempts. + * + * @param context The Android context + * @return true if credentials have been saved, false otherwise + */ + suspend fun hasSavedCredentials(context: Context): Boolean { + return CredentialPersistenceManager.hasSavedCredentials(context) + } + + /** + * Clears the saved credentials flag. + * Useful for testing or when user signs out permanently. + * + * @param context The Android context + */ + suspend fun clearSavedCredentialsFlag(context: Context) { + CredentialPersistenceManager.clearSavedCredentialsFlag(context) + } + } + + private val credentialManager: CredentialManager = + provider?.getCredentialManager(context) + ?: testCredentialManagerProvider?.getCredentialManager(context) + ?: CredentialManager.create(context) /** * Saves a password credential to the system credential manager. @@ -62,6 +122,8 @@ class PasswordCredentialHandler( try { credentialManager.createCredential(context, request) + // Mark that credentials have been saved successfully + CredentialPersistenceManager.setCredentialsSaved(context) } catch (e: CreateCredentialCancellationException) { // User cancelled the save operation throw PasswordCredentialCancelledException("User cancelled password save operation", e) 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 29a406821..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 @@ -29,11 +30,13 @@ import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -44,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. @@ -67,6 +72,8 @@ import com.firebase.ui.auth.configuration.theme.AuthUITheme * @param enabled If the button is enabled. Defaults to true. * @param style Optional custom styling for the button. * @param stringProvider The [AuthUIStringProvider] for localized strings + * @param subtitle Optional subtitle text to display below the provider label (e.g., user email) + * @param label Optional custom label to override the default provider label * * @since 10.0.0 */ @@ -78,19 +85,32 @@ fun AuthProviderButton( enabled: Boolean = true, style: AuthUITheme.ProviderStyle? = null, stringProvider: AuthUIStringProvider, + subtitle: String? = null, + label: String? = null, + showAsContinue: Boolean = false, ) { val context = LocalContext.current - val providerStyle = resolveProviderStyle(provider, style) - val providerLabel = resolveProviderLabel(provider, stringProvider, context) + 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, - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues( + horizontal = 12.dp, + vertical = if (subtitle != null) 12.dp else 8.dp + ), colors = ButtonDefaults.buttonColors( containerColor = providerStyle.backgroundColor, contentColor = providerStyle.contentColor, ), - shape = providerStyle.shape, + shape = providerStyle.shape ?: RoundedCornerShape(4.dp), elevation = ButtonDefaults.buttonElevation( defaultElevation = providerStyle.elevation ), @@ -123,11 +143,30 @@ fun AuthProviderButton( } Spacer(modifier = Modifier.width(12.dp)) } - Text( - text = providerLabel, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + + if (subtitle != null) { + Column( + verticalArrangement = Arrangement.Center + ) { + Text( + text = providerLabel, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Text( + text = subtitle, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.bodySmall, + ) + } + } else { + Text( + text = providerLabel, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } } } } @@ -135,27 +174,47 @@ 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 is AuthProvider.Apple -> { @@ -163,22 +222,23 @@ internal fun resolveProviderLabel( if (provider.locale != null) { val appleLocale = java.util.Locale.forLanguageTag(provider.locale) val appleStringProvider = DefaultAuthUIStringProvider(context, appleLocale) - appleStringProvider.signInWithApple + if (showAsContinue) appleStringProvider.continueWithApple else appleStringProvider.signInWithApple } else { - stringProvider.signInWithApple + if (showAsContinue) stringProvider.continueWithApple else stringProvider.signInWithApple } } + else -> when (Provider.fromId(provider.providerId)) { - Provider.GOOGLE -> stringProvider.signInWithGoogle - Provider.FACEBOOK -> stringProvider.signInWithFacebook - Provider.TWITTER -> stringProvider.signInWithTwitter - Provider.GITHUB -> stringProvider.signInWithGithub - Provider.EMAIL -> stringProvider.signInWithEmail - Provider.PHONE -> stringProvider.signInWithPhone + Provider.GOOGLE -> if (showAsContinue) stringProvider.continueWithGoogle else stringProvider.signInWithGoogle + Provider.FACEBOOK -> if (showAsContinue) stringProvider.continueWithFacebook else stringProvider.signInWithFacebook + Provider.TWITTER -> if (showAsContinue) stringProvider.continueWithTwitter else stringProvider.signInWithTwitter + Provider.GITHUB -> if (showAsContinue) stringProvider.continueWithGithub else stringProvider.signInWithGithub + Provider.EMAIL -> if (showAsContinue) stringProvider.continueWithEmail else stringProvider.signInWithEmail + Provider.PHONE -> if (showAsContinue) stringProvider.continueWithPhone else stringProvider.signInWithPhone Provider.ANONYMOUS -> stringProvider.signInAnonymously - Provider.MICROSOFT -> stringProvider.signInWithMicrosoft - Provider.YAHOO -> stringProvider.signInWithYahoo - Provider.APPLE -> stringProvider.signInWithApple + Provider.MICROSOFT -> if (showAsContinue) stringProvider.continueWithMicrosoft else stringProvider.signInWithMicrosoft + Provider.YAHOO -> if (showAsContinue) stringProvider.continueWithYahoo else stringProvider.signInWithYahoo + Provider.APPLE -> if (showAsContinue) stringProvider.continueWithApple else stringProvider.signInWithApple null -> "Unknown Provider" } } diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt index 96c2e7978..6d4aef708 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialog.kt @@ -126,7 +126,11 @@ private fun getRecoveryMessage( ): String { return when (error) { is AuthException.NetworkException -> stringProvider.networkErrorRecoveryMessage - is AuthException.InvalidCredentialsException -> stringProvider.invalidCredentialsRecoveryMessage + is AuthException.InvalidCredentialsException -> { + // Use the actual error message from Firebase if available, otherwise fallback to generic message + error.message?.takeIf { it.isNotBlank() && it != "Invalid credentials provided" } + ?: stringProvider.invalidCredentialsRecoveryMessage + } is AuthException.UserNotFoundException -> stringProvider.userNotFoundRecoveryMessage is AuthException.WeakPasswordException -> { // Include specific reason if available diff --git a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt index 5c69b19bc..bf4c3b6a5 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/method_picker/AuthMethodPicker.kt @@ -18,11 +18,17 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -34,9 +40,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.firebase.ui.auth.R import com.firebase.ui.auth.configuration.auth_provider.AuthProvider +import com.firebase.ui.auth.configuration.auth_provider.Provider import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider import com.firebase.ui.auth.configuration.theme.AuthUIAsset import com.firebase.ui.auth.ui.components.AuthProviderButton +import com.firebase.ui.auth.util.SignInPreferenceManager /** * Renders the provider selection screen. @@ -59,6 +67,7 @@ import com.firebase.ui.auth.ui.components.AuthProviderButton * @param customLayout An optional custom layout composable for the provider buttons. * @param termsOfServiceUrl The URL for the Terms of Service. * @param privacyPolicyUrl The URL for the Privacy Policy. + * @param lastSignInPreference The last sign-in preference to show a "Continue as..." button. * * @since 10.0.0 */ @@ -71,6 +80,7 @@ fun AuthMethodPicker( customLayout: @Composable ((List, (AuthProvider) -> Unit) -> Unit)? = null, termsOfServiceUrl: String? = null, privacyPolicyUrl: String? = null, + lastSignInPreference: SignInPreferenceManager.SignInPreference? = null, ) { val context = LocalContext.current val inPreview = LocalInspectionMode.current @@ -103,6 +113,37 @@ fun AuthMethodPicker( .testTag("AuthMethodPicker LazyColumn"), horizontalAlignment = Alignment.CenterHorizontally, ) { + // Show "Continue as..." button if last sign-in preference exists + lastSignInPreference?.let { preference -> + val lastProvider = providers.find { it.providerId == preference.providerId } + if (lastProvider != null) { + item { + ContinueAsButton( + provider = lastProvider, + identifier = preference.identifier, + onClick = { onProviderSelected(lastProvider) } + ) + Spacer(modifier = Modifier.height(24.dp)) + + // Divider with "or" + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + HorizontalDivider(modifier = Modifier.weight(1f)) + Text( + text = stringProvider.orContinueWith, + modifier = Modifier.padding(horizontal = 8.dp), + style = MaterialTheme.typography.bodySmall + ) + HorizontalDivider(modifier = Modifier.weight(1f)) + } + Spacer(modifier = Modifier.height(24.dp)) + } + } + } + + // Show all providers itemsIndexed(providers) { index, provider -> Box( modifier = Modifier @@ -139,6 +180,33 @@ fun AuthMethodPicker( } } +/** + * A prominent "Continue as..." button that shows the last-used provider and identifier. + * + * @param provider The authentication provider + * @param identifier The user identifier (email, phone number, etc.) + * @param onClick Callback when the button is clicked + */ +@Composable +private fun ContinueAsButton( + provider: AuthProvider, + identifier: String?, + onClick: () -> Unit +) { + val stringProvider = LocalAuthUIStringProvider.current + + AuthProviderButton( + modifier = Modifier + .fillMaxWidth() + .testTag("ContinueAsButton"), + onClick = onClick, + provider = provider, + stringProvider = stringProvider, + subtitle = identifier, + showAsContinue = true + ) +} + @Preview(showBackground = true) @Composable fun PreviewAuthMethodPicker() { 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 6e9ed8492..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,12 +64,14 @@ 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 import com.firebase.ui.auth.ui.screens.email.EmailAuthScreen import com.firebase.ui.auth.ui.screens.phone.PhoneAuthScreen import com.firebase.ui.auth.util.EmailLinkPersistenceManager +import com.firebase.ui.auth.util.SignInPreferenceManager import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult import com.google.firebase.auth.MultiFactorResolver @@ -112,6 +117,13 @@ fun FirebaseAuthScreen( val pendingLinkingCredential = remember { mutableStateOf(null) } val pendingResolver = remember { mutableStateOf(null) } val emailLinkFromDifferentDevice = remember { mutableStateOf(null) } + val lastSignInPreference = + remember { mutableStateOf(null) } + + // Load last sign-in preference on launch + LaunchedEffect(authState) { + lastSignInPreference.value = SignInPreferenceManager.getLastSignIn(context) + } val anonymousProvider = configuration.providers.filterIsInstance().firstOrNull() @@ -155,6 +167,7 @@ fun FirebaseAuthScreen( val onSignInWithApple = appleProvider?.let { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -163,6 +176,7 @@ fun FirebaseAuthScreen( val onSignInWithGithub = githubProvider?.let { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -171,6 +185,7 @@ fun FirebaseAuthScreen( val onSignInWithMicrosoft = microsoftProvider?.let { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -179,6 +194,7 @@ fun FirebaseAuthScreen( val onSignInWithYahoo = yahooProvider?.let { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -187,6 +203,7 @@ fun FirebaseAuthScreen( val onSignInWithTwitter = twitterProvider?.let { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -195,6 +212,7 @@ fun FirebaseAuthScreen( val genericOAuthHandlers = genericOAuthProviders.associateWith { authUI.rememberOAuthSignInHandler( + context = context, activity = activity, config = configuration, provider = it @@ -203,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 @@ -211,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 -> @@ -222,6 +253,7 @@ fun FirebaseAuthScreen( logo = logoAsset, termsOfServiceUrl = configuration.tosUrl, privacyPolicyUrl = configuration.privacyPolicyUrl, + lastSignInPreference = lastSignInPreference.value, onProviderSelected = { provider -> when (provider) { is AuthProvider.Anonymous -> onSignInAnonymously?.invoke() @@ -320,6 +352,7 @@ fun FirebaseAuthScreen( coroutineScope.launch { try { authUI.signOut(context) + // Keep sign-in preference for "Continue as..." on next launch } catch (e: Exception) { onSignInFailure(AuthException.from(e)) } finally { @@ -432,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 @@ -474,6 +508,12 @@ fun FirebaseAuthScreen( if (state.user.uid != lastSuccessfulUserId.value) { onSignInSuccess(result) lastSuccessfulUserId.value = state.user.uid + + // Reload sign-in preference (may have been updated by provider) + coroutineScope.launch { + lastSignInPreference.value = + SignInPreferenceManager.getLastSignIn(context) + } } } @@ -560,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 } @@ -624,7 +664,7 @@ data class AuthSuccessUiContext( private fun SuccessDestination( authState: AuthState, stringProvider: AuthUIStringProvider, - uiContext: AuthSuccessUiContext + uiContext: AuthSuccessUiContext, ) { when (authState) { is AuthState.Success -> { @@ -669,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() @@ -702,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 @@ -734,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 3cc3030f2..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 @@ -35,6 +35,10 @@ import com.firebase.ui.auth.configuration.auth_provider.sendSignInLinkToEmail import com.firebase.ui.auth.configuration.auth_provider.signInWithEmailAndPassword import com.firebase.ui.auth.configuration.auth_provider.signInWithEmailLink import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider +import com.firebase.ui.auth.credentialmanager.PasswordCredentialCancelledException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialHandler +import com.firebase.ui.auth.credentialmanager.PasswordCredentialNotFoundException import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController import com.google.firebase.auth.AuthCredential import com.google.firebase.auth.AuthResult @@ -95,6 +99,7 @@ class EmailAuthContentState( val onConfirmPasswordChange: (String) -> Unit, val displayName: String, val onDisplayNameChange: (String) -> Unit, + val onRetrievedCredential: (Pair) -> Unit, val onSignInClick: () -> Unit, val onSignInEmailLinkClick: () -> Unit, val onSignUpClick: () -> Unit, @@ -163,6 +168,9 @@ fun EmailAuthScreen( val resetLinkSent = authState is AuthState.PasswordResetLinkSent val emailSignInLinkSent = authState is AuthState.EmailSignInLinkSent + // Track if credentials were retrieved from Credential Manager + val retrievedCredential = remember { mutableStateOf?>(null) } + LaunchedEffect(authState) { Log.d("EmailAuthScreen", "Current state: $authState") when (val state = authState) { @@ -237,15 +245,24 @@ fun EmailAuthScreen( onDisplayNameChange = { displayName -> displayNameValue.value = displayName }, + onRetrievedCredential = { credential -> + retrievedCredential.value = credential + }, onSignInClick = { coroutineScope.launch { try { + // Check if user is signing in with retrieved credentials + val isUsingRetrievedCredential = retrievedCredential.value?.let { (email, password) -> + email == emailTextValue.value && password == passwordTextValue.value + } ?: false + authUI.signInWithEmailAndPassword( context = context, config = configuration, email = emailTextValue.value, password = passwordTextValue.value, credentialForLinking = authCredentialForLinking, + skipCredentialSave = isUsingRetrievedCredential ) } catch (e: Exception) { onError(AuthException.from(e)) @@ -350,6 +367,7 @@ private fun DefaultEmailAuthContent( password = state.password, onEmailChange = state.onEmailChange, onPasswordChange = state.onPasswordChange, + onRetrievedCredential = state.onRetrievedCredential, onSignInClick = state.onSignInClick, onGoToSignUp = state.onGoToSignUp, onGoToResetPassword = state.onGoToResetPassword, @@ -385,7 +403,8 @@ private fun DefaultEmailAuthContent( onPasswordChange = state.onPasswordChange, onConfirmPasswordChange = state.onConfirmPasswordChange, onSignUpClick = state.onSignUpClick, - onGoToSignIn = state.onGoToSignIn + onGoToSignIn = state.onGoToSignIn, + onNavigateBack = onCancel ) } @@ -397,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/SignInUI.kt b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt index 962be4160..eabde87a1 100644 --- a/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt +++ b/auth/src/main/java/com/firebase/ui/auth/ui/screens/email/SignInUI.kt @@ -14,6 +14,7 @@ package com.firebase.ui.auth.ui.screens.email +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -46,7 +47,9 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -65,6 +68,10 @@ import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvi import com.firebase.ui.auth.configuration.theme.AuthUITheme import com.firebase.ui.auth.configuration.validators.EmailValidator import com.firebase.ui.auth.configuration.validators.PasswordValidator +import com.firebase.ui.auth.credentialmanager.PasswordCredentialCancelledException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialException +import com.firebase.ui.auth.credentialmanager.PasswordCredentialHandler +import com.firebase.ui.auth.credentialmanager.PasswordCredentialNotFoundException import com.firebase.ui.auth.ui.components.AuthTextField import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController import com.firebase.ui.auth.ui.components.TermsAndPrivacyForm @@ -80,12 +87,14 @@ fun SignInUI( password: String, onEmailChange: (String) -> Unit, onPasswordChange: (String) -> Unit, + onRetrievedCredential: (Pair) -> Unit, onSignInClick: () -> Unit, onGoToSignUp: () -> Unit, onGoToResetPassword: () -> Unit, onGoToEmailLinkSignIn: () -> Unit, onNavigateBack: (() -> Unit)? = null, ) { + val context = LocalContext.current val provider = configuration.providers.filterIsInstance().first() val stringProvider = LocalAuthUIStringProvider.current val emailValidator = remember { EmailValidator(stringProvider) } @@ -102,6 +111,45 @@ fun SignInUI( } } + // Retrieve saved credentials when in SignIn mode + val credentialRetrievalAttempted = remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if (configuration.isCredentialManagerEnabled && + !credentialRetrievalAttempted.value && + PasswordCredentialHandler.hasSavedCredentials(context)) { + credentialRetrievalAttempted.value = true + + try { + val credentialHandler = PasswordCredentialHandler(context) + val credential = credentialHandler.getPassword() + + Log.d("EmailAuthScreen", "Retrieved credential for: ${credential.username}") + + // Auto-fill the email and password fields + onEmailChange(credential.username) + onPasswordChange(credential.password) + + emailValidator.validate(credential.username) + passwordValidator.validate(credential.password) + + // Store retrieved credential to compare later + onRetrievedCredential(Pair(credential.username, credential.password)) + + onSignInClick() + } catch (e: PasswordCredentialNotFoundException) { + Log.d("EmailAuthScreen", "No saved credentials found") + // No credentials saved - user will enter manually + } catch (e: PasswordCredentialCancelledException) { + Log.d("EmailAuthScreen", "User cancelled credential selection") + // User cancelled - let them enter manually + } catch (e: PasswordCredentialException) { + Log.w("EmailAuthScreen", "Failed to retrieve credentials", e) + // Failed to retrieve - let them enter manually + } + } + } + Scaffold( modifier = modifier, topBar = { @@ -289,6 +337,7 @@ fun PreviewSignInUI() { emailSignInLinkSent = false, onEmailChange = { email -> }, onPasswordChange = { password -> }, + onRetrievedCredential = { credential -> }, onSignInClick = {}, onGoToSignUp = {}, onGoToResetPassword = {}, 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 300b2112e..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 @@ -193,6 +193,7 @@ fun PhoneAuthScreen( coroutineScope.launch { try { authUI.signInWithPhoneAuthCredential( + context = context, config = configuration, credential = state.credential ) @@ -265,6 +266,7 @@ fun PhoneAuthScreen( try { verificationId.value?.let { id -> authUI.submitVerificationCode( + context = context, config = configuration, verificationId = id, code = verificationCodeValue.value @@ -344,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/main/java/com/firebase/ui/auth/util/CredentialPersistenceManager.kt b/auth/src/main/java/com/firebase/ui/auth/util/CredentialPersistenceManager.kt new file mode 100644 index 000000000..e62534409 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/util/CredentialPersistenceManager.kt @@ -0,0 +1,75 @@ +/* + * 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.util + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first + +private val Context.credentialDataStore: DataStore by preferencesDataStore( + name = "com.firebase.ui.auth.util.CredentialPersistenceManager" +) + +/** + * Manages persistence for credential manager state. + * + * This class tracks whether credentials have been saved to the Android Credential Manager + * to prevent unnecessary credential retrieval attempts when no credentials exist. + * + * @since 10.0.0 + */ +object CredentialPersistenceManager { + + private val KEY_HAS_SAVED_CREDENTIALS = booleanPreferencesKey("has_saved_credentials") + + /** + * Marks that credentials have been successfully saved to the credential manager. + * + * @param context The Android context + */ + suspend fun setCredentialsSaved(context: Context) { + context.credentialDataStore.edit { prefs -> + prefs[KEY_HAS_SAVED_CREDENTIALS] = true + } + } + + /** + * Checks if credentials have been saved at least once. + * This prevents unnecessary credential retrieval attempts. + * + * @param context The Android context + * @return true if credentials have been saved, false otherwise + */ + suspend fun hasSavedCredentials(context: Context): Boolean { + val prefs = context.credentialDataStore.data.first() + return prefs[KEY_HAS_SAVED_CREDENTIALS] ?: false + } + + /** + * Clears the saved credentials flag. + * Useful for testing or when user signs out permanently. + * + * @param context The Android context + */ + suspend fun clearSavedCredentialsFlag(context: Context) { + context.credentialDataStore.edit { prefs -> + prefs.remove(KEY_HAS_SAVED_CREDENTIALS) + } + } +} diff --git a/auth/src/main/java/com/firebase/ui/auth/util/SignInPreferenceManager.kt b/auth/src/main/java/com/firebase/ui/auth/util/SignInPreferenceManager.kt new file mode 100644 index 000000000..7bfcc10f4 --- /dev/null +++ b/auth/src/main/java/com/firebase/ui/auth/util/SignInPreferenceManager.kt @@ -0,0 +1,119 @@ +/* + * 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.util + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first + +private val Context.signInPreferenceDataStore: DataStore by preferencesDataStore( + name = "com.firebase.ui.auth.util.SignInPreferenceManager" +) + +/** + * Manages persistence for the last-used sign-in method. + * + * This class tracks which authentication provider was last used to sign in, + * along with the user identifier (email, phone number, etc.). This enables + * a better UX by showing "Continue as [identifier]" with the last-used provider + * prominently on the method picker screen. + * + * @since 10.0.0 + */ +object SignInPreferenceManager { + + private val KEY_LAST_PROVIDER_ID = stringPreferencesKey("last_provider_id") + private val KEY_LAST_IDENTIFIER = stringPreferencesKey("last_identifier") + private val KEY_LAST_TIMESTAMP = longPreferencesKey("last_timestamp") + + /** + * Saves the last-used sign-in method and user identifier. + * + * This should be called after a successful sign-in to track the user's + * preferred sign-in method. + * + * @param context The Android context + * @param providerId The provider ID (e.g., "google.com", "facebook.com", "password", "phone") + * @param identifier The user identifier (email for social/email auth, phone number for phone auth) + */ + suspend fun saveLastSignIn( + context: Context, + providerId: String, + identifier: String? + ) { + context.signInPreferenceDataStore.edit { prefs -> + prefs[KEY_LAST_PROVIDER_ID] = providerId + identifier?.let { prefs[KEY_LAST_IDENTIFIER] = it } + prefs[KEY_LAST_TIMESTAMP] = System.currentTimeMillis() + } + } + + /** + * Retrieves the last-used sign-in preference. + * + * @param context The Android context + * @return [SignInPreference] containing the last-used provider and identifier, or null if none + */ + suspend fun getLastSignIn(context: Context): SignInPreference? { + val prefs = context.signInPreferenceDataStore.data.first() + val providerId = prefs[KEY_LAST_PROVIDER_ID] + val identifier = prefs[KEY_LAST_IDENTIFIER] + val timestamp = prefs[KEY_LAST_TIMESTAMP] + + return if (providerId != null && timestamp != null) { + SignInPreference( + providerId = providerId, + identifier = identifier, + timestamp = timestamp + ) + } else { + null + } + } + + /** + * Clears the saved sign-in preference. + * + * This should be called when the user signs out permanently or + * when resetting authentication state. + * + * @param context The Android context + */ + suspend fun clearLastSignIn(context: Context) { + context.signInPreferenceDataStore.edit { prefs -> + prefs.remove(KEY_LAST_PROVIDER_ID) + prefs.remove(KEY_LAST_IDENTIFIER) + prefs.remove(KEY_LAST_TIMESTAMP) + } + } + + /** + * Data class representing a saved sign-in preference. + * + * @property providerId The provider ID (e.g., "google.com", "facebook.com", "password", "phone") + * @property identifier The user identifier (email, phone number, etc.), may be null + * @property timestamp The timestamp when this preference was saved + */ + data class SignInPreference( + val providerId: String, + val identifier: String?, + val timestamp: Long + ) +} diff --git a/auth/src/main/res/values-ar/strings.xml b/auth/src/main/res/values-ar/strings.xml index ba6e8ca42..dcf4ca88c 100755 --- a/auth/src/main/res/values-ar/strings.xml +++ b/auth/src/main/res/values-ar/strings.xml @@ -12,15 +12,24 @@ الهاتف البريد الإلكتروني تسجيل الدخول عبر Google + تسجيل الدخول عبر Google تسجيل الدخول عبر Facebook + تسجيل الدخول عبر Facebook تسجيل الدخول عبر Twitter + تسجيل الدخول عبر Twitter تسجيل الدخول باستخدام GitHub + تسجيل الدخول باستخدام GitHub تسجيل الدخول عبر البريد الإلكتروني + تسجيل الدخول عبر البريد الإلكتروني تسجيل الدخول عبر رقم الهاتف + تسجيل الدخول عبر رقم الهاتف المتابعة كضيف تسجيل الدخول باستخدام Apple + تسجيل الدخول باستخدام Apple تسجيل الدخول باستخدام Microsoft + تسجيل الدخول باستخدام Microsoft تسجيل الدخول باستخدام Yahoo + تسجيل الدخول باستخدام Yahoo التالي البريد الإلكتروني رقم الهاتف diff --git a/auth/src/main/res/values-b+es+419/strings.xml b/auth/src/main/res/values-b+es+419/strings.xml index cb5645e0b..017b893bd 100755 --- a/auth/src/main/res/values-b+es+419/strings.xml +++ b/auth/src/main/res/values-b+es+419/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-bg/strings.xml b/auth/src/main/res/values-bg/strings.xml index 12a0dfb60..8b9db9839 100755 --- a/auth/src/main/res/values-bg/strings.xml +++ b/auth/src/main/res/values-bg/strings.xml @@ -12,15 +12,24 @@ Телефон Имейл Вход с Google + Вход с Google Вход с Facebook + Вход с Facebook Вход с Twitter + Вход с Twitter Вход с GitHub + Вход с GitHub Вход с имейл + Вход с имейл Вход с телефон + Вход с телефон Продължаване като гост Вход с профил в Apple + Вход с профил в Apple Вход с профил в Microsoft + Вход с профил в Microsoft Вход с профил в Yahoo + Вход с профил в Yahoo Напред Имейл Телефонен номер diff --git a/auth/src/main/res/values-bn/strings.xml b/auth/src/main/res/values-bn/strings.xml index cf12fa4da..747f19c8c 100755 --- a/auth/src/main/res/values-bn/strings.xml +++ b/auth/src/main/res/values-bn/strings.xml @@ -12,15 +12,24 @@ ফোন ইমেল আইডি Google দিয়ে সাইন-ইন করুন + Google দিয়ে সাইন-ইন করুন Facebook দিয়ে সাইন-ইন করুন + Facebook দিয়ে সাইন-ইন করুন Twitter দিয়ে সাইন-ইন করুন + Twitter দিয়ে সাইন-ইন করুন GitHub ব্যবহার করে সাইন-ইন করুন + GitHub ব্যবহার করে সাইন-ইন করুন ইমেল দিয়ে সাইন-ইন করুন + ইমেল দিয়ে সাইন-ইন করুন ফোন দিয়ে সাইন-ইন করুন + ফোন দিয়ে সাইন-ইন করুন অতিথি হিসেবে চালিয়ে যান Apple-এর সাথে সাইন-ইন করুন + Apple-এর সাথে সাইন-ইন করুন Microsoft-এর সাথে সাইন-ইন করুন + Microsoft-এর সাথে সাইন-ইন করুন Yahoo-এর সাথে সাইন-ইন করুন + Yahoo-এর সাথে সাইন-ইন করুন পরবর্তী ইমেল ফোন নম্বর diff --git a/auth/src/main/res/values-ca/strings.xml b/auth/src/main/res/values-ca/strings.xml index 7579e8fd2..5b56f2489 100755 --- a/auth/src/main/res/values-ca/strings.xml +++ b/auth/src/main/res/values-ca/strings.xml @@ -12,15 +12,24 @@ Telèfon Adreça electrònica Inicia la sessió amb Google + Inicia la sessió amb Google Inicia la sessió amb Facebook + Inicia la sessió amb Facebook Inicia la sessió amb Twitter + Inicia la sessió amb Twitter Inicia la sessió amb GitHub + Inicia la sessió amb GitHub Inicia la sessió amb l\'adreça electrònica + Inicia la sessió amb l\'adreça electrònica Inicia la sessió amb el telèfon + Inicia la sessió amb el telèfon Continua com a convidat Inicia la sessió amb Apple + Inicia la sessió amb Apple Inicia la sessió amb Microsoft + Inicia la sessió amb Microsoft Inicia la sessió amb Yahoo + Inicia la sessió amb Yahoo Següent Adreça electrònica Número de telèfon diff --git a/auth/src/main/res/values-cs/strings.xml b/auth/src/main/res/values-cs/strings.xml index 24416214f..57afae344 100755 --- a/auth/src/main/res/values-cs/strings.xml +++ b/auth/src/main/res/values-cs/strings.xml @@ -12,15 +12,24 @@ Telefon E-mail Přihlásit se přes Google + Přihlásit se přes Google Přihlásit se přes Facebook + Přihlásit se přes Facebook Přihlásit se přes Twitter + Přihlásit se přes Twitter Přihlásit se přes GitHub + Přihlásit se přes GitHub Přihlásit se pomocí e-mailu + Přihlásit se pomocí e-mailu Přihlásit se pomocí telefonu + Přihlásit se pomocí telefonu Pokračovat jako host Přihlásit se účtem Apple + Přihlásit se účtem Apple Přihlásit se účtem Microsoft + Přihlásit se účtem Microsoft Přihlásit se účtem Yahoo + Přihlásit se účtem Yahoo Další E-mail Telefonní číslo diff --git a/auth/src/main/res/values-da/strings.xml b/auth/src/main/res/values-da/strings.xml index ffc351bbd..0b91742aa 100755 --- a/auth/src/main/res/values-da/strings.xml +++ b/auth/src/main/res/values-da/strings.xml @@ -12,15 +12,24 @@ Telefon Mail Log ind med Google + Log ind med Google Log ind med Facebook + Log ind med Facebook Log ind med Twitter + Log ind med Twitter Log ind med GitHub + Log ind med GitHub Log ind med mail + Log ind med mail Log ind med telefon + Log ind med telefon Fortsæt som gæst Log ind med Apple + Log ind med Apple Log ind med Microsoft + Log ind med Microsoft Log ind med Yahoo + Log ind med Yahoo Næste Mail Telefonnummer diff --git a/auth/src/main/res/values-de-rAT/strings.xml b/auth/src/main/res/values-de-rAT/strings.xml index f335dbae7..427f52319 100755 --- a/auth/src/main/res/values-de-rAT/strings.xml +++ b/auth/src/main/res/values-de-rAT/strings.xml @@ -12,15 +12,24 @@ Telefon E-Mail Über Google anmelden + Über Google anmelden Über Facebook anmelden + Über Facebook anmelden Über Twitter anmelden + Über Twitter anmelden Über GitHub anmelden + Über GitHub anmelden Mit einer E-Mail-Adresse anmelden + Mit einer E-Mail-Adresse anmelden Mit einer Telefonnummer anmelden + Mit einer Telefonnummer anmelden Als Gast fortfahren Über Apple anmelden + Über Apple anmelden Über Microsoft anmelden + Über Microsoft anmelden Über Yahoo anmelden + Über Yahoo anmelden Weiter E-Mail-Adresse Telefonnummer diff --git a/auth/src/main/res/values-de-rCH/strings.xml b/auth/src/main/res/values-de-rCH/strings.xml index 1bef81579..316f26f09 100755 --- a/auth/src/main/res/values-de-rCH/strings.xml +++ b/auth/src/main/res/values-de-rCH/strings.xml @@ -12,15 +12,24 @@ Telefon E-Mail Über Google anmelden + Über Google anmelden Über Facebook anmelden + Über Facebook anmelden Über Twitter anmelden + Über Twitter anmelden Über GitHub anmelden + Über GitHub anmelden Mit einer E-Mail-Adresse anmelden + Mit einer E-Mail-Adresse anmelden Mit einer Telefonnummer anmelden + Mit einer Telefonnummer anmelden Als Gast fortfahren Über Apple anmelden + Über Apple anmelden Über Microsoft anmelden + Über Microsoft anmelden Über Yahoo anmelden + Über Yahoo anmelden Weiter E-Mail-Adresse Telefonnummer diff --git a/auth/src/main/res/values-de/strings.xml b/auth/src/main/res/values-de/strings.xml index 5d5d6959b..e7c0a9bd4 100755 --- a/auth/src/main/res/values-de/strings.xml +++ b/auth/src/main/res/values-de/strings.xml @@ -12,15 +12,24 @@ Telefon E-Mail Über Google anmelden + Über Google anmelden Über Facebook anmelden + Über Facebook anmelden Über Twitter anmelden + Über Twitter anmelden Über GitHub anmelden + Über GitHub anmelden Mit einer E-Mail-Adresse anmelden + Mit einer E-Mail-Adresse anmelden Mit einer Telefonnummer anmelden + Mit einer Telefonnummer anmelden Als Gast fortfahren Über Apple anmelden + Über Apple anmelden Über Microsoft anmelden + Über Microsoft anmelden Über Yahoo anmelden + Über Yahoo anmelden Weiter E-Mail-Adresse Telefonnummer diff --git a/auth/src/main/res/values-el/strings.xml b/auth/src/main/res/values-el/strings.xml index b2a60e94a..7a1872d87 100755 --- a/auth/src/main/res/values-el/strings.xml +++ b/auth/src/main/res/values-el/strings.xml @@ -12,15 +12,24 @@ Τηλέφωνο Ηλεκτρονικό ταχυδρομείο Σύνδεση μέσω Google + Σύνδεση μέσω Google Σύνδεση μέσω Facebook + Σύνδεση μέσω Facebook Σύνδεση μέσω Twitter + Σύνδεση μέσω Twitter Σύνδεση μέσω GitHub + Σύνδεση μέσω GitHub Σύνδεση μέσω ηλεκτρονικού ταχυδρομείου + Σύνδεση μέσω ηλεκτρονικού ταχυδρομείου Σύνδεση μέσω τηλεφώνου + Σύνδεση μέσω τηλεφώνου Συνέχεια ως επισκέπτης Σύνδεση μέσω Apple + Σύνδεση μέσω Apple Σύνδεση μέσω Microsoft + Σύνδεση μέσω Microsoft Σύνδεση μέσω Yahoo + Σύνδεση μέσω Yahoo Επόμενο Ηλεκτρονικό ταχυδρομείο Αριθμός τηλεφώνου diff --git a/auth/src/main/res/values-en-rAU/strings.xml b/auth/src/main/res/values-en-rAU/strings.xml index f5fa2e477..33bd778b7 100755 --- a/auth/src/main/res/values-en-rAU/strings.xml +++ b/auth/src/main/res/values-en-rAU/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rCA/strings.xml b/auth/src/main/res/values-en-rCA/strings.xml index 38867fd9c..9534d4b43 100755 --- a/auth/src/main/res/values-en-rCA/strings.xml +++ b/auth/src/main/res/values-en-rCA/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rGB/strings.xml b/auth/src/main/res/values-en-rGB/strings.xml index 62a4b3d9a..f86d87442 100755 --- a/auth/src/main/res/values-en-rGB/strings.xml +++ b/auth/src/main/res/values-en-rGB/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rIE/strings.xml b/auth/src/main/res/values-en-rIE/strings.xml index eb97f5eb1..5d7f7b2f3 100755 --- a/auth/src/main/res/values-en-rIE/strings.xml +++ b/auth/src/main/res/values-en-rIE/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rIN/strings.xml b/auth/src/main/res/values-en-rIN/strings.xml index eb97f5eb1..5d7f7b2f3 100755 --- a/auth/src/main/res/values-en-rIN/strings.xml +++ b/auth/src/main/res/values-en-rIN/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rSG/strings.xml b/auth/src/main/res/values-en-rSG/strings.xml index eb97f5eb1..5d7f7b2f3 100755 --- a/auth/src/main/res/values-en-rSG/strings.xml +++ b/auth/src/main/res/values-en-rSG/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-en-rZA/strings.xml b/auth/src/main/res/values-en-rZA/strings.xml index eb97f5eb1..5d7f7b2f3 100755 --- a/auth/src/main/res/values-en-rZA/strings.xml +++ b/auth/src/main/res/values-en-rZA/strings.xml @@ -12,15 +12,24 @@ Phone Email Sign in with Google + Continue with Google Sign in with Facebook + Continue with Facebook Sign in with X + Continue with X Sign in with GitHub + Continue with GitHub Sign in with email + Continue with email Sign in with phone + Continue with phone Continue as a guest Sign in with Apple + Continue with Apple Sign in with Microsoft + Continue with Microsoft Sign in with Yahoo + Continue with Yahoo Next Email Phone number diff --git a/auth/src/main/res/values-es-rAR/strings.xml b/auth/src/main/res/values-es-rAR/strings.xml index 5a882557c..df6acc012 100755 --- a/auth/src/main/res/values-es-rAR/strings.xml +++ b/auth/src/main/res/values-es-rAR/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rBO/strings.xml b/auth/src/main/res/values-es-rBO/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rBO/strings.xml +++ b/auth/src/main/res/values-es-rBO/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rCL/strings.xml b/auth/src/main/res/values-es-rCL/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rCL/strings.xml +++ b/auth/src/main/res/values-es-rCL/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rCO/strings.xml b/auth/src/main/res/values-es-rCO/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rCO/strings.xml +++ b/auth/src/main/res/values-es-rCO/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rCR/strings.xml b/auth/src/main/res/values-es-rCR/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rCR/strings.xml +++ b/auth/src/main/res/values-es-rCR/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rDO/strings.xml b/auth/src/main/res/values-es-rDO/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rDO/strings.xml +++ b/auth/src/main/res/values-es-rDO/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rEC/strings.xml b/auth/src/main/res/values-es-rEC/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rEC/strings.xml +++ b/auth/src/main/res/values-es-rEC/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rGT/strings.xml b/auth/src/main/res/values-es-rGT/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rGT/strings.xml +++ b/auth/src/main/res/values-es-rGT/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rHN/strings.xml b/auth/src/main/res/values-es-rHN/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rHN/strings.xml +++ b/auth/src/main/res/values-es-rHN/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rMX/strings.xml b/auth/src/main/res/values-es-rMX/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rMX/strings.xml +++ b/auth/src/main/res/values-es-rMX/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rNI/strings.xml b/auth/src/main/res/values-es-rNI/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rNI/strings.xml +++ b/auth/src/main/res/values-es-rNI/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rPA/strings.xml b/auth/src/main/res/values-es-rPA/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rPA/strings.xml +++ b/auth/src/main/res/values-es-rPA/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rPE/strings.xml b/auth/src/main/res/values-es-rPE/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rPE/strings.xml +++ b/auth/src/main/res/values-es-rPE/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rPR/strings.xml b/auth/src/main/res/values-es-rPR/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rPR/strings.xml +++ b/auth/src/main/res/values-es-rPR/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rPY/strings.xml b/auth/src/main/res/values-es-rPY/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rPY/strings.xml +++ b/auth/src/main/res/values-es-rPY/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rSV/strings.xml b/auth/src/main/res/values-es-rSV/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rSV/strings.xml +++ b/auth/src/main/res/values-es-rSV/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rUS/strings.xml b/auth/src/main/res/values-es-rUS/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rUS/strings.xml +++ b/auth/src/main/res/values-es-rUS/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rUY/strings.xml b/auth/src/main/res/values-es-rUY/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rUY/strings.xml +++ b/auth/src/main/res/values-es-rUY/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es-rVE/strings.xml b/auth/src/main/res/values-es-rVE/strings.xml index 37c0546f1..9ebc9ee65 100755 --- a/auth/src/main/res/values-es-rVE/strings.xml +++ b/auth/src/main/res/values-es-rVE/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Acceder con Google + Acceder con Google Acceder con Facebook + Acceder con Facebook Acceder con Twitter + Acceder con Twitter Acceder con GitHub + Acceder con GitHub Acceder con el correo electrónico + Acceder con el correo electrónico Acceder con el teléfono + Acceder con el teléfono Continuar como invitado Acceder con Apple + Acceder con Apple Acceder con Microsoft + Acceder con Microsoft Acceder con Yahoo + Acceder con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-es/strings.xml b/auth/src/main/res/values-es/strings.xml index 49412d50f..5f8895314 100755 --- a/auth/src/main/res/values-es/strings.xml +++ b/auth/src/main/res/values-es/strings.xml @@ -12,15 +12,24 @@ Teléfono Correo electrónico Iniciar sesión con Google + Iniciar sesión con Google Iniciar sesión con Facebook + Iniciar sesión con Facebook Iniciar sesión con Twitter + Iniciar sesión con Twitter Iniciar sesión con GitHub + Iniciar sesión con GitHub Iniciar sesión con el correo electrónico + Iniciar sesión con el correo electrónico Iniciar sesión con el teléfono + Iniciar sesión con el teléfono Continuar como invitado Iniciar sesión con Apple + Iniciar sesión con Apple Iniciar sesión con Microsoft + Iniciar sesión con Microsoft Iniciar sesión con Yahoo + Iniciar sesión con Yahoo Siguiente Correo electrónico Número de teléfono diff --git a/auth/src/main/res/values-fa/strings.xml b/auth/src/main/res/values-fa/strings.xml index 2382d4580..2215781c2 100755 --- a/auth/src/main/res/values-fa/strings.xml +++ b/auth/src/main/res/values-fa/strings.xml @@ -12,15 +12,24 @@ تلفن ایمیل ورود به سیستم با Google‎ + ورود به سیستم با Google‎ ورود به سیستم با Facebook + ورود به سیستم با Facebook ورود به سیستم با Twitter + ورود به سیستم با Twitter ورود به سیستم با GitHub + ورود به سیستم با GitHub ورود به سیستم با ایمیل + ورود به سیستم با ایمیل ورود به سیستم با تلفن + ورود به سیستم با تلفن ادامه به‌عنوان مهمان ورود به سیستم با Apple + ورود به سیستم با Apple ورود به سیستم با Microsoft + ورود به سیستم با Microsoft ورود به سیستم با Yahoo + ورود به سیستم با Yahoo بعدی ایمیل شماره تلفن diff --git a/auth/src/main/res/values-fi/strings.xml b/auth/src/main/res/values-fi/strings.xml index cd9294ed9..7c45770a8 100755 --- a/auth/src/main/res/values-fi/strings.xml +++ b/auth/src/main/res/values-fi/strings.xml @@ -12,15 +12,24 @@ Puhelin Sähköposti Kirjaudu Google-tilillä + Kirjaudu Google-tilillä Kirjaudu Facebook-tilillä + Kirjaudu Facebook-tilillä Kirjaudu Twitter-tilillä + Kirjaudu Twitter-tilillä Kirjaudu GitHub-tilillä + Kirjaudu GitHub-tilillä Kirjaudu sähköpostilla + Kirjaudu sähköpostilla Kirjaudu puhelimella + Kirjaudu puhelimella Jatka vieraana Kirjaudu sisään Apple-tilillä + Kirjaudu sisään Apple-tilillä Kirjaudu sisään Microsoft-tilillä + Kirjaudu sisään Microsoft-tilillä Kirjaudu sisään Yahoo-tilillä + Kirjaudu sisään Yahoo-tilillä Seuraava Sähköposti Puhelinnumero diff --git a/auth/src/main/res/values-fil/strings.xml b/auth/src/main/res/values-fil/strings.xml index 29c678526..fc2474b26 100755 --- a/auth/src/main/res/values-fil/strings.xml +++ b/auth/src/main/res/values-fil/strings.xml @@ -12,15 +12,24 @@ Telepono Email Mag-sign in sa Google + Mag-sign in sa Google Mag-sign in sa Facebook + Mag-sign in sa Facebook Mag-sign in sa Twitter + Mag-sign in sa Twitter Mag-sign in sa GitHub + Mag-sign in sa GitHub Mag-sign in gamit ang email + Mag-sign in gamit ang email Mag-sign in gamit ang telepono + Mag-sign in gamit ang telepono Magpatuloy bilang bisita Mag-sign in sa Apple + Mag-sign in sa Apple Mag-sign in sa Microsoft + Mag-sign in sa Microsoft Mag-sign in sa Yahoo + Mag-sign in sa Yahoo Susunod Mag-email Numero ng Telepono diff --git a/auth/src/main/res/values-fr-rCH/strings.xml b/auth/src/main/res/values-fr-rCH/strings.xml index c02f2021b..dd6c99b1b 100755 --- a/auth/src/main/res/values-fr-rCH/strings.xml +++ b/auth/src/main/res/values-fr-rCH/strings.xml @@ -12,15 +12,24 @@ Numéro de téléphone Adresse e-mail Se connecter avec Google + Se connecter avec Google Se connecter avec Facebook + Se connecter avec Facebook Se connecter avec Twitter + Se connecter avec Twitter Se connecter avec GitHub + Se connecter avec GitHub Se connecter avec une adresse e-mail + Se connecter avec une adresse e-mail Se connecter avec un téléphone + Se connecter avec un téléphone Continuer en tant qu\'invité Se connecter avec Apple + Se connecter avec Apple Se connecter avec Microsoft + Se connecter avec Microsoft Se connecter avec Yahoo + Se connecter avec Yahoo Suivant E-mail Numéro de téléphone diff --git a/auth/src/main/res/values-fr/strings.xml b/auth/src/main/res/values-fr/strings.xml index c753053a8..ceaf93d6e 100755 --- a/auth/src/main/res/values-fr/strings.xml +++ b/auth/src/main/res/values-fr/strings.xml @@ -12,15 +12,24 @@ Numéro de téléphone Adresse e-mail Se connecter avec Google + Se connecter avec Google Se connecter avec Facebook + Se connecter avec Facebook Se connecter avec Twitter + Se connecter avec Twitter Se connecter avec GitHub + Se connecter avec GitHub Se connecter avec une adresse e-mail + Se connecter avec une adresse e-mail Se connecter avec un téléphone + Se connecter avec un téléphone Continuer en tant qu\'invité Se connecter avec Apple + Se connecter avec Apple Se connecter avec Microsoft + Se connecter avec Microsoft Se connecter avec Yahoo + Se connecter avec Yahoo Suivant E-mail Numéro de téléphone diff --git a/auth/src/main/res/values-gsw/strings.xml b/auth/src/main/res/values-gsw/strings.xml index 42bd8e23f..2b586c600 100755 --- a/auth/src/main/res/values-gsw/strings.xml +++ b/auth/src/main/res/values-gsw/strings.xml @@ -12,15 +12,24 @@ Telefon E-Mail Über Google anmelden + Über Google anmelden Über Facebook anmelden + Über Facebook anmelden Über Twitter anmelden + Über Twitter anmelden Über GitHub anmelden + Über GitHub anmelden Mit einer E-Mail-Adresse anmelden + Mit einer E-Mail-Adresse anmelden Mit einer Telefonnummer anmelden + Mit einer Telefonnummer anmelden Als Gast fortfahren Über Apple anmelden + Über Apple anmelden Über Microsoft anmelden + Über Microsoft anmelden Über Yahoo anmelden + Über Yahoo anmelden Weiter E-Mail-Adresse Telefonnummer diff --git a/auth/src/main/res/values-gu/strings.xml b/auth/src/main/res/values-gu/strings.xml index 138244ce3..f331a75d3 100755 --- a/auth/src/main/res/values-gu/strings.xml +++ b/auth/src/main/res/values-gu/strings.xml @@ -12,15 +12,24 @@ ફોન ઇમેઇલ Google વડે સાઇન ઇન કરો + Google વડે સાઇન ઇન કરો Facebook વડે સાઇન ઇન કરો + Facebook વડે સાઇન ઇન કરો Twitter વડે સાઇન ઇન કરો + Twitter વડે સાઇન ઇન કરો GitHub વડે સાઇન ઇન કરો + GitHub વડે સાઇન ઇન કરો ઇમેઇલ વડે સાઇન ઇન કરો + ઇમેઇલ વડે સાઇન ઇન કરો ફોન વડે સાઇન ઇન કરો + ફોન વડે સાઇન ઇન કરો અતિથિ તરીકે ચાલુ રાખો Apple વડે સાઇન ઇન કરો + Apple વડે સાઇન ઇન કરો Microsoft વડે સાઇન ઇન કરો + Microsoft વડે સાઇન ઇન કરો Yahoo વડે સાઇન ઇન કરો + Yahoo વડે સાઇન ઇન કરો આગળ ઇમેઇલ ફોન નંબર diff --git a/auth/src/main/res/values-hi/strings.xml b/auth/src/main/res/values-hi/strings.xml index 805bd88d7..847f6c169 100755 --- a/auth/src/main/res/values-hi/strings.xml +++ b/auth/src/main/res/values-hi/strings.xml @@ -12,15 +12,24 @@ फ़ोन ईमेल Google से प्रवेश करें + Google से प्रवेश करें Facebook से प्रवेश करें + Facebook से प्रवेश करें Twitter से प्रवेश करें + Twitter से प्रवेश करें GitHub के साथ साइन इन करें + GitHub के साथ साइन इन करें ईमेल से प्रवेश करें + ईमेल से प्रवेश करें फ़ोन से प्रवेश करें + फ़ोन से प्रवेश करें मेहमान के रूप में जारी रखें Apple खाते से साइन इन करें + Apple खाते से साइन इन करें Microsoft खाते से साइन इन करें + Microsoft खाते से साइन इन करें Yahoo खाते से साइन इन करें + Yahoo खाते से साइन इन करें अगला ईमेल फ़ोन नंबर diff --git a/auth/src/main/res/values-hr/strings.xml b/auth/src/main/res/values-hr/strings.xml index bfdc450ac..4a481d5b3 100755 --- a/auth/src/main/res/values-hr/strings.xml +++ b/auth/src/main/res/values-hr/strings.xml @@ -12,15 +12,24 @@ Telefon E-adresa Prijava putem Googlea + Prijava putem Googlea Prijava putem Facebooka + Prijava putem Facebooka Prijava putem Twittera + Prijava putem Twittera Prijava putem GitHuba + Prijava putem GitHuba Prijava putem e-adrese + Prijava putem e-adrese Prijava putem telefona + Prijava putem telefona Nastavi kao gost Prijavite se Apple računom + Prijavite se Apple računom Prijavite se Microsoft računom + Prijavite se Microsoft računom Prijavite se Yahoo računom + Prijavite se Yahoo računom Dalje E-adresa Telefonski broj diff --git a/auth/src/main/res/values-hu/strings.xml b/auth/src/main/res/values-hu/strings.xml index a9b258bec..6f8f96e28 100755 --- a/auth/src/main/res/values-hu/strings.xml +++ b/auth/src/main/res/values-hu/strings.xml @@ -12,15 +12,24 @@ Telefon E-mail Bejelentkezés Google-fiókkal + Bejelentkezés Google-fiókkal Bejelentkezés Facebook-fiókkal + Bejelentkezés Facebook-fiókkal Bejelentkezés Twitter-fiókkal + Bejelentkezés Twitter-fiókkal Bejelentkezés GitHubbal + Bejelentkezés GitHubbal Bejelentkezés e-mail-fiókkal + Bejelentkezés e-mail-fiókkal Bejelentkezés telefonnal + Bejelentkezés telefonnal Folytatás vendégként Bejelentkezés Apple-fiókkal + Bejelentkezés Apple-fiókkal Bejelentkezés Microsoft-fiókkal + Bejelentkezés Microsoft-fiókkal Bejelentkezés Yahoo-fiókkal + Bejelentkezés Yahoo-fiókkal Következő E-mail Telefonszám diff --git a/auth/src/main/res/values-in/strings.xml b/auth/src/main/res/values-in/strings.xml index 01da423e5..5e125242a 100755 --- a/auth/src/main/res/values-in/strings.xml +++ b/auth/src/main/res/values-in/strings.xml @@ -12,15 +12,24 @@ Ponsel Email Login dengan Google + Login dengan Google Login dengan Facebook + Login dengan Facebook Login dengan Twitter + Login dengan Twitter Login dengan GitHub + Login dengan GitHub Login dengan email + Login dengan email Login dengan nomor telepon + Login dengan nomor telepon Lanjutkan sebagai tamu Login dengan Apple + Login dengan Apple Login dengan Microsoft + Login dengan Microsoft Login dengan Yahoo + Login dengan Yahoo Berikutnya Email Nomor Telepon diff --git a/auth/src/main/res/values-it/strings.xml b/auth/src/main/res/values-it/strings.xml index 6c0c5c6d4..dd6ab26a7 100755 --- a/auth/src/main/res/values-it/strings.xml +++ b/auth/src/main/res/values-it/strings.xml @@ -12,15 +12,24 @@ Telefono Email Accedi con Google + Accedi con Google Accedi con Facebook + Accedi con Facebook Accedi con Twitter + Accedi con Twitter Accedi con GitHub + Accedi con GitHub Accedi con l\'indirizzo email + Accedi con l\'indirizzo email Accedi con il telefono + Accedi con il telefono Continua come ospite Accedi con Apple + Accedi con Apple Accedi con Microsoft + Accedi con Microsoft Accedi con Yahoo + Accedi con Yahoo Avanti Indirizzo email Numero di telefono diff --git a/auth/src/main/res/values-iw/strings.xml b/auth/src/main/res/values-iw/strings.xml index 089494e0e..712bfec7d 100755 --- a/auth/src/main/res/values-iw/strings.xml +++ b/auth/src/main/res/values-iw/strings.xml @@ -12,15 +12,24 @@ טלפון אימייל כניסה באמצעות Google + כניסה באמצעות Google כניסה באמצעות Facebook + כניסה באמצעות Facebook כניסה באמצעות Twitter + כניסה באמצעות Twitter כניסה באמצעות GitHub + כניסה באמצעות GitHub כניסה באמצעות אימייל + כניסה באמצעות אימייל כניסה באמצעות הטלפון + כניסה באמצעות הטלפון המשך כאורח כניסה באמצעות Apple + כניסה באמצעות Apple כניסה באמצעות Microsoft + כניסה באמצעות Microsoft כניסה באמצעות Yahoo + כניסה באמצעות Yahoo הבא‏ אימייל מספר טלפון diff --git a/auth/src/main/res/values-ja/strings.xml b/auth/src/main/res/values-ja/strings.xml index ec4b1f021..dc900e2d8 100755 --- a/auth/src/main/res/values-ja/strings.xml +++ b/auth/src/main/res/values-ja/strings.xml @@ -12,15 +12,24 @@ 電話 メール Google でログイン + Google でログイン Facebook でログイン + Facebook でログイン Twitter でログイン + Twitter でログイン GitHub でログイン + GitHub でログイン メールアドレスでログイン + メールアドレスでログイン 電話番号でログイン + 電話番号でログイン ゲストとして続行 Apple アカウントでログイン + Apple アカウントでログイン Microsoft アカウントでログイン + Microsoft アカウントでログイン Yahoo アカウントでログイン + Yahoo アカウントでログイン 次へ メールアドレス 電話番号 diff --git a/auth/src/main/res/values-kn/strings.xml b/auth/src/main/res/values-kn/strings.xml index 1ce94c19a..31f780a6a 100755 --- a/auth/src/main/res/values-kn/strings.xml +++ b/auth/src/main/res/values-kn/strings.xml @@ -12,15 +12,24 @@ ಫೋನ್ ಇಮೇಲ್ Google ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ + Google ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ Facebook ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ + Facebook ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ Twitter ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ + Twitter ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ GitHub ಮೂಲಕ ಸೈನ್‌ ಇನ್‌ ಮಾಡಿ + GitHub ಮೂಲಕ ಸೈನ್‌ ಇನ್‌ ಮಾಡಿ ಇಮೇಲ್ ಜೊತೆ ಸೈನ್ ಇನ್ ಮಾಡಿ + ಇಮೇಲ್ ಜೊತೆ ಸೈನ್ ಇನ್ ಮಾಡಿ ಫೋನ್‌ ಮೂಲಕ ಸೈನ್‌ ಇನ್‌ ಮಾಡಿ + ಫೋನ್‌ ಮೂಲಕ ಸೈನ್‌ ಇನ್‌ ಮಾಡಿ ಅತಿಥಿಯಂತೆ ಮುಂದುವರಿಸಿ Apple ಮೂಲಕ ಸೈನ್‌ ಇನ್ ಮಾಡಿ + Apple ಮೂಲಕ ಸೈನ್‌ ಇನ್ ಮಾಡಿ Microsoft ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ + Microsoft ಮೂಲಕ ಸೈನ್ ಇನ್ ಮಾಡಿ Yahoo ಮೂಲಕ ಸೈನ್‌ ಇನ್ ಮಾಡಿ + Yahoo ಮೂಲಕ ಸೈನ್‌ ಇನ್ ಮಾಡಿ ಮುಂದೆ ಇಮೇಲ್ ಫೋನ್ ಸಂಖ್ಯೆ diff --git a/auth/src/main/res/values-ko/strings.xml b/auth/src/main/res/values-ko/strings.xml index ad5bc3248..eb6e1d22b 100755 --- a/auth/src/main/res/values-ko/strings.xml +++ b/auth/src/main/res/values-ko/strings.xml @@ -12,15 +12,24 @@ 전화 이메일 Google 계정으로 로그인 + Google 계정으로 로그인 Facebook으로 로그인 + Facebook으로 로그인 Twitter로 로그인 + Twitter로 로그인 GitHub로 로그인 + GitHub로 로그인 이메일로 로그인 + 이메일로 로그인 전화로 로그인 + 전화로 로그인 비회원으로 진행 Apple 계정으로 로그인 + Apple 계정으로 로그인 Microsoft 계정으로 로그인 + Microsoft 계정으로 로그인 Yahoo 계정으로 로그인 + Yahoo 계정으로 로그인 다음 이메일 전화번호 diff --git a/auth/src/main/res/values-ln/strings.xml b/auth/src/main/res/values-ln/strings.xml index 94cb14751..0ee345992 100755 --- a/auth/src/main/res/values-ln/strings.xml +++ b/auth/src/main/res/values-ln/strings.xml @@ -12,15 +12,24 @@ Numéro de téléphone Adresse e-mail Se connecter avec Google + Se connecter avec Google Se connecter avec Facebook + Se connecter avec Facebook Se connecter avec Twitter + Se connecter avec Twitter Se connecter avec GitHub + Se connecter avec GitHub Se connecter avec une adresse e-mail + Se connecter avec une adresse e-mail Se connecter avec un téléphone + Se connecter avec un téléphone Continuer en tant qu\'invité Se connecter avec Apple + Se connecter avec Apple Se connecter avec Microsoft + Se connecter avec Microsoft Se connecter avec Yahoo + Se connecter avec Yahoo Suivant E-mail Numéro de téléphone diff --git a/auth/src/main/res/values-lt/strings.xml b/auth/src/main/res/values-lt/strings.xml index 979e9cc8d..cef70d317 100755 --- a/auth/src/main/res/values-lt/strings.xml +++ b/auth/src/main/res/values-lt/strings.xml @@ -12,15 +12,24 @@ Telefono numeris El. pašto Prisijungti per „Google“ + Prisijungti per „Google“ Prisijungti per „Facebook“ + Prisijungti per „Facebook“ Prisijungti per „Twitter“ + Prisijungti per „Twitter“ Prisijungti per „GitHub“ + Prisijungti per „GitHub“ Prisijungti nurodant el. pašto adresą + Prisijungti nurodant el. pašto adresą Prisijungti nurodant telefono numerį + Prisijungti nurodant telefono numerį Tęsti kaip svečiui Prisijungti per „Apple“ + Prisijungti per „Apple“ Prisijungti per „Microsoft“ + Prisijungti per „Microsoft“ Prisijungti per „Yahoo“ + Prisijungti per „Yahoo“ Kitas El. pašto adresas Telefono numeris diff --git a/auth/src/main/res/values-lv/strings.xml b/auth/src/main/res/values-lv/strings.xml index b7c1de4a6..fbba1e17a 100755 --- a/auth/src/main/res/values-lv/strings.xml +++ b/auth/src/main/res/values-lv/strings.xml @@ -12,15 +12,24 @@ Tālrunis E-pasts Pierakstīties ar Google + Pierakstīties ar Google Pierakstīties ar Facebook + Pierakstīties ar Facebook Pierakstīties ar Twitter + Pierakstīties ar Twitter Pierakstīties, izmantojot GitHub + Pierakstīties, izmantojot GitHub Pierakstīties ar e-pasta adresi + Pierakstīties ar e-pasta adresi Pierakstīties ar tālruni + Pierakstīties ar tālruni Turpināt kā viesim Pierakstīties ar Apple kontu + Pierakstīties ar Apple kontu Pierakstīties ar Microsoft kontu + Pierakstīties ar Microsoft kontu Pierakstīties ar Yahoo kontu + Pierakstīties ar Yahoo kontu Tālāk E-pasts Tālruņa numurs diff --git a/auth/src/main/res/values-mo/strings.xml b/auth/src/main/res/values-mo/strings.xml index 46c2fac46..73ac609a4 100755 --- a/auth/src/main/res/values-mo/strings.xml +++ b/auth/src/main/res/values-mo/strings.xml @@ -12,15 +12,24 @@ Număr de telefon Adresă de e-mail Conectați-vă cu Google + Conectați-vă cu Google Conectați-vă cu Facebook + Conectați-vă cu Facebook Conectați-vă cu Twitter + Conectați-vă cu Twitter Conectați-vă cu GitHub + Conectați-vă cu GitHub Conectați-vă cu adresa de e-mail + Conectați-vă cu adresa de e-mail Conectați-vă cu numărul de telefon + Conectați-vă cu numărul de telefon Continuați ca invitat Conectați-vă cu Apple + Conectați-vă cu Apple Conectați-vă cu Microsoft + Conectați-vă cu Microsoft Conectați-vă cu Yahoo + Conectați-vă cu Yahoo Înainte Adresă de e-mail Număr de telefon diff --git a/auth/src/main/res/values-mr/strings.xml b/auth/src/main/res/values-mr/strings.xml index 34bd8d832..00b6ee391 100755 --- a/auth/src/main/res/values-mr/strings.xml +++ b/auth/src/main/res/values-mr/strings.xml @@ -12,15 +12,24 @@ फोन ईमेल Googleने साइन इन करा + Googleने साइन इन करा Facebookने साइन इन करा + Facebookने साइन इन करा Twitterने साइन इन करा + Twitterने साइन इन करा GitHub सह साइन इन करा + GitHub सह साइन इन करा ईमेलने साइन इन करा + ईमेलने साइन इन करा फोनने साइन इन करा + फोनने साइन इन करा अतिथी म्हणून सुरू ठेवा Apple वरून साइन इन करा + Apple वरून साइन इन करा Microsoft वरून साइन इन करा + Microsoft वरून साइन इन करा Yahoo वरून साइन इन करा + Yahoo वरून साइन इन करा पुढील ईमेल फोन नंबर diff --git a/auth/src/main/res/values-ms/strings.xml b/auth/src/main/res/values-ms/strings.xml index 9f0e6b074..be8b30ccc 100755 --- a/auth/src/main/res/values-ms/strings.xml +++ b/auth/src/main/res/values-ms/strings.xml @@ -12,15 +12,24 @@ Telefon E-mel Log masuk dengan Google + Log masuk dengan Google Log masuk dengan Facebook + Log masuk dengan Facebook Log masuk dengan Twitter + Log masuk dengan Twitter Log masuk dengan GitHub + Log masuk dengan GitHub Log masuk dengan e-mel + Log masuk dengan e-mel Log masuk dengan telefon + Log masuk dengan telefon Teruskan sebagai tetamu Log masuk dengan Apple + Log masuk dengan Apple Log masuk dengan Microsoft + Log masuk dengan Microsoft Log masuk dengan Yahoo + Log masuk dengan Yahoo Seterusnya E-mel Nombor Telefon diff --git a/auth/src/main/res/values-nb/strings.xml b/auth/src/main/res/values-nb/strings.xml index f1c3486e0..5cc9c83ac 100755 --- a/auth/src/main/res/values-nb/strings.xml +++ b/auth/src/main/res/values-nb/strings.xml @@ -12,15 +12,24 @@ Telefon E-post Logg på med Google + Logg på med Google Logg på med Facebook + Logg på med Facebook Logg på med Twitter + Logg på med Twitter Logg på med GitHub + Logg på med GitHub Logg på med e-postadresse + Logg på med e-postadresse Logg på med telefonnummeret ditt + Logg på med telefonnummeret ditt Fortsett som gjest Logg på med Apple-ID-en din + Logg på med Apple-ID-en din Logg på med Microsoft-kontoen din + Logg på med Microsoft-kontoen din Logg på med Yahoo-kontoen din + Logg på med Yahoo-kontoen din Neste E-post Telefonnummer diff --git a/auth/src/main/res/values-nl/strings.xml b/auth/src/main/res/values-nl/strings.xml index 2eb2b6b5a..8d011cc4f 100755 --- a/auth/src/main/res/values-nl/strings.xml +++ b/auth/src/main/res/values-nl/strings.xml @@ -12,15 +12,24 @@ Telefoon E-mailadres Inloggen met Google + Inloggen met Google Inloggen met Facebook + Inloggen met Facebook Inloggen met Twitter + Inloggen met Twitter Inloggen met GitHub + Inloggen met GitHub Inloggen met e-mailadres + Inloggen met e-mailadres Inloggen met telefoon + Inloggen met telefoon Doorgaan als gast Inloggen met Apple + Inloggen met Apple Inloggen met Microsoft + Inloggen met Microsoft Inloggen met Yahoo + Inloggen met Yahoo Volgende E-mail Telefoonnummer diff --git a/auth/src/main/res/values-no/strings.xml b/auth/src/main/res/values-no/strings.xml index 04a8c51cd..6afe11726 100755 --- a/auth/src/main/res/values-no/strings.xml +++ b/auth/src/main/res/values-no/strings.xml @@ -12,15 +12,24 @@ Telefon E-post Logg på med Google + Logg på med Google Logg på med Facebook + Logg på med Facebook Logg på med Twitter + Logg på med Twitter Logg på med GitHub + Logg på med GitHub Logg på med e-postadresse + Logg på med e-postadresse Logg på med telefonnummeret ditt + Logg på med telefonnummeret ditt Fortsett som gjest Logg på med Apple-ID-en din + Logg på med Apple-ID-en din Logg på med Microsoft-kontoen din + Logg på med Microsoft-kontoen din Logg på med Yahoo-kontoen din + Logg på med Yahoo-kontoen din Neste E-post Telefonnummer diff --git a/auth/src/main/res/values-pl/strings.xml b/auth/src/main/res/values-pl/strings.xml index 173fceb0b..7fd45ad4e 100755 --- a/auth/src/main/res/values-pl/strings.xml +++ b/auth/src/main/res/values-pl/strings.xml @@ -12,15 +12,24 @@ Telefon E-mail Zaloguj się przez Google + Zaloguj się przez Google Zaloguj się przez Facebooka + Zaloguj się przez Facebooka Zaloguj się przez Twittera + Zaloguj się przez Twittera Zaloguj się przez GitHuba + Zaloguj się przez GitHuba Zaloguj się za pomocą e-maila + Zaloguj się za pomocą e-maila Zaloguj się z użyciem numeru telefonu + Zaloguj się z użyciem numeru telefonu Kontynuuj jako gość Zaloguj się przez Apple + Zaloguj się przez Apple Zaloguj się przez Microsoft + Zaloguj się przez Microsoft Zaloguj się przez Yahoo + Zaloguj się przez Yahoo Dalej Adres e-mail Numer telefonu diff --git a/auth/src/main/res/values-pt-rBR/strings.xml b/auth/src/main/res/values-pt-rBR/strings.xml index ca0029dbe..7e805ba3e 100755 --- a/auth/src/main/res/values-pt-rBR/strings.xml +++ b/auth/src/main/res/values-pt-rBR/strings.xml @@ -12,15 +12,24 @@ Telefone E-mail Fazer login com o Google + Fazer login com o Google Fazer login com o Facebook + Fazer login com o Facebook Fazer login com o Twitter + Fazer login com o Twitter Fazer login com o GitHub + Fazer login com o GitHub Fazer login com o e-mail + Fazer login com o e-mail Fazer login com o telefone + Fazer login com o telefone Continuar como convidado Fazer login com a Apple + Fazer login com a Apple Fazer login com a Microsoft + Fazer login com a Microsoft Fazer login com o Yahoo + Fazer login com o Yahoo Próxima E-mail Número de telefone diff --git a/auth/src/main/res/values-pt-rPT/strings.xml b/auth/src/main/res/values-pt-rPT/strings.xml index 1b5a66801..3844841bc 100755 --- a/auth/src/main/res/values-pt-rPT/strings.xml +++ b/auth/src/main/res/values-pt-rPT/strings.xml @@ -12,15 +12,24 @@ Telemóvel Email Iniciar sessão com o Google + Iniciar sessão com o Google Iniciar sessão com o Facebook + Iniciar sessão com o Facebook Iniciar sessão com o Twitter + Iniciar sessão com o Twitter Iniciar sessão com o GitHub + Iniciar sessão com o GitHub Iniciar sessão com o email + Iniciar sessão com o email Iniciar sessão com o telemóvel + Iniciar sessão com o telemóvel Continuar como convidado Iniciar sessão com a Apple + Iniciar sessão com a Apple Iniciar sessão com a Microsoft + Iniciar sessão com a Microsoft Iniciar sessão com o Yahoo + Iniciar sessão com o Yahoo Seguinte Email Número de telefone diff --git a/auth/src/main/res/values-pt/strings.xml b/auth/src/main/res/values-pt/strings.xml index f101f629a..54473de13 100755 --- a/auth/src/main/res/values-pt/strings.xml +++ b/auth/src/main/res/values-pt/strings.xml @@ -12,15 +12,24 @@ Telefone E-mail Fazer login com o Google + Fazer login com o Google Fazer login com o Facebook + Fazer login com o Facebook Fazer login com o Twitter + Fazer login com o Twitter Fazer login com o GitHub + Fazer login com o GitHub Fazer login com o e-mail + Fazer login com o e-mail Fazer login com o telefone + Fazer login com o telefone Continuar como convidado Fazer login com a Apple + Fazer login com a Apple Fazer login com a Microsoft + Fazer login com a Microsoft Fazer login com o Yahoo + Fazer login com o Yahoo Próxima E-mail Número de telefone diff --git a/auth/src/main/res/values-ro/strings.xml b/auth/src/main/res/values-ro/strings.xml index 9d3afcf6f..c91b2199c 100755 --- a/auth/src/main/res/values-ro/strings.xml +++ b/auth/src/main/res/values-ro/strings.xml @@ -12,15 +12,24 @@ Număr de telefon Adresă de e-mail Conectați-vă cu Google + Conectați-vă cu Google Conectați-vă cu Facebook + Conectați-vă cu Facebook Conectați-vă cu Twitter + Conectați-vă cu Twitter Conectați-vă cu GitHub + Conectați-vă cu GitHub Conectați-vă cu adresa de e-mail + Conectați-vă cu adresa de e-mail Conectați-vă cu numărul de telefon + Conectați-vă cu numărul de telefon Continuați ca invitat Conectați-vă cu Apple + Conectați-vă cu Apple Conectați-vă cu Microsoft + Conectați-vă cu Microsoft Conectați-vă cu Yahoo + Conectați-vă cu Yahoo Înainte Adresă de e-mail Număr de telefon diff --git a/auth/src/main/res/values-ru/strings.xml b/auth/src/main/res/values-ru/strings.xml index 53c4fc56b..f41c29527 100755 --- a/auth/src/main/res/values-ru/strings.xml +++ b/auth/src/main/res/values-ru/strings.xml @@ -12,15 +12,24 @@ Телефон Адрес электронной почты Войти через аккаунт Google + Войти через аккаунт Google Войти через аккаунт Facebook + Войти через аккаунт Facebook Войти через аккаунт Twitter + Войти через аккаунт Twitter Войти через аккаунт GitHub + Войти через аккаунт GitHub Войти по адресу электронной почты + Войти по адресу электронной почты Войти по номеру телефона + Войти по номеру телефона Продолжить как гость Войти через аккаунт Apple + Войти через аккаунт Apple Войти через аккаунт Microsoft + Войти через аккаунт Microsoft Войти через аккаунт Yahoo + Войти через аккаунт Yahoo Далее Адрес электронной почты Номер телефона diff --git a/auth/src/main/res/values-sk/strings.xml b/auth/src/main/res/values-sk/strings.xml index 74126c1e7..2643ea9d2 100755 --- a/auth/src/main/res/values-sk/strings.xml +++ b/auth/src/main/res/values-sk/strings.xml @@ -12,15 +12,24 @@ Telefón E‑mail Prihlásiť sa účtom Google + Prihlásiť sa účtom Google Prihlásiť sa cez Facebook + Prihlásiť sa cez Facebook Prihlásiť sa cez Twitter + Prihlásiť sa cez Twitter Prihlásiť sa cez GitHub + Prihlásiť sa cez GitHub Prihlásiť sa e-mailom + Prihlásiť sa e-mailom Prihlásiť sa telefónom + Prihlásiť sa telefónom Pokračovať ako hosť Prihlásiť sa cez Apple + Prihlásiť sa cez Apple Prihlásiť sa cez Microsoft + Prihlásiť sa cez Microsoft Prihlásiť sa cez Yahoo + Prihlásiť sa cez Yahoo Ďalej E-mail Telefónne číslo diff --git a/auth/src/main/res/values-sl/strings.xml b/auth/src/main/res/values-sl/strings.xml index e6001c924..12c7fee84 100755 --- a/auth/src/main/res/values-sl/strings.xml +++ b/auth/src/main/res/values-sl/strings.xml @@ -12,15 +12,24 @@ Telefon E-pošta Prijava z Googlom + Prijava z Googlom Prijava z računom za Facebook + Prijava z računom za Facebook Prijava z računom za Twitter + Prijava z računom za Twitter Prijava z računom za GitHub + Prijava z računom za GitHub Prijava z e-poštnim naslovom + Prijava z e-poštnim naslovom Prijava s telefonom + Prijava s telefonom Nadaljuj kot gost Prijava z računom za Apple + Prijava z računom za Apple Prijava z računom za Microsoft + Prijava z računom za Microsoft Prijava z računom za Yahoo + Prijava z računom za Yahoo Naprej E-pošta Telefonska številka diff --git a/auth/src/main/res/values-sr/strings.xml b/auth/src/main/res/values-sr/strings.xml index 63c3c21c9..666d446ad 100755 --- a/auth/src/main/res/values-sr/strings.xml +++ b/auth/src/main/res/values-sr/strings.xml @@ -12,15 +12,24 @@ Телефон Имејл Пријави ме помоћу Google-а + Пријави ме помоћу Google-а Пријави ме помоћу Facebook-а + Пријави ме помоћу Facebook-а Пријави ме помоћу Twitter-а + Пријави ме помоћу Twitter-а Пријавите се помоћу GitHub-а + Пријавите се помоћу GitHub-а Пријави ме помоћу имејла + Пријави ме помоћу имејла Пријави ме помоћу телефона + Пријави ме помоћу телефона Наставите као гост Пријавите се преко Apple налога + Пријавите се преко Apple налога Пријавите се преко Microsoft налога + Пријавите се преко Microsoft налога Пријавите се преко Yahoo налога + Пријавите се преко Yahoo налога Даље Имејл Број телефона diff --git a/auth/src/main/res/values-sv/strings.xml b/auth/src/main/res/values-sv/strings.xml index 8d45ca544..79c33bf9d 100755 --- a/auth/src/main/res/values-sv/strings.xml +++ b/auth/src/main/res/values-sv/strings.xml @@ -12,15 +12,24 @@ Mobil E-post Logga in med Google + Logga in med Google Logga in med Facebook + Logga in med Facebook Logga in med Twitter + Logga in med Twitter Logga in med GitHub + Logga in med GitHub Logga in med e-post + Logga in med e-post Logga in med telefon + Logga in med telefon Fortsätt som gäst Logga in med Apple + Logga in med Apple Logga in med Microsoft + Logga in med Microsoft Logga in med Yahoo + Logga in med Yahoo Nästa E-post Telefonnummer diff --git a/auth/src/main/res/values-ta/strings.xml b/auth/src/main/res/values-ta/strings.xml index f41eb7176..a01ca8e42 100755 --- a/auth/src/main/res/values-ta/strings.xml +++ b/auth/src/main/res/values-ta/strings.xml @@ -12,15 +12,24 @@ ஃபோன் மின்னஞ்சல் Google மூலம் உள்நுழைக + Google மூலம் உள்நுழைக Facebook மூலம் உள்நுழைக + Facebook மூலம் உள்நுழைக Twitter மூலம் உள்நுழைக + Twitter மூலம் உள்நுழைக GitHub பயன்படுத்தி உள்நுழைக + GitHub பயன்படுத்தி உள்நுழைக மின்னஞ்சல் மூலம் உள்நுழைக + மின்னஞ்சல் மூலம் உள்நுழைக ஃபோன் எண் மூலம் உள்நுழைக + ஃபோன் எண் மூலம் உள்நுழைக கெஸ்டாகத் தொடர்க Apple மூலம் உள்நுழை + Apple மூலம் உள்நுழை Microsoft மூலம் உள்நுழை + Microsoft மூலம் உள்நுழை Yahoo மூலம் உள்நுழை + Yahoo மூலம் உள்நுழை அடுத்து மின்னஞ்சல் ஃபோன் எண் diff --git a/auth/src/main/res/values-th/strings.xml b/auth/src/main/res/values-th/strings.xml index 248e64533..9dc12174b 100755 --- a/auth/src/main/res/values-th/strings.xml +++ b/auth/src/main/res/values-th/strings.xml @@ -12,15 +12,24 @@ โทรศัพท์ อีเมล ลงชื่อเข้าใช้ด้วย Google + ลงชื่อเข้าใช้ด้วย Google ลงชื่อเข้าใช้ด้วย Facebook + ลงชื่อเข้าใช้ด้วย Facebook ลงชื่อเข้าใช้ด้วย Twitter + ลงชื่อเข้าใช้ด้วย Twitter ลงชื่อเข้าใช้ด้วย GitHub + ลงชื่อเข้าใช้ด้วย GitHub ลงชื่อเข้าใช้ด้วยอีเมล + ลงชื่อเข้าใช้ด้วยอีเมล ลงชื่อเข้าใช้ด้วยโทรศัพท์ + ลงชื่อเข้าใช้ด้วยโทรศัพท์ ดำเนินการต่อในฐานะผู้เข้าร่วม ลงชื่อเข้าใช้ด้วย Apple + ลงชื่อเข้าใช้ด้วย Apple ลงชื่อเข้าใช้ด้วย Microsoft + ลงชื่อเข้าใช้ด้วย Microsoft ลงชื่อเข้าใช้ด้วย Yahoo + ลงชื่อเข้าใช้ด้วย Yahoo ถัดไป อีเมล หมายเลขโทรศัพท์ diff --git a/auth/src/main/res/values-tl/strings.xml b/auth/src/main/res/values-tl/strings.xml index e9810f666..bcafcca5e 100755 --- a/auth/src/main/res/values-tl/strings.xml +++ b/auth/src/main/res/values-tl/strings.xml @@ -12,15 +12,24 @@ Telepono Email Mag-sign in sa Google + Mag-sign in sa Google Mag-sign in sa Facebook + Mag-sign in sa Facebook Mag-sign in sa Twitter + Mag-sign in sa Twitter Mag-sign in sa GitHub + Mag-sign in sa GitHub Mag-sign in gamit ang email + Mag-sign in gamit ang email Mag-sign in gamit ang telepono + Mag-sign in gamit ang telepono Magpatuloy bilang bisita Mag-sign in sa Apple + Mag-sign in sa Apple Mag-sign in sa Microsoft + Mag-sign in sa Microsoft Mag-sign in sa Yahoo + Mag-sign in sa Yahoo Susunod Mag-email Numero ng Telepono diff --git a/auth/src/main/res/values-tr/strings.xml b/auth/src/main/res/values-tr/strings.xml index 65614a56c..59b2ba45b 100755 --- a/auth/src/main/res/values-tr/strings.xml +++ b/auth/src/main/res/values-tr/strings.xml @@ -12,15 +12,24 @@ Telefon E-posta Google ile oturum aç + Google ile oturum aç Facebook ile oturum aç + Facebook ile oturum aç Twitter ile oturum aç + Twitter ile oturum aç GitHub ile oturum aç + GitHub ile oturum aç E-posta ile oturum aç + E-posta ile oturum aç Telefonla oturum aç + Telefonla oturum aç Misafir olarak devam et Apple ile oturum aç + Apple ile oturum aç Microsoft ile oturum aç + Microsoft ile oturum aç Yahoo ile oturum aç + Yahoo ile oturum aç İleri E-posta Telefon Numarası diff --git a/auth/src/main/res/values-uk/strings.xml b/auth/src/main/res/values-uk/strings.xml index 9774eb323..2361de7c4 100755 --- a/auth/src/main/res/values-uk/strings.xml +++ b/auth/src/main/res/values-uk/strings.xml @@ -12,15 +12,24 @@ Телефон Електронна пошта Увійти через Google + Увійти через Google Увійти через Facebook + Увійти через Facebook Увійти через Twitter + Увійти через Twitter Увійти через GitHub + Увійти через GitHub Увійти, використовуючи електронну адресу + Увійти, використовуючи електронну адресу Увійти, використовуючи телефон + Увійти, використовуючи телефон Продовжити як гість Увійти через обліковий запис Apple + Увійти через обліковий запис Apple Увійти через обліковий запис Microsoft + Увійти через обліковий запис Microsoft Увійти через обліковий запис Yahoo + Увійти через обліковий запис Yahoo Далі Електронна адреса Номер телефону diff --git a/auth/src/main/res/values-ur/strings.xml b/auth/src/main/res/values-ur/strings.xml index ca8d277dc..9e4268e6c 100755 --- a/auth/src/main/res/values-ur/strings.xml +++ b/auth/src/main/res/values-ur/strings.xml @@ -12,15 +12,24 @@ فون ای میل Google کے ساتھ سائن ان کریں + Google کے ساتھ سائن ان کریں Facebook کے ساتھ سائن ان کریں + Facebook کے ساتھ سائن ان کریں Twitter کے ساتھ سائن ان کریں + Twitter کے ساتھ سائن ان کریں GitHub کے ساتھ سائن ان کریں + GitHub کے ساتھ سائن ان کریں ای میل کے ساتھ سائن ان کریں + ای میل کے ساتھ سائن ان کریں فون کے ساتھ سائں ان کریں + فون کے ساتھ سائں ان کریں مہمان کے طور پر جاری رکھیں Apple کے ساتھ سائن ان کریں + Apple کے ساتھ سائن ان کریں Microsoft کے ساتھ سائن ان کریں + Microsoft کے ساتھ سائن ان کریں Yahoo کے ساتھ سائن ان کریں + Yahoo کے ساتھ سائن ان کریں اگلا ای میل فون نمبر diff --git a/auth/src/main/res/values-vi/strings.xml b/auth/src/main/res/values-vi/strings.xml index b77158b5d..f67c4b703 100755 --- a/auth/src/main/res/values-vi/strings.xml +++ b/auth/src/main/res/values-vi/strings.xml @@ -12,15 +12,24 @@ Điện thoại Email Đăng nhập bằng Google + Đăng nhập bằng Google Đăng nhập bằng Facebook + Đăng nhập bằng Facebook Đăng nhập bằng Twitter + Đăng nhập bằng Twitter Đăng nhập bằng GitHub + Đăng nhập bằng GitHub Đăng nhập bằng email + Đăng nhập bằng email Đăng nhập bằng điện thoại + Đăng nhập bằng điện thoại Tiếp tục với vai trò người dùng khách Đăng nhập bằng Apple + Đăng nhập bằng Apple Đăng nhập bằng Microsoft + Đăng nhập bằng Microsoft Đăng nhập bằng Yahoo + Đăng nhập bằng Yahoo Tiếp Email Số điện thoại diff --git a/auth/src/main/res/values-zh-rCN/strings.xml b/auth/src/main/res/values-zh-rCN/strings.xml index 6fef868ca..64f584490 100755 --- a/auth/src/main/res/values-zh-rCN/strings.xml +++ b/auth/src/main/res/values-zh-rCN/strings.xml @@ -12,15 +12,24 @@ 电话 电子邮件 使用 Google 帐号登录 + 使用 Google 帐号登录 使用 Facebook 帐号登录 + 使用 Facebook 帐号登录 使用 Twitter 帐号登录 + 使用 Twitter 帐号登录 使用 GitHub 帐号登录 + 使用 GitHub 帐号登录 使用电子邮件地址登录 + 使用电子邮件地址登录 使用电话号码登录 + 使用电话号码登录 以访客身份继续 使用 Apple 帐号登录 + 使用 Apple 帐号登录 使用 Microsoft 帐号登录 + 使用 Microsoft 帐号登录 使用 Yahoo 帐号登录 + 使用 Yahoo 帐号登录 继续 电子邮件地址 电话号码 diff --git a/auth/src/main/res/values-zh-rHK/strings.xml b/auth/src/main/res/values-zh-rHK/strings.xml index 34a72a156..a2b6f9530 100755 --- a/auth/src/main/res/values-zh-rHK/strings.xml +++ b/auth/src/main/res/values-zh-rHK/strings.xml @@ -12,15 +12,24 @@ 電話 電子郵件 使用 Google 帳戶登入 + 使用 Google 帳戶登入 使用 Facebook 帳戶登入 + 使用 Facebook 帳戶登入 使用 Twitter 帳戶登入 + 使用 Twitter 帳戶登入 使用 GitHub 登入 + 使用 GitHub 登入 使用電子郵件地址登入 + 使用電子郵件地址登入 使用電話號碼登入 + 使用電話號碼登入 繼續以訪客身分使用 使用 Apple 帳戶登入 + 使用 Apple 帳戶登入 使用 Microsoft 帳戶登入 + 使用 Microsoft 帳戶登入 使用 Yahoo 帳戶登入 + 使用 Yahoo 帳戶登入 繼續 電子郵件 電話號碼 diff --git a/auth/src/main/res/values-zh-rTW/strings.xml b/auth/src/main/res/values-zh-rTW/strings.xml index e890b3915..97939b4c9 100755 --- a/auth/src/main/res/values-zh-rTW/strings.xml +++ b/auth/src/main/res/values-zh-rTW/strings.xml @@ -12,15 +12,24 @@ 電話 電子郵件 使用 Google 帳戶登入 + 使用 Google 帳戶登入 使用 Facebook 帳戶登入 + 使用 Facebook 帳戶登入 使用 Twitter 帳戶登入 + 使用 Twitter 帳戶登入 使用 GitHub 登入 + 使用 GitHub 登入 使用電子郵件地址登入 + 使用電子郵件地址登入 使用電話號碼登入 + 使用電話號碼登入 繼續以訪客身分使用 使用 Apple 帳戶登入 + 使用 Apple 帳戶登入 使用 Microsoft 帳戶登入 + 使用 Microsoft 帳戶登入 使用 Yahoo 帳戶登入 + 使用 Yahoo 帳戶登入 繼續 電子郵件 電話號碼 diff --git a/auth/src/main/res/values-zh/strings.xml b/auth/src/main/res/values-zh/strings.xml index 8e1db852e..a845558c9 100755 --- a/auth/src/main/res/values-zh/strings.xml +++ b/auth/src/main/res/values-zh/strings.xml @@ -12,15 +12,24 @@ 电话 电子邮件 使用 Google 帐号登录 + 使用 Google 帐号登录 使用 Facebook 帐号登录 + 使用 Facebook 帐号登录 使用 Twitter 帐号登录 + 使用 Twitter 帐号登录 使用 GitHub 帐号登录 + 使用 GitHub 帐号登录 使用电子邮件地址登录 + 使用电子邮件地址登录 使用电话号码登录 + 使用电话号码登录 以访客身份继续 使用 Apple 帐号登录 + 使用 Apple 帐号登录 使用 Microsoft 帐号登录 + 使用 Microsoft 帐号登录 使用 Yahoo 帐号登录 + 使用 Yahoo 帐号登录 继续 电子邮件地址 电话号码 diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml index 4d0018336..ee88a98a5 100644 --- a/auth/src/main/res/values/strings.xml +++ b/auth/src/main/res/values/strings.xml @@ -30,6 +30,15 @@ Sign in with Apple Sign in with Microsoft Sign in with Yahoo + Continue with Google + Continue with Facebook + Continue with X + Continue with GitHub + Continue with email + Continue with phone + Continue with Apple + Continue with Microsoft + Continue with Yahoo Signed in as %1$s diff --git a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt index b6561315d..05f61c538 100644 --- a/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/FirebaseAuthUITest.kt @@ -18,6 +18,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.configuration.auth_provider.AuthProvider import com.google.android.gms.tasks.TaskCompletionSource import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp @@ -26,6 +27,7 @@ import com.google.firebase.FirebaseOptions import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuthRecentLoginRequiredException import com.google.firebase.auth.FirebaseUser +import com.google.firebase.auth.UserInfo import kotlinx.coroutines.CancellationException import kotlinx.coroutines.test.runTest import org.junit.After @@ -401,6 +403,156 @@ class FirebaseAuthUITest { } } + @Test + fun `signOut() calls Google sign out when user provider is Google`() = runTest { + // Setup mock user with Google provider + val mockUser = mock(FirebaseUser::class.java) + val mockUserInfo = mock(UserInfo::class.java) + `when`(mockUserInfo.providerId).thenReturn("google.com") + `when`(mockUser.providerId).thenReturn("google.com") + `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) + + // Setup mock auth + val mockAuth = mock(FirebaseAuth::class.java) + `when`(mockAuth.currentUser).thenReturn(mockUser) + doNothing().`when`(mockAuth).signOut() + + // Create mock credential manager provider + var googleSignOutCalled = false + val mockCredentialManagerProvider = object : AuthProvider.Google.CredentialManagerProvider { + override suspend fun getGoogleCredential( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + serverClientId: String, + filterByAuthorizedAccounts: Boolean, + autoSelectEnabled: Boolean, + ): AuthProvider.Google.GoogleSignInResult { + throw UnsupportedOperationException("Not used in this test") + } + + override suspend fun clearCredentialState( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + ) { + googleSignOutCalled = true + } + } + + // Create instance with mock auth and inject test provider + val instance = FirebaseAuthUI.create(defaultApp, mockAuth) + instance.testCredentialManagerProvider = mockCredentialManagerProvider + val context = ApplicationProvider.getApplicationContext() + + // Perform sign out + instance.signOut(context) + + // Verify Google sign out was called + assertThat(googleSignOutCalled).isTrue() + verify(mockAuth).signOut() + } + + @Test + fun `signOut() calls Facebook sign out when user provider is Facebook`() = runTest { + // Setup mock user with Facebook provider + val mockUser = mock(FirebaseUser::class.java) + val mockUserInfo = mock(UserInfo::class.java) + `when`(mockUserInfo.providerId).thenReturn("facebook.com") + `when`(mockUser.providerId).thenReturn("facebook.com") + `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) + + // Setup mock auth + val mockAuth = mock(FirebaseAuth::class.java) + `when`(mockAuth.currentUser).thenReturn(mockUser) + doNothing().`when`(mockAuth).signOut() + + // Create mock login manager provider + var facebookSignOutCalled = false + val mockLoginManagerProvider = object : AuthProvider.Facebook.LoginManagerProvider { + override fun getCredential(token: String): com.google.firebase.auth.AuthCredential { + throw UnsupportedOperationException("Not used in this test") + } + + override fun logOut() { + facebookSignOutCalled = true + } + } + + // Create instance with mock auth and inject test provider + val instance = FirebaseAuthUI.create(defaultApp, mockAuth) + instance.testLoginManagerProvider = mockLoginManagerProvider + val context = ApplicationProvider.getApplicationContext() + + // Perform sign out + instance.signOut(context) + + // Verify Facebook sign out was called + assertThat(facebookSignOutCalled).isTrue() + verify(mockAuth).signOut() + } + + @Test + fun `signOut() does not call Google or Facebook sign out when user provider is Email`() = + runTest { + // Setup mock user with Email provider + val mockUser = mock(FirebaseUser::class.java) + val mockUserInfo = mock(UserInfo::class.java) + `when`(mockUserInfo.providerId).thenReturn("password") + `when`(mockUser.providerId).thenReturn("password") + `when`(mockUser.providerData).thenReturn(listOf(mockUserInfo)) + + // Setup mock auth + val mockAuth = mock(FirebaseAuth::class.java) + `when`(mockAuth.currentUser).thenReturn(mockUser) + doNothing().`when`(mockAuth).signOut() + + // Create mock providers + var googleSignOutCalled = false + val mockCredentialManagerProvider = + object : AuthProvider.Google.CredentialManagerProvider { + override suspend fun getGoogleCredential( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + serverClientId: String, + filterByAuthorizedAccounts: Boolean, + autoSelectEnabled: Boolean, + ): AuthProvider.Google.GoogleSignInResult { + throw UnsupportedOperationException("Not used in this test") + } + + override suspend fun clearCredentialState( + context: Context, + credentialManager: androidx.credentials.CredentialManager, + ) { + googleSignOutCalled = true + } + } + + var facebookSignOutCalled = false + val mockLoginManagerProvider = object : AuthProvider.Facebook.LoginManagerProvider { + override fun getCredential(token: String): com.google.firebase.auth.AuthCredential { + throw UnsupportedOperationException("Not used in this test") + } + + override fun logOut() { + facebookSignOutCalled = true + } + } + + // Create instance with mock auth and inject test providers + val instance = FirebaseAuthUI.create(defaultApp, mockAuth) + instance.testCredentialManagerProvider = mockCredentialManagerProvider + instance.testLoginManagerProvider = mockLoginManagerProvider + val context = ApplicationProvider.getApplicationContext() + + // Perform sign out + instance.signOut(context) + + // Verify neither Google nor Facebook sign out was called + assertThat(googleSignOutCalled).isFalse() + assertThat(facebookSignOutCalled).isFalse() + verify(mockAuth).signOut() + } + // ============================================================================================= // Delete Account Tests // ============================================================================================= 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/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt index b5109bc36..155de1f82 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/FacebookAuthProviderFirebaseAuthUI.kt @@ -65,7 +65,7 @@ class FacebookAuthProviderFirebaseAuthUITest { private lateinit var mockFirebaseAuth: FirebaseAuth @Mock - private lateinit var mockFBAuthCredentialProvider: AuthProvider.Facebook.CredentialProvider + private lateinit var mockFBAuthCredentialProvider: AuthProvider.Facebook.LoginManagerProvider private lateinit var firebaseApp: FirebaseApp private lateinit var applicationContext: Context diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt index 3e5fdb981..1d027d9ea 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/OAuthProviderFirebaseAuthUITest.kt @@ -131,9 +131,10 @@ class OAuthProviderFirebaseAuthUITest { } instance.signInWithProvider( + applicationContext, config = config, activity = mockActivity, - provider = githubProvider + provider = githubProvider, ) // Verify OAuth provider was built and used @@ -186,6 +187,7 @@ class OAuthProviderFirebaseAuthUITest { } instance.signInWithProvider( + applicationContext, config = config, activity = mockActivity, provider = yahooProvider @@ -231,6 +233,7 @@ class OAuthProviderFirebaseAuthUITest { try { instance.signInWithProvider( + applicationContext, config = config, activity = mockActivity, provider = githubProvider @@ -271,6 +274,7 @@ class OAuthProviderFirebaseAuthUITest { try { instance.signInWithProvider( + applicationContext, config = config, activity = mockActivity, provider = microsoftProvider diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt index c836ac218..81386183c 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/PhoneAuthProviderFirebaseAuthUITest.kt @@ -317,6 +317,7 @@ class PhoneAuthProviderFirebaseAuthUITest { } val result = instance.submitVerificationCode( + applicationContext, config = config, verificationId = "test-verification-id", code = "123456", @@ -359,6 +360,7 @@ class PhoneAuthProviderFirebaseAuthUITest { } val result = instance.signInWithPhoneAuthCredential( + applicationContext, config = config, credential = mockCredential ) @@ -399,6 +401,7 @@ class PhoneAuthProviderFirebaseAuthUITest { } val result = instance.signInWithPhoneAuthCredential( + applicationContext, config = config, credential = mockCredential ) 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/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt b/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt index adf1a939b..ca53181eb 100644 --- a/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/ui/components/ErrorRecoveryDialogLogicTest.kt @@ -56,7 +56,43 @@ class ErrorRecoveryDialogLogicTest { // Act val message = getRecoveryMessage(error, mockStringProvider) - // Assert + // Assert - Should show the actual error message since it's not the generic fallback + Truth.assertThat(message).isEqualTo("Invalid credentials") + } + + @Test + fun `getRecoveryMessage returns actual Firebase error message for InvalidCredentialsException`() { + // Arrange - Simulate a real Firebase error message + val error = AuthException.InvalidCredentialsException("The email address is badly formatted.") + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert - Should show the actual Firebase error, not the generic message + Truth.assertThat(message).isEqualTo("The email address is badly formatted.") + } + + @Test + fun `getRecoveryMessage returns generic message for InvalidCredentialsException with generic error text`() { + // Arrange - When error message is the generic fallback + val error = AuthException.InvalidCredentialsException("Invalid credentials provided") + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert - Should show the localized generic message + Truth.assertThat(message).isEqualTo("Incorrect password.") + } + + @Test + fun `getRecoveryMessage returns generic message for InvalidCredentialsException with blank message`() { + // Arrange + val error = AuthException.InvalidCredentialsException("") + + // Act + val message = getRecoveryMessage(error, mockStringProvider) + + // Assert - Should show the localized generic message Truth.assertThat(message).isEqualTo("Incorrect password.") } @@ -245,7 +281,11 @@ class ErrorRecoveryDialogLogicTest { private fun getRecoveryMessage(error: AuthException, stringProvider: AuthUIStringProvider): String { return when (error) { is AuthException.NetworkException -> stringProvider.networkErrorRecoveryMessage - is AuthException.InvalidCredentialsException -> stringProvider.invalidCredentialsRecoveryMessage + is AuthException.InvalidCredentialsException -> { + // Use the actual error message from Firebase if available, otherwise fallback to generic message + error.message?.takeIf { it.isNotBlank() && it != "Invalid credentials provided" } + ?: stringProvider.invalidCredentialsRecoveryMessage + } is AuthException.UserNotFoundException -> stringProvider.userNotFoundRecoveryMessage is AuthException.WeakPasswordException -> { val baseMessage = stringProvider.weakPasswordRecoveryMessage diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 740e3213e..f82a29287 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -69,7 +69,7 @@ object Config { } object Firebase { - const val bom = "com.google.firebase:firebase-bom:33.9.0" + const val bom = "com.google.firebase:firebase-bom:34.7.0" const val auth = "com.google.firebase:firebase-auth" const val database = "com.google.firebase:firebase-database" const val firestore = "com.google.firebase:firebase-firestore" 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 b79ab3772..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 @@ -194,6 +196,7 @@ class AccessibilityTest { onEmailChange = {}, onPasswordChange = {}, onSignInClick = {}, + onRetrievedCredential = {}, onGoToEmailLinkSignIn = {}, onGoToSignUp = {}, onGoToResetPassword = {}, @@ -284,6 +287,7 @@ class AccessibilityTest { password = "", onEmailChange = {}, onPasswordChange = {}, + onRetrievedCredential = {}, onSignInClick = {}, onGoToEmailLinkSignIn = {}, onGoToSignUp = {}, diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/AnonymousAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/AnonymousAuthScreenTest.kt index ba68bad9a..59c5d829a 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/AnonymousAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/AnonymousAuthScreenTest.kt @@ -181,6 +181,7 @@ class AnonymousAuthScreenTest { ) } isAnonymousUpgradeEnabled = true + isCredentialManagerEnabled = false } // Track auth state changes diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt index 50fca4728..423aa8d62 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/EmailAuthScreenTest.kt @@ -24,6 +24,10 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput +import androidx.credentials.CreatePasswordRequest +import androidx.credentials.CredentialManager +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import com.firebase.ui.auth.AuthState @@ -35,6 +39,8 @@ import com.firebase.ui.auth.configuration.auth_provider.AuthProvider 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.credentialmanager.CredentialManagerProvider +import com.firebase.ui.auth.credentialmanager.PasswordCredentialHandler import com.firebase.ui.auth.testutil.AUTH_STATE_WAIT_TIMEOUT_MS import com.firebase.ui.auth.testutil.EmailLinkTestActivity import com.firebase.ui.auth.testutil.EmulatorAuthApi @@ -43,18 +49,27 @@ import com.firebase.ui.auth.testutil.verifyEmailInEmulator import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions -import com.google.firebase.auth.ActionCodeSettings import com.google.firebase.auth.actionCodeSettings +import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assume import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config +import androidx.credentials.PasswordCredential as AndroidPasswordCredential @Config(sdk = [34]) @RunWith(RobolectricTestRunner::class) @@ -69,9 +84,14 @@ class EmailAuthScreenTest { private lateinit var authUI: FirebaseAuthUI private lateinit var emulatorApi: EmulatorAuthApi + @Mock + private lateinit var mockCredentialManager: CredentialManager + + private lateinit var closeable: AutoCloseable + @Before fun setUp() { - MockitoAnnotations.openMocks(this) + closeable = MockitoAnnotations.openMocks(this) applicationContext = ApplicationProvider.getApplicationContext() @@ -104,15 +124,28 @@ class EmailAuthScreenTest { // Clear emulator data emulatorApi.clearEmulatorData() + + // Set up test credential manager provider + PasswordCredentialHandler.testCredentialManagerProvider = + object : CredentialManagerProvider { + override fun getCredentialManager(context: Context): CredentialManager { + return mockCredentialManager + } + } } @After fun tearDown() { + closeable.close() + // Clean up after each test to prevent test pollution FirebaseAuthUI.clearInstanceCache() // Clear emulator data emulatorApi.clearEmulatorData() + + // Clear test credential manager provider + PasswordCredentialHandler.testCredentialManagerProvider = null } @Test @@ -127,6 +160,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } composeAndroidTestRule.setContent { @@ -166,6 +200,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } // Track auth state changes @@ -259,6 +294,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } // Track auth state changes @@ -333,6 +369,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } // Track auth state changes @@ -422,6 +459,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } // Track auth state changes @@ -514,6 +552,7 @@ class EmailAuthScreenTest { ) ) } + isCredentialManagerEnabled = false } // Track auth state changes and email link (lifted state) @@ -620,25 +659,26 @@ class EmailAuthScreenTest { // Use ActivityScenario to launch EmailLinkTestActivity with the deep link intent // This properly simulates the Android deep link flow - when a user clicks the email link, // Android launches the app with an ACTION_VIEW intent - val extractedEmailLink = ActivityScenario.launch(deepLinkIntent).use { scenario -> - var emailLinkFromIntent: String? = null + val extractedEmailLink = + ActivityScenario.launch(deepLinkIntent).use { scenario -> + var emailLinkFromIntent: String? = null - scenario.onActivity { activity -> - // Verify the intent was received correctly - assertThat(activity.intent.action).isEqualTo(Intent.ACTION_VIEW) - assertThat(activity.intent.data).isEqualTo(deepLinkUri) + scenario.onActivity { activity -> + // Verify the intent was received correctly + assertThat(activity.intent.action).isEqualTo(Intent.ACTION_VIEW) + assertThat(activity.intent.data).isEqualTo(deepLinkUri) - // Verify the activity extracted the email link - assertThat(activity.emailLinkFromIntent).isNotNull() - assertThat(activity.emailLinkFromIntent).isEqualTo(emailLinkFromEmulator) + // Verify the activity extracted the email link + assertThat(activity.emailLinkFromIntent).isNotNull() + assertThat(activity.emailLinkFromIntent).isEqualTo(emailLinkFromEmulator) - emailLinkFromIntent = activity.emailLinkFromIntent + emailLinkFromIntent = activity.emailLinkFromIntent - println("TEST: Email link extracted by activity: $emailLinkFromIntent") - } + println("TEST: Email link extracted by activity: $emailLinkFromIntent") + } - emailLinkFromIntent - } + emailLinkFromIntent + } requireNotNull(extractedEmailLink) { "Failed to extract email link from intent" } @@ -672,6 +712,340 @@ class EmailAuthScreenTest { .assertIsDisplayed() } + @Test + fun `sign up saves credential, then sign in retrieves it and auto-signs in`() = runBlocking { + val name = "Credential Test User" + val email = "credential-test-${System.currentTimeMillis()}@example.com" + val password = "Test@1234" + + // Mock credential manager responses + val mockPasswordCredential = mock() + whenever(mockPasswordCredential.id).thenReturn(email) + whenever(mockPasswordCredential.password).thenReturn(password) + + val mockCredentialResponse = mock() + whenever(mockCredentialResponse.credential).thenReturn(mockPasswordCredential) + + // Mock successful credential save + whenever(mockCredentialManager.createCredential(any(), any())) + .thenReturn(mock()) + + // Mock successful credential retrieval + whenever(mockCredentialManager.getCredential(any(), any())) + .thenReturn(mockCredentialResponse) + + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + } + + // Track auth state changes + var currentAuthState: AuthState = AuthState.Idle + + composeAndroidTestRule.setContent { + TestFirebaseAuthScreen(configuration = configuration, authUI = authUI) + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + // STEP 1: Sign up and verify credential saved + println("TEST: Starting sign-up flow...") + + // Click on email provider + composeAndroidTestRule.onNodeWithText(stringProvider.signInWithEmail) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.waitForIdle() + + // Click sign-up + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .assertIsDisplayed() + .performClick() + + // Fill in sign-up form + composeAndroidTestRule.onNodeWithText(stringProvider.emailHint) + .performTextInput(email) + composeAndroidTestRule.onNodeWithText(stringProvider.nameHint) + .performTextInput(name) + composeAndroidTestRule.onNodeWithText(stringProvider.passwordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.confirmPasswordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .performScrollTo() + .performClick() + + shadowOf(Looper.getMainLooper()).idle() + + // Wait for sign-up to complete + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.RequiresEmailVerification + } + + shadowOf(Looper.getMainLooper()).idle() + + // Verify user was created + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) + + // Verify credentials were saved + verify(mockCredentialManager, times(1)).createCredential( + any(), + any() + ) + println("TEST: Sign-up complete, credentials saved") + + // STEP 2: Sign out to test credential retrieval + println("TEST: Signing out to test credential retrieval...") + authUI.auth.signOut() + shadowOf(Looper.getMainLooper()).idle() + composeAndroidTestRule.waitForIdle() + assertThat(authUI.auth.currentUser).isNull() + + // STEP 3: Navigate to SignInUI screen to trigger credential retrieval + println("TEST: Navigating to sign-in screen to trigger credential retrieval...") + + // Click on email provider to show SignInUI, which will trigger auto-retrieval + composeAndroidTestRule.onNodeWithText(stringProvider.signInWithEmail) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.waitForIdle() + shadowOf(Looper.getMainLooper()).idle() + + // SignInUI's LaunchedEffect should now trigger credential retrieval and auto-sign-in + println("TEST: Waiting for automatic credential retrieval and auto-sign-in...") + + // Wait for auto-sign-in to complete + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.RequiresEmailVerification + } + + shadowOf(Looper.getMainLooper()).idle() + + // Verify credentials were retrieved + verify(mockCredentialManager, times(1)).getCredential(any(), any()) + + // Verify auto-sign-in succeeded + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) + + println("TEST: Credential retrieval and auto-sign-in successful") + } + + @Test + fun `sign in with retrieved credential does not prompt to save again`() = runBlocking { + val name = "No Duplicate Save Test User" + val email = "no-duplicate-${System.currentTimeMillis()}@example.com" + val password = "Test@1234" + + // Mock credential manager responses + val mockPasswordCredential = mock() + whenever(mockPasswordCredential.id).thenReturn(email) + whenever(mockPasswordCredential.password).thenReturn(password) + + val mockCredentialResponse = mock() + whenever(mockCredentialResponse.credential).thenReturn(mockPasswordCredential) + + // Mock successful credential save (should only be called once during sign-up) + whenever(mockCredentialManager.createCredential(any(), any())) + .thenReturn(mock()) + + // Mock successful credential retrieval + whenever(mockCredentialManager.getCredential(any(), any())) + .thenReturn(mockCredentialResponse) + + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + } + + var currentAuthState: AuthState = AuthState.Idle + + composeAndroidTestRule.setContent { + TestFirebaseAuthScreen(configuration = configuration, authUI = authUI) + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + // STEP 1: Sign up and save credential + println("TEST: Starting sign-up flow...") + + composeAndroidTestRule.onNodeWithText(stringProvider.signInWithEmail) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.waitForIdle() + + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.onNodeWithText(stringProvider.emailHint) + .performTextInput(email) + composeAndroidTestRule.onNodeWithText(stringProvider.nameHint) + .performTextInput(name) + composeAndroidTestRule.onNodeWithText(stringProvider.passwordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.confirmPasswordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .performScrollTo() + .performClick() + + shadowOf(Looper.getMainLooper()).idle() + + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.RequiresEmailVerification + } + + shadowOf(Looper.getMainLooper()).idle() + + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) + + // Verify credentials were saved during sign-up (first call) + verify(mockCredentialManager, times(1)).createCredential(any(), any()) + println("TEST: Sign-up complete, credentials saved (createCredential called once)") + + // STEP 2: Sign out + println("TEST: Signing out...") + authUI.auth.signOut() + shadowOf(Looper.getMainLooper()).idle() + composeAndroidTestRule.waitForIdle() + assertThat(authUI.auth.currentUser).isNull() + + // STEP 3: Navigate to SignInUI to trigger credential retrieval + println("TEST: Navigating to sign-in screen...") + + composeAndroidTestRule.onNodeWithText(stringProvider.signInWithEmail) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.waitForIdle() + shadowOf(Looper.getMainLooper()).idle() + + println("TEST: Waiting for automatic credential retrieval and auto-sign-in...") + + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.RequiresEmailVerification + } + + shadowOf(Looper.getMainLooper()).idle() + + // Verify credentials were retrieved (may be called multiple times due to navigation/remounting) + verify(mockCredentialManager, atLeast(1)).getCredential(any(), any()) + + // Verify auto-sign-in succeeded + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) + + // CRITICAL: Verify createCredential was NOT called again (still only 1 time from sign-up) + // This is the main point of this test - verifying skipCredentialSave logic works + verify(mockCredentialManager, times(1)).createCredential(any(), any()) + println("TEST: Verified no duplicate save prompt (createCredential still called only once)") + } + + @Test + fun `credential manager disabled skips save and retrieve`() = runBlocking { + val name = "Disabled Test User" + val email = "disabled-cm-${System.currentTimeMillis()}@example.com" + val password = "Test@1234" + + val configuration = authUIConfiguration { + context = applicationContext + providers { + provider( + AuthProvider.Email( + emailLinkActionCodeSettings = null, + passwordValidationRules = emptyList() + ) + ) + } + isCredentialManagerEnabled = false // DISABLED + } + + var currentAuthState: AuthState = AuthState.Idle + + composeAndroidTestRule.setContent { + TestFirebaseAuthScreen(configuration = configuration, authUI = authUI) + val authState by authUI.authStateFlow().collectAsState(AuthState.Idle) + currentAuthState = authState + } + + // Sign up + composeAndroidTestRule.onNodeWithText(stringProvider.signInWithEmail) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.waitForIdle() + + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .assertIsDisplayed() + .performClick() + + composeAndroidTestRule.onNodeWithText(stringProvider.emailHint) + .performTextInput(email) + composeAndroidTestRule.onNodeWithText(stringProvider.nameHint) + .performTextInput(name) + composeAndroidTestRule.onNodeWithText(stringProvider.passwordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.confirmPasswordHint) + .performScrollTo() + .performTextInput(password) + composeAndroidTestRule.onNodeWithText(stringProvider.signupPageTitle.uppercase()) + .performScrollTo() + .performClick() + + shadowOf(Looper.getMainLooper()).idle() + + // Wait for sign-up + composeAndroidTestRule.waitUntil(timeoutMillis = AUTH_STATE_WAIT_TIMEOUT_MS) { + shadowOf(Looper.getMainLooper()).idle() + currentAuthState is AuthState.RequiresEmailVerification + } + + shadowOf(Looper.getMainLooper()).idle() + + // Verify credentials were not saved + verify(mockCredentialManager, never()).createCredential( + any(), + any() + ) + + // Verify user created + assertThat(authUI.auth.currentUser).isNotNull() + assertThat(authUI.auth.currentUser!!.email).isEqualTo(email) + + // With isCredentialManagerEnabled=false, PasswordCredentialHandler won't be invoked + // Test passes if sign-up works without credential manager + println("TEST: With credential manager disabled, sign-up works correctly") + } + @Composable private fun TestFirebaseAuthScreen( configuration: AuthUIConfiguration, diff --git a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/GoogleAuthScreenTest.kt b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/GoogleAuthScreenTest.kt index eab1c29d2..64103ec32 100644 --- a/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/GoogleAuthScreenTest.kt +++ b/e2eTest/src/test/java/com/firebase/ui/auth/ui/screens/GoogleAuthScreenTest.kt @@ -119,6 +119,11 @@ class GoogleAuthScreenTest { autoSelectEnabled = autoSelectEnabled ) } + + override suspend fun clearCredentialState( + context: Context, + credentialManager: CredentialManager, + ) {} } authUI.testCredentialManagerProvider = testCredentialManagerProvider