Skip to content

Commit 21ac4ad

Browse files
authored
Merge branch 'version-10.0.0-dev' into feat/accesibility
2 parents 7e5fbd3 + d7ce8ac commit 21ac4ad

File tree

13 files changed

+1891
-44
lines changed

13 files changed

+1891
-44
lines changed

auth/build.gradle.kts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,13 @@ dependencies {
8989
implementation(Config.Libs.Androidx.fragment)
9090
implementation(Config.Libs.Androidx.customTabs)
9191
implementation(Config.Libs.Androidx.constraint)
92+
93+
// Google Authentication
9294
implementation(Config.Libs.Androidx.credentials)
93-
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")
95+
implementation(Config.Libs.Androidx.credentialsPlayServices)
96+
implementation(Config.Libs.Misc.googleid)
97+
implementation(Config.Libs.PlayServices.auth)
98+
//api(Config.Libs.PlayServices.auth)
9499

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

105109
implementation(platform(Config.Libs.Firebase.bom))
106110
api(Config.Libs.Firebase.auth)
107-
api(Config.Libs.PlayServices.auth)
108111

109112
// Phone number validation
110113
implementation(Config.Libs.Misc.libphonenumber)

auth/src/main/java/com/firebase/ui/auth/compose/FirebaseAuthUI.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.firebase.ui.auth.compose
1616

1717
import android.content.Context
1818
import androidx.annotation.RestrictTo
19+
import com.firebase.ui.auth.compose.configuration.auth_provider.signOutFromGoogle
1920
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
2021
import com.google.firebase.FirebaseApp
2122
import com.google.firebase.auth.FirebaseAuth
@@ -341,13 +342,16 @@ class FirebaseAuthUI private constructor(
341342
* @throws AuthException.UnknownException for other errors
342343
* @since 10.0.0
343344
*/
344-
fun signOut(context: Context) {
345+
suspend fun signOut(context: Context) {
345346
try {
346347
// Update state to loading
347348
updateAuthState(AuthState.Loading("Signing out..."))
348349

349350
// Sign out from Firebase Auth
350351
auth.signOut()
352+
.also {
353+
signOutFromGoogle(context)
354+
}
351355

352356
// Update state to idle (user signed out)
353357
updateAuthState(AuthState.Idle)

auth/src/main/java/com/firebase/ui/auth/compose/configuration/auth_provider/AuthProvider.kt

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ import android.app.Activity
1818
import android.content.Context
1919
import android.net.Uri
2020
import android.util.Log
21-
import androidx.annotation.RestrictTo
22-
import androidx.annotation.VisibleForTesting
2321
import androidx.compose.ui.graphics.Color
2422
import androidx.core.net.toUri
23+
import androidx.credentials.CredentialManager
24+
import androidx.credentials.GetCredentialRequest
2525
import androidx.datastore.preferences.core.stringPreferencesKey
2626
import com.facebook.AccessToken
2727
import com.firebase.ui.auth.R
@@ -33,6 +33,11 @@ import com.firebase.ui.auth.util.Preconditions
3333
import com.firebase.ui.auth.util.data.ContinueUrlBuilder
3434
import com.firebase.ui.auth.util.data.PhoneNumberUtils
3535
import com.firebase.ui.auth.util.data.ProviderAvailability
36+
import com.google.android.gms.auth.api.identity.AuthorizationRequest
37+
import com.google.android.gms.auth.api.identity.Identity
38+
import com.google.android.gms.common.api.Scope
39+
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
40+
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
3641
import com.google.firebase.FirebaseException
3742
import com.google.firebase.auth.ActionCodeSettings
3843
import com.google.firebase.auth.AuthCredential
@@ -484,16 +489,19 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
484489
)
485490
}
486491
})
487-
activity?.let {
488-
options.setActivity(it)
489-
}
490-
forceResendingToken?.let {
491-
options.setForceResendingToken(it)
492-
}
493-
multiFactorSession?.let {
494-
options.setMultiFactorSession(it)
495-
}
496-
PhoneAuthProvider.verifyPhoneNumber(options.build())
492+
.apply {
493+
activity?.let {
494+
setActivity(it)
495+
}
496+
forceResendingToken?.let {
497+
setForceResendingToken(it)
498+
}
499+
multiFactorSession?.let {
500+
setMultiFactorSession(it)
501+
}
502+
}
503+
.build()
504+
PhoneAuthProvider.verifyPhoneNumber(options)
497505
}
498506
}
499507
}
@@ -582,6 +590,87 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
582590
)
583591
}
584592
}
593+
594+
/**
595+
* Result container for Google Sign-In credential flow.
596+
* @suppress
597+
*/
598+
internal data class GoogleSignInResult(
599+
val credential: AuthCredential,
600+
val displayName: String?,
601+
val photoUrl: Uri?
602+
)
603+
604+
/**
605+
* An interface to wrap the Authorization API for requesting OAuth scopes.
606+
* @suppress
607+
*/
608+
internal interface AuthorizationProvider {
609+
suspend fun authorize(context: Context, scopes: List<Scope>)
610+
}
611+
612+
/**
613+
* The default implementation of [AuthorizationProvider].
614+
* @suppress
615+
*/
616+
internal class DefaultAuthorizationProvider : AuthorizationProvider {
617+
override suspend fun authorize(context: Context, scopes: List<Scope>) {
618+
val authorizationRequest = AuthorizationRequest.builder()
619+
.setRequestedScopes(scopes)
620+
.build()
621+
622+
Identity.getAuthorizationClient(context)
623+
.authorize(authorizationRequest)
624+
.await()
625+
}
626+
}
627+
628+
/**
629+
* An interface to wrap the Credential Manager flow for Google Sign-In.
630+
* @suppress
631+
*/
632+
internal interface CredentialManagerProvider {
633+
suspend fun getGoogleCredential(
634+
context: Context,
635+
serverClientId: String,
636+
filterByAuthorizedAccounts: Boolean,
637+
autoSelectEnabled: Boolean
638+
): GoogleSignInResult
639+
}
640+
641+
/**
642+
* The default implementation of [CredentialManagerProvider].
643+
* @suppress
644+
*/
645+
internal class DefaultCredentialManagerProvider : CredentialManagerProvider {
646+
override suspend fun getGoogleCredential(
647+
context: Context,
648+
serverClientId: String,
649+
filterByAuthorizedAccounts: Boolean,
650+
autoSelectEnabled: Boolean
651+
): GoogleSignInResult {
652+
val credentialManager = CredentialManager.create(context)
653+
val googleIdOption = GetGoogleIdOption.Builder()
654+
.setServerClientId(serverClientId)
655+
.setFilterByAuthorizedAccounts(filterByAuthorizedAccounts)
656+
.setAutoSelectEnabled(autoSelectEnabled)
657+
.build()
658+
659+
val request = GetCredentialRequest.Builder()
660+
.addCredentialOption(googleIdOption)
661+
.build()
662+
663+
val result = credentialManager.getCredential(context, request)
664+
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(result.credential.data)
665+
val credential = GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null)
666+
667+
return GoogleSignInResult(
668+
credential = credential,
669+
displayName = googleIdTokenCredential.displayName,
670+
photoUrl = googleIdTokenCredential.profilePictureUri
671+
)
672+
}
673+
}
585674
}
586675

