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