// Checkout da loja — dados do cliente, entrega, pagamento via Pix (BR Code real) ou cartão. const { useState: ckState, useEffect: ckEffect, useRef: ckRef } = React; const onlyDigits = (s) => (s||'').replace(/\D/g,''); const maskPhone = (v) => { const d = onlyDigits(v).slice(0,11); return d.length<=10 ? d.replace(/(\d{2})(\d{0,4})(\d{0,4})/, (m,a,b,c)=>[a&&`(${a}) `,b,c&&`-${c}`].filter(Boolean).join('')) : d.replace(/(\d{2})(\d{5})(\d{0,4})/, '($1) $2-$3'); }; const maskCard = (v) => onlyDigits(v).slice(0,16).replace(/(\d{4})(?=\d)/g,'$1 '); const maskExp = (v) => { const d = onlyDigits(v).slice(0,4); return d.length>2 ? d.slice(0,2)+'/'+d.slice(2) : d; }; function luhn(num) { const d = onlyDigits(num); if (d.length < 13) return false; let sum = 0, alt = false; for (let i = d.length-1; i>=0; i--) { let n = +d[i]; if (alt) { n*=2; if (n>9) n-=9; } sum+=n; alt=!alt; } return sum % 10 === 0; } function Checkout({open, onClose, items, total, onDone, showToast}) { const store = useStore(); const cfg = store.settings; const [step, setStep] = ckState('dados'); const [c, setC] = ckState({name:'', phone:'', email:'', cpf:''}); const [ship, setShip] = ckState(cfg.delivery.pickup ? 'retirada' : 'entrega'); const [addr, setAddr] = ckState({street:'', num:'', bairro:'', ref:''}); const [method, setMethod] = ckState(cfg.pix.enabled ? 'pix' : 'cartao'); const [inst, setInst] = ckState(1); const [card, setCard] = ckState({number:'', holder:'', exp:'', cvv:''}); const [order, setOrder] = ckState(null); const [busy, setBusy] = ckState(false); const [err, setErr] = ckState(''); ckEffect(() => { if (open) { setStep('dados'); setOrder(null); setErr(''); } }, [open]); if (!open) return null; const freeShip = total >= (+cfg.delivery.freeAbove || 0); const fee = ship === 'entrega' && !freeShip ? (+cfg.delivery.localFee || 0) : 0; const grand = total + fee; const maxInst = Math.max(1, Math.min(+cfg.card.maxInstallments || 1, Math.floor(grand / (+cfg.card.minInstallment || 30)) || 1)); const validDados = c.name.trim().length > 2 && onlyDigits(c.phone).length >= 10 && (ship === 'retirada' || (addr.street.trim() && addr.num.trim() && addr.bairro.trim())); const createOrder = (status, extra = {}) => { const o = { id: uid('o'), code: orderCode(), createdAt: new Date().toISOString(), customer: {...c, phone: onlyDigits(c.phone)}, items: items.map(({p, qty}) => ({id:p.id, name:p.name, qty, price:p.priceN})), subtotal: total, fee, total: grand, delivery: ship, address: ship === 'entrega' ? addr : null, method, installments: method === 'cartao' ? inst : 1, status, ...extra, }; setStore(s => ({ orders: [o, ...(s.orders||[])], products: s.products.map(p => { const it = o.items.find(i => i.id === p.id); return it ? {...p, stock: Math.max(0, (+p.stock||0) - it.qty)} : p; }), })); setOrder(o); return o; }; const goPix = () => { createOrder('aguardando'); setStep('pix'); }; const payCard = () => { setErr(''); if (!luhn(card.number)) return setErr('Número do cartão inválido.'); if (onlyDigits(card.exp).length !== 4) return setErr('Validade incompleta.'); if (onlyDigits(card.cvv).length < 3) return setErr('CVV incompleto.'); if (card.holder.trim().length < 3) return setErr('Informe o nome impresso no cartão.'); setBusy(true); // Em produção esta etapa vira: tokenizar no SDK do gateway → POST /api/orders → POST /api/payments/card setTimeout(() => { setBusy(false); const paid = !!cfg.card.publicKey; createOrder(paid ? 'pago' : 'aguardando', {cardLast4: onlyDigits(card.number).slice(-4), demo: !paid}); setStep('ok'); onDone(); showToast(paid ? 'Pagamento aprovado' : 'Pedido registrado'); }, 1300); }; const confirmPix = () => { setStore(s => ({orders: s.orders.map(o => o.id === order.id ? {...o, status:'aguardando'} : o)})); setStep('ok'); onDone(); showToast('Pedido registrado — envie o comprovante'); }; const waLink = () => { const num = onlyDigits(cfg.contact.whatsapp); const txt = encodeURIComponent(`Olá! Fiz o pedido ${order?.code} no site (${brlc(grand)}) e estou enviando o comprovante do Pix.`); return `https://wa.me/${num}?text=${txt}`; }; return (
e.stopPropagation()}>
Finalizar compra