587676
/**
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.firebase.ui.auth.compose.configuration.auth_provider
2+
3+
import android.content.Context
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.rememberCoroutineScope
7+
import androidx.credentials.ClearCredentialStateRequest
8+
import androidx.credentials.CredentialManager
9+
import androidx.credentials.exceptions.GetCredentialException
10+
import androidx.credentials.exceptions.NoCredentialException
11+
import com.firebase.ui.auth.compose.AuthException
12+
import com.firebase.ui.auth.compose.AuthState
13+
import com.firebase.ui.auth.compose.FirebaseAuthUI
14+
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
15+
import com.google.android.gms.common.api.Scope
16+
import com.google.android.libraries.identity.googleid.GoogleIdTokenParsingException
17+
import kotlinx.coroutines.CancellationException
18+
import kotlinx.coroutines.launch
19+
20+
/**
21+
* Creates a remembered callback for Google Sign-In that can be invoked from UI components.
22+
*
23+
* This Composable function returns a lambda that, when invoked, initiates the Google Sign-In
24+
* flow using [signInWithGoogle]. The callback is stable across recompositions and automatically
25+
* handles coroutine scoping and error state management.
26+
*
27+
* **Usage:**
28+
* ```kotlin
29+
* val onSignInWithGoogle = authUI.rememberGoogleSignInHandler(
30+
* context = context,
31+
* config = configuration,
32+
* provider = googleProvider
33+
* )
34+
*
35+
* Button(onClick = onSignInWithGoogle) {
36+
* Text("Sign in with Google")
37+
* }
38+
* ```
39+
*
40+
* **Error Handling:**
41+
* - Catches all exceptions and converts them to [AuthException]
42+
* - Automatically updates [AuthState.Error] on failures
43+
* - Logs errors for debugging purposes
44+
*
45+
* @param context Android context for Credential Manager
46+
* @param config Authentication UI configuration
47+
* @param provider Google provider configuration with server client ID and optional scopes
48+
* @return A callback function that initiates Google Sign-In when invoked
49+
*
50+
* @see signInWithGoogle
51+
* @see AuthProvider.Google
52+
*/
53+
@Composable
54+
internal fun FirebaseAuthUI.rememberGoogleSignInHandler(
55+
context: Context,
56+
config: AuthUIConfiguration,
57+
provider: AuthProvider.Google,
58+
): () -> Unit {
59+
val coroutineScope = rememberCoroutineScope()
60+
return remember(this) {
61+
{
62+
coroutineScope.launch {
63+
try {
64+
signInWithGoogle(context, config, provider)
65+
} catch (e: AuthException) {
66+
updateAuthState(AuthState.Error(e))
67+
} catch (e: Exception) {
68+
val authException = AuthException.from(e)
69+
updateAuthState(AuthState.Error(authException))
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Signs in with Google using Credential Manager and optionally requests OAuth scopes.
78+
*
79+
* This function implements Google Sign-In using Android's Credential Manager API with
80+
* comprehensive error handling.
81+
*
82+
* **Flow:**
83+
* 1. If [AuthProvider.Google.scopes] are specified, requests OAuth authorization first
84+
* 2. Attempts sign-in using Credential Manager
85+
* 3. Creates Firebase credential and calls [signInAndLinkWithCredential]
86+
*
87+
* **Scopes Behavior:**
88+
* - If [AuthProvider.Google.scopes] is not empty, requests OAuth authorization before sign-in
89+
* - Basic profile, email, and ID token are always included automatically
90+
* - Scopes are requested using the AuthorizationClient API
91+
*
92+
* **Error Handling:**
93+
* - [GoogleIdTokenParsingException]: Library version mismatch
94+
* - [NoCredentialException]: No Google accounts on device
95+
* - [GetCredentialException]: User cancellation, configuration errors, or no credentials
96+
* - Configuration errors trigger detailed developer guidance logs
97+
*
98+
* @param context Android context for Credential Manager
99+
* @param config Authentication UI configuration
100+
* @param provider Google provider configuration with optional scopes
101+
* @param authorizationProvider Provider for OAuth scopes authorization (for testing)
102+
* @param credentialManagerProvider Provider for Credential Manager flow (for testing)
103+
*
104+
* @throws AuthException.InvalidCredentialsException if token parsing fails
105+
* @throws AuthException.AuthCancelledException if user cancels or no accounts found
106+
* @throws AuthException if sign-in or linking fails
107+
*
108+
* @see AuthProvider.Google
109+
* @see signInAndLinkWithCredential
110+
*/
111+
internal suspend fun FirebaseAuthUI.signInWithGoogle(
112+
context: Context,
113+
config: AuthUIConfiguration,
114+
provider: AuthProvider.Google,
115+
authorizationProvider: AuthProvider.Google.AuthorizationProvider = AuthProvider.Google.DefaultAuthorizationProvider(),
116+
credentialManagerProvider: AuthProvider.Google.CredentialManagerProvider = AuthProvider.Google.DefaultCredentialManagerProvider(),
117+
) {
118+
try {
119+
updateAuthState(AuthState.Loading("Signing in with google..."))
120+
121+
// Request OAuth scopes if specified (before sign-in)
122+
if (provider.scopes.isNotEmpty()) {
123+
try {
124+
val requestedScopes = provider.scopes.map { Scope(it) }
125+
authorizationProvider.authorize(context, requestedScopes)
126+
} catch (e: Exception) {
127+
val authException = AuthException.from(e)
128+
updateAuthState(AuthState.Error(authException))
129+
}
130+
}
131+
132+
val result = credentialManagerProvider.getGoogleCredential(
133+
context = context,
134+
serverClientId = provider.serverClientId!!,
135+
filterByAuthorizedAccounts = true,
136+
autoSelectEnabled = false
137+
)
138+
139+
signInAndLinkWithCredential(
140+
config = config,
141+
credential = result.credential,
142+
provider = provider,
143+
displayName = result.displayName,
144+
photoUrl = result.photoUrl,
145+
)
146+
} catch (e: CancellationException) {
147+
val cancelledException = AuthException.AuthCancelledException(
148+
message = "Sign in with google was cancelled",
149+
cause = e
150+
)
151+
updateAuthState(AuthState.Error(cancelledException))
152+
throw cancelledException
153+
154+
} catch (e: AuthException) {
155+
updateAuthState(AuthState.Error(e))
156+
throw e
157+
158+
} catch (e: Exception) {
159+
val authException = AuthException.from(e)
160+
updateAuthState(AuthState.Error(authException))
161+
throw authException
162+
}
163+
}
164+
165+
/**
166+
* Signs out from Google and clears credential state.
167+
*
168+
* This function clears the cached Google credentials, ensuring that the account picker
169+
* will be shown on the next sign-in attempt instead of automatically signing in with
170+
* the previously used account.
171+
*
172+
* **When to call:**
173+
* - After user explicitly signs out
174+
* - Before allowing user to select a different Google account
175+
* - When switching between accounts
176+
*
177+
* **Note:** This does not sign out from Firebase Auth itself. Call [FirebaseAuthUI.signOut]
178+
* separately if you need to sign out from Firebase.
179+
*
180+
* @param context Android context for Credential Manager
181+
*/
182+
internal suspend fun signOutFromGoogle(context: Context) {
183+
try {
184+
val credentialManager = CredentialManager.create(context)
185+
credentialManager.clearCredentialState(
186+
ClearCredentialStateRequest()
187+
)
188+
} catch (_: Exception) {
189+
190+
}
191+
}

0 commit comments

Comments
 (0)