feat: PixCheckout com QR code e polling de status
This commit is contained in:
parent
66fa4e77fd
commit
b60dc6f45d
@ -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
12
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
88
src/components/checkout/PixCheckout.tsx
Normal file
88
src/components/checkout/PixCheckout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user