Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,13 @@ dependencies {
implementation(Config.Libs.Androidx.fragment)
implementation(Config.Libs.Androidx.customTabs)
implementation(Config.Libs.Androidx.constraint)

// Google Authentication
implementation(Config.Libs.Androidx.credentials)
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
implementation(Config.Libs.Androidx.credentialsPlayServices)
implementation(Config.Libs.Misc.googleid)
implementation(Config.Libs.PlayServices.auth)
//api(Config.Libs.PlayServices.auth)

implementation(Config.Libs.Androidx.lifecycleExtensions)
implementation("androidx.core:core-ktx:1.13.1")
Expand All @@ -99,12 +104,10 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.navigation:navigation-compose:2.8.3")
implementation("com.google.zxing:core:3.5.3")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
annotationProcessor(Config.Libs.Androidx.lifecycleCompiler)

implementation(platform(Config.Libs.Firebase.bom))
api(Config.Libs.Firebase.auth)
api(Config.Libs.PlayServices.auth)

// Phone number validation
implementation(Config.Libs.Misc.libphonenumber)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package com.firebase.ui.auth.compose

import android.content.Context
import androidx.annotation.RestrictTo
import com.firebase.ui.auth.compose.configuration.auth_provider.signOutFromGoogle
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
import com.google.firebase.FirebaseApp
import com.google.firebase.auth.FirebaseAuth
Expand Down Expand Up @@ -341,13 +342,16 @@ class FirebaseAuthUI private constructor(
* @throws AuthException.UnknownException for other errors
* @since 10.0.0
*/
fun signOut(context: Context) {
suspend fun signOut(context: Context) {
try {
// Update state to loading
updateAuthState(AuthState.Loading("Signing out..."))

// Sign out from Firebase Auth
auth.signOut()
.also {
signOutFromGoogle(context)
}

// Update state to idle (user signed out)
updateAuthState(AuthState.Idle)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import android.app.Activity
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.datastore.preferences.core.stringPreferencesKey
import com.facebook.AccessToken
import com.firebase.ui.auth.R
Expand All @@ -33,6 +33,11 @@ import com.firebase.ui.auth.util.Preconditions
import com.firebase.ui.auth.util.data.ContinueUrlBuilder
import com.firebase.ui.auth.util.data.PhoneNumberUtils
import com.firebase.ui.auth.util.data.ProviderAvailability
import com.google.android.gms.auth.api.identity.AuthorizationRequest
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.common.api.Scope
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.firebase.FirebaseException
import com.google.firebase.auth.ActionCodeSettings
import com.google.firebase.auth.AuthCredential
Expand Down Expand Up @@ -484,16 +489,19 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
)
}
})
activity?.let {
options.setActivity(it)
}
forceResendingToken?.let {
options.setForceResendingToken(it)
}
multiFactorSession?.let {
options.setMultiFactorSession(it)
}
PhoneAuthProvider.verifyPhoneNumber(options.build())
.apply {
activity?.let {
setActivity(it)
}
forceResendingToken?.let {
setForceResendingToken(it)
}
multiFactorSession?.let {
setMultiFactorSession(it)
}
}
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
}
}
Expand Down Expand Up @@ -582,6 +590,87 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
)
}
}

/**
* Result container for Google Sign-In credential flow.
* @suppress
*/
internal data class GoogleSignInResult(
val credential: AuthCredential,
val displayName: String?,
val photoUrl: Uri?
)

/**
* An interface to wrap the Authorization API for requesting OAuth scopes.
* @suppress
*/
internal interface AuthorizationProvider {
suspend fun authorize(context: Context, scopes: List<Scope>)
}

/**
* The default implementation of [AuthorizationProvider].
* @suppress
*/
internal class DefaultAuthorizationProvider : AuthorizationProvider {
override suspend fun authorize(context: Context, scopes: List<Scope>) {
val authorizationRequest = AuthorizationRequest.builder()
.setRequestedScopes(scopes)
.build()

Identity.getAuthorizationClient(context)
.authorize(authorizationRequest)
.await()
}
}

/**
* An interface to wrap the Credential Manager flow for Google Sign-In.
* @suppress
*/
internal interface CredentialManagerProvider {
suspend fun getGoogleCredential(
context: Context,
serverClientId: String,
filterByAuthorizedAccounts: Boolean,
autoSelectEnabled: Boolean
): GoogleSignInResult
}

