Skip to content

Commit bcd45a5

Browse files
authored
feat(authenticator): Allow customer to override error strings per exception type (#266)
1 parent c60267d commit bcd45a5

File tree

9 files changed

+256
-113
lines changed

9 files changed

+256
-113
lines changed

authenticator/api/authenticator.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,3 +961,7 @@ public final class com/amplifyframework/ui/authenticator/util/MissingConfigurati
961961
public fun <init> ()V
962962
}
963963

964+
public final class com/amplifyframework/ui/authenticator/util/UnsupportedNextStepException : com/amplifyframework/auth/AuthException {
965+
public static final field $stable I
966+
}
967+

authenticator/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ android {
66
namespace = "com.amplifyframework.ui.authenticator"
77
defaultConfig {
88
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
9+
consumerProguardFiles += file("consumer-rules.pro")
910
}
1011

1112
compileOptions {

authenticator/consumer-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Keep AuthExceptions names since these can be mapped to error strings reflectively
2+
-keepnames class * extends com.amplifyframework.auth.AuthException

authenticator/src/main/java/com/amplifyframework/ui/authenticator/AuthenticatorViewModel.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
7979
import com.amplifyframework.ui.authenticator.util.RealAuthProvider
8080
import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
8181
import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
82+
import com.amplifyframework.ui.authenticator.util.UnsupportedNextStepException
8283
import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
8384
import com.amplifyframework.ui.authenticator.util.toFieldError
8485
import kotlinx.coroutines.channels.BufferOverflow
@@ -92,7 +93,6 @@ import org.jetbrains.annotations.VisibleForTesting
9293

9394
internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) :
9495
AndroidViewModel(application) {
95-
9696
// Constructor for compose viewModels provider
9797
constructor(application: Application) : this(application, RealAuthProvider())
9898

@@ -218,6 +218,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
218218
}
219219

220220
private suspend fun handleSignUpFailure(error: AuthException) = handleAuthException(error)
221+
221222
private suspend fun handleSignUpConfirmFailure(error: AuthException) = handleAuthException(error)
222223

223224
private suspend fun handleSignUpSuccess(username: String, password: String, result: AuthSignUpResult) {
@@ -439,10 +440,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
439440
)
440441
else -> {
441442
// Generic error for any other next steps that may be added in the future
442-
val exception = AuthException(
443-
"Unsupported next step $nextStep.",
444-
"Authenticator does not support this Authentication flow, disable it to use Authenticator."
445-
)
443+
val exception = UnsupportedNextStepException(nextStep)
446444
logger.error("Unsupported next step $nextStep", exception)
447445
sendMessage(UnknownErrorMessage(exception))
448446
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/strings/StringResolver.kt

Lines changed: 77 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515

1616
package com.amplifyframework.ui.authenticator.strings
1717

18+
import android.annotation.SuppressLint
1819
import androidx.compose.runtime.Composable
1920
import androidx.compose.runtime.ReadOnlyComposable
20-
import androidx.compose.ui.ExperimentalComposeUiApi
21+
import androidx.compose.ui.platform.LocalContext
2122
import androidx.compose.ui.res.pluralStringResource
2223
import androidx.compose.ui.res.stringResource
2324
import com.amplifyframework.auth.AuthException
@@ -27,8 +28,13 @@ import com.amplifyframework.ui.authenticator.forms.FieldError
2728
import com.amplifyframework.ui.authenticator.forms.FieldKey
2829
import com.amplifyframework.ui.authenticator.forms.PasswordError
2930
import com.amplifyframework.ui.authenticator.locals.LocalStringResolver
31+
import com.amplifyframework.ui.authenticator.util.toResourceName
32+
import kotlin.reflect.KClass
3033

3134
internal open class StringResolver {
35+
// Avoid recomputing the same error message for each type of exception
36+
private val cachedErrorMessages = mutableMapOf<KClass<out AuthException>, String>()
37+
3238
@Composable
3339
@ReadOnlyComposable
3440
open fun label(config: FieldConfig): String {
@@ -41,54 +47,49 @@ internal open class StringResolver {
4147

4248
@Composable
4349
@ReadOnlyComposable
44-
private fun title(config: FieldConfig): String {
45-
return config.label ?: when (config.key) {
46-
FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm)
47-
FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code)
48-
FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password)
49-
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
50-
FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email)
51-
FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username)
52-
FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate)
53-
FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name)
54-
FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name)
55-
FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name)
56-
FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name)
57-
FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website)
58-
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
59-
FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname)
60-
FieldKey.PreferredUsername ->
61-
stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username)
62-
FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile)
63-
FieldKey.VerificationAttribute ->
64-
stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute)
65-
else -> ""
66-
}
50+
private fun title(config: FieldConfig): String = config.label ?: when (config.key) {
51+
FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm)
52+
FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code)
53+
FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password)
54+
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
55+
FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email)
56+
FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username)
57+
FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate)
58+
FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name)
59+
FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name)
60+
FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name)
61+
FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name)
62+
FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website)
63+
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
64+
FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname)
65+
FieldKey.PreferredUsername ->
66+
stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username)
67+
FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile)
68+
FieldKey.VerificationAttribute ->
69+
stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute)
70+
else -> ""
6771
}
6872

