From 01e97eb0783c0b882e1067182e4ec11ebef9b8d7 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 26 Oct 2025 18:14:53 -0300 Subject: [PATCH 01/11] feat: add donation support with footer card and navigation Add donation functionality to encourage community support, including a footer card on the home page, a highlighted "Apoiar" link in the header, and comprehensive English messages for the donation section to promote transparency and user engagement. --- app/[locale]/apoiar/obrigado/page.tsx | 101 +++++++++ app/[locale]/apoiar/page.tsx | 286 ++++++++++++++++++++++++++ app/[locale]/page.tsx | 8 + app/api/donations/create/route.ts | 49 +++++ app/api/donations/verify/route.ts | 36 ++++ app/api/webhooks/abacatepay/route.ts | 45 ++++ components/Header.tsx | 7 +- components/donation-card.tsx | 216 +++++++++++++++++++ components/donation-footer-card.tsx | 35 ++++ components/share-button.tsx | 26 +++ lib/abacatepay.ts | 100 +++++++++ messages/en.json | 71 +++++++ messages/pt.json | 71 +++++++ package.json | 1 + pnpm-lock.yaml | 52 +++++ types/donation.ts | 59 ++++++ 16 files changed, 1161 insertions(+), 2 deletions(-) create mode 100644 app/[locale]/apoiar/obrigado/page.tsx create mode 100644 app/[locale]/apoiar/page.tsx create mode 100644 app/api/donations/create/route.ts create mode 100644 app/api/donations/verify/route.ts create mode 100644 app/api/webhooks/abacatepay/route.ts create mode 100644 components/donation-card.tsx create mode 100644 components/donation-footer-card.tsx create mode 100644 components/share-button.tsx create mode 100644 lib/abacatepay.ts create mode 100644 types/donation.ts diff --git a/app/[locale]/apoiar/obrigado/page.tsx b/app/[locale]/apoiar/obrigado/page.tsx new file mode 100644 index 0000000..2ae41eb --- /dev/null +++ b/app/[locale]/apoiar/obrigado/page.tsx @@ -0,0 +1,101 @@ +import Header from "@/components/Header"; +import { Button } from "@/components/ui/button"; +import { BlurFade } from "@/components/ui/blur-fade"; +import { Heart, Share2, Home } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import Link from "next/link"; + +export async function generateMetadata({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const t = await getTranslations({ locale, namespace: "donation.thankYou" }); + return { + title: t("title"), + description: t("subtitle"), + }; +} + +export default async function ThankYouPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const t = await getTranslations({ locale, namespace: "donation.thankYou" }); + + return ( +
+
+ +
+ +
+ {/* Ícone de coração animado */} +
+
+ +
+
+ + {/* Título e subtítulo */} +

+ {t("title")} +

+

+ {t("subtitle")} +

+ + {/* Mensagem de agradecimento */} +
+

+ {t("message")} +

+
+ + {/* Botões de ação */} +
+ + + + + +
+ + {/* Informação adicional */} +
+

+ 💡 Dica: Compartilhe nossa plataforma com seus + amigos! Quanto mais pessoas usarem o Tibia Trade Pagodera, mais + forte nossa comunidade fica. +

+
+
+
+
+
+ ); +} diff --git a/app/[locale]/apoiar/page.tsx b/app/[locale]/apoiar/page.tsx new file mode 100644 index 0000000..1dccefd --- /dev/null +++ b/app/[locale]/apoiar/page.tsx @@ -0,0 +1,286 @@ +import Header from "@/components/Header"; +import { DonationCard } from "@/components/donation-card"; +import { BlurFade } from "@/components/ui/blur-fade"; +import { NumberTicker } from "@/components/ui/number-ticker"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { Heart, Server, Code, Shield, AlertTriangle } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import Link from "next/link"; +import { ShareButton } from "@/components/share-button"; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "donation" }); + return { + title: t("title"), + description: t("subtitle"), + }; +} + +export default async function SupportPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "donation" }); + + // Dados mockados para demonstração + const stats = { + totalSupporters: 1247, + totalRaised: 18542, + monthsOnline: 12, + uptime: 99.8, + }; + + const transparencyData = [ + { + icon: Server, + title: t("transparency.hosting"), + percentage: 40, + description: t("transparency.hostingDesc"), + color: "text-blue-500", + }, + { + icon: Code, + title: t("transparency.development"), + percentage: 35, + description: t("transparency.developmentDesc"), + color: "text-green-500", + }, + { + icon: Shield, + title: t("transparency.support"), + percentage: 15, + description: t("transparency.supportDesc"), + color: "text-purple-500", + }, + { + icon: AlertTriangle, + title: t("transparency.emergency"), + percentage: 10, + description: t("transparency.emergencyDesc"), + color: "text-orange-500", + }, + ]; + + const faqItems = [ + { + question: t("faq.q1"), + answer: t("faq.a1"), + }, + { + question: t("faq.q2"), + answer: t("faq.a2"), + }, + { + question: t("faq.q3"), + answer: t("faq.a3"), + }, + { + question: t("faq.q4"), + answer: t("faq.a4"), + }, + { + question: t("faq.q5"), + answer: t("faq.a5"), + }, + ]; + + return ( +
+
+ +
+ {/* Hero Section */} + +
+
+
+ +
+
+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+
+ + {/* História & Missão */} + +
+

+ {t("story.title")} +

+
+
+

+ {t("story.text")} +

+
+
+
+
+ + {/* Transparência */} + +
+

+ {t("transparency.title")} +

+
+ {transparencyData.map((item, idx) => ( +
+
+
+ +
+
+
+

{item.title}

+ {item.percentage}% +
+

+ {item.description} +

+
+
+
+ ))} +
+
+
+ + {/* Prova Social */} + +
+
+
+

+ +

+

+ {t("social.supporters")} +

+
+
+

+ R$ +

+

+ {t("social.raised")} +

+
+
+

+ +

+

+ {t("social.months")} +

+
+
+

+ % +

+

+ {t("social.uptime")} +

+
+
+
+
+ + {/* Opções de Doação */} + +
+

+ Escolha como apoiar +

+
+ + + +
+
+
+ + {/* FAQ */} + +
+

+ {t("faq.title")} +

+ + {faqItems.map((item, idx) => ( + + + {item.question} + + + {item.answer} + + + ))} + +
+
+ + {/* CTA Final */} + +
+
+

{t("cta.final")}

+

+ {t("cta.subtitle")} +

+
+ + +
+
+
+
+
+
+ ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 7826884..a275135 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -6,6 +6,7 @@ import { HowItWorks } from "@/components/sections/how-it-works"; import { FeaturedItems } from "@/components/sections/featured-items"; import { MarketOffers } from "@/components/sections/market-offers"; import { MarketStats } from "@/components/sections/market-stats"; +import { DonationFooterCard } from "@/components/donation-footer-card"; export default function Home() { return ( @@ -20,6 +21,13 @@ export default function Home() { + + {/* Card de Doação no Footer */} +
+
+ +
+
); } diff --git a/app/api/donations/create/route.ts b/app/api/donations/create/route.ts new file mode 100644 index 0000000..f7ec4b0 --- /dev/null +++ b/app/api/donations/create/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { abacatePay } from "@/lib/abacatepay"; +import { DonationData } from "@/types/donation"; + +export async function POST(request: NextRequest) { + try { + const body: DonationData = await request.json(); + + // Validação básica + if (!body.amount || body.amount < 5 || body.amount > 10000) { + return NextResponse.json( + { + success: false, + error: "Valor deve estar entre R$ 5,00 e R$ 10.000,00", + }, + { status: 400 } + ); + } + + const billing = await abacatePay.createBilling({ + amount: body.amount, + metadata: { + donorName: body.donorName, + donorEmail: body.donorEmail, + message: body.message, + }, + returnUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/apoiar/obrigado`, + completionUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/donations/verify`, + }); + + return NextResponse.json({ + success: true, + billingId: billing.id, + pixCode: billing.pixCode, + pixQrCode: billing.pixQrCode, + expiresAt: billing.expiresAt, + }); + } catch (error) { + console.error("Donation creation error:", error); + return NextResponse.json( + { + success: false, + error: + error instanceof Error ? error.message : "Erro interno do servidor", + }, + { status: 500 } + ); + } +} diff --git a/app/api/donations/verify/route.ts b/app/api/donations/verify/route.ts new file mode 100644 index 0000000..0d5b5cf --- /dev/null +++ b/app/api/donations/verify/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { abacatePay } from "@/lib/abacatepay"; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const billingId = searchParams.get("billingId"); + + if (!billingId) { + return NextResponse.json( + { success: false, error: "ID da cobrança é obrigatório" }, + { status: 400 } + ); + } + + const billing = await abacatePay.getBillingStatus(billingId); + + return NextResponse.json({ + success: true, + status: billing.status, + amount: billing.amount, + createdAt: billing.createdAt, + expiresAt: billing.expiresAt, + }); + } catch (error) { + console.error("Billing verification error:", error); + return NextResponse.json( + { + success: false, + error: + error instanceof Error ? error.message : "Erro ao verificar cobrança", + }, + { status: 500 } + ); + } +} diff --git a/app/api/webhooks/abacatepay/route.ts b/app/api/webhooks/abacatepay/route.ts new file mode 100644 index 0000000..4908f46 --- /dev/null +++ b/app/api/webhooks/abacatepay/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import { abacatePay } from "@/lib/abacatepay"; + +export async function POST(request: NextRequest) { + try { + const payload = await request.text(); + const signature = request.headers.get("x-abacatepay-signature"); + + if (!signature || !abacatePay.verifyWebhookSignature(payload, signature)) { + return NextResponse.json( + { error: "Assinatura inválida" }, + { status: 401 } + ); + } + + const event = JSON.parse(payload); + + if (event.type === "billing.paid") { + // Processar doação confirmada + console.log("Donation confirmed:", event.data.id); + + // Aqui você pode: + // 1. Salvar no banco de dados + // 2. Enviar email de agradecimento + // 3. Atualizar estatísticas + // 4. Notificar a equipe + + // Por enquanto, apenas log + console.log("Donation details:", { + id: event.data.id, + amount: event.data.amount, + metadata: event.data.metadata, + paidAt: event.data.paidAt, + }); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Webhook error:", error); + return NextResponse.json( + { error: "Erro ao processar webhook" }, + { status: 500 } + ); + } +} diff --git a/components/Header.tsx b/components/Header.tsx index cd12236..c217f3c 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -23,6 +23,7 @@ export default function Header() { { href: `/${locale}/market`, label: t("market") }, { href: `/${locale}/sobre`, label: t("about") }, { href: `/${locale}/precos`, label: t("prices") }, + { href: `/${locale}/apoiar`, label: "Apoiar", highlight: true }, ]; return ( @@ -41,14 +42,16 @@ export default function Header() { {/* Desktop Navigation */} - {links.map(({ href, label }) => ( + {links.map(({ href, label, highlight }) => ( {label} diff --git a/components/donation-card.tsx b/components/donation-card.tsx new file mode 100644 index 0000000..270390e --- /dev/null +++ b/components/donation-card.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Heart, Coffee, Pizza, Loader2, Copy, Check } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import QRCode from "react-qr-code"; +import { DonationTier, DonationResponse } from "@/types/donation"; + +interface DonationCardProps extends DonationTier {} + +export function DonationCard({ + amount, + title, + description, + icon, + badge, + highlighted, +}: DonationCardProps) { + const [customAmount, setCustomAmount] = useState(amount || 10); + const [loading, setLoading] = useState(false); + const [showPixModal, setShowPixModal] = useState(false); + const [pixData, setPixData] = useState<{ + code: string; + qrCode: string; + } | null>(null); + const [copied, setCopied] = useState(false); + + const icons = { + coffee: Coffee, + pizza: Pizza, + heart: Heart, + }; + + const Icon = icons[icon]; + + const handleDonate = async () => { + setLoading(true); + try { + const response = await fetch("/api/donations/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ amount: customAmount }), + }); + + const data: DonationResponse = await response.json(); + + if (data.success && data.pixCode && data.pixQrCode) { + setPixData({ + code: data.pixCode, + qrCode: data.pixQrCode, + }); + setShowPixModal(true); + } else { + console.error("Erro ao criar doação:", data.error); + alert("Erro ao processar doação. Tente novamente."); + } + } catch (error) { + console.error("Donation error:", error); + alert("Erro ao processar doação. Tente novamente."); + } finally { + setLoading(false); + } + }; + + const handleCopyPixCode = async () => { + if (pixData?.code) { + try { + await navigator.clipboard.writeText(pixData.code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error("Erro ao copiar código PIX:", error); + } + } + }; + + return ( + <> +
+ {badge && ( + + {badge} + + )} + +
+
+ +
+ +

{title}

+

{description}

+ + {amount ? ( +
+ R$ {amount.toFixed(2)} +
+ ) : ( + setCustomAmount(Number(e.target.value))} + min={5} + max={10000} + step={5} + className="text-center text-2xl font-bold" + placeholder="R$ 10,00" + /> + )} + + +
+
+ + + + + + Escaneie o QR Code para Pagar + + + + {pixData && ( +
+
+ +
+ +
+

+ Ou copie o código PIX: +

+
+ + +
+
+ +

+ O pagamento expira em 30 minutos +

+ +
+ + +
+
+ )} +
+
+ + ); +} diff --git a/components/donation-footer-card.tsx b/components/donation-footer-card.tsx new file mode 100644 index 0000000..88c2c9e --- /dev/null +++ b/components/donation-footer-card.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Heart, Coffee } from "lucide-react"; +import { BlurFade } from "@/components/ui/blur-fade"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; + +export function DonationFooterCard() { + const t = useTranslations("donation"); + + return ( + +
+
+
+ +
+ +
+

{t("title")}

+

{t("subtitle")}

+
+ + + + +
+
+
+ ); +} diff --git a/components/share-button.tsx b/components/share-button.tsx new file mode 100644 index 0000000..ec2c8a2 --- /dev/null +++ b/components/share-button.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { Share2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export function ShareButton() { + const handleShare = () => { + if (navigator.share) { + navigator.share({ + title: "Tibia Trade Pagodera", + text: "Conheça o marketplace mais seguro para negociar itens no servidor Pagodera!", + url: window.location.origin, + }); + } else { + navigator.clipboard.writeText(window.location.origin); + alert("Link copiado para a área de transferência!"); + } + }; + + return ( + + ); +} diff --git a/lib/abacatepay.ts b/lib/abacatepay.ts new file mode 100644 index 0000000..92010ed --- /dev/null +++ b/lib/abacatepay.ts @@ -0,0 +1,100 @@ +interface AbacatePayConfig { + apiKey: string; + accountId: string; + baseUrl?: string; +} + +interface CreateBillingParams { + amount: number; + metadata?: { + donorName?: string; + donorEmail?: string; + message?: string; + }; + returnUrl?: string; + completionUrl?: string; +} + +interface BillingResponse { + id: string; + amount: number; + status: "PENDING" | "PAID" | "EXPIRED" | "CANCELLED"; + pixCode: string; + pixQrCode: string; + expiresAt: string; + createdAt: string; +} + +class AbacatePayClient { + private config: AbacatePayConfig; + + constructor(config: AbacatePayConfig) { + this.config = { + ...config, + baseUrl: config.baseUrl || "https://api.abacatepay.com/v1", + }; + } + + async createBilling(params: CreateBillingParams): Promise { + const response = await fetch(`${this.config.baseUrl}/billing`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + account_id: this.config.accountId, + amount: params.amount * 100, // centavos + metadata: params.metadata, + return_url: params.returnUrl, + completion_url: params.completionUrl, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + `Failed to create billing: ${errorData.message || response.statusText}` + ); + } + + return response.json(); + } + + async getBillingStatus(billingId: string): Promise { + const response = await fetch( + `${this.config.baseUrl}/billing/${billingId}`, + { + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + }, + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + `Failed to get billing status: ${ + errorData.message || response.statusText + }` + ); + } + + return response.json(); + } + + verifyWebhookSignature(payload: string, signature: string): boolean { + // Implementar verificação de assinatura conforme documentação do AbacatePay + // Por enquanto, retorna true para desenvolvimento + // Em produção, implementar a verificação real + return true; + } +} + +// Instância do cliente (será configurada com variáveis de ambiente) +export const abacatePay = new AbacatePayClient({ + apiKey: process.env.ABACATEPAY_API_KEY || "demo_key", + accountId: process.env.ABACATEPAY_ACCOUNT_ID || "demo_account", +}); + +export type { CreateBillingParams, BillingResponse }; diff --git a/messages/en.json b/messages/en.json index e496758..85ced38 100644 --- a/messages/en.json +++ b/messages/en.json @@ -187,5 +187,76 @@ "title": "Prices Page Under Construction", "description": "We're finalizing our pricing structure and plans. Check back soon!" } + }, + "donation": { + "title": "Support Tibia Trade Pagodera", + "subtitle": "We keep the project free with help from people like you", + "story": { + "title": "Our Project, Our Community", + "text": "Tibia Trade Pagodera was born from the passion to create a safer and more efficient trading experience for our community. We started as a small project, but with big dreams of revolutionizing how players trade items on the Pagodera server.\n\nOver the months, we faced technical challenges, learned from our community, and constantly evolved. Every line of code was written thinking of you, players who trust our platform for their trades.\n\nToday, we are more than a trading platform - we are a community united by passion for Tibia and mutual trust." + }, + "transparency": { + "title": "How We Use Your Donations", + "hosting": "Hosting & Infrastructure", + "development": "Development & Improvements", + "support": "Support & Moderation", + "emergency": "Emergency Reserve", + "hostingDesc": "Servers, CDN and infrastructure to keep the platform always online", + "developmentDesc": "New features, fixes and improvements to user experience", + "supportDesc": "Support team and moderation to keep the community safe", + "emergencyDesc": "Reserve for emergencies and critical improvements" + }, + "social": { + "supporters": "supporters", + "raised": "raised", + "months": "months online", + "uptime": "uptime" + }, + "tiers": { + "coffee": { + "title": "A Coffee", + "description": "Helps with basic costs", + "badge": "Popular" + }, + "lunch": { + "title": "Team Lunch", + "description": "Keeps the server running for 1 day", + "badge": "Recommended" + }, + "custom": { + "title": "Custom Amount", + "description": "You choose how much you want to support" + } + }, + "cta": { + "support": "Support with PIX", + "share": "Share Project", + "final": "Every help makes a difference", + "subtitle": "Even $5 helps us keep the project alive and free for everyone" + }, + "faq": { + "title": "Frequently Asked Questions", + "q1": "Where does my money go?", + "a1": "All donations are used exclusively to maintain and improve the platform. We publish quarterly reports showing exactly how the money was spent.", + "q2": "Do I get anything in return?", + "a2": "The project remains 100% free for everyone. Your reward is knowing you're helping maintain a tool that benefits the entire community.", + "q3": "How does PIX payment work?", + "a3": "You scan the QR Code or copy the PIX code and make the payment through your banking app. Payment is instant and secure.", + "q4": "Can I cancel my donation?", + "a4": "Donations are one-time and non-recurring. No cancellation needed - you pay only once for the chosen amount.", + "q5": "Are you a company?", + "a5": "We are an independent project created by players for players. We are not a commercial company, just Tibia enthusiasts." + }, + "supporters": { + "title": "Our Amazing Supporters", + "viewAll": "View all supporters" + }, + "thankYou": { + "title": "Thank you for your support!", + "subtitle": "Your contribution makes all the difference to our community", + "message": "We received your donation and are very grateful for your support. With people like you, we can keep Tibia Trade Pagodera free and constantly improve our platform.", + "share": "Share with friends", + "back": "Back to home" + } } } diff --git a/messages/pt.json b/messages/pt.json index 5e51667..dd1c3cc 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -187,5 +187,76 @@ "title": "Página de Preços em Construção", "description": "Estamos finalizando nossa estrutura de preços e planos. Volte em breve para conferir!" } + }, + "donation": { + "title": "Apoie o Tibia Trade Pagodera", + "subtitle": "Mantemos o projeto gratuito com a ajuda de pessoas como você", + "story": { + "title": "Nosso Projeto, Nossa Comunidade", + "text": "O Tibia Trade Pagodera nasceu da paixão por criar uma experiência de trading mais segura e eficiente para nossa comunidade. Começamos como um projeto pequeno, mas com grandes sonhos de revolucionar como os jogadores negociam itens no servidor Pagodera.\n\nAo longo dos meses, enfrentamos desafios técnicos, aprendemos com nossa comunidade e evoluímos constantemente. Cada linha de código foi escrita pensando em vocês, jogadores que confiam em nossa plataforma para suas negociações.\n\nHoje, somos mais que uma plataforma de trading - somos uma comunidade unida pela paixão pelo Tibia e pela confiança mútua." + }, + "transparency": { + "title": "Como Usamos Suas Doações", + "hosting": "Hospedagem & Infraestrutura", + "development": "Desenvolvimento & Melhorias", + "support": "Suporte & Moderação", + "emergency": "Reserva de Emergência", + "hostingDesc": "Servidores, CDN e infraestrutura para manter a plataforma sempre online", + "developmentDesc": "Novas funcionalidades, correções e melhorias na experiência do usuário", + "supportDesc": "Equipe de suporte e moderação para manter a comunidade segura", + "emergencyDesc": "Reserva para emergências e melhorias críticas" + }, + "social": { + "supporters": "apoiadores", + "raised": "arrecadados", + "months": "meses online", + "uptime": "uptime" + }, + "tiers": { + "coffee": { + "title": "Um Café", + "description": "Ajuda com custos básicos", + "badge": "Popular" + }, + "lunch": { + "title": "Almoço da Equipe", + "description": "Mantém o servidor rodando por 1 dia", + "badge": "Recomendado" + }, + "custom": { + "title": "Valor Livre", + "description": "Você escolhe quanto quer apoiar" + } + }, + "cta": { + "support": "Apoiar com PIX", + "share": "Compartilhar Projeto", + "final": "Toda ajuda faz diferença", + "subtitle": "Mesmo R$ 5 nos ajuda a manter o projeto vivo e gratuito para todos" + }, + "faq": { + "title": "Perguntas Frequentes", + "q1": "Para onde vai meu dinheiro?", + "a1": "Todas as doações são usadas exclusivamente para manter e melhorar a plataforma. Publicamos relatórios trimestrais mostrando exatamente como o dinheiro foi gasto.", + "q2": "Recebo algo em troca?", + "a2": "O projeto continua 100% gratuito para todos. Sua recompensa é saber que está ajudando a manter uma ferramenta que beneficia toda a comunidade.", + "q3": "Como funciona o pagamento PIX?", + "a3": "Você escaneia o QR Code ou copia o código PIX e faz o pagamento pelo seu app bancário. O pagamento é instantâneo e seguro.", + "q4": "Posso cancelar minha doação?", + "a4": "As doações são únicas e não recorrentes. Não há cancelamento necessário - você paga apenas uma vez pelo valor escolhido.", + "q5": "Vocês são uma empresa?", + "a5": "Somos um projeto independente criado por jogadores para jogadores. Não somos uma empresa comercial, apenas apaixonados por Tibia." + }, + "supporters": { + "title": "Nossos Incríveis Apoiadores", + "viewAll": "Ver todos os apoiadores" + }, + "thankYou": { + "title": "Obrigado pelo seu apoio!", + "subtitle": "Sua contribuição faz toda a diferença para nossa comunidade", + "message": "Recebemos sua doação e ficamos muito gratos pelo seu apoio. Com pessoas como você, conseguimos manter o Tibia Trade Pagodera gratuito e melhorar constantemente nossa plataforma.", + "share": "Compartilhe com amigos", + "back": "Voltar ao início" + } } } diff --git a/package.json b/package.json index 865f444..6e6a1bd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "next-themes": "^0.4.6", "react": "19.1.0", "react-dom": "19.1.0", + "react-qr-code": "^2.0.18", "recharts": "^3.3.0", "tailwind-merge": "^3.3.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d18b300..4684f13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: react-dom: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + react-qr-code: + specifier: ^2.0.18 + version: 2.0.18(react@19.1.0) recharts: specifier: ^3.3.0 version: 3.3.0(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react-is@19.2.0)(react@19.1.0)(redux@5.0.1) @@ -1055,6 +1058,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -1125,6 +1131,10 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + lucide-react@0.540.0: resolution: {integrity: sha512-armkCAqQvO62EIX4Hq7hqX/q11WSZu0Jd23cnnqx0/49yIxGXyL/zyZfBxNN9YDx0ensPTb4L+DjTh3yQXUxtQ==} peerDependencies: @@ -1199,6 +1209,10 @@ packages: sass: optional: true + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + phenomenon@1.6.0: resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==} @@ -1213,14 +1227,28 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + qr.js@0.0.0: + resolution: {integrity: sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==} + react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: react: ^19.1.0 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@19.2.0: resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + react-qr-code@2.0.18: + resolution: {integrity: sha512-v1Jqz7urLMhkO6jkgJuBYhnqvXagzceg3qJUWayuCK/c6LTIonpWbwxR1f1APGd4xrW/QcQEovNrAojbUz65Tg==} + peerDependencies: + react: '*' + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -2211,6 +2239,8 @@ snapshots: jiti@2.6.1: {} + js-tokens@4.0.0: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -2260,6 +2290,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + lucide-react@0.540.0(react@19.1.0): dependencies: react: 19.1.0 @@ -2324,6 +2358,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + object-assign@4.1.1: {} + phenomenon@1.6.0: {} picocolors@1.1.1: {} @@ -2340,13 +2376,29 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + qr.js@0.0.0: {} + react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 scheduler: 0.26.0 + react-is@16.13.1: {} + react-is@19.2.0: {} + react-qr-code@2.0.18(react@19.1.0): + dependencies: + prop-types: 15.8.1 + qr.js: 0.0.0 + react: 19.1.0 + react-redux@9.2.0(@types/react@19.2.2)(react@19.1.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 diff --git a/types/donation.ts b/types/donation.ts new file mode 100644 index 0000000..5450cad --- /dev/null +++ b/types/donation.ts @@ -0,0 +1,59 @@ +export interface DonationTier { + id: string; + amount?: number; + title: string; + description: string; + icon: "coffee" | "pizza" | "heart"; + badge?: string; + highlighted?: boolean; +} + +export interface DonationData { + amount: number; + donorName?: string; + donorEmail?: string; + message?: string; +} + +export interface DonationResponse { + success: boolean; + billingId?: string; + pixCode?: string; + pixQrCode?: string; + expiresAt?: string; + error?: string; +} + +export interface BillingStatus { + id: string; + amount: number; + status: "PENDING" | "PAID" | "EXPIRED" | "CANCELLED"; + pixCode: string; + pixQrCode: string; + expiresAt: string; + createdAt: string; +} + +export interface DonationStats { + totalSupporters: number; + totalRaised: number; + monthsOnline: number; + uptime: number; +} + +export interface TransparencyData { + hosting: number; + development: number; + support: number; + emergency: number; +} + +export interface Supporter { + id: string; + name: string; + avatar?: string; + amount: number; + message?: string; + createdAt: string; + anonymous?: boolean; +} From 00501e6699eac4a63753969b93c9e387368e308a Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 26 Oct 2025 18:32:15 -0300 Subject: [PATCH 02/11] refactor(support): replace mock data with real costs and simplify stats Updated support page to use real data instead of mock values, including monthly costs --- ABACATEPAY_SETUP.md | 43 ++++++++++++ app/[locale]/apoiar/page.tsx | 112 +++++++++--------------------- app/api/donations/create/route.ts | 22 +++--- components/donation-card.tsx | 6 +- lib/abacatepay.ts | 73 +++++++++++++++++-- messages/en.json | 63 +++++++++-------- messages/pt.json | 65 +++++++++-------- types/donation.ts | 1 + 8 files changed, 235 insertions(+), 150 deletions(-) create mode 100644 ABACATEPAY_SETUP.md diff --git a/ABACATEPAY_SETUP.md b/ABACATEPAY_SETUP.md new file mode 100644 index 0000000..13d68fb --- /dev/null +++ b/ABACATEPAY_SETUP.md @@ -0,0 +1,43 @@ +# Variáveis de Ambiente - AbacatePay + +## Configuração para Desenvolvimento/Teste + +```env +# Chave de API para ambiente de teste +ABACATE_TEST_APIKEY=your_test_api_key_here + +# ID da conta AbacatePay +ABACATEPAY_ACCOUNT_ID=your_account_id_here + +# URLs da aplicação +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_ABACATEPAY_WEBHOOK_URL=http://localhost:3000/api/webhooks/abacatepay +``` + +## Configuração para Produção + +```env +# Chave de API para ambiente de produção (usado automaticamente quando NODE_ENV=production) +ABACATE_APIKEY=your_production_api_key_here + +# ID da conta AbacatePay +ABACATEPAY_ACCOUNT_ID=your_account_id_here + +# URLs da aplicação +NEXT_PUBLIC_BASE_URL=https://seudominio.com +NEXT_PUBLIC_ABACATEPAY_WEBHOOK_URL=https://seudominio.com/api/webhooks/abacatepay +``` + +## Como Funciona + +O sistema automaticamente escolhe a chave correta baseado no ambiente: + +- **Desenvolvimento/Teste**: Usa `ABACATE_TEST_APIKEY` +- **Produção**: Usa `ABACATE_APIKEY` (quando `NODE_ENV=production`) + +## Obtenção das Chaves + +1. Acesse o painel do AbacatePay +2. Vá em "Configurações" > "API Keys" +3. Gere uma chave para teste e outra para produção +4. Configure as URLs de webhook no painel do AbacatePay diff --git a/app/[locale]/apoiar/page.tsx b/app/[locale]/apoiar/page.tsx index 1dccefd..9bcef60 100644 --- a/app/[locale]/apoiar/page.tsx +++ b/app/[locale]/apoiar/page.tsx @@ -10,7 +10,7 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; -import { Heart, Server, Code, Shield, AlertTriangle } from "lucide-react"; +import { Heart } from "lucide-react"; import { getTranslations } from "next-intl/server"; import Link from "next/link"; import { ShareButton } from "@/components/share-button"; @@ -36,44 +36,19 @@ export default async function SupportPage({ const { locale } = await params; const t = await getTranslations({ locale, namespace: "donation" }); - // Dados mockados para demonstração + // Dados REAIS para demonstração const stats = { - totalSupporters: 1247, - totalRaised: 18542, - monthsOnline: 12, - uptime: 99.8, + monthsOnline: 1, + uptime: 100, }; - const transparencyData = [ - { - icon: Server, - title: t("transparency.hosting"), - percentage: 40, - description: t("transparency.hostingDesc"), - color: "text-blue-500", - }, - { - icon: Code, - title: t("transparency.development"), - percentage: 35, - description: t("transparency.developmentDesc"), - color: "text-green-500", - }, - { - icon: Shield, - title: t("transparency.support"), - percentage: 15, - description: t("transparency.supportDesc"), - color: "text-purple-500", - }, - { - icon: AlertTriangle, - title: t("transparency.emergency"), - percentage: 10, - description: t("transparency.emergencyDesc"), - color: "text-orange-500", - }, - ]; + // Custos mensais REAIS + const monthlyCosts = { + vps: 69, // R$ 69/mês + devHours: 40, // média horas/mês + devRate: 50, // R$ 50/hora + total: 2069, // R$ 69 + (40 * 50) + }; const faqItems = [ { @@ -136,58 +111,39 @@ export default async function SupportPage({ - {/* Transparência */} + {/* Custos Reais */}

- {t("transparency.title")} + {t("costs.title")}

-
- {transparencyData.map((item, idx) => ( -
-
-
- -
-
-
-

{item.title}

- {item.percentage}% -
-

- {item.description} -

-
-
-
- ))} +
+
+ {t("costs.vps")} + + {t("costs.vpsValue")} + +
+
+ {t("costs.dev")} + + {t("costs.devValue")} + +
+
+ {t("costs.total")} + + {t("costs.totalValue")} + +
- {/* Prova Social */} + {/* Estatísticas Simplificadas */}
-
-
-

- -

-

- {t("social.supporters")} -

-
-
-

- R$ -

-

- {t("social.raised")} -

-
+

diff --git a/app/api/donations/create/route.ts b/app/api/donations/create/route.ts index f7ec4b0..faa1434 100644 --- a/app/api/donations/create/route.ts +++ b/app/api/donations/create/route.ts @@ -17,22 +17,24 @@ export async function POST(request: NextRequest) { ); } - const billing = await abacatePay.createBilling({ + const billing = await abacatePay.createDonationBilling({ amount: body.amount, - metadata: { - donorName: body.donorName, - donorEmail: body.donorEmail, - message: body.message, - }, - returnUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/apoiar/obrigado`, - completionUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/donations/verify`, + donorEmail: body.donorEmail, + donorName: body.donorName, + message: body.message, }); + // AbacatePay pode retornar diferentes formatos + const pixCode = billing.pixCode || billing.qrCode; + const pixQrCode = billing.pixQrCode || billing.qrCode; + const paymentUrl = billing.url || billing.paymentUrl; + return NextResponse.json({ success: true, billingId: billing.id, - pixCode: billing.pixCode, - pixQrCode: billing.pixQrCode, + paymentUrl, // URL para redirecionar + pixCode, + pixQrCode, expiresAt: billing.expiresAt, }); } catch (error) { diff --git a/components/donation-card.tsx b/components/donation-card.tsx index 270390e..3e5334a 100644 --- a/components/donation-card.tsx +++ b/components/donation-card.tsx @@ -52,7 +52,11 @@ export function DonationCard({ const data: DonationResponse = await response.json(); - if (data.success && data.pixCode && data.pixQrCode) { + if (data.success && data.paymentUrl) { + // Redirecionar para página de pagamento do AbacatePay + window.location.href = data.paymentUrl; + } else if (data.success && data.pixCode && data.pixQrCode) { + // Fallback: mostrar modal com QR Code se disponível setPixData({ code: data.pixCode, qrCode: data.pixQrCode, diff --git a/lib/abacatepay.ts b/lib/abacatepay.ts index 92010ed..6f3384e 100644 --- a/lib/abacatepay.ts +++ b/lib/abacatepay.ts @@ -19,10 +19,13 @@ interface BillingResponse { id: string; amount: number; status: "PENDING" | "PAID" | "EXPIRED" | "CANCELLED"; - pixCode: string; - pixQrCode: string; - expiresAt: string; + pixCode?: string; + pixQrCode?: string; + qrCode?: string; + expiresAt?: string; createdAt: string; + url?: string; // URL para redirecionamento + paymentUrl?: string; // URL alternativa de pagamento } class AbacatePayClient { @@ -61,6 +64,56 @@ class AbacatePayClient { return response.json(); } + async createDonationBilling(params: { + amount: number; + donorEmail?: string; + donorName?: string; + message?: string; + }): Promise { + const requestBody = { + products: [ + { + external_id: `donation-${Date.now()}`, + name: "Doação - Tibia Trade Pagodera", + quantity: 1, + price: Math.round(params.amount * 100), // R$ para centavos + }, + ], + return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/apoiar/obrigado`, + completion_url: `${process.env.NEXT_PUBLIC_BASE_URL}/api/donations/verify`, + }; + + console.log("🔍 AbacatePay Request URL:", `${this.config.baseUrl}/billing`); + console.log( + "🔍 AbacatePay Request Body:", + JSON.stringify(requestBody, null, 2) + ); + + const response = await fetch(`${this.config.baseUrl}/billing`, { + method: "POST", + headers: { + Authorization: `Bearer ${this.config.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + console.log("🔍 AbacatePay Response Status:", response.status); + console.log("🔍 AbacatePay Response OK:", response.ok); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + console.error("🔍 AbacatePay Error Response:", errorData); + throw new Error( + `Failed to create billing: ${errorData.message || response.statusText}` + ); + } + + const result = await response.json(); + console.log("🔍 AbacatePay Success Response:", result); + return result; + } + async getBillingStatus(billingId: string): Promise { const response = await fetch( `${this.config.baseUrl}/billing/${billingId}`, @@ -92,8 +145,20 @@ class AbacatePayClient { } // Instância do cliente (será configurada com variáveis de ambiente) +const isProduction = process.env.NODE_ENV === "production"; +const apiKey = isProduction + ? process.env.ABACATE_APIKEY || "demo_key" + : process.env.ABACATE_TEST_APIKEY || "demo_key"; + +// Log para debug (apenas em desenvolvimento) +if (process.env.NODE_ENV !== "production") { + console.log( + `🔧 AbacatePay: Using ${isProduction ? "PRODUCTION" : "TEST"} environment` + ); +} + export const abacatePay = new AbacatePayClient({ - apiKey: process.env.ABACATEPAY_API_KEY || "demo_key", + apiKey, accountId: process.env.ABACATEPAY_ACCOUNT_ID || "demo_account", }); diff --git a/messages/en.json b/messages/en.json index 85ced38..08d73cf 100644 --- a/messages/en.json +++ b/messages/en.json @@ -189,63 +189,70 @@ } }, "donation": { - "title": "Support Tibia Trade Pagodera", - "subtitle": "We keep the project free with help from people like you", + "title": "Support Project Development", + "subtitle": "Your voluntary donation helps maintain and evolve the platform", "story": { "title": "Our Project, Our Community", - "text": "Tibia Trade Pagodera was born from the passion to create a safer and more efficient trading experience for our community. We started as a small project, but with big dreams of revolutionizing how players trade items on the Pagodera server.\n\nOver the months, we faced technical challenges, learned from our community, and constantly evolved. Every line of code was written thinking of you, players who trust our platform for their trades.\n\nToday, we are more than a trading platform - we are a community united by passion for Tibia and mutual trust." + "text": "Tibia Trade Pagodera is developed with dedication to create the best trading experience for our community. Each voluntary donation helps us continue evolving and maintaining service quality.\n\nWe are not a company, we are developers passionate about Tibia who believe in total transparency. Every penny is invested directly in the project.\n\nYour trust and support make all the difference." }, "transparency": { - "title": "How We Use Your Donations", - "hosting": "Hosting & Infrastructure", - "development": "Development & Improvements", - "support": "Support & Moderation", + "title": "Total Cost Transparency", + "hosting": "VPS Infrastructure", + "development": "Development", + "support": "Maintenance & Support", "emergency": "Emergency Reserve", - "hostingDesc": "Servers, CDN and infrastructure to keep the platform always online", - "developmentDesc": "New features, fixes and improvements to user experience", - "supportDesc": "Support team and moderation to keep the community safe", - "emergencyDesc": "Reserve for emergencies and critical improvements" + "hostingDesc": "R$ 69/month - Dedicated server to ensure performance", + "developmentDesc": "R$ 50/hour - Real cost of development and improvements", + "supportDesc": "Monitoring, bug fixes and customer service", + "emergencyDesc": "Buffer for unforeseen situations and scalability" + }, + "costs": { + "title": "Estimated Monthly Costs", + "vps": "VPS (Server)", + "dev": "Development (average 40h/month)", + "total": "Total Monthly Estimate", + "vpsValue": "R$ 69/month", + "devValue": "R$ 2,000/month", + "totalValue": "R$ 2,069/month" }, "social": { - "supporters": "supporters", - "raised": "raised", - "months": "months online", + "months": "month online", "uptime": "uptime" }, "tiers": { "coffee": { "title": "A Coffee", - "description": "Helps with basic costs", + "description": "Equals ~10 minutes of development", "badge": "Popular" }, "lunch": { "title": "Team Lunch", - "description": "Keeps the server running for 1 day", + "description": "Equals ~30 minutes of development", "badge": "Recommended" }, "custom": { "title": "Custom Amount", - "description": "You choose how much you want to support" + "description": "You choose how much you want to contribute" } }, "cta": { - "support": "Support with PIX", + "support": "Make Donation", "share": "Share Project", - "final": "Every help makes a difference", - "subtitle": "Even $5 helps us keep the project alive and free for everyone" + "final": "Every contribution matters", + "subtitle": "Each donation helps us dedicate more time to the project and keep the infrastructure running" }, "faq": { "title": "Frequently Asked Questions", - "q1": "Where does my money go?", - "a1": "All donations are used exclusively to maintain and improve the platform. We publish quarterly reports showing exactly how the money was spent.", - "q2": "Do I get anything in return?", - "a2": "The project remains 100% free for everyone. Your reward is knowing you're helping maintain a tool that benefits the entire community.", + "q1": "Is the project free?", + "a1": "The project charges transaction fees to sustain itself. Donations are voluntary for those who want to contribute beyond normal usage fees.", + "q2": "Where does my donation go?", + "a2": "100% invested in the project: server cost (R$ 69/month) and development hours (R$ 50/hour). We maintain total transparency of expenses.", "q3": "How does PIX payment work?", - "a3": "You scan the QR Code or copy the PIX code and make the payment through your banking app. Payment is instant and secure.", - "q4": "Can I cancel my donation?", - "a4": "Donations are one-time and non-recurring. No cancellation needed - you pay only once for the chosen amount.", + "a3": "You choose the amount, we generate a QR Code or PIX code, you pay through your banking app. Instant and secure payment.", + "q4": "Do I get anything in return?", + "a4": "We don't offer direct benefits. Your donation is voluntary and helps the project evolve for everyone.", "q5": "Are you a company?", - "a5": "We are an independent project created by players for players. We are not a commercial company, just Tibia enthusiasts." + "a5": "No. We are independent developers passionate about Tibia. Transparency and community are our values." }, "supporters": { "title": "Our Amazing Supporters", diff --git a/messages/pt.json b/messages/pt.json index dd1c3cc..e75c509 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -189,63 +189,70 @@ } }, "donation": { - "title": "Apoie o Tibia Trade Pagodera", - "subtitle": "Mantemos o projeto gratuito com a ajuda de pessoas como você", + "title": "Apoie o Desenvolvimento do Projeto", + "subtitle": "Sua doação voluntária ajuda a manter e evoluir a plataforma", "story": { "title": "Nosso Projeto, Nossa Comunidade", - "text": "O Tibia Trade Pagodera nasceu da paixão por criar uma experiência de trading mais segura e eficiente para nossa comunidade. Começamos como um projeto pequeno, mas com grandes sonhos de revolucionar como os jogadores negociam itens no servidor Pagodera.\n\nAo longo dos meses, enfrentamos desafios técnicos, aprendemos com nossa comunidade e evoluímos constantemente. Cada linha de código foi escrita pensando em vocês, jogadores que confiam em nossa plataforma para suas negociações.\n\nHoje, somos mais que uma plataforma de trading - somos uma comunidade unida pela paixão pelo Tibia e pela confiança mútua." + "text": "O Tibia Trade Pagodera é desenvolvido com dedicação para criar a melhor experiência de trading para nossa comunidade. Cada doação voluntária nos ajuda a continuar evoluindo e mantendo a qualidade do serviço.\n\nNão somos uma empresa, somos desenvolvedores apaixonados por Tibia que acreditam em transparência total. Todo centavo é investido diretamente no projeto.\n\nSua confiança e apoio fazem toda a diferença." }, "transparency": { - "title": "Como Usamos Suas Doações", - "hosting": "Hospedagem & Infraestrutura", - "development": "Desenvolvimento & Melhorias", - "support": "Suporte & Moderação", - "emergency": "Reserva de Emergência", - "hostingDesc": "Servidores, CDN e infraestrutura para manter a plataforma sempre online", - "developmentDesc": "Novas funcionalidades, correções e melhorias na experiência do usuário", - "supportDesc": "Equipe de suporte e moderação para manter a comunidade segura", - "emergencyDesc": "Reserva para emergências e melhorias críticas" + "title": "Transparência Total de Custos", + "hosting": "Infraestrutura VPS", + "development": "Desenvolvimento", + "support": "Manutenção & Suporte", + "emergency": "Reserva para Emergências", + "hostingDesc": "R$ 69/mês - Servidor dedicado para garantir performance", + "developmentDesc": "R$ 50/hora - Custo real de desenvolvimento e melhorias", + "supportDesc": "Monitoramento, correções de bugs e atendimento", + "emergencyDesc": "Buffer para situações imprevistas e escalabilidade" + }, + "costs": { + "title": "Custos Mensais Estimados", + "vps": "VPS (Servidor)", + "dev": "Desenvolvimento (média 40h/mês)", + "total": "Total Mensal Estimado", + "vpsValue": "R$ 69/mês", + "devValue": "R$ 2.000/mês", + "totalValue": "R$ 2.069/mês" }, "social": { - "supporters": "apoiadores", - "raised": "arrecadados", - "months": "meses online", + "months": "mês online", "uptime": "uptime" }, "tiers": { "coffee": { "title": "Um Café", - "description": "Ajuda com custos básicos", + "description": "Equivale a ~10 minutos de desenvolvimento", "badge": "Popular" }, "lunch": { "title": "Almoço da Equipe", - "description": "Mantém o servidor rodando por 1 dia", + "description": "Equivale a ~30 minutos de desenvolvimento", "badge": "Recomendado" }, "custom": { "title": "Valor Livre", - "description": "Você escolhe quanto quer apoiar" + "description": "Você escolhe quanto quer contribuir" } }, "cta": { - "support": "Apoiar com PIX", + "support": "Fazer Doação", "share": "Compartilhar Projeto", - "final": "Toda ajuda faz diferença", - "subtitle": "Mesmo R$ 5 nos ajuda a manter o projeto vivo e gratuito para todos" + "final": "Toda contribuição é importante", + "subtitle": "Cada doação nos ajuda a dedicar mais tempo ao projeto e manter a infraestrutura funcionando" }, "faq": { "title": "Perguntas Frequentes", - "q1": "Para onde vai meu dinheiro?", - "a1": "Todas as doações são usadas exclusivamente para manter e melhorar a plataforma. Publicamos relatórios trimestrais mostrando exatamente como o dinheiro foi gasto.", - "q2": "Recebo algo em troca?", - "a2": "O projeto continua 100% gratuito para todos. Sua recompensa é saber que está ajudando a manter uma ferramenta que beneficia toda a comunidade.", + "q1": "O projeto é gratuito?", + "a1": "O projeto cobra taxas de transação para se manter. As doações são voluntárias para quem deseja contribuir além das taxas normais de uso.", + "q2": "Para onde vai minha doação?", + "a2": "100% investido no projeto: custo de servidor (R$ 69/mês) e horas de desenvolvimento (R$ 50/hora). Mantemos transparência total dos gastos.", "q3": "Como funciona o pagamento PIX?", - "a3": "Você escaneia o QR Code ou copia o código PIX e faz o pagamento pelo seu app bancário. O pagamento é instantâneo e seguro.", - "q4": "Posso cancelar minha doação?", - "a4": "As doações são únicas e não recorrentes. Não há cancelamento necessário - você paga apenas uma vez pelo valor escolhido.", + "a3": "Você escolhe o valor, geramos um QR Code ou código PIX, você paga pelo seu app bancário. Pagamento instantâneo e seguro.", + "q4": "Recebo algo em troca?", + "a4": "Não oferecemos benefícios diretos. Sua doação é voluntária e ajuda o projeto a evoluir para todos.", "q5": "Vocês são uma empresa?", - "a5": "Somos um projeto independente criado por jogadores para jogadores. Não somos uma empresa comercial, apenas apaixonados por Tibia." + "a5": "Não. Somos desenvolvedores independentes apaixonados por Tibia. Transparência e comunidade são nossos valores." }, "supporters": { "title": "Nossos Incríveis Apoiadores", diff --git a/types/donation.ts b/types/donation.ts index 5450cad..1807420 100644 --- a/types/donation.ts +++ b/types/donation.ts @@ -18,6 +18,7 @@ export interface DonationData { export interface DonationResponse { success: boolean; billingId?: string; + paymentUrl?: string; pixCode?: string; pixQrCode?: string; expiresAt?: string; From ef521876eb04106325c7e587ee588bc11ff04f8b Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 26 Oct 2025 21:16:59 -0300 Subject: [PATCH 03/11] feat: enhance AbacatePay donation system with updated docs and form fields Updated ABACATEPAY_SETUP.md with comprehensive setup guide, including environment variables, API key instructions, webhook configuration, donation flow, and field details for better integration clarity. Added required fields (phone, CPF/CNPJ) to the donation form in app/[locale]/apoiar/page.tsx to collect essential donor information and improve compliance. This ensures smoother setup and more robust donation processing. --- ABACATEPAY_SETUP.md | 71 +++-- TESTE_ABACATEPAY.md | 162 ++++++++++ app/[locale]/apoiar/page.tsx | 16 +- app/api/donations/create/route.ts | 136 ++++++-- app/api/webhooks/abacatepay/route.ts | 50 ++- components/donation-card.tsx | 449 ++++++++++++++++++++++++--- components/ui/textarea.tsx | 23 ++ lib/abacatepay.ts | 146 +++++---- types/donation.ts | 37 ++- 9 files changed, 922 insertions(+), 168 deletions(-) create mode 100644 TESTE_ABACATEPAY.md create mode 100644 components/ui/textarea.tsx diff --git a/ABACATEPAY_SETUP.md b/ABACATEPAY_SETUP.md index 13d68fb..aae7729 100644 --- a/ABACATEPAY_SETUP.md +++ b/ABACATEPAY_SETUP.md @@ -1,43 +1,70 @@ -# Variáveis de Ambiente - AbacatePay +# Configuração - Sistema de Doações PIX via AbacatePay -## Configuração para Desenvolvimento/Teste +## Variáveis de Ambiente + +### Desenvolvimento/Teste + +Crie o arquivo `.env.local` na raiz do projeto: ```env # Chave de API para ambiente de teste -ABACATE_TEST_APIKEY=your_test_api_key_here +ABACATE_TEST_APIKEY=abc_test_sua_chave_aqui -# ID da conta AbacatePay -ABACATEPAY_ACCOUNT_ID=your_account_id_here - -# URLs da aplicação +# URL base da aplicação NEXT_PUBLIC_BASE_URL=http://localhost:3000 -NEXT_PUBLIC_ABACATEPAY_WEBHOOK_URL=http://localhost:3000/api/webhooks/abacatepay ``` -## Configuração para Produção +### Produção ```env -# Chave de API para ambiente de produção (usado automaticamente quando NODE_ENV=production) -ABACATE_APIKEY=your_production_api_key_here +# Chave de API para ambiente de produção +ABACATE_APIKEY=abc_live_sua_chave_aqui -# ID da conta AbacatePay -ABACATEPAY_ACCOUNT_ID=your_account_id_here - -# URLs da aplicação +# URL base da aplicação NEXT_PUBLIC_BASE_URL=https://seudominio.com -NEXT_PUBLIC_ABACATEPAY_WEBHOOK_URL=https://seudominio.com/api/webhooks/abacatepay ``` ## Como Funciona O sistema automaticamente escolhe a chave correta baseado no ambiente: -- **Desenvolvimento/Teste**: Usa `ABACATE_TEST_APIKEY` +- **Desenvolvimento**: Usa `ABACATE_TEST_APIKEY` - **Produção**: Usa `ABACATE_APIKEY` (quando `NODE_ENV=production`) -## Obtenção das Chaves +## Obtenção da Chave API -1. Acesse o painel do AbacatePay -2. Vá em "Configurações" > "API Keys" -3. Gere uma chave para teste e outra para produção -4. Configure as URLs de webhook no painel do AbacatePay +1. Acesse https://abacatepay.com +2. Crie uma conta gratuita +3. Vá em "Configurações" > "API Keys" +4. Copie sua chave de teste (começa com `abc_test_...`) +5. Cole no arquivo `.env.local` + +## Endpoint do Webhook + +Configure no painel do AbacatePay: + +**Desenvolvimento**: `http://localhost:3000/api/webhooks/abacatepay` +**Produção**: `https://seudominio.com/api/webhooks/abacatepay` + +## Estrutura do Sistema + +### Fluxo de Doação + +1. Usuário clica em "Apoiar com PIX" +2. Preenche formulário: nome (obrigatório), email (opcional), mensagem (opcional) +3. Sistema gera QR Code PIX via endpoint `/pixQrCode/create` +4. Usuário escaneia QR Code ou copia código PIX +5. Pagamento é processado no app bancário +6. Webhook confirma pagamento automaticamente +7. Dados são registrados para futuros agradecimentos e benefícios + +### Campos Coletados + +- **Nome**: obrigatório (mínimo 3 caracteres) - para dar os devidos agradecimentos +- **Email**: opcional - para enviar agradecimento por email +- **Mensagem**: opcional (máximo 280 caracteres) - mensagem personalizada para a equipe + +### Expiração + +- QR Code expira em 10 minutos +- Timer exibido no modal para o usuário acompanhar diff --git a/TESTE_ABACATEPAY.md b/TESTE_ABACATEPAY.md new file mode 100644 index 0000000..0c7fcc6 --- /dev/null +++ b/TESTE_ABACATEPAY.md @@ -0,0 +1,162 @@ +# Guia de Teste - Sistema de Doações AbacatePay + +## 🔧 Configuração Inicial + +### 1. Criar conta no AbacatePay +- Acesse: https://abacatepay.com +- Crie uma conta gratuita +- Acesse o Dashboard + +### 2. Obter chave de API +- No Dashboard, vá para "API Keys" ou "Configurações" +- Copie sua **TEST API KEY** (começa com `abc_test_...`) +- Você também precisará da **PRODUCTION API KEY** quando for para produção + +### 3. Configurar produtos (OPCIONAL) +O sistema de PIX não requer produtos pré-cadastrados no AbacatePay. Os valores são enviados diretamente na requisição. + +Os produtos já estão definidos no código: +- Café: R$ 5,00 (500 centavos) +- Lanche: R$ 10,00 (1000 centavos) +- Almoço: R$ 25,00 (2500 centavos) + +### 4. Configurar variáveis de ambiente + +Crie um arquivo `.env.local` na raiz do projeto: + +```env +# AbacatePay API Keys +ABACATE_TEST_APIKEY=sua_chave_de_teste_aqui + +# URL base da aplicação +NEXT_PUBLIC_BASE_URL=http://localhost:3000 +``` + +**IMPORTANTE**: Não é necessário `ABACATEPAY_ACCOUNT_ID` nem produtos pré-cadastrados. O sistema PIX funciona diretamente com a API. + +## 🧪 Testando o Sistema + +### Ambiente de Teste (Development) + +1. **Inicie o servidor de desenvolvimento**: +```bash +pnpm run dev +``` + +2. **Acesse a página de doações**: +``` +http://localhost:3000/pt/apoiar +``` + +3. **Clique em um dos cards de doação** + +4. **Verifique os logs no terminal**: +``` +🔧 AbacatePay: Using TEST environment +🔍 AbacatePay Request: { + "frequency": "ONE_TIME", + "methods": ["PIX"], + "products": [ + { + "externalId": "prod_...", + "name": "Doação Café", + "quantity": 1, + "price": 500 + } + ], + ... +} +🔍 AbacatePay Response Status: 200 +``` + +5. **Resultado esperado**: +- Status 200 ✅ +- Redirecionamento para página de pagamento do AbacatePay +- OU exibição de QR Code PIX (modal) + +### Estrutura da Requisição Correta + +```json +{ + "frequency": "ONE_TIME", + "methods": ["PIX"], + "products": [ + { + "externalId": "prod_6ryJmhfZGNJGjNaYaA1YqhDn", + "name": "Doação Café", + "quantity": 1, + "price": 500 + } + ], + "returnUrl": "http://localhost:3000/apoiar/obrigado", + "completionUrl": "http://localhost:3000/api/donations/verify" +} +``` + +**Campos obrigatórios:** +- ✅ `externalId`: ID do produto pré-cadastrado +- ✅ `name`: Nome do produto +- ✅ `quantity`: Quantidade (sempre 1) +- ✅ `price`: Valor em centavos (R$ 5,00 = 500) + +## 🔍 Troubleshooting + +### Erro: "Route POST:/v1/billing not found" +**Solução**: Endpoint correto é `/billing/create`, não `/billing` + +### Erro: "must have required property 'name'" +**Solução**: Adicionar campo `name` no array de produtos + +### Erro: "Product not found" +**Solução**: Verificar se os IDs dos produtos estão corretos e existem no painel + +### Erro: "Invalid API key" +**Solução**: +- Verifique se a chave está correta no `.env.local` +- Certifique-se de usar `ABACATE_TEST_APIKEY` em development +- Reinicie o servidor após alterar variáveis de ambiente + +### Erro: "401 Unauthorized" +**Solução**: Chave de API expirada ou inválida. Gere uma nova no painel. + +## 📋 Checklist de Validação + +- [ ] Conta criada no AbacatePay +- [ ] API Key de teste obtida +- [ ] 3 produtos criados no painel (R$ 5, R$ 10, R$ 25) +- [ ] IDs dos produtos atualizados em `types/donation.ts` +- [ ] Arquivo `.env.local` criado com as variáveis +- [ ] Servidor reiniciado após configurar variáveis +- [ ] Página `/pt/apoiar` carrega sem erros +- [ ] Logs mostram "Using TEST environment" +- [ ] Clique em um card envia requisição +- [ ] Status 200 retornado +- [ ] Redirecionamento funciona ou QR Code é exibido + +## 🚀 Deploy em Produção + +Quando estiver pronto para produção: + +1. Configure as variáveis no Vercel/Netlify: +``` +ABACATE_APIKEY=abc_live_... +ABACATEPAY_ACCOUNT_ID=... +NEXT_PUBLIC_BASE_URL=https://seusite.com +``` + +2. O sistema detecta automaticamente o ambiente: +```typescript +const isProduction = process.env.NODE_ENV === "production"; +const apiKey = isProduction + ? process.env.ABACATE_APIKEY + : process.env.ABACATE_TEST_APIKEY; +``` + +3. Teste novamente em produção antes de divulgar + +## 📞 Suporte + +- Documentação: https://docs.abacatepay.com +- API Reference: https://abacatepay.readme.io +- Dashboard: https://abacatepay.com/dashboard + diff --git a/app/[locale]/apoiar/page.tsx b/app/[locale]/apoiar/page.tsx index 9bcef60..a3dbc8a 100644 --- a/app/[locale]/apoiar/page.tsx +++ b/app/[locale]/apoiar/page.tsx @@ -179,6 +179,15 @@ export default async function SupportPage({ icon="coffee" badge={t("tiers.coffee.badge")} /> + -

diff --git a/app/api/donations/create/route.ts b/app/api/donations/create/route.ts index faa1434..f7bf29d 100644 --- a/app/api/donations/create/route.ts +++ b/app/api/donations/create/route.ts @@ -1,49 +1,135 @@ import { NextRequest, NextResponse } from "next/server"; import { abacatePay } from "@/lib/abacatepay"; -import { DonationData } from "@/types/donation"; +import { DONATION_PRODUCTS, DonorData } from "@/types/donation"; export async function POST(request: NextRequest) { try { - const body: DonationData = await request.json(); + const body = await request.json(); - // Validação básica - if (!body.amount || body.amount < 5 || body.amount > 10000) { + // Validar tier + const validTiers: Array = [ + "coffee", + "snack", + "lunch", + ]; + if (!body.tier || !validTiers.includes(body.tier)) { return NextResponse.json( - { - success: false, - error: "Valor deve estar entre R$ 5,00 e R$ 10.000,00", - }, + { success: false, error: "Tier de doação inválido" }, { status: 400 } ); } - const billing = await abacatePay.createDonationBilling({ - amount: body.amount, - donorEmail: body.donorEmail, - donorName: body.donorName, - message: body.message, - }); + const product = + DONATION_PRODUCTS[body.tier as keyof typeof DONATION_PRODUCTS]; - // AbacatePay pode retornar diferentes formatos - const pixCode = billing.pixCode || billing.qrCode; - const pixQrCode = billing.pixQrCode || billing.qrCode; - const paymentUrl = billing.url || billing.paymentUrl; + let pixData; + + // Fluxo: Anônimo ou Identificado + if (body.anonymous === true) { + // Doação anônima - sem customer + pixData = await abacatePay.createPixQrCode({ + amount: product.price, + description: product.name, + expiresIn: 600, + }); + + console.log("🔍 PIX criado (anônimo):", pixData); + + console.log("📝 Registro de doação anônima:", { + type: "donation_created", + pixId: pixData.id, + amount: product.price, + tier: body.tier, + anonymous: true, + createdAt: new Date().toISOString(), + }); + } else { + // Doação identificada - validar campos obrigatórios + if (!body.donorName || body.donorName.trim().length < 3) { + return NextResponse.json( + { success: false, error: "Nome é obrigatório (mínimo 3 caracteres)" }, + { status: 400 } + ); + } + + if (!body.donorPhone || body.donorPhone.trim().length === 0) { + return NextResponse.json( + { success: false, error: "Telefone é obrigatório" }, + { status: 400 } + ); + } + + if (!body.donorEmail) { + return NextResponse.json( + { + success: false, + error: "Email é obrigatório para doação identificada", + }, + { status: 400 } + ); + } + + if (!body.donorTaxId || body.donorTaxId.trim().length === 0) { + return NextResponse.json( + { success: false, error: "CPF/CNPJ é obrigatório" }, + { status: 400 } + ); + } + + if (body.donorMessage && body.donorMessage.length > 280) { + return NextResponse.json( + { + success: false, + error: "Mensagem deve ter no máximo 280 caracteres", + }, + { status: 400 } + ); + } + + pixData = await abacatePay.createPixQrCode({ + amount: product.price, + description: product.name, + expiresIn: 600, + customer: { + name: body.donorName.trim(), + email: body.donorEmail.trim(), + cellphone: body.donorPhone.trim(), + taxId: body.donorTaxId.trim(), + }, + }); + + console.log("🔍 PIX criado (identificado):", pixData); + + console.log("📝 Registro de doação identificada:", { + type: "donation_created", + pixId: pixData.id, + amount: product.price, + tier: body.tier, + anonymous: false, + donor: { + name: body.donorName, + email: body.donorEmail, + phone: body.donorPhone, + taxId: body.donorTaxId, + message: body.donorMessage, + }, + createdAt: new Date().toISOString(), + }); + } return NextResponse.json({ success: true, - billingId: billing.id, - paymentUrl, // URL para redirecionar - pixCode, - pixQrCode, - expiresAt: billing.expiresAt, + pixId: pixData.id, + brCode: pixData.brCode, + brCodeBase64: pixData.brCodeBase64, + expiresAt: pixData.expiresAt, }); } catch (error) { console.error("Donation creation error:", error); return NextResponse.json( { success: false, - error: - error instanceof Error ? error.message : "Erro interno do servidor", + error: error instanceof Error ? error.message : "Erro interno", }, { status: 500 } ); diff --git a/app/api/webhooks/abacatepay/route.ts b/app/api/webhooks/abacatepay/route.ts index 4908f46..eea8e0b 100644 --- a/app/api/webhooks/abacatepay/route.ts +++ b/app/api/webhooks/abacatepay/route.ts @@ -15,23 +15,47 @@ export async function POST(request: NextRequest) { const event = JSON.parse(payload); - if (event.type === "billing.paid") { - // Processar doação confirmada - console.log("Donation confirmed:", event.data.id); + console.log("🔔 Webhook recebido:", JSON.stringify(event, null, 2)); - // Aqui você pode: - // 1. Salvar no banco de dados - // 2. Enviar email de agradecimento - // 3. Atualizar estatísticas - // 4. Notificar a equipe + // Capturar todos os tipos de eventos de pagamento + const isPaidEvent = + event.type === "pix.paid" || + event.type === "billing.paid" || + event.type === "payment.paid" || + event.data?.status === "PAID"; - // Por enquanto, apenas log - console.log("Donation details:", { - id: event.data.id, + if (isPaidEvent) { + console.log("✅ Doação confirmada via webhook:", event.data.id); + + // 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(), + createdAt: new Date().toISOString(), metadata: event.data.metadata, - paidAt: event.data.paidAt, - }); + }; + + console.log("📝 Registro de pagamento:", donationRecord); + + // TODO: Quando implementar banco de dados, salvar aqui: + // await db.donations.create(donationRecord); + + // TODO: Quando implementar email, enviar aqui: + // if (event.data.customer?.email) { + // await sendThankYouEmail({ + // to: event.data.customer.email, + // donorName: event.data.customer.name, + // amount: event.data.amount, + // }); + // } + + // TODO: Atualizar estatísticas + // await updateDonationStats(event.data.amount); + } else { + console.log("ℹ️ Evento de webhook (não é pagamento):", event.type); } return NextResponse.json({ success: true }); diff --git a/components/donation-card.tsx b/components/donation-card.tsx index 3e5334a..8497d24 100644 --- a/components/donation-card.tsx +++ b/components/donation-card.tsx @@ -1,22 +1,33 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Heart, Coffee, Pizza, Loader2, Copy, Check } from "lucide-react"; +import { Textarea } from "@/components/ui/textarea"; +import { + Heart, + Coffee, + Pizza, + Loader2, + Copy, + Check, + Lock, + User, + CheckCircle, +} from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import QRCode from "react-qr-code"; import { DonationTier, DonationResponse } from "@/types/donation"; interface DonationCardProps extends DonationTier {} export function DonationCard({ + id, amount, title, description, @@ -24,14 +35,27 @@ export function DonationCard({ badge, highlighted, }: DonationCardProps) { - const [customAmount, setCustomAmount] = useState(amount || 10); + // Estados para escolha inicial + const [showChoiceModal, setShowChoiceModal] = useState(false); + const [showDonorForm, setShowDonorForm] = useState(false); + const [donorName, setDonorName] = useState(""); + const [donorEmail, setDonorEmail] = useState(""); + const [donorPhone, setDonorPhone] = useState(""); + const [donorTaxId, setDonorTaxId] = useState(""); + const [donorMessage, setDonorMessage] = useState(""); + + // Estados para PIX const [loading, setLoading] = useState(false); const [showPixModal, setShowPixModal] = useState(false); const [pixData, setPixData] = useState<{ - code: string; - qrCode: string; + pixId: string; + brCode: string; + brCodeBase64: string; + expiresAt?: string; } | null>(null); const [copied, setCopied] = useState(false); + const [error, setError] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(null); const icons = { coffee: Coffee, @@ -41,43 +65,197 @@ export function DonationCard({ const Icon = icons[icon]; - const handleDonate = async () => { + // Atualizar timer e verificar pagamento automaticamente + useEffect(() => { + if (!pixData?.expiresAt || !pixData?.pixId) { + setTimeRemaining(null); + return; + } + + const updateTimer = () => { + try { + const expires = new Date(pixData!.expiresAt!); + const now = new Date(); + const diff = expires.getTime() - now.getTime(); + + if (diff <= 0) { + setTimeRemaining("Expirado"); + return; + } + + const minutes = Math.floor(diff / 60000); + const seconds = Math.floor((diff % 60000) / 1000); + + setTimeRemaining(`${minutes}:${seconds.toString().padStart(2, "0")}`); + } catch { + setTimeRemaining(null); + } + }; + + // Atualizar timer imediatamente + updateTimer(); + + // Atualizar timer a cada segundo + const timerInterval = setInterval(updateTimer, 1000); + + return () => clearInterval(timerInterval); + }, [pixData?.expiresAt, pixData?.pixId]); + + // Funções de formatação + const formatCPF = (value: string): string => { + const numbers = value.replace(/\D/g, ""); + if (numbers.length <= 11) { + return numbers + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d{1,2})$/, "$1-$2"); + } + return value; // Não formatar se for CNPJ (14 dígitos) + }; + + const formatCNPJ = (value: string): string => { + const numbers = value.replace(/\D/g, ""); + if (numbers.length > 11) { + return numbers + .replace(/^(\d{2})(\d)/, "$1.$2") + .replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3") + .replace(/\.(\d{3})(\d)/, ".$1/$2") + .replace(/(\d{4})(\d)/, "$1-$2"); + } + return value; + }; + + const formatPhone = (value: string): string => { + const numbers = value.replace(/\D/g, ""); + if (numbers.length <= 10) { + // Telefone fixo (10 dígitos) + return numbers.replace(/^(\d{2})(\d{4})(\d{4})$/, "($1) $2-$3"); + } else { + // Celular (11 dígitos) + return numbers.replace(/^(\d{2})(\d{5})(\d{4})$/, "($1) $2-$3"); + } + }; + + const handleDonateClick = () => { + setShowChoiceModal(true); + setError(null); + }; + + const handleAnonymousDonation = async () => { setLoading(true); + setError(null); + setShowChoiceModal(false); + try { const response = await fetch("/api/donations/create", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amount: customAmount }), + body: JSON.stringify({ + tier: id, + anonymous: true, + }), }); const data: DonationResponse = await response.json(); - if (data.success && data.paymentUrl) { - // Redirecionar para página de pagamento do AbacatePay - window.location.href = data.paymentUrl; - } else if (data.success && data.pixCode && data.pixQrCode) { - // Fallback: mostrar modal com QR Code se disponível + if (data.success && data.brCode && data.brCodeBase64 && data.pixId) { setPixData({ - code: data.pixCode, - qrCode: data.pixQrCode, + pixId: data.pixId, + brCode: data.brCode, + brCodeBase64: data.brCodeBase64, + expiresAt: data.expiresAt, }); setShowPixModal(true); } else { - console.error("Erro ao criar doação:", data.error); - alert("Erro ao processar doação. Tente novamente."); + setError(data.error || "Erro ao processar doação"); + setShowChoiceModal(true); } } catch (error) { console.error("Donation error:", error); - alert("Erro ao processar doação. Tente novamente."); + setError("Erro ao processar doação. Tente novamente."); + setShowChoiceModal(true); + } finally { + setLoading(false); + } + }; + + const handleIdentifiedClick = () => { + setShowChoiceModal(false); + setShowDonorForm(true); + setError(null); + }; + + const handleSubmitForm = async () => { + // Validação + if (!donorName.trim() || donorName.trim().length < 3) { + setError("Nome deve ter no mínimo 3 caracteres"); + return; + } + + if (!donorPhone.trim()) { + setError("Telefone é obrigatório"); + return; + } + + if (!donorEmail.trim()) { + setError("Email é obrigatório"); + return; + } + + if (!donorTaxId.trim()) { + setError("CPF/CNPJ é obrigatório"); + return; + } + + if (donorMessage && donorMessage.length > 280) { + setError("Mensagem deve ter no máximo 280 caracteres"); + return; + } + + setLoading(true); + setError(null); + + try { + const response = await fetch("/api/donations/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + tier: id, + anonymous: false, + donorName: donorName.trim(), + donorEmail: donorEmail.trim(), + donorPhone: donorPhone.replace(/\D/g, ""), // Apenas números + donorTaxId: donorTaxId.replace(/\D/g, ""), // Apenas números + donorMessage: donorMessage.trim() || undefined, + }), + }); + + const data: DonationResponse = await response.json(); + + if (data.success && data.brCode && data.brCodeBase64 && data.pixId) { + setPixData({ + pixId: data.pixId, + brCode: data.brCode, + brCodeBase64: data.brCodeBase64, + expiresAt: data.expiresAt, + }); + setShowDonorForm(false); + setShowPixModal(true); + } else { + setError(data.error || "Erro ao processar doação"); + } + } catch (error) { + console.error("Donation error:", error); + setError("Erro ao processar doação. Tente novamente."); } finally { setLoading(false); } }; const handleCopyPixCode = async () => { - if (pixData?.code) { + if (pixData?.brCode) { try { - await navigator.clipboard.writeText(pixData.code); + await navigator.clipboard.writeText(pixData.brCode); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (error) { @@ -113,27 +291,16 @@ export function DonationCard({

{title}

{description}

- {amount ? ( + {amount && (
R$ {amount.toFixed(2)}
- ) : ( - setCustomAmount(Number(e.target.value))} - min={5} - max={10000} - step={5} - className="text-center text-2xl font-bold" - placeholder="R$ 10,00" - /> )}
+ {/* Modal de Escolha (Anônimo vs Identificado) */} + + + + + Como deseja contribuir? + + + +
+ {/* Card Anônimo */} +
+
+
+ +
+

Doação Anônima

+
+

+ Rápido e sem cadastro. Gere o PIX imediatamente. +

+ +
+ + {/* Card Identificado */} +
+ + Recomendado + +
+
+ +
+

Quero me Identificar

+
+

+ Ajude-nos a agradecer e oferecer benefícios +

+ +
+
+ + Agradecimento personalizado por email +
+
+ + Acesso antecipado a novos recursos +
+
+ + Histórico de suas contribuições +
+
+ + +
+
+
+
+ + {/* Modal de Formulário do Doador Identificado */} + + + + Informações para Doação + + +
+ {error && ( +
+ {error} +
+ )} + +
+ + setDonorName(e.target.value)} + placeholder="Seu nome ou apelido" + maxLength={50} + /> +
+ +
+ + setDonorEmail(e.target.value)} + placeholder="seu@email.com" + /> +
+ +
+ + setDonorPhone(formatPhone(e.target.value))} + placeholder="(11) 98888-7777" + maxLength={15} + /> +
+ +
+ + { + const value = e.target.value.replace(/\D/g, ""); + if (value.length <= 11) { + setDonorTaxId(formatCPF(value)); + } else { + setDonorTaxId(formatCNPJ(value)); + } + }} + placeholder="000.000.000-00 ou 00.000.000/0000-00" + maxLength={18} + /> +
+ +
+ +