Skip to content

Commit 3d08a55

Browse files
committed
feat: Error Recovery & Dialog
1 parent f1dfd0c commit 3d08a55

File tree

89 files changed

+1084
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1084
-42
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright 2025 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.firebase.ui.auth.compose
16+
17+
import androidx.compose.material3.AlertDialog
18+
import androidx.compose.material3.MaterialTheme
19+
import androidx.compose.material3.Text
20+
import androidx.compose.material3.TextButton
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.text.style.TextAlign
24+
import androidx.compose.ui.window.DialogProperties
25+
import com.firebase.ui.auth.compose.configuration.stringprovider.AuthUIStringProvider
26+
27+
/**
28+
* A composable dialog for displaying authentication errors with recovery options.
29+
*
30+
* This dialog provides friendly error messages and actionable recovery suggestions
31+
* based on the specific [AuthException] type. It integrates with [AuthUIStringProvider]
32+
* for localization support.
33+
*
34+
* **Example usage:**
35+
* ```kotlin
36+
* var showError by remember { mutableStateOf<AuthException?>(null) }
37+
*
38+
* if (showError != null) {
39+
* ErrorRecoveryDialog(
40+
* error = showError!!,
41+
* stringProvider = stringProvider,
42+
* onRetry = {
43+
* showError = null
44+
* // Retry authentication operation
45+
* },
46+
* onDismiss = {
47+
* showError = null
48+
* }
49+
* )
50+
* }
51+
* ```
52+
*
53+
* @param error The [AuthException] to display recovery information for
54+
* @param stringProvider The [AuthUIStringProvider] for localized strings
55+
* @param onRetry Callback invoked when the user taps the retry action
56+
* @param onDismiss Callback invoked when the user dismisses the dialog
57+
* @param onRecover Optional callback for custom recovery actions based on the exception type
58+
* @param modifier Optional [Modifier] for the dialog
59+
* @param properties Optional [DialogProperties] for dialog configuration
60+
*
61+
* @since 10.0.0
62+
*/
63+
@Composable
64+
fun ErrorRecoveryDialog(
65+
error: AuthException,
66+
stringProvider: AuthUIStringProvider,
67+
onRetry: () -> Unit,
68+
onDismiss: () -> Unit,
69+
onRecover: ((AuthException) -> Unit)? = null,
70+
modifier: Modifier = Modifier,
71+
properties: DialogProperties = DialogProperties()
72+
) {
73+
AlertDialog(
74+
onDismissRequest = onDismiss,
75+
title = {
76+
Text(
77+
text = stringProvider.errorDialogTitle,
78+
style = MaterialTheme.typography.headlineSmall
79+
)
80+
},
81+
text = {
82+
Text(
83+
text = getRecoveryMessage(error, stringProvider),
84+
style = MaterialTheme.typography.bodyMedium,
85+
textAlign = TextAlign.Start
86+
)
87+
},
88+
confirmButton = {
89+
if (isRecoverable(error)) {
90+
TextButton(
91+
onClick = {
92+
onRecover?.invoke(error) ?: onRetry()
93+
}
94+
) {
95+
Text(
96+
text = getRecoveryActionText(error, stringProvider),
97+
style = MaterialTheme.typography.labelLarge
98+
)
99+
}
100+
}
101+
},
102+
dismissButton = {
103+
TextButton(onClick = onDismiss) {
104+
Text(
105+
text = stringProvider.dismissAction,
106+
style = MaterialTheme.typography.labelLarge
107+
)
108+
}
109+
},
110+
modifier = modifier,
111+
properties = properties
112+
)
113+
}
114+
115+
/**
116+
* Gets the appropriate recovery message for the given [AuthException].
117+
*
118+
* @param error The [AuthException] to get the message for
119+
* @param stringProvider The [AuthUIStringProvider] for localized strings
120+
* @return The localized recovery message
121+
*/
122+
private fun getRecoveryMessage(
123+
error: AuthException,
124+
stringProvider: AuthUIStringProvider
125+
): String {
126+
return when (error) {
127+
is AuthException.NetworkException -> stringProvider.networkErrorRecoveryMessage
128+
is AuthException.InvalidCredentialsException -> stringProvider.invalidCredentialsRecoveryMessage
129+
is AuthException.UserNotFoundException -> stringProvider.userNotFoundRecoveryMessage
130+
is AuthException.WeakPasswordException -> {
131+
// Include specific reason if available
132+
val baseMessage = stringProvider.weakPasswordRecoveryMessage
133+
error.reason?.let { reason ->
134+
"$baseMessage\n\nReason: $reason"
135+
} ?: baseMessage
136+
}
137+
is AuthException.EmailAlreadyInUseException -> {
138+
// Include email if available
139+
val baseMessage = stringProvider.emailAlreadyInUseRecoveryMessage
140+
error.email?.let { email ->
141+
"$baseMessage ($email)"
142+
} ?: baseMessage
143+
}
144+
is AuthException.TooManyRequestsException -> stringProvider.tooManyRequestsRecoveryMessage
145+
is AuthException.MfaRequiredException -> stringProvider.mfaRequiredRecoveryMessage
146+
is AuthException.AccountLinkingRequiredException -> stringProvider.accountLinkingRequiredRecoveryMessage
147+
is AuthException.AuthCancelledException -> stringProvider.authCancelledRecoveryMessage
148+
is AuthException.UnknownException -> stringProvider.unknownErrorRecoveryMessage
149+
else -> stringProvider.unknownErrorRecoveryMessage
150+
}
151+
}
152+
153+
/**
154+
* Gets the appropriate recovery action text for the given [AuthException].
155+
*
156+
* @param error The [AuthException] to get the action text for
157+
* @param stringProvider The [AuthUIStringProvider] for localized strings
158+
* @return The localized action text
159+
*/
160+
private fun getRecoveryActionText(
161+
error: AuthException,
162+
stringProvider: AuthUIStringProvider
163+
): String {
164+
return when (error) {
165+
is AuthException.AuthCancelledException -> stringProvider.continueText
166+
is AuthException.EmailAlreadyInUseException -> stringProvider.signInDefault // Use existing "Sign in" text
167+
is AuthException.AccountLinkingRequiredException -> stringProvider.continueText // Use "Continue" for linking
168+
is AuthException.MfaRequiredException -> stringProvider.continueText // Use "Continue" for MFA
169+
is AuthException.NetworkException,
170+
is AuthException.InvalidCredentialsException,
171+
is AuthException.UserNotFoundException,
172+
is AuthException.WeakPasswordException,
173+
is AuthException.TooManyRequestsException,
174+
is AuthException.UnknownException -> stringProvider.retryAction
175+
else -> stringProvider.retryAction
176+
}
177+
}
178+
179+
/**
180+
* Determines if the given [AuthException] is recoverable through user action.
181+
*
182+
* @param error The [AuthException] to check
183+
* @return `true` if the error is recoverable, `false` otherwise
184+
*/
185+
private fun isRecoverable(error: AuthException): Boolean {
186+
return when (error) {
187+
is AuthException.NetworkException -> true
188+
is AuthException.InvalidCredentialsException -> true
189+
is AuthException.UserNotFoundException -> true
190+
is AuthException.WeakPasswordException -> true
191+
is AuthException.EmailAlreadyInUseException -> true
192+
is AuthException.TooManyRequestsException -> false // User must wait
193+
is AuthException.MfaRequiredException -> true
194+
is AuthException.AccountLinkingRequiredException -> true
195+
is AuthException.AuthCancelledException -> true
196+
is AuthException.UnknownException -> true
197+
else -> true
198+
}
199+
}

auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/AuthUIStringProvider.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,44 @@ interface AuthUIStringProvider {
184184

185185
/** TOTP Code prompt */
186186
val enterTOTPCode: String
187+
188+
// Error Recovery Dialog Strings
189+
/** Error dialog title */
190+
val errorDialogTitle: String
191+
192+
/** Retry action button text */
193+
val retryAction: String
194+
195+
/** Dismiss action button text */
196+
val dismissAction: String
197+
198+
/** Network error recovery message */
199+
val networkErrorRecoveryMessage: String
200+
201+
/** Invalid credentials recovery message */
202+
val invalidCredentialsRecoveryMessage: String
203+
204+
/** User not found recovery message */
205+
val userNotFoundRecoveryMessage: String
206+
207+
/** Weak password recovery message */
208+
val weakPasswordRecoveryMessage: String
209+
210+
/** Email already in use recovery message */
211+
val emailAlreadyInUseRecoveryMessage: String
212+
213+
/** Too many requests recovery message */
214+
val tooManyRequestsRecoveryMessage: String
215+
216+
/** MFA required recovery message */
217+
val mfaRequiredRecoveryMessage: String
218+
219+
/** Account linking required recovery message */
220+
val accountLinkingRequiredRecoveryMessage: String
221+
222+
/** Auth cancelled recovery message */
223+
val authCancelledRecoveryMessage: String
224+
225+
/** Unknown error recovery message */
226+
val unknownErrorRecoveryMessage: String
187227
}

