Skip to content
Open
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
@@ -1,7 +1,6 @@
package com.firebase.ui.auth.configuration.auth_provider

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.AuthState
Expand All @@ -14,27 +13,32 @@ import kotlinx.coroutines.tasks.await
/**
* Creates a remembered launcher function for anonymous sign-in.
*
* @param config Authentication UI configuration
* @param onSignInFailure Callback invoked with the resulting [AuthException] on failure
* @return A launcher function that starts the anonymous sign-in flow when invoked
*
* @see signInAnonymously
* @see createOrLinkUserWithEmailAndPassword for upgrading anonymous accounts
*/
@Composable
internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(config: AuthUIConfiguration): () -> Unit {
internal fun FirebaseAuthUI.rememberAnonymousSignInHandler(
config: AuthUIConfiguration,
onSignInFailure: (AuthException) -> Unit = {},
): () -> Unit {
Comment thread
demolaf marked this conversation as resolved.
val context = androidx.compose.ui.platform.LocalContext.current
val coroutineScope = rememberCoroutineScope()
return remember(this) {
{
coroutineScope.launch {
try {
signInAnonymously(config)
} catch (e: AuthException) {
// Already an AuthException, don't re-wrap it
updateAuthState(AuthState.Error(e))
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
}
return {
coroutineScope.launch {
try {
signInAnonymously(config)
} catch (e: AuthException) {
// Already an AuthException, don't re-wrap it
updateAuthState(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import com.facebook.AccessToken
import com.facebook.CallbackManager
import com.facebook.FacebookCallback
Expand All @@ -47,6 +49,7 @@ import kotlinx.coroutines.launch
* @param config The [AuthUIConfiguration] containing authentication settings
* @param provider The [AuthProvider.Facebook] configuration with scopes and credential provider
* @param loginManagerProvider Provides logout operations to clear stale Facebook sessions
* @param onSignInFailure Callback invoked with the resulting [AuthException] on failure
*
* @return A launcher function that starts the Facebook sign-in flow when invoked
*
Expand All @@ -58,10 +61,15 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
config: AuthUIConfiguration,
provider: AuthProvider.Facebook,
loginManagerProvider: AuthProvider.Facebook.LoginManagerProvider = AuthProvider.Facebook.DefaultLoginManagerProvider(),
onSignInFailure: (AuthException) -> Unit = {},
Comment thread
demolaf marked this conversation as resolved.
): () -> Unit {
val coroutineScope = rememberCoroutineScope()
val callbackManager = remember { CallbackManager.Factory.create() }
val loginManager = LoginManager.getInstance()
val currentContext by rememberUpdatedState(context)
val currentConfig by rememberUpdatedState(config)
val currentProvider by rememberUpdatedState(provider)
val currentOnSignInFailure by rememberUpdatedState(onSignInFailure)

val launcher = rememberLauncherForActivityResult(
loginManager.createLogInActivityResultContract(
Expand All @@ -71,25 +79,27 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(
onResult = {},
)

DisposableEffect(config) {
DisposableEffect(Unit) {
loginManager.registerCallback(
callbackManager,
object : FacebookCallback<LoginResult> {
override fun onSuccess(result: LoginResult) {
coroutineScope.launch {
try {
signInWithFacebook(
context = context,
config = config,
provider = provider,
context = currentContext,
config = currentConfig,
provider = currentProvider,
accessToken = result.accessToken,
)
} catch (e: AuthException) {
// Already an AuthException, don't re-wrap it
updateAuthState(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) currentOnSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, currentContext)
updateAuthState(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) currentOnSignInFailure(authException)
}
}
}
Expand All @@ -100,12 +110,13 @@ internal fun FirebaseAuthUI.rememberSignInWithFacebookLauncher(

override fun onError(error: FacebookException) {
Log.e("FacebookAuthProvider", "Error during Facebook sign in", error)
val authException = AuthException.from(error, context)
val authException = AuthException.from(error, currentContext)
updateAuthState(
AuthState.Error(
authException
)
)
currentOnSignInFailure(authException)
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.firebase.ui.auth.configuration.auth_provider
import android.content.Context
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.credentials.CredentialManager
import androidx.credentials.exceptions.GetCredentialException
Expand Down Expand Up @@ -47,6 +46,7 @@ import kotlinx.coroutines.launch
* @param context Android context for Credential Manager
* @param config Authentication UI configuration
* @param provider Google provider configuration with server client ID and optional scopes
* @param onSignInFailure Callback invoked with the resulting [AuthException] on failure
* @return A callback function that initiates Google Sign-In when invoked
*
* @see signInWithGoogle
Expand All @@ -57,19 +57,20 @@ internal fun FirebaseAuthUI.rememberGoogleSignInHandler(
context: Context,
config: AuthUIConfiguration,
provider: AuthProvider.Google,
onSignInFailure: (AuthException) -> Unit = {},
Comment thread
demolaf marked this conversation as resolved.
): () -> Unit {
val coroutineScope = rememberCoroutineScope()
return remember(this, config) {
{
coroutineScope.launch {
try {
signInWithGoogle(context, config, provider)
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
}
return {
coroutineScope.launch {
try {
signInWithGoogle(context, config, provider)
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.firebase.ui.auth.configuration.auth_provider
import android.app.Activity
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.AuthState
Expand Down Expand Up @@ -41,6 +40,7 @@ import kotlinx.coroutines.tasks.await
*
* @param config Authentication UI configuration
* @param provider OAuth provider configuration
* @param onSignInFailure Callback invoked with the resulting [AuthException] on failure
*
* @return Lambda that triggers OAuth sign-in when invoked
*
Expand All @@ -54,29 +54,30 @@ internal fun FirebaseAuthUI.rememberOAuthSignInHandler(
activity: Activity?,
config: AuthUIConfiguration,
provider: AuthProvider.OAuth,
onSignInFailure: (AuthException) -> Unit = {},
Comment thread
demolaf marked this conversation as resolved.
): () -> Unit {
val coroutineScope = rememberCoroutineScope()
activity ?: throw IllegalStateException(
"OAuth sign-in requires an Activity. " +
"Ensure FirebaseAuthScreen is used within an Activity."
)

return remember(this, provider.providerId, config) {
{
coroutineScope.launch {
try {
signInWithProvider(
context = context,
config = config,
activity = activity,
provider = provider
)
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
}
return {
coroutineScope.launch {
try {
signInWithProvider(
context = context,
config = config,
activity = activity,
provider = provider
)
} catch (e: AuthException) {
updateAuthState(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
updateAuthState(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ fun FirebaseAuthScreen(
)
)
},
onSignInFailure = onSignInFailure,
)
val continueWithProvider: (String) -> Unit = { providerId ->
configuration.providers.find { it.providerId == providerId }?.let { onProviderSelected(it) }
Expand Down Expand Up @@ -1012,6 +1013,7 @@ private fun FirebaseAuthUI.rememberOnProviderSelected(
config: AuthUIConfiguration,
onNavigate: (AuthRoute) -> Unit,
onUnknownProvider: ((AuthProvider) -> Unit)? = null,
onSignInFailure: (AuthException) -> Unit = {},
): (AuthProvider) -> Unit {
val anonymousProvider = config.providers.filterIsInstance<AuthProvider.Anonymous>().firstOrNull()
val googleProvider = config.providers.filterIsInstance<AuthProvider.Google>().firstOrNull()
Expand All @@ -1023,16 +1025,18 @@ private fun FirebaseAuthUI.rememberOnProviderSelected(
val twitterProvider = config.providers.filterIsInstance<AuthProvider.Twitter>().firstOrNull()
val genericOAuthProviders = config.providers.filterIsInstance<AuthProvider.GenericOAuth>()

val onSignInAnonymously = anonymousProvider?.let { rememberAnonymousSignInHandler(config) }
val onSignInWithGoogle = googleProvider?.let { rememberGoogleSignInHandler(context, config, it) }
val onSignInWithFacebook = facebookProvider?.let { rememberSignInWithFacebookLauncher(context, config, it) }
val onSignInWithApple = appleProvider?.let { rememberOAuthSignInHandler(context, activity, config, it) }
val onSignInWithGithub = githubProvider?.let { rememberOAuthSignInHandler(context, activity, config, it) }
val onSignInWithMicrosoft = microsoftProvider?.let { rememberOAuthSignInHandler(context, activity, config, it) }
val onSignInWithYahoo = yahooProvider?.let { rememberOAuthSignInHandler(context, activity, config, it) }
val onSignInWithTwitter = twitterProvider?.let { rememberOAuthSignInHandler(context, activity, config, it) }
val onSignInAnonymously = anonymousProvider?.let { rememberAnonymousSignInHandler(config, onSignInFailure) }
val onSignInWithGoogle = googleProvider?.let { rememberGoogleSignInHandler(context, config, it, onSignInFailure) }
val onSignInWithFacebook = facebookProvider?.let {
rememberSignInWithFacebookLauncher(context, config, it, onSignInFailure = onSignInFailure)
}
val onSignInWithApple = appleProvider?.let { rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure) }
val onSignInWithGithub = githubProvider?.let { rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure) }
val onSignInWithMicrosoft = microsoftProvider?.let { rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure) }
val onSignInWithYahoo = yahooProvider?.let { rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure) }
val onSignInWithTwitter = twitterProvider?.let { rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure) }
val genericOAuthHandlers = genericOAuthProviders.associateWith {
rememberOAuthSignInHandler(context, activity, config, it)
rememberOAuthSignInHandler(context, activity, config, it, onSignInFailure)
}

return { provider ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.firebase.ui.auth.configuration.auth_provider

import android.content.Context
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.AuthState
Expand All @@ -38,6 +39,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
Expand All @@ -54,6 +56,9 @@ import org.robolectric.annotation.Config
@Config(manifest = Config.NONE)
class AnonymousAuthProviderFirebaseAuthUITest {

@get:Rule
val composeTestRule = createComposeRule()

@Mock
private lateinit var mockFirebaseAuth: FirebaseAuth

Expand Down Expand Up @@ -214,6 +219,35 @@ class AnonymousAuthProviderFirebaseAuthUITest {
assertThat(errorState.exception).isInstanceOf(AuthException.UnknownException::class.java)
}

// =============================================================================================
// rememberAnonymousSignInHandler - onSignInFailure reporting
// =============================================================================================

@Test
fun `rememberAnonymousSignInHandler reports failure via onSignInFailure immediately, at the source`() {
val networkException = FirebaseNetworkException("Network error")
val taskCompletionSource = TaskCompletionSource<AuthResult>()
taskCompletionSource.setException(networkException)
`when`(mockFirebaseAuth.signInAnonymously()).thenReturn(taskCompletionSource.task)

val instance = FirebaseAuthUI.create(firebaseApp, mockFirebaseAuth)
val reportedFailures = mutableListOf<AuthException>()
var launcher: (() -> Unit)? = null

composeTestRule.setContent {
launcher = instance.rememberAnonymousSignInHandler(
config = config,
onSignInFailure = { reportedFailures.add(it) },
)
}

composeTestRule.runOnIdle { launcher?.invoke() }
composeTestRule.waitForIdle()

assertThat(reportedFailures).hasSize(1)
assertThat(reportedFailures.single()).isInstanceOf(AuthException.NetworkException::class.java)
}

// =============================================================================================
// Anonymous Account Upgrade Tests
// =============================================================================================
Expand Down
Loading