import SwiftUI #if canImport(UIKit) import UIKit #elseif canImport(AppKit) import AppKit #endif struct CheckoutView: View { @Binding var appState: AppState @State var storeInfo: StoreInfoResult? = nil @State var errorMessage: String? = nil @State var deliveryType: CheckoutDeliveryType = .delivery @State var paymentMethod: CheckoutPaymentMethod = .pix @State var useInAppPayment = true @State var discountValue: Double = 0 @State var baseDeliveryFee: Double? = nil @State var selectedCustomerAddress: CustomerAddress? = nil @State var addressValidationMessage: String? = nil @State var addressValidationBlocked = false @State var isValidatingAddress = false @State var showAddressNotServedAlert = false @State var isRestoringAddress = false @State var lastAcceptedAddressState: AddressState? = nil @State var isSubmittingOrder = false @State var pixPaymentContext: PixPaymentContext? = nil @State var cardPaymentContext: CardPaymentContext? = nil @State var orderTrackingContext: OrderTrackingContext? = nil var paymentConfig: StorePaymentMethodsInfo? { storeInfo?.paymentMethods } var availableDeliveryTypes: [CheckoutDeliveryType] { let deliveryEnabled = paymentConfig?.paymentOnDelivery ?? true let pickupEnabled = paymentConfig?.paymentOnPickup ?? true var values: [CheckoutDeliveryType] = [] if deliveryEnabled { values.append(.delivery) } if pickupEnabled { values.append(.pickup) } return values.isEmpty ? [.delivery, .pickup] : values } var availableInAppPaymentMethods: [CheckoutPaymentMethod] { [.pix, .creditCard] } var availableStoreMachineMethods: [CheckoutPaymentMethod] { var methods: [CheckoutPaymentMethod] = [] if paymentConfig?.acceptCash == true { methods.append(.money) } if paymentConfig?.hasAnyCreditCard == true { methods.append(.creditCard) } if paymentConfig?.hasAnyDebitCard == true { methods.append(.debitCard) } if paymentConfig?.hasAnyVoucher == true { methods.append(.voucher) } return methods } var isDeliveryMode: Bool { deliveryType == .delivery } private var deliveryToggle: Binding { Binding( get: { isDeliveryMode }, set: { isOn in let next: CheckoutDeliveryType = isOn ? .delivery : .pickup if availableDeliveryTypes.contains(next) { deliveryType = next } } ) } private var subtotalValue: Double { appState.cart.items.reduce(0) { $0 + (Double($1.quantity) * $1.unitPrice) } } private var deliveryFeeValue: Double { isDeliveryMode ? (baseDeliveryFee ?? 0) : 0 } var totalValue: Double { max(0, subtotalValue + deliveryFeeValue - discountValue) } private var sectionTitleColor: Color { AppColors.textMuted } var canConfirmPayment: Bool { if isDeliveryMode { if addressValidationBlocked { return false } if baseDeliveryFee == nil { return false } if isValidatingAddress { return false } } if useInAppPayment == false && availableStoreMachineMethods.isEmpty { return false } return true } var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 18) { deliveryTypeSection addressSection orderSummarySection paymentSection if let errorMessage, errorMessage.isEmpty == false { Text(errorMessage) .font(.caption) .foregroundStyle(Color.red) } if let addressValidationMessage, addressValidationMessage.isEmpty == false { Text(addressValidationMessage) .font(.caption) .foregroundStyle(addressValidationBlocked ? Color.red : AppColors.primary) } } .padding(.horizontal, 20) .padding(.top, 16) .padding(.bottom, 10) } .background(AppColors.backgroundLight) .navigationTitle("Finalizar Pedido") .appInlineNavigationTitle() .appBottomSafeAreaInset { bottomBar } .task { await loadStoreInfoIfNeeded() await refreshSelectedCustomerAddress() normalizeSelectedOptions() await validateDeliveryAddressIfNeeded() } .onChange(of: checkoutAddressWatchKey) { _, _ in if isRestoringAddress { return } Task { await refreshSelectedCustomerAddress() await validateDeliveryAddressIfNeeded() } } .onChange(of: deliveryType) { _, _ in Task { await validateDeliveryAddressIfNeeded() } } .alert("Seu endereço não é atendido por esta loja.\nDeseja trocar mesmo assim?", isPresented: $showAddressNotServedAlert) { Button("Não", role: .cancel) { restoreLastAcceptedAddress() } Button("Sim", role: .destructive) { appState.cart.clear() addressValidationBlocked = false addressValidationMessage = nil } } .navigationDestination(item: $pixPaymentContext) { context in PaymentPixView( context: context, onPaymentConfirmed: { appState.cart.clear() SessionStateStore.clearPendingCartOrder() } ) { appState.cart.clear() SessionStateStore.clearPendingCartOrder() orderTrackingContext = OrderTrackingContext(orderId: context.orderId, shortId: context.shortId) } } .navigationDestination(item: $cardPaymentContext) { context in PaymentCardView( context: context, onPaymentConfirmed: { appState.cart.clear() SessionStateStore.clearPendingCartOrder() } ) { appState.cart.clear() SessionStateStore.clearPendingCartOrder() orderTrackingContext = OrderTrackingContext(orderId: context.orderId, shortId: context.shortId) } } .navigationDestination(item: $orderTrackingContext) { context in OrderTrackingView(orderId: context.orderId, initialShortId: context.shortId) } } private var deliveryTypeSection: some View { VStack(alignment: .leading, spacing: 10) { Text("TIPO DE ENTREGA") .font(AppTypography.overline) .foregroundStyle(sectionTitleColor) HStack(spacing: 10) { VStack(alignment: .leading, spacing: 2) { Text(isDeliveryMode ? "Entrega" : "Retirada") .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) } Spacer() Toggle("", isOn: deliveryToggle) .labelsHidden() .tint(AppColors.primary) .disabled(availableDeliveryTypes.count <= 1) } .padding(14) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } private var addressSection: some View { VStack(alignment: .leading, spacing: 10) { Text(addressSectionTitle) .font(AppTypography.overline) .foregroundStyle(sectionTitleColor) HStack(spacing: 12) { Circle() .fill(AppColors.brandSoft) .frame(width: 44, height: 44) .overlay( Image(systemName: "mappin.and.ellipse") .foregroundStyle(AppColors.primary) ) VStack(alignment: .leading, spacing: 4) { Text(isDeliveryMode ? "Casa" : (appState.cart.storeName ?? "Loja")) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) Text(isDeliveryMode ? customerAddressLabel : storeAddressLabel) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) } Spacer() if isDeliveryMode { Button("Alterar") { appState.activeModal = .addressPicker } .font(AppTypography.heading3) .foregroundStyle(AppColors.primary) .buttonStyle(.plain) } } .padding(14) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } private var orderSummarySection: some View { VStack(alignment: .leading, spacing: 10) { Text("RESUMO DO PEDIDO") .font(AppTypography.overline) .foregroundStyle(sectionTitleColor) VStack(alignment: .leading, spacing: 12) { ForEach(appState.cart.items) { item in HStack(alignment: .center, spacing: 10) { Text("\(item.quantity)x") .font(AppTypography.heading3) .foregroundStyle(AppColors.textMuted) .padding(.horizontal, 10) .padding(.vertical, 6) .background(AppColors.backgroundLight) .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) VStack(alignment: .leading, spacing: 2) { Text(item.name) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) if let details = item.details, details.isEmpty == false { Text(details) .font(.caption) .foregroundStyle(AppColors.textMuted) .lineLimit(1) } } Spacer() Text(formatCurrency(Double(item.quantity) * item.unitPrice)) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) } } Divider() .padding(.vertical, 4) summaryRow("Subtotal", formatCurrency(subtotalValue)) summaryRow("Taxa de entrega", deliveryFeeLabel) summaryRow("Desconto", "-\(formatCurrency(discountValue))", valueColor: Color.green) } .padding(14) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } private var paymentSection: some View { VStack(alignment: .leading, spacing: 10) { Text("MÉTODO DE PAGAMENTO") .font(AppTypography.overline) .foregroundStyle(sectionTitleColor) VStack(alignment: .leading, spacing: 12) { paymentGroupCard( title: "Pagar Pelo App", subtitle: "Mais rápido e seguro", isSelected: useInAppPayment ) { VStack(spacing: 0) { ForEach(availableInAppPaymentMethods, id: \.rawValue) { method in paymentRow(method, isInAppGroup: true) if method != availableInAppPaymentMethods.last { Divider() } } } } onTap: { useInAppPayment = true if availableInAppPaymentMethods.contains(paymentMethod) == false { paymentMethod = .pix } } paymentGroupCard( title: "Pagar Na Maquininha Da Loja", subtitle: "Pague na entrega/retirada com os métodos aceitos pela loja", isSelected: useInAppPayment == false ) { if availableStoreMachineMethods.isEmpty { Text("Loja não informou métodos presenciais.") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .padding(.horizontal, 14) .padding(.bottom, 14) } else { VStack(spacing: 0) { ForEach(availableStoreMachineMethods, id: \.rawValue) { method in paymentRow(method, isInAppGroup: false) if method != availableStoreMachineMethods.last { Divider() } } } } } onTap: { guard availableStoreMachineMethods.isEmpty == false else { return } useInAppPayment = false if availableStoreMachineMethods.contains(paymentMethod) == false, let first = availableStoreMachineMethods.first { paymentMethod = first } } } } } private func paymentRow(_ method: CheckoutPaymentMethod, isInAppGroup: Bool) -> some View { let subtitle = paymentSubtitle(for: method, isInAppGroup: isInAppGroup) return Button { paymentMethod = method } label: { HStack(spacing: 12) { RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(AppColors.backgroundLight) .frame(width: 54, height: 54) .overlay( Image(systemName: method.iconName) .font(.system(size: 20, weight: .semibold)) .foregroundStyle(method == .pix ? Color.green : AppColors.textPrimary) ) VStack(alignment: .leading, spacing: 3) { Text(method.label) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) if let subtitle { Text(subtitle) .font(.caption) .foregroundStyle(AppColors.textMuted) } } Spacer() Circle() .stroke(paymentMethod == method ? AppColors.tertiary : AppColors.textMuted.opacity(0.45), lineWidth: 2) .frame(width: 24, height: 24) .background( Circle() .fill(paymentMethod == method ? AppColors.tertiary : Color.clear) ) } .padding(14) } .buttonStyle(.plain) } private func paymentSubtitle(for method: CheckoutPaymentMethod, isInAppGroup: Bool) -> String? { if isInAppGroup == false { if method == .pix { return "Pagamento presencial (QR da loja)" } return nil } return method.subtitle } private func paymentGroupCard( title: String, subtitle: String, isSelected: Bool, @ViewBuilder content: () -> Content, onTap: @escaping () -> Void ) -> some View { VStack(alignment: .leading, spacing: 10) { HStack(spacing: 10) { VStack(alignment: .leading, spacing: 2) { Text(title) .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) Text(subtitle) .font(.caption) .foregroundStyle(AppColors.textMuted) } Spacer() Circle() .stroke(isSelected ? AppColors.tertiary : AppColors.textMuted.opacity(0.45), lineWidth: 2) .frame(width: 22, height: 22) .background( Circle() .fill(isSelected ? AppColors.tertiary : Color.clear) ) } .padding(.horizontal, 14) .padding(.top, 14) .appContentShape(Rectangle()) .onTapGesture(perform: onTap) content() .allowsHitTesting(isSelected) .opacity(isSelected ? 1 : 0.82) } .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } private var bottomBar: some View { VStack(spacing: 12) { HStack { Text("Total a pagar") .font(AppTypography.heading3) .foregroundStyle(AppColors.textMuted) Spacer() Text(formatCurrency(totalValue)) .font(AppTypography.heading1) .foregroundStyle(AppColors.textPrimary) } Button { Task { await handleConfirmPaymentTap() } } label: { HStack(spacing: 10) { Text("Confirmar e Pagar") .font(AppTypography.heading2) Image(systemName: "checkmark") .font(.system(size: 18, weight: .bold)) } .foregroundStyle(AppColors.textInverse) .frame(maxWidth: .infinity, minHeight: 54) .background(AppColors.primary) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } .buttonStyle(.plain) .disabled(canConfirmPayment == false || isValidatingAddress || isSubmittingOrder) .opacity((canConfirmPayment && isValidatingAddress == false && isSubmittingOrder == false) ? 1 : 0.65) } .padding(.horizontal, 20) .padding(.top, 10) .padding(.bottom, UIDevice.bottomNotch + 40) .background(AppColors.surface.opacity(0.98)) } private func summaryRow(_ title: String, _ value: String, valueColor: Color? = nil) -> some View { HStack { Text(title) .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) Spacer() Text(value) .font(AppTypography.heading3) .foregroundStyle(valueColor ?? AppColors.textMuted) } } private var customerAddressLabel: String { if let address = selectedCustomerAddress { let street = (address.address ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let number = (address.number ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let neighborhood = (address.neighborhood ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let city = (address.city ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let state = (address.state ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let firstLine = [street, number].filter { $0.isEmpty == false }.joined(separator: ", ") let secondLine = [neighborhood, city, state].filter { $0.isEmpty == false }.joined(separator: " - ") let joined = [firstLine, secondLine].filter { $0.isEmpty == false }.joined(separator: "\n") if joined.isEmpty == false { return joined } } let value = appState.address.display.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? "Defina seu endereço" : value } private var addressSectionTitle: String { if isDeliveryMode { return "ENDEREÇO DE ENTREGA" } let storeName = (appState.cart.storeName ?? "LOJA") .trimmingCharacters(in: .whitespacesAndNewlines) .uppercased() return "ENDEREÇO DE \(storeName)" } private var storeAddressLabel: String { guard let address = storeInfo?.address else { return "Endereço da loja indisponível" } let street = (address.street ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let number = (address.number ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let neighborhood = (address.neighborhood ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let city = (address.city ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let state = (address.state ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let firstLine = [street, number].filter { $0.isEmpty == false }.joined(separator: ", ") let secondLine = [neighborhood, city, state].filter { $0.isEmpty == false }.joined(separator: " - ") let joined = [firstLine, secondLine].filter { $0.isEmpty == false }.joined(separator: "\n") return joined.isEmpty ? "Endereço da loja indisponível" : joined } private var deliveryFeeLabel: String { if isDeliveryMode == false { return formatCurrency(0) } if let baseDeliveryFee { return formatCurrency(baseDeliveryFee) } return "Calculando..." } } struct PixPaymentContext: Identifiable, Hashable { let id: String let orderId: String let shortId: String? let copyPaste: String let qrCodeImageBase64: String? let expirationDate: String? } struct OrderTrackingContext: Identifiable, Hashable { var id: String { orderId } let orderId: String let shortId: String? } struct CardPaymentContext: Identifiable, Hashable { var id: String { orderId } let orderId: String let shortId: String? let total: Double } struct PaymentPixView: View { let context: PixPaymentContext var onPaymentConfirmed: (() -> Void)? = nil var onOpenTracking: (() -> Void)? = nil @Environment(\.dismiss) var dismiss @State var tracker = OrderRealtimeTracker() @State var latestOrder: PublicOrderResult? = nil @State var hasOpenedTracking = false @State var hasShownPixExpiredSnackbar = false @State var currentTime = Date() private var qrImageSource: String? { guard let raw = context.qrCodeImageBase64?.trimmingCharacters(in: .whitespacesAndNewlines), raw.isEmpty == false else { return nil } if raw.lowercased().hasPrefix("data:image") { return raw } return "data:image/png;base64,\(raw)" } var body: some View { ScrollView(showsIndicators: false) { VStack(spacing: 14) { RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) .fill(AppColors.surface) .overlay( VStack(spacing: 10) { Text("Escaneie o QR Code abaixo ou copie o código para pagar no seu aplicativo do banco.") .font(AppTypography.body) .foregroundStyle(AppColors.textMuted) .multilineTextAlignment(.center) .padding(.horizontal, 12) .padding(.top, 14) AsyncStoreImage(imageURL: qrImageSource) .frame(width: 220, height: 220) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .stroke(AppColors.tertiary.opacity(0.35), lineWidth: 2) ) Text("AGUARDANDO PAGAMENTO") .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) .padding(.horizontal, 20) .padding(.vertical, 8) .background(AppColors.brandSoft) .clipShape(Capsule()) } ) .frame(maxWidth: .infinity, minHeight: 380) Text("Código PIX") .font(AppTypography.heading3) .foregroundStyle(AppColors.textPrimary) RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) .fill(AppColors.surface) .frame(height: 20) .overlay( VStack(spacing: 8) { Text(context.copyPaste) .font(.system(size: 12, weight: .medium, design: .monospaced)) .foregroundStyle(AppColors.textMuted) .lineLimit(0) .multilineTextAlignment(.center) .padding(.horizontal, 10) } .padding(.vertical, 14) ) if let expirationDate = context.expirationDate, expirationDate.isEmpty == false { Text(expirationLabel) .font(.caption) .foregroundStyle(isPixExpired ? Color.red : AppColors.textMuted) } PrimaryButton(title: "Copiar Código PIX") { if isPixExpired { showPixExpiredSnackbar() return } copyToClipboard(context.copyPaste) SnackbarCenter.shared.show(title: "Código PIX copiado.", style: .success, icon: "doc.on.doc.fill", duration: 2.0) } .disabled(isPixExpired) .opacity(isPixExpired ? 0.5 : 1.0) .padding(.top, 10) } .padding(20) } .background(AppColors.backgroundLight) .navigationTitle("Pagamento via PIX") .appInlineNavigationTitle() .task { tracker.onOrderUpdated = { updated in latestOrder = updated if updated.isPaymentConfirmed { onPaymentConfirmed?() openTrackingOnce() } } tracker.start(orderId: context.orderId, jwt: DefaultTokenStore().jwt) } .task { while Task.isCancelled == false { currentTime = Date() if isPixExpired { showPixExpiredSnackbar() return } try? await Task.sleep(nanoseconds: 1_000_000_000) } } .onDisappear { tracker.stop() } } private func openTrackingOnce() { guard hasOpenedTracking == false else { return } hasOpenedTracking = true onOpenTracking?() } private func copyToClipboard(_ value: String) { #if canImport(UIKit) UIPasteboard.general.string = value #elseif canImport(AppKit) NSPasteboard.general.clearContents() NSPasteboard.general.setString(value, forType: .string) #endif } private var parsedExpirationDate: Date? { let raw = (context.expirationDate ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard raw.isEmpty == false else { return nil } let iso = ISO8601DateFormatter() iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = iso.date(from: raw) { return date } iso.formatOptions = [.withInternetDateTime] if let date = iso.date(from: raw) { return date } let formatter = DateFormatter() formatter.locale = Locale(identifier: "pt_BR") let formats = [ "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", "dd/MM/yyyy HH:mm:ss", "dd/MM/yyyy HH:mm" ] for format in formats { formatter.dateFormat = format if let date = formatter.date(from: raw) { return date } } return nil } private var isPixExpired: Bool { guard let parsedExpirationDate else { return false } return currentTime >= parsedExpirationDate } private var expirationLabel: String { guard let parsedExpirationDate else { return "Expira em: --" } if isPixExpired { return "Expirado" } let remaining = max(0, Int(parsedExpirationDate.timeIntervalSince(currentTime))) let day = 24 * 60 * 60 let hour = 60 * 60 if remaining >= day { let days = remaining / day return "Expira em: \(days) dia(s)" } if remaining >= hour { let hours = remaining / hour return "Expira em: \(hours) hora(s)" } if remaining >= 60 { let minutes = remaining / 60 return "Expira em: \(minutes) min" } return "Vai expirar em \(remaining) segundos" } private func showPixExpiredSnackbar() { guard hasShownPixExpiredSnackbar == false else { return } hasShownPixExpiredSnackbar = true SnackbarCenter.shared.show( title: "PIX expirou. Gere um novo pedido para continuar.", style: .warning, icon: "clock.badge.xmark.fill", duration: 4.0 ) } } struct PaymentCardView: View { let context: CardPaymentContext var onPaymentConfirmed: (() -> Void)? = nil var onOpenTracking: (() -> Void)? = nil @Environment(\.dismiss) var dismiss @State var cardHolderName = "" @State var cardNumber = "" @State var expiry = "" @State var cvv = "" @State var tracker = OrderRealtimeTracker() @State var latestOrder: PublicOrderResult? = nil @State var hasOpenedTracking = false var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 14) { RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous) .fill(AppColors.surface) .overlay( HStack { VStack(alignment: .leading, spacing: 4) { Text("Total do Pedido") .font(AppTypography.caption) .foregroundStyle(AppColors.textMuted) Text(formatCurrency(context.total)) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) } Spacer() } .padding(14) ) .frame(height: 88) Text("Dados do Cartão") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) VStack(spacing: 10) { labeledField("Número do Cartão", placeholder: "0000 0000 0000 0000", text: $cardNumber) labeledField("Nome no Cartão", placeholder: "Como impresso no cartão", text: $cardHolderName) HStack(spacing: 10) { labeledField("Validade", placeholder: "MM/AA", text: $expiry) labeledField("CVV", placeholder: "•••", text: $cvv) } } .padding(14) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) PrimaryButton(title: "Salvar e Pagar") { if latestOrder?.isPaymentConfirmed == true { onPaymentConfirmed?() openTrackingOnce() } else { SnackbarCenter.shared.show(title: "Pagamento ainda não confirmado.", style: .info, icon: "clock.fill", duration: 2.0) } } Button("Apenas Pagar") { if latestOrder?.isPaymentConfirmed == true { onPaymentConfirmed?() openTrackingOnce() } else { SnackbarCenter.shared.show(title: "Aguardando confirmação do pagamento.", style: .info, icon: "clock.fill", duration: 2.0) } } .font(AppTypography.heading3) .foregroundStyle(AppColors.textMuted) .frame(maxWidth: .infinity, minHeight: 48) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) .buttonStyle(.plain) } .padding(20) } .background(AppColors.backgroundLight) .navigationTitle("Pagamento") .appInlineNavigationTitle() .task { tracker.onOrderUpdated = { updated in latestOrder = updated if updated.isPaymentConfirmed { onPaymentConfirmed?() openTrackingOnce() } } tracker.start(orderId: context.orderId, jwt: DefaultTokenStore().jwt) } .onDisappear { tracker.stop() } } private func openTrackingOnce() { guard hasOpenedTracking == false else { return } hasOpenedTracking = true onOpenTracking?() } private func labeledField(_ label: String, placeholder: String, text: Binding) -> 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)) } } private func formatCurrency(_ value: Double) -> String { String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",") } }