feat(cards): implement full saved card management and transparent checkout
- ApiCardModels.swift: SavedCard, SaveCardPayload, CreditCardOrderPayload, etc. - ApiOrderModels.swift: add savedCardId, clientCpfCnpj, creditCard, creditCardHolderInfo to CreateOrderPayload; Codable for item/addon payloads - ApiService.swift: listCards, saveCard, updateCard, deleteCard, updateProfileCpf - AppState.ProfileState: add cpf field - CheckoutView: CardSelectionSheet (list saved cards + add new), rewrite PaymentCardView (submits order with card data), CardPaymentContext carries full order data - CheckoutView+Logic: buildCreateOrderPayload accepts card params, new loadSavedCards/confirmOrderWithSavedCard/handleOrderResponse helpers, redirect credit card confirm to CardSelectionSheet - OrdersView: CREDIT_CARD pending orders go to tracking (not card form, since card is now submitted with order) - UserProfileView: CPF field with mask, save via PATCH /api/customer/profile
This commit is contained in:
@@ -234,7 +234,13 @@ extension CheckoutView {
|
||||
return
|
||||
}
|
||||
|
||||
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: effectivePaymentMethod)
|
||||
// Crédito pelo app → abre seleção de cartão antes de criar pedido
|
||||
if useInAppPayment && effectivePaymentMethod == .creditCard {
|
||||
showCardSelectionSheet = true
|
||||
return
|
||||
}
|
||||
|
||||
let payloadBuildResult = buildCreateOrderPayload(paymentMethod: effectivePaymentMethod, savedCardId: nil)
|
||||
guard case .success(let payload) = payloadBuildResult else {
|
||||
let message: String
|
||||
if case .failure(let reason) = payloadBuildResult {
|
||||
@@ -251,83 +257,31 @@ extension CheckoutView {
|
||||
|
||||
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 criar o pedido.",
|
||||
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 orderSnapshot = result.asPublicOrderResult()
|
||||
SessionStateStore.saveTrackedOrder(orderSnapshot)
|
||||
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
let isInAppMethod = availableInAppPaymentMethods.contains(effectivePaymentMethod)
|
||||
if useInAppPayment == false || isInAppMethod == false {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
if response.error == false, let result = response.result {
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: response.message ?? "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if orderSnapshot.isPaymentConfirmed {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
return
|
||||
}
|
||||
|
||||
SessionStateStore.savePendingCartOrderId(orderId)
|
||||
|
||||
if effectivePaymentMethod == .creditCard {
|
||||
cardPaymentContext = CardPaymentContext(
|
||||
orderId: orderId,
|
||||
shortId: result.shortId,
|
||||
total: totalValue
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let pixFromPayment = result.payment?.pix
|
||||
let pixFromPayload = result.paymentPayload
|
||||
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste
|
||||
: pixFromPayload?.copyPaste
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage
|
||||
: pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
|
||||
guard let copyPaste, copyPaste.trimmingCharacters(in: .whitespacesAndNewlines).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
|
||||
}
|
||||
|
||||
pixPaymentContext = PixPaymentContext(
|
||||
id: orderId,
|
||||
orderId: orderId,
|
||||
shortId: result.shortId,
|
||||
copyPaste: copyPaste,
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
)
|
||||
handleOrderResponse(response, effectivePaymentMethod: effectivePaymentMethod)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
}
|
||||
|
||||
func buildCreateOrderPayload(paymentMethod: CheckoutPaymentMethod) -> Result<CreateOrderPayload, CheckoutPayloadValidationError> {
|
||||
func buildCreateOrderPayload(
|
||||
paymentMethod: CheckoutPaymentMethod,
|
||||
savedCardId: String? = nil,
|
||||
creditCard: CreditCardOrderPayload? = nil,
|
||||
creditCardHolderInfo: SaveCardHolderInfoPayload? = nil,
|
||||
clientCpfCnpj: String? = nil
|
||||
) -> Result<CreateOrderPayload, CheckoutPayloadValidationError> {
|
||||
guard appState.cart.items.isEmpty == false else { return .failure(.emptyCart) }
|
||||
|
||||
let profileName = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -360,18 +314,96 @@ extension CheckoutView {
|
||||
|
||||
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
|
||||
customer: CreateOrderCustomerPayload(
|
||||
name: profileName,
|
||||
phone: profilePhone,
|
||||
email: profileEmail,
|
||||
asaasId: nil
|
||||
),
|
||||
items: appState.cart.toOrderItemsPayload(),
|
||||
total: totalValue,
|
||||
paymentMethod: paymentMethod.rawValue,
|
||||
deliveryType: deliveryType.rawValue,
|
||||
address: addressPayload,
|
||||
savedCardId: savedCardId,
|
||||
clientCpfCnpj: clientCpfCnpj,
|
||||
creditCard: creditCard,
|
||||
creditCardHolderInfo: creditCardHolderInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadSavedCards() async {
|
||||
do {
|
||||
let response = try await ApiService().listCards()
|
||||
if response.error == false, let cards = response.result {
|
||||
savedCards = cards
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func confirmOrderWithSavedCard(cardId: String, storeId: String) async {
|
||||
let payloadResult = buildCreateOrderPayload(paymentMethod: .creditCard, savedCardId: cardId)
|
||||
guard case .success(let payload) = payloadResult else { return }
|
||||
|
||||
isSubmittingOrder = true
|
||||
defer { isSubmittingOrder = false }
|
||||
|
||||
do {
|
||||
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
|
||||
handleOrderResponse(response, effectivePaymentMethod: .creditCard)
|
||||
} catch {
|
||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
||||
}
|
||||
}
|
||||
|
||||
func handleOrderResponse(_ response: ApiEnvelope<CreateOrderResult>, effectivePaymentMethod: CheckoutPaymentMethod) {
|
||||
if response.error {
|
||||
SnackbarCenter.shared.show(
|
||||
title: response.message ?? "Não foi possível criar o pedido.",
|
||||
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 orderSnapshot = result.asPublicOrderResult()
|
||||
SessionStateStore.saveTrackedOrder(orderSnapshot)
|
||||
let orderId = result.id ?? UUID().uuidString
|
||||
|
||||
if orderSnapshot.isPaymentConfirmed {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
return
|
||||
}
|
||||
|
||||
SessionStateStore.savePendingCartOrderId(orderId)
|
||||
|
||||
let pixFromPayment = result.payment?.pix
|
||||
let pixFromPayload = result.paymentPayload
|
||||
let copyPaste = (pixFromPayment?.copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.copyPaste : pixFromPayload?.copyPaste
|
||||
let qrCodeImage = (pixFromPayment?.qrCodeImage?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.qrCodeImage : pixFromPayload?.qrCodeImage
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate : pixFromPayload?.expirationDate
|
||||
|
||||
if let copyPaste, copyPaste.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
|
||||
pixPaymentContext = PixPaymentContext(
|
||||
id: orderId, orderId: orderId, shortId: result.shortId,
|
||||
copyPaste: copyPaste, qrCodeImageBase64: qrCodeImage, expirationDate: expirationDate
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user