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
20 changes: 18 additions & 2 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ dependencies {
implementation(Config.Libs.Androidx.materialDesign)
implementation(Config.Libs.Androidx.activity)
implementation(Config.Libs.Androidx.Compose.materialIconsExtended)
implementation(Config.Libs.Androidx.datastorePreferences)
// The new activity result APIs force us to include Fragment 1.3.0
// See https://issuetracker.google.com/issues/152554847
implementation(Config.Libs.Androidx.fragment)
implementation(Config.Libs.Androidx.customTabs)
implementation(Config.Libs.Androidx.constraint)
implementation("androidx.credentials:credentials:1.3.0")
implementation(Config.Libs.Androidx.credentials)
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")

implementation(Config.Libs.Androidx.lifecycleExtensions)
Expand All @@ -111,12 +112,27 @@ dependencies {

testImplementation(Config.Libs.Test.junit)
testImplementation(Config.Libs.Test.truth)
testImplementation(Config.Libs.Test.mockito)
testImplementation(Config.Libs.Test.core)
testImplementation(Config.Libs.Test.robolectric)
testImplementation(Config.Libs.Test.kotlinReflect)
testImplementation(Config.Libs.Provider.facebook)
testImplementation(Config.Libs.Test.mockitoCore)
testImplementation(Config.Libs.Test.mockitoInline)
testImplementation(Config.Libs.Test.mockitoKotlin)
testImplementation(Config.Libs.Androidx.credentials)
testImplementation(Config.Libs.Test.composeUiTestJunit4)

debugImplementation(project(":internal:lintchecks"))
}

val mockitoAgent by configurations.creating

dependencies {
mockitoAgent(Config.Libs.Test.mockitoCore) {
isTransitive = false
}
}

tasks.withType<Test>().configureEach {
jvmArgs("-javaagent:${mockitoAgent.asPath}")
}
77 changes: 68 additions & 9 deletions auth/src/main/java/com/firebase/ui/auth/compose/AuthException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package com.firebase.ui.auth.compose

import com.firebase.ui.auth.compose.AuthException.Companion.from
import com.google.firebase.FirebaseException
import com.google.firebase.auth.FirebaseAuthException
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
Expand Down Expand Up @@ -204,6 +205,39 @@ abstract class AuthException(
cause: Throwable? = null
) : AuthException(message, cause)

class InvalidEmailLinkException(
cause: Throwable? = null
) : AuthException("You are are attempting to sign in with an invalid email link", cause)

class EmailLinkWrongDeviceException(
cause: Throwable? = null
) : AuthException("You must open the email link on the same device.", cause)

class EmailLinkCrossDeviceLinkingException(
cause: Throwable? = null
) : AuthException(
"You must determine if you want to continue linking or " +
"complete the sign in", cause
)

class EmailLinkPromptForEmailException(
cause: Throwable? = null
) : AuthException("Please enter your email to continue signing in", cause)

class EmailLinkDifferentAnonymousUserException(
cause: Throwable? = null
) : AuthException(
"The session associated with this sign-in request has either expired or " +
"was cleared", cause
)

class EmailMismatchException(
cause: Throwable? = null
) : AuthException(
"You are are attempting to sign in a different email than previously " +
"provided", cause
)

companion object {
/**
* Creates an appropriate [AuthException] instance from a Firebase authentication exception.
Expand Down Expand Up @@ -244,86 +278,111 @@ abstract class AuthException(
cause = firebaseException
)
}

is FirebaseAuthInvalidUserException -> {
when (firebaseException.errorCode) {
"ERROR_USER_NOT_FOUND" -> UserNotFoundException(
message = firebaseException.message ?: "User not found",
cause = firebaseException
)

"ERROR_USER_DISABLED" -> InvalidCredentialsException(
message = firebaseException.message ?: "User account has been disabled",
cause = firebaseException
)

else -> UserNotFoundException(
message = firebaseException.message ?: "User account error",
cause = firebaseException
)
}
}

is FirebaseAuthWeakPasswordException -> {
WeakPasswordException(
message = firebaseException.message ?: "Password is too weak",
cause = firebaseException,
reason = firebaseException.reason
)
}

is FirebaseAuthUserCollisionException -> {
when (firebaseException.errorCode) {
"ERROR_EMAIL_ALREADY_IN_USE" -> EmailAlreadyInUseException(
message = firebaseException.message ?: "Email address is already in use",
message = firebaseException.message
?: "Email address is already in use",
cause = firebaseException,
email = firebaseException.email
)

"ERROR_ACCOUNT_EXISTS_WITH_DIFFERENT_CREDENTIAL" -> AccountLinkingRequiredException(
message = firebaseException.message ?: "Account already exists with different credentials",
message = firebaseException.message
?: "Account already exists with different credentials",
cause = firebaseException
)

"ERROR_CREDENTIAL_ALREADY_IN_USE" -> AccountLinkingRequiredException(
message = firebaseException.message ?: "Credential is already associated with a different user account",
message = firebaseException.message
?: "Credential is already associated with a different user account",
cause = firebaseException
)

else -> AccountLinkingRequiredException(
message = firebaseException.message ?: "Account collision error",
cause = firebaseException
)
}
}

is FirebaseAuthMultiFactorException -> {
MfaRequiredException(
message = firebaseException.message ?: "Multi-factor authentication required",
message = firebaseException.message
?: "Multi-factor authentication required",
cause = firebaseException
)
}

is FirebaseAuthRecentLoginRequiredException -> {
InvalidCredentialsException(
message = firebaseException.message ?: "Recent login required for this operation",
message = firebaseException.message
?: "Recent login required for this operation",
cause = firebaseException
)
}

is FirebaseAuthException -> {
// Handle FirebaseAuthException and check for specific error codes
when (firebaseException.errorCode) {
"ERROR_TOO_MANY_REQUESTS" -> TooManyRequestsException(
message = firebaseException.message ?: "Too many requests. Please try again later",
message = firebaseException.message
?: "Too many requests. Please try again later",
cause = firebaseException
)

else -> UnknownException(
message = firebaseException.message ?: "An unknown authentication error occurred",
message = firebaseException.message
?: "An unknown authentication error occurred",
cause = firebaseException
)
}
}

is FirebaseException -> {
// Handle general Firebase exceptions, which include network errors
NetworkException(
message = firebaseException.message ?: "Network error occurred",
cause = firebaseException
)
}

else -> {
// Check for common cancellation patterns
if (firebaseException.message?.contains("cancelled", ignoreCase = true) == true ||
firebaseException.message?.contains("canceled", ignoreCase = true) == true) {
if (firebaseException.message?.contains(
"cancelled",
ignoreCase = true
) == true ||
firebaseException.message?.contains("canceled", ignoreCase = true) == true
) {
AuthCancelledException(
message = firebaseException.message ?: "Authentication was cancelled",
cause = firebaseException
Expand Down
53 changes: 46 additions & 7 deletions auth/src/main/java/com/firebase/ui/auth/compose/AuthState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

package com.firebase.ui.auth.compose

import com.firebase.ui.auth.compose.AuthState.Companion.Cancelled
import com.firebase.ui.auth.compose.AuthState.Companion.Idle
import com.google.firebase.auth.AuthCredential
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.MultiFactorResolver
Expand Down Expand Up @@ -72,8 +75,8 @@ abstract class AuthState private constructor() {
if (this === other) return true
if (other !is Success) return false
return result == other.result &&
user == other.user &&
isNewUser == other.isNewUser
user == other.user &&
isNewUser == other.isNewUser
}

override fun hashCode(): Int {
Expand Down Expand Up @@ -101,7 +104,7 @@ abstract class AuthState private constructor() {
if (this === other) return true
if (other !is Error) return false
return exception == other.exception &&
isRecoverable == other.isRecoverable
isRecoverable == other.isRecoverable
}

override fun hashCode(): Int {
Expand Down Expand Up @@ -137,7 +140,7 @@ abstract class AuthState private constructor() {
if (this === other) return true
if (other !is RequiresMfa) return false
return resolver == other.resolver &&
hint == other.hint
hint == other.hint
}

override fun hashCode(): Int {
Expand All @@ -164,7 +167,7 @@ abstract class AuthState private constructor() {
if (this === other) return true
if (other !is RequiresEmailVerification) return false
return user == other.user &&
email == other.email
email == other.email
}

override fun hashCode(): Int {
Expand All @@ -191,7 +194,7 @@ abstract class AuthState private constructor() {
if (this === other) return true
if (other !is RequiresProfileCompletion) return false
return user == other.user &&
missingFields == other.missingFields
missingFields == other.missingFields
}

override fun hashCode(): Int {
Expand All @@ -204,6 +207,42 @@ abstract class AuthState private constructor() {
"AuthState.RequiresProfileCompletion(user=$user, missingFields=$missingFields)"
}

/**
* Pending credential for an anonymous upgrade merge conflict.
*
* Emitted when an anonymous user attempts to convert to a permanent account but
* Firebase detects that the target email already belongs to another user. The UI can
* prompt the user to resolve the conflict by signing in with the existing account and
* later linking the stored [pendingCredential].
*/
class MergeConflict(
val pendingCredential: AuthCredential
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MergeConflict) return false
return pendingCredential == other.pendingCredential
}

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

override fun toString(): String =
"AuthState.MergeConflict(pendingCredential=$pendingCredential)"
}

/**
* Password reset link has been sent to the user's email.
*/
class PasswordResetLinkSent : AuthState() {
override fun equals(other: Any?): Boolean = other is PasswordResetLinkSent
override fun hashCode(): Int = javaClass.hashCode()
override fun toString(): String = "AuthState.PasswordResetLinkSent"
}

companion object {
/**
* Creates an Idle state instance.
Expand All @@ -219,4 +258,4 @@ abstract class AuthState private constructor() {
@JvmStatic
val Cancelled: Cancelled = Cancelled()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@

package com.firebase.ui.auth.compose

import android.content.Context
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 android.content.Context
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -168,7 +168,8 @@ class FirebaseAuthUI private constructor(
// Check if email verification is required
if (!currentUser.isEmailVerified &&
currentUser.email != null &&
currentUser.providerData.any { it.providerId == "password" }) {
currentUser.providerData.any { it.providerId == "password" }
) {
AuthState.RequiresEmailVerification(
user = currentUser,
email = currentUser.email!!
Expand Down Expand Up @@ -374,7 +375,7 @@ class FirebaseAuthUI private constructor(
} catch (e: IllegalStateException) {
throw IllegalStateException(
"Default FirebaseApp is not initialized. " +
"Make sure to call FirebaseApp.initializeApp(Context) first.",
"Make sure to call FirebaseApp.initializeApp(Context) first.",
e
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
package com.firebase.ui.auth.compose.configuration

import android.content.Context
import java.util.Locale
import com.google.firebase.auth.ActionCodeSettings
import androidx.compose.ui.graphics.vector.ImageVector
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvidersBuilder
import com.firebase.ui.auth.compose.configuration.auth_provider.Provider
import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringProvider
import com.firebase.ui.auth.compose.configuration.string_provider.DefaultAuthUIStringProvider
import com.firebase.ui.auth.compose.configuration.theme.AuthUITheme

fun actionCodeSettings(block: ActionCodeSettings.Builder.() -> Unit) =
ActionCodeSettings.newBuilder().apply(block).build()
import com.google.firebase.auth.ActionCodeSettings
import java.util.Locale

fun authUIConfiguration(block: AuthUIConfigurationBuilder.() -> Unit) =
AuthUIConfigurationBuilder().apply(block).build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.firebase.ui.auth.compose.configuration.string_provider.AuthUIStringPr

/**
* An abstract class representing a set of validation rules that can be applied to a password field,
* typically within the [AuthProvider.Email] configuration.
* typically within the [com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider.Email] configuration.
*/
abstract class PasswordRule {
/**
Expand Down
Loading