Skip to content

Commit 15248bb

Browse files
committed
add validations for text inputs
1 parent 7e1dcee commit 15248bb

File tree

12 files changed

+237
-78
lines changed

12 files changed

+237
-78
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailAuthView.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ public struct EmailAuthView {
4444

4545
private var isValid: Bool {
4646
return if authService.authenticationFlow == .signIn {
47-
!email.isEmpty && !password.isEmpty
47+
FormValidators.email.isValid(input: email) && !password.isEmpty
4848
} else {
49-
!email.isEmpty && !password.isEmpty && password == confirmPassword
49+
FormValidators.email.isValid(input: email) &&
50+
FormValidators.atLeast6Characters.isValid(input: password) &&
51+
FormValidators.confirmPassword(password: password).isValid(input: confirmPassword)
5052
}
5153
}
5254

@@ -84,6 +86,10 @@ extension EmailAuthView: View {
8486
prompt: authService.string.emailInputLabel,
8587
keyboardType: .emailAddress,
8688
contentType: .emailAddress,
89+
validations: [
90+
FormValidators.email
91+
],
92+
maintainsValidationMessage: authService.authenticationFlow == .signUp,
8793
onSubmit: { _ in
8894
self.focus = .password
8995
},
@@ -98,7 +104,11 @@ extension EmailAuthView: View {
98104
label: authService.string.passwordFieldLabel,
99105
prompt: authService.string.passwordInputLabel,
100106
contentType: .password,
101-
sensitive: true,
107+
isSecureTextField: true,
108+
validations: authService.authenticationFlow == .signUp ? [
109+
FormValidators.atLeast6Characters
110+
] : [],
111+
maintainsValidationMessage: authService.authenticationFlow == .signUp,
102112
onSubmit: { _ in
103113
Task { try await signInWithEmailPassword() }
104114
},
@@ -125,7 +135,11 @@ extension EmailAuthView: View {
125135
label: authService.string.confirmPasswordFieldLabel,
126136
prompt: authService.string.confirmPasswordInputLabel,
127137
contentType: .password,
128-
sensitive: true,
138+
isSecureTextField: true,
139+
validations: [
140+
FormValidators.confirmPassword(password: password)
141+
],
142+
maintainsValidationMessage: true,
129143
onSubmit: { _ in
130144
Task { try await createUserWithEmailPassword() }
131145
},

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EmailLinkView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ extension EmailLinkView: View {
4848
prompt: authService.string.emailInputLabel,
4949
keyboardType: .emailAddress,
5050
contentType: .emailAddress,
51+
validations: [
52+
FormValidators.email
53+
],
5154
leading: {
5255
Image(systemName: "at")
5356
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterPhoneNumberView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ struct EnterPhoneNumberView: View {
3838
prompt: authService.string.enterPhoneNumberPlaceholder,
3939
keyboardType: .phonePad,
4040
contentType: .telephoneNumber,
41+
validations: [
42+
FormValidators.phoneNumber
43+
],
4144
onChange: { _ in }
4245
) {
4346
CountrySelector(

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/EnterVerificationCodeView.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,13 @@ struct EnterVerificationCodeView: View {
4848
.padding(.bottom)
4949
.frame(maxWidth: .infinity, alignment: .leading)
5050

51-
VerificationCodeInputField(code: $verificationCode)
51+
VerificationCodeInputField(
52+
code: $verificationCode,
53+
validations: [
54+
FormValidators.verificationCode
55+
],
56+
maintainsValidationMessage: true
57+
)
5258

5359
Button(action: {
5460
Task {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/MFAEnrolmentView.swift

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@ extension MFAEnrolmentView: View {
374374
prompt: authService.string.enterPhoneNumberPrompt,
375375
keyboardType: .phonePad,
376376
contentType: .telephoneNumber,
377+
validations: [
378+
FormValidators.phoneNumber
379+
],
380+
maintainsValidationMessage: true,
377381
onChange: { _ in }
378382
) {
379383
CountrySelector(
@@ -388,6 +392,10 @@ extension MFAEnrolmentView: View {
388392
text: $displayName,
389393
label: authService.string.displayNameFieldLabel,
390394
prompt: authService.string.enterDisplayNameForDevicePrompt,
395+
validations: [
396+
FormValidators.notEmpty(label: "Display name")
397+
],
398+
maintainsValidationMessage: true,
391399
leading: {
392400
Image(systemName: "person")
393401
}
@@ -430,17 +438,13 @@ extension MFAEnrolmentView: View {
430438
.multilineTextAlignment(.center)
431439
}
432440

433-
AuthTextField(
434-
text: $verificationCode,
435-
label: authService.string.verificationCodeFieldLabel,
436-
prompt: "Enter 6-digit code",
437-
keyboardType: .numberPad,
438-
contentType: .oneTimeCode,
439-
leading: {
440-
Image(systemName: "number")
441-
}
441+
VerificationCodeInputField(
442+
code: $verificationCode,
443+
validations: [
444+
FormValidators.verificationCode
445+
],
446+
maintainsValidationMessage: true
442447
)
443-
.focused($focus, equals: .verificationCode)
444448
.accessibilityIdentifier("verification-code-field")
445449

446450
Button {
@@ -579,23 +583,23 @@ extension MFAEnrolmentView: View {
579583
text: $displayName,
580584
label: authService.string.displayNameFieldLabel,
581585
prompt: authService.string.enterDisplayNameForAuthenticatorPrompt,
586+
validations: [
587+
FormValidators.notEmpty(label: "Display name")
588+
],
589+
maintainsValidationMessage: true,
582590
leading: {
583591
Image(systemName: "person")
584592
}
585593
)
586594
.accessibilityIdentifier("display-name-field")
587595

588-
AuthTextField(
589-
text: $totpCode,
590-
label: authService.string.verificationCodeFieldLabel,
591-
prompt: authService.string.enterCodeFromAppPrompt,
592-
keyboardType: .numberPad,
593-
contentType: .oneTimeCode,
594-
leading: {
595-
Image(systemName: "number")
596-
}
596+
VerificationCodeInputField(
597+
code: $totpCode,
598+
validations: [
599+
FormValidators.verificationCode
600+
],
601+
maintainsValidationMessage: true
597602
)
598-
.focused($focus, equals: .totpCode)
599603
.accessibilityIdentifier("totp-code-field")
600604

601605
Button {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordPromptView.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import FirebaseAuthUIComponents
1516
import FirebaseCore
1617
import SwiftUI
1718

@@ -31,16 +32,22 @@ extension PasswordPromptSheet: View {
3132

3233
Divider()
3334

34-
LabeledContent {
35-
TextField(authService.string.passwordInputLabel, text: $password)
36-
.textInputAutocapitalization(.never)
37-
.disableAutocorrection(true)
38-
.submitLabel(.next)
39-
} label: {
40-
Image(systemName: "lock")
41-
}.padding(.vertical, 10)
42-
.background(Divider(), alignment: .bottom)
43-
.padding(.bottom, 4)
35+
AuthTextField(
36+
text: $password,
37+
label: authService.string.passwordFieldLabel,
38+
prompt: authService.string.passwordInputLabel,
39+
contentType: .password,
40+
isSecureTextField: true,
41+
onSubmit: { _ in
42+
if !password.isEmpty {
43+
coordinator.submit(password: password)
44+
}
45+
},
46+
leading: {
47+
Image(systemName: "lock")
48+
}
49+
)
50+
.submitLabel(.next)
4451

4552
Button(action: {
4653
coordinator.submit(password: password)

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/PasswordRecoveryView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ extension PasswordRecoveryView: View {
4444
prompt: authService.string.emailInputLabel,
4545
keyboardType: .emailAddress,
4646
contentType: .emailAddress,
47+
validations: [
48+
FormValidators.email
49+
],
4750
leading: {
4851
Image(systemName: "at")
4952
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/UpdatePasswordView.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ public struct UpdatePasswordView {
3434
@State private var confirmPassword = ""
3535

3636
@FocusState private var focus: FocusableField?
37+
3738
private var isValid: Bool {
38-
!password.isEmpty && password == confirmPassword
39+
FormValidators.atLeast6Characters.isValid(input: password) &&
40+
FormValidators.confirmPassword(password: password).isValid(input: confirmPassword)
3941
}
4042
}
4143

@@ -48,7 +50,11 @@ extension UpdatePasswordView: View {
4850
label: "Type new password",
4951
prompt: authService.string.passwordInputLabel,
5052
contentType: .password,
51-
sensitive: true,
53+
isSecureTextField: true,
54+
validations: [
55+
FormValidators.atLeast6Characters
56+
],
57+
maintainsValidationMessage: true,
5258
leading: {
5359
Image(systemName: "lock")
5460
}
@@ -61,7 +67,11 @@ extension UpdatePasswordView: View {
6167
label: "Retype new password",
6268
prompt: authService.string.confirmPasswordInputLabel,
6369
contentType: .password,
64-
sensitive: true,
70+
isSecureTextField: true,
71+
validations: [
72+
FormValidators.confirmPassword(password: password)
73+
],
74+
maintainsValidationMessage: true,
6575
leading: {
6676
Image(systemName: "lock")
6777
}

FirebaseSwiftUI/FirebaseAuthUIComponents/Sources/Components/AuthTextField.swift

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,34 @@
1414

1515
import SwiftUI
1616

17-
public struct FieldValidation: Identifiable, Equatable {
18-
public let id = UUID()
19-
public let message: String
20-
public var valid: Bool = false
21-
22-
public init(message: String, valid: Bool = false) {
23-
self.message = message
24-
self.valid = valid
25-
}
26-
}
27-
2817
public struct AuthTextField<Leading: View>: View {
2918
@FocusState private var isFocused: Bool
30-
@State var invalidInput: Bool = false
3119
@State var obscured: Bool = true
32-
20+
@State var hasInteracted: Bool = false
21+
3322
@Binding var text: String
3423
let label: String
3524
let prompt: String
3625
var textAlignment: TextAlignment = .leading
3726
var keyboardType: UIKeyboardType = .default
3827
var contentType: UITextContentType? = nil
3928
var isSecureTextField: Bool = false
40-
var validations: [FieldValidation] = []
29+
var validations: [FormValidator] = []
30+
var maintainsValidationMessage: Bool = false
4131
var formState: ((Bool) -> Void)? = nil
4232
var onSubmit: ((String) -> Void)? = nil
4333
var onChange: ((String) -> Void)? = nil
4434
private let leading: () -> Leading?
45-
35+
4636
public init(text: Binding<String>,
4737
label: String,
4838
prompt: String,
4939
textAlignment: TextAlignment = .leading,
5040
keyboardType: UIKeyboardType = .default,
5141
contentType: UITextContentType? = nil,
52-
sensitive: Bool = false,
53-
validations: [FieldValidation] = [],
42+
isSecureTextField: Bool = false,
43+
validations: [FormValidator] = [],
44+
maintainsValidationMessage: Bool = false,
5445
formState: ((Bool) -> Void)? = nil,
5546
onSubmit: ((String) -> Void)? = nil,
5647
onChange: ((String) -> Void)? = nil,
@@ -61,18 +52,19 @@ public struct AuthTextField<Leading: View>: View {
6152
self.textAlignment = textAlignment
6253
self.keyboardType = keyboardType
6354
self.contentType = contentType
64-
isSecureTextField = sensitive
55+
self.isSecureTextField = isSecureTextField
6556
self.validations = validations
57+
self.maintainsValidationMessage = maintainsValidationMessage
6658
self.formState = formState
6759
self.onSubmit = onSubmit
6860
self.onChange = onChange
6961
self.leading = leading
7062
}
71-
63+
7264
var allRequirementsMet: Bool {
73-
validations.allSatisfy { $0.valid == true }
65+
validations.allSatisfy { $0.isValid(input: text) }
7466
}
75-
67+
7668
public var body: some View {
7769
VStack(alignment: .leading) {
7870
Text(LocalizedStringResource(stringLiteral: label))
@@ -124,8 +116,16 @@ public struct AuthTextField<Leading: View>: View {
124116
onSubmit?(text)
125117
}
126118
.onChange(of: text) { _, newValue in
119+
if !hasInteracted {
120+
hasInteracted = true
121+
}
127122
onChange?(newValue)
128123
}
124+
.onChange(of: isFocused) { _, focused in
125+
if !focused && !text.isEmpty {
126+
hasInteracted = true
127+
}
128+
}
129129
.multilineTextAlignment(textAlignment)
130130
.textFieldStyle(.plain)
131131
.padding(.vertical, 12)
@@ -142,28 +142,19 @@ public struct AuthTextField<Leading: View>: View {
142142
isFocused = true
143143
}
144144
}
145-
if !validations.isEmpty {
145+
if !validations.isEmpty && hasInteracted && (maintainsValidationMessage || !allRequirementsMet) {
146146
VStack(alignment: .leading, spacing: 4) {
147-
ForEach(validations) { validation in
148-
HStack {
149-
Image(systemName: isSecureTextField ? "lock.open" : "x.square")
150-
.foregroundStyle(validation.valid ? .gray : .red)
151-
Text(validation.message)
152-
.strikethrough(validation.valid, color: .gray)
153-
.foregroundStyle(.gray)
154-
.fixedSize(horizontal: false, vertical: true)
155-
}
147+
ForEach(validations) { validator in
148+
let isValid = validator.isValid(input: text)
149+
Text(validator.message)
150+
.font(.caption)
151+
.strikethrough(isValid, color: .gray)
152+
.foregroundStyle(isValid ? .gray : .red)
153+
.fixedSize(horizontal: false, vertical: true)
156154
}
157155
}
158156
.onChange(of: allRequirementsMet) { _, newValue in
159157
formState?(newValue)
160-
if !newValue {
161-
withAnimation(.easeInOut(duration: 0.08).repeatCount(4)) {
162-
invalidInput = true
163-
} completion: {
164-
invalidInput = false
165-
}
166-
}
167158
}
168159
}
169160
}

0 commit comments

Comments
 (0)