Skip to content
Draft
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
37 changes: 27 additions & 10 deletions auth/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,24 @@ android {
}

dependencies {
implementation(platform(Config.Libs.Androidx.Compose.bom))
implementation(Config.Libs.Androidx.Compose.ui)
implementation(Config.Libs.Androidx.Compose.uiGraphics)
implementation(Config.Libs.Androidx.Compose.material3)
implementation(Config.Libs.Androidx.Compose.foundation)
implementation(Config.Libs.Androidx.Compose.tooling)
implementation(Config.Libs.Androidx.Compose.toolingPreview)
implementation(Config.Libs.Androidx.Compose.activityCompose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(Config.Libs.Androidx.materialDesign)
implementation(Config.Libs.Androidx.activity)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.datastore.preferences)
// 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(libs.androidx.credentials)
implementation("androidx.credentials:credentials-play-services-auth:1.3.0")

implementation(Config.Libs.Androidx.lifecycleExtensions)
Expand All @@ -110,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(libs.androidx.ui.test.junit4)
testImplementation(libs.mockito)
testImplementation(libs.mockito.inline)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.androidx.credentials)

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

val mockitoAgent by configurations.creating

dependencies {
mockitoAgent(libs.mockito) {
isTransitive = false
}
}

tasks.withType<Test>().configureEach {
jvmArgs("-javaagent:${mockitoAgent.asPath}")
}
76 changes: 67 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,38 @@ 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 +277,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
61 changes: 60 additions & 1 deletion 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,8 @@

package com.firebase.ui.auth.compose

import com.firebase.ui.auth.compose.configuration.auth_provider.AuthProvider
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 @@ -204,6 +206,63 @@ abstract class AuthState private constructor() {
"AuthState.RequiresProfileCompletion(user=$user, missingFields=$missingFields)"
}

/**
* The user needs to sign in with a different provider.
*
* Emitted when a user tries to sign up with an email that already exists
* and needs to use the existing provider to sign in instead.
*
* @property provider The [AuthProvider] the user should sign in with
* @property email The email address of the existing account
*/
class RequiresSignIn(
val provider: AuthProvider,
val email: String
) : AuthState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RequiresSignIn) return false
return provider == other.provider &&
email == other.email
}

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

override fun toString(): String =
"AuthState.RequiresSignIn(provider=$provider, email=$email)"
}

/**
* 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)"
}

companion object {
/**
* Creates an Idle state instance.
Expand All @@ -219,4 +278,4 @@ abstract class AuthState private constructor() {
@JvmStatic
val Cancelled: Cancelled = Cancelled()
}
}
}
Original file line number Diff line number Diff line change
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 @@ -20,11 +20,11 @@ import com.google.firebase.auth.ActionCodeSettings
import androidx.compose.ui.graphics.vector.ImageVector
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.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.theme.AuthUITheme

fun actionCodeSettings(block: ActionCodeSettings.Builder.() -> Unit) =
ActionCodeSettings.newBuilder().apply(block).build()

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