6973
@Composable
7074
@ReadOnlyComposable
71-
open fun hint(config: FieldConfig): String? {
72-
return config.hint ?: when {
73-
config.key == FieldKey.ConfirmPassword ->
74-
stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm)
75-
config is FieldConfig.Date -> "yyyy-mm-dd"
76-
else -> {
77-
val label = label(config)
78-
stringResource(R.string.amplify_ui_authenticator_field_hint, label)
79-
}
75+
open fun hint(config: FieldConfig): String? = config.hint ?: when {
76+
config.key == FieldKey.ConfirmPassword ->
77+
stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm)
78+
config is FieldConfig.Date -> "yyyy-mm-dd"
79+
else -> {
80+
val label = label(config)
81+
stringResource(R.string.amplify_ui_authenticator_field_hint, label)
8082
}
8183
}
8284

83-
@OptIn(ExperimentalComposeUiApi::class)
8485
@Composable
8586
@ReadOnlyComposable
86-
open fun error(config: FieldConfig, error: FieldError): String {
87-
return when (error) {
88-
is FieldError.InvalidPassword -> {
89-
var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements)
90-
error.errors.forEach {
91-
errorText += "\n" + when (it) {
87+
open fun error(config: FieldConfig, error: FieldError): String = when (error) {
88+
is FieldError.InvalidPassword -> {
89+
var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements)
90+
error.errors.forEach {
91+
errorText += "\n" +
92+
when (it) {
9293
is PasswordError.InvalidPasswordLength ->
9394
pluralStringResource(
9495
id = R.plurals.amplify_ui_authenticator_field_password_too_short,
@@ -105,54 +106,57 @@ internal open class StringResolver {
105106
stringResource(R.string.amplify_ui_authenticator_field_password_missing_lower)
106107
else -> ""
107108
}
108-
}
109-
errorText
110-
}
111-
FieldError.PasswordsDoNotMatch ->
112-
stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password)
113-
FieldError.MissingRequired -> {
114-
val label = title(config)
115-
stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label)
116-
}
117-
FieldError.InvalidFormat -> {
118-
val label = title(config)
119-
stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label)
120-
}
121-
FieldError.FieldValueExists -> {
122-
val label = title(config)
123-
stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label)
124-
}
125-
FieldError.ConfirmationCodeIncorrect -> {
126-
stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code)
127109
}
128-
is FieldError.Custom -> error.message
129-
FieldError.NotFound -> {
130-
val label = title(config)
131-
stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label)
132-
}
133-
else -> ""
110+
errorText
111+
}
112+
FieldError.PasswordsDoNotMatch ->
113+
stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password)
114+
FieldError.MissingRequired -> {
115+
val label = title(config)
116+
stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label)
117+
}
118+
FieldError.InvalidFormat -> {
119+
val label = title(config)
120+
stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label)
121+
}
122+
FieldError.FieldValueExists -> {
123+
val label = title(config)
124+
stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label)
125+
}
126+
FieldError.ConfirmationCodeIncorrect -> {
127+
stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code)
128+
}
129+
is FieldError.Custom -> error.message
130+
FieldError.NotFound -> {
131+
val label = title(config)
132+
stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label)
134133
}
134+
else -> ""
135135
}
136136

