feat(payment): identify card brand and display logo dynamically

This commit is contained in:
Daniel Arantes Loverde
2026-06-04 15:23:02 -03:00
parent 2d27240cee
commit 46d955a92f
7 changed files with 83 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -760,6 +760,46 @@ struct PaymentCardView: View {
@State var latestOrder: PublicOrderResult? = nil @State var latestOrder: PublicOrderResult? = nil
@State var hasOpenedTracking = false @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 { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
@@ -786,11 +826,38 @@ struct PaymentCardView: View {
.foregroundStyle(AppColors.textPrimary) .foregroundStyle(AppColors.textPrimary)
VStack(spacing: 10) { 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) labeledField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $cardHolderName)
HStack(spacing: 10) { HStack(spacing: 10) {
labeledField("Validade", placeholder: "MM/AA", text: $expiry) 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) 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) .padding(14)
@@ -847,17 +914,25 @@ struct PaymentCardView: View {
onOpenTracking?() 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) { VStack(alignment: .leading, spacing: 6) {
Text(label) Text(label)
.font(AppTypography.caption) .font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted) .foregroundStyle(AppColors.textMuted)
TextField(placeholder, text: text) HStack(spacing: 8) {
.appNoAutoCap() TextField(placeholder, text: text)
.padding(.horizontal, 12) .appNoAutoCap()
.frame(height: 46) if let brandLogo, brandLogo.isEmpty == false {
.background(AppColors.backgroundLight) Image(brandLogo)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
}
.padding(.horizontal, 12)
.frame(height: 46)
.background(AppColors.backgroundLight)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
} }
} }