Skip to content
Merged
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
4 changes: 4 additions & 0 deletions authenticator/api/authenticator.api
Original file line number Diff line number Diff line change
Expand Up @@ -961,3 +961,7 @@ public final class com/amplifyframework/ui/authenticator/util/MissingConfigurati
public fun <init> ()V
}

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

1 change: 1 addition & 0 deletions authenticator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ android {
namespace = "com.amplifyframework.ui.authenticator"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles += file("consumer-rules.pro")
}

compileOptions {
Expand Down
2 changes: 2 additions & 0 deletions authenticator/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Keep AuthExceptions names since these can be mapped to error strings reflectively
-keepnames class * extends com.amplifyframework.auth.AuthException
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import com.amplifyframework.ui.authenticator.util.PasswordResetMessage
import com.amplifyframework.ui.authenticator.util.RealAuthProvider
import com.amplifyframework.ui.authenticator.util.UnableToResetPasswordMessage
import com.amplifyframework.ui.authenticator.util.UnknownErrorMessage
import com.amplifyframework.ui.authenticator.util.UnsupportedNextStepException
import com.amplifyframework.ui.authenticator.util.isConnectivityIssue
import com.amplifyframework.ui.authenticator.util.toFieldError
import kotlinx.coroutines.channels.BufferOverflow
Expand All @@ -92,7 +93,6 @@ import org.jetbrains.annotations.VisibleForTesting

internal class AuthenticatorViewModel(application: Application, private val authProvider: AuthProvider) :
AndroidViewModel(application) {

// Constructor for compose viewModels provider
constructor(application: Application) : this(application, RealAuthProvider())

Expand Down Expand Up @@ -218,6 +218,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
}

private suspend fun handleSignUpFailure(error: AuthException) = handleAuthException(error)

private suspend fun handleSignUpConfirmFailure(error: AuthException) = handleAuthException(error)

private suspend fun handleSignUpSuccess(username: String, password: String, result: AuthSignUpResult) {
Expand Down Expand Up @@ -439,10 +440,7 @@ internal class AuthenticatorViewModel(application: Application, private val auth
)
else -> {
// Generic error for any other next steps that may be added in the future
val exception = AuthException(
"Unsupported next step $nextStep.",
"Authenticator does not support this Authentication flow, disable it to use Authenticator."
)
val exception = UnsupportedNextStepException(nextStep)
logger.error("Unsupported next step $nextStep", exception)
sendMessage(UnknownErrorMessage(exception))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@

package com.amplifyframework.ui.authenticator.strings

import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import com.amplifyframework.auth.AuthException
Expand All @@ -27,8 +28,13 @@ import com.amplifyframework.ui.authenticator.forms.FieldError
import com.amplifyframework.ui.authenticator.forms.FieldKey
import com.amplifyframework.ui.authenticator.forms.PasswordError
import com.amplifyframework.ui.authenticator.locals.LocalStringResolver
import com.amplifyframework.ui.authenticator.util.toResourceName
import kotlin.reflect.KClass

internal open class StringResolver {
// Avoid recomputing the same error message for each type of exception
private val cachedErrorMessages = mutableMapOf<KClass<out AuthException>, String>()

@Composable
@ReadOnlyComposable
open fun label(config: FieldConfig): String {
Expand All @@ -41,54 +47,49 @@ internal open class StringResolver {

@Composable
@ReadOnlyComposable
private fun title(config: FieldConfig): String {
return config.label ?: when (config.key) {
FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm)
FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code)
FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password)
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email)
FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username)
FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate)
FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name)
FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name)
FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name)
FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name)
FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website)
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname)
FieldKey.PreferredUsername ->
stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username)
FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile)
FieldKey.VerificationAttribute ->
stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute)
else -> ""
}
private fun title(config: FieldConfig): String = config.label ?: when (config.key) {
FieldKey.ConfirmPassword -> stringResource(R.string.amplify_ui_authenticator_field_label_password_confirm)
FieldKey.ConfirmationCode -> stringResource(R.string.amplify_ui_authenticator_field_label_confirmation_code)
FieldKey.Password -> stringResource(R.string.amplify_ui_authenticator_field_label_password)
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
FieldKey.Email -> stringResource(R.string.amplify_ui_authenticator_field_label_email)
FieldKey.Username -> stringResource(R.string.amplify_ui_authenticator_field_label_username)
FieldKey.Birthdate -> stringResource(R.string.amplify_ui_authenticator_field_label_birthdate)
FieldKey.FamilyName -> stringResource(R.string.amplify_ui_authenticator_field_label_family_name)
FieldKey.GivenName -> stringResource(R.string.amplify_ui_authenticator_field_label_given_name)
FieldKey.MiddleName -> stringResource(R.string.amplify_ui_authenticator_field_label_middle_name)
FieldKey.Name -> stringResource(R.string.amplify_ui_authenticator_field_label_name)
FieldKey.Website -> stringResource(R.string.amplify_ui_authenticator_field_label_website)
FieldKey.PhoneNumber -> stringResource(R.string.amplify_ui_authenticator_field_label_phone_number)
FieldKey.Nickname -> stringResource(R.string.amplify_ui_authenticator_field_label_nickname)
FieldKey.PreferredUsername ->
stringResource(R.string.amplify_ui_authenticator_field_label_preferred_username)
FieldKey.Profile -> stringResource(R.string.amplify_ui_authenticator_field_label_profile)
FieldKey.VerificationAttribute ->
stringResource(R.string.amplify_ui_authenticator_field_label_verification_attribute)
else -> ""
}

