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
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@

package com.firebase.ui.auth.compose.configuration

import android.content.Context
import android.graphics.Color
import android.util.Log
import androidx.compose.ui.graphics.vector.ImageVector
import com.firebase.ui.auth.AuthUI
import com.firebase.ui.auth.R
import com.firebase.ui.auth.util.Preconditions
import com.firebase.ui.auth.util.data.PhoneNumberUtils
import com.firebase.ui.auth.util.data.ProviderAvailability
import com.google.firebase.auth.ActionCodeSettings
import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FacebookAuthProvider
Expand Down Expand Up @@ -78,6 +85,14 @@ abstract class AuthProvider(open val providerId: String) {
*/
val isEmailLinkSignInEnabled: Boolean = false,

/**
* Forces email link sign-in to complete on the same device that initiated it.
*
* When enabled, prevents email links from being opened on different devices,
* which is required for security when upgrading anonymous users. Defaults to true.
*/
val isEmailLinkForceSameDeviceEnabled: Boolean = true,
Copy link
Author

Choose a reason for hiding this comment

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

this was a requirement and a config validation in the old auth library for EmailAuthProvider. When anonymous upgrade is enabled and email link is enabled, you must force same device for security purposes. here


/**
* Settings for email link actions.
*/
Expand All @@ -100,11 +115,10 @@ abstract class AuthProvider(open val providerId: String) {
) : AuthProvider(providerId = Provider.EMAIL.id) {
fun validate() {
if (isEmailLinkSignInEnabled) {
val actionCodeSettings = actionCodeSettings
?: requireNotNull(actionCodeSettings) {
"ActionCodeSettings cannot be null when using " +
"email link sign in."
}
val actionCodeSettings = requireNotNull(actionCodeSettings) {
"ActionCodeSettings cannot be null when using " +
"email link sign in."
}

check(actionCodeSettings.canHandleCodeInApp()) {
"You must set canHandleCodeInApp in your " +
Expand All @@ -118,6 +132,11 @@ abstract class AuthProvider(open val providerId: String) {
* Phone number authentication provider configuration.
*/
class Phone(
/**
* The phone number in international format.
*/
val defaultNumber: String?,

/**
* The default country code to pre-select.
*/
Expand Down Expand Up @@ -147,7 +166,31 @@ abstract class AuthProvider(open val providerId: String) {
* Enables automatic retrieval of the SMS code. Defaults to true.
*/
val isAutoRetrievalEnabled: Boolean = true
) : AuthProvider(providerId = Provider.PHONE.id)
) : AuthProvider(providerId = Provider.PHONE.id) {
fun validate() {
defaultNumber?.let {
check(PhoneNumberUtils.isValid(it)) {
"Invalid phone number: $it"
}
}

defaultCountryCode?.let {
check(PhoneNumberUtils.isValidIso(it)) {
"Invalid country iso: $it"
}
}

allowedCountries?.forEach { code ->
check(
PhoneNumberUtils.isValidIso(code) ||
PhoneNumberUtils.isValid(code)
) {
"Invalid input: You must provide a valid country iso (alpha-2) " +
"or code (e-164). e.g. 'us' or '+1'. Invalid code: $code"
}
}
}
}

/**
* Google Sign-In provider configuration.
Expand Down Expand Up @@ -186,12 +229,40 @@ abstract class AuthProvider(open val providerId: String) {
providerId = Provider.GOOGLE.id,
scopes = scopes,
customParameters = customParameters
)
) {
fun validate(context: Context) {
if (serverClientId == null) {
Preconditions.checkConfigured(
context,
"Check your google-services plugin configuration, the" +
" default_web_client_id string wasn't populated.",
R.string.default_web_client_id
)
} else {
require(serverClientId.isNotBlank()) {
"Server client ID cannot be blank."
}
}

val hasEmailScope = scopes.contains("email")
if (!hasEmailScope) {
Log.w(
"AuthProvider.Google",
"The scopes do not include 'email'. In most cases this is a mistake!"
)
}
}
}

/**
* Facebook Login provider configuration.
*/
class Facebook(
/**
* The Facebook application ID.
*/
val applicationId: String? = null,

/**
* The list of scopes (permissions) to request. Defaults to email and public_profile.
*/
Expand All @@ -210,7 +281,30 @@ abstract class AuthProvider(open val providerId: String) {
providerId = Provider.FACEBOOK.id,
scopes = scopes,
customParameters = customParameters
)
) {
fun validate(context: Context) {
if (!ProviderAvailability.IS_FACEBOOK_AVAILABLE) {
throw RuntimeException(
"Facebook provider cannot be configured " +
"without dependency. Did you forget to add " +
"'com.facebook.android:facebook-login:VERSION' dependency?"
)
}

if (applicationId == null) {
Preconditions.checkConfigured(
context,
"Facebook provider unconfigured. Make sure to " +
"add a `facebook_application_id` string or provide applicationId parameter.",
R.string.facebook_application_id
)
} else {
require(applicationId.isNotBlank()) {
"Facebook application ID cannot be blank"
}
}
}
}

/**
* Twitter/X authentication provider configuration.
Expand Down Expand Up @@ -314,7 +408,16 @@ abstract class AuthProvider(open val providerId: String) {
/**
* Anonymous authentication provider. It has no configurable properties.
*/
object Anonymous : AuthProvider(providerId = Provider.ANONYMOUS.id)
object Anonymous : AuthProvider(providerId = Provider.ANONYMOUS.id) {
fun validate(providers: List<AuthProvider>) {
if (providers.size == 1 && providers.first() is Anonymous) {
throw IllegalStateException(
"Sign in as guest cannot be the only sign in method. " +
"In this case, sign the user in anonymously your self; no UI is needed."
)
}
}
}

/**
* A generic OAuth provider for any unsupported provider.
Expand Down Expand Up @@ -353,5 +456,15 @@ abstract class AuthProvider(open val providerId: String) {
providerId = providerId,
scopes = scopes,
customParameters = customParameters
)
) {
fun validate() {
require(providerId.isNotBlank()) {
"Provider ID cannot be null or empty"
}

require(buttonLabel.isNotBlank()) {
"Button label cannot be null or empty"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,7 @@ class AuthUIConfigurationBuilder {
}

// Cannot have only anonymous provider
if (providers.size == 1 && providers.first() is AuthProvider.Anonymous) {
throw IllegalStateException(
"Sign in as guest cannot be the only sign in method. " +
"In this case, sign the user in anonymously your self; no UI is needed."
)
}
AuthProvider.Anonymous.validate(providers)

// Check for duplicate providers
val providerIds = providers.map { it.providerId }
Expand All @@ -89,7 +84,21 @@ class AuthUIConfigurationBuilder {
// Provider specific validations
providers.forEach { provider ->
when (provider) {
is AuthProvider.Email -> provider.validate()
is AuthProvider.Email -> {
provider.validate()

if (isAnonymousUpgradeEnabled && provider.isEmailLinkSignInEnabled) {
check(provider.isEmailLinkForceSameDeviceEnabled) {
"You must force the same device flow when using email link sign in " +
"with anonymous user upgrade"
}
}
}

is AuthProvider.Phone -> provider.validate()
is AuthProvider.Google -> provider.validate(context)
is AuthProvider.Facebook -> provider.validate(context)
is AuthProvider.GenericOAuth -> provider.validate()
else -> null
}
}
Expand Down
Loading