feat: PIX codes expire after 30 minutes, with a countdown
All checks were successful
Build and deploy / Validate source (push) Successful in 11s
Build and deploy / Integration suite on a real stack (push) Successful in 2m50s
Build and deploy / Secret scan and release gate (push) Successful in 8s
Build and deploy / Publish images (push) Successful in 1m54s

A PIX is created with a 30-minute date_of_expiration, and the PIX page
counts down to it. When it runs out the page says the code expired and
offers a new one. The API keeps one open code per quote: the same code
until it expires, then exactly one new attempt (serialised per quote, with
its own idempotency key), the old one marked expired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-28 15:40:26 -03:00
parent 875a7ef9c7
commit 641aafc87d
7 changed files with 81 additions and 10 deletions

View File

@@ -7,6 +7,7 @@
const naPagina = () => ['pagamento','pix'].includes(document.documentElement.dataset.rota);
const naPix = () => document.documentElement.dataset.rota === 'pix';
let shownCart = null;
let pixClock = null;
let busy = false;
let draftId = localStorage.getItem('dtf-quote');
let requestKey = localStorage.getItem('dtf-request-key');
@@ -162,6 +163,7 @@
if(version!==refreshVersion || draftId!==shownId) return;
if (!naPagina()) { message(''); vaiPara(PAGAMENTO); return; }
shownCart=cartSnapshot();
clearInterval(pixClock);
unmountCard();
actions.replaceChildren();
message('');
@@ -319,9 +321,26 @@
try{ await navigator.clipboard.writeText(intent.pix_qr_code); copiar.textContent='Código copiado'; }
catch(_){ code.select(); }
},linha);
box.append(linha,node('p','Aguardando a confirmação do pagamento…','aguarda'));
const prazo=node('p',null,'prazo');
box.append(linha,prazo,node('p','Aguardando a confirmação do pagamento…','aguarda'));
actions.append(box);
waitForOrder();
// The code stops working when it expires; a new one is a click away.
const fim=Date.parse(intent.expires_at);
if (!Number.isFinite(fim)) { prazo.remove(); return; }
const tick=()=>{
const s=Math.max(0,Math.ceil((fim-Date.now())/1000));
prazo.textContent='Pague em '+String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');
if (s>0) return;
clearInterval(pixClock);
const fimBox=node('div',null,'pixBox');
fimBox.append(node('h4','O código PIX expirou'),
node('p','Gere um novo código para pagar. O anterior não pode mais ser pago.','pagNota'));
button('Gerar novo PIX',()=>refresh(),fimBox);
actions.replaceChildren(fimBox);
};
tick();
pixClock=setInterval(tick,1000);
}
// Card: Mercado Pago's own form (Card Payment Brick). The card is typed into
@@ -399,7 +418,7 @@
// The cart page keeps only the sending progress and errors, never a payment.
window.addEventListener('dtf-page-changed',()=>{
if (naPagina()) refresh();
else if (!busy) { refreshVersion++; message(''); actions.replaceChildren(); pintaResumo(null); }
else if (!busy) { refreshVersion++; clearInterval(pixClock); message(''); actions.replaceChildren(); pintaResumo(null); }
});
refresh();
})();

View File

@@ -254,6 +254,7 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
.pixBox .codigo{display:flex;gap:10px;width:100%}
.pixBox .codigo input{flex:1;min-width:0;font-size:13px}
.pixBox .aguarda{font-size:14px;color:var(--texto2)}
.pixBox .prazo{font-family:"Inter",sans-serif;font-weight:800;font-size:18px;font-variant-numeric:tabular-nums}
.confirmado{border:1px solid #BFE6CE;background:var(--verde-fundo);border-radius:16px;padding:24px}
.confirmado h4{font-size:22px;color:#14532D;margin-bottom:8px}
.confirmado p{font-size:15px;color:#14532D;margin-bottom:18px;line-height:1.5}