auth/src/main/java/com/firebase/ui/auth/compose/configuration/stringprovider/DefaultAuthUIStringProvider.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,34 @@ class DefaultAuthUIStringProvider(
179179
get() = localizedContext.getString(R.string.fui_progress_dialog_loading)
180180
override val noInternet: String
181181
get() = localizedContext.getString(R.string.fui_no_internet)
182+
183+
/**
184+
* Error Recovery Dialog Strings
185+
*/
186+
override val errorDialogTitle: String
187+
get() = localizedContext.getString(R.string.fui_error_dialog_title)
188+
override val retryAction: String
189+
get() = localizedContext.getString(R.string.fui_error_retry_action)
190+
override val dismissAction: String
191+
get() = localizedContext.getString(R.string.fui_email_link_dismiss_button)
192+
override val networkErrorRecoveryMessage: String
193+
get() = localizedContext.getString(R.string.fui_no_internet)
194+
override val invalidCredentialsRecoveryMessage: String
195+
get() = localizedContext.getString(R.string.fui_error_invalid_password)
196+
override val userNotFoundRecoveryMessage: String
197+
get() = localizedContext.getString(R.string.fui_error_email_does_not_exist)
198+
override val weakPasswordRecoveryMessage: String
199+
get() = localizedContext.resources.getQuantityString(R.plurals.fui_error_weak_password, 6, 6)
200+
override val emailAlreadyInUseRecoveryMessage: String
201+
get() = localizedContext.getString(R.string.fui_email_account_creation_error)
202+
override val tooManyRequestsRecoveryMessage: String
203+
get() = localizedContext.getString(R.string.fui_error_too_many_attempts)
204+
override val mfaRequiredRecoveryMessage: String
205+
get() = localizedContext.getString(R.string.fui_error_mfa_required_message)
206+
override val accountLinkingRequiredRecoveryMessage: String
207+
get() = localizedContext.getString(R.string.fui_error_account_linking_required_message)
208+
override val authCancelledRecoveryMessage: String
209+
get() = localizedContext.getString(R.string.fui_error_auth_cancelled_message)
210+
override val unknownErrorRecoveryMessage: String
211+
get() = localizedContext.getString(R.string.fui_error_unknown)
182212
}

auth/src/main/res/values-ar/strings.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,10 @@
8888
<string name="fui_resend_code">إعادة إرسال الرمز</string>
8989
<string name="fui_verify_phone_number">تأكيد ملكية رقم الهاتف</string>
9090
<string name="fui_sms_terms_of_service">عند النقر على “%1$s”، قد يتمّ إرسال رسالة قصيرة SMS وقد يتمّ تطبيق رسوم الرسائل والبيانات.</string>
91-
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">يشير النقر على “%1$s” إلى موافقتك على %2$s و%3$s. وقد يتمّ إرسال رسالة قصيرة كما قد تنطبق رسوم الرسائل والبيانات.</string>
91+
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">يشير النقر على "%1$s" إلى موافقتك على %2$s و%3$s. وقد يتمّ إرسال رسالة قصيرة كما قد تنطبق رسوم الرسائل والبيانات.</string>
92+
<string name="fui_error_dialog_title" translation_description="Title for error recovery dialog">خطأ في المصادقة</string>
93+
<string name="fui_error_retry_action" translation_description="Retry button text in error dialog">أعد المحاولة</string>
94+
<string name="fui_error_mfa_required_message" translation_description="Error message when MFA is required">مطلوب تحقق إضافي. يرجى إكمال المصادقة متعددة العوامل.</string>
95+
<string name="fui_error_account_linking_required_message" translation_description="Error message when account linking is required">يجب ربط الحساب. جرب طريقة تسجيل دخول مختلفة.</string>
96+
<string name="fui_error_auth_cancelled_message" translation_description="Error message when authentication is cancelled">تم إلغاء المصادقة. أعد المحاولة عندما تكون جاهزاً.</string>
9297
</resources>

auth/src/main/res/values-b+es+419/strings.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,10 @@
8888
<string name="fui_resend_code">Reenviar código</string>
8989
<string name="fui_verify_phone_number">Verificar número de teléfono</string>
9090
<string name="fui_sms_terms_of_service">Si presionas “%1$s”, se enviará un SMS. Se aplicarán las tarifas de mensajes y datos.</string>
91-
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">Si presionas “%1$s”, indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos.</string>
91+
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">Si presionas "%1$s", indicas que aceptas nuestras %2$s y %3$s. Es posible que se te envíe un SMS. Podrían aplicarse las tarifas de mensajes y datos.</string>
92+
<string name="fui_error_dialog_title" translation_description="Title for error recovery dialog">Error de autenticación</string>
93+
<string name="fui_error_retry_action" translation_description="Retry button text in error dialog">Intentar de nuevo</string>
94+
<string name="fui_error_mfa_required_message" translation_description="Error message when MFA is required">Se requiere verificación adicional. Complete la autenticación de múltiples factores.</string>
95+
<string name="fui_error_account_linking_required_message" translation_description="Error message when account linking is required">Es necesario vincular la cuenta. Pruebe con un método de inicio de sesión diferente.</string>
96+
<string name="fui_error_auth_cancelled_message" translation_description="Error message when authentication is cancelled">La autenticación fue cancelada. Vuelva a intentarlo cuando esté listo.</string>
9297
</resources>

