Skip to content

Commit d3fa7ef

Browse files
committed
feat: Core: Auth State & User Accessors
1 parent bae4ce4 commit d3fa7ef

File tree

3 files changed

+755
-0
lines changed

3 files changed

+755
-0
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.firebase.ui.auth.compose
16+
17+
import com.google.firebase.auth.AuthResult
18+
import com.google.firebase.auth.FirebaseUser
19+
import com.google.firebase.auth.MultiFactorResolver
20+
21+
/**
22+
* Represents the authentication state in Firebase Auth UI.
23+
*
24+
* This class encapsulates all possible authentication states that can occur during
25+
* the authentication flow, including success, error, and intermediate states.
26+
*
27+
* Use the companion object factory methods or specific subclass constructors to create instances.
28+
*
29+
* @since 10.0.0
30+
*/
31+
abstract class AuthState private constructor() {
32+
33+
/**
34+
* Initial state before any authentication operation has been started.
35+
*/
36+
class Idle internal constructor() : AuthState() {
37+
override fun equals(other: Any?): Boolean = other is Idle
38+
override fun hashCode(): Int = javaClass.hashCode()
39+
override fun toString(): String = "AuthState.Idle"
40+
}
41+
42+
/**
43+
* Authentication operation is in progress.
44+
*
45+
* @property message Optional message describing what is being loaded
46+
*/
47+
class Loading(val message: String? = null) : AuthState() {
48+
override fun equals(other: Any?): Boolean {
49+
if (this === other) return true
50+
if (other !is Loading) return false
51+
return message == other.message
52+
}
53+
54+
override fun hashCode(): Int = message?.hashCode() ?: 0
55+
56+
override fun toString(): String = "AuthState.Loading(message=$message)"
57+
}
58+
59+
/**
60+
* Authentication completed successfully.
61+
*
62+
* @property result The [AuthResult] containing the authenticated user, may be null if not available
63+
* @property user The authenticated [FirebaseUser]
64+
* @property isNewUser Whether this is a newly created user account
65+
*/
66+
class Success(
67+
val result: AuthResult?,
68+
val user: FirebaseUser,
69+
val isNewUser: Boolean = false
70+
) : AuthState() {
71+
override fun equals(other: Any?): Boolean {
72+
if (this === other) return true
73+
if (other !is Success) return false
74+
return result == other.result &&
75+
user == other.user &&
76+
isNewUser == other.isNewUser
77+
}
78+
79+
override fun hashCode(): Int {
80+
var result1 = result?.hashCode() ?: 0
81+
result1 = 31 * result1 + user.hashCode()
82+
result1 = 31 * result1 + isNewUser.hashCode()
83+
return result1
84+
}
85+
86+
override fun toString(): String =
87+
"AuthState.Success(result=$result, user=$user, isNewUser=$isNewUser)"
88+
}
89+
90+
/**
91+
* An error occurred during authentication.
92+
*
93+
* @property exception The [Exception] that occurred
94+
* @property isRecoverable Whether the error can be recovered from
95+
*/
96+
class Error(
97+
val exception: Exception,
98+
val isRecoverable: Boolean = true
99+
) : AuthState() {
100+
override fun equals(other: Any?): Boolean {
101+
if (this === other) return true
102+
if (other !is Error) return false
103+
return exception == other.exception &&
104+
isRecoverable == other.isRecoverable
105+
}
106+
107+
override fun hashCode(): Int {
108+
var result = exception.hashCode()
109+
result = 31 * result + isRecoverable.hashCode()
110+
return result
111+
}
112+
113+
override fun toString(): String =
114+
"AuthState.Error(exception=$exception, isRecoverable=$isRecoverable)"
115+
}
116+
117+
/**
118+
* Authentication was cancelled by the user.
119+
*/
120+
class Cancelled internal constructor() : AuthState() {
121+
override fun equals(other: Any?): Boolean = other is Cancelled
122+
override fun hashCode(): Int = javaClass.hashCode()
123+
override fun toString(): String = "AuthState.Cancelled"
124+
}
125+
126+
/**
127+
* Multi-factor authentication is required to complete sign-in.
128+
*
129+
* @property resolver The [MultiFactorResolver] to complete MFA
130+
* @property hint Optional hint about which factor to use
131+
*/
132+
class RequiresMfa(
133+
val resolver: MultiFactorResolver,
134+
val hint: String? = null
135+
) : AuthState() {
136+
override fun equals(other: Any?): Boolean {
137+
if (this === other) return true
138+
if (other !is RequiresMfa) return false
139+
return resolver == other.resolver &&
140+
hint == other.hint
141+
}
142+
143+
override fun hashCode(): Int {
144+
var result = resolver.hashCode()
145+
result = 31 * result + (hint?.hashCode() ?: 0)
146+
return result
147+
}
148+
149+
override fun toString(): String =
150+
"AuthState.RequiresMfa(resolver=$resolver, hint=$hint)"
151+
}
152+
153+
/**
154+
* Email verification is required before the user can access the app.
155+
*
156+
* @property user The [FirebaseUser] who needs to verify their email
157+
* @property email The email address that needs verification
158+
*/
159+
class RequiresEmailVerification(
160+
val user: FirebaseUser,
161+
val email: String
162+
) : AuthState() {
163+
override fun equals(other: Any?): Boolean {
164+
if (this === other) return true
165+
if (other !is RequiresEmailVerification) return false
166+
return user == other.user &&
167+
email == other.email
168+
}
169+
170+
override fun hashCode(): Int {
171+
var result = user.hashCode()
172+
result = 31 * result + email.hashCode()
173+
return result
174+
}
175+
176+
override fun toString(): String =
177+
"AuthState.RequiresEmailVerification(user=$user, email=$email)"
178+
}
179+
180+
/**
181+
* The user needs to complete their profile information.
182+
*
183+
* @property user The [FirebaseUser] who needs to complete their profile
184+
* @property missingFields List of profile fields that need to be completed
185+
*/
186+
class RequiresProfileCompletion(
187+
val user: FirebaseUser,
188+
val missingFields: List<String> = emptyList()
189+
) : AuthState() {
190+
override fun equals(other: Any?): Boolean {
191+
if (this === other) return true
192+
if (other !is RequiresProfileCompletion) return false
193+
return user == other.user &&
194+
missingFields == other.missingFields
195+
}
196+
197+
override fun hashCode(): Int {
198+
var result = user.hashCode()
199+
result = 31 * result + missingFields.hashCode()
200+
return result
201+
}
202+
203+
override fun toString(): String =
204+
"AuthState.RequiresProfileCompletion(user=$user, missingFields=$missingFields)"
205+
}
206+
207+
companion object {
208+
/**
209+
* Creates an Idle state instance.
210+
* @return A new [Idle] state
211+
*/
212+
@JvmStatic
213+
val Idle: Idle = Idle()
214+
215+
/**
216+
* Creates a Cancelled state instance.
217+
* @return A new [Cancelled] state
218+
*/
219+
@JvmStatic
220+
val Cancelled: Cancelled = Cancelled()
221+
}
222+
}

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

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ package com.firebase.ui.auth.compose
1717
import androidx.annotation.RestrictTo
1818
import com.google.firebase.FirebaseApp
1919
import com.google.firebase.auth.FirebaseAuth
20+
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
21+
import com.google.firebase.auth.FirebaseUser
2022
import com.google.firebase.auth.ktx.auth
2123
import com.google.firebase.ktx.Firebase
24+
import kotlinx.coroutines.channels.awaitClose
25+
import kotlinx.coroutines.flow.Flow
26+
import kotlinx.coroutines.flow.MutableStateFlow
27+
import kotlinx.coroutines.flow.callbackFlow
2228
import java.util.concurrent.ConcurrentHashMap
2329

