Skip to content

Commit 7b068bb

Browse files
authored
fix(SignUpAttribute): Fixing required sign up attributes being rendered as optional. (#35)
1 parent 1e13280 commit 7b068bb

File tree

4 files changed

+103
-45
lines changed

4 files changed

+103
-45
lines changed

Sources/Authenticator/Models/SignUpAttribute.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,16 @@ extension CognitoConfiguration.SignUpAttribute {
188188
}
189189
}
190190
}
191+
192+
extension CognitoConfiguration.UsernameAttribute {
193+
var asSignUpAttribute: SignUpAttribute {
194+
switch self {
195+
case .username:
196+
return .username
197+
case .email:
198+
return .email
199+
case .phoneNumber:
200+
return .phoneNumber
201+
}
202+
}
203+
}

Sources/Authenticator/Models/SignUpField.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public protocol SignUpField {
1919
public struct BaseSignUpField: SignUpField {
2020
public let label: String
2121
public let placeholder: String
22-
public let isRequired: Bool
22+
internal(set) public var isRequired: Bool
2323
public let attributeType: SignUpAttribute
2424
public let validator: FieldValidator?
2525
let inputType: InputType
@@ -32,23 +32,23 @@ public struct BaseSignUpField: SignUpField {
3232
inputType: InputType = .text,
3333
validator: FieldValidator? = nil
3434
) {
35-
if isRequired {
36-
self.label = label
37-
} else {
38-
self.label = "authenticator.field.label.optional".localized(using: label)
39-
}
35+
self.label = label
4036
self.placeholder = placeholder
4137
self.isRequired = isRequired
4238
self.attributeType = attributeType
4339
self.inputType = inputType
4440
self.validator = validator
4541
}
42+
43+
var displayedLabel: String {
44+
return isRequired ? label : "authenticator.field.label.optional".localized(using: label)
45+
}
4646
}
4747

4848
/// A field that is displayed using a provided custom View
4949
public struct CustomSignUpField: SignUpField {
5050
public let label: String?
51-
public let isRequired: Bool
51+
internal(set) public var isRequired: Bool
5252
public let attributeType: SignUpAttribute
5353
public let validator: FieldValidator?
5454
public let content: (Binding<String>) -> any View
@@ -69,6 +69,11 @@ public struct CustomSignUpField: SignUpField {
6969
self.content = content
7070
self.errorContent = errorContent
7171
}
72+
73+
var displayedLabel: String? {
74+
guard let label = label else { return nil }
75+
return isRequired ? label : "authenticator.field.label.optional".localized(using: label)
76+
}
7277
}
7378

7479

Sources/Authenticator/States/SignUpState.swift

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,26 +90,59 @@ public class SignUpState: AuthenticatorBaseState {
9090
setBusy(true)
9191
let cognitoConfiguration = authenticatorState.configuration
9292

93-
var existingFields: Set<String> = []
93+
var existingFields: Set<SignUpAttribute> = []
9494
var inputs = signUpFields.compactMap { field -> Field? in
95-
guard !existingFields.contains(field.rawValue) else {
95+
guard !existingFields.contains(field.attributeType) else {
9696
log.warn("Skipping configuring field of type '\(field.rawValue)' because it was already present.")
9797
return nil
9898
}
9999

100-
existingFields.insert(field.rawValue)
100+
existingFields.insert(field.attributeType)
101101
return Field(field: field)
102102
}
103+
104+
// Validate username attribute is present and required
105+
let usernameAttribute = cognitoConfiguration.usernameAttribute
106+
if existingFields.contains(usernameAttribute.asSignUpAttribute),
107+
let usernameField = inputs.first(where: { $0.field.attributeType == usernameAttribute.asSignUpAttribute }) {
108+
if !usernameField.isRequired {
109+
log.verbose("Marking username attribute \(usernameAttribute.rawValue) as required")
110+
usernameField.isRequired = true
111+
}
112+
} else {
113+
// Add username field at the top
114+
log.verbose("Adding missing username attribute \(usernameAttribute.rawValue) to Sign Up Fields")
115+
inputs.insert(.init(field: .signUpField(from: usernameAttribute)), at: 0)
116+
existingFields.insert(usernameAttribute.asSignUpAttribute)
117+
}
118+
119+
// Validate all required sign up attributes are present
120+
for attribute in cognitoConfiguration.signupAttributes {
121+
if existingFields.contains(attribute.asSignUpAttribute),
122+
let field = inputs.first(where: { $0.field.attributeType == attribute.asSignUpAttribute }) {
123+
if !field.isRequired {
124+
log.verbose("Marking sign up attribute \(attribute.rawValue) as required")
125+
field.isRequired = true
126+
}
127+
} else {
128+
log.verbose("Adding missing required sign up attribute \(attribute.rawValue) to Sign Up Fields")
129+
inputs.append(.init(field: .signUpField(from: attribute, isRequired: true)))
130+
existingFields.insert(attribute.asSignUpAttribute)
131+
}
132+
}
103133

134+
// Validate all verification attributes are present
104135
for attribute in cognitoConfiguration.verificationMechanisms {
105-
if let index = inputs.firstIndex(where: { $0.field.attributeType == attribute.asSignUpAttribute }) {
106-
if !inputs[index].field.isRequired {
136+
if existingFields.contains(attribute.asSignUpAttribute),
137+
let field = inputs.first(where: { $0.field.attributeType == attribute.asSignUpAttribute }) {
138+
if !field.isRequired {
107139
log.verbose("Marking verification attribute \(attribute.rawValue) as required")
108-
inputs[index] = Field(field: .signUpField(from: attribute))
140+
field.isRequired = true
109141
}
110142
} else {
111143
log.verbose("Adding missing verification attribute \(attribute.rawValue) to Sign Up Fields")
112-
inputs.append(Field(field: .signUpField(from: attribute)))
144+
inputs.append(.init(field: .signUpField(from: attribute)))
145+
existingFields.insert(attribute.asSignUpAttribute)
113146
}
114147
}
115148
self.fields = inputs
@@ -126,26 +159,22 @@ public class SignUpState: AuthenticatorBaseState {
126159
.confirmPassword()
127160
]
128161

162+
var existingFields: Set<SignUpAttribute> = []
129163
for field in initialSignUpFields {
130164
fields.append(.init(field: field))
165+
existingFields.insert(field.attributeType)
131166
}
132167

133-
for attribute in cognitoConfiguration.signupAttributes {
134-
guard !fields.contains(where: { $0.field.attributeType == attribute.asSignUpAttribute } ) else {
135-
continue
136-
}
137-
138-
let isVerificationAttribute = cognitoConfiguration.verificationMechanisms.contains {
139-
$0.rawValue == attribute.rawValue
140-
}
141-
let field: SignUpField = .signUpField(from: attribute, isRequired: isVerificationAttribute)
142-
fields.append(Field(field: field))
168+
// Add all required sign up attributes
169+
for attribute in cognitoConfiguration.signupAttributes where !existingFields.contains(attribute.asSignUpAttribute) {
170+
fields.append(.init(field: .signUpField(from: attribute, isRequired: true)))
171+
existingFields.insert(attribute.asSignUpAttribute)
143172
}
144173

145-
for attribute in cognitoConfiguration.verificationMechanisms {
146-
if !(fields.contains { $0.field.attributeType == attribute.asSignUpAttribute }){
147-
fields.append(Field(field: .signUpField(from: attribute)))
148-
}
174+
// Add all verification mechanisms that might not be present
175+
for attribute in cognitoConfiguration.verificationMechanisms where !existingFields.contains(attribute.asSignUpAttribute) {
176+
fields.append(.init(field: .signUpField(from: attribute)))
177+
existingFields.insert(attribute.asSignUpAttribute)
149178
}
150179

151180
setBusy(false)
@@ -155,7 +184,7 @@ public class SignUpState: AuthenticatorBaseState {
155184
public extension SignUpState {
156185
/// Represents a pair between a `SignUpField` and the value that is provided by the user
157186
class Field: ObservableObject, Hashable {
158-
public let field: SignUpField
187+
private(set) public var field: SignUpField
159188
@Published public var value: String = ""
160189

161190
init(field: SignUpField) {
@@ -169,6 +198,25 @@ public extension SignUpState {
169198
public func hash(into hasher: inout Hasher) {
170199
return hasher.combine(field.attributeType)
171200
}
201+
202+
var isRequired: Bool {
203+
set {
204+
guard isRequired != newValue else { return }
205+
switch field {
206+
case var baseField as BaseSignUpField:
207+
baseField.isRequired = newValue
208+
field = baseField
209+
case var customField as CustomSignUpField:
210+
customField.isRequired = newValue
211+
field = customField
212+
default:
213+
log.error("Unsupported SignUpField of type \(type(of: self)) cannot be mutated")
214+
}
215+
}
216+
get {
217+
field.isRequired
218+
}
219+
}
172220
}
173221
}
174222

Sources/Authenticator/Views/Internal/SignUpInputField.swift

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,28 @@ struct SignUpInputField: View {
3636
switch field.inputType {
3737
case .text:
3838
TextField(
39-
field.label,
39+
field.displayedLabel,
4040
text: $field.value,
4141
placeholder: field.placeholder,
4242
validator: validator
4343
)
4444
case .password:
4545
PasswordField(
46-
field.label,
46+
field.displayedLabel,
4747
text: $field.value,
4848
placeholder: field.placeholder,
4949
validator: validator
5050
)
5151
case .date:
5252
DatePicker(
53-
field.label,
53+
field.displayedLabel,
5454
text: $field.value,
5555
placeholder: field.placeholder,
5656
validator: validator
5757
)
5858
case .phoneNumber:
5959
PhoneNumberField(
60-
field.label,
60+
field.displayedLabel,
6161
text: $field.value,
6262
placeholder: field.placeholder,
6363
validator: validator
@@ -72,7 +72,7 @@ struct SignUpInputField: View {
7272

7373
@ViewBuilder func customView(for field: CustomSignUpField) -> some View {
7474
VStack(alignment: .leading, spacing: theme.components.field.spacing.vertical) {
75-
if let label = field.label {
75+
if let label = field.displayedLabel {
7676
HStack {
7777
SwiftUI.Text(label)
7878
.foregroundColor(foregroundColor)
@@ -92,11 +92,12 @@ struct SignUpInputField: View {
9292
}
9393
if case .error(let message) = validator.state, let errorMessage = message {
9494
AnyView(
95-
field.errorContent(errorMessage)
95+
field.errorContent(String(format: errorMessage, field.label ?? "authenticator.validator.field".localized()))
9696
.font(theme.fonts.subheadline)
9797
)
98-
.foregroundColor(borderColor)
98+
.foregroundColor(foregroundColor)
9999
.transition(options.contentTransition)
100+
.accessibilityHidden(true)
100101
}
101102
}
102103
}
@@ -109,13 +110,4 @@ struct SignUpInputField: View {
109110
return theme.colors.foreground.error
110111
}
111112
}
112-
113-
private var borderColor: Color {
114-
switch validator.state {
115-
case .normal:
116-
return theme.colors.border.primary
117-
case .error:
118-
return theme.colors.border.error
119-
}
120-
}
121113
}

0 commit comments

Comments
 (0)