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(); });