2430
/**
@@ -56,6 +62,154 @@ class FirebaseAuthUI private constructor(
5662
val app: FirebaseApp,
5763
val auth: FirebaseAuth
5864
) {
65+
66+
private val _authStateFlow = MutableStateFlow<AuthState>(AuthState.Idle)
67+
68+
/**
69+
* Checks whether a user is currently signed in.
70+
*
71+
* This method directly mirrors the state of [FirebaseAuth] and returns true if there is
72+
* a currently signed-in user, false otherwise.
73+
*
74+
* **Example:**
75+
* ```kotlin
76+
* val authUI = FirebaseAuthUI.getInstance()
77+
* if (authUI.isSignedIn()) {
78+
* // User is signed in
79+
* navigateToHome()
80+
* } else {
81+
* // User is not signed in
82+
* navigateToLogin()
83+
* }
84+
* ```
85+
*
86+
* @return `true` if a user is signed in, `false` otherwise
87+
*/
88+
fun isSignedIn(): Boolean = auth.currentUser != null
89+
90+
/**
91+
* Returns the currently signed-in user, or null if no user is signed in.
92+
*
93+
* This method returns the same value as [FirebaseAuth.currentUser] and provides
94+
* direct access to the current user object.
95+
*
96+
* **Example:**
97+
* ```kotlin
98+
* val authUI = FirebaseAuthUI.getInstance()
99+
* val user = authUI.getCurrentUser()
100+
* user?.let {
101+
* println("User email: ${it.email}")
102+
* println("User ID: ${it.uid}")
103+
* }
104+
* ```
105+
*
106+
* @return The currently signed-in [FirebaseUser], or `null` if no user is signed in
107+
*/
108+
fun getCurrentUser(): FirebaseUser? = auth.currentUser
109+
110+
/**
111+
* Returns a [Flow] that emits [AuthState] changes.
112+
*
113+
* This flow observes changes to the authentication state and emits appropriate
114+
* [AuthState] objects. The flow will emit:
115+
* - [AuthState.Idle] when there's no active authentication operation
116+
* - [AuthState.Loading] during authentication operations
117+
* - [AuthState.Success] when a user successfully signs in
118+
* - [AuthState.Error] when an authentication error occurs
119+
* - [AuthState.Cancelled] when authentication is cancelled
120+
* - [AuthState.RequiresMfa] when multi-factor authentication is needed
121+
* - [AuthState.RequiresEmailVerification] when email verification is needed
122+
*
123+
* The flow automatically emits [AuthState.Success] or [AuthState.Idle] based on
124+
* the current authentication state when collection starts.
125+
*
126+
* **Example:**
127+
* ```kotlin
128+
* val authUI = FirebaseAuthUI.getInstance()
129+
*
130+
* lifecycleScope.launch {
131+
* authUI.authStateFlow().collect { state ->
132+
* when (state) {
133+
* is AuthState.Success -> {
134+
* // User is signed in
135+
* updateUI(state.user)
136+
* }
137+
* is AuthState.Error -> {
138+
* // Handle error
139+
* showError(state.exception.message)
140+
* }
141+
* is AuthState.Loading -> {
142+
* // Show loading indicator
143+
* showProgressBar()
144+
* }
145+
* // ... handle other states
146+
* }
147+
* }
148+
* }
149+
* ```
150+
*
151+
* @return A [Flow] of [AuthState] that emits authentication state changes
152+
*/
153+
fun authStateFlow(): Flow<AuthState> = callbackFlow {
154+
// Set initial state based on current auth state
155+
val initialState = auth.currentUser?.let { user ->
156+
AuthState.Success(result = null, user = user, isNewUser = false)
157+
} ?: AuthState.Idle
158+
159+
trySend(initialState)
160+
161+
// Create auth state listener
162+
val authStateListener = AuthStateListener { firebaseAuth ->
163+
val currentUser = firebaseAuth.currentUser
164+
val state = if (currentUser != null) {
165+
// Check if email verification is required
166+
if (!currentUser.isEmailVerified &&
167+
currentUser.email != null &&
168+
currentUser.providerData.any { it.providerId == "password" }) {
169+
AuthState.RequiresEmailVerification(
170+
user = currentUser,
171+
email = currentUser.email!!
172+
)
173+
} else {
174+
AuthState.Success(
175+
result = null,
176+
user = currentUser,
177+
isNewUser = false
178+
)
179+
}
180+
} else {
181+
AuthState.Idle
182+
}
183+
trySend(state)
184+
}
185+
186+
// Add listener
187+
auth.addAuthStateListener(authStateListener)
188+
189+
// Also observe internal state changes
190+
_authStateFlow.value.let { currentState ->
191+
if (currentState !is AuthState.Idle && currentState !is AuthState.Success) {
192+
trySend(currentState)
193+
}
194+
}
195+
196+
// Remove listener when flow collection is cancelled
197+
awaitClose {
198+
auth.removeAuthStateListener(authStateListener)
199+
}
200+
}
201+
202+
/**
203+
* Updates the internal authentication state.
204+
* This method is intended for internal use by authentication operations.
205+
*
206+
* @param state The new [AuthState] to emit
207+
* @suppress This is an internal API
208+
*/
209+
internal fun updateAuthState(state: AuthState) {
210+
_authStateFlow.value = state
211+
}
212+
59213
companion object {
60214
/** Cache for singleton instances per FirebaseApp. Thread-safe via ConcurrentHashMap. */
61215
private val instanceCache = ConcurrentHashMap<String, FirebaseAuthUI>()

0 commit comments

Comments
 (0)