fix(checkout): display snackbar on top of sheet modals
This commit is contained in:
@@ -929,89 +929,93 @@ struct CardSelectionSheet: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
HStack {
|
||||
Text("Selecionar Cartão")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if savedCards.isEmpty == false {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(savedCards) { card in
|
||||
savedCardRow(card)
|
||||
if card.id != savedCards.last?.id {
|
||||
Divider().padding(.horizontal, 14)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
if let selectedCardId {
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Pagar \(formatCurrency(cardContext.total))") {
|
||||
guard isSubmitting == false else { return }
|
||||
onSavedCardConfirmed(selectedCardId)
|
||||
}
|
||||
.disabled(isSubmitting)
|
||||
}
|
||||
|
||||
Text("ou")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
ZStack(alignment: .top) {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
HStack {
|
||||
Text("Selecionar Cartão")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button {
|
||||
showNewCardSheet = true
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(AppColors.primary)
|
||||
Text("Adicionar novo cartão")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.sheet(isPresented: $showNewCardSheet) {
|
||||
PaymentCardView(
|
||||
context: cardContext,
|
||||
appState: $appState,
|
||||
onOrderCreated: { orderId, shortId in
|
||||
showNewCardSheet = false
|
||||
onOrderCreated(orderId, shortId)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
if savedCards.isEmpty == false {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(savedCards) { card in
|
||||
savedCardRow(card)
|
||||
if card.id != savedCards.last?.id {
|
||||
Divider().padding(.horizontal, 14)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
if let selectedCardId {
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Pagar \(formatCurrency(cardContext.total))") {
|
||||
guard isSubmitting == false else { return }
|
||||
onSavedCardConfirmed(selectedCardId)
|
||||
}
|
||||
.disabled(isSubmitting)
|
||||
}
|
||||
|
||||
Text("ou")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
Button {
|
||||
showNewCardSheet = true
|
||||
} label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(AppColors.primary)
|
||||
Text("Adicionar novo cartão")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.sheet(isPresented: $showNewCardSheet) {
|
||||
PaymentCardView(
|
||||
context: cardContext,
|
||||
appState: $appState,
|
||||
onOrderCreated: { orderId, shortId in
|
||||
showNewCardSheet = false
|
||||
onOrderCreated(orderId, shortId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SnackbarOverlay(center: SnackbarCenter.shared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,140 +1137,144 @@ struct PaymentCardView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Novo Cartão")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Total
|
||||
ZStack(alignment: .top) {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Total do Pedido")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
Text(formatCurrency(context.total))
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
Text("Novo Cartão")
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
|
||||
// Dados do cartão
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
sectionHeader("DADOS DO CARTÃO")
|
||||
VStack(spacing: 10) {
|
||||
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 }
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Total
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Total do Pedido")
|
||||
.font(AppTypography.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.textCase(nil)
|
||||
Text(formatCurrency(context.total))
|
||||
.font(AppTypography.heading2)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(14)
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
labeledField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $holderName)
|
||||
// Dados do cartão
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
sectionHeader("DADOS DO CARTÃO")
|
||||
VStack(spacing: 10) {
|
||||
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 }
|
||||
}
|
||||
|
||||
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("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("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 }
|
||||
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)
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
// Salvar cartão
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Salvar cartão")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Text("Pague mais rápido nas próximas compras")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $saveCard)
|
||||
.labelsHidden()
|
||||
.tint(AppColors.primary)
|
||||
}
|
||||
.padding(14)
|
||||
|
||||
if saveCard {
|
||||
Divider().padding(.horizontal, 14)
|
||||
labeledField("Apelido do cartão (opcional)", placeholder: "Ex: Meu Visa", text: $nickname)
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.animation(.easeInOut(duration: 0.2), value: saveCard)
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Confirmar e Pagar") {
|
||||
guard canSubmit, isSubmitting == false else { return }
|
||||
Task { await submitOrder() }
|
||||
// Salvar cartão
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Salvar cartão")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Text("Pague mais rápido nas próximas compras")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
Spacer()
|
||||
Toggle("", isOn: $saveCard)
|
||||
.labelsHidden()
|
||||
.tint(AppColors.primary)
|
||||
}
|
||||
.padding(14)
|
||||
|
||||
if saveCard {
|
||||
Divider().padding(.horizontal, 14)
|
||||
labeledField("Apelido do cartão (opcional)", placeholder: "Ex: Meu Visa", text: $nickname)
|
||||
.padding(14)
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
.animation(.easeInOut(duration: 0.2), value: saveCard)
|
||||
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Confirmar e Pagar") {
|
||||
guard canSubmit, isSubmitting == false else { return }
|
||||
Task { await submitOrder() }
|
||||
}
|
||||
.disabled(canSubmit == false || isSubmitting)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.disabled(canSubmit == false || isSubmitting)
|
||||
}
|
||||
.padding(20)
|
||||
.background(AppColors.backgroundLight)
|
||||
|
||||
SnackbarOverlay(center: SnackbarCenter.shared)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
} // VStack
|
||||
.background(AppColors.backgroundLight)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -1527,68 +1535,72 @@ struct ChangePaymentCardSheet: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Cartão de Crédito").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button { dismiss() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 12, weight: .bold)).foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30).background(AppColors.surface).clipShape(Circle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
if savedCards.isEmpty == false {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(savedCards) { card in
|
||||
Button { selectedCardId = card.id } label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "creditcard.fill").font(.system(size: 18)).foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 44, height: 44).background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(card.displayLabel).font(AppTypography.heading3).foregroundStyle(AppColors.textPrimary)
|
||||
Text("Vence \(card.expiryLabel)").font(.caption).foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
Spacer()
|
||||
Circle().stroke(selectedCardId == card.id ? AppColors.tertiary : AppColors.textMuted.opacity(0.45), lineWidth: 2)
|
||||
.frame(width: 22, height: 22).background(Circle().fill(selectedCardId == card.id ? AppColors.tertiary : Color.clear))
|
||||
}.padding(14)
|
||||
}.buttonStyle(.plain)
|
||||
if card.id != savedCards.last?.id { Divider().padding(.horizontal, 14) }
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
if let selectedCardId {
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Pagar com este Cartão") {
|
||||
guard isSubmitting == false else { return }
|
||||
Task { await changeToSavedCard(cardId: selectedCardId) }
|
||||
}.disabled(isSubmitting)
|
||||
}
|
||||
Text("ou").font(AppTypography.caption).foregroundStyle(AppColors.textMuted).frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
Button { showNewCardForm = true } label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "plus.circle.fill").font(.system(size: 18)).foregroundStyle(AppColors.primary)
|
||||
Text("Novo cartão").font(AppTypography.heading3).foregroundStyle(AppColors.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(14).background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
ZStack(alignment: .top) {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Cartão de Crédito").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button { dismiss() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 12, weight: .bold)).foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30).background(AppColors.surface).clipShape(Circle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.bottom, 32)
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
if savedCards.isEmpty == false {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(savedCards) { card in
|
||||
Button { selectedCardId = card.id } label: {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: "creditcard.fill").font(.system(size: 18)).foregroundStyle(AppColors.textPrimary)
|
||||
.frame(width: 44, height: 44).background(AppColors.backgroundLight)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(card.displayLabel).font(AppTypography.heading3).foregroundStyle(AppColors.textPrimary)
|
||||
Text("Vence \(card.expiryLabel)").font(.caption).foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
Spacer()
|
||||
Circle().stroke(selectedCardId == card.id ? AppColors.tertiary : AppColors.textMuted.opacity(0.45), lineWidth: 2)
|
||||
.frame(width: 22, height: 22).background(Circle().fill(selectedCardId == card.id ? AppColors.tertiary : Color.clear))
|
||||
}.padding(14)
|
||||
}.buttonStyle(.plain)
|
||||
if card.id != savedCards.last?.id { Divider().padding(.horizontal, 14) }
|
||||
}
|
||||
}
|
||||
.background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
if let selectedCardId {
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Pagar com este Cartão") {
|
||||
guard isSubmitting == false else { return }
|
||||
Task { await changeToSavedCard(cardId: selectedCardId) }
|
||||
}.disabled(isSubmitting)
|
||||
}
|
||||
Text("ou").font(AppTypography.caption).foregroundStyle(AppColors.textMuted).frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
Button { showNewCardForm = true } label: {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "plus.circle.fill").font(.system(size: 18)).foregroundStyle(AppColors.primary)
|
||||
Text("Novo cartão").font(AppTypography.heading3).foregroundStyle(AppColors.primary)
|
||||
Spacer()
|
||||
}
|
||||
.padding(14).background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
.sheet(isPresented: $showNewCardForm) {
|
||||
ChangePaymentNewCardView(pixContext: pixContext, appState: $appState) { orderId, shortId in
|
||||
showNewCardForm = false; onConfirmed(orderId, shortId)
|
||||
.background(AppColors.backgroundLight)
|
||||
.sheet(isPresented: $showNewCardForm) {
|
||||
ChangePaymentNewCardView(pixContext: pixContext, appState: $appState) { orderId, shortId in
|
||||
showNewCardForm = false; onConfirmed(orderId, shortId)
|
||||
}
|
||||
}
|
||||
|
||||
SnackbarOverlay(center: SnackbarCenter.shared)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,55 +1700,59 @@ struct ChangePaymentNewCardView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Novo Cartão").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button { dismiss() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 12, weight: .bold)).foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30).background(AppColors.surface).clipShape(Circle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
VStack(spacing: 10) {
|
||||
cardField("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 m = stride(from: 0, to: d.count, by: 4).map { i -> String in let s = d.index(d.startIndex, offsetBy: i); let e = d.index(s, offsetBy: min(4, d.count - i)); return String(d[s..<e]) }.joined(separator: " ")
|
||||
if m != v { cardNumber = m }
|
||||
}
|
||||
cardField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $holderName)
|
||||
HStack(spacing: 10) {
|
||||
cardField("Validade", placeholder: "MM/AAAA", text: $expiry).keyboardType(.numberPad)
|
||||
.onChange(of: expiry) { _, v in let d = String(v.filter(\.isNumber).prefix(6)); let m = d.count <= 2 ? d : "\(d.prefix(2))/\(d.dropFirst(2))"; if m != v { expiry = m } }
|
||||
cardField("CVV", placeholder: "•••", text: $cvv).keyboardType(.numberPad)
|
||||
.onChange(of: cvv) { _, v in let d = String(v.filter(\.isNumber).prefix(4)); if d != v { cvv = d } }
|
||||
}
|
||||
cardField("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 m: String
|
||||
if d.count <= 3 { m = d } else if d.count <= 6 { m = "\(d.prefix(3)).\(d.dropFirst(3))" }
|
||||
else if d.count <= 9 { m = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6))" }
|
||||
else { m = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6).prefix(3))-\(d.dropFirst(9))" }
|
||||
if m != v { cpf = m }
|
||||
}
|
||||
}
|
||||
.padding(14).background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Confirmar e Pagar") {
|
||||
guard canSubmit, isSubmitting == false else { return }
|
||||
Task { await submit() }
|
||||
}.disabled(canSubmit == false || isSubmitting)
|
||||
ZStack(alignment: .top) {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Novo Cartão").font(AppTypography.heading2).foregroundStyle(AppColors.textPrimary)
|
||||
Spacer()
|
||||
Button { dismiss() } label: {
|
||||
Image(systemName: "xmark").font(.system(size: 12, weight: .bold)).foregroundStyle(AppColors.textMuted)
|
||||
.frame(width: 30, height: 30).background(AppColors.surface).clipShape(Circle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 20).padding(.top, 20).padding(.bottom, 16)
|
||||
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 16) {
|
||||
VStack(spacing: 10) {
|
||||
cardField("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 m = stride(from: 0, to: d.count, by: 4).map { i -> String in let s = d.index(d.startIndex, offsetBy: i); let e = d.index(s, offsetBy: min(4, d.count - i)); return String(d[s..<e]) }.joined(separator: " ")
|
||||
if m != v { cardNumber = m }
|
||||
}
|
||||
cardField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $holderName)
|
||||
HStack(spacing: 10) {
|
||||
cardField("Validade", placeholder: "MM/AAAA", text: $expiry).keyboardType(.numberPad)
|
||||
.onChange(of: expiry) { _, v in let d = String(v.filter(\.isNumber).prefix(6)); let m = d.count <= 2 ? d : "\(d.prefix(2))/\(d.dropFirst(2))"; if m != v { expiry = m } }
|
||||
cardField("CVV", placeholder: "•••", text: $cvv).keyboardType(.numberPad)
|
||||
.onChange(of: cvv) { _, v in let d = String(v.filter(\.isNumber).prefix(4)); if d != v { cvv = d } }
|
||||
}
|
||||
cardField("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 m: String
|
||||
if d.count <= 3 { m = d } else if d.count <= 6 { m = "\(d.prefix(3)).\(d.dropFirst(3))" }
|
||||
else if d.count <= 9 { m = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6))" }
|
||||
else { m = "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6).prefix(3))-\(d.dropFirst(9))" }
|
||||
if m != v { cpf = m }
|
||||
}
|
||||
}
|
||||
.padding(14).background(AppColors.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
|
||||
|
||||
PrimaryButton(title: isSubmitting ? "Processando..." : "Confirmar e Pagar") {
|
||||
guard canSubmit, isSubmitting == false else { return }
|
||||
Task { await submit() }
|
||||
}.disabled(canSubmit == false || isSubmitting)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
|
||||
SnackbarOverlay(center: SnackbarCenter.shared)
|
||||
}
|
||||
.background(AppColors.backgroundLight)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
Reference in New Issue
Block a user