diff --git a/Sources/Authenticator/Models/SignUpAttribute.swift b/Sources/Authenticator/Models/SignUpAttribute.swift index 9ed1d6b..eec46f6 100644 --- a/Sources/Authenticator/Models/SignUpAttribute.swift +++ b/Sources/Authenticator/Models/SignUpAttribute.swift @@ -188,3 +188,16 @@ extension CognitoConfiguration.SignUpAttribute { } } } + +extension CognitoConfiguration.UsernameAttribute { + var asSignUpAttribute: SignUpAttribute { + switch self { + case .username: + return .username + case .email: + return .email + case .phoneNumber: + return .phoneNumber + } + } +} diff --git a/Sources/Authenticator/Models/SignUpField.swift b/Sources/Authenticator/Models/SignUpField.swift index b66fc13..85ce31e 100644 --- a/Sources/Authenticator/Models/SignUpField.swift +++ b/Sources/Authenticator/Models/SignUpField.swift @@ -19,7 +19,7 @@ public protocol SignUpField { public struct BaseSignUpField: SignUpField { public let label: String public let placeholder: String - public let isRequired: Bool + internal(set) public var isRequired: Bool public let attributeType: SignUpAttribute public let validator: FieldValidator? let inputType: InputType @@ -32,23 +32,23 @@ public struct BaseSignUpField: SignUpField { inputType: InputType = .text, validator: FieldValidator? = nil ) { - if isRequired { - self.label = label - } else { - self.label = "authenticator.field.label.optional".localized(using: label) - } + self.label = label self.placeholder = placeholder self.isRequired = isRequired self.attributeType = attributeType self.inputType = inputType self.validator = validator } + + var displayedLabel: String { + return isRequired ? label : "authenticator.field.label.optional".localized(using: label) + } } /// A field that is displayed using a provided custom View public struct CustomSignUpField: SignUpField { public let label: String? - public let isRequired: Bool + internal(set) public var isRequired: Bool public let attributeType: SignUpAttribute public let validator: FieldValidator? public let content: (Binding) -> any View @@ -69,6 +69,11 @@ public struct CustomSignUpField: SignUpField { self.content = content self.errorContent = errorContent } + + var displayedLabel: String? { + guard let label = label else { return nil } + return isRequired ? label : "authenticator.field.label.optional".localized(using: label) + } } diff --git a/Sources/Authenticator/States/SignUpState.swift b/Sources/Authenticator/States/SignUpState.swift index 3e8b59d..696ce28 100644 --- a/Sources/Authenticator/States/SignUpState.swift +++ b/Sources/Authenticator/States/SignUpState.swift @@ -90,26 +90,59 @@ public class SignUpState: AuthenticatorBaseState { setBusy(true) let cognitoConfiguration = authenticatorState.configuration - var existingFields: Set = [] + var existingFields: Set = [] var inputs = signUpFields.compactMap { field -> Field? in - guard !existingFields.contains(field.rawValue) else { + guard !existingFields.contains(field.attributeType) else { log.warn("Skipping configuring field of type '\(field.rawValue)' because it was already present.") return nil } - existingFields.insert(field.rawValue) + existingFields.insert(field.attributeType) return Field(field: field) } + + // Validate username attribute is present and required + let usernameAttribute = cognitoConfiguration.usernameAttribute + if existingFields.contains(usernameAttribute.asSignUpAttribute), + let usernameField = inputs.first(where: { $0.field.attributeType == usernameAttribute.asSignUpAttribute }) { + if !usernameField.isRequired { + log.verbose("Marking username attribute \(usernameAttribute.rawValue) as required") + usernameField.isRequired = true + } + } else { + // Add username field at the top + log.verbose("Adding missing username attribute \(usernameAttribute.rawValue) to Sign Up Fields") + inputs.insert(.init(field: .signUpField(from: usernameAttribute)), at: 0) + existingFields.insert(usernameAttribute.asSignUpAttribute) + } + + // Validate all required sign up attributes are present + for attribute in cognitoConfiguration.signupAttributes { + if existingFields.contains(attribute.asSignUpAttribute), + let field = inputs.first(where: { $0.field.attributeType == attribute.asSignUpAttribute }) { + if !field.isRequired { + log.verbose("Marking sign up attribute \(attribute.rawValue) as required") + field.isRequired = true + } + } else { + log.verbose("Adding missing required sign up attribute \(attribute.rawValue) to Sign Up Fields") + inputs.append(.init(field: .signUpField(from: attribute, isRequired: true))) + existingFields.insert(attribute.asSignUpAttribute) + } + } + // Validate all verification attributes are present for attribute in cognitoConfiguration.verificationMechanisms { - if let index = inputs.firstIndex(where: { $0.field.attributeType == attribute.asSignUpAttribute }) { - if !inputs[index].field.isRequired { + if existingFields.contains(attribute.asSignUpAttribute), + let field = inputs.first(where: { $0.field.attributeType == attribute.asSignUpAttribute }) { + if !field.isRequired { log.verbose("Marking verification attribute \(attribute.rawValue) as required") - inputs[index] = Field(field: .signUpField(from: attribute)) + field.isRequired = true } } else { log.verbose("Adding missing verification attribute \(attribute.rawValue) to Sign Up Fields") - inputs.append(Field(field: .signUpField(from: attribute))) + inputs.append(.init(field: .signUpField(from: attribute))) + existingFields.insert(attribute.asSignUpAttribute) } } self.fields = inputs @@ -126,26 +159,22 @@ public class SignUpState: AuthenticatorBaseState { .confirmPassword() ] + var existingFields: Set = [] for field in initialSignUpFields { fields.append(.init(field: field)) + existingFields.insert(field.attributeType) } - for attribute in cognitoConfiguration.signupAttributes { - guard !fields.contains(where: { $0.field.attributeType == attribute.asSignUpAttribute } ) else { - continue - } - - let isVerificationAttribute = cognitoConfiguration.verificationMechanisms.contains { - $0.rawValue == attribute.rawValue - } - let field: SignUpField = .signUpField(from: attribute, isRequired: isVerificationAttribute) - fields.append(Field(field: field)) + // Add all required sign up attributes + for attribute in cognitoConfiguration.signupAttributes where !existingFields.contains(attribute.asSignUpAttribute) { + fields.append(.init(field: .signUpField(from: attribute, isRequired: true))) + existingFields.insert(attribute.asSignUpAttribute) } - for attribute in cognitoConfiguration.verificationMechanisms { - if !(fields.contains { $0.field.attributeType == attribute.asSignUpAttribute }){ - fields.append(Field(field: .signUpField(from: attribute))) - } + // Add all verification mechanisms that might not be present + for attribute in cognitoConfiguration.verificationMechanisms where !existingFields.contains(attribute.asSignUpAttribute) { + fields.append(.init(field: .signUpField(from: attribute))) + existingFields.insert(attribute.asSignUpAttribute) } setBusy(false) @@ -155,7 +184,7 @@ public class SignUpState: AuthenticatorBaseState { public extension SignUpState { /// Represents a pair between a `SignUpField` and the value that is provided by the user class Field: ObservableObject, Hashable { - public let field: SignUpField + private(set) public var field: SignUpField @Published public var value: String = "" init(field: SignUpField) { @@ -169,6 +198,25 @@ public extension SignUpState { public func hash(into hasher: inout Hasher) { return hasher.combine(field.attributeType) } + + var isRequired: Bool { + set { + guard isRequired != newValue else { return } + switch field { + case var baseField as BaseSignUpField: + baseField.isRequired = newValue + field = baseField + case var customField as CustomSignUpField: + customField.isRequired = newValue + field = customField + default: + log.error("Unsupported SignUpField of type \(type(of: self)) cannot be mutated") + } + } + get { + field.isRequired + } + } } } diff --git a/Sources/Authenticator/Views/Internal/SignUpInputField.swift b/Sources/Authenticator/Views/Internal/SignUpInputField.swift index e86cb69..01ea3d1 100644 --- a/Sources/Authenticator/Views/Internal/SignUpInputField.swift +++ b/Sources/Authenticator/Views/Internal/SignUpInputField.swift @@ -36,28 +36,28 @@ struct SignUpInputField: View { switch field.inputType { case .text: TextField( - field.label, + field.displayedLabel, text: $field.value, placeholder: field.placeholder, validator: validator ) case .password: PasswordField( - field.label, + field.displayedLabel, text: $field.value, placeholder: field.placeholder, validator: validator ) case .date: DatePicker( - field.label, + field.displayedLabel, text: $field.value, placeholder: field.placeholder, validator: validator ) case .phoneNumber: PhoneNumberField( - field.label, + field.displayedLabel, text: $field.value, placeholder: field.placeholder, validator: validator @@ -72,7 +72,7 @@ struct SignUpInputField: View { @ViewBuilder func customView(for field: CustomSignUpField) -> some View { VStack(alignment: .leading, spacing: theme.components.field.spacing.vertical) { - if let label = field.label { + if let label = field.displayedLabel { HStack { SwiftUI.Text(label) .foregroundColor(foregroundColor) @@ -92,11 +92,12 @@ struct SignUpInputField: View { } if case .error(let message) = validator.state, let errorMessage = message { AnyView( - field.errorContent(errorMessage) + field.errorContent(String(format: errorMessage, field.label ?? "authenticator.validator.field".localized())) .font(theme.fonts.subheadline) ) - .foregroundColor(borderColor) + .foregroundColor(foregroundColor) .transition(options.contentTransition) + .accessibilityHidden(true) } } } @@ -109,13 +110,4 @@ struct SignUpInputField: View { return theme.colors.foreground.error } } - - private var borderColor: Color { - switch validator.state { - case .normal: - return theme.colors.border.primary - case .error: - return theme.colors.border.error - } - } }