Skip to content

Commit

Permalink
fix(SignUpAttribute): Fixing required sign up attributes being render…
Browse files Browse the repository at this point in the history
…ed as optional. (#35)
  • Loading branch information
sebaland authored Aug 30, 2023
1 parent 1e13280 commit 7b068bb
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 45 deletions.
13 changes: 13 additions & 0 deletions Sources/Authenticator/Models/SignUpAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
19 changes: 12 additions & 7 deletions Sources/Authenticator/Models/SignUpField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<String>) -> any View
Expand All @@ -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)
}
}


Expand Down
92 changes: 70 additions & 22 deletions Sources/Authenticator/States/SignUpState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,59 @@ public class SignUpState: AuthenticatorBaseState {
setBusy(true)
let cognitoConfiguration = authenticatorState.configuration

var existingFields: Set<String> = []
var existingFields: Set<SignUpAttribute> = []
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
Expand All @@ -126,26 +159,22 @@ public class SignUpState: AuthenticatorBaseState {
.confirmPassword()
]

var existingFields: Set<SignUpAttribute> = []
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)
Expand All @@ -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) {
Expand All @@ -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
}
}
}
}

Expand Down
24 changes: 8 additions & 16 deletions Sources/Authenticator/Views/Internal/SignUpInputField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
}
}
}
Expand All @@ -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
}
}
}

0 comments on commit 7b068bb

Please sign in to comment.