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 deliveryType: String
|
||||
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 {
|
||||
@@ -16,7 +22,7 @@ struct CreateOrderCustomerPayload: Encodable {
|
||||
let asaasId: String?
|
||||
}
|
||||
|
||||
struct CreateOrderItemPayload: Encodable {
|
||||
struct CreateOrderItemPayload: Codable {
|
||||
let productId: String
|
||||
let name: String
|
||||
let qty: Int
|
||||
@@ -24,7 +30,7 @@ struct CreateOrderItemPayload: Encodable {
|
||||
let addons: [CreateOrderAddonPayload]
|
||||
}
|
||||
|
||||
struct CreateOrderAddonPayload: Encodable {
|
||||
struct CreateOrderAddonPayload: Codable {
|
||||
let addonId: String
|
||||
let name: String
|
||||
let qty: Int
|
||||
|
||||
@@ -426,6 +426,49 @@ final class ApiService {
|
||||
)
|
||||
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 {
|
||||
static func decodeFlexibleString<K: CodingKey>(from container: KeyedDecodingContainer<K>, keys: [K]) -> String? {
|
||||
|
||||
@@ -92,6 +92,7 @@ struct ProfileState {
|
||||
var email: String = ""
|
||||
var phone: String = ""
|
||||
var profilePicture: String = ""
|
||||
var cpf: String = ""
|
||||
}
|
||||
|
||||
struct AddressState {
|
||||
|
||||
@@ -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 {
|
||||
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)
|
||||
@@ -370,8 +324,101 @@ extension CheckoutView {
|
||||
total: totalValue,
|
||||
paymentMethod: paymentMethod.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
|
||||
|
||||
struct OrdersView: View {
|
||||
@Binding var appState: AppState
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var isLoading = false
|
||||
@State var errorMessage: String? = nil
|
||||
@@ -58,7 +59,8 @@ struct OrdersView: View {
|
||||
initialShortId: context.shortId,
|
||||
fallbackPaymentMethod: context.paymentMethod,
|
||||
fallbackTotal: context.total,
|
||||
routeIntent: context.intent
|
||||
routeIntent: context.intent,
|
||||
appState: $appState
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -629,11 +631,11 @@ struct OrderEntryDestinationView: View {
|
||||
let fallbackPaymentMethod: String?
|
||||
let fallbackTotal: Double?
|
||||
let routeIntent: OrderRouteIntent
|
||||
@Binding var appState: AppState
|
||||
|
||||
@State var isResolvingRoute = true
|
||||
@State var didResolve = false
|
||||
@State var pixContext: PixPaymentContext? = nil
|
||||
@State var cardContext: CardPaymentContext? = nil
|
||||
@State var orderTrackingContext: OrderTrackingContext? = nil
|
||||
@State var orderDetails: PublicOrderResult? = nil
|
||||
|
||||
@@ -651,6 +653,7 @@ struct OrderEntryDestinationView: View {
|
||||
} else if let pixContext {
|
||||
PaymentPixView(
|
||||
context: pixContext,
|
||||
appState: $appState,
|
||||
onPaymentConfirmed: {
|
||||
orderTrackingContext = OrderTrackingContext(orderId: pixContext.orderId, shortId: pixContext.shortId)
|
||||
},
|
||||
@@ -658,16 +661,6 @@ struct OrderEntryDestinationView: View {
|
||||
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 {
|
||||
OrderDetailsView(order: orderDetails, orderId: orderId, initialShortId: initialShortId)
|
||||
} else {
|
||||
@@ -712,24 +705,30 @@ struct OrderEntryDestinationView: View {
|
||||
let expirationDate = (pixFromPayment?.expirationDate?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false)
|
||||
? pixFromPayment?.expirationDate
|
||||
: pixFromPayload?.expirationDate
|
||||
let storeId = order.storeId ?? ""
|
||||
pixContext = PixPaymentContext(
|
||||
id: order.id,
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
storeId: storeId,
|
||||
copyPaste: copyPaste?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||
? (copyPaste ?? "")
|
||||
: "Código PIX indisponível no momento. Aguarde e tente novamente.",
|
||||
qrCodeImageBase64: qrCodeImage,
|
||||
expirationDate: expirationDate
|
||||
expirationDate: expirationDate,
|
||||
total: order.total ?? 0,
|
||||
profileName: "",
|
||||
profileEmail: "",
|
||||
profilePhone: "",
|
||||
addressZip: nil,
|
||||
addressNumber: nil,
|
||||
deliveryType: order.deliveryType ?? "DELIVERY",
|
||||
itemsJSON: "[]"
|
||||
)
|
||||
return
|
||||
}
|
||||
if normalizedMethod == "CREDIT_CARD" || normalizedMethod == "DEBIT_CARD" {
|
||||
cardContext = CardPaymentContext(
|
||||
orderId: order.id,
|
||||
shortId: order.shortId ?? initialShortId,
|
||||
total: order.total ?? fallbackTotal ?? 0
|
||||
)
|
||||
orderTrackingContext = OrderTrackingContext(orderId: order.id, shortId: order.shortId ?? initialShortId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ struct ProfileView: View {
|
||||
|
||||
VStack(spacing: 14) {
|
||||
NavigationLink {
|
||||
OrdersView()
|
||||
OrdersView(appState: $appState)
|
||||
} label: {
|
||||
ProfileMenuRow(icon: "list.bullet.clipboard.fill", title: "Meus Pedidos")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct UserProfileView: View {
|
||||
@State var name: String = ""
|
||||
@State var email: String = ""
|
||||
@State var phone: String = ""
|
||||
@State var cpf: String = ""
|
||||
@State var profilePicture: String = ""
|
||||
@State var isSaving = false
|
||||
|
||||
@@ -97,6 +98,14 @@ struct UserProfileView: View {
|
||||
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()
|
||||
}
|
||||
.padding(16)
|
||||
@@ -148,6 +157,15 @@ struct UserProfileView: View {
|
||||
email = appState.profile.email
|
||||
phone = formatPhoneForDisplay(appState.profile.phone)
|
||||
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
|
||||
@@ -186,6 +204,14 @@ struct UserProfileView: View {
|
||||
appState.profile.email = customer?.email ?? cleanEmail
|
||||
appState.profile.phone = customer?.phoneNumber ?? normalizedPhone
|
||||
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.makeUserKey(profileId: appState.profile.id, email: appState.profile.email)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user