feat(cards): add input masks and char limits to card form fields

- Card number: groups of 4 digits (0000 0000 0000 0000), max 16 digits
- Expiry: MM/AAAA mask, max 6 digits
- CVV: digits only, max 4
- CPF: 000.000.000-00 mask, max 11 digits
- canSubmit validates stripped digit counts
This commit is contained in:
Daniel Arantes Loverde
2026-06-03 17:58:44 -03:00
parent a548955320
commit abd58ac36d

View File

@@ -997,10 +997,10 @@ struct PaymentCardView: View {
}
var canSubmit: Bool {
holderName.isEmpty == false &&
holderName.trimmingCharacters(in: .whitespaces).isEmpty == false &&
cardNumber.filter(\.isNumber).count >= 13 &&
expiry.count >= 4 &&
cvv.count >= 3 &&
expiry.filter(\.isNumber).count == 6 &&
cvv.filter(\.isNumber).count >= 3 &&
cpf.filter(\.isNumber).count == 11
}
@@ -1051,15 +1051,49 @@ struct PaymentCardView: View {
VStack(spacing: 10) {
labeledField("Número do Cartão", placeholder: "0000 0000 0000 0000", text: $cardNumber)
.keyboardType(.numberPad)
.onChange(of: cardNumber) { _, v in
let d = String(v.filter(\.isNumber).prefix(16))
let masked = stride(from: 0, to: d.count, by: 4)
.map { i -> String in
let start = d.index(d.startIndex, offsetBy: i)
let end = d.index(start, offsetBy: min(4, d.count - i))
return String(d[start..<end])
}.joined(separator: " ")
if masked != v { cardNumber = masked }
}
labeledField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $holderName)
HStack(spacing: 10) {
labeledField("Validade", placeholder: "MM/AAAA", text: $expiry)
.keyboardType(.numberPad)
.onChange(of: expiry) { _, v in
let d = String(v.filter(\.isNumber).prefix(6))
let masked: String
if d.count <= 2 { masked = d }
else { masked = "\(d.prefix(2))/\(d.dropFirst(2))" }
if masked != v { expiry = masked }
}
labeledField("CVV", placeholder: "•••", text: $cvv)
.keyboardType(.numberPad)
.onChange(of: cvv) { _, v in
let d = String(v.filter(\.isNumber).prefix(4))
if d != v { cvv = d }
}
}
labeledField("CPF do Titular", placeholder: "000.000.000-00", text: $cpf)
.keyboardType(.numberPad)
.onChange(of: cpf) { _, v in
let d = String(v.filter(\.isNumber).prefix(11))
let masked: String
if d.count <= 3 { masked = d }
else if d.count <= 6 { masked = "\(d.prefix(3)).\(d.dropFirst(3))" }
else if d.count <= 9 { masked = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6))" }
else { masked = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6).prefix(3))-\(d.dropFirst(9))" }
if masked != v { cpf = masked }
}
}
.padding(14)
}