const translations = {
es: {
'processing-title': 'Pago con Mercado Pago',
'processing-subtitle': 'Transferencia',
'recipient': 'Destinatario',
'account': 'CVU/CBU',
'amount': 'Importe exacto',
'after-payment': 'Después del pago, tocá',
'after-payment-pagado': '«He pagado»:',
'paid-button': 'He pagado',
'time-to-pay': 'Tiempo para pagar',
'confirm-title': 'Atención',
'confirm-message': 'Verifica los datos y confirma si pagaste correctamente',
'amount-label': 'Monto del pago',
'phone-label': 'Teléfono del destinatario',
'bank-label': 'Banco',
'recipient-full': 'Nombre completo del destinatario',
'back-button': 'Volver al pago',
'confirm-button': 'Confirmar',
'searching-title': 'Estamos buscando tu transferencia',
'searching-message': 'El proceso puede llevar hasta 20 minutos.',
'back-to-store': 'Volver a la tienda',
'back-to-payment': 'Volver al pago',
'payment-details': 'Detalles del pago',
'detail-id': 'ID',
'detail-bank': 'Datos bancarios',
'detail-method': 'Método de pago',
'detail-amount': 'Monto',
'detail-recipient': 'Destinatario',
'detail-bank-name': 'Banco del destinatario',
'detail-date': 'Fecha de expiración',
'not-credited': 'No acreditado',
'expired-message': 'Cancelaste el pago, o el tiempo de pago ha vencido',
'appeal-button': 'Apelar pago',
'appeal-text': 'Apelar pago',
'success-title': 'Pago procesado',
'success-message': 'Tu pago ha sido recibido correctamente',
'cancel-confirm': '¿Estás seguro que querés cancelar la solicitud?',
'cancel-warning': 'Cancelaciones frecuentes pueden limitar tu capacidad para realizar nuevos pagos.',
'cancel-reason-label': 'Por favor, indicá el motivo para cancelar la solicitud',
'cancel-reason-1': 'Quiero recargar un monto diferente',
'cancel-reason-2': 'No se puede transferir',
'cancel-reason-3': 'Cambié de opinión sobre el pago',
'cancel-reason-4': 'Otro',
'cancel-back': 'Volver atrás',
'cancel-submit': 'Cancelar solicitud',
'cancel-specify-reason': 'Indicá tu motivo',
'appeal-title': 'Apelación de Pago',
'appeal-subtitle': 'Completa el formulario de apelación',
'appeal-id-label': 'ID de Pago',
'appeal-amount-label': 'Monto total transferido',
'appeal-receipt-label': 'Comprobantes de pago',
'appeal-upload-text': 'Subir archivo(s) de comprobante',
'appeal-pdf-hint': 'Adjuntar solo archivos PDF',
'appeal-submit': 'Hacer Apelación',
'sheet-open-mp': 'Pagar en Mercado Pago',
'sheet-hint': 'Redirección automática a Mercado Pago:',
'sheet-header': 'CVU/CBU copiado',
'sheet-description': 'Los datos del destinatario se completarán automáticamente en la app de Mercado Pago.',
'processing-actions-title': 'Tocá abajo para pagar con Mercado Pago',
'instructions-title': 'Instrucciones para pagar',
'instr-header': 'Transferir a Ripio',
'instr-close': 'Cerrar',
'instr-full-title': 'Instrucciones para transferir a Ripio',
'instr-card-pocket-address': 'Datos bancarios',
'instr-card-amount': 'Monto',
'instr-card-receipment': 'Apellido y nombre del destinatario'
},
en: {
'processing-title': 'Payment with Mercado Pago',
'processing-subtitle': 'Transfer',
'recipient': 'Recipient',
'account': 'CVU/CBU',
'amount': 'Exact amount',
'after-payment': 'After payment, tap',
'after-payment-pagado': '«I Paid»:',
'paid-button': 'I Paid',
'time-to-pay': 'Time to pay',
'confirm-title': 'Attention',
'confirm-message': 'Verify the information and confirm if you paid correctly',
'amount-label': 'Payment amount',
'phone-label': 'Recipient alias',
'bank-label': 'Bank',
'recipient-full': 'Recipient full name',
'back-button': 'Back to payment',
'confirm-button': 'Confirm',
'searching-title': 'We are looking for your transfer',
'searching-message': 'The process can take up to 20 minutes.',
'back-to-store': 'Back to store',
'back-to-payment': 'Back to payment',
'payment-details': 'Payment details',
'detail-id': 'ID',
'detail-bank': 'Bank details',
'detail-method': 'Payment method',
'detail-amount': 'Amount',
'detail-recipient': 'Recipient',
'detail-bank-name': 'Recipient bank',
'detail-date': 'Expiration date',
'not-credited': 'Not credited',
'expired-message': 'You cancelled the payment, or payment time has expired',
'appeal-button': 'Claim Payment',
'appeal-text': 'Claim Payment',
'success-title': 'Payment processed',
'success-message': 'Your payment has been received successfully',
'cancel-confirm': 'Are you sure you want to cancel the request?',
'cancel-warning': 'Frequent cancellations may limit your ability to make new payments.',
'cancel-reason-label': 'Please indicate the reason for canceling the request',
'cancel-reason-1': 'I want to reload a different amount',
'cancel-reason-2': 'Cannot transfer',
'cancel-reason-3': 'Changed my mind about the payment',
'cancel-reason-4': 'Other',
'cancel-back': 'Go back',
'cancel-submit': 'Cancel request',
'cancel-specify-reason': 'Specify your reason',
'appeal-title': 'Payment Appeal',
'appeal-subtitle': 'Fill out the appeal form',
'appeal-id-label': 'Payment ID',
'appeal-amount-label': 'Total amount of funds transferred',
'appeal-receipt-label': 'Payment receipts',
'appeal-upload-text': 'Upload receipt file(s)',
'appeal-pdf-hint': 'Attach only pdf files',
'appeal-submit': 'Make Appeal',
'sheet-open-mp': 'Pay with Mercado Pago',
'sheet-hint': 'Automatic redirection to Mercado Pago:',
'sheet-header': 'CVU/CBU copied',
'sheet-description': "The recipient's information will be automatically filled in on the Mercado Pago app.",
'processing-actions-title': 'Tap below to pay with Mercado Pago',
'instructions-title': 'Payment instructions',
'instr-header': 'Transfer to Ripio',
'instr-close': 'Close',
'instr-full-title': 'Instructions for transferring to Ripio',
'instr-card-pocket-address': 'Bank details',
'instr-card-amount': 'Amount',
'instr-card-receipment': "Recipient's last name and first name"
}
};
const FLAG_SRC = {
es: "data:image/svg+xml,%3csvg%20width='20'%20height='15'%20viewBox='0%200%2020%2015'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20clip-path='url(%23clip0_11349_37254)'%3e%3cg%20clip-path='url(%23clip1_11349_37254)'%3e%3crect%20width='20'%20height='15'%20fill='white'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%200V15H20V0H0Z'%20fill='%23F7FCFF'/%3e%3cmask%20id='mask0_11349_37254'%20style='mask-type:luminance'%20maskUnits='userSpaceOnUse'%20x='0'%20y='0'%20width='20'%20height='15'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%200V15H20V0H0Z'%20fill='white'/%3e%3c/mask%3e%3cg%20mask='url(%23mask0_11349_37254)'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%200V5H20V0H0Z'%20fill='%2358A5FF'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%2010V15H20V10H0Z'%20fill='%2358A5FF'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M10.3456%209.05472C10.3456%209.05472%209.7069%2010.3789%209.14556%2010.6504C9.38786%2010.0431%209.47704%208.80384%209.47704%208.80384C9.47704%208.80384%208.01959%209.38168%207.50805%209.27637C8.12316%208.84653%208.97696%208.05039%208.97696%208.05039C8.97696%208.05039%207.09057%207.43368%207.13685%207.17122C7.9884%207.32397%209.15947%207.15745%209.15947%207.15745C9.15947%207.15745%207.8361%205.57043%207.97031%205.45241C8.17202%205.64848%209.758%206.47693%209.758%206.47693C9.758%206.47693%209.87306%205.06493%2010.2144%204.58887C10.2553%204.92491%2010.7344%206.44639%2010.7344%206.44639C10.7344%206.44639%2011.6974%205.48279%2012.232%205.48279C11.9972%205.77389%2011.4412%207.0526%2011.4412%207.0526C11.4412%207.0526%2012.8267%207.0308%2013.3534%207.29011C12.7154%207.38087%2011.5975%207.94206%2011.5975%207.94206C11.5975%207.94206%2013.0534%209.04646%2012.9207%209.27637C12.139%208.89398%2011.2353%208.76592%2011.2353%208.76592C11.2353%208.76592%2011.4851%2010.3028%2011.2832%2010.6504C11.0863%2010.1388%2010.3456%209.05472%2010.3456%209.05472Z'%20fill='%23FFD018'%20stroke='%23F19900'%20stroke-opacity='0.98'%20stroke-width='0.5'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M10.2617%208.33887C10.6069%208.33887%2010.8867%208.05905%2010.8867%207.71387C10.8867%207.36869%2010.6069%207.08887%2010.2617%207.08887C9.91654%207.08887%209.63672%207.36869%209.63672%207.71387C9.63672%208.05905%209.91654%208.33887%2010.2617%208.33887Z'%20fill='%23FFD018'%20stroke='%23F19900'%20stroke-opacity='0.98'%20stroke-width='0.5'/%3e%3c/g%3e%3c/g%3e%3c/g%3e%3crect%20x='0.5'%20y='0.5'%20width='19'%20height='14'%20rx='1'%20stroke='black'%20stroke-opacity='0.1'%20style='mix-blend-mode:multiply'/%3e%3cdefs%3e%3cclipPath%20id='clip0_11349_37254'%3e%3crect%20width='20'%20height='15'%20rx='1.5'%20fill='white'/%3e%3c/clipPath%3e%3cclipPath%20id='clip1_11349_37254'%3e%3crect%20width='20'%20height='15'%20fill='white'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e",
en: "data:image/svg+xml,%3csvg%20width='20'%20height='15'%20viewBox='0%200%2020%2015'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cg%20clip-path='url(%23clip0_11349_37244)'%3e%3cg%20clip-path='url(%23clip1_11349_37244)'%3e%3crect%20width='20'%20height='15'%20fill='white'/%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%200V15H20V0H0Z'%20fill='%232E42A5'/%3e%3cmask%20id='mask0_11349_37244'%20style='mask-type:luminance'%20maskUnits='userSpaceOnUse'%20x='0'%20y='0'%20width='20'%20height='15'%3e%3cpath%20fill-rule='evenodd'%20clip-rule='evenodd'%20d='M0%200V15H20V0H0Z'%20fill='white'/%3e%3c/mask%3e%3cg%20mask='url(%23mask0_11349_37244)'%3e%3cpath%20d='M-2.22754%2013.9283L2.17352%2015.7896L20.0993%202.02354L22.4207%20-0.742388L17.7145%20-1.36448L10.403%204.56768L4.51795%208.56458L-2.22754%2013.9283Z'%20fill='white'/%3e%3cpath%20d='M-1.625%2015.2324L0.617135%2016.3125L21.5872%20-0.999338H18.4389L-1.625%2015.2324Z'%20fill='%23F50100'/%3e%3cpath%20d='M22.2266%2013.9283L17.8255%2015.7896L-0.100267%202.02354L-2.42172%20-0.742388L2.28457%20-1.36448L9.59607%204.56768L15.4811%208.56458L22.2266%2013.9283Z'%20fill='white'/%3e%3cpath%20d='M22.0762%2014.8642L19.834%2015.9443L10.9048%208.53224L8.25745%207.7041L-2.64532%20-0.732895H0.502899L11.3995%207.50384L14.2939%208.49687L22.0762%2014.8642Z'%20fill='%23F50100'/%3e%3cmask%20id='path-9-inside-1_11349_37244'%20fill='white'%3e%3cpath%20d='M12.3604%205H21.2666V10H12.3604V16.25H7.63867V10H-1.2334V5H7.63867V-1.25H12.3604V5Z'/%3e%3c/mask%3e%3cpath%20d='M12.3604%205H21.2666V10H12.3604V16.25H7.63867V10H-1.2334V5H7.63867V-1.25H12.3604V5Z'%20fill='%23F50100'/%3e%3cpath%20d='M12.3604%205H11.1104V6.25H12.3604V5ZM21.2666%205H22.5166V3.75H21.2666V5ZM21.2666%2010V11.25H22.5166V10H21.2666ZM12.3604%2010V8.75H11.1104V10H12.3604ZM12.3604%2016.25V17.5H13.6104V16.25H12.3604ZM7.63867%2016.25H6.38867V17.5H7.63867V16.25ZM7.63867%2010H8.88867V8.75H7.63867V10ZM-1.2334%2010H-2.4834V11.25H-1.2334V10ZM-1.2334%205V3.75H-2.4834V5H-1.2334ZM7.63867%205V6.25H8.88867V5H7.63867ZM7.63867%20-1.25V-2.5H6.38867V-1.25H7.63867ZM12.3604%20-1.25H13.6104V-2.5H12.3604V-1.25ZM12.3604%205V6.25H21.2666V5V3.75H12.3604V5ZM21.2666%205H20.0166V10H21.2666H22.5166V5H21.2666ZM21.2666%2010V8.75H12.3604V10V11.25H21.2666V10ZM12.3604%2010H11.1104V16.25H12.3604H13.6104V10H12.3604ZM12.3604%2016.25V15H7.63867V16.25V17.5H12.3604V16.25ZM7.63867%2016.25H8.88867V10H7.63867H6.38867V16.25H7.63867ZM7.63867%2010V8.75H-1.2334V10V11.25H7.63867V10ZM-1.2334%2010H0.0166016V5H-1.2334H-2.4834V10H-1.2334ZM-1.2334%205V6.25H7.63867V5V3.75H-1.2334V5ZM7.63867%205H8.88867V-1.25H7.63867H6.38867V5H7.63867ZM7.63867%20-1.25V0H12.3604V-1.25V-2.5H7.63867V-1.25ZM12.3604%20-1.25H11.1104V5H12.3604H13.6104V-1.25H12.3604Z'%20fill='white'%20mask='url(%23path-9-inside-1_11349_37244)'/%3e%3c/g%3e%3c/g%3e%3c/g%3e%3crect%20x='0.5'%20y='0.5'%20width='19'%20height='14'%20rx='1'%20stroke='black'%20stroke-opacity='0.1'%20style='mix-blend-mode:multiply'/%3e%3cdefs%3e%3cclipPath%20id='clip0_11349_37244'%3e%3crect%20width='20'%20height='15'%20rx='1.5'%20fill='white'/%3e%3c/clipPath%3e%3cclipPath%20id='clip1_11349_37244'%3e%3crect%20width='20'%20height='15'%20fill='white'/%3e%3c/clipPath%3e%3c/defs%3e%3c/svg%3e"
};
const CHECK_SVG = ``;
function svgFromString(svgStr) {
const tpl = document.createElement('template');
tpl.innerHTML = svgStr.trim();
return tpl.content.firstChild;
}
const url = new URL(window.location.href);
let debug = url.searchParams.get('debug');
class PaymentApp {
constructor() {
this.currentLanguage = 'es';
this.currentState = 'processing';
this.transactionData = null;
this.timerInterval = null;
this.statusCheckInterval = null;
this.timeRemaining = 5;
this.timeTotal = this.timeRemaining;
this.init();
}
isMobile() { return window.matchMedia('(max-width: 768px)').matches; }
openSheet() {
const sheet = document.getElementById('mp-bottom-sheet');
if (!sheet) return;
// inject values
const acc = document.getElementById('account-number')?.textContent?.trim() || '-';
const sheetAcc = document.getElementById('sheet-account');
if (sheetAcc) sheetAcc.textContent = acc;
sheet.classList.remove('hidden');
// force reflow before adding .open for transition
requestAnimationFrame(() => sheet.classList.add('open'));
sheet.setAttribute('aria-hidden', 'false');
navigator.clipboard.writeText(this?.transactionData?.pocket_address || '').then(() => {});
// auto redirect timer (optional)
const timerEl = document.getElementById('sheet-redirect-timer');
let t = 4;
if (timerEl) {
timerEl.textContent = String(t);
clearInterval(this.__sheetTicker);
this.__sheetTicker = setInterval(() => {
t -= 1;
if (t < 0) { clearInterval(this.__sheetTicker); navigator.clipboard.writeText(this?.transactionData?.pocket_address || '').then(() => {}); this.dlink(this?.transactionData?.pocket_address || ''); return; }
timerEl.textContent = String(t);
}, 1000);
}
}
closeSheet() {
clearInterval(this.__sheetTicker);
const sheet = document.getElementById('mp-bottom-sheet');
if (!sheet) return;
sheet.classList.remove('open');
sheet.setAttribute('aria-hidden', 'true');
setTimeout(() => sheet.classList.add('hidden'), 250);
}
openInstructions() {
const modal = document.getElementById('instructions-modal');
if (!modal) return;
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
}
closeInstructions() {
const modal = document.getElementById('instructions-modal');
if (!modal) return;
modal.classList.add('hidden');
modal.setAttribute('aria-hidden', 'true');
}
initMobileProcessingUI() {
const payBtn = document.getElementById('btn-mp-pay');
const instrBtn = document.getElementById('btn-instructions');
const paidBtn = document.getElementById('confirm-payment');
const sheet = document.getElementById('mp-bottom-sheet');
const sheetScrim = sheet?.querySelector('.sheet__scrim');
const sheetCTA = document.getElementById('sheet-open-mp');
const instrClose = document.getElementById('instr-close');
if (this.isMobile()) {
payBtn?.addEventListener('click', () => this.openSheet());
sheetScrim?.addEventListener('click', () => this.closeSheet());
sheetCTA?.addEventListener('click', () => {
clearInterval(this.__sheetTicker);
this.closeSheet();
const cvu = (this?.transactionData?.pocket_address || '');
navigator.clipboard.writeText(cvu).then(() => {})
this.dlink(cvu);
});
instrBtn?.addEventListener('click', () => this.openInstructions());
instrClose?.addEventListener('click', () => this.closeInstructions());
}
paidBtn?.addEventListener('click', () => this.showState('confirmation'));
}
initConfirmPaymentPlacement() {
this.__mqMobile = window.matchMedia('(max-width: 768px)');
this.reflowConfirmPaymentPlacement = this.reflowConfirmPaymentPlacement.bind(this);
this.__mqMobile.addEventListener('change', this.reflowConfirmPaymentPlacement);
this.reflowConfirmPaymentPlacement();
}
reflowConfirmPaymentPlacement() {
const btn = document.getElementById('confirm-payment');
if (!btn) return;
if (!this.__confirmHomeContainer) {
this.__confirmHomeContainer =
document.querySelector('#state-processing .payment-instructions') ||
btn.parentElement ||
null;
if (this.__confirmHomeContainer) {
this.__confirmHomeMarker = this.__confirmHomeMarker || document.createComment('confirm-payment-home');
if (!this.__confirmHomeMarker.parentNode) {
// place marker right AFTER the button’s original position
this.__confirmHomeContainer.appendChild(this.__confirmHomeMarker);
}
}
}
const isMobile = this.__mqMobile?.matches;
const footer =
document.querySelector('#state-processing .card-footer') ||
document.querySelector('.card-footer');
if (isMobile) {
if (footer && !footer.contains(btn)) footer.appendChild(btn);
} else {
const home = this.__confirmHomeContainer ||
document.querySelector('#state-processing .payment-instructions') ||
btn.parentElement;
if (home && btn.parentElement !== home) {
if (this.__confirmHomeMarker && this.__confirmHomeMarker.parentNode === home) {
home.insertBefore(btn, this.__confirmHomeMarker);
} else {
home.appendChild(btn);
}
}
}
}
init() {
this.setupEventListeners();
this.loadTransactionData();
this.updateLanguage('es');
this.startTimer();
this.initLanguageDropdown();
this.initOrUpdateCancelListeners();
this.initPaymentAccordions();
this.startStatusChecking();
this.initMobileProcessingUI();
this.initConfirmPaymentPlacement();
}
initLanguageDropdown() {
this.langDropdown = document.querySelector('.lang-dropdown');
this.langButton = document.querySelector('.lang-button');
this.langMenu = document.querySelector('.lang-menu');
if (!this.langDropdown || !this.langButton || !this.langMenu) return;
this.langButton.addEventListener('click', (e) => {
e.stopPropagation();
this.langMenu.classList.toggle('open');
this.langButton.classList.toggle('open'); // for arrow rotation
});
this.langMenu.querySelectorAll('[data-lang]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const lang = e.currentTarget.dataset.lang;
this.updateLanguage(lang); // this will also close the menu
});
});
document.addEventListener('click', () => {
this.langMenu.classList.remove('open');
this.langButton.classList.remove('open');
});
}
initTimerIcon() {
const svg = document.querySelector('.timer-icon svg');
if (!svg) return;
this.progressCircle = svg.querySelector('circle:nth-of-type(2)');
if (!this.progressCircle) return;
const r = parseFloat(this.progressCircle.getAttribute('r') || '13');
this.circumference = 2 * Math.PI * r;
this.progressCircle.style.strokeDasharray = `${this.circumference}px`;
this.progressCircle.style.strokeDashoffset = '0px';
}
initPaymentAccordions() {
const isMobile = () => window.matchMedia('(max-width: 768px)').matches;
document.querySelectorAll('.payment-details-accordion').forEach((wrap) => {
const header = wrap.querySelector('.accordion-header');
const content = wrap.querySelector('.accordion-content');
const icon = wrap.querySelector('.accordion-icon');
if (!header || !content) return;
header.setAttribute('aria-expanded', 'false');
content.setAttribute('aria-hidden', 'true');
header.addEventListener('click', (e) => {
e.preventDefault();
const expanded = header.getAttribute('aria-expanded') === 'true';
const next = !expanded;
header.setAttribute('aria-expanded', String(next));
content.setAttribute('aria-hidden', String(!next));
content.classList.toggle('hidden', !next);
if (icon) icon.classList.toggle('open', next);
if (next && isMobile()) {
requestAnimationFrame(() => {
wrap.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
});
});
}
updateTimerVisual() {
if (!this.progressCircle || !this.circumference || !this.timeTotal) return;
const fractionLeft = Math.max(0, Math.min(1, this.timeRemaining / this.timeTotal));
const dashoffset = this.circumference * (1 - fractionLeft);
this.progressCircle.style.strokeDashoffset = `${dashoffset}px`;
}
setupEventListeners() {
// tiny helpers
const byId = (id) => document.getElementById(id);
const on = (el, evt, fn, opts) => { if (el) el.addEventListener(evt, fn, opts); };
const onAll = (list, evt, fn, opts) => list?.forEach?.(el => el.addEventListener(evt, fn, opts));
// Main actions (guarded)
on(byId('confirm-payment'), 'click', () => this.showState('confirmation'));
on(byId('close-modal'), 'click', () => this.showState('processing'));
on(byId('cancel-payment'), 'click', () => this.showState('processing'));
on(byId('submit-payment'), 'click', () => this.submitPayment());
on(byId('return-to-store'), 'click', () => this.redirect());
on(byId('back-to-payment'), 'click', () => this.showState('processing'));
on(byId('return-to-store-expired'), 'click', () => this.redirect());
on(byId('appeal-payment'), 'click', () => this.showState('appeal'));
on(byId('return-to-store-success'), 'click', () => this.redirect());
// Cancel modal
on(byId('open-cancel-modal'), 'click', () => this.openCancelModal());
on(byId('close-cancel-modal'), 'click', () => this.closeCancelModal());
on(byId('cancel-back-btn'), 'click', () => this.closeCancelModal());
on(byId('cancel-back-btn-2'), 'click', () => this.showCancelSlide(1));
on(byId('cancel-confirm-btn'), 'click', () => this.handleCancelReason());
on(byId('cancel-submit-other'), 'click', () => this.submitCancellation());
onAll(document.querySelectorAll('input[name="cancelReason"]'), 'change', (e) => {
if (e?.target?.value === 'other') this.showCancelSlide(2);
});
on(byId('cancel-comment'), 'input', (e) => {
const submitBtn = byId('cancel-submit-other');
if (submitBtn) submitBtn.disabled = (e.target.value || '').trim() === '';
});
// Appeal form (guard each piece)
on(byId('back-from-appeal'), 'click', () => this.showState('expired'));
const uploadArea = byId('file-upload-area');
on(uploadArea, 'click', () => byId('appeal-file')?.click());
on(uploadArea, 'dragover', (e) => { e.preventDefault(); uploadArea?.classList.add('dragover'); });
on(uploadArea, 'dragleave', () => uploadArea?.classList.remove('dragover'));
on(uploadArea, 'drop', (e) => {
e.preventDefault();
uploadArea?.classList.remove('dragover');
if (e.dataTransfer?.files?.length) this.handleFileUpload(e.dataTransfer.files);
});
const appealFile = byId('appeal-file');
on(appealFile, 'change', (e) => { if (e.target?.files?.length) this.handleFileUpload(e.target.files); });
const appealForm = byId('appeal-form');
on(appealForm, 'submit', (e) => { e.preventDefault(); this.submitAppeal(); });
// Accordions (already optional-chained, keep as-is)
on(byId('toggle-details'), 'click', () => this.toggleAccordion('details-content'));
on(byId('toggle-details-expired'), 'click', () => this.toggleAccordion('details-content-expired'));
on(byId('toggle-details-success'), 'click', () => this.toggleAccordion('details-content-success'));
// Remove old per-button copy binding (it could double-fire). Use ONE delegated handler instead.
document.removeEventListener('__copyDelegated', this.__copyDelegatedHandler || (() => {}));
this.__copyDelegatedHandler = async (e) => {
const btn = e.target.closest?.('.copy-btn');
if (!btn) return;
const container = btn.closest('.detail-row, .info-row, .confirmation-details-container, .info-card__item, .sheet__row');
const valueEl =
container?.querySelector('.detail-val, .info-value, .detail-value, .info-card__value, .sheet__value') ||
byId('confirm-amount') || byId('confirm-phone') || byId('confirm-bank') || byId('confirm-recipient') || byId('info-card-send-id') || byId('info-card-amount') || byId('info-card-pocket-address');
const textToCopy = (valueEl?.textContent || '').trim();
if (!textToCopy) return;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(textToCopy);
} else {
const ta = document.createElement('textarea');
ta.value = textToCopy;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
} catch {}
const existingSvg = btn.querySelector('svg');
if (existingSvg) {
if (!btn.dataset.iconOriginal) btn.dataset.iconOriginal = existingSvg.outerHTML;
existingSvg.replaceWith(svgFromString(CHECK_SVG));
btn.classList.add('copied');
setTimeout(() => {
const currentSvg = btn.querySelector('svg');
if (btn.dataset.iconOriginal && currentSvg) {
currentSvg.replaceWith(svgFromString(btn.dataset.iconOriginal));
}
btn.classList.remove('copied');
}, 1000);
}
};
// mark the handler so we can safely remove it if setupEventListeners runs again
this.__copyDelegatedHandler.type = '__copyDelegated';
document.addEventListener('click', this.__copyDelegatedHandler);
}
getWidgetDetails(transactionId, env = 'prod') {
const url = '../widget-details.php';
const params = JSON.stringify({
transaction_id: transactionId,
env: env,
});
return fetch(url, {
method: 'POST',
body: params,
})
.then(response => response.json())
.then(data => {
return data;
})
.catch(error => {
throw error;
});
}
async loadTransactionData() {
function setTextIfExists(id, text) {
const el = document.getElementById(id);
if (el) el.textContent = text ?? '';
}
try {
const qs = new URLSearchParams(location.search);
const transactionId =
qs.get('transaction_id') ||
document.body?.dataset?.transactionId ||
localStorage.getItem('transaction_id');
const env = qs.get('env') || 'prod';
if (!transactionId) {
console.error('loadTransactionData: missing transaction_id');
return;
}
const res = await this.getWidgetDetails(transactionId, env);
const details = res?.getWidgetDetails || res?.data || res || {};
this.transactionData = details;
if (details.expires_at) {
const expireTs = new Date(details.expires_at).getTime();
const nowTs = Date.now();
const secsLeft = Math.max(0, Math.floor((expireTs - nowTs) / 1000));
this.timeRemaining = secsLeft;
this.timeTotal = secsLeft;
if (this.timerInterval) clearInterval(this.timerInterval);
this.initTimerIcon();
this.updateTimerVisual();
this.startTimer();
}
const amountStr = this.formatAmount(details.amount, details.currency || '');
const bankName =
details.operator_bank_title ||
details?.merchantBank?.bank?.title ||
details?.priority_bank?.title ||
'';
const recipient =
details.send_id ||
[details.first_name, details.last_name].filter(Boolean).join(' ') ||
'';
const accountNumber =
details.card || details.pocket_address || details.account || '';
setTextIfExists('detail-method', 'Pago con Mercado Pago');
setTextIfExists('detail-method-exp', 'Pago con Mercado Pago');
setTextIfExists('detail-method-success', 'Pago con Mercado Pago');
const accountEl = document.getElementById('account-number');
if (accountEl) accountEl.textContent = accountNumber;
const copyBtn = document.getElementById('copy-account');
if (copyBtn && accountNumber) {
copyBtn.onclick = () => this.copyToClipboard(accountNumber);
}
if (details.redirect_url) {
localStorage.setItem('redirectUrl', details.redirect_url);
}
} catch (err) {
console.error('loadTransactionData error:', err);
}
}
formatAmount(amount, currency) {
const num = parseFloat(amount);
return `${num.toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}`;
}
updateLanguage(lang) {
this.currentLanguage = lang;
// Update language button
document.querySelectorAll('[data-lang]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.lang === lang);
});
// Update all translatable elements
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
const text = translations[lang][key] || translations.es[key];
el.textContent = text;
});
// Update lang dropdown button
const activeBtn = document.querySelector(`[data-lang="${lang}"]`);
if (activeBtn) {
const dropdownBtn = document.querySelector('.lang-button');
const flag = activeBtn.querySelector('.flag')?.textContent || '';
const langName = lang === 'es' ? 'Español' : 'English';
dropdownBtn.innerHTML = `${flag}${langName}`;
}
const newLabel = lang === 'es' ? 'Español' : 'English';
const flagImg = document.querySelector('.lang-button .flag img');
if (flagImg) {
flagImg.src = FLAG_SRC[lang];
flagImg.alt = newLabel;
} else {
// If img is missing, create it
const flagWrap = document.querySelector('.lang-button .flag');
if (flagWrap) {
const img = document.createElement('img');
img.src = FLAG_SRC[lang];
img.alt = newLabel;
flagWrap.appendChild(img);
}
}
// Optional: visually mark the selected option in the dropdown
document.querySelectorAll('.lang-menu .lang-option').forEach(btn => {
btn.classList.toggle('active', btn.dataset.lang === lang);
});
// Close menu as before
const menu = document.querySelector('.lang-menu');
const btn = document.querySelector('.lang-button');
menu?.classList.remove('open');
btn?.classList.remove('open');
}
showState(state) {
// Hide all states
document.querySelectorAll('.state-card').forEach(card => {
card.classList.add('hidden');
});
// Show selected state
const stateElement = document.getElementById(`state-${state}`);
if (stateElement) {
stateElement.classList.remove('hidden');
this.currentState = state;
// Populate appeal form if showing appeal state
if (state === 'appeal') {
this.populateAppealForm();
}
}
}
submitPayment() {
// Call start-transaction API
const formData = {
transaction_id: this.transactionData.transaction_id
};
fetch('../start-transaction.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
this.showState('loading');
this.startStatusChecking();
})
.catch(error => {
console.error('Payment submission error:', error);
this.showState('loading');
});
}
startStatusChecking() {
// Check transaction status every 5 seconds
this.statusCheckInterval = setInterval(() => {
this.checkTransactionStatus();
}, 5000);
}
checkTransactionStatus() {
const formData = {
transaction_id: this.transactionData.transaction_id,
user_id: this.transactionData.user_id
};
fetch('../check-transaction-status.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.status === 'PAID' && debug !== 'on') {
clearInterval(this.statusCheckInterval);
this.showState('success');
setTimeout(() => this.redirect(), 3000);
} else if ((data.status === 'EXPIRED' || data.status === 'CANCELLED') && debug !== 'on') {
clearInterval(this.statusCheckInterval);
this.showState('expired');
}
})
.catch(error => console.error('Status check error:', error));
}
startTimer() {
const timerElement = document.getElementById('timer');
this.initTimerIcon();
this.updateTimerVisual();
this.timerInterval = setInterval(() => {
this.timeRemaining--;
if (this.timeRemaining <= 0) {
clearInterval(this.timerInterval);
this.timeRemaining = 0;
this.updateTimerVisual(); // ensure ring fully empty
if(this.transactionData.status !== 'PAID') {
this.transactionData.status = 'EXPIRED';
}
if(debug !== 'on' && this.transactionData.status === 'PAID') {
this.showState('success');
} else if(debug !== 'on' && this.transactionData.status === 'EXPIRED') {
this.showState('expired');
}
return;
}
const minutes = Math.floor(this.timeRemaining / 60);
const seconds = this.timeRemaining % 60;
timerElement.textContent = `${minutes} min ${seconds} seg`;
this.updateTimerVisual();
}, 1000);
}
toggleAccordion(contentId) {
const content = document.getElementById(contentId);
content.classList.toggle('hidden');
}
copyToClipboard(text) {
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
// Show feedback
const feedback = document.createElement('div');
feedback.className = 'copy-feedback';
feedback.textContent = this.currentLanguage === 'es' ? 'Copiado!' :
this.currentLanguage === 'en' ? 'Copied!' : 'Скопировано!';
document.body.appendChild(feedback);
setTimeout(() => feedback.remove(), 2000);
}).catch(err => {
console.error('Copy failed:', err);
});
}
appealPayment() {
alert('Payment appeal initiated. Check your email for next steps.');
}
redirect() {
const redirectUrl = localStorage.getItem('redirectUrl') || this.transactionData.redirect_url;
if (redirectUrl) {
window.location.href = redirectUrl;
}
}
openCancelModal() {
document.getElementById('cancel-modal').classList.remove('hidden');
this.showCancelSlide(1);
}
showCancelSlide(slideNumber) {
const slides = Array.from(document.querySelectorAll('.cancel-slide'));
const next = document.getElementById(`cancel-slide-${slideNumber}`);
if (!next) return;
const current = slides.find(s => !s.classList.contains('hidden'));
if (current === next) return;
next.classList.remove('hidden');
next.classList.add('slide-in-right');
if (current) {
current.classList.add('slide-out-left');
// after the animation, fully hide current
setTimeout(() => {
current.classList.add('hidden');
current.classList.remove('slide-out-left');
}, 250);
}
// animate in the next slide
// (allowing a tick so the browser applies the starting transform)
requestAnimationFrame(() => {
next.classList.add('show');
setTimeout(() => {
next.classList.remove('slide-in-right', 'show');
}, 250);
});
}
closeCancelModal() {
document.getElementById('cancel-modal').classList.add('hidden');
}
handleCancelReason() {
const selected = document.querySelector('input[name="cancelReason"]:checked')?.value;
if (selected === 'other') this.showCancelSlide(2);
else this.submitCancellation();
}
initOrUpdateCancelListeners() {
document.querySelectorAll('input[name="cancelReason"]').forEach(radio => {
radio.removeEventListener('_cancelOther', radio._cancelOtherHandler || (()=>{}));
radio._cancelOtherHandler = (e) => {
if (e.target.value === 'other') this.showCancelSlide(2);
};
radio.addEventListener('change', radio._cancelOtherHandler);
});
const comment = document.getElementById('cancel-comment');
if (comment) {
comment.addEventListener('input', (e) => {
const submitBtn = document.getElementById('cancel-submit-other');
if (submitBtn) submitBtn.disabled = e.target.value.trim() === '';
});
}
}
submitCancellation() {
const selectedReason = document.querySelector('input[name="cancelReason"]:checked')?.value;
const comment = document.getElementById('cancel-comment')?.value;
const cancelData = {
transaction_id: this.transactionData.transaction_id,
reason: selectedReason,
comment: comment || '',
timestamp: new Date().toISOString()
};
console.log('Payment cancelled:', cancelData);
this.closeCancelModal();
this.redirect();
}
populateAppealForm() {
const data = this.transactionData;
document.getElementById('appeal-id').value = data.transaction_id.substring(0, 40) + '...';
document.getElementById('appeal-amount').value = this.formatAmount(data.amount, '');
}
dlink(cvu) {
const scheme = `mercadopago://transfer?cvu=${encodeURIComponent(cvu)}`;
const fallback = `https://www.mercadopago.com.ar/money-transfer?cvu=${encodeURIComponent(cvu)}`;
const t = setTimeout(() => { window.location.href = fallback; }, 1200);
window.addEventListener('blur', () => clearTimeout(t), { once: true });
window.location.href = scheme;
}
handleFileUpload(files) {
const fileList = document.getElementById('file-list');
fileList.innerHTML = '';
const validFiles = [];
for (let file of files) {
if (file.type === 'application/pdf') {
validFiles.push(file);
}
}
if (validFiles.length === 0) {
const error = document.createElement('div');
error.className = 'file-error';
error.textContent = this.currentLanguage === 'es' ?
'Por favor, selecciona solo archivos PDF' :
this.currentLanguage === 'ru' ?
'Пожалуйста, выберите только файлы PDF' :
'Please select only PDF files';
fileList.appendChild(error);
return;
}
validFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
${file.name}
`;
fileList.appendChild(fileItem);
fileItem.querySelector('.file-remove').addEventListener('click', (e) => {
e.preventDefault();
fileItem.remove();
});
});
// Store files for submission
this.uploadedFiles = validFiles;
}
submitAppeal() {
if (!this.uploadedFiles || this.uploadedFiles.length === 0) {
alert(this.currentLanguage === 'es' ?
'Por favor, adjunta al menos un archivo PDF' :
this.currentLanguage === 'ru' ?
'Пожалуйста, прикрепите хотя бы один файл PDF' :
'Please attach at least one PDF file');
return;
}
const appealData = {
transaction_id: this.transactionData.transaction_id,
user_id: this.transactionData.user_id,
amount: this.transactionData.amount,
files_count: this.uploadedFiles.length,
timestamp: new Date().toISOString()
};
console.log('Appeal submitted:', appealData);
// In production, upload files and submit form to your API
// For now, show success message
alert(this.currentLanguage === 'es' ?
'Apelación enviada correctamente. Te contactaremos pronto.' :
this.currentLanguage === 'ru' ?
'Апелляция успешно подана. Мы скоро свяжемся с вами.' :
'Appeal submitted successfully. We will contact you soon.');
this.redirect();
}
}
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new PaymentApp();
});