feat(payment): identify card brand and display logo dynamically
This commit is contained in:
@@ -760,6 +760,46 @@ struct PaymentCardView: View {
|
||||
@State var latestOrder: PublicOrderResult? = nil
|
||||
@State var hasOpenedTracking = false
|
||||
|
||||
private var detectedBrandLogo: String? {
|
||||
let clean = cardNumber.filter(\.isNumber)
|
||||
guard !clean.isEmpty else { return nil }
|
||||
|
||||
// Sodexo
|
||||
if clean.hasPrefix("506766") || clean.hasPrefix("603389") {
|
||||
return "sodexo_logo"
|
||||
}
|
||||
|
||||
// Alelo
|
||||
if clean.hasPrefix("5067") || clean.hasPrefix("5078") || clean.hasPrefix("6278") || clean.hasPrefix("6367") {
|
||||
return "alelocard_logo"
|
||||
}
|
||||
|
||||
// Hipercard
|
||||
if clean.hasPrefix("3841") || clean.hasPrefix("606282") || clean.hasPrefix("637095") || clean.hasPrefix("637568") {
|
||||
return "hipercard_logo"
|
||||
}
|
||||
|
||||
// Visa
|
||||
if clean.hasPrefix("4") {
|
||||
return "visacard_logo"
|
||||
}
|
||||
|
||||
// Mastercard
|
||||
if (51...55).contains(Int(clean.prefix(2)) ?? 0) {
|
||||
return "mastercard_logo"
|
||||
}
|
||||
if let prefix4 = Int(clean.prefix(4)), (2221...2720).contains(prefix4) {
|
||||
return "mastercard_logo"
|
||||
}
|
||||
|
||||
// Amex
|
||||
if clean.hasPrefix("34") || clean.hasPrefix("37") {
|
||||
return "amexcard_logo"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
@@ -786,11 +826,38 @@ struct PaymentCardView: View {
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
labeledField("Número do Cartão", placeholder: "0000 0000 0000 0000", text: $cardNumber)
|
||||
labeledField("Número do Cartão", placeholder: "0000 0000 0000 0000", text: $cardNumber, brandLogo: detectedBrandLogo)
|
||||
.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: $cardHolderName)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
labeledField("Validade", placeholder: "MM/AA", text: $expiry)
|
||||
.keyboardType(.numberPad)
|
||||
.onChange(of: expiry) { _, v in
|
||||
let d = String(v.filter(\.isNumber).prefix(4))
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
@@ -847,17 +914,25 @@ struct PaymentCardView: View {
|
||||
onOpenTracking?()
|
||||
}
|
||||
|
||||
private func labeledField(_ label: String, placeholder: String, text: Binding<String>) -> some View {
|
||||
private func labeledField(_ label: String, placeholder: String, text: Binding<String>, brandLogo: String? = nil) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(label)
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
TextField(placeholder, text: text)
|
||||
.appNoAutoCap()
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 46)
|
||||
.background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
HStack(spacing: 8) {
|
||||
TextField(placeholder, text: text)
|
||||
.appNoAutoCap()
|
||||
if let brandLogo, brandLogo.isEmpty == false {
|
||||
Image(brandLogo)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.frame(height: 46)
|
||||
.background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user