@Composable
@ReadOnlyComposable
open fun hint(config: FieldConfig): String? {
return config.hint ?: when {
config.key == FieldKey.ConfirmPassword ->
stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm)
config is FieldConfig.Date -> "yyyy-mm-dd"
else -> {
val label = label(config)
stringResource(R.string.amplify_ui_authenticator_field_hint, label)
}
open fun hint(config: FieldConfig): String? = config.hint ?: when {
config.key == FieldKey.ConfirmPassword ->
stringResource(R.string.amplify_ui_authenticator_field_hint_password_confirm)
config is FieldConfig.Date -> "yyyy-mm-dd"
else -> {
val label = label(config)
stringResource(R.string.amplify_ui_authenticator_field_hint, label)
}
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
@ReadOnlyComposable
open fun error(config: FieldConfig, error: FieldError): String {
return when (error) {
is FieldError.InvalidPassword -> {
var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements)
error.errors.forEach {
errorText += "\n" + when (it) {
open fun error(config: FieldConfig, error: FieldError): String = when (error) {
is FieldError.InvalidPassword -> {
var errorText = stringResource(R.string.amplify_ui_authenticator_field_password_requirements)
error.errors.forEach {
errorText += "\n" +
when (it) {
is PasswordError.InvalidPasswordLength ->
pluralStringResource(
id = R.plurals.amplify_ui_authenticator_field_password_too_short,
Expand All @@ -105,54 +106,57 @@ internal open class StringResolver {
stringResource(R.string.amplify_ui_authenticator_field_password_missing_lower)
else -> ""
}
}
errorText
}
FieldError.PasswordsDoNotMatch ->
stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password)
FieldError.MissingRequired -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label)
}
FieldError.InvalidFormat -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label)
}
FieldError.FieldValueExists -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label)
}
FieldError.ConfirmationCodeIncorrect -> {
stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code)
}
is FieldError.Custom -> error.message
FieldError.NotFound -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label)
}
else -> ""
errorText
}
FieldError.PasswordsDoNotMatch ->
stringResource(R.string.amplify_ui_authenticator_field_warn_unmatched_password)
FieldError.MissingRequired -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_empty, label)
}
FieldError.InvalidFormat -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_invalid_format, label)
}
FieldError.FieldValueExists -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_existing, label)
}
FieldError.ConfirmationCodeIncorrect -> {
stringResource(R.string.amplify_ui_authenticator_field_warn_incorrect_code)
}
is FieldError.Custom -> error.message
FieldError.NotFound -> {
val label = title(config)
stringResource(R.string.amplify_ui_authenticator_field_warn_not_found, label)
}
else -> ""
}

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

