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" - /> )} + + + {/* 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} + /> +
+ +
+ +