{step==='ok' ? 'Pedido confirmado' : step==='pix' ? 'Pague com Pix' : step==='pagamento' ? 'Pagamento' : 'Seus dados'}

{step === 'dados' && ( <>
setC({...c, name:e.target.value})} placeholder="Como no documento"/> setC({...c, phone:maskPhone(e.target.value)})} placeholder="(92) 99999-0000" inputMode="tel"/> setC({...c, email:e.target.value})} placeholder="seu@email.com" type="email"/>
Como quer receber
{cfg.delivery.pickup && ( )} {cfg.delivery.local && ( )}
{ship === 'entrega' && (
setAddr({...addr, street:e.target.value})}/> setAddr({...addr, num:e.target.value})}/> setAddr({...addr, bairro:e.target.value})}/> setAddr({...addr, ref:e.target.value})} placeholder="Perto de…"/>
)} )} {step === 'pagamento' && ( <>
Forma de pagamento
{cfg.pix.enabled && ( )} {cfg.card.enabled && ( )}
{method === 'cartao' && ( <>
setCard({...card, number:maskCard(e.target.value)})} placeholder="0000 0000 0000 0000" inputMode="numeric"/> setCard({...card, holder:e.target.value.toUpperCase()})} placeholder="ANGELLA B SANTOS"/> setCard({...card, exp:maskExp(e.target.value)})} placeholder="MM/AA" inputMode="numeric"/> setCard({...card, cvv:onlyDigits(e.target.value).slice(0,4)})} placeholder="123" inputMode="numeric"/>
{!cfg.card.publicKey &&
Modo demonstração: nenhum gateway conectado ainda. Cadastre a chave pública no painel → Pagamentos para cobrar de verdade.
} {err &&
{err}
} )} {method === 'pix' && !cfg.pix.key &&
A chave Pix ainda não foi cadastrada. Painel → Pagamentos → chave Pix.
} )} {step === 'pix' && order && ( )} {step === 'ok' && order && (

Obrigada, {order.customer.name.split(' ')[0]}!

Pedido {order.code} registrado.{' '} {order.method === 'pix' ? 'Assim que o Pix cair, confirmamos por WhatsApp.' : order.status === 'pago' ? 'Pagamento aprovado no cartão.' : 'Vamos confirmar o pagamento e avisar você.'}

Total{brlc(order.total)}
Pagamento{order.method === 'pix' ? 'Pix' : `Cartão ${order.installments}x`}
Entrega{order.delivery === 'retirada' ? 'Retirada no studio' : 'Entrega em Parintins'}
Falar no WhatsApp
)}
{step === 'dados' && <> {brlc(grand)} } {step === 'pagamento' && <> } {step === 'pix' && <> } {step === 'ok' && }
); } function CkTotals({items, total, fee, grand}) { return (
{items.map(({p, qty}) => (
{qty}× {p.name}{brlc(p.priceN*qty)}
))}
Entrega{fee ? brlc(fee) : 'Grátis'}
Total{brlc(grand)}
); } function PixPane({order, cfg, showToast}) { const canvas = ckRef(null); const payload = pixPayload({key: cfg.pix.key, merchant: cfg.pix.merchant, city: cfg.pix.city, amount: order.total, txid: order.code}); const [copied, setCopied] = ckState(false); ckEffect(() => { const cv = canvas.current; if (!payload || !cv || typeof window.qrcode !== 'function') return; try { const qr = window.qrcode(0, 'M'); qr.addData(payload); qr.make(); const n = qr.getModuleCount(), quiet = 2, px = 6, size = (n + quiet*2) * px; cv.width = size; cv.height = size; cv.style.width = cv.style.height = '208px'; const ctx = cv.getContext('2d'); ctx.fillStyle = '#F9F5F2'; ctx.fillRect(0, 0, size, size); ctx.fillStyle = '#4A0E1C'; for (let r = 0; r < n; r++) for (let c2 = 0; c2 < n; c2++) if (qr.isDark(r, c2)) ctx.fillRect((c2+quiet)*px, (r+quiet)*px, px, px); } catch (e) { console.warn('QR', e); } }, [payload]); const copy = () => { navigator.clipboard?.writeText(payload).then(()=>{ setCopied(true); showToast('Código Pix copiado'); setTimeout(()=>setCopied(false), 2200); }); }; if (!payload) return
Chave Pix não cadastrada. Peça à Angella para cadastrar em Painel → Pagamentos.
; return (
{brlc(order.total)}

Aponte a câmera do seu banco para o QR Code ou use o código copia e cola. Pedido {order.code}.

{payload}

Recebedor: {cfg.pix.merchant} · {cfg.pix.city}

); } Object.assign(window, { Checkout });