137-
@Suppress("UNUSED_EXPRESSION")
137+
@SuppressLint("DiscouragedApi")
138138
@Composable
139139
@ReadOnlyComposable
140140
open fun error(error: AuthException): String {
141-
return when (error) {
142-
else -> stringResource(R.string.amplify_ui_authenticator_error_unknown)
141+
val context = LocalContext.current
142+
return cachedErrorMessages.getOrPut(error::class) {
143+
// Check if the customer application has defined a specific string for this Exception type. If not, return
144+
// the generic error message.
145+
val resourceName = error.toResourceName()
146+
val resourceId = context.resources.getIdentifier(resourceName, "string", context.packageName)
147+
val message = if (resourceId != 0) stringResource(resourceId) else null
148+
message ?: stringResource(R.string.amplify_ui_authenticator_error_unknown)
143149
}
144150
}
145151

146152
companion object {
147153
@Composable
148154
@ReadOnlyComposable
149-
fun label(config: FieldConfig) =
150-
LocalStringResolver.current.label(config = config)
155+
fun label(config: FieldConfig) = LocalStringResolver.current.label(config = config)
151156

152157
@Composable
153158
@ReadOnlyComposable
154-
fun hint(config: FieldConfig) =
155-
LocalStringResolver.current.hint(config = config)
159+
fun hint(config: FieldConfig) = LocalStringResolver.current.hint(config = config)
156160

157161
@Composable
158162
@ReadOnlyComposable
@@ -161,7 +165,6 @@ internal open class StringResolver {
161165

162166
@Composable
163167
@ReadOnlyComposable
164-
fun error(error: AuthException) =
165-
LocalStringResolver.current.error(error = error)
168+
fun error(error: AuthException) = LocalStringResolver.current.error(error = error)
166169
}
167170
}

authenticator/src/main/java/com/amplifyframework/ui/authenticator/util/AuthenticatorMessage.kt

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515

1616
package com.amplifyframework.ui.authenticator.util
1717

18+
import android.annotation.SuppressLint
1819
import android.content.Context
1920
import androidx.compose.runtime.Composable
2021
import androidx.compose.ui.res.stringResource
2122
import com.amplifyframework.auth.AuthException
2223
import com.amplifyframework.ui.authenticator.R
24+
import kotlin.reflect.KClass
2325

2426
/**
2527
* Messages that may be displayed in the Authenticator UI.
@@ -52,9 +54,7 @@ interface AuthenticatorMessage {
5254
}
5355
}
5456

55-
internal abstract class AuthenticatorMessageImpl(
56-
private val resource: Int
57-
) : AuthenticatorMessage {
57+
internal abstract class AuthenticatorMessageImpl(protected val resource: Int) : AuthenticatorMessage {
5858

5959
override val message: String
6060
@Composable
@@ -83,48 +83,68 @@ internal object CodeSentMessage :
8383
/**
8484
* The user cannot reset their password because their account is in an invalid state.
8585
*/
86-
internal class UnableToResetPasswordMessage(
87-
override val cause: AuthException
88-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password), AuthenticatorMessage.Error
86+
internal class UnableToResetPasswordMessage(override val cause: AuthException) :
87+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password),
88+
AuthenticatorMessage.Error
89+
90+
// Avoid recomputing the same error message multiple times
91+
private typealias ErrorCache = MutableMap<KClass<out AuthException>, String>
92+
private val cachedErrorMessages: ErrorCache = mutableMapOf()
8993

9094
/**
9195
* An unknown error occurred.
9296
*/
93-
internal class UnknownErrorMessage(
94-
override val cause: AuthException
95-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown), AuthenticatorMessage.Error
97+
internal class UnknownErrorMessage(override val cause: AuthException) :
98+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown),
99+
AuthenticatorMessage.Error {
100+
101+
override fun message(context: Context): String {
102+
return message(context, cachedErrorMessages)
103+
}
104+
105+
@SuppressLint("DiscouragedApi")
106+
internal fun message(context: Context, cache: ErrorCache): String {
107+
return cache.getOrPut(cause::class) {
108+
// Check if the customer application has defined a specific string for this Exception type. If not, return
109+
// the generic error message.
110+
val resourceName = cause.toResourceName()
111+
val resourceId = context.resources.getIdentifier(resourceName, "string", context.packageName)
112+
if (resourceId != 0) context.getString(resourceId) else super.message(context)
113+
}
114+
}
115+
}
96116

97117
/**
98118
* The username or password were incorrect.
99119
*/
100-
internal class InvalidLoginMessage(
101-
override val cause: AuthException
102-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin), AuthenticatorMessage.Error
120+
internal class InvalidLoginMessage(override val cause: AuthException) :
121+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin),
122+
AuthenticatorMessage.Error
103123

104124
/**
105125
* The server could not send a confirmation code to the user.
106126
*/
107-
internal class CannotSendCodeMessage(
108-
override val cause: AuthException
109-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code), AuthenticatorMessage.Error
127+
internal class CannotSendCodeMessage(override val cause: AuthException) :
128+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code),
129+
AuthenticatorMessage.Error
110130

111131
/**
112132
* The entered confirmation code has expired.
113133
*/
114-
internal class ExpiredCodeMessage(
115-
override val cause: AuthException
116-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code), AuthenticatorMessage.Error
134+
internal class ExpiredCodeMessage(override val cause: AuthException) :
135+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code),
136+
AuthenticatorMessage.Error
117137

118138
/**
119139
* The device may not have connectivity.
120140
*/
121-
internal class NetworkErrorMessage(
122-
override val cause: AuthException
123-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network), AuthenticatorMessage.Error
141+
internal class NetworkErrorMessage(override val cause: AuthException) :
142+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network),
143+
AuthenticatorMessage.Error
124144

125145
/**
126146
* User tried an action too many times.
127147
*/
128-
internal class LimitExceededMessage(
129-
override val cause: AuthException
130-
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded), AuthenticatorMessage.Error
148+
internal class LimitExceededMessage(override val cause: AuthException) :
149+
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded),
150+
AuthenticatorMessage.Error

0 commit comments

Comments
 (0)