All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 2m19s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 50s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
454 lines
22 KiB
JavaScript
454 lines
22 KiB
JavaScript
/* Checkout bridge only: approved commercial functions in web/index.html stay intact.
|
|
The cart sends the order; the quote is paid on its own page (/pagamento). */
|
|
(() => {
|
|
const status = document.getElementById('checkoutStatus');
|
|
const actions = document.getElementById('checkoutActions');
|
|
const resumo = document.getElementById('pagResumo');
|
|
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');
|
|
let requestBody = localStorage.getItem('dtf-request-body');
|
|
let quotedCart = localStorage.getItem('dtf-quote-cart');
|
|
let refreshVersion = 0;
|
|
function cartSnapshot() {
|
|
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
|
return JSON.stringify({customer:cliente,delivery:entrega,items:items.map(item=>({
|
|
mode:item.modo,metres:item.metros,grade:item.nota,production:item.production,
|
|
quality:item.qualityStatus,
|
|
acknowledged:item.qualityAcknowledged,
|
|
files:(item.localFiles||[]).map(file=>({name:file.name,size:file.size,lastModified:file.lastModified}))
|
|
}))});
|
|
}
|
|
function clearDraft() {
|
|
draftId=null;quotedCart=null;requestKey=null;requestBody=null;
|
|
for(const key of ['dtf-quote','dtf-quote-cart','dtf-request-key','dtf-request-body'])
|
|
localStorage.removeItem(key);
|
|
actions.replaceChildren();
|
|
}
|
|
const api = async (path, body) => {
|
|
const response = await fetch('/api'+path, {
|
|
credentials: 'same-origin', headers: {'Content-Type':'application/json'},
|
|
...(body === undefined ? {} : {method:'POST', body:JSON.stringify(body)})
|
|
});
|
|
const text = await response.text();
|
|
let data;
|
|
try { data = text ? JSON.parse(text) : {}; }
|
|
catch (_) { throw new Error(response.ok ? 'Resposta inválida do serviço.' : 'O serviço está indisponível. Tente novamente em instantes.'); }
|
|
if (!response.ok) { const error=new Error(typeof data.detail === 'string' ? data.detail : 'Confira os dados do pedido ('+response.status+').');error.status=response.status;throw error; }
|
|
return data;
|
|
};
|
|
const ready = api('/session');
|
|
ready.then(session=>{
|
|
window.dtfUploadMaxBytes=session.max_upload_bytes;
|
|
const limit=document.getElementById('zLimite');
|
|
if(limit)limit.textContent='Até '+(session.max_upload_bytes/1048576).toFixed(0)+
|
|
' MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.';
|
|
}).catch(()=>{});
|
|
window.dtfSessionReady=ready;
|
|
window.dtfApi=api;
|
|
ready.catch(error => { status.textContent = error.message; });
|
|
function message(text) { status.textContent = text; }
|
|
function button(label, handler, into = actions, className = 'pagBtn') {
|
|
const el = document.createElement('button');
|
|
el.type = 'button';
|
|
el.className = className;
|
|
el.textContent = label;
|
|
el.onclick = handler;
|
|
into.append(el);
|
|
return el;
|
|
}
|
|
function node(tag, text, className) {
|
|
const el=document.createElement(tag);
|
|
if (text!=null) el.textContent=text;
|
|
if (className) el.className=className;
|
|
return el;
|
|
}
|
|
// What the customer is paying for, from the server's approval.
|
|
function pintaResumo(quote) {
|
|
const approved = quote ? quote.approved : null;
|
|
resumo.hidden = !approved;
|
|
if (!approved) { resumo.replaceChildren(); return; }
|
|
const linha = (label, value, className='l') => {
|
|
const row=document.createElement('div'); row.className=className;
|
|
const name=document.createElement('span'); name.textContent=label;
|
|
const amount=document.createElement('b'); amount.textContent=value;
|
|
row.append(name,amount); return row;
|
|
};
|
|
const rows = approved.items.map(item => linha(
|
|
(MODOS[item.mode]?.tit || item.mode)+' · '+fmtM(Number(item.billed_metres))+' m', rs(item.total_cents/100)));
|
|
const pickup = approved.freight.service === 'pickup';
|
|
const prazo = approved.freight.days ? ' · até '+approved.freight.days+' dias úteis' : '';
|
|
rows.push(linha(pickup ? 'Retirada em Franca' : 'Frete'+prazo,
|
|
approved.freight.total_cents ? rs(approved.freight.total_cents/100) : 'Grátis'));
|
|
rows.push(linha('Total', rs(approved.total_cents/100), 'tot'));
|
|
resumo.replaceChildren(node('h4','Resumo do pedido'), ...rows);
|
|
}
|
|
function semPedido() {
|
|
pintaResumo(null);
|
|
actions.replaceChildren();
|
|
message('Nenhum pedido aguardando pagamento.');
|
|
button('Ir para o carrinho', () => vaiPara(CARRINHO));
|
|
}
|
|
async function upload(file) {
|
|
const session=await ready;
|
|
return window.dtfUpload(file,{api,progress:message,scope:session.cart_scope});
|
|
}
|
|
// The cart's package: billed metres and value. The charged freight is
|
|
// quoted again by the server from the approved items.
|
|
const pacote = () => {
|
|
const items=[...pedido,...(itemAtual?[itemAtual]:[])];
|
|
return {metres:items.reduce((t,it)=>t+(it.cob||0),0), cents:Math.round(items.reduce((t,it)=>t+(it.total||0),0)*100)};
|
|
};
|
|
let freightQuoted = null;
|
|
window.dtfFreight = async () => {
|
|
const cep = entrega.cep;
|
|
const service = (await ready).freight_service;
|
|
if (!service) {
|
|
$('cepMsg').textContent = 'A entrega ainda não está disponível. Escolha a retirada em Franca.';
|
|
return;
|
|
}
|
|
const {metres, cents} = pacote();
|
|
if (!metres) { $('cepMsg').textContent = 'Adicione um item ao pedido para cotar o frete.'; return; }
|
|
$('cepMsg').textContent = 'Cotando o frete…';
|
|
try {
|
|
const result = await api('/freight',{service,postal_code:cep,metres:metres.toFixed(2),declared_cents:cents});
|
|
if (entrega.cep !== cep || entrega.tipo !== 'frete') return;
|
|
entrega.valor = result.total_cents/100;
|
|
entrega.dias = result.days || null;
|
|
entrega.cotado = true;
|
|
freightQuoted = JSON.stringify(pacote());
|
|
$('cepMsg').textContent = '';
|
|
pintaEntrega();
|
|
} catch(error) { $('cepMsg').textContent = error.message; }
|
|
};
|
|
// A different package is a different freight: quote it again.
|
|
window.addEventListener('dtf-cart-changed',()=>{
|
|
if (entrega.tipo==='frete' && entrega.cotado && freightQuoted && freightQuoted!==JSON.stringify(pacote())) {
|
|
entrega.cotado=false; freightQuoted=null; window.dtfFreight();
|
|
}
|
|
});
|
|
window.dtfCheckout = async () => {
|
|
if (busy) return;
|
|
// The cart that was sent goes straight to its payment; a changed cart is sent again.
|
|
if (draftId && quotedCart === cartSnapshot()) { await refresh(true); return; }
|
|
if (draftId) clearDraft();
|
|
if (!clienteOk() || !entrega.cotado) return;
|
|
if (!enderecoOk()) return message('Preencha o endereço de entrega.');
|
|
if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.');
|
|
const cart = [...pedido,...(itemAtual?[itemAtual]:[])];
|
|
if (!cart.length) return message('Adicione um item ao pedido.');
|
|
const initialCart=cartSnapshot();
|
|
busy = true;
|
|
$('bPagar').disabled = true;
|
|
try {
|
|
await ready;
|
|
if((await api('/session')).cart_scope !== (await ready).cart_scope) throw new Error('Sua conta ou sessão mudou. Recarregue a página antes de enviar o carrinho.');
|
|
const items=[];
|
|
for (const item of cart) {
|
|
if (!item.localFiles?.length) throw new Error('Selecione novamente os arquivos deste item.');
|
|
const uploads=[];
|
|
for (const file of item.localFiles) uploads.push(await upload(file));
|
|
if (item.production?.version!==2 || item.production.sources?.length!==uploads.length)
|
|
throw new Error('A montagem deste item precisa ser refeita antes da cotação.');
|
|
items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads,
|
|
production:{...item.production,sources:item.production.sources.map((source,index)=>({
|
|
upload_id:uploads[index],...source}))},
|
|
quality_status:item.qualityStatus,quality_acknowledged:item.qualityAcknowledged});
|
|
}
|
|
const service = entrega.tipo==='retira' ? 'pickup' : (await ready).freight_service;
|
|
if (!service) throw new Error('A entrega ainda não está disponível. Escolha a retirada em Franca.');
|
|
const content = {customer:{...cliente},items,freight:{service,postal_code:entrega.tipo==='retira'?'':entrega.cep}};
|
|
const destination = destinoApi();
|
|
if (destination) content.destination = destination;
|
|
if (cartSnapshot()!==initialCart) throw new Error('O carrinho mudou durante o envio. Confira os itens e envie de novo.');
|
|
const serialized = JSON.stringify(content);
|
|
if (!requestKey || serialized !== requestBody) {
|
|
requestKey = crypto.randomUUID(); requestBody = serialized;
|
|
localStorage.setItem('dtf-request-key',requestKey);
|
|
localStorage.setItem('dtf-request-body',requestBody);
|
|
}
|
|
const quote = await api('/quotes',{request_key:requestKey,...content});
|
|
quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart);
|
|
draftId=quote.id; localStorage.setItem('dtf-quote',draftId);
|
|
await refresh(true);
|
|
} catch(error) { message(error.message); }
|
|
finally { busy=false; pintaEntrega(); }
|
|
};
|
|
// `go`: the customer asked to pay, so the cart page moves to the payment page.
|
|
async function refresh(go) {
|
|
if (!naPagina()) { if (!go || !draftId) return; }
|
|
else if (!draftId) { semPedido(); return; }
|
|
const version=++refreshVersion, shownId=draftId;
|
|
try {
|
|
await ready;
|
|
const quote=await api('/quotes/'+shownId);
|
|
if(version!==refreshVersion || draftId!==shownId) return;
|
|
if (!naPagina()) { message(''); vaiPara(PAGAMENTO); return; }
|
|
shownCart=cartSnapshot();
|
|
clearInterval(pixClock);
|
|
unmountCard();
|
|
actions.replaceChildren();
|
|
message('');
|
|
pintaResumo(quote);
|
|
document.querySelector('.pagVolta').hidden = quote.status==='paid';
|
|
if (quote.status==='paid') { confirmado(quote); return; }
|
|
if (naPix() && quote.status!=='approved') { vaiPara(PAGAMENTO); return; }
|
|
if (!quotedCart || quotedCart!==cartSnapshot()) {
|
|
message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.');
|
|
button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();});
|
|
return;
|
|
}
|
|
if (quote.status==='pending_review') {
|
|
message((await ready).environment==='local'
|
|
? 'Arquivos enviados. No Kanban, confira metragem e nota e aprove a cotação '+draftId.slice(0,8)+'.'
|
|
: 'Arquivos enviados. Nossa equipe está conferindo a cotação '+draftId.slice(0,8)+'; o valor final aparece aqui em seguida.');
|
|
} else if (quote.status==='approved') {
|
|
if ((await ready).payment_provider === 'mercadopago') {
|
|
if (naPix()) await paginaPix(quote, version);
|
|
else await escolhaPagamento(quote, version);
|
|
return;
|
|
}
|
|
if ((await ready).environment !== 'local') {
|
|
message('Cotação revisada. O pagamento online ainda não está disponível.');
|
|
return;
|
|
}
|
|
// Local stack only: the simulated payment.
|
|
message('Total validado no servidor: '+rs(quote.approved.total_cents/100)+' · inclui frete. Cotação válida por 24 horas.');
|
|
button('Criar pedido de teste',async event=>{
|
|
if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; }
|
|
event.target.disabled=true;
|
|
try {
|
|
const order=await api('/orders/dev-paid',{quote_id:draftId});
|
|
pedido=[]; itemAtual=null; limpaPaineis();
|
|
await window.dtfClearCart?.();
|
|
message('Pedido #'+order.number+' criado e disponível no Kanban.');
|
|
await refresh();
|
|
} catch(error) { message(error.message); event.target.disabled=false; }
|
|
});
|
|
} else {
|
|
message('Cotação expirada. Envie o carrinho de novo para pagar.');
|
|
button('Voltar ao carrinho',()=>{clearDraft();vaiPara(CARRINHO);});
|
|
}
|
|
} catch(error) {
|
|
if(version!==refreshVersion || draftId!==shownId) return;
|
|
message(error.message);
|
|
actions.replaceChildren();
|
|
button('Limpar referência e tentar de novo',clearDraft);
|
|
}
|
|
}
|
|
// The order is created by Mercado Pago's notification, never by this page,
|
|
// so after paying the page only waits for the order to exist.
|
|
let waitTimer=null;
|
|
function waitForOrder() {
|
|
clearInterval(waitTimer);
|
|
waitTimer=setInterval(async()=>{
|
|
try {
|
|
const quote=await api('/quotes/'+draftId);
|
|
if (quote.status==='paid') {
|
|
clearInterval(waitTimer);
|
|
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');
|
|
box.append(node('h4','Pagamento confirmado'),
|
|
node('p','Pedido #'+quote.order.number+' recebido e enviado para a produção. Você acompanha cada etapa em Meus pedidos.'));
|
|
button('Ver meus pedidos',()=>location.assign('/portal.html?order='+quote.order.id),box);
|
|
actions.append(box);
|
|
}
|
|
const cartOk=()=>!!quotedCart && quotedCart===cartSnapshot();
|
|
|
|
// 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?[['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');
|
|
const area=node('div',null,'area');
|
|
const radios=opcoes.map(([key,label])=>{
|
|
const el=button(label,()=>{ if(escolhido!==key){ escolhido=key; pinta(); } },grupo,'metodo');
|
|
el.setAttribute('role','radio'); el.dataset.metodo=key;
|
|
return el;
|
|
});
|
|
actions.append(node('h4','Forma de pagamento'),grupo,area);
|
|
async function pinta() {
|
|
radios.forEach(el=>el.setAttribute('aria-checked',String(el.dataset.metodo===escolhido)));
|
|
unmountCard(); area.replaceChildren(); message('');
|
|
if (escolhido!=='pix') { await showCard(quote.approved, area, version, escolhido); return; }
|
|
area.append(node('p','Clique em Pagar para gerar o QR code do PIX.','pagNota'));
|
|
button('Pagar',async event=>{
|
|
if (!cartOk()) { await refresh(); return; }
|
|
event.target.disabled=true;
|
|
try {
|
|
await api('/payments/intent',{quote_id:draftId,method:{type:'pix'}});
|
|
vaiPara(PAGAMENTO_PIX);
|
|
} catch(error) { message(error.message); event.target.disabled=false; }
|
|
},area);
|
|
}
|
|
await pinta();
|
|
}
|
|
|
|
// The PIX page. The intent is idempotent on the quote, so reopening this
|
|
// page shows the same QR code instead of creating another charge.
|
|
async function paginaPix(quote, version) {
|
|
let intent;
|
|
try { intent=await api('/payments/intent',{quote_id:draftId,method:{type:'pix'}}); }
|
|
catch(error) {
|
|
if (version!==refreshVersion) return;
|
|
message(error.message);
|
|
button('Escolher outra forma de pagamento',()=>vaiPara(PAGAMENTO),actions,'pagBtn sec');
|
|
return;
|
|
}
|
|
if (version!==refreshVersion) return;
|
|
if (!intent.pix_qr_code) { message('Não foi possível gerar o PIX. Tente de novo em instantes.'); return; }
|
|
const box=node('div',null,'pixBox');
|
|
box.append(node('h4','Pague com PIX'),
|
|
node('p','Abra o app do seu banco, escolha pagar com PIX e leia o QR code ou cole o código abaixo.','pagNota'));
|
|
if (intent.pix_qr_code_base64) {
|
|
const img=document.createElement('img');
|
|
img.src='data:image/png;base64,'+intent.pix_qr_code_base64;
|
|
img.alt='QR code do PIX';img.width=240;img.height=240;
|
|
box.append(img);
|
|
}
|
|
const linha=node('div',null,'codigo');
|
|
const code=document.createElement('input');
|
|
code.readOnly=true;code.value=intent.pix_qr_code;code.setAttribute('aria-label','Código PIX copia e cola');
|
|
linha.append(code);
|
|
const copiar=button('Copiar código',async()=>{
|
|
try{ await navigator.clipboard.writeText(intent.pix_qr_code); copiar.textContent='Código copiado'; }
|
|
catch(_){ code.select(); }
|
|
},linha);
|
|
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
|
|
// Mercado Pago's secure fields and becomes a one-time token; the number never
|
|
// reaches this page's code or our server.
|
|
let sdkLoading=null;
|
|
function loadMercadoPago() {
|
|
if (window.MercadoPago) return Promise.resolve();
|
|
sdkLoading = sdkLoading || new Promise((resolve,reject)=>{
|
|
const script=document.createElement('script');
|
|
script.src='https://sdk.mercadopago.com/js/v2';
|
|
script.onload=resolve;
|
|
script.onerror=()=>{sdkLoading=null;reject(new Error('Não foi possível carregar o formulário do Mercado Pago.'));};
|
|
document.head.append(script);
|
|
});
|
|
return sdkLoading;
|
|
}
|
|
let cardBrick=null;
|
|
function unmountCard() {
|
|
if (cardBrick) { try { cardBrick.unmount(); } catch(_) {} cardBrick=null; }
|
|
}
|
|
async function showCard(approved, into, version, tipo) {
|
|
const holder=node('div');
|
|
holder.id='cardPaymentBrick';
|
|
into.append(holder);
|
|
try { await loadMercadoPago(); }
|
|
catch(error) { message(error.message); return; }
|
|
const session=await ready;
|
|
// The customer may have switched to PIX, or the page re-rendered, meanwhile.
|
|
if (!holder.isConnected || version!==refreshVersion) return;
|
|
unmountCard();
|
|
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}},
|
|
// 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']}},
|
|
// The option above already names the card; the form's own title
|
|
// ("crédito ou débito") would contradict it.
|
|
visual:{hideFormTitle:true,
|
|
style:{customVariables:{baseColor:'#FFA81A',buttonTextColor:'#03060B'}}}},
|
|
callbacks:{
|
|
onReady:()=>{},
|
|
onError:error=>{ console.error(error); message('Erro no formulário do cartão. Confira os dados e tente de novo.'); },
|
|
onSubmit:async data=>{
|
|
if (!cartOk()) { await refresh(); throw new Error('cart changed'); }
|
|
message('');
|
|
const result=await api('/payments/intent',{quote_id:draftId,method:{
|
|
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'
|
|
? 'Pagamento aprovado. Confirmando seu pedido…'
|
|
: 'Pagamento em análise pelo Mercado Pago. Esta página atualiza assim que ele for confirmado.','pagNota'));
|
|
waitForOrder();
|
|
} else {
|
|
message('Pagamento recusado pelo Mercado Pago ('+(result.status_detail||result.status)+'). '+
|
|
'Confira os dados ou use outro cartão.');
|
|
throw new Error('rejected');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (!holder.isConnected || version!==refreshVersion) unmountCard();
|
|
}
|
|
const quoteFromPortal=new URLSearchParams(location.search).get('quote');
|
|
if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){
|
|
if(draftId!==quoteFromPortal){quotedCart=null;localStorage.removeItem('dtf-quote-cart');}
|
|
draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId);
|
|
}
|
|
// Re-render the payment page only when the cart really changed: re-rendering
|
|
// would remount the card form under the customer's typing.
|
|
window.addEventListener('dtf-cart-changed',()=>{if(draftId && naPagina() && shownCart!==cartSnapshot()) refresh();});
|
|
// 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++; clearInterval(pixClock); message(''); actions.replaceChildren(); pintaResumo(null); }
|
|
});
|
|
refresh();
|
|
})();
|