Skip to content

Commit

Permalink
SDKI-155: Improve multi-line card form validation behaviors (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
seamless-pay-ios committed Nov 25, 2024
1 parent dafad3e commit 9589dad
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 120 deletions.
8 changes: 4 additions & 4 deletions ObjC/include/SPCardValidator.m
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,13 @@ + (NSInteger)currentMonth {
}

+ (SPCardValidationState)validationStateForPostalCode:(NSString *)postalCode {
// Check for nil or empty string
if (postalCode == nil || postalCode.length == 0) {
// Check length less than minimum
if (postalCode == nil || postalCode.length < self.postalCodeMinLength) {
return SPCardValidationStateIncomplete;
}

// Check length (3-10 characters)
if (postalCode.length < self.postalCodeMinLength || postalCode.length > self.postalCodeMaxLength) {
// Check length greater than maximum
if (postalCode.length > self.postalCodeMaxLength) {
return SPCardValidationStateInvalid;
}

Expand Down
33 changes: 16 additions & 17 deletions SeamlessPay/UI/Components/LineTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class LineTextField: SPFormTextField {
get { errorLabel.text }
set {
errorLabel.text = newValue
validText = newValue?.isEmpty ?? true
updateAppearance()
}
}
Expand Down Expand Up @@ -113,15 +114,16 @@ public class LineTextField: SPFormTextField {
}

private var errorMessageHeight: CGFloat {
let height: CGFloat
if errorMessage?.isEmpty ?? true {
height = 0
} else {
height = appearance.errorFont.lineHeight
}

return height

errorLabel.textRect(
forBounds: CGRect(
x: 0,
y: 0,
width: errorLabel.frame.width,
height: CGFloat.greatestFiniteMagnitude
),
limitedToNumberOfLines: 0
)
.height
}

private var floatingPlaceholderWidth: CGFloat {
Expand All @@ -139,10 +141,6 @@ public class LineTextField: SPFormTextField {
}

// MARK: - Overrides
override public var validText: Bool {
didSet { updateAppearance() }
}

override public var borderStyle: UITextField.BorderStyle {
get { .none }
set { super.borderStyle = .none }
Expand Down Expand Up @@ -179,7 +177,6 @@ public class LineTextField: SPFormTextField {

// MARK: - Setup Methods
private func setupTextField() {
validText = true
textAlignment = .left
borderStyle = .none
rightView = rightImageView
Expand All @@ -195,7 +192,7 @@ public class LineTextField: SPFormTextField {
}

private func setupErrorLabel() {
errorLabel.numberOfLines = 1
errorLabel.numberOfLines = 0
addSubview(errorLabel)
}

Expand Down Expand Up @@ -349,7 +346,7 @@ public class LineTextField: SPFormTextField {

// MARK: - Appearance
extension LineTextField {
@objc func updateAppearance() {
func updateAppearance() {
errorColor = appearance.textInvalidColor
defaultColor = appearance.textValidColor
backgroundFrameLayer.cornerRadius = appearance.cornerRadius
Expand All @@ -358,7 +355,9 @@ extension LineTextField {
floatingPlaceholderLabel.font = appearance.floatingPlaceholderFont
errorLabel.font = appearance.errorFont

switch (isFirstResponder, validText) {
let isFieldValid = errorMessage?.isEmpty ?? true

switch (isFirstResponder, isFieldValid) {
case (true, true): // focus and valid
backgroundFrameLayer.borderColor = appearance.borderFocusValidColor.cgColor
backgroundFrameLayer.backgroundColor = appearance.backgroundFocusValidColor.cgColor
Expand Down
41 changes: 38 additions & 3 deletions SeamlessPay/UI/PaymentInputs/Direct/CardFormViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,50 @@ extension CardFormViewModel {

extension CardFormViewModel {
func formValidationForField(_ fieldType: SPCardFieldType) -> FormValidationError? {
// Return early if field is not required
guard isFieldRequired(fieldType) else {
return nil
}

let validationState = validationState(for: fieldType)
let value = valueForField(fieldType)
let isFieldEmpty = value?.isEmpty ?? true
let isFieldEmpty = valueForField(fieldType)?.isEmpty ?? true

switch (fieldType, validationState, isFieldEmpty) {
return getValidationError(for: fieldType, state: validationState, isEmpty: isFieldEmpty)
}

func realTimeValidationForField(
_ fieldType: SPCardFieldType,
isFocused: Bool
) -> FormValidationError? {
// Return early if field is not required
guard isFieldRequired(fieldType) else {
return .none
}

let validationState = validationState(for: fieldType)
let isFieldEmpty = valueForField(fieldType)?.isEmpty ?? true

// Return nil for incomplete empty fields
if validationState == .incomplete {
// Return nil for empty fields if not focused
if isFocused {
return .none
}
// Return nil for empty fields if not focused
if isFieldEmpty {
return .none
}
}

return getValidationError(for: fieldType, state: validationState, isEmpty: isFieldEmpty)
}

private func getValidationError(
for fieldType: SPCardFieldType,
state: SPCardValidationState,
isEmpty: Bool
) -> FormValidationError? {
switch (fieldType, state, isEmpty) {
// No error
case (_, .valid, _):
return .none
Expand Down
70 changes: 39 additions & 31 deletions SeamlessPay/UI/PaymentInputs/Direct/MultiLineCardForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public class MultiLineCardForm: UIControl, CardForm {
numberField,
expirationAndCvcStackView,
postalCodeTitleLabel,
postalCodeField
postalCodeField,
]
)
stackView.axis = .vertical
Expand Down Expand Up @@ -203,7 +203,7 @@ private extension MultiLineCardForm {

func configureViews() {
cvcField.isHidden = !viewModel.cvcDisplayed
[postalCodeTitleLabel, postalCodeField].forEach { view in
for view in [postalCodeTitleLabel, postalCodeField] {
view.isHidden = !viewModel.postalCodeDisplayed
}

Expand All @@ -230,7 +230,7 @@ private extension MultiLineCardForm {
textField.translatesAutoresizingMaskIntoConstraints = false
textField.keyboardType = .asciiCapableNumberPad
textField.formDelegate = self
textField.validText = true
textField.errorMessage = .none
textField.autocorrectionType = .no
textField.clearButtonMode = .never

Expand Down Expand Up @@ -366,7 +366,8 @@ extension MultiLineCardForm: SPFormTextFieldDelegate {
}

public func textFieldDidEndEditing(_ textField: UITextField) {
guard let fieldType = SPCardFieldType(rawValue: textField.tag) else {
guard let fieldType = SPCardFieldType(rawValue: textField.tag),
let textField = textField as? LineTextField else {
return
}

Expand All @@ -379,6 +380,8 @@ extension MultiLineCardForm: SPFormTextFieldDelegate {
updateImages()
onDidEndEditing()
}

realTimeValidationForField(fieldType, isFocused: false)
}

public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
Expand All @@ -400,23 +403,19 @@ extension MultiLineCardForm: SPFormTextFieldDelegate {
return
}

// Reset the error message set during on submit validation
// when the user starts changing the input
textField.errorMessage = .none
// if fieldType == .number {
// // Changing the card number field can invalidate the CVC, e.g., going from 4
// // digit Amex CVC to 3 digit Visa
// realTimeValidationForField(.CVC, isFocused: false)
// }

if fieldType == .number {
// Changing the card number field can invalidate the CVC, e.g., going from 4
// digit Amex CVC to 3 digit Visa
cvcField.validText = viewModel.validationState(for: .CVC) != .invalid
}
realTimeValidationForField(fieldType, isFocused: true)

let state = viewModel.validationState(for: fieldType)
textField.validText = true

switch state {
case .invalid:
textField.validText = false
case .incomplete:
case .incomplete,
.invalid:
break
case .valid:
if fieldType == .CVC {
Expand Down Expand Up @@ -541,38 +540,49 @@ extension MultiLineCardForm {
}
}

// MARK: - Realtime Validation
extension MultiLineCardForm {
func realTimeValidationForField(_ fieldType: SPCardFieldType, isFocused: Bool) {
if let error = viewModel.realTimeValidationForField(fieldType, isFocused: isFocused) {
handleValidationError(error)
} else {
let field = allFields.first { $0.tag == fieldType.rawValue }
field?.errorMessage = .none
}
}
}

// MARK: - On Submit Validation
extension MultiLineCardForm {
func validateForm() -> Bool {
onWillEndEditingForReturn()
_ = resignFirstResponder()

let errors = formValidation()

if !errors.isEmpty {
// Reset all fields to valid
resetAllFieldsToValid()
// Reset all fields to valid
resetAllFieldsToValid()

// Handle error
for error in errors {
handleFormValidationError(error)
}
// Handle errors
for error in errors {
handleValidationError(error)
}

onWillEndEditingForReturn()
_ = resignFirstResponder()
becomeFirstResponder()

return errors.isEmpty
}

private func formValidation() -> [FormValidationError] {
allFields.compactMap { field in
guard let fieldType = SPCardFieldType(rawValue: field.tag),
let error = viewModel.formValidationForField(fieldType) else {
guard let fieldType = SPCardFieldType(rawValue: field.tag) else {
return .none
}
return error
return viewModel.formValidationForField(fieldType)
}
}

private func handleFormValidationError(_ error: FormValidationError) {
private func handleValidationError(_ error: FormValidationError) {
// Look up for the field that failed
let failedField: LineTextField

Expand Down Expand Up @@ -640,7 +650,6 @@ extension MultiLineCardForm {
return
}

failedField.validText = false
failedField.errorMessage = errorMessage

// Update the images to show the error state after stricter validation
Expand All @@ -656,7 +665,6 @@ extension MultiLineCardForm {

private func resetAllFieldsToValid() {
for field in allFields {
field.validText = true
field.errorMessage = .none
}
}
Expand Down
11 changes: 10 additions & 1 deletion Tests/SeamlessPayTests/UI/Components/LineTextFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ class LineTextFieldTests: XCTestCase {
}

func testSetupErrorLabel() {
XCTAssertEqual(lineTextField.errorLabel.numberOfLines, 1)
XCTAssertEqual(lineTextField.errorLabel.numberOfLines, 0)
}

func testErrorMessageUpdatesValidText() {
// Test setting error message updates validText
lineTextField.errorMessage = "Test Error"
XCTAssertFalse(lineTextField.validText)

lineTextField.errorMessage = ""
XCTAssertTrue(lineTextField.validText)
}
}
Loading

0 comments on commit 9589dad

Please sign in to comment.