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
20 changes: 20 additions & 0 deletions auth/src/main/java/com/firebase/ui/auth/AuthException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -379,13 +379,33 @@ abstract class AuthException(
* }
* ```
*
* Messages are resolved against [context]'s own configuration, so this overload honours
* neither a custom [AuthUIStringProvider] nor the `locale` a host configured. Prefer the
* [AuthUIStringProvider] overload wherever one is reachable, which inside an auth flow it
* always is, as `config.stringProvider`. This overload exists for the entry points that
* genuinely have no configuration to draw on, such as [FirebaseAuthUI.signOut],
* [FirebaseAuthUI.withReauth] and [FirebaseAuthUI.delete].
*
* @param firebaseException The Firebase exception to convert
* @param context Used to build a [DefaultAuthUIStringProvider] for the error messages
* @return An appropriate [AuthException] subtype
*/
@JvmStatic
fun from(firebaseException: Exception, context: Context): AuthException =
from(firebaseException, DefaultAuthUIStringProvider(context))

/**
* Creates an [AuthException] from [firebaseException], taking message text from
* [stringProvider] so it honours the host's configured strings and locale.
*
* This is the preferred overload; see the [Context] one above for the exception mapping
* table and an example. A `null` [stringProvider], or one whose resource for a given error
* is blank, falls back to the Firebase SDK's own message.
*
* @param firebaseException The Firebase exception to convert
* @param stringProvider Supplies localized message text; pass `config.stringProvider`
* @return An appropriate [AuthException] subtype
*/
@JvmStatic
@JvmOverloads
fun from(firebaseException: Exception, stringProvider: AuthUIStringProvider? = null): AuthException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import kotlinx.coroutines.tasks.await
internal fun AuthFlowScope.rememberAnonymousSignInHandler(
onSignInFailure: (AuthException) -> Unit = {},
): () -> Unit {
val context = androidx.compose.ui.platform.LocalContext.current
val coroutineScope = rememberCoroutineScope()
return {
coroutineScope.launch {
Expand All @@ -33,7 +32,7 @@ internal fun AuthFlowScope.rememberAnonymousSignInHandler(
emit(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
Expand Down Expand Up @@ -63,7 +62,7 @@ internal suspend fun AuthFlowScope.signInAnonymously() {
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ internal suspend fun AuthFlowScope.createOrLinkUserWithEmailAndPassword(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -351,7 +351,7 @@ internal suspend fun AuthFlowScope.signInWithEmailAndPassword(
throw e
} catch (e: Exception) {
val authException = recoverLegacyDifferentSignInMethod(email, e)
?: AuthException.from(e, context)
?: AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand All @@ -365,7 +365,7 @@ private suspend fun AuthFlowScope.recoverLegacyDifferentSignInMethod(
return null
}

val authException = AuthException.from(cause)
val authException = AuthException.from(cause, config.stringProvider)
if (authException !is AuthException.InvalidCredentialsException &&
authException !is AuthException.UserNotFoundException) {
return null
Expand Down Expand Up @@ -500,7 +500,7 @@ internal suspend fun AuthFlowScope.signInAndLinkWithCredential(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -565,7 +565,7 @@ internal suspend fun AuthFlowScope.sendSignInLinkToEmail(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -702,7 +702,7 @@ internal suspend fun AuthFlowScope.signInWithEmailLink(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -824,7 +824,7 @@ internal suspend fun AuthFlowScope.sendPasswordResetEmail(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ internal fun AuthFlowScope.rememberSignInWithFacebookLauncher(
currentScope.emit(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) currentOnSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, currentContext)
val authException = AuthException.from(e, currentScope.config.stringProvider)
currentScope.emit(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) currentOnSignInFailure(authException)
}
Expand All @@ -108,7 +108,7 @@ internal fun AuthFlowScope.rememberSignInWithFacebookLauncher(

override fun onError(error: FacebookException) {
Log.e("FacebookAuthProvider", "Error during Facebook sign in", error)
val authException = AuthException.from(error, currentContext)
val authException = AuthException.from(error, currentScope.config.stringProvider)
currentScope.emit(
AuthState.Error(
authException
Expand Down Expand Up @@ -203,7 +203,7 @@ internal suspend fun AuthFlowScope.signInWithFacebook(
emit(AuthState.Error(e))
throw e
} catch (e: FacebookException) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
} catch (e: CancellationException) {
Expand All @@ -217,7 +217,7 @@ internal suspend fun AuthFlowScope.signInWithFacebook(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal fun AuthFlowScope.rememberGoogleSignInHandler(
emit(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
Expand Down Expand Up @@ -73,7 +73,7 @@ internal suspend fun AuthFlowScope.signInWithGoogle(
authorizationProvider.authorize(context, requestedScopes)
} catch (e: Exception) {
// Continue with sign-in even if scope authorization fails
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
}
}
Expand Down Expand Up @@ -193,7 +193,7 @@ internal suspend fun AuthFlowScope.signInWithGoogle(
throw e

} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal fun AuthFlowScope.rememberOAuthSignInHandler(
emit(AuthState.Error(e))
if (e !is AuthException.AuthCancelledException) onSignInFailure(e)
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
if (authException !is AuthException.AuthCancelledException) onSignInFailure(authException)
}
Expand Down Expand Up @@ -191,7 +191,7 @@ internal suspend fun AuthFlowScope.signInWithProvider(
throw e

} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ internal suspend fun AuthFlowScope.verifyPhoneNumber(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -107,7 +107,7 @@ internal suspend fun AuthFlowScope.submitVerificationCode(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down Expand Up @@ -161,7 +161,7 @@ internal suspend fun AuthFlowScope.signInWithPhoneAuthCredential(
emit(AuthState.Error(e))
throw e
} catch (e: Exception) {
val authException = AuthException.from(e, context)
val authException = AuthException.from(e, config.stringProvider)
emit(AuthState.Error(authException))
throw authException
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import com.firebase.ui.auth.AuthException
import com.firebase.ui.auth.AuthState
import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider
import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider

/**
* CompositionLocal for accessing the top-level dialog controller from any composable.
Expand All @@ -40,7 +42,7 @@ val LocalTopLevelDialogController = compositionLocalOf<TopLevelDialogController?
* **Usage:**
* ```kotlin
* // At the root of your auth flow (FirebaseAuthScreen):
* val dialogController = rememberTopLevelDialogController(stringProvider) { authState }
* val dialogController = rememberTopLevelDialogController { authState }
*
* CompositionLocalProvider(LocalTopLevelDialogController provides dialogController) {
* // Your auth screens...
Expand All @@ -64,12 +66,32 @@ val LocalTopLevelDialogController = compositionLocalOf<TopLevelDialogController?
* }
* ```
*
* [CurrentDialog] resolves its strings from [LocalAuthUIStringProvider] at render time, so call
* it inside the `CompositionLocalProvider` that supplies that local.
*
* @since 10.0.0
*/
class TopLevelDialogController(
private val stringProvider: AuthUIStringProvider,
private val currentAuthState: () -> AuthState
) {
/**
* Only ever set by the deprecated constructor below. A caller that passed a provider without
* also providing [LocalAuthUIStringProvider] still works, instead of trading a compile-time
* argument for a runtime `error("No AuthUIStringProvider provided")`.
*/
private var explicitStringProvider: AuthUIStringProvider? = null

@Deprecated(
"The string provider is now read from LocalAuthUIStringProvider at render time.",
ReplaceWith("TopLevelDialogController(currentAuthState)")
)
constructor(
stringProvider: AuthUIStringProvider,
currentAuthState: () -> AuthState
) : this(currentAuthState) {
explicitStringProvider = stringProvider
}

private var dialogState by mutableStateOf<DialogState?>(null)
private val shownErrorStates = mutableSetOf<AuthState.Error>()

Expand Down Expand Up @@ -125,11 +147,14 @@ class TopLevelDialogController(
/**
* Composable that renders the current dialog, if any.
* This should be called once at the root level of your auth flow.
*
* Uses the existing [ErrorRecoveryDialog] component.
*
* Uses the existing [ErrorRecoveryDialog] component. Strings come from
* [LocalAuthUIStringProvider], read here at render time, unless the controller was built
* through the deprecated constructor that takes one explicitly.
*/
@Composable
fun CurrentDialog() {
val stringProvider = explicitStringProvider ?: LocalAuthUIStringProvider.current
val state = dialogState
when (state) {
is DialogState.ErrorDialog -> {
Expand Down Expand Up @@ -174,16 +199,40 @@ class TopLevelDialogController(
* live auth state on every [TopLevelDialogController.showErrorDialog] call without being
* recreated (and losing its de-duplication history) whenever the auth state changes.
*
* Keyed on [stringProvider] rather than left unkeyed: callers must pass a `remember`ed
* [stringProvider] (stable across recompositions), otherwise the controller — and its
* de-duplication history — would be recreated on every recomposition.
* The `remember` is deliberately unkeyed, so any key would be a way to lose a dialog that was
* just shown. Nothing kept across recompositions goes stale as a result: strings are resolved
* from [LocalAuthUIStringProvider] at render time, and [authState] is read through
* [rememberUpdatedState] rather than captured, so the first composition's lambda is not pinned
* for the controller's life.
*/
@Composable
fun rememberTopLevelDialogController(
authState: () -> AuthState
): TopLevelDialogController {
val currentAuthState by rememberUpdatedState(authState)
return remember {
TopLevelDialogController { currentAuthState() }
}
}
Comment thread
demolaf marked this conversation as resolved.

/**
* Creates and remembers a [TopLevelDialogController] bound to an explicit [stringProvider].
*
* Kept only for source compatibility. It still keys the `remember` on [stringProvider], so a
* caller whose provider is not stable across recompositions loses the controller's state — that
* is the reason to move to the single-argument overload above.
*/
@Deprecated(
"The string provider is now read from LocalAuthUIStringProvider at render time.",
ReplaceWith("rememberTopLevelDialogController(authState)")
)
@Composable
fun rememberTopLevelDialogController(
stringProvider: AuthUIStringProvider,
authState: () -> AuthState
): TopLevelDialogController {
return remember(stringProvider) {
@Suppress("DEPRECATION")
TopLevelDialogController(stringProvider, authState)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ import com.firebase.ui.auth.configuration.auth_provider.rememberOAuthSignInHandl
import com.firebase.ui.auth.configuration.auth_provider.rememberSignInWithFacebookLauncher
import com.firebase.ui.auth.configuration.auth_provider.signInWithEmailLink
import com.firebase.ui.auth.configuration.string_provider.AuthUIStringProvider
import com.firebase.ui.auth.configuration.string_provider.DefaultAuthUIStringProvider
import com.firebase.ui.auth.configuration.string_provider.LocalAuthUIStringProvider
import com.firebase.ui.auth.configuration.theme.LocalAuthUITheme
import com.firebase.ui.auth.ui.components.LocalTopLevelDialogController
Expand Down Expand Up @@ -181,7 +180,9 @@ fun FirebaseAuthScreen(
val activity = LocalActivity.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val stringProvider = remember(context) { DefaultAuthUIStringProvider(context) }
// The host's provider, not one built from LocalContext: only this honours a custom
// AuthUIStringProvider and the configured locale.
val stringProvider = configuration.stringProvider

// The reauth effects below run outside composition, so they cannot call stringResource
// themselves.
Expand All @@ -202,7 +203,7 @@ fun FirebaseAuthScreen(
hostAuthFlowScope(authUI, configuration, hostStateHolder)
}
val authState = rawAuthState
val dialogController = rememberTopLevelDialogController(stringProvider) { authState }
val dialogController = rememberTopLevelDialogController { authState }
val lastSuccessfulUserId = remember { mutableStateOf<String?>(null) }
val pendingLinkingCredential = remember { mutableStateOf<AuthCredential?>(null) }
val pendingResolver = remember { mutableStateOf<MultiFactorResolver?>(null) }
Expand Down
Loading