feat: PixCheckout com QR code e polling de status

This commit is contained in:
Rodribm10 2026-04-13 23:59:14 -03:00
parent 66fa4e77fd
commit b60dc6f45d
3 changed files with 101 additions and 0 deletions

View File

@ -22,6 +22,7 @@
"clsx": "^2.1.1",
"lucide-react": "^0.454.0",
"motion": "^12.4.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^2.5.0"

12
pnpm-lock.yaml generated
View File

@ -29,6 +29,9 @@ importers:
motion:
specifier: ^12.4.0
version: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
qrcode.react:
specifier: ^4.2.0
version: 4.2.0(react@19.2.5)
react:
specifier: ^19.1.0
version: 19.2.5
@ -1604,6 +1607,11 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qrcode.react@4.2.0:
resolution: {integrity: sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom@19.2.5:
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
@ -3307,6 +3315,10 @@ snapshots:
punycode@2.3.1: {}
qrcode.react@4.2.0(react@19.2.5):
dependencies:
react: 19.2.5
react-dom@19.2.5(react@19.2.5):
dependencies:
react: 19.2.5

View File

@ -0,0 +1,88 @@
import { useEffect, useState } from 'react'
import { QRCodeSVG } from 'qrcode.react'
import { chatwootApi, type CreateReservationResponse } from '@/lib/chatwootApi'
import { Button } from '@/components/ui/button'
import { formatBRL } from '@/lib/formatters'
interface Props {
reservation: CreateReservationResponse
depositCents: number
onPaid: () => void
onCancel: () => void
}
export function PixCheckout({ reservation, depositCents, onPaid, onCancel }: Props) {
const [copied, setCopied] = useState(false)
const [statusMsg, setStatusMsg] = useState<string>('Aguardando pagamento...')
useEffect(() => {
let canceled = false
const interval = setInterval(async () => {
try {
const s = await chatwootApi.getStatus(reservation.reservation_id)
if (canceled) return
if (s.status === 'paid') {
setStatusMsg('Pagamento confirmado!')
clearInterval(interval)
onPaid()
} else if (s.status === 'expired' || s.status === 'canceled') {
setStatusMsg(`Reserva ${s.status}`)
clearInterval(interval)
}
} catch (err) {
console.error('Erro no polling:', err)
}
}, 3000)
return () => {
canceled = true
clearInterval(interval)
}
}, [reservation.reservation_id, onPaid])
const handleCopy = async () => {
await navigator.clipboard.writeText(reservation.pix.copia_e_cola)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<section className="mx-auto max-w-md space-y-6 rounded-2xl border border-champagne/30 bg-midnight/60 p-8 backdrop-blur">
<header className="text-center">
<p className="text-xs uppercase tracking-[0.3em] text-rose-gold">Pagamento</p>
<h2 className="mt-2 font-serif text-3xl text-gradient-gold">
{formatBRL(depositCents)}
</h2>
<p className="mt-1 text-sm text-slate">{statusMsg}</p>
</header>
<div className="mx-auto flex w-fit items-center justify-center rounded-xl border border-champagne/40 bg-ivory p-4 glow-champagne">
<QRCodeSVG value={reservation.pix.copia_e_cola} size={220} level="M" />
</div>
<div>
<label className="text-xs uppercase tracking-widest text-champagne">
Pix copia-e-cola
</label>
<div className="mt-2 flex items-stretch gap-2">
<input
readOnly
value={reservation.pix.copia_e_cola}
className="flex-1 truncate rounded-lg border border-champagne/30 bg-obsidian/60 px-3 py-2 text-xs text-ivory"
/>
<Button variant="secondary" size="sm" onClick={handleCopy}>
{copied ? 'Copiado!' : 'Copiar'}
</Button>
</div>
</div>
<p className="text-center text-xs text-slate">
Expira em 1h. Após o pagamento, esta tela atualiza automaticamente.
</p>
<Button variant="ghost" size="sm" onClick={onCancel} className="w-full">
Cancelar e voltar
</Button>
</section>
)
}