payment and list
This commit is contained in:
@@ -152,12 +152,28 @@ struct CheckoutView: View {
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $pixPaymentContext) { context in
|
||||
PaymentPixView(context: context) {
|
||||
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) {
|
||||
PaymentCardView(
|
||||
context: context,
|
||||
onPaymentConfirmed: {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
}
|
||||
) {
|
||||
appState.cart.clear()
|
||||
SessionStateStore.clearPendingCartOrder()
|
||||
orderTrackingContext = OrderTrackingContext(orderId: context.orderId, shortId: context.shortId)
|
||||
}
|
||||
}
|
||||
@@ -552,11 +568,14 @@ struct CardPaymentContext: Identifiable, Hashable {
|
||||
|
||||
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),
|
||||
@@ -590,51 +609,51 @@ struct PaymentPixView: View {
|
||||
Text("AGUARDANDO PAGAMENTO")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 8)
|
||||
.background(AppColors.brandSoft)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity, minHeight: 360)
|
||||
.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("Código PIX")
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textPrimary)
|
||||
Text(context.copyPaste)
|
||||
.font(.system(size: 12, weight: .medium, design: .monospaced))
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.lineLimit(3)
|
||||
.lineLimit(0)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 12)
|
||||
if let expirationDate = context.expirationDate, expirationDate.isEmpty == false {
|
||||
Text("Expira em: \(expirationDate)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
Button("Já realizei o pagamento") {
|
||||
if latestOrder?.isPaymentConfirmed == true {
|
||||
openTrackingOnce()
|
||||
} else {
|
||||
SnackbarCenter.shared.show(title: "Pagamento ainda não confirmado.", style: .info, icon: "clock.fill", duration: 2.0)
|
||||
}
|
||||
}
|
||||
.font(AppTypography.heading3)
|
||||
.foregroundStyle(AppColors.textMuted)
|
||||
.padding(.top, 8)
|
||||
.disabled(isPixExpired)
|
||||
.opacity(isPixExpired ? 0.5 : 1.0)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
@@ -645,11 +664,22 @@ struct PaymentPixView: View {
|
||||
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()
|
||||
}
|
||||
@@ -659,7 +689,6 @@ struct PaymentPixView: View {
|
||||
guard hasOpenedTracking == false else { return }
|
||||
hasOpenedTracking = true
|
||||
onOpenTracking?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func copyToClipboard(_ value: String) {
|
||||
@@ -670,10 +699,83 @@ struct PaymentPixView: View {
|
||||
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 = ""
|
||||
@@ -723,6 +825,7 @@ struct PaymentCardView: View {
|
||||
|
||||
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)
|
||||
@@ -731,6 +834,7 @@ struct PaymentCardView: View {
|
||||
|
||||
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)
|
||||
@@ -752,6 +856,7 @@ struct PaymentCardView: View {
|
||||
tracker.onOrderUpdated = { updated in
|
||||
latestOrder = updated
|
||||
if updated.isPaymentConfirmed {
|
||||
onPaymentConfirmed?()
|
||||
openTrackingOnce()
|
||||
}
|
||||
}
|
||||
@@ -766,7 +871,6 @@ struct PaymentCardView: View {
|
||||
guard hasOpenedTracking == false else { return }
|
||||
hasOpenedTracking = true
|
||||
onOpenTracking?()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func labeledField(_ label: String, placeholder: String, text: Binding<String>) -> some View {
|
||||
|
||||
Reference in New Issue
Block a user