/**
* The default implementation of [CredentialManagerProvider].
* @suppress
*/
internal class DefaultCredentialManagerProvider : CredentialManagerProvider {
override suspend fun getGoogleCredential(
context: Context,
serverClientId: String,
filterByAuthorizedAccounts: Boolean,
autoSelectEnabled: Boolean
): GoogleSignInResult {
val credentialManager = CredentialManager.create(context)
val googleIdOption = GetGoogleIdOption.Builder()
.setServerClientId(serverClientId)
.setFilterByAuthorizedAccounts(filterByAuthorizedAccounts)
.setAutoSelectEnabled(autoSelectEnabled)
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val result = credentialManager.getCredential(context, request)
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(result.credential.data)
val credential = GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null)

return GoogleSignInResult(
credential = credential,
displayName = googleIdTokenCredential.displayName,
photoUrl = googleIdTokenCredential.profilePictureUri
)
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package com.firebase.ui.auth.compose.configuration.auth_provider

import android.content.Context
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
import com.firebase.ui.auth.compose.AuthException
import com.firebase.ui.auth.compose.AuthState
import com.firebase.ui.auth.compose.FirebaseAuthUI
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
import com.google.android.gms.common.api.Scope
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch

/**
* Creates a remembered callback for Google Sign-In that can be invoked from UI components.
*
* This Composable function returns a lambda that, when invoked, initiates the Google Sign-In
* flow using [signInWithGoogle]. The callback is stable across recompositions and automatically
* handles coroutine scoping and error state management.
*
* **Usage:**
* ```kotlin
* val onSignInWithGoogle = authUI.rememberGoogleSignInHandler(
* context = context,
* config = configuration,
* provider = googleProvider
* )
*
* Button(onClick = onSignInWithGoogle) {
* Text("Sign in with Google")
* }
* ```
*
* **Error Handling:**
* - Catches all exceptions and converts them to [AuthException]
* - Automatically updates [AuthState.Error] on failures
* - Logs errors for debugging purposes
*
* @param context Android context for Credential Manager
* @param config Authentication UI configuration
* @param provider Google provider configuration with server client ID and optional scopes
* @return A callback function that initiates Google Sign-In when invoked
*
* @see signInWithGoogle
* @see AuthProvider.Google
*/
@Composable
internal fun FirebaseAuthUI.rememberGoogleSignInHandler(
context: Context,
config: AuthUIConfiguration,
provider: AuthProvider.Google,
): () -> Unit {
val coroutineScope = rememberCoroutineScope()
return remember(this) {
{
coroutineScope.launch {
try {
signInWithGoogle(context, config, provider)
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
} catch (e: Exception) {
val authException = AuthException.from(e)
updateAuthState(AuthState.Error(authException))
}
}
}
}
}

/**
* Signs in with Google using Credential Manager and optionally requests OAuth scopes.
*
* This function implements Google Sign-In using Android's Credential Manager API with
* comprehensive error handling.
*
* **Flow:**
* 1. If [AuthProvider.Google.scopes] are specified, requests OAuth authorization first
* 2. Attempts sign-in using Credential Manager
* 3. Creates Firebase credential and calls [signInAndLinkWithCredential]
*
* **Scopes Behavior:**
* - If [AuthProvider.Google.scopes] is not empty, requests OAuth authorization before sign-in
* - Basic profile, email, and ID token are always included automatically
* - Scopes are requested using the AuthorizationClient API
*
* **Error Handling:**
* - [GoogleIdTokenParsingException]: Library version mismatch
* - [NoCredentialException]: No Google accounts on device
* - [GetCredentialException]: User cancellation, configuration errors, or no credentials
* - Configuration errors trigger detailed developer guidance logs
*
* @param context Android context for Credential Manager
* @param config Authentication UI configuration
* @param provider Google provider configuration with optional scopes
* @param authorizationProvider Provider for OAuth scopes authorization (for testing)
* @param credentialManagerProvider Provider for Credential Manager flow (for testing)
*
* @throws AuthException.InvalidCredentialsException if token parsing fails
* @throws AuthException.AuthCancelledException if user cancels or no accounts found
* @throws AuthException if sign-in or linking fails
*
* @see AuthProvider.Google
* @see signInAndLinkWithCredential
*/
internal suspend fun FirebaseAuthUI.signInWithGoogle(
context: Context,
config: AuthUIConfiguration,
provider: AuthProvider.Google,
authorizationProvider: AuthProvider.Google.AuthorizationProvider = AuthProvider.Google.DefaultAuthorizationProvider(),
credentialManagerProvider: AuthProvider.Google.CredentialManagerProvider = AuthProvider.Google.DefaultCredentialManagerProvider(),
) {
try {
updateAuthState(AuthState.Loading("Signing in with google..."))

// Request OAuth scopes if specified (before sign-in)
if (provider.scopes.isNotEmpty()) {
try {
val requestedScopes = provider.scopes.map { Scope(it) }
authorizationProvider.authorize(context, requestedScopes)
} catch (e: Exception) {
val authException = AuthException.from(e)
updateAuthState(AuthState.Error(authException))
}
}

val result = credentialManagerProvider.getGoogleCredential(
context = context,
serverClientId = provider.serverClientId!!,
filterByAuthorizedAccounts = true,
autoSelectEnabled = false
)

signInAndLinkWithCredential(
config = config,
credential = result.credential,
provider = provider,
displayName = result.displayName,
photoUrl = result.photoUrl,
)
} catch (e: CancellationException) {
val cancelledException = AuthException.AuthCancelledException(
message = "Sign in with google was cancelled",
cause = e
)
updateAuthState(AuthState.Error(cancelledException))
throw cancelledException

} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
throw e

} catch (e: Exception) {
val authException = AuthException.from(e)
updateAuthState(AuthState.Error(authException))
throw authException
}
}

/**
* Signs out from Google and clears credential state.
*
* This function clears the cached Google credentials, ensuring that the account picker
* will be shown on the next sign-in attempt instead of automatically signing in with
* the previously used account.
*
* **When to call:**
* - After user explicitly signs out
* - Before allowing user to select a different Google account
* - When switching between accounts
*
* **Note:** This does not sign out from Firebase Auth itself. Call [FirebaseAuthUI.signOut]
* separately if you need to sign out from Firebase.
*
* @param context Android context for Credential Manager
*/
internal suspend fun signOutFromGoogle(context: Context) {
try {
val credentialManager = CredentialManager.create(context)
credentialManager.clearCredentialState(
ClearCredentialStateRequest()
)
} catch (_: Exception) {

}
}
Loading