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 5a19beac4..a22e6028a 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 = true + private const val USE_AUTH_EMULATOR = false private const val AUTH_EMULATOR_HOST = "10.0.2.2" private const val AUTH_EMULATOR_PORT = 9099 } diff --git a/auth/README.md b/auth/README.md index ecc3208aa..6061eea06 100644 --- a/auth/README.md +++ b/auth/README.md @@ -138,6 +138,7 @@ If using Facebook Login, add your Facebook App ID to `strings.xml`: YOUR_FACEBOOK_APP_ID fbYOUR_FACEBOOK_APP_ID + CHANGE-ME ``` @@ -489,9 +490,6 @@ Configure Facebook Login with optional permissions: ```kotlin val facebookProvider = AuthProvider.Facebook( - // Optional: Facebook application ID (reads from strings.xml if not provided) - applicationId = "YOUR_FACEBOOK_APP_ID", - // Optional: Permissions to request (default: ["email", "public_profile"]) scopes = listOf("email", "public_profile", "user_friends"), 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 1b659a8fc..5cf392a8c 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 @@ -485,7 +485,21 @@ abstract class AuthProvider(open val providerId: String, open val providerName: /** * The OAuth 2.0 client ID for your server. */ - val serverClientId: String?, + var serverClientId: String?, + + /** + * Whether to filter by authorized accounts. + * When true, only shows Google accounts that have previously authorized this app. + * Defaults to true, with automatic fallback to false if no authorized accounts found. + */ + val filterByAuthorizedAccounts: Boolean = true, + + /** + * Whether to enable auto-select for single account scenarios. + * When true, automatically selects the account if only one is available. + * Defaults to false for better user control. + */ + val autoSelectEnabled: Boolean = false, /** * A map of custom OAuth parameters. @@ -505,8 +519,9 @@ abstract class AuthProvider(open val providerId: String, open val providerName: " default_web_client_id string wasn't populated.", R.string.default_web_client_id ) + serverClientId = context.getString(R.string.default_web_client_id) } else { - require(serverClientId.isNotBlank()) { + require(serverClientId!!.isNotBlank()) { "Server client ID cannot be blank." } } @@ -529,7 +544,7 @@ abstract class AuthProvider(open val providerId: String, open val providerName: val credential: AuthCredential, val idToken: String, val displayName: String?, - val photoUrl: Uri? + val photoUrl: Uri?, ) /** @@ -567,7 +582,7 @@ abstract class AuthProvider(open val providerId: String, open val providerName: credentialManager: CredentialManager, serverClientId: String, filterByAuthorizedAccounts: Boolean, - autoSelectEnabled: Boolean + autoSelectEnabled: Boolean, ): GoogleSignInResult suspend fun clearCredentialState( @@ -600,8 +615,10 @@ abstract class AuthProvider(open val providerId: String, open val providerName: .build() val result = credentialManager.getCredential(context, request) - val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(result.credential.data) - val credential = GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null) + val googleIdTokenCredential = + GoogleIdTokenCredential.createFrom(result.credential.data) + val credential = + GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null) return GoogleSignInResult( credential = credential, @@ -624,11 +641,6 @@ abstract class AuthProvider(open val providerId: String, open val providerName: * Facebook Login provider configuration. */ class Facebook( - /** - * The Facebook application ID. - */ - val applicationId: String? = null, - /** * The list of scopes (permissions) to request. Defaults to email and public_profile. */ @@ -653,18 +665,26 @@ abstract class AuthProvider(open val providerId: String, open val providerName: ) } - if (applicationId == null) { - Preconditions.checkConfigured( - context, - "Facebook provider unconfigured. Make sure to " + - "add a `facebook_application_id` string or provide applicationId parameter.", - R.string.facebook_application_id - ) - } else { - require(applicationId.isNotBlank()) { - "Facebook application ID cannot be blank" - } - } + Preconditions.checkConfigured( + context, + "Facebook provider unconfigured. Make sure to " + + "add a `facebook_application_id` string to your strings.xml", + R.string.facebook_application_id + ) + + Preconditions.checkConfigured( + context, + "Facebook provider unconfigured. Make sure to " + + "add a `facebook_login_protocol_scheme` string to your strings.xml", + R.string.facebook_login_protocol_scheme + ) + + Preconditions.checkConfigured( + context, + "Facebook provider unconfigured. Make sure to " + + "add a `facebook_client_token` string to your strings.xml", + R.string.facebook_client_token + ) } /** 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 2afdb3599..4d18cb0a9 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 @@ -133,14 +133,48 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle( } } - val result = + // Try with configured filterByAuthorizedAccounts setting + // If default (true), fallback to false if no authorized accounts found + // See: https://developer.android.com/identity/sign-in/credential-manager-siwg#siwg-button + val result = if (provider.filterByAuthorizedAccounts) { + // Default behavior: Try authorized accounts first, fallback to all accounts + try { + (testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential( + context = context, + credentialManager = CredentialManager.create(context), + serverClientId = provider.serverClientId!!, + filterByAuthorizedAccounts = true, + autoSelectEnabled = provider.autoSelectEnabled + ) + } catch (e: NoCredentialException) { + // No authorized accounts found, try again with all accounts for sign-up flow + Log.d("GoogleAuthProvider", "No authorized accounts found, showing all Google accounts for sign-up") + try { + (testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential( + context = context, + credentialManager = CredentialManager.create(context), + serverClientId = provider.serverClientId!!, + filterByAuthorizedAccounts = false, + autoSelectEnabled = provider.autoSelectEnabled + ) + } catch (fallbackException: NoCredentialException) { + // No Google accounts available on device at all + throw AuthException.UnknownException( + message = "No Google accounts available.\n\nPlease add a Google account to your device and try again.", + cause = fallbackException + ) + } + } + } else { + // Developer explicitly wants to show all accounts (no fallback needed) (testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential( context = context, credentialManager = CredentialManager.create(context), serverClientId = provider.serverClientId!!, - filterByAuthorizedAccounts = true, - autoSelectEnabled = false + filterByAuthorizedAccounts = false, + autoSelectEnabled = provider.autoSelectEnabled ) + } idTokenFromResult = result.idToken signInAndLinkWithCredential( 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 80cf85ad6..4afcfa84b 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 @@ -294,12 +294,13 @@ class AuthUIConfigurationTest { } @Test + @Config(manifest = Config.NONE, qualifiers = "night") fun `validation accepts all supported providers`() { val config = authUIConfiguration { context = applicationContext providers { provider(AuthProvider.Google(scopes = listOf(), serverClientId = "test_client_id")) - provider(AuthProvider.Facebook(applicationId = "test_app_id")) + provider(AuthProvider.Facebook()) provider(AuthProvider.Twitter(customParameters = mapOf())) provider(AuthProvider.Github(customParameters = mapOf())) provider(AuthProvider.Microsoft(customParameters = mapOf(), tenant = null)) diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AuthProviderTest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AuthProviderTest.kt index f64bfc2a6..718d38ad3 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AuthProviderTest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/AuthProviderTest.kt @@ -2,6 +2,7 @@ package com.firebase.ui.auth.configuration.auth_provider import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.firebase.ui.auth.R import com.google.common.truth.Truth.assertThat import com.google.firebase.auth.actionCodeSettings import org.junit.Before @@ -264,32 +265,34 @@ class AuthProviderTest { } } + @Test + @Config(manifest = Config.NONE, qualifiers = "night") + fun `google provider assigns default_web_client_id to serverClientId when null`() { + val provider = AuthProvider.Google( + scopes = listOf("email"), + serverClientId = null + ) + + provider.validate(applicationContext) + + assertThat(provider.serverClientId) + .isEqualTo(applicationContext.getString(R.string.default_web_client_id)) + } + // ============================================================================================= // Facebook Provider Tests // ============================================================================================= @Test + @Config(manifest = Config.NONE, qualifiers = "night") fun `facebook provider with valid configuration should succeed`() { - val provider = AuthProvider.Facebook(applicationId = "application_id") + val provider = AuthProvider.Facebook() provider.validate(applicationContext) } @Test - fun `facebook provider with empty application id throws`() { - val provider = AuthProvider.Facebook(applicationId = "") - - try { - provider.validate(applicationContext) - assertThat(false).isTrue() // Should not reach here - } catch (e: Exception) { - assertThat(e).isInstanceOf(IllegalArgumentException::class.java) - assertThat(e.message).isEqualTo("Facebook application ID cannot be blank") - } - } - - @Test - fun `facebook provider validates facebook_application_id when applicationId is null`() { + fun `facebook provider validates facebook_application_id`() { val provider = AuthProvider.Facebook() try { @@ -299,7 +302,7 @@ class AuthProviderTest { assertThat(e).isInstanceOf(IllegalStateException::class.java) assertThat(e.message).isEqualTo( "Facebook provider unconfigured. Make sure to " + - "add a `facebook_application_id` string or provide applicationId parameter." + "add a `facebook_application_id` string to your strings.xml" ) } } @@ -400,4 +403,4 @@ class AuthProviderTest { assertThat(e.message).isEqualTo("Button label cannot be null or empty") } } -} \ No newline at end of file +} 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 155de1f82..1e48bae90 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 @@ -103,6 +103,7 @@ class FacebookAuthProviderFirebaseAuthUITest { } @Test + @Config(manifest = Config.NONE, qualifiers = "night") fun `signInWithFacebook - successful sign in signs user in and emits Success authState`() = runTest { val authStateListeners = mutableListOf() doAnswer { invocation -> @@ -118,9 +119,7 @@ class FacebookAuthProviderFirebaseAuthUITest { whenever(mockFirebaseAuth.currentUser).thenReturn(null) val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) - val provider = spy(AuthProvider.Facebook( - applicationId = "000000000000" - )) + val provider = spy(AuthProvider.Facebook()) val config = authUIConfiguration { context = applicationContext providers { @@ -175,6 +174,7 @@ class FacebookAuthProviderFirebaseAuthUITest { } @Test + @Config(manifest = Config.NONE, qualifiers = "night") fun `signInWithFacebook - handles account collision by saving credential and emitting error`() = runTest { EmailLinkPersistenceManager.default.clear(applicationContext) EmailLinkPersistenceManager.default.saveEmail( @@ -185,9 +185,7 @@ class FacebookAuthProviderFirebaseAuthUITest { ) val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) - val provider = spy(AuthProvider.Facebook( - applicationId = "000000000000" - )) + val provider = spy(AuthProvider.Facebook()) val config = authUIConfiguration { context = applicationContext providers { @@ -238,11 +236,10 @@ class FacebookAuthProviderFirebaseAuthUITest { } @Test + @Config(manifest = Config.NONE, qualifiers = "night") fun `signInWithFacebook - converts FacebookException into AuthException`() = runTest { val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) - val provider = spy(AuthProvider.Facebook( - applicationId = "000000000000" - )) + val provider = spy(AuthProvider.Facebook()) val config = authUIConfiguration { context = applicationContext providers { diff --git a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt index 9091e7d7c..2fd855c37 100644 --- a/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt +++ b/auth/src/test/java/com/firebase/ui/auth/configuration/auth_provider/GoogleAuthProviderFirebaseAuthUITest.kt @@ -602,6 +602,200 @@ class GoogleAuthProviderFirebaseAuthUITest { verify(mockFirebaseAuth, never()).signInWithCredential(any()) } + // ============================================================================================= + // signInWithGoogle - Configuration Properties + // ============================================================================================= + + @Test + fun `Sign in with Google with default settings passes filterByAuthorizedAccounts=true`() = runTest { + val mockCredential = mock(AuthCredential::class.java) + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + + val googleSignInResult = AuthProvider.Google.GoogleSignInResult( + credential = mockCredential, + idToken = "test-id-token", + displayName = "Test User", + photoUrl = null + ) + + `when`( + mockCredentialManagerProvider.getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(false) + ) + ).thenReturn(googleSignInResult) + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithCredential(mockCredential)) + .thenReturn(taskCompletionSource.task) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val googleProvider = AuthProvider.Google( + serverClientId = "test-client-id", + scopes = emptyList() + // filterByAuthorizedAccounts defaults to true + // autoSelectEnabled defaults to false + ) + val config = authUIConfiguration { + context = applicationContext + providers { + provider(googleProvider) + } + } + + instance.signInWithGoogle( + context = applicationContext, + config = config, + provider = googleProvider, + authorizationProvider = mockAuthorizationProvider, + credentialManagerProvider = mockCredentialManagerProvider + ) + + // Verify correct parameters were passed + verify(mockCredentialManagerProvider).getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(false) + ) + + verify(mockFirebaseAuth).signInWithCredential(mockCredential) + } + + @Test + fun `Sign in with Google with filterByAuthorizedAccounts=false passes correct parameter`() = runTest { + val mockCredential = mock(AuthCredential::class.java) + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + + val googleSignInResult = AuthProvider.Google.GoogleSignInResult( + credential = mockCredential, + idToken = "test-id-token", + displayName = "Test User", + photoUrl = null + ) + + `when`( + mockCredentialManagerProvider.getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(false), + autoSelectEnabled = eq(false) + ) + ).thenReturn(googleSignInResult) + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithCredential(mockCredential)) + .thenReturn(taskCompletionSource.task) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val googleProvider = AuthProvider.Google( + serverClientId = "test-client-id", + scopes = emptyList(), + filterByAuthorizedAccounts = false + ) + val config = authUIConfiguration { + context = applicationContext + providers { + provider(googleProvider) + } + } + + instance.signInWithGoogle( + context = applicationContext, + config = config, + provider = googleProvider, + authorizationProvider = mockAuthorizationProvider, + credentialManagerProvider = mockCredentialManagerProvider + ) + + // Verify filterByAuthorizedAccounts=false was passed + verify(mockCredentialManagerProvider).getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(false), + autoSelectEnabled = eq(false) + ) + + verify(mockFirebaseAuth).signInWithCredential(mockCredential) + } + + @Test + fun `Sign in with Google with autoSelectEnabled=true passes correct parameter`() = runTest { + val mockCredential = mock(AuthCredential::class.java) + val mockUser = mock(FirebaseUser::class.java) + val mockAuthResult = mock(AuthResult::class.java) + `when`(mockAuthResult.user).thenReturn(mockUser) + + val googleSignInResult = AuthProvider.Google.GoogleSignInResult( + credential = mockCredential, + idToken = "test-id-token", + displayName = "Test User", + photoUrl = null + ) + + `when`( + mockCredentialManagerProvider.getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(true) + ) + ).thenReturn(googleSignInResult) + + val taskCompletionSource = TaskCompletionSource() + taskCompletionSource.setResult(mockAuthResult) + `when`(mockFirebaseAuth.signInWithCredential(mockCredential)) + .thenReturn(taskCompletionSource.task) + `when`(mockFirebaseAuth.currentUser).thenReturn(null) + + val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth) + val googleProvider = AuthProvider.Google( + serverClientId = "test-client-id", + scopes = emptyList(), + autoSelectEnabled = true + ) + val config = authUIConfiguration { + context = applicationContext + providers { + provider(googleProvider) + } + } + + instance.signInWithGoogle( + context = applicationContext, + config = config, + provider = googleProvider, + authorizationProvider = mockAuthorizationProvider, + credentialManagerProvider = mockCredentialManagerProvider + ) + + // Verify autoSelectEnabled=true was passed + verify(mockCredentialManagerProvider).getGoogleCredential( + context = eq(applicationContext), + credentialManager = any(), + serverClientId = eq("test-client-id"), + filterByAuthorizedAccounts = eq(true), + autoSelectEnabled = eq(true) + ) + + verify(mockFirebaseAuth).signInWithCredential(mockCredential) + } + // ============================================================================================= // signInWithGoogle - State Management // ============================================================================================= diff --git a/auth/src/test/res/values-night/config.xml b/auth/src/test/res/values-night/config.xml new file mode 100644 index 000000000..0b31fe015 --- /dev/null +++ b/auth/src/test/res/values-night/config.xml @@ -0,0 +1,7 @@ + + + test_client_id + test_app_id + test_login_scheme + test_client_token +