This commit is contained in:
Daniel Arantes Loverde
2026-02-15 08:18:06 -03:00
parent d7b8b1864a
commit 0f5ad4c268
35 changed files with 4090 additions and 309 deletions

View File

@@ -0,0 +1,335 @@
import SwiftUI
extension CheckoutView {
enum CheckoutPayloadValidationError: LocalizedError {
case emptyCart
case missingCustomerName
case missingCustomerEmail
case missingCustomerPhone
case missingAddressStreet
case missingAddressNumber
case missingAddressNeighborhood
var errorDescription: String? {
switch self {
case .emptyCart: return "Carrinho vazio."
case .missingCustomerName: return "Nome do cliente não informado."
case .missingCustomerEmail: return "Email do cliente não informado."
case .missingCustomerPhone: return "Telefone do cliente não informado."
case .missingAddressStreet: return "Rua do endereço não informada."
case .missingAddressNumber: return "Número do endereço não informado."
case .missingAddressNeighborhood: return "Bairro do endereço não informado."
}
}
}
var checkoutAddressWatchKey: String {
let selectedId = appState.address.selectedId ?? "nil"
let display = appState.address.display.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let lat = appState.address.latitude.map { String(format: "%.5f", $0) } ?? "nil"
let lng = appState.address.longitude.map { String(format: "%.5f", $0) } ?? "nil"
return "\(selectedId)|\(display)|\(lat)|\(lng)"
}
func formatCurrency(_ value: Double) -> String {
String(format: "R$ %.2f", value).replacingOccurrences(of: ".", with: ",")
}
@MainActor
func loadStoreInfoIfNeeded() async {
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else { return }
do {
let response = try await ApiService().storeInfo(storeId: storeId)
if response.error {
errorMessage = response.message ?? "Não foi possível carregar opções de checkout."
return
}
storeInfo = response.result
errorMessage = nil
} catch {
errorMessage = "Não foi possível carregar opções de checkout."
}
}
@MainActor
func refreshSelectedCustomerAddress() async {
do {
let response = try await ApiService().profile()
if let customer = response.result {
appState.profile.id = customer.id
appState.profile.name = customer.name
appState.profile.email = customer.email
if let phoneNumber = customer.phoneNumber, phoneNumber.isEmpty == false {
appState.profile.phone = phoneNumber
}
SessionStateStore.setActiveUserKey(
SessionStateStore.makeUserKey(profileId: customer.id, email: customer.email)
)
}
let addresses = response.result?.addressBook ?? []
if let selectedId = appState.address.selectedId, selectedId.isEmpty == false {
selectedCustomerAddress = addresses.first(where: { $0.id == selectedId })
} else {
selectedCustomerAddress = nil
}
if selectedCustomerAddress == nil {
let display = appState.address.display
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased()
if display.isEmpty == false, display != "defina seu endereco" {
selectedCustomerAddress = addresses.first {
($0.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == display
}
}
}
if selectedCustomerAddress == nil {
selectedCustomerAddress = addresses.first
}
if let selected = selectedCustomerAddress {
appState.address.selectedId = selected.id
let label = (selected.label ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
if label.isEmpty == false {
appState.address.display = label
}
if let lat = selected.latLong?.first, let lng = selected.latLong?.dropFirst().first {
appState.address.latitude = lat
appState.address.longitude = lng
}
SessionStateStore.saveAddress(appState.address)
}
} catch {
selectedCustomerAddress = nil
}
}
@MainActor
func validateDeliveryAddressIfNeeded() async {
guard isDeliveryMode else {
addressValidationBlocked = false
addressValidationMessage = nil
baseDeliveryFee = nil
return
}
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else { return }
baseDeliveryFee = nil
let payload = ValidateDeliveryAddressPayload(
address: ValidateDeliveryAddressDataPayload(
street: selectedCustomerAddress?.address,
number: selectedCustomerAddress?.number,
neighborhood: selectedCustomerAddress?.neighborhood,
city: selectedCustomerAddress?.city,
state: selectedCustomerAddress?.state,
zip: selectedCustomerAddress?.zipCode,
lat: appState.address.latitude,
lng: appState.address.longitude
)
)
isValidatingAddress = true
defer { isValidatingAddress = false }
do {
let response = try await ApiService().validateDeliveryAddress(storeId: storeId, payload: payload)
if response.error {
addressValidationBlocked = true
addressValidationMessage = response.message ?? "Não foi possível validar o endereço de entrega."
baseDeliveryFee = nil
return
}
let result = response.result
let allowed = result?.deliveryAllowed ?? false
addressValidationBlocked = allowed == false
addressValidationMessage = result?.reasonMessage
if allowed {
lastAcceptedAddressState = appState.address
} else {
showAddressNotServedAlert = true
baseDeliveryFee = nil
}
if allowed {
if let fee = result?.deliveryFee {
baseDeliveryFee = fee
} else {
addressValidationBlocked = true
addressValidationMessage = "Não foi possível calcular a taxa de entrega para este endereço."
baseDeliveryFee = nil
}
}
} catch {
addressValidationBlocked = true
addressValidationMessage = "Não foi possível validar o endereço de entrega."
baseDeliveryFee = nil
}
}
func normalizeSelectedOptions() {
if availableDeliveryTypes.contains(deliveryType) == false,
let first = availableDeliveryTypes.first {
deliveryType = first
}
if useInAppPayment {
if availableInAppPaymentMethods.contains(paymentMethod) == false,
let first = availableInAppPaymentMethods.first {
paymentMethod = first
}
} else {
if availableStoreMachineMethods.contains(paymentMethod) == false,
let first = availableStoreMachineMethods.first {
paymentMethod = first
}
}
if lastAcceptedAddressState == nil {
lastAcceptedAddressState = appState.address
}
}
func restoreLastAcceptedAddress() {
guard let snapshot = lastAcceptedAddressState else { return }
isRestoringAddress = true
appState.address = snapshot
SessionStateStore.saveAddress(snapshot)
Task { @MainActor in
await refreshSelectedCustomerAddress()
addressValidationBlocked = false
addressValidationMessage = nil
isRestoringAddress = false
}
}
@MainActor
func handleConfirmPaymentTap() async {
guard canConfirmPayment else { return }
if useInAppPayment == false {
SnackbarCenter.shared.show(title: "Pagamento presencial selecionado.", style: .info, icon: "creditcard.fill", duration: 2.0)
return
}
if paymentMethod == .creditCard {
openCardPayment = true
return
}
guard paymentMethod == .pix else {
SnackbarCenter.shared.show(title: "Método de pagamento não suportado no app.", style: .warning, icon: "exclamationmark.triangle.fill", duration: 2.0)
return
}
guard let storeId = appState.cart.storeId, storeId.isEmpty == false else {
SnackbarCenter.shared.show(title: "Loja do pedido não encontrada.", style: .error, icon: "xmark.octagon.fill", duration: 2.0)
return
}
await refreshSelectedCustomerAddress()
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: .pix)
guard case .success(let payload) = payloadBuildResult else {
let message: String
if case .failure(let reason) = payloadBuildResult {
message = reason.localizedDescription
} else {
message = "Dados do pedido incompletos."
}
SnackbarCenter.shared.show(title: message, style: .error, icon: "xmark.octagon.fill", duration: 2.0)
return
}
isSubmittingOrder = true
defer { isSubmittingOrder = false }
do {
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
if response.error {
SnackbarCenter.shared.show(
title: response.message ?? "Não foi possível gerar o pagamento PIX.",
style: .error,
icon: "xmark.octagon.fill",
duration: 3.0
)
return
}
guard let result = response.result else {
SnackbarCenter.shared.show(title: "Resposta de pagamento inválida.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
return
}
let pixPayload = result.payment?.pix ?? result.paymentPayload
guard let copyPaste = pixPayload?.copyPaste, copyPaste.isEmpty == false else {
SnackbarCenter.shared.show(title: "Código PIX não retornado pela API.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
return
}
let orderId = result.id ?? UUID().uuidString
pixPaymentContext = PixPaymentContext(
id: orderId,
orderId: orderId,
shortId: result.shortId,
copyPaste: copyPaste,
qrCodeImageBase64: pixPayload?.qrCodeImage,
expirationDate: pixPayload?.expirationDate
)
} catch {
SnackbarCenter.shared.show(title: "Não foi possível gerar o pagamento PIX.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
}
}
func buildCreateOrderPayload(paymentMethod: CheckoutPaymentMethod) -> Result<CreateOrderPayload, CheckoutPayloadValidationError> {
guard appState.cart.items.isEmpty == false else { return .failure(.emptyCart) }
let profileName = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
let profileEmail = appState.profile.email.trimmingCharacters(in: .whitespacesAndNewlines)
let profilePhone = appState.profile.phone.trimmingCharacters(in: .whitespacesAndNewlines)
guard profileName.isEmpty == false else { return .failure(.missingCustomerName) }
guard profileEmail.isEmpty == false else { return .failure(.missingCustomerEmail) }
guard profilePhone.isEmpty == false else { return .failure(.missingCustomerPhone) }
let addressPayload: CreateOrderAddressPayload?
if isDeliveryMode {
let street = selectedCustomerAddress?.address?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let number = selectedCustomerAddress?.number?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let neighborhood = selectedCustomerAddress?.neighborhood?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard street.isEmpty == false else { return .failure(.missingAddressStreet) }
guard number.isEmpty == false else { return .failure(.missingAddressNumber) }
guard neighborhood.isEmpty == false else { return .failure(.missingAddressNeighborhood) }
addressPayload = CreateOrderAddressPayload(
street: street,
number: number,
neighborhood: neighborhood,
city: selectedCustomerAddress?.city,
state: selectedCustomerAddress?.state,
zip: selectedCustomerAddress?.zipCode,
complement: selectedCustomerAddress?.complement
)
} else {
addressPayload = nil
}
return .success(
CreateOrderPayload(
customer: CreateOrderCustomerPayload(
name: profileName,
phone: profilePhone,
email: profileEmail,
asaasId: nil
),
items: appState.cart.toOrderItemsPayload(),
total: totalValue,
paymentMethod: paymentMethod.rawValue,
deliveryType: deliveryType.rawValue,
address: addressPayload
)
)
}
}