Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ class AuthUIConfigurationBuilder {
"At least one provider must be configured"
}

// No unsupported providers
// No unsupported providers (allow predefined providers and custom OIDC providers starting with "oidc.")
val supportedProviderIds = Provider.entries.map { it.id }.toSet()
val unknownProviders = providers.filter { it.providerId !in supportedProviderIds }
val unknownProviders = providers.filter { provider ->
provider.providerId !in supportedProviderIds && !provider.providerId.startsWith("oidc.")
}
require(unknownProviders.isEmpty()) {
"Unknown providers: ${unknownProviders.joinToString { it.providerId }}"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,22 @@ class AuthProvidersBuilder {
/**
* Enum class to represent all possible providers.
*/
internal enum class Provider(val id: String, val isSocialProvider: Boolean = false) {
GOOGLE(GoogleAuthProvider.PROVIDER_ID, isSocialProvider = true),
FACEBOOK(FacebookAuthProvider.PROVIDER_ID, isSocialProvider = true),
TWITTER(TwitterAuthProvider.PROVIDER_ID, isSocialProvider = true),
GITHUB(GithubAuthProvider.PROVIDER_ID, isSocialProvider = true),
EMAIL(EmailAuthProvider.PROVIDER_ID),
PHONE(PhoneAuthProvider.PROVIDER_ID),
ANONYMOUS("anonymous"),
MICROSOFT("microsoft.com"),
YAHOO("yahoo.com"),
APPLE("apple.com");
internal enum class Provider(
val id: String,
val providerName: String,
val isSocialProvider: Boolean = false,
) {
GOOGLE(GoogleAuthProvider.PROVIDER_ID, providerName = "Google", isSocialProvider = true),
FACEBOOK(FacebookAuthProvider.PROVIDER_ID, providerName = "Facebook", isSocialProvider = true),
TWITTER(TwitterAuthProvider.PROVIDER_ID, providerName = "Twitter", isSocialProvider = true),
GITHUB(GithubAuthProvider.PROVIDER_ID, providerName = "Github", isSocialProvider = true),
EMAIL(EmailAuthProvider.PROVIDER_ID, providerName = "Email"),
PHONE(PhoneAuthProvider.PROVIDER_ID, providerName = "Phone"),
ANONYMOUS("anonymous", providerName = "Anonymous"),
MICROSOFT("microsoft.com", providerName = "Microsoft"),
YAHOO("yahoo.com", providerName = "Yahoo"),
APPLE("apple.com", providerName = "Apple"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Are those not considered social provider here?

Copy link
Author

Choose a reason for hiding this comment

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

yes actually

LINE("oidc.line", providerName = "LINE", isSocialProvider = true);

companion object {
fun fromId(id: String): Provider? {
Expand All @@ -93,83 +98,20 @@ internal enum class Provider(val id: String, val isSocialProvider: Boolean = fal
}
}

/**
* Base abstract class for OAuth authentication providers with common properties.
*/
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 = providerId, name = name)

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

companion object {
internal fun canUpgradeAnonymous(config: AuthUIConfiguration, auth: FirebaseAuth): Boolean {
val currentUser = auth.currentUser
return config.isAnonymousUpgradeEnabled
&& currentUser != null
&& currentUser.isAnonymous
}

/**
* Merges profile information (display name and photo URL) with the current user's profile.
*
* This method updates the user's profile only if the current profile is incomplete
* (missing display name or photo URL). This prevents overwriting existing profile data.
*
* **Use case:**
* After creating a new user account or linking credentials, update the profile with
* information from the sign-up form or social provider.
*
* @param auth The [FirebaseAuth] instance
* @param displayName The display name to set (if current is empty)
* @param photoUri The photo URL to set (if current is null)
*
* **Note:** This operation always succeeds to minimize login interruptions.
* Failures are logged but don't prevent sign-in completion.
*/
internal suspend fun mergeProfile(
auth: FirebaseAuth,
displayName: String?,
photoUri: Uri?,
) {
try {
val currentUser = auth.currentUser ?: return

// Only update if current profile is incomplete
val currentDisplayName = currentUser.displayName
val currentPhotoUrl = currentUser.photoUrl

if (!currentDisplayName.isNullOrEmpty() && currentPhotoUrl != null) {
// Profile is complete, no need to update
return
}

// Build profile update with provided values
val nameToSet =
if (currentDisplayName.isNullOrEmpty()) displayName else currentDisplayName
val photoToSet = currentPhotoUrl ?: photoUri

if (nameToSet != null || photoToSet != null) {
val profileUpdates = UserProfileChangeRequest.Builder()
.setDisplayName(nameToSet)
.setPhotoUri(photoToSet)
.build()
abstract class AuthProvider(open val providerId: String, open val providerName: String) {
/**
* Base abstract class for OAuth authentication providers with common properties.
*/
abstract class OAuth(
override val providerId: String,

currentUser.updateProfile(profileUpdates).await()
}
} catch (e: Exception) {
// Log error but don't throw - profile update failure shouldn't prevent sign-in
Log.e("AuthProvider.Email", "Error updating profile", e)
}
}
}
override val providerName: String,
open val scopes: List<String> = emptyList(),
open val customParameters: Map<String, String> = emptyMap(),
) : AuthProvider(providerId = providerId, providerName = providerName)

/**
* Email/Password authentication provider configuration.
Expand Down Expand Up @@ -212,7 +154,7 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A list of custom password validation rules.
*/
val passwordValidationRules: List<PasswordRule>,
) : AuthProvider(providerId = Provider.EMAIL.id, name = "Email") {
) : AuthProvider(providerId = Provider.EMAIL.id, providerName = Provider.EMAIL.providerName) {
companion object {
const val SESSION_ID_LENGTH = 10
val KEY_EMAIL = stringPreferencesKey("com.firebase.ui.auth.data.client.email")
Expand Down Expand Up @@ -338,7 +280,7 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* Enables instant verification of the phone number. Defaults to true.
*/
val isInstantVerificationEnabled: Boolean = true,
) : AuthProvider(providerId = Provider.PHONE.id, name = "Phone") {
) : AuthProvider(providerId = Provider.PHONE.id, providerName = Provider.PHONE.providerName) {
/**
* Sealed class representing the result of phone number verification.
*
Expand Down Expand Up @@ -562,9 +504,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String> = emptyMap(),
) : OAuthProvider(
) : OAuth(
providerId = Provider.GOOGLE.id,
name = "Google",
providerName = Provider.GOOGLE.providerName,
scopes = scopes,
customParameters = customParameters
) {
Expand Down Expand Up @@ -691,9 +633,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String> = emptyMap(),
) : OAuthProvider(
) : OAuth(
providerId = Provider.FACEBOOK.id,
name = "Facebook",
providerName = Provider.FACEBOOK.providerName,
scopes = scopes,
customParameters = customParameters
) {
Expand Down Expand Up @@ -835,9 +777,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String>,
) : OAuthProvider(
) : OAuth(
providerId = Provider.TWITTER.id,
name = "Twitter",
providerName = Provider.TWITTER.providerName,
customParameters = customParameters
)

Expand All @@ -854,9 +796,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String>,
) : OAuthProvider(
) : OAuth(
providerId = Provider.GITHUB.id,
name = "Github",
providerName = Provider.GITHUB.providerName,
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -879,9 +821,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String>,
) : OAuthProvider(
) : OAuth(
providerId = Provider.MICROSOFT.id,
name = "Microsoft",
providerName = Provider.MICROSOFT.providerName,
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -899,9 +841,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String>,
) : OAuthProvider(
) : OAuth(
providerId = Provider.YAHOO.id,
name = "Yahoo",
providerName = Provider.YAHOO.providerName,
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -924,9 +866,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* A map of custom OAuth parameters.
*/
override val customParameters: Map<String, String>,
) : OAuthProvider(
) : OAuth(
providerId = Provider.APPLE.id,
name = "Apple",
providerName = Provider.APPLE.providerName,
scopes = scopes,
customParameters = customParameters
)
Expand All @@ -936,7 +878,7 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
*/
object Anonymous : AuthProvider(
providerId = Provider.ANONYMOUS.id,
name = "Anonymous"
providerName = Provider.ANONYMOUS.providerName
) {
internal fun validate(providers: List<AuthProvider>) {
if (providers.size == 1 && providers.first() is Anonymous) {
Expand All @@ -948,14 +890,26 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
}
}

class Line(
override val scopes: List<String>,
override val customParameters: Map<String, String>,
) : OAuth(
providerId = Provider.LINE.id,
providerName = Provider.LINE.providerName,
scopes = scopes,
customParameters = customParameters
) {
internal fun validate() {}
}

/**
* A generic OAuth provider for any unsupported provider.
*/
class GenericOAuth(
/**
* The provider name.
*/
override val name: String,
override val providerName: String,

/**
* The provider ID as configured in the Firebase console.
Expand Down Expand Up @@ -991,9 +945,9 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
* An optional content color for the provider button.
*/
val contentColor: Color?,
) : OAuthProvider(
) : OAuth(
providerId = providerId,
name = name,
providerName = providerName,
scopes = scopes,
customParameters = customParameters
) {
Expand All @@ -1007,4 +961,66 @@ abstract class AuthProvider(open val providerId: String, open val name: String)
}
}
}

companion object {
internal fun canUpgradeAnonymous(config: AuthUIConfiguration, auth: FirebaseAuth): Boolean {
val currentUser = auth.currentUser
return config.isAnonymousUpgradeEnabled
&& currentUser != null
&& currentUser.isAnonymous
}

/**
* Merges profile information (display name and photo URL) with the current user's profile.
*
* This method updates the user's profile only if the current profile is incomplete
* (missing display name or photo URL). This prevents overwriting existing profile data.
*
* **Use case:**
* After creating a new user account or linking credentials, update the profile with
* information from the sign-up form or social provider.
*
* @param auth The [FirebaseAuth] instance
* @param displayName The display name to set (if current is empty)
* @param photoUri The photo URL to set (if current is null)
*
* **Note:** This operation always succeeds to minimize login interruptions.
* Failures are logged but don't prevent sign-in completion.
*/
internal suspend fun mergeProfile(
auth: FirebaseAuth,
displayName: String?,
photoUri: Uri?,
) {
try {
val currentUser = auth.currentUser ?: return

// Only update if current profile is incomplete
val currentDisplayName = currentUser.displayName
val currentPhotoUrl = currentUser.photoUrl

if (!currentDisplayName.isNullOrEmpty() && currentPhotoUrl != null) {
// Profile is complete, no need to update
return
}

// Build profile update with provided values
val nameToSet =
if (currentDisplayName.isNullOrEmpty()) displayName else currentDisplayName
val photoToSet = currentPhotoUrl ?: photoUri

if (nameToSet != null || photoToSet != null) {
val profileUpdates = UserProfileChangeRequest.Builder()
.setDisplayName(nameToSet)
.setPhotoUri(photoToSet)
.build()

currentUser.updateProfile(profileUpdates).await()
}
} catch (e: Exception) {
// Log error but don't throw - profile update failure shouldn't prevent sign-in
Log.e("AuthProvider.Email", "Error updating profile", e)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ internal suspend fun FirebaseAuthUI.signInAndLinkWithCredential(
val accountLinkingException = AuthException.AccountLinkingRequiredException(
message = "An account already exists with the email ${email ?: ""}. " +
"Please sign in with your existing account to link " +
"your ${provider?.name ?: "this provider"} account.",
"your ${provider?.providerName ?: "this provider"} account.",
email = email,
credential = credentialForException,
cause = e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,6 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
}

override fun onCancel() {
// val cancelledException = AuthException.AuthCancelledException(
// message = "Sign in with facebook was cancelled",
// )
// updateAuthState(AuthState.Error(cancelledException))
updateAuthState(AuthState.Idle)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ internal fun FirebaseAuthUI.rememberGoogleSignInHandler(
try {
signInWithGoogle(context, config, provider)
} catch (e: AuthException) {
// Log.d("rememberGoogleSignInHandler", "exception: $e")
updateAuthState(AuthState.Error(e))
} catch (e: Exception) {
val authException = AuthException.from(e)
Expand Down Expand Up @@ -123,7 +124,10 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle(
try {
val requestedScopes = provider.scopes.map { Scope(it) }
authorizationProvider.authorize(context, requestedScopes)
// Log.d("GoogleSignIn", "Successfully authorized scopes: ${provider.scopes}")
} catch (e: Exception) {
// Log.w("GoogleSignIn", "Failed to authorize scopes: ${provider.scopes}", e)
// Continue with sign-in even if scope authorization fails
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove the 3 commented logs here

val authException = AuthException.from(e)
updateAuthState(AuthState.Error(authException))
}
Expand Down
Loading