companion object {
@Composable
@ReadOnlyComposable
fun label(config: FieldConfig) =
LocalStringResolver.current.label(config = config)
fun label(config: FieldConfig) = LocalStringResolver.current.label(config = config)

@Composable
@ReadOnlyComposable
fun hint(config: FieldConfig) =
LocalStringResolver.current.hint(config = config)
fun hint(config: FieldConfig) = LocalStringResolver.current.hint(config = config)

@Composable
@ReadOnlyComposable
Expand All @@ -161,7 +165,6 @@ internal open class StringResolver {

@Composable
@ReadOnlyComposable
fun error(error: AuthException) =
LocalStringResolver.current.error(error = error)
fun error(error: AuthException) = LocalStringResolver.current.error(error = error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

package com.amplifyframework.ui.authenticator.util

import android.annotation.SuppressLint
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.amplifyframework.auth.AuthException
import com.amplifyframework.ui.authenticator.R
import kotlin.reflect.KClass

/**
* Messages that may be displayed in the Authenticator UI.
Expand Down Expand Up @@ -52,9 +54,7 @@ interface AuthenticatorMessage {
}
}

internal abstract class AuthenticatorMessageImpl(
private val resource: Int
) : AuthenticatorMessage {
internal abstract class AuthenticatorMessageImpl(protected val resource: Int) : AuthenticatorMessage {

override val message: String
@Composable
Expand Down Expand Up @@ -83,48 +83,68 @@ internal object CodeSentMessage :
/**
* The user cannot reset their password because their account is in an invalid state.
*/
internal class UnableToResetPasswordMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password), AuthenticatorMessage.Error
internal class UnableToResetPasswordMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_cannot_reset_password),
AuthenticatorMessage.Error

// Avoid recomputing the same error message multiple times
private typealias ErrorCache = MutableMap<KClass<out AuthException>, String>
private val cachedErrorMessages: ErrorCache = mutableMapOf()

/**
* An unknown error occurred.
*/
internal class UnknownErrorMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown), AuthenticatorMessage.Error
internal class UnknownErrorMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_unknown),
AuthenticatorMessage.Error {

override fun message(context: Context): String {
return message(context, cachedErrorMessages)
}

@SuppressLint("DiscouragedApi")
internal fun message(context: Context, cache: ErrorCache): String {
return cache.getOrPut(cause::class) {
// Check if the customer application has defined a specific string for this Exception type. If not, return
// the generic error message.
val resourceName = cause.toResourceName()
val resourceId = context.resources.getIdentifier(resourceName, "string", context.packageName)
if (resourceId != 0) context.getString(resourceId) else super.message(context)
}
}
}

/**
* The username or password were incorrect.
*/
internal class InvalidLoginMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin), AuthenticatorMessage.Error
internal class InvalidLoginMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_invalid_signin),
AuthenticatorMessage.Error

/**
* The server could not send a confirmation code to the user.
*/
internal class CannotSendCodeMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code), AuthenticatorMessage.Error
internal class CannotSendCodeMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_send_code),
AuthenticatorMessage.Error

/**
* The entered confirmation code has expired.
*/
internal class ExpiredCodeMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code), AuthenticatorMessage.Error
internal class ExpiredCodeMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_expired_code),
AuthenticatorMessage.Error

/**
* The device may not have connectivity.
*/
internal class NetworkErrorMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network), AuthenticatorMessage.Error
internal class NetworkErrorMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_network),
AuthenticatorMessage.Error

/**
* User tried an action too many times.
*/
internal class LimitExceededMessage(
override val cause: AuthException
) : AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded), AuthenticatorMessage.Error
internal class LimitExceededMessage(override val cause: AuthException) :
AuthenticatorMessageImpl(R.string.amplify_ui_authenticator_error_limit_exceeded),
AuthenticatorMessage.Error
Loading