auth/src/main/res/values-bg/strings.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,10 @@
8888
<string name="fui_resend_code">Повторно изпращане на кода</string>
8989
<string name="fui_verify_phone_number">Потвърждаване на телефонния номер</string>
9090
<string name="fui_sms_terms_of_service">Докосвайки „%1$s“, може да получите SMS съобщение. То може да се таксува по тарифите за данни и SMS.</string>
91-
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">Докосвайки „%1$s“, приемате нашите %2$s и %3$s. Възможно е да получите SMS съобщение. То може да се таксува по тарифите за данни и SMS.</string>
91+
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">Докосвайки „%1$s", приемате нашите %2$s и %3$s. Възможно е да получите SMS съобщение. То може да се таксува по тарифите за данни и SMS.</string>
92+
<string name="fui_error_dialog_title" translation_description="Title for error recovery dialog">Грешка при удостоверяване</string>
93+
<string name="fui_error_retry_action" translation_description="Retry button text in error dialog">Опитай отново</string>
94+
<string name="fui_error_mfa_required_message" translation_description="Error message when MFA is required">Необходима е допълнителна проверка. Моля, завършете многофакторното удостоверяване.</string>
95+
<string name="fui_error_account_linking_required_message" translation_description="Error message when account linking is required">Акаунтът трябва да бъде свързан. Опитайте различен метод за влизане.</string>
96+
<string name="fui_error_auth_cancelled_message" translation_description="Error message when authentication is cancelled">Удостоверяването беше отменено. Опитайте отново, когато сте готови.</string>
9297
</resources>

auth/src/main/res/values-bn/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,10 @@
8989
<string name="fui_verify_phone_number">ফোন নম্বর যাচাই করুন</string>
9090
<string name="fui_sms_terms_of_service">%1$s এ ট্যাপ করলে আপনি একটি এসএমএস পাঠাতে পারেন। মেসেজ ও ডেটার চার্জ প্রযোজ্য।</string>
9191
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">“%1$s” বোতামে ট্যাপ করার অর্থ, আপনি আমাদের %2$s এবং %3$s-এর সাথে সম্মত। একটি এসএমএস পাঠানো হতে পারে। মেসেজ এবং ডেটার উপরে প্রযোজ্য চার্জ লাগতে পারে।</string>
92+
93+
<string name="fui_error_dialog_title" translation_description="Title for error recovery dialog">Authentication Error</string>
94+
<string name="fui_error_retry_action" translation_description="Retry button text in error dialog">Try again</string>
95+
<string name="fui_error_mfa_required_message" translation_description="Error message when MFA is required">Additional verification required. Please complete multi-factor authentication.</string>
96+
<string name="fui_error_account_linking_required_message" translation_description="Error message when account linking is required">Account needs to be linked. Please try a different sign-in method.</string>
97+
<string name="fui_error_auth_cancelled_message" translation_description="Error message when authentication is cancelled">Authentication was cancelled. Please try again when ready.</string>
9298
</resources>

auth/src/main/res/values-ca/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,10 @@
8989
<string name="fui_verify_phone_number">Verifica el número de telèfon</string>
9090
<string name="fui_sms_terms_of_service">En tocar %1$s, és possible que s\'enviï un SMS. Es poden aplicar tarifes de dades i missatges.</string>
9191
<string name="fui_sms_terms_of_service_and_privacy_policy_extended">En tocar %1$s, acceptes les nostres %2$s i la nostra %3$s. És possible que s\'enviï un SMS. Es poden aplicar tarifes de dades i missatges.</string>
92+
93+
<string name="fui_error_dialog_title" translation_description="Title for error recovery dialog">Authentication Error</string>
94+
<string name="fui_error_retry_action" translation_description="Retry button text in error dialog">Try again</string>
95+
<string name="fui_error_mfa_required_message" translation_description="Error message when MFA is required">Additional verification required. Please complete multi-factor authentication.</string>
96+
<string name="fui_error_account_linking_required_message" translation_description="Error message when account linking is required">Account needs to be linked. Please try a different sign-in method.</string>
97+
<string name="fui_error_auth_cancelled_message" translation_description="Error message when authentication is cancelled">Authentication was cancelled. Please try again when ready.</string>
9298
</resources>

0 commit comments

Comments
 (0)