Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -57,6 +63,7 @@ class HighLevelApiDemoActivity : ComponentActivity() {
tosUrl = "https://policies.google.com/terms"
privacyPolicyUrl = "https://policies.google.com/privacy"
isAnonymousUpgradeEnabled = false
isMfaEnabled = false
transitions = AuthUITransitions(
enterTransition = { slideInHorizontally { it } },
exitTransition = { slideOutHorizontally { -it } },
Expand Down Expand Up @@ -193,12 +200,14 @@ class HighLevelApiDemoActivity : ComponentActivity() {
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AppAuthenticatedContent(
state: AuthState,
uiContext: AuthSuccessUiContext
) {
val stringProvider = uiContext.stringProvider
val configuration = uiContext.configuration
when (state) {
is AuthState.Success -> {
val user = uiContext.authUI.getCurrentUser()
Expand Down Expand Up @@ -226,8 +235,25 @@ private fun AppAuthenticatedContent(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = uiContext.onManageMfa) {
Text(stringProvider.manageMfaAction)
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Above
),
tooltip = {
PlainTooltip {
Text(stringProvider.mfaDisabledTooltip)
}
},
state = rememberTooltipState(
initialIsVisible = !configuration.isMfaEnabled
)
) {
Button(
onClick = uiContext.onManageMfa,
enabled = configuration.isMfaEnabled
) {
Text(stringProvider.manageMfaAction)
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = uiContext.onSignOut) {
Expand Down
4 changes: 1 addition & 3 deletions auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ If using Facebook Login, add your Facebook App ID to `strings.xml`:
<resources>
<string name="facebook_application_id" translatable="false">YOUR_FACEBOOK_APP_ID</string>
<string name="facebook_login_protocol_scheme" translatable="false">fbYOUR_FACEBOOK_APP_ID</string>
<string name="facebook_client_token" translatable="false">CHANGE-ME</string>
</resources>
```

Expand Down Expand Up @@ -489,9 +490,6 @@ Configure Facebook Login with optional permissions:

```kotlin
val facebookProvider = AuthProvider.Facebook(
// Optional: Facebook application ID (reads from strings.xml if not provided)
applicationId = "YOUR_FACEBOOK_APP_ID",

// Optional: Permissions to request (default: ["email", "public_profile"])
scopes = listOf("email", "public_profile", "user_friends"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,21 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
/**
* The OAuth 2.0 client ID for your server.
*/
val serverClientId: String?,
var serverClientId: String?,

/**
* Whether to filter by authorized accounts.
* When true, only shows Google accounts that have previously authorized this app.
* Defaults to true, with automatic fallback to false if no authorized accounts found.
*/
val filterByAuthorizedAccounts: Boolean = true,

/**
* Whether to enable auto-select for single account scenarios.
* When true, automatically selects the account if only one is available.
* Defaults to false for better user control.
*/
val autoSelectEnabled: Boolean = false,

/**
* A map of custom OAuth parameters.
Expand All @@ -505,8 +519,9 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
" default_web_client_id string wasn't populated.",
R.string.default_web_client_id
)
serverClientId = context.getString(R.string.default_web_client_id)
} else {
require(serverClientId.isNotBlank()) {
require(serverClientId!!.isNotBlank()) {
"Server client ID cannot be blank."
}
}
Expand All @@ -529,7 +544,7 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
val credential: AuthCredential,
val idToken: String,
val displayName: String?,
val photoUrl: Uri?
val photoUrl: Uri?,
)

/**
Expand Down Expand Up @@ -567,7 +582,7 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
credentialManager: CredentialManager,
serverClientId: String,
filterByAuthorizedAccounts: Boolean,
autoSelectEnabled: Boolean
autoSelectEnabled: Boolean,
): GoogleSignInResult

suspend fun clearCredentialState(
Expand Down Expand Up @@ -600,8 +615,10 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
.build()

val result = credentialManager.getCredential(context, request)
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(result.credential.data)
val credential = GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null)
val googleIdTokenCredential =
GoogleIdTokenCredential.createFrom(result.credential.data)
val credential =
GoogleAuthProvider.getCredential(googleIdTokenCredential.idToken, null)

return GoogleSignInResult(
credential = credential,
Expand All @@ -624,11 +641,6 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
* 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 @@ -653,18 +665,26 @@ abstract class AuthProvider(open val providerId: String, open val providerName:
)
}

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"
}
}
Preconditions.checkConfigured(
context,
"Facebook provider unconfigured. Make sure to " +
"add a `facebook_application_id` string to your strings.xml",
R.string.facebook_application_id
)

Preconditions.checkConfigured(
context,
"Facebook provider unconfigured. Make sure to " +
"add a `facebook_login_protocol_scheme` string to your strings.xml",
R.string.facebook_login_protocol_scheme
)

Preconditions.checkConfigured(
context,
"Facebook provider unconfigured. Make sure to " +
"add a `facebook_client_token` string to your strings.xml",
R.string.facebook_client_token
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,48 @@ internal suspend fun FirebaseAuthUI.signInWithGoogle(
}
}

val result =
// Try with configured filterByAuthorizedAccounts setting
// If default (true), fallback to false if no authorized accounts found
// See: https://developer.android.com/identity/sign-in/credential-manager-siwg#siwg-button
val result = if (provider.filterByAuthorizedAccounts) {
// Default behavior: Try authorized accounts first, fallback to all accounts
try {
(testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential(
context = context,
credentialManager = CredentialManager.create(context),
serverClientId = provider.serverClientId!!,
filterByAuthorizedAccounts = true,
autoSelectEnabled = provider.autoSelectEnabled
)
} catch (e: NoCredentialException) {
// No authorized accounts found, try again with all accounts for sign-up flow
Log.d("GoogleAuthProvider", "No authorized accounts found, showing all Google accounts for sign-up")
try {
(testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential(
context = context,
credentialManager = CredentialManager.create(context),
serverClientId = provider.serverClientId!!,
filterByAuthorizedAccounts = false,
autoSelectEnabled = provider.autoSelectEnabled
)
} catch (fallbackException: NoCredentialException) {
// No Google accounts available on device at all
throw AuthException.UnknownException(
message = "No Google accounts available.\n\nPlease add a Google account to your device and try again.",
cause = fallbackException
)
}
}
} else {
// Developer explicitly wants to show all accounts (no fallback needed)
(testCredentialManagerProvider ?: credentialManagerProvider).getGoogleCredential(
context = context,
credentialManager = CredentialManager.create(context),
serverClientId = provider.serverClientId!!,
filterByAuthorizedAccounts = true,
autoSelectEnabled = false
filterByAuthorizedAccounts = false,
autoSelectEnabled = provider.autoSelectEnabled
)
}
idTokenFromResult = result.idToken

signInAndLinkWithCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,7 @@ interface AuthUIStringProvider {

/** Tooltip message shown when new account sign-up is disabled */
val newAccountsDisabledTooltip: String

/** Tooltip message shown when MFA is disabled */
val mfaDisabledTooltip: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,7 @@ class DefaultAuthUIStringProvider(

override val newAccountsDisabledTooltip: String
get() = localizedContext.getString(R.string.fui_new_accounts_disabled_tooltip)

override val mfaDisabledTooltip: String
get() = localizedContext.getString(R.string.fui_mfa_disabled_tooltip)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
Expand Down Expand Up @@ -348,6 +354,7 @@ fun FirebaseAuthScreen(
AuthSuccessUiContext(
authUI = authUI,
stringProvider = stringProvider,
configuration = configuration,
onSignOut = {
coroutineScope.launch {
try {
Expand All @@ -362,7 +369,15 @@ fun FirebaseAuthScreen(
}
},
onManageMfa = {
navController.navigate(AuthRoute.MfaEnrollment.route)
if (configuration.isMfaEnabled) {
navController.navigate(AuthRoute.MfaEnrollment.route)
} else {
val exception = AuthException.AuthCancelledException(
message = "Multi-factor authentication is disabled in the configuration. " +
"Enable MFA in AuthUIConfiguration to use this feature."
)
authUI.updateAuthState(AuthState.Error(exception))
}
},
onReloadUser = {
coroutineScope.launch {
Expand Down Expand Up @@ -410,6 +425,7 @@ fun FirebaseAuthScreen(
SuccessDestination(
authState = authState,
stringProvider = stringProvider,
configuration = configuration,
uiContext = uiContext
)
}
Expand Down Expand Up @@ -654,6 +670,7 @@ sealed class AuthRoute(val route: String) {
data class AuthSuccessUiContext(
val authUI: FirebaseAuthUI,
val stringProvider: AuthUIStringProvider,
val configuration: AuthUIConfiguration,
val onSignOut: () -> Unit,
val onManageMfa: () -> Unit,
val onReloadUser: () -> Unit,
Expand All @@ -664,13 +681,15 @@ data class AuthSuccessUiContext(
private fun SuccessDestination(
authState: AuthState,
stringProvider: AuthUIStringProvider,
configuration: AuthUIConfiguration,
uiContext: AuthSuccessUiContext,
) {
when (authState) {
is AuthState.Success -> {
AuthSuccessContent(
authUI = uiContext.authUI,
stringProvider = stringProvider,
configuration = configuration,
onSignOut = uiContext.onSignOut,
onManageMfa = uiContext.onManageMfa
)
Expand Down Expand Up @@ -704,10 +723,12 @@ private fun SuccessDestination(
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AuthSuccessContent(
authUI: FirebaseAuthUI,
stringProvider: AuthUIStringProvider,
configuration: AuthUIConfiguration,
onSignOut: () -> Unit,
onManageMfa: () -> Unit,
) {
Expand All @@ -726,8 +747,25 @@ private fun AuthSuccessContent(
Spacer(modifier = Modifier.height(16.dp))
}
if (user != null && authUI.auth.app.options.projectId != null) {
Button(onClick = onManageMfa) {
Text(stringProvider.manageMfaAction)
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Above
),
tooltip = {
PlainTooltip {
Text(stringProvider.mfaDisabledTooltip)
}
},
state = rememberTooltipState(
initialIsVisible = !configuration.isMfaEnabled
)
) {
Button(
onClick = onManageMfa,
enabled = configuration.isMfaEnabled
) {
Text(stringProvider.manageMfaAction)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
Expand Down
1 change: 1 addition & 0 deletions auth/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,5 @@

<!-- Tooltips -->
<string name="fui_new_accounts_disabled_tooltip" translation_description="Tooltip shown when sign-up button is disabled because new accounts are not allowed">This button is currently disabled because new accounts are not allowed</string>
<string name="fui_mfa_disabled_tooltip" translation_description="Tooltip shown when manage MFA button is disabled because MFA is not enabled">المصادقة متعددة العوامل معطلة حاليًا</string>
</resources>
1 change: 1 addition & 0 deletions auth/src/main/res/values-b+es+419/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,5 @@

<!-- Tooltips -->
<string name="fui_new_accounts_disabled_tooltip" translation_description="Tooltip shown when sign-up button is disabled because new accounts are not allowed">This button is currently disabled because new accounts are not allowed</string>
<string name="fui_mfa_disabled_tooltip" translation_description="Tooltip shown when manage MFA button is disabled because MFA is not enabled">Multi-factor authentication is currently disabled</string>
</resources>
Loading