feat: offer credit card, debit card and PIX, with the bank's 3-D Secure step
All checks were successful
Build and deploy / Validate source (push) Successful in 5s
Build and deploy / Integration suite on a real stack (push) Successful in 2m3s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 1m31s

The payment page lists credit card (preselected), debit card and PIX. Each
card option limits Mercado Pago's form to its kind; debit is paid at once.
Card payments ask for 3-D Secure when the issuer requires it, and a
challenge opens the bank's page in a frame, which needs
PAYMENT_CHALLENGE_SOURCES=https: (frames and form posts only). A card left
waiting for that confirmation stops blocking a new attempt after ten
minutes, and a refusal reported by the notification returns the customer to
the payment choice. Written from the documentation; not yet run with a real
debit card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-28 13:24:06 -03:00
parent 7b6675d4d4
commit eae3dad306
11 changed files with 86 additions and 12 deletions

View File

@@ -225,10 +225,29 @@
pedido=[]; itemAtual=null; limpaPaineis();
await window.dtfClearCart?.();
await refresh();
} else if (quote.payment?.status==='rejected') {
// Refused after the bank's confirmation or the provider's review.
clearInterval(waitTimer);
await refresh();
message('Pagamento recusado. Tente de novo ou escolha outra forma de pagamento.');
}
} catch(_) {}
},3000);
}
// 3-D Secure: the bank's own page, opened by posting its request in a frame.
function showChallenge(challenge, into) {
unmountCard();
const frame=document.createElement('iframe');
frame.name='confirmacaoBanco'; frame.title='Confirmação do banco'; frame.className='desafio';
const form=document.createElement('form');
form.method='POST'; form.action=challenge.url; form.target=frame.name;
const input=document.createElement('input');
input.type='hidden'; input.name='creq'; input.value=challenge.creq;
form.append(input);
into.replaceChildren(node('p','Confirme o pagamento na página do seu banco, abaixo.','pagNota'),frame,form);
form.submit();
form.remove();
}
function confirmado(quote) {
clearInterval(waitTimer);
const box=node('div',null,'confirmado');
@@ -242,7 +261,7 @@
// Paying: card (the default, paid on this page) or PIX (paid on its own page).
async function escolhaPagamento(quote, version) {
const session=await ready;
const opcoes=[...(session.payment_public_key?[['card','Cartão de crédito']]:[]),['pix','PIX']];
const opcoes=[...(session.payment_public_key?[['credito','Cartão de crédito'],['debito','Cartão de débito']]:[]),['pix','PIX']];
let escolhido=opcoes[0][0];
const grupo=node('div',null,'metodos');
grupo.setAttribute('role','radiogroup'); grupo.setAttribute('aria-label','Forma de pagamento');
@@ -256,7 +275,7 @@
async function pinta() {
radios.forEach(el=>el.setAttribute('aria-checked',String(el.dataset.metodo===escolhido)));
unmountCard(); area.replaceChildren(); message('');
if (escolhido==='card') { await showCard(quote.approved, area, version); return; }
if (escolhido!=='pix') { await showCard(quote.approved, area, version, escolhido); return; }
area.append(node('p','Ao pagar, mostramos o QR code e o código copia e cola. O pedido vai para a produção assim que o PIX é confirmado.','pagNota'));
button('Pagar',async event=>{
if (!cartOk()) { await refresh(); return; }
@@ -325,7 +344,7 @@
function unmountCard() {
if (cardBrick) { try { cardBrick.unmount(); } catch(_) {} cardBrick=null; }
}
async function showCard(approved, into, version) {
async function showCard(approved, into, version, tipo) {
const holder=node('div');
holder.id='cardPaymentBrick';
into.append(holder);
@@ -338,7 +357,10 @@
const mp=new window.MercadoPago(session.payment_public_key,{locale:'pt-BR'});
cardBrick=await mp.bricks().create('cardPayment','cardPaymentBrick',{
initialization:{amount:approved.total_cents/100, payer:{email:approved.customer.mail}},
customization:{paymentMethods:{maxInstallments:12},
// Each option takes only its kind of card; debit is paid at once.
customization:{paymentMethods:tipo==='debito'
? {maxInstallments:1,types:{excluded:['credit_card']}}
: {maxInstallments:12,types:{excluded:['debit_card']}},
visual:{style:{customVariables:{baseColor:'#FFA81A',buttonTextColor:'#03060B'}}}},
callbacks:{
onReady:()=>{},
@@ -350,6 +372,7 @@
type:'card', token:data.token, payment_method_id:data.payment_method_id,
installments:Number(data.installments)||1,
issuer_id:data.issuer_id==null?null:String(data.issuer_id)}});
if (result.challenge) { showChallenge(result.challenge, into); waitForOrder(); return; }
if (result.status==='approved' || result.status==='pending') {
unmountCard();
into.replaceChildren(node('p',result.status==='approved'

View File

@@ -239,7 +239,7 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
.pagResumo .l span{color:var(--texto2)}
.pagResumo .tot{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid #EFEFEF;margin-top:10px;padding-top:12px}
.pagResumo .tot b{font-family:"Inter",sans-serif;font-weight:900;font-size:24px}
.metodos{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:20px}
.metodos{display:grid;grid-template-columns:1fr;gap:10px;max-width:420px;margin-bottom:20px}
.metodo{height:56px;display:flex;align-items:center;gap:12px;padding:0 16px;border:2px solid var(--linha);border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;text-align:left;cursor:pointer}
.metodo::before{content:"";width:18px;height:18px;flex-shrink:0;border-radius:50%;border:2px solid #B5B8BD;box-sizing:border-box}
.metodo[aria-checked="true"]{border-color:var(--laranja);background:var(--aviso-fundo)}
@@ -248,6 +248,7 @@ html:is([data-rota="pagamento"],[data-rota="pix"]) .pagGrid{display:grid;grid-te
.pagBtn:hover:not(:disabled){background:#FFB53D}
.pagBtn:disabled{background:#EDEDED;color:#8A8D92;cursor:default}
.pagBtn.sec{background:#fff;border:1px solid #D6D6D6}
.desafio{display:block;width:100%;max-width:520px;height:560px;border:1px solid var(--linha);border-radius:12px;background:#fff}
.pixBox{display:flex;flex-direction:column;align-items:flex-start;gap:12px}
.pixBox img{border:1px solid var(--linha);border-radius:12px;padding:8px;background:#fff}
.pixBox .codigo{display:flex;gap:10px;width:100%}