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
222 changes: 222 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/compose/AuthState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Copyright 2025 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.firebase.ui.auth.compose

import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.MultiFactorResolver

/**
* Represents the authentication state in Firebase Auth UI.
*
* This class encapsulates all possible authentication states that can occur during
* the authentication flow, including success, error, and intermediate states.
*
* Use the companion object factory methods or specific subclass constructors to create instances.
*
* @since 10.0.0
*/
abstract class AuthState private constructor() {

/**
* Initial state before any authentication operation has been started.
*/
class Idle internal constructor() : AuthState() {
override fun equals(other: Any?): Boolean = other is Idle
override fun hashCode(): Int = javaClass.hashCode()
override fun toString(): String = "AuthState.Idle"
}

/**
* Authentication operation is in progress.
*
* @property message Optional message describing what is being loaded
*/
class Loading(val message: String? = null) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Loading) return false
return message == other.message
}

override fun hashCode(): Int = message?.hashCode() ?: 0

override fun toString(): String = "AuthState.Loading(message=$message)"
}

/**
* Authentication completed successfully.
*
* @property result The [AuthResult] containing the authenticated user, may be null if not available
* @property user The authenticated [FirebaseUser]
* @property isNewUser Whether this is a newly created user account
*/
class Success(
val result: AuthResult?,
val user: FirebaseUser,
val isNewUser: Boolean = false
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Success) return false
return result == other.result &&
user == other.user &&
isNewUser == other.isNewUser
}

override fun hashCode(): Int {
var result1 = result?.hashCode() ?: 0
result1 = 31 * result1 + user.hashCode()
result1 = 31 * result1 + isNewUser.hashCode()
return result1
}

override fun toString(): String =
"AuthState.Success(result=$result, user=$user, isNewUser=$isNewUser)"
}

/**
* An error occurred during authentication.
*
* @property exception The [Exception] that occurred
* @property isRecoverable Whether the error can be recovered from
*/
class Error(
val exception: Exception,
val isRecoverable: Boolean = true
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Error) return false
return exception == other.exception &&
isRecoverable == other.isRecoverable
}

override fun hashCode(): Int {
var result = exception.hashCode()
result = 31 * result + isRecoverable.hashCode()
return result
}

override fun toString(): String =
"AuthState.Error(exception=$exception, isRecoverable=$isRecoverable)"
}

/**
* Authentication was cancelled by the user.
*/
class Cancelled internal constructor() : AuthState() {
override fun equals(other: Any?): Boolean = other is Cancelled
override fun hashCode(): Int = javaClass.hashCode()
override fun toString(): String = "AuthState.Cancelled"
}

/**
* Multi-factor authentication is required to complete sign-in.
*
* @property resolver The [MultiFactorResolver] to complete MFA
* @property hint Optional hint about which factor to use
*/
class RequiresMfa(
val resolver: MultiFactorResolver,
val hint: String? = null
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RequiresMfa) return false
return resolver == other.resolver &&
hint == other.hint
}

override fun hashCode(): Int {
var result = resolver.hashCode()
result = 31 * result + (hint?.hashCode() ?: 0)
return result
}

override fun toString(): String =
"AuthState.RequiresMfa(resolver=$resolver, hint=$hint)"
}

/**
* Email verification is required before the user can access the app.
*
* @property user The [FirebaseUser] who needs to verify their email
* @property email The email address that needs verification
*/
class RequiresEmailVerification(
val user: FirebaseUser,
val email: String
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RequiresEmailVerification) return false
return user == other.user &&
email == other.email
}

override fun hashCode(): Int {
var result = user.hashCode()
result = 31 * result + email.hashCode()
return result
}

override fun toString(): String =
"AuthState.RequiresEmailVerification(user=$user, email=$email)"
}

/**
* The user needs to complete their profile information.
*
* @property user The [FirebaseUser] who needs to complete their profile
* @property missingFields List of profile fields that need to be completed
*/
class RequiresProfileCompletion(
val user: FirebaseUser,
val missingFields: List<String> = emptyList()
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RequiresProfileCompletion) return false
return user == other.user &&
missingFields == other.missingFields
}

override fun hashCode(): Int {
var result = user.hashCode()
result = 31 * result + missingFields.hashCode()
return result
}

override fun toString(): String =
"AuthState.RequiresProfileCompletion(user=$user, missingFields=$missingFields)"
}

companion object {
/**
* Creates an Idle state instance.
* @return A new [Idle] state
*/
@JvmStatic
val Idle: Idle = Idle()

/**
* Creates a Cancelled state instance.
* @return A new [Cancelled] state
*/
@JvmStatic
val Cancelled: Cancelled = Cancelled()
}
}
154 changes: 154 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/compose/FirebaseAuthUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ package com.firebase.ui.auth.compose
import androidx.annotation.RestrictTo
import com.google.firebase.FirebaseApp
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import java.util.concurrent.ConcurrentHashMap

/**
Expand Down Expand Up @@ -56,6 +62,154 @@ class FirebaseAuthUI private constructor(
val app: FirebaseApp,
val auth: FirebaseAuth
) {

private val _authStateFlow = MutableStateFlow<AuthState>(AuthState.Idle)

/**
* Checks whether a user is currently signed in.
*
* This method directly mirrors the state of [FirebaseAuth] and returns true if there is
* a currently signed-in user, false otherwise.
*
* **Example:**
* ```kotlin
* val authUI = FirebaseAuthUI.getInstance()
* if (authUI.isSignedIn()) {
* // User is signed in
* navigateToHome()
* } else {
* // User is not signed in
* navigateToLogin()
* }
* ```
*
* @return `true` if a user is signed in, `false` otherwise
*/
fun isSignedIn(): Boolean = auth.currentUser != null

/**
* Returns the currently signed-in user, or null if no user is signed in.
*
* This method returns the same value as [FirebaseAuth.currentUser] and provides
* direct access to the current user object.
*
* **Example:**
* ```kotlin
* val authUI = FirebaseAuthUI.getInstance()
* val user = authUI.getCurrentUser()
* user?.let {
* println("User email: ${it.email}")
* println("User ID: ${it.uid}")
* }
* ```
*
* @return The currently signed-in [FirebaseUser], or `null` if no user is signed in
*/
fun getCurrentUser(): FirebaseUser? = auth.currentUser

/**
* Returns a [Flow] that emits [AuthState] changes.
*
* This flow observes changes to the authentication state and emits appropriate
* [AuthState] objects. The flow will emit:
* - [AuthState.Idle] when there's no active authentication operation
* - [AuthState.Loading] during authentication operations
* - [AuthState.Success] when a user successfully signs in
* - [AuthState.Error] when an authentication error occurs
* - [AuthState.Cancelled] when authentication is cancelled
* - [AuthState.RequiresMfa] when multi-factor authentication is needed
* - [AuthState.RequiresEmailVerification] when email verification is needed
*
* The flow automatically emits [AuthState.Success] or [AuthState.Idle] based on
* the current authentication state when collection starts.
*
* **Example:**
* ```kotlin
* val authUI = FirebaseAuthUI.getInstance()
*
* lifecycleScope.launch {
* authUI.authStateFlow().collect { state ->
* when (state) {
* is AuthState.Success -> {
* // User is signed in
* updateUI(state.user)
* }
* is AuthState.Error -> {
* // Handle error
* showError(state.exception.message)
* }
* is AuthState.Loading -> {
* // Show loading indicator
* showProgressBar()
* }
* // ... handle other states
* }
* }
* }
* ```
*
* @return A [Flow] of [AuthState] that emits authentication state changes
*/
fun authStateFlow(): Flow<AuthState> = callbackFlow {
// Set initial state based on current auth state
val initialState = auth.currentUser?.let { user ->
AuthState.Success(result = null, user = user, isNewUser = false)
} ?: AuthState.Idle

trySend(initialState)

// Create auth state listener
val authStateListener = AuthStateListener { firebaseAuth ->
val currentUser = firebaseAuth.currentUser
Copy link

@demolaf demolaf Sep 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the potential of having to handle more states, would it be better to use when instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum, we are determining states here depending on conditions, not sure it's required.

val state = if (currentUser != null) {
// Check if email verification is required
if (!currentUser.isEmailVerified &&
currentUser.email != null &&
currentUser.providerData.any { it.providerId == "password" }) {
AuthState.RequiresEmailVerification(
user = currentUser,
email = currentUser.email!!
)
} else {
AuthState.Success(
result = null,
user = currentUser,
isNewUser = false
)
}
} else {
AuthState.Idle
}
trySend(state)
}

// Add listener
auth.addAuthStateListener(authStateListener)

// Also observe internal state changes
_authStateFlow.value.let { currentState ->
if (currentState !is AuthState.Idle && currentState !is AuthState.Success) {
trySend(currentState)
}
}

// Remove listener when flow collection is cancelled
awaitClose {
auth.removeAuthStateListener(authStateListener)
}
}

/**
* Updates the internal authentication state.
* This method is intended for internal use by authentication operations.
*
* @param state The new [AuthState] to emit
* @suppress This is an internal API
*/
internal fun updateAuthState(state: AuthState) {
_authStateFlow.value = state
}

companion object {
/** Cache for singleton instances per FirebaseApp. Thread-safe via ConcurrentHashMap. */
private val instanceCache = ConcurrentHashMap<String, FirebaseAuthUI>()
Expand Down
Loading