Files
asaas-checkout/components/pedido-status.tsx
Felipe Carvalho 038ce3f556 feat: initial commit — asaas-checkout template white-label
Template genérico de checkout com ASAAS, parametrizado via env vars.
Inclui fluxo completo: checkout → pedido → polling → webhook → admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 06:40:41 +02:00

264 lines
9.9 KiB
TypeScript

"use client"
import { useEffect, useState, useCallback } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { useToast } from "@/components/ui/use-toast"
import {
CheckCircle2,
Clock,
XCircle,
Copy,
ExternalLink,
Calendar,
Loader2,
} from "lucide-react"
type Pedido = {
id: string
status: string
valor_centavos: number
metodo_pagamento: string
pix_copia_cola: string | null
pix_qrcode_url: string | null
asaas_invoice_url: string | null
paid_at: string | null
clientes: { nome: string; email: string } | null
produtos: { nome: string; validade: string; midia: string | null } | null
}
const STATUS_PAGO = ["PAID", "RECEIVED", "CONFIRMED"]
const STATUS_CANCELADO = ["CANCELED", "CANCELLED", "OVERDUE", "REFUNDED"]
const LINK_AGENDAMENTO = process.env.NEXT_PUBLIC_AFTER_PAYMENT_REDIRECT ?? "/"
export default function PedidoStatus({ pedidoInicial }: { pedidoInicial: Pedido }) {
const router = useRouter()
const { toast } = useToast()
const [pedido, setPedido] = useState(pedidoInicial)
const [polling, setPolling] = useState(true)
const isPago = STATUS_PAGO.includes(pedido.status)
const isCancelado = STATUS_CANCELADO.includes(pedido.status)
const valorFormatado = (pedido.valor_centavos / 100).toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
})
const fetchStatus = useCallback(async () => {
try {
const res = await fetch(`/api/pedido/${pedido.id}`)
if (!res.ok) return
const data: Pedido = await res.json()
setPedido(data)
if (STATUS_PAGO.includes(data.status) || STATUS_CANCELADO.includes(data.status)) {
setPolling(false)
}
} catch {
// silencioso — tenta de novo no próximo ciclo
}
}, [pedido.id])
useEffect(() => {
if (!polling || isPago || isCancelado) return
const interval = setInterval(fetchStatus, 5000)
return () => clearInterval(interval)
}, [polling, isPago, isCancelado, fetchStatus])
function copiarPix() {
if (!pedido.pix_copia_cola) return
navigator.clipboard.writeText(pedido.pix_copia_cola)
toast({ title: "Código PIX copiado!" })
}
// ── Pago ──────────────────────────────────────────────────────────────
if (isPago) {
return (
<Card className="max-w-2xl mx-auto border-green-200 bg-green-50">
<CardHeader className="text-center pb-2">
<CheckCircle2 className="h-14 w-14 text-green-500 mx-auto mb-3" />
<CardTitle className="text-2xl text-green-800">Pagamento confirmado!</CardTitle>
<p className="text-green-700 text-sm mt-1">
{pedido.clientes?.nome}, seu certificado está sendo processado.
</p>
</CardHeader>
<CardContent className="space-y-5">
<ResumoCompra pedido={pedido} valorFormatado={valorFormatado} />
<div className="bg-white border border-green-200 rounded-xl p-5 text-center space-y-3">
<Calendar className="h-8 w-8 text-primary mx-auto" />
<p className="font-semibold text-lg">Próximo passo: agende sua validação</p>
<p className="text-sm text-muted-foreground">
Para emitir o certificado, é obrigatório realizar uma videoconferência de
Clique no botão abaixo para continuar para o próximo passo.
</p>
<Button
size="lg"
className="w-full mt-2"
onClick={() => router.push(LINK_AGENDAMENTO)}
>
<Calendar className="mr-2 h-4 w-4" />
Continuar
</Button>
<p className="text-xs text-muted-foreground">
O link também foi enviado para {pedido.clientes?.email}
</p>
</div>
</CardContent>
</Card>
)
}
// ── Cancelado / Vencido ───────────────────────────────────────────────
if (isCancelado) {
return (
<Card className="max-w-2xl mx-auto border-red-200 bg-red-50">
<CardHeader className="text-center pb-2">
<XCircle className="h-14 w-14 text-red-400 mx-auto mb-3" />
<CardTitle className="text-2xl text-red-800">Pagamento não realizado</CardTitle>
<p className="text-red-700 text-sm mt-1">
Este pedido foi cancelado ou expirou.
</p>
</CardHeader>
<CardContent className="space-y-4">
<ResumoCompra pedido={pedido} valorFormatado={valorFormatado} />
<Button variant="outline" className="w-full" onClick={() => router.push("/comprar")}>
Fazer novo pedido
</Button>
</CardContent>
</Card>
)
}
// ── PIX ───────────────────────────────────────────────────────────────
if (pedido.metodo_pagamento === "PIX") {
return (
<Card className="max-w-2xl mx-auto">
<CardHeader className="text-center pb-2">
<Clock className="h-10 w-10 text-yellow-500 mx-auto mb-2" />
<CardTitle className="text-xl">Aguardando pagamento PIX</CardTitle>
<p className="text-sm text-muted-foreground flex items-center justify-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
Confirmação automática em segundos após o pagamento
</p>
</CardHeader>
<CardContent className="space-y-5">
<ResumoCompra pedido={pedido} valorFormatado={valorFormatado} />
{pedido.pix_qrcode_url && (
<div className="flex justify-center">
<img
src={`data:image/png;base64,${pedido.pix_qrcode_url}`}
alt="QR Code PIX"
className="w-52 h-52 rounded-xl border"
/>
</div>
)}
{pedido.pix_copia_cola && (
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
PIX Copia e Cola
</p>
<div className="bg-muted rounded-lg p-3 text-xs break-all select-all font-mono leading-relaxed">
{pedido.pix_copia_cola}
</div>
<Button variant="outline" className="w-full" onClick={copiarPix}>
<Copy className="mr-2 h-4 w-4" />
Copiar código PIX
</Button>
</div>
)}
<p className="text-xs text-center text-muted-foreground">
Valor: <strong>{valorFormatado}</strong>
</p>
</CardContent>
</Card>
)
}
// ── BOLETO ────────────────────────────────────────────────────────────
if (pedido.metodo_pagamento === "BOLETO") {
return (
<Card className="max-w-2xl mx-auto">
<CardHeader className="text-center pb-2">
<Clock className="h-10 w-10 text-yellow-500 mx-auto mb-2" />
<CardTitle className="text-xl">Boleto gerado</CardTitle>
<p className="text-sm text-muted-foreground">
Após o pagamento, a confirmação pode levar até 2 dias úteis.
</p>
</CardHeader>
<CardContent className="space-y-5">
<ResumoCompra pedido={pedido} valorFormatado={valorFormatado} />
{pedido.asaas_invoice_url && (
<Button
className="w-full"
onClick={() => window.open(pedido.asaas_invoice_url!, "_blank")}
>
<ExternalLink className="mr-2 h-4 w-4" />
Abrir boleto
</Button>
)}
<p className="text-xs text-center text-muted-foreground">
O link do boleto também foi enviado para{" "}
<strong>{pedido.clientes?.email}</strong>
</p>
</CardContent>
</Card>
)
}
// ── CARTÃO (pendente/processando) ────────────────────────────────────
return (
<Card className="max-w-2xl mx-auto">
<CardHeader className="text-center pb-2">
<Loader2 className="h-10 w-10 text-blue-500 mx-auto mb-2 animate-spin" />
<CardTitle className="text-xl">Processando pagamento</CardTitle>
<p className="text-sm text-muted-foreground">Aguarde enquanto confirmamos seu cartão.</p>
</CardHeader>
<CardContent>
<ResumoCompra pedido={pedido} valorFormatado={valorFormatado} />
</CardContent>
</Card>
)
}
function ResumoCompra({
pedido,
valorFormatado,
}: {
pedido: Pedido
valorFormatado: string
}) {
return (
<div className="bg-muted rounded-xl p-4 space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Produto</span>
<span className="font-medium">{pedido.produtos?.nome ?? "—"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Validade</span>
<span>{pedido.produtos?.validade ?? "—"}</span>
</div>
{pedido.produtos?.midia && (
<div className="flex justify-between">
<span className="text-muted-foreground">Mídia</span>
<span>{pedido.produtos.midia}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-muted-foreground">Valor</span>
<span className="font-semibold text-primary">{valorFormatado}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Pedido</span>
<span className="font-mono text-xs">{pedido.id.slice(0, 8).toUpperCase()}</span>
</div>
</div>
)
}