Merge branch 'feature/card-management' into feature/card-brand-logo
# Conflicts: # pedi-foods/Sources/PediFoods/Views/Main/CheckoutView.swift
This commit is contained in:
83
pedi-foods/Sources/PediFoods/Services/ApiCardModels.swift
Normal file
83
pedi-foods/Sources/PediFoods/Services/ApiCardModels.swift
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct SavedCard: Decodable, Identifiable, Hashable {
|
||||||
|
let id: String
|
||||||
|
let nickname: String?
|
||||||
|
let holderName: String
|
||||||
|
let last4: String
|
||||||
|
let brand: String?
|
||||||
|
let expiryMonth: String
|
||||||
|
let expiryYear: String
|
||||||
|
let isDefault: Bool
|
||||||
|
|
||||||
|
var displayLabel: String {
|
||||||
|
if let nickname, nickname.isEmpty == false { return nickname }
|
||||||
|
let brandLabel = (brand ?? "Cartão").capitalized
|
||||||
|
return "\(brandLabel) •••• \(last4)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiryLabel: String { "\(expiryMonth)/\(expiryYear)" }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaveCardCreditCardPayload: Encodable {
|
||||||
|
let holderName: String
|
||||||
|
let number: String
|
||||||
|
let expiryMonth: String
|
||||||
|
let expiryYear: String
|
||||||
|
let ccv: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaveCardHolderInfoPayload: Encodable {
|
||||||
|
let name: String
|
||||||
|
let email: String
|
||||||
|
let cpfCnpj: String
|
||||||
|
let postalCode: String
|
||||||
|
let addressNumber: String
|
||||||
|
let phone: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SaveCardPayload: Encodable {
|
||||||
|
let creditCard: SaveCardCreditCardPayload
|
||||||
|
let creditCardHolderInfo: SaveCardHolderInfoPayload
|
||||||
|
let nickname: String?
|
||||||
|
let isDefault: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdateCardPayload: Encodable {
|
||||||
|
let nickname: String?
|
||||||
|
let isDefault: Bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SavedCardResult: Decodable {
|
||||||
|
let id: String
|
||||||
|
let holderName: String
|
||||||
|
let last4: String
|
||||||
|
let brand: String?
|
||||||
|
let expiryMonth: String
|
||||||
|
let expiryYear: String
|
||||||
|
let isDefault: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreditCardOrderPayload: Encodable {
|
||||||
|
let holderName: String
|
||||||
|
let number: String
|
||||||
|
let expiryMonth: String
|
||||||
|
let expiryYear: String
|
||||||
|
let ccv: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChangePaymentMethodPayload: Encodable {
|
||||||
|
let paymentMethod: String
|
||||||
|
let clientCpfCnpj: String?
|
||||||
|
let creditCard: CreditCardOrderPayload?
|
||||||
|
let creditCardHolderInfo: SaveCardHolderInfoPayload?
|
||||||
|
let savedCardId: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChangePaymentMethodResult: Decodable {
|
||||||
|
let paymentMethod: String?
|
||||||
|
let paymentLocation: String?
|
||||||
|
let paymentId: String?
|
||||||
|
let paymentPayload: CreateOrderPaymentPayload?
|
||||||
|
let payment: CreateOrderPaymentInfo?
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@ struct CreateOrderPayload: Encodable {
|
|||||||
let paymentMethod: String
|
let paymentMethod: String
|
||||||
let deliveryType: String
|
let deliveryType: String
|
||||||
let address: CreateOrderAddressPayload?
|
let address: CreateOrderAddressPayload?
|
||||||
|
// Cartão salvo
|
||||||
|
let savedCardId: String?
|
||||||
|
// Novo cartão (checkout transparente)
|
||||||
|
let clientCpfCnpj: String?
|
||||||
|
let creditCard: CreditCardOrderPayload?
|
||||||
|
let creditCardHolderInfo: SaveCardHolderInfoPayload?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateOrderCustomerPayload: Encodable {
|
struct CreateOrderCustomerPayload: Encodable {
|
||||||
@@ -16,7 +22,7 @@ struct CreateOrderCustomerPayload: Encodable {
|
|||||||
let asaasId: String?
|
let asaasId: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateOrderItemPayload: Encodable {
|
struct CreateOrderItemPayload: Codable {
|
||||||
let productId: String
|
let productId: String
|
||||||
let name: String
|
let name: String
|
||||||
let qty: Int
|
let qty: Int
|
||||||
@@ -24,7 +30,7 @@ struct CreateOrderItemPayload: Encodable {
|
|||||||
let addons: [CreateOrderAddonPayload]
|
let addons: [CreateOrderAddonPayload]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateOrderAddonPayload: Encodable {
|
struct CreateOrderAddonPayload: Codable {
|
||||||
let addonId: String
|
let addonId: String
|
||||||
let name: String
|
let name: String
|
||||||
let qty: Int
|
let qty: Int
|
||||||
|
|||||||
@@ -426,6 +426,49 @@ final class ApiService {
|
|||||||
)
|
)
|
||||||
return try await sendEnvelope(req)
|
return try await sendEnvelope(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Cards
|
||||||
|
|
||||||
|
func listCards() async throws -> ApiEnvelope<[SavedCard]> {
|
||||||
|
let req = ApiRequest(path: "/api/customer/cards", method: "GET", module: .customer, requiresAuth: true)
|
||||||
|
return try await sendEnvelope(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveCard(payload: SaveCardPayload) async throws -> ApiEnvelope<SavedCardResult> {
|
||||||
|
let body = try JSONEncoder().encode(payload)
|
||||||
|
let req = ApiRequest(path: "/api/customer/cards", method: "POST", module: .customer, requiresAuth: true, body: body)
|
||||||
|
return try await sendEnvelope(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCard(cardId: String, payload: UpdateCardPayload) async throws -> ApiEnvelope<SavedCardResult> {
|
||||||
|
let body = try JSONEncoder().encode(payload)
|
||||||
|
let req = ApiRequest(path: "/api/customer/cards/\(cardId)", method: "PATCH", module: .customer, requiresAuth: true, body: body)
|
||||||
|
return try await sendEnvelope(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteCard(cardId: String) async throws -> ApiEnvelope<EmptyResult> {
|
||||||
|
let req = ApiRequest(path: "/api/customer/cards/\(cardId)", method: "DELETE", module: .customer, requiresAuth: true)
|
||||||
|
return try await sendEnvelope(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changePaymentMethod(storeId: String, orderId: String, payload: ChangePaymentMethodPayload) async throws -> ApiEnvelope<ChangePaymentMethodResult> {
|
||||||
|
let body = try JSONEncoder().encode(payload)
|
||||||
|
let req = ApiRequest(path: "/api/store/\(storeId)/orders/\(orderId)/payment-method", method: "PATCH", module: .store, requiresAuth: true, body: body)
|
||||||
|
return try await sendEnvelope(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Profile CPF
|
||||||
|
|
||||||
|
func updateProfileCpf(cpf: String) async throws -> ApiEnvelope<EmptyResult> {
|
||||||
|
let payload = ["cpf": cpf]
|
||||||
|
let body = try JSONEncoder().encode(payload)
|
||||||
|
let req = ApiRequest(path: "/api/customer/profile", method: "PATCH", module: .customer, requiresAuth: true, body: body)
|
||||||
|
let result: ApiEnvelope<EmptyResult> = try await sendEnvelope(req)
|
||||||
|
if result.error == false {
|
||||||
|
AppContentCache.shared.invalidate(prefix: profileCachePrefix)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
extension ApiService {
|
extension ApiService {
|
||||||
static func decodeFlexibleString<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> String? {
|
static func decodeFlexibleString<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> String? {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ struct ProfileState {
|
|||||||
var email: String = ""
|
var email: String = ""
|
||||||
var phone: String = ""
|
var phone: String = ""
|
||||||
var profilePicture: String = ""
|
var profilePicture: String = ""
|
||||||
|
var cpf: String = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AddressState {
|
struct AddressState {
|
||||||
|
|||||||
@@ -234,7 +234,13 @@ extension CheckoutView {
|
|||||||
return
|
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 {
|
guard case .success(let payload) = payloadBuildResult else {
|
||||||
let message: String
|
let message: String
|
||||||
if case .failure(let reason) = payloadBuildResult {
|
if case .failure(let reason) = payloadBuildResult {
|
||||||
@@ -251,83 +257,31 @@ extension CheckoutView {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let response = try await ApiService().createOrder(storeId: storeId, payload: payload)
|
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)
|
let isInAppMethod = availableInAppPaymentMethods.contains(effectivePaymentMethod)
|
||||||
if useInAppPayment == false || isInAppMethod == false {
|
if useInAppPayment == false || isInAppMethod == false {
|
||||||
appState.cart.clear()
|
if response.error == false, let result = response.result {
|
||||||
SessionStateStore.clearPendingCartOrder()
|
let orderId = result.id ?? UUID().uuidString
|
||||||
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
handleOrderResponse(response, effectivePaymentMethod: effectivePaymentMethod)
|
||||||
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
|
|
||||||
)
|
|
||||||
} catch {
|
} catch {
|
||||||
SnackbarCenter.shared.show(title: "Não foi possível criar o pedido.", style: .error, icon: "xmark.octagon.fill", duration: 3.0)
|
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) }
|
guard appState.cart.items.isEmpty == false else { return .failure(.emptyCart) }
|
||||||
|
|
||||||
let profileName = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
let profileName = appState.profile.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -360,18 +314,111 @@ extension CheckoutView {
|
|||||||
|
|
||||||
return .success(
|
return .success(
|
||||||
CreateOrderPayload(
|
CreateOrderPayload(
|
||||||
customer: CreateOrderCustomerPayload(
|
customer: CreateOrderCustomerPayload(
|
||||||
name: profileName,
|
name: profileName,
|
||||||
phone: profilePhone,
|
phone: profilePhone,
|
||||||
email: profileEmail,
|
email: profileEmail,
|
||||||
asaasId: nil
|
asaasId: nil
|
||||||
),
|
),
|
||||||
items: appState.cart.toOrderItemsPayload(),
|
items: appState.cart.toOrderItemsPayload(),
|
||||||
total: totalValue,
|
total: totalValue,
|
||||||
paymentMethod: paymentMethod.rawValue,
|
paymentMethod: paymentMethod.rawValue,
|
||||||
deliveryType: deliveryType.rawValue,
|
deliveryType: deliveryType.rawValue,
|
||||||
address: addressPayload
|
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 {
|
||||||
|
let itemsData = (try? JSONEncoder().encode(appState.cart.toOrderItemsPayload())).flatMap { String(data: $0, encoding: .utf8) } ?? "[]"
|
||||||
|
let storeId = appState.cart.storeId ?? ""
|
||||||
|
pixPaymentContext = PixPaymentContext(
|
||||||
|
id: orderId,
|
||||||
|
orderId: orderId,
|
||||||
|
shortId: result.shortId,
|
||||||
|
storeId: storeId,
|
||||||
|
copyPaste: copyPaste,
|
||||||
|
qrCodeImageBase64: qrCodeImage,
|
||||||
|
expirationDate: expirationDate,
|
||||||
|
total: totalValue,
|
||||||
|
profileName: appState.profile.name,
|
||||||
|
profileEmail: appState.profile.email,
|
||||||
|
profilePhone: appState.profile.phone,
|
||||||
|
addressZip: selectedCustomerAddress?.zipCode,
|
||||||
|
addressNumber: selectedCustomerAddress?.number,
|
||||||
|
deliveryType: deliveryType.rawValue,
|
||||||
|
itemsJSON: itemsData
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderTrackingContext = OrderTrackingContext(orderId: orderId, shortId: result.shortId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct OrdersView: View {
|
struct OrdersView: View {
|
||||||
|
@Binding var appState: AppState
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State var isLoading = false
|
@State var isLoading = false
|
||||||
@State var errorMessage: String? = nil
|
@State var errorMessage: String? = nil
|
||||||
@@ -58,7 +59,8 @@ struct OrdersView: View {
|
|||||||
initialShortId: context.shortId,
|
initialShortId: context.shortId,
|
||||||
fallbackPaymentMethod: context.paymentMethod,
|
fallbackPaymentMethod: context.paymentMethod,
|
||||||
fallbackTotal: context.total,
|
fallbackTotal: context.total,
|
||||||
routeIntent: context.intent
|
routeIntent: context.intent,
|
||||||
|
appState: $appState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -629,11 +631,11 @@ struct OrderEntryDestinationView: View {
|
|||||||
let fallbackPaymentMethod: String?
|
let fallbackPaymentMethod: String?
|
||||||
let fallbackTotal: Double?
|
let fallbackTotal: Double?
|
||||||
let routeIntent: OrderRouteIntent
|
let routeIntent: OrderRouteIntent
|
||||||
|
@Binding var appState: AppState
|
||||||
|
|
||||||
@State var isResolvingRoute = true
|
@State var isResolvingRoute = true
|
||||||
@State var didResolve = false
|
@State var didResolve = false
|
||||||
@State var pixContext: PixPaymentContext? = nil
|
@State var pixContext: PixPaymentContext? = nil
|
||||||
@State var cardContext: CardPaymentContext? = nil
|
|
||||||
@State var orderTrackingContext: OrderTrackingContext? = nil
|
@State var orderTrackingContext: OrderTrackingContext? = nil
|
||||||
@State var orderDetails: PublicOrderResult? = nil
|
@State var orderDetails: PublicOrderResult? = nil
|
||||||
|
|
||||||
@@ -651,6 +653,7 @@ struct OrderEntryDestinationView: View {
|
|||||||
} else if let pixContext {
|
} else if let pixContext {
|
||||||
PaymentPixView(
|
PaymentPixView(
|
||||||
context: pixContext,
|
context: pixContext,
|
||||||
|
appState: $appState,
|
||||||
onPaymentConfirmed: {
|
onPaymentConfirmed: {
|
||||||
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
||||||
},
|
},
|
||||||
@@ -658,16 +661,6 @@ struct OrderEntryDestinationView: View {
|
|||||||
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else if let cardContext {
|
|
||||||
PaymentCardView(
|
|
||||||
context: cardContext,
|
|
||||||
onPaymentConfirmed: {
|
|
||||||
orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId)
|
|
||||||
},
|
|
||||||
onOpenTracking: {
|
|
||||||
orderTrackingContext = OrderTrackingContext(orderId: cardContext.orderId, shortId: cardContext.shortId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if let orderDetails {
|
} else if let orderDetails {
|
||||||
OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId)
|
OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId)
|
||||||
} else {
|
} else {
|
||||||
@@ -712,24 +705,30 @@ struct OrderEntryDestinationView: View {
|
|||||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||||
? pixFromPayment?.expirationDate
|
? pixFromPayment?.expirationDate
|
||||||
: pixFromPayload?.expirationDate
|
: pixFromPayload?.expirationDate
|
||||||
|
let storeId = order.storeId ?? ""
|
||||||
pixContext = PixPaymentContext(
|
pixContext = PixPaymentContext(
|
||||||
id: order.id,
|
id: order.id,
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
shortId: order.shortId ?? initialShortId,
|
shortId: order.shortId ?? initialShortId,
|
||||||
|
storeId: storeId,
|
||||||
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||||
? (copyPaste ?? "")
|
? (copyPaste ?? "")
|
||||||
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
||||||
qrCodeImageBase64: qrCodeImage,
|
qrCodeImageBase64: qrCodeImage,
|
||||||
expirationDate: expirationDate
|
expirationDate: expirationDate,
|
||||||
|
total: order.total ?? 0,
|
||||||
|
profileName: "",
|
||||||
|
profileEmail: "",
|
||||||
|
profilePhone: "",
|
||||||
|
addressZip: nil,
|
||||||
|
addressNumber: nil,
|
||||||
|
deliveryType: order.deliveryType ?? "DELIVERY",
|
||||||
|
itemsJSON: "[]"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||||
cardContext = CardPaymentContext(
|
orderTrackingContext = OrderTrackingContext(orderId: order.id, shortId: order.shortId ?? initialShortId)
|
||||||
orderId: order.id,
|
|
||||||
shortId: order.shortId ?? initialShortId,
|
|
||||||
total: order.total ?? fallbackTotal ?? 0
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct ProfileView: View {
|
|||||||
|
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 14) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
OrdersView()
|
OrdersView(appState: $appState)
|
||||||
} label: {
|
} label: {
|
||||||
ProfileMenuRow(icon: "list.bullet.clipboard.fill", title: "Meus Pedidos")
|
ProfileMenuRow(icon: "list.bullet.clipboard.fill", title: "Meus Pedidos")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct UserProfileView: View {
|
|||||||
@State var name: String = ""
|
@State var name: String = ""
|
||||||
@State var email: String = ""
|
@State var email: String = ""
|
||||||
@State var phone: String = ""
|
@State var phone: String = ""
|
||||||
|
@State var cpf: String = ""
|
||||||
@State var profilePicture: String = ""
|
@State var profilePicture: String = ""
|
||||||
@State var isSaving = false
|
@State var isSaving = false
|
||||||
|
|
||||||
@@ -97,6 +98,14 @@ struct UserProfileView: View {
|
|||||||
phone = masked
|
phone = masked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textFieldSection(title: "CPF", placeholder: "000.000.000-00", text: $cpf)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
.onChange(of: cpf) { _, newValue in
|
||||||
|
let digits = newValue.filter(\.isNumber)
|
||||||
|
let masked = formatCPF(digits)
|
||||||
|
if masked != newValue { cpf = masked }
|
||||||
|
}
|
||||||
.appNoAutoCap()
|
.appNoAutoCap()
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
@@ -148,6 +157,15 @@ struct UserProfileView: View {
|
|||||||
email = appState.profile.email
|
email = appState.profile.email
|
||||||
phone = formatPhoneForDisplay(appState.profile.phone)
|
phone = formatPhoneForDisplay(appState.profile.phone)
|
||||||
profilePicture = appState.profile.profilePicture
|
profilePicture = appState.profile.profilePicture
|
||||||
|
cpf = formatCPF(appState.profile.cpf.filter(\.isNumber))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatCPF(_ digits: String) -> String {
|
||||||
|
let d = String(digits.prefix(11))
|
||||||
|
if d.count <= 3 { return d }
|
||||||
|
if d.count <= 6 { return "\(d.prefix(3)).\(d.dropFirst(3))" }
|
||||||
|
if d.count <= 9 { return "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6))" }
|
||||||
|
return "\(d.prefix(3)).\(d.dropFirst(3).prefix(3)).\(d.dropFirst(6).prefix(3))-\(d.dropFirst(9))"
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -186,6 +204,14 @@ struct UserProfileView: View {
|
|||||||
appState.profile.email = customer?.email ?? cleanEmail
|
appState.profile.email = customer?.email ?? cleanEmail
|
||||||
appState.profile.phone = customer?.phoneNumber ?? normalizedPhone
|
appState.profile.phone = customer?.phoneNumber ?? normalizedPhone
|
||||||
appState.profile.profilePicture = customer?.profilePicture ?? cleanPhoto
|
appState.profile.profilePicture = customer?.profilePicture ?? cleanPhoto
|
||||||
|
|
||||||
|
let cleanCpf = cpf.filter(\.isNumber)
|
||||||
|
if cleanCpf.count == 11 {
|
||||||
|
appState.profile.cpf = cleanCpf
|
||||||
|
Task<Void, Never> {
|
||||||
|
do { _ = try await ApiService().updateProfileCpf(cpf: cleanCpf) } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
SessionStateStore.setActiveUserKey(
|
SessionStateStore.setActiveUserKey(
|
||||||
SessionStateStore.makeUserKey(profileId: appState.profile.id, email: appState.profile.email)
|
SessionStateStore.makeUserKey(profileId: appState.profile.id, email: appState.profile.email)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user