diff --git a/VDS/Components/TextFields/InputField/InputField.swift b/VDS/Components/TextFields/InputField/InputField.swift index 875405b9..cee206f7 100644 --- a/VDS/Components/TextFields/InputField/InputField.swift +++ b/VDS/Components/TextFields/InputField/InputField.swift @@ -94,7 +94,11 @@ open class InputField: EntryFieldBase { /// Value for the textField open override var value: String? { - textField.text + if fieldType == .creditCard { + return creditCardRawNumber + } else { + return textField.text + } } var _showError: Bool = false @@ -170,21 +174,29 @@ open class InputField: EntryFieldBase { .textPublisher .sink { [weak self] newText in print("textPublisher newText: \(newText)") - self?.process(text: newText) - self?.validate() self?.sendActions(for: .valueChanged) }.store(in: &subscribers) textField .publisher(for: .editingDidBegin) .sink { [weak self] _ in - self?.setNeedsUpdate() + guard let self else { return } + if self.fieldType == .creditCard { + self.isCreditCardMasked = false + self.textField.text = self.formatCreditCardNumber(self.creditCardRawNumber) + } + self.setNeedsUpdate() }.store(in: &subscribers) textField .publisher(for: .editingDidEnd) .sink { [weak self] _ in - self?.validate() + guard let self else { return } + if self.fieldType == .creditCard { + self.isCreditCardMasked = true + self.textField.text = self.maskCreditCardNumber(self.creditCardRawNumber) + } + self.validate() }.store(in: &subscribers) stackView.addArrangedSubview(successLabel) @@ -328,7 +340,7 @@ open class InputField: EntryFieldBase { rules.append(.init(rule)) case .securityCode: minWidth = 88.0 - + isSecureTextEntry = true } //textField @@ -375,6 +387,16 @@ open class InputField: EntryFieldBase { super.updateRules() switch fieldType { + case .creditCard: + if let text = textField.text, text.count > 0 { + let rule = CharacterCountRule().copyWith { + $0.maxLength = "XXXX XXXX XXXX XXXX".count + $0.compareType = .equals + $0.errorMessage = "Enter a valid credit card." + } + rules.append(.init(rule)) + } + case .tel: if let text = textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { @@ -473,6 +495,46 @@ open class InputField: EntryFieldBase { return formattedString } + //--------------------------------------------------- + // MARK: - Credit Card + //--------------------------------------------------- + private var isCreditCardMasked: Bool = false + private var creditCardRawNumber: String = "" + private var creditCardMaxLength = 16 + + private func formatCreditCardNumber(_ number: String) -> String { + // Format the number in the style XXXX XXXX XXXX XXXX + var formattedNumber = "" + for (index, char) in number.enumerated() { + if index != 0 && index % 4 == 0 { + formattedNumber.append(" ") + } + formattedNumber.append(char) + } + + return formattedNumber + } + + private func updateCardTypeIcon(rawNumber: String) { +// let firstFourDigits = String(rawNumber.prefix(4)) +// if let icon = cardTypeIcons[firstFourDigits] { +// cardTypeIconView.image = icon +// } else { +// cardTypeIconView.image = nil +// } + } + + private func maskCreditCardNumber(_ number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == creditCardMaxLength else { return number } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = formatCreditCardNumber(maskedSection) + return formattedMaskSection + " " + lastFourDigits + } + + //--------------------------------------------------- // MARK: - Telephone //--------------------------------------------------- private func formatUSNumber(_ number: String) -> String { @@ -530,31 +592,55 @@ open class InputField: EntryFieldBase { } extension InputField: UITextFieldDelegate { - public func process(text changedText: String) { - var newText: String = changedText - switch fieldType { - case .date: - guard newText.count <= dateFormat.maxLength else { return } - let numericText = newText.compactMap { $0.isNumber ? $0 : nil } - var formattedText = "" - - for (index, char) in numericText.enumerated() { - if (index == 2 || (index == 4 && (dateFormat != .mmyy))) && index < dateFormat.maxLength { - formattedText += "/" - } - formattedText.append(char) - } - newText = formattedText - - default: break - } - } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { // case text, number, inlineAction, password, creditCard, tel, date, securityCode switch fieldType { + case .creditCard: + let allowedCharacters = CharacterSet.decimalDigits + if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { + return false + } + + // Get the current text + let currentText = textField.text ?? "" + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + if rawNumber.count > creditCardMaxLength { + return false + } + + // Format the number with spaces + let formattedNumber = formatCreditCardNumber(rawNumber) + + // Update the icon based on the first four digits + updateCardTypeIcon(rawNumber: rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = getTelCursorPosition(textField: textField, + range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // if all passes, then set the number1 + creditCardRawNumber = rawNumber + + // Prevent the default behavior + return false + case .date: // Allow only numbers and limit the length of text. guard let oldText = textField.text,