migration

This commit is contained in:
Daniel Arantes Loverde
2026-08-11 13:22:02 -03:00
parent c4d6998595
commit 7702836fe7
231 changed files with 4329 additions and 1958 deletions

View File

@@ -0,0 +1,244 @@
import SwiftUI
struct SavedCardsView: View {
@Binding var appState: AppState
@Environment(\.dismiss) var dismiss
@State private var cards: [SavedCard] = []
@State private var isLoading = false
@State private var errorMessage: String? = nil
@State private var openSwipeRowId: String? = nil
@State private var deletingCardId: String? = nil
@State private var showAddCard = false
private var canDelete: Bool { cards.count > 1 }
private let tabBarClearance: CGFloat = 96
var body: some View {
ZStack {
AppColors.backgroundLight.ignoresSafeArea()
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {
screenHeader
VStack(spacing: 12) {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
.padding(.top, 32)
} else if let errorMessage {
Text(errorMessage)
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.multilineTextAlignment(.center)
.padding(.top, 32)
} else if cards.isEmpty {
Text("Nenhum cartão cadastrado")
.font(AppTypography.body)
.foregroundStyle(AppColors.textMuted)
.padding(.top, 32)
} else {
ForEach(cards) { card in
if canDelete {
SwipeToDeleteAddressRow(
rowId: card.id,
openRowId: $openSwipeRowId,
isDeleting: deletingCardId == card.id,
onDelete: { deleteCard(card) }
) {
cardRow(card)
.appContentShape(Rectangle())
.onTapGesture {
if openSwipeRowId == card.id { openSwipeRowId = nil }
}
}
.id(card.id)
.opacity(deletingCardId == card.id ? 0.6 : 1.0)
.disabled(deletingCardId != nil)
} else {
cardRow(card)
}
}
}
}
}
.padding(.horizontal, 20)
.padding(.top, 18)
.padding(.bottom, tabBarClearance + 20)
}
VStack {
Spacer()
addCardButton
.padding(.bottom, tabBarClearance)
}
}
.navigationBarBackButtonHidden(true)
.appHiddenNavigationBar()
.sheet(isPresented: $showAddCard) {
NavigationStack {
AddCardFormView(appState: appState, isFirstCard: cards.isEmpty) { newCard in
cards.append(newCard)
}
}
}
.task { await loadCards() }
}
private var screenHeader: some View {
ZStack {
Text("Meus Cartões")
.font(AppTypography.heading2)
.foregroundStyle(AppColors.textPrimary)
HStack {
Button(action: { dismiss() }) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(AppColors.textPrimary)
.frame(width: 52, height: 52)
.background(AppColors.surface)
.clipShape(Circle())
.shadow(color: .black.opacity(0.06), radius: 8, y: 2)
}
.buttonStyle(.plain)
Spacer()
}
}
}
private var addCardButton: some View {
ZStack(alignment: .bottom) {
Rectangle()
.fill(AppColors.backgroundLight)
.frame(height: 136)
Button(action: { showAddCard = true }) {
HStack(spacing: 12) {
Image(systemName: "creditcard.fill")
.font(.system(size: 20))
Text("Adicionar novo cartão")
.font(AppTypography.heading3)
}
.foregroundStyle(AppColors.textInverse)
.frame(maxWidth: .infinity)
.padding(.vertical, 18)
.background(AppColors.primary)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusXL, style: .continuous))
}
.buttonStyle(.plain)
.padding(.horizontal, 20)
.padding(.bottom, 14)
}
}
private func cardRow(_ card: SavedCard) -> some View {
HStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(AppColors.backgroundLight)
.frame(width: 52, height: 52)
if let logo = brandLogoName(for: card.brand) {
Image(logo)
.resizable()
.scaledToFit()
.frame(width: 36, height: 24)
} else {
Image(systemName: "creditcard.fill")
.font(.system(size: 22))
.foregroundStyle(AppColors.textMuted)
}
}
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 6) {
Text(card.displayLabel)
.font(AppTypography.heading3)
.foregroundStyle(AppColors.textPrimary)
if card.isDefault {
Text("Principal")
.font(AppTypography.caption)
.foregroundStyle(AppColors.primary)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(AppColors.brandSoft)
.clipShape(Capsule())
}
}
Text("Vence \(card.expiryLabel)")
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted)
if !canDelete {
Text("Ao menos um cartão deve permanecer")
.font(AppTypography.caption)
.foregroundStyle(AppColors.textMuted.opacity(0.7))
}
}
Spacer(minLength: 0)
}
.padding(14)
.background(AppColors.surface)
.clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous))
}
private func brandLogoName(for brand: String?) -> String? {
switch brand?.lowercased()
.trimmingCharacters(in: .whitespacesAndNewlines)
.folding(options: .diacriticInsensitive, locale: .current) {
case "visa": return "visacard_logo"
case "mastercard", "master": return "mastercard_logo"
case "amex", "american express", "americanexpress": return "amexcard_logo"
case "hipercard": return "hipercard_logo"
case "alelo": return "alelocard_logo"
case "sodexo": return "sodexo_logo"
default: return nil
}
}
@MainActor
private func loadCards() async {
guard isLoading == false else { return }
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let response = try await ApiService().listCards()
if response.error == false {
cards = response.result ?? []
} else {
errorMessage = response.message ?? "Erro ao carregar cartões."
}
} catch {
errorMessage = "Não foi possível carregar seus cartões."
}
}
private func deleteCard(_ card: SavedCard) {
guard canDelete, deletingCardId == nil else { return }
deletingCardId = card.id
openSwipeRowId = nil
Task {
do {
let response = try await ApiService().deleteCard(cardId: card.id)
if response.error == false {
cards.removeAll { $0.id == card.id }
} else {
SnackbarCenter.shared.show(
title: response.message ?? "Não foi possível excluir o cartão.",
style: .error,
icon: "xmark.octagon.fill",
duration: 3.0
)
}
} catch {
SnackbarCenter.shared.show(
title: "Erro ao excluir cartão.",
style: .error,
icon: "xmark.octagon.fill",
duration: 3.0
)
}
deletingCardId = nil
}
}
}