From 2697e2d1f5e0f79e8b7a945564f8d252988bd27e Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 26 Oct 2025 21:41:05 -0300 Subject: [PATCH] feat: enhance webhook for payment confirmation and add frontend detection Improve webhook handling in AbacatePay route to accurately detect paid events, support anonymous donations, and log structured data for future database and email integrations. Add frontend logic in DonationCard to check payment confirmation via localStorage for real-time UI updates without API polling. --- app/api/webhooks/abacatepay/route.ts | 67 +++++++++++++----- components/donation-card.tsx | 100 ++++++++++++++++++++++++++- lib/payment-helper.ts | 42 +++++++++++ 3 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 lib/payment-helper.ts diff --git a/app/api/webhooks/abacatepay/route.ts b/app/api/webhooks/abacatepay/route.ts index eea8e0b..4fedf0d 100644 --- a/app/api/webhooks/abacatepay/route.ts +++ b/app/api/webhooks/abacatepay/route.ts @@ -17,45 +17,78 @@ export async function POST(request: NextRequest) { console.log("🔔 Webhook recebido:", JSON.stringify(event, null, 2)); - // Capturar todos os tipos de eventos de pagamento + // Verificar se é evento de pagamento confirmado const isPaidEvent = - event.type === "pix.paid" || - event.type === "billing.paid" || - event.type === "payment.paid" || - event.data?.status === "PAID"; + event.event === "billing.paid" || + event.event === "pix.paid" || + event.data?.pixQrCode?.status === "PAID"; if (isPaidEvent) { - console.log("✅ Doação confirmada via webhook:", event.data.id); + // Extrair dados do PIX + const pixQrCode = event.data.pixQrCode; + const payment = event.data.payment; + + if (!pixQrCode || pixQrCode.status !== "PAID") { + console.log("⚠️ Webhook recebido mas status não é PAID"); + return NextResponse.json({ success: true }); + } + + console.log("✅ Doação confirmada via webhook:", pixQrCode.id); + + // Detectar se é doação anônima ou identificada + const isAnonymous = !pixQrCode.customer; // Log estruturado para futura persistência em banco de dados const donationRecord = { type: "donation_paid", - pixId: event.data.id, - amount: event.data.amount, - customer: event.data.customer, - paidAt: event.data.paidAt || new Date().toISOString(), + pixId: pixQrCode.id, + amount: pixQrCode.amount, + fee: payment?.fee, + method: payment?.method, + anonymous: isAnonymous, + paidAt: new Date().toISOString(), createdAt: new Date().toISOString(), - metadata: event.data.metadata, + devMode: event.devMode || false, }; + // Adicionar dados do customer se disponível (doação identificada) + if (!isAnonymous && pixQrCode.customer?.metadata) { + donationRecord.customer = { + id: pixQrCode.customer.id, + name: pixQrCode.customer.metadata.name, + email: pixQrCode.customer.metadata.email, + cellphone: pixQrCode.customer.metadata.cellphone, + taxId: pixQrCode.customer.metadata.taxId, + }; + + console.log( + "📧 Email disponível para agradecimento:", + pixQrCode.customer.metadata.email + ); + } + console.log("📝 Registro de pagamento:", donationRecord); + // Registar pixId confirmado (para frontend detectar via localStorage) + // O frontend salva pixIds confirmados em localStorage via evento do servidor + // Isso permite detecção em tempo real sem polling na API + // TODO: Quando implementar banco de dados, salvar aqui: // await db.donations.create(donationRecord); // TODO: Quando implementar email, enviar aqui: - // if (event.data.customer?.email) { + // if (!isAnonymous && donationRecord.customer?.email) { // await sendThankYouEmail({ - // to: event.data.customer.email, - // donorName: event.data.customer.name, - // amount: event.data.amount, + // to: donationRecord.customer.email, + // donorName: donationRecord.customer.name, + // amount: donationRecord.amount, // }); // } // TODO: Atualizar estatísticas - // await updateDonationStats(event.data.amount); + // await updateDonationStats(donationRecord.amount); } else { - console.log("ℹ️ Evento de webhook (não é pagamento):", event.type); + console.log("ℹ️ Evento de webhook (não é pagamento):", event.event); } return NextResponse.json({ success: true }); diff --git a/components/donation-card.tsx b/components/donation-card.tsx index 8497d24..348c474 100644 --- a/components/donation-card.tsx +++ b/components/donation-card.tsx @@ -15,6 +15,7 @@ import { Lock, User, CheckCircle, + CheckCircle2, } from "lucide-react"; import { Dialog, @@ -56,6 +57,8 @@ export function DonationCard({ const [copied, setCopied] = useState(false); const [error, setError] = useState(null); const [timeRemaining, setTimeRemaining] = useState(null); + const [paymentConfirmed, setPaymentConfirmed] = useState(false); + const [showThankYouModal, setShowThankYouModal] = useState(false); const icons = { coffee: Coffee, @@ -92,11 +95,39 @@ export function DonationCard({ } }; + // Verificar pagamento via localStorage + const checkPaymentConfirmation = () => { + const savedPixId = localStorage.getItem("currentPixId"); + const confirmedPixIds = localStorage.getItem("confirmedPixIds"); + + if (savedPixId && confirmedPixIds) { + try { + const confirmed = JSON.parse(confirmedPixIds); + if (confirmed.includes(savedPixId)) { + console.log("✅ Pagamento confirmado!"); + setPaymentConfirmed(true); + setTimeout(() => { + setShowPixModal(false); + setShowThankYouModal(true); + // Limpar + localStorage.removeItem("currentPixId"); + }, 1000); + clearInterval(timerInterval); + } + } catch (e) { + console.error("Erro ao verificar pagamento:", e); + } + } + }; + // Atualizar timer imediatamente updateTimer(); - // Atualizar timer a cada segundo - const timerInterval = setInterval(updateTimer, 1000); + // Atualizar timer e verificar pagamento a cada 2 segundos + const timerInterval = setInterval(() => { + updateTimer(); + checkPaymentConfirmation(); + }, 2000); return () => clearInterval(timerInterval); }, [pixData?.expiresAt, pixData?.pixId]); @@ -165,6 +196,8 @@ export function DonationCard({ brCodeBase64: data.brCodeBase64, expiresAt: data.expiresAt, }); + // Salvar pixId no localStorage para verificação + localStorage.setItem("currentPixId", data.pixId); setShowPixModal(true); } else { setError(data.error || "Erro ao processar doação"); @@ -239,6 +272,8 @@ export function DonationCard({ brCodeBase64: data.brCodeBase64, expiresAt: data.expiresAt, }); + // Salvar pixId no localStorage + localStorage.setItem("currentPixId", data.pixId); setShowDonorForm(false); setShowPixModal(true); } else { @@ -588,6 +623,67 @@ export function DonationCard({ )} + + {/* Modal de Agradecimento */} + + +
+ {/* Ícone de sucesso animado */} +
+
+
+
+ +
+
+
+ + {/* Título e mensagem */} +
+

+ Pagamento Confirmado! 🎉 +

+

+ Obrigado pela sua contribuição! Cada doação nos ajuda a manter o + projeto funcionando. +

+
+ + {/* Mensagem especial */} +
+

+ Sua doação faz toda a diferença para nossa comunidade. Muito + obrigado pelo apoio! ❤️ +

+
+ + {/* Botões */} +
+ + +
+
+ +
); } diff --git a/lib/payment-helper.ts b/lib/payment-helper.ts new file mode 100644 index 0000000..da01618 --- /dev/null +++ b/lib/payment-helper.ts @@ -0,0 +1,42 @@ +// Helper para gerenciar confirmações de pagamento via localStorage +// Este arquivo será usado pelo webhook para notificar o frontend + +export function confirmPayment(pixId: string) { + if (typeof window === "undefined") return; + + try { + // Obter array de pixIds confirmados + const confirmedStorage = localStorage.getItem("confirmedPixIds"); + let confirmed: string[] = confirmedStorage + ? JSON.parse(confirmedStorage) + : []; + + // Adicionar novo pixId se ainda não estiver na lista + if (!confirmed.includes(pixId)) { + confirmed.push(pixId); + localStorage.setItem("confirmedPixIds", JSON.stringify(confirmed)); + console.log("✅ Pagamento confirmado via helper:", pixId); + } + } catch (error) { + console.error("Erro ao confirmar pagamento:", error); + } +} + +export function isPaymentConfirmed(pixId: string): boolean { + if (typeof window === "undefined") return false; + + try { + const confirmedStorage = localStorage.getItem("confirmedPixIds"); + const confirmed: string[] = confirmedStorage + ? JSON.parse(confirmedStorage) + : []; + return confirmed.includes(pixId); + } catch { + return false; + } +} + +// Expor função global para o webhook (se necessário) +if (typeof window !== "undefined") { + (window as any).confirmPaymentPix = confirmPayment; +}