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
2 changes: 2 additions & 0 deletions .firebase/hosting.cHVibGlj.cache
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
index.html,1760725054923,96d3ff69603ba92f085431c7b56242a873ddcdd5a1c9691f7836b093f8114a5a
.well-known/assetlinks.json,1760725039101,cbfe2437a47d2f4a2bca9bb7c1c789b4684d6a13694821e46e4177ccce023f4b
24 changes: 19 additions & 5 deletions auth/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
android:name="com.facebook.sdk.ApplicationId"
android:value="@string/facebook_application_id" />

<meta-data
android:name="com.facebook.sdk.ClientToken"
android:value="@string/facebook_client_token"/>

<activity
android:name=".KickoffActivity"
android:label=""
Expand Down Expand Up @@ -120,14 +124,24 @@
</activity>

<!-- Email Link Sign-In Handler Activity for Compose -->
<!-- IMPORTANT: This activity is NOT exported by default -->
<!-- Users must declare this activity with an intent filter in their app's AndroidManifest.xml -->
<!-- See documentation for setup instructions -->
<!-- This activity handles deep links for passwordless email authentication -->
<!-- The host is automatically read from firebase_web_host in config.xml -->
<!-- NOTE: firebase_web_host must be lowercase (e.g., project-id.firebaseapp.com) -->
<activity
android:name=".compose.ui.screens.EmailSignInLinkHandlerActivity"
android:label=""
android:exported="false"
android:theme="@style/FirebaseUI.Transparent" />
android:exported="true"
android:theme="@style/FirebaseUI.Transparent">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="@string/firebase_web_host"
tools:ignore="AppLinksAutoVerify,AppLinkUrlError" />
</intent-filter>
</activity>

<provider
android:name=".data.client.AuthUiInitProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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.AuthCredential
import com.google.firebase.auth.FirebaseAuthException
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.FirebaseAuthInvalidUserException
Expand Down Expand Up @@ -167,13 +168,19 @@ abstract class AuthException(
* Account linking is required to complete sign-in.
*
* This exception is thrown when a user tries to sign in with a provider
* that needs to be linked to an existing account.
* that needs to be linked to an existing account. For example, when a user
* tries to sign in with Facebook but an account already exists with that
* email using a different provider (like email/password).
*
* @property message The detailed error message
* @property email The email address that already has an account (optional)
* @property credential The credential that should be linked after signing in (optional)
* @property cause The underlying [Throwable] that caused this exception
*/
class AccountLinkingRequiredException(
message: String,
val email: String? = null,
val credential: AuthCredential? = null,
cause: Throwable? = null
) : AuthException(message, cause)

Expand Down
27 changes: 0 additions & 27 deletions auth/src/main/java/com/firebase/ui/auth/compose/AuthState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -209,33 +209,6 @@ 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.tasks.await
import java.util.concurrent.ConcurrentHashMap

Expand Down Expand Up @@ -221,7 +218,7 @@ class FirebaseAuthUI private constructor(
) { firebaseState, internalState ->
// Prefer non-idle internal states (like PasswordResetLinkSent, Error, etc.)
if (internalState !is AuthState.Idle) internalState else firebaseState
}
}.distinctUntilChanged()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import android.app.Activity
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.datastore.preferences.core.stringPreferencesKey
import com.facebook.AccessToken
import com.firebase.ui.auth.R
import com.firebase.ui.auth.compose.configuration.AuthUIConfiguration
import com.firebase.ui.auth.compose.configuration.AuthUIConfigurationDsl
Expand All @@ -44,8 +48,8 @@ import com.google.firebase.auth.PhoneAuthProvider
import com.google.firebase.auth.TwitterAuthProvider
import com.google.firebase.auth.UserProfileChangeRequest
import com.google.firebase.auth.actionCodeSettings
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.Serializable
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
Expand Down Expand Up @@ -89,14 +93,16 @@ internal enum class Provider(val id: String, val isSocialProvider: Boolean = fal
*/
abstract class OAuthProvider(
override val providerId: String,

override val name: String,
open val scopes: List<String> = emptyList(),
open val customParameters: Map<String, String> = emptyMap(),
) : AuthProvider(providerId)
) : AuthProvider(providerId = providerId, name = name)

/**
* Base abstract class for authentication providers.
*/
abstract class AuthProvider(open val providerId: String) {
abstract class AuthProvider(open val providerId: String, open val name: String) {

companion object {
internal fun canUpgradeAnonymous(config: AuthUIConfiguration, auth: FirebaseAuth): Boolean {
Expand Down Expand Up @@ -201,7 +207,7 @@ abstract class AuthProvider(open val providerId: String) {
* A list of custom password validation rules.
*/
val passwordValidationRules: List<PasswordRule>,
) : AuthProvider(providerId = Provider.EMAIL.id) {
) : AuthProvider(providerId = Provider.EMAIL.id, name = "Email") {
companion object {
const val SESSION_ID_LENGTH = 10
val KEY_EMAIL = stringPreferencesKey("com.firebase.ui.auth.data.client.email")
Expand Down Expand Up @@ -327,7 +333,7 @@ abstract class AuthProvider(open val providerId: String) {
* Enables instant verification of the phone number. Defaults to true.
*/
val isInstantVerificationEnabled: Boolean = true,
) : AuthProvider(providerId = Provider.PHONE.id) {
) : AuthProvider(providerId = Provider.PHONE.id, name = "Phone") {
/**
* Sealed class representing the result of phone number verification.
*
Expand Down Expand Up @@ -550,6 +556,7 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String> = emptyMap(),
) : OAuthProvider(
providerId = Provider.GOOGLE.id,
name = "Google",
scopes = scopes,
customParameters = customParameters
) {
Expand Down Expand Up @@ -591,17 +598,13 @@ abstract class AuthProvider(open val providerId: String) {
*/
override val scopes: List<String> = listOf("email", "public_profile"),

/**
* if true, enable limited login mode. Defaults to false.
*/
val limitedLogin: Boolean = false,

/**
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String> = emptyMap(),
) : OAuthProvider(
providerId = Provider.FACEBOOK.id,
name = "Facebook",
scopes = scopes,
customParameters = customParameters
) {
Expand All @@ -627,6 +630,113 @@ abstract class AuthProvider(open val providerId: String) {
}
}
}

/**
* An interface to wrap the static `FacebookAuthProvider.getCredential` method to make it testable.
* @suppress
*/
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
interface CredentialProvider {
fun getCredential(token: String): AuthCredential
}

/**
* The default implementation of [CredentialProvider] that calls the static method.
* @suppress
*/
internal class DefaultCredentialProvider : CredentialProvider {
override fun getCredential(token: String): AuthCredential {
return FacebookAuthProvider.getCredential(token)
}
}

/**
* Internal data class to hold Facebook profile information.
*/
internal class FacebookProfileData(
val displayName: String?,
val email: String?,
val photoUrl: Uri?,
)

/**
* Fetches user profile data from Facebook Graph API.
*
* @param accessToken The Facebook access token
* @return FacebookProfileData containing user's display name, email, and photo URL
*/
internal suspend fun fetchFacebookProfile(accessToken: AccessToken): FacebookProfileData? {
return suspendCancellableCoroutine { continuation ->
val request =
com.facebook.GraphRequest.newMeRequest(accessToken) { jsonObject, response ->
try {
val error = response?.error
if (error != null) {
Log.e(
"FirebaseAuthUI.signInWithFacebook",
"Graph API error: ${error.errorMessage}"
)
continuation.resume(null)
return@newMeRequest
}

if (jsonObject == null) {
Log.e(
"FirebaseAuthUI.signInWithFacebook",
"Graph API returned null response"
)
continuation.resume(null)
return@newMeRequest
}

val name = jsonObject.optString("name")
val email = jsonObject.optString("email")

// Extract photo URL from picture object
val photoUrl = try {
jsonObject.optJSONObject("picture")
?.optJSONObject("data")
?.optString("url")
?.takeIf { it.isNotEmpty() }?.toUri()
} catch (e: Exception) {
Log.w(
"FirebaseAuthUI.signInWithFacebook",
"Error parsing photo URL",
e
)
null
}

Log.d(
"FirebaseAuthUI.signInWithFacebook",
"Profile fetched: name=$name, email=$email, hasPhoto=${photoUrl != null}"
)

continuation.resume(
FacebookProfileData(
displayName = name,
email = email,
photoUrl = photoUrl
)
)
} catch (e: Exception) {
Log.e(
"FirebaseAuthUI.signInWithFacebook",
"Error processing Graph API response",
e
)
continuation.resume(null)
}
}

// Request specific fields: id, name, email, and picture
val parameters = android.os.Bundle().apply {
putString("fields", "id,name,email,picture")
}
request.parameters = parameters
request.executeAsync()
}
}
}

/**
Expand All @@ -639,6 +749,7 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String>,
) : OAuthProvider(
providerId = Provider.TWITTER.id,
name = "Twitter",
customParameters = customParameters
)

Expand All @@ -657,6 +768,7 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String>,
) : OAuthProvider(
providerId = Provider.GITHUB.id,
name = "Github",
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -681,6 +793,7 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String>,
) : OAuthProvider(
providerId = Provider.MICROSOFT.id,
name = "Microsoft",
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -700,6 +813,7 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String>,
) : OAuthProvider(
providerId = Provider.YAHOO.id,
name = "Yahoo",
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -724,14 +838,18 @@ abstract class AuthProvider(open val providerId: String) {
override val customParameters: Map<String, String>,
) : OAuthProvider(
providerId = Provider.APPLE.id,
name = "Apple",
scopes = scopes,
customParameters = customParameters
)

/**
* Anonymous authentication provider. It has no configurable properties.
*/
object Anonymous : AuthProvider(providerId = Provider.ANONYMOUS.id) {
object Anonymous : AuthProvider(
providerId = Provider.ANONYMOUS.id,
name = "Anonymous"
) {
internal fun validate(providers: List<AuthProvider>) {
if (providers.size == 1 && providers.first() is Anonymous) {
throw IllegalStateException(
Expand All @@ -746,6 +864,11 @@ abstract class AuthProvider(open val providerId: String) {
* A generic OAuth provider for any unsupported provider.
*/
class GenericOAuth(
/**
* The provider name.
*/
override val name: String,

/**
* The provider ID as configured in the Firebase console.
*/
Expand Down Expand Up @@ -782,6 +905,7 @@ abstract class AuthProvider(open val providerId: String) {
val contentColor: Color?,
) : OAuthProvider(
providerId = providerId,
name = name,
scopes = scopes,
customParameters = customParameters
) {
Expand Down
Loading