if (!customElements.get('remex-p2p')) { const _inProd = document.querySelector('meta[name="app-env"]')?.getAttribute('content') != 'dev', cpl = document.querySelector('meta[name="x-cpl"]')?.getAttribute('content') || '1.0.0'; _loadScript = async (scriptPath, componentName) => { return new Promise((resolve, reject) => { if (componentName && customElements.get(componentName)) resolve(true); else { const script = document.createElement('script'); if (componentName) { script.tik = setInterval(() => { if (customElements.get(componentName)) { clearInterval(script.tik); resolve(true); } }, 100); } script.src = `/cdn/${cpl}/${_inProd ? scriptPath.replace('.min.js', '').replace('.js', '.min.js') : scriptPath}`; script.async = true; script.onload = () => { if (script.tik) clearInterval(script.tik); resolve(true); }; script.onerror = () => { if (script.tik) clearInterval(script.tik); reject(false); }; document.head.appendChild(script); } }) }; // _loadScript('microfronts/remex-transition/index.js', 'remex-transition'); _loadScript('microfronts/remex-icon/index.js', 'remex-icon'); _loadScript('microfronts/remex-p2p/routing.js', 'remex-router'); _loadScript('microfronts/remex-select/index.js', 'remex-select'); _loadScript('microfronts/remex-p2p/menu.js', 'remex-p2p-menu'); _loadScript('microfronts/remex-p2p/header.js', 'remex-p2p-header'); _loadScript('microfronts/remex-dialog/index.js', 'remex-dialog'); _loadScript('microfronts/remex-card-loading/index.js', 'remex-card-loading'); _loadScript('microfronts/remex-pagginator/index.js', 'remex-pagginator'); if (!document.querySelector(`script[src="/cdn/${cpl}/js/remex/tooltip${_inProd ? '.min' : ''}.js"]`)) { _loadScript('js/remex/tooltip.js'); } class RemesitaP2P extends HTMLElement { constructor() { super(); //this.attachShadow({ mode: 'open' });//shadowRoot support this.uniqueSuffix = '_' + Math.random().toString(36).substr(2, 9); // Generate unique suffix this.classList.add(this.uniqueSuffix); this.isTelegram = typeof Telegram !== 'undefined' && Telegram.WebApp && Telegram.WebApp.initData; this.defaultStyles = ` :host{color:var(--text-color, #005f75);} h5{color:var(--text-color,#000) !important;} .${this.uniqueSuffix} .auth-form { max-width: 100%; margin: auto; padding: 20px; background: #ffffff0a; border-radius: 4px; box-shadow: none; text-align: center;border: 1px solid var(--input-bg,--auth-input-light-border-color,#127a8b);animation: fadeInAnimation .5s ease-in-out;} .${this.uniqueSuffix} .auth-form .auth-button { background: #18a2b8 !important;color: #ffffff !important; } .${this.uniqueSuffix} .auth-form label { color: #c7dade !important; } .${this.uniqueSuffix} .auth-form .auth-button-cancel{ color: #c7dade !important; } .${this.uniqueSuffix} .auth-form .auth-footer a { color: #afbec1 !important;} .${this.uniqueSuffix} .auth-form .recovery-password-action { color: #afbec1 !important;} .${this.uniqueSuffix} .auth-form .auth-footer a b {font-size: 14px;color: #fff !important;} .${this.uniqueSuffix} .auth-form .action-help { color: #a8aaab !important;} .${this.uniqueSuffix} .auth-form .auth-subtitle { color: #ffffff !important;} .${this.uniqueSuffix} .auth-form .auth-input-icon { color:#072932 !important;} .${this.uniqueSuffix}{ -webkit-transition: all 1s ease-in-out; -moz-transition: all 1s ease-in-out;-o-transition: all 1s ease-in-out;transition: all 1s ease-in-out;transition: all 1s ease-in-out;} .${this.uniqueSuffix} input[name="intentAmount"]::-webkit-inner-spin-button, .${this.uniqueSuffix} input[name="intentAmount"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } .${this.uniqueSuffix} input[name="intentAmount"] {-moz-appearance: textfield;} .${this.uniqueSuffix} .btn-tab,.${this.uniqueSuffix} .btn-tab:hover{background: var(--tab-btn-bg,#fff) !important;color: var(--tab-btn-text,#015f75) !important;} .${this.uniqueSuffix} .btn-tab-active,.${this.uniqueSuffix} .btn-tab-active:hover{background: var(--tab-btn-active-bg,#ffffff1a) !important;color: var(--tab-btn-active-text,#FFF) !important;box-shadow:0px 0px 1px #015f75 !important;} .${this.uniqueSuffix} .announcement-details{display:none;} .${this.uniqueSuffix}.choiced .anouncement, .${this.uniqueSuffix}.choiced .btn-intent{display:none;} .${this.uniqueSuffix}.choiced .intent-nav{display:none;} .${this.uniqueSuffix} .nickname{background: linear-gradient(276.46deg, #03a9f4 1.16%, #00bcd4 51.05%, rgb(182 182 182) 105.69%);-webkit-background-clip: text;-webkit-text-fill-color: transparent;text-fill-color: transparent;font-family: Mija !important;font-size:calc(1rem + 0.4vw)!important} .${this.uniqueSuffix}.choiced .anouncement.choiced{display:block;} .${this.uniqueSuffix}.choiced .announcement-details{display:flex;background: linear-gradient(0deg, rgb(230 249 254 / 5%) 0%, rgb(4 86 107 / 0%) 82%);border-bottom-left-radius: 14px;border-bottom-right-radius: 14px;} .${this.uniqueSuffix} .announcement-terms{max-height: 300px;overflow: auto;border:1px solid rgb(23 86 102 / 86%)} .${this.uniqueSuffix} .announcement-terms::-webkit-scrollbar {width: 2px;} .${this.uniqueSuffix} .announcement-terms::-webkit-scrollbar-track {background:rgba(255, 255, 255, 0.1);border-radius: 2px;} .${this.uniqueSuffix} .announcement-terms::-webkit-scrollbar-thumb {background:rgba(221, 250, 255, 0.86);border-radius: 1px;} .${this.uniqueSuffix} .announcement-terms::-webkit-scrollbar-thumb:hover {background: #ffffff00;} .${this.uniqueSuffix} .announcement-terms::-webkit-scrollbar-track-piece {background: #f9f9f900;} .${this.uniqueSuffix} .verified-item{ color: var(--text-primary-color,#3dbeda) !important;} .${this.uniqueSuffix} .verified-item:after{content: '✔';color:var(--text-primary-color,#3dbeda);margin-left: 2px;margin-right: 2px;} .${this.uniqueSuffix} .nav-link{color: var(--nav-text,white);text-transform: uppercase;font-family:Mija !important;font-weight: bold;font-size:calc(0.8rem + 0.6vw)!important} .${this.uniqueSuffix} .nav-link.active{color:var(--text-primary-color,#3dbeda);border-bottom: 1px solid var(--text-primary-color,#3dbeda);} .${this.uniqueSuffix} #deal-unseen{display:none;color:white !important;animation: borderCircleInfinitedAnimated 1s infinite;background:var(--text-primary-color,#3dbeda); border-radius: 1rem; min-width: 12px; min-height: 12px; margin-left: 2px; font-size: 0.5rem !important; padding: 0.2rem; position: relative; top: -4px;} @keyframes borderCircleInfinitedAnimated {0% {box-shadow: 0 0 0 0 rgba(61, 190, 218, 0.7);}100% {box-shadow: 0 0 0 10px rgba(61, 190, 218, 0);}} .bg-dark{ background-color: var(--bg-primary , #005f75 ) !important; } .btn.btn-dark{ background-color: var(--bg-primary , #005f75 ) !important; } .show-sm{display:none;} @media(min-width:992px){.${this.uniqueSuffix} .hidden-lg{display:none;} .bk-lg{display:block;} } @media(max-width:992px){.${this.uniqueSuffix} .show-sm{display:block;} .hidden-sm{display:none;}.${this.uniqueSuffix} .onlinestatus{margin-top:-16px}.${this.uniqueSuffix}.choiced ${this.uniqueSuffix} .badge-sm{background: #2f7e8f; border-radius: 5px; padding: 2px 4px; margin-right: 4px;display: flex;align-items: center;justify-content: center;}} @media(max-width:768px){.${this.uniqueSuffix} .hidden-xs{display:none;}.${this.uniqueSuffix} .onlinestatus{margin-top:-16px}.${this.uniqueSuffix}.choiced .${this.uniqueSuffix}.choiced .announcement-details{display:flex;background:transparent;}} .btn-navigation-back){ animation: fadeIn .5s ease-in-out; transition: all 0.5s ease-in-out; } #${this.uniqueSuffix}anouncementsContainer{color:var(--text-color, #005f75);} `; this.loadingHtml = ``; this.overrideStyles = null; this.data = {}; this.wallet = null; this.lang = document.querySelector('html')?.getAttribute('lang') ?? 'es'; switch (this.lang) { case 'es': this.i18n = { 'You already have an ad to sell SRM created, you must delete it to be able to create another one.': 'Ya tienes un anuncio para vender SRM creado, debes eliminarlo para poder crear otro.', 'You already have an ad to buy SRM created, you must delete it to be able to create another one.': 'Ya tienes un anuncio para comprar SRM creado, debes eliminarlo para poder crear otro.', 'The announcement has been withdrawn': 'El anuncio ha sido retirado', 'Payment method not found': 'Método de pago no encontrado', 'The owner of announcement does not accept this payment method.': 'El anunciante no acepta este método de pago.', 'The advertiser has already committed their balance': 'El anunciante ya ha comprometido su saldo', 'Your balance is not enough for this deal.': 'Tu saldo no es suficiente para este trato.', 'Seller balance is not enough for this deal.': 'El saldo del vendedor no es suficiente para este trato.', 'P2P seles is disabled for seller customer level.': 'Las ventas de saldo estan desabilitdas en el nivel de cliente del vendedor', 'The amount exceeds the maximum allowed in the seller customer level.': 'El monto excede el máximo permitido en el nivel de cliente del vendedor', 'The seller has exceeded the maximum number of allowed daily operations.': 'El vendedor ha superado el número máximo de operaciones diarias permitidas.', 'You can not buy your own announcement.': 'No eres el anunciante de este anuncio', 'The advertiser has already committed their balance.': 'El anunciante ya ha comprometido su saldo', 'The owner of announcement does not accept this payment method.': 'El anunciante no acepta este método de pago', 'Payment method not found.': 'Forma de pago no encontrado', 'The announcement has been withdrawn.': 'El anuncio ha sido retirado', 'Announcement not found.': 'Anuncio no encontrado', 'Your balance is not enough for this deal.': 'Tu saldo no es suficiente para este trato.', 'Seller balance is not enough for this deal.': 'El saldo del vendedor no es suficiente para este trato.', 'This deal had already been cancelled.': 'Este trato ya había sido cancelado.', 'This deal had already been accepted.': 'Este trato ya había sido aceptado.', 'This deal had already been completed.': 'Este trat ya había sido completado.', 'Deal not found.': 'Trato no encontrado.', "Let us avoid sharing contact information and using words that are prohibited in the terms of the advertisement.": "Evitemos compartir información de contacto y usar palabras prohibidas en los términos del anuncio.", 'You must define the terms of the announcement, so we can show your counterparty what your conditions are for creating a deal.': 'Debes definir los términos del anuncio, para que podamos mostrarle a tu contraparte cuáles son tus condiciones para crear un trato.', }; break; case 'en': this.i18n = { 'No hay tratos para mostrar.': 'No deals to show.', 'No tienes anuncios que administrar.': 'You have no ads to manage.', 'Mis Tratos': 'My Deals', 'Mis anuncios P2P': 'My P2P Announcement', 'Publica un anuncio': 'Post an Announcement', 'Anuncio creado correctamente': 'Announcement created successfully', 'El Anuncio ha sido actualizado': 'The Announcement has been updated', 'El Anuncio ha sido eliminado': 'The Announcement has been deleted', 'Ocurrió un error cargando los detalles del anuncio, por eso te redireccionamos nuevamente a la lista de anuncios.': 'An error occurred loading the announcement details, so we redirect you back to the list of announcements.', 'No se encontró el anuncio solicitado': 'The requested announcement was not found', 'Todos los pagos (*)': 'All payment methods (*)', 'Anuncios': 'Announcements', 'Entendido': 'Understood', 'Para acceder a esta sección debes estar autenticado': 'To access this section you must be authenticated', 'Serás redireccionado a la pantalla de autenticación': 'You will be redirected to the authentication screen', 'Anunciante': 'Advertiser', 'Indicadores': 'Indicators', 'Cantidad': 'Amount', 'Precio': 'Price', 'Método de pago': 'Payment method', 'Estado': 'Status', 'Fecha': 'Date', 'Acciones': 'Actions', 'Ver detalles': 'View details', 'Aceptar': 'Accept', 'Cancelar': 'Cancel', 'Cerrar': 'Close', 'Publicar': 'Post', 'Editar': 'Edit', 'Eliminar': 'Delete', 'Comprar': 'Buy', 'Vender': 'Sell', 'Reputación': 'Reputation', 'Tratos': 'Deals', 'Menú': 'Menu', 'Formas de pago': 'Payment methods', 'Límites': 'Limits', 'Ocurrió un error al crear el trato, inténtelo luego': 'An error occurred creating the deal, please try again later', 'Debes iniciar sesión para poder crear un trato': 'You must log in to create a deal', }; break; default: this.i18n = {}; } this.onInitialized = () => void 0; const filterStored = JSON.parse(window.localStorage.getItem('remex-p2p-filter') ?? null); this._filterState = filterStored ?? { intent: 'buy', paymentMethod: '*', amount: null, pgSize: 50, pg: 1 }; const self = this; this.filterState = new Proxy(this._filterState, { set(target, property, value) { if (target[property] !== value) { target[property] = value; const event = new CustomEvent('filterStateChanged', { detail: { property, value }, bubbles: true, composed: true }); window.localStorage.setItem('remex-p2p-filter', JSON.stringify(self._filterState)); self.dispatchEvent(event); } return true; } }); // Inicializa el temporizador de debounce this.debounceTimer = null; this.initialized = false; this.observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes') { this.data[mutation.attributeName] = this.getAttribute(mutation.attributeName) || ''; this.refresh(); } }); }); this.router = new Router(); this.setupRouterListeners(); this.addEventListener('forceauth', async (e) => { return this.redirectToAuth(); }); this.addEventListener('showpeer', (e) => this.router.navigate('/p2p/profile/' + e.detail)); this.addEventListener('force-navigate', (e) => this.router.navigate(e.detail)); this.addEventListener('profile', (e) => { this.router.navigate(`/p2p/profile/me`); }); this.addEventListener('wallet-refresh', (e) => { this.wallet = e.detail; }); //si se actualiza el trato actualizar balance de wallet this.addEventListener('deal-confirmed', async (e) => { if (this.querySelector('remex-p2p-header')) { await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.addEventListener('deal-canceled', async (e) => { if (this.querySelector('remex-p2p-header')) { await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.addEventListener('deal-accepted', async (e) => { if (this.querySelector('remex-p2p-header')) { await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.addEventListener('RemexAuthLogout', async (e) => { if (this.querySelector('remex-p2p-header')) { this.querySelector('remex-p2p-header').headers = await this.buildRequestHeaders(); await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.addEventListener('RemexAuthLogged', async (e) => { if (this.querySelector('remex-p2p-header')) { this.querySelector('remex-p2p-header').headers = await this.buildRequestHeaders(); await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.addEventListener('sessionCreated', async (e) => { console.log('Sesión creada', e.detail); if (this.querySelector('remex-p2p-header')) { this.querySelector('remex-p2p-header').headers = await this.buildRequestHeaders(); await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); } }); this.realtimeSuscriberInit(); } static get observedAttributes() { return ['intent', 'payment-method', 'amount', 'currency', 'theme']; } attributeChangedCallback(name, oldValue, newValue) { if (name === 'theme') { this.applyTheme(newValue); } if (name === 'intent' && ['buy', 'sell'].includes(newValue)) { this.filterState.intent = newValue; } if (name === 'payment-method') { this.filterState.paymentMethod = newValue; } if (name === 'amount') { this.filterState.amount = parseFloat(newValue); } if (name === 'currency') { this.currency = newValue; } if (this.initialized) { this.handleFilterStateChanged(); } } applyTheme(themeName) { const themes = { dark: { 'background-color': '#1a1a1a', 'color': '#ffffff' }, light: { 'background-color': '#ffffff', 'color': '#000000' } }; this.overrideStyles = themes[themeName] || null; this.refresh(); } realtimeSuscriberInit() { if (!window.auth?.user?.id ?? false) return false; (new Promise((resolve, reject) => { if (undefined !== window.DeepstreamClient) resolve(true); else { const script = document.createElement('script'); script.tik = setInterval(() => { if (undefined !== window.DeepstreamClient) { clearInterval(script.tik); resolve(true); } }, 100); script.src = `/assets/js/ds.min.js`; script.async = true; script.onload = () => { if (script.tik) clearInterval(script.tik); resolve(true); }; script.onerror = () => { if (script.tik) clearInterval(script.tik); reject(false); }; document.head.appendChild(script); } })).then(() => { if (undefined === window.realtime) { const { DeepstreamClient } = window.DeepstreamClient; window.realtime = { client: new DeepstreamClient('dev' == document.querySelector('meta[name="app-env"]')?.getAttribute('content') ? `wss://dev.remesita.com/deepstream` : `wss://remesita.com/deepstream`), login: () => new Promise((resolve, reject) => { if (window.realtime.client.getConnectionState() == 'OPEN') resolve(true); else window.realtime.client.login({ uid: window.auth.user.id }, (isoLoged) => isoLoged ? resolve(true) : reject(false)); }) }; } window.realtime.login().then(() => { console.log('realtime login from layout'); if (!this._realTime) { const key = `p2p/${window.auth.user.id}`; this._realTime = window.realtime.client.record.getRecord(key); this._realTime.subscribe('deal', (deal) => { }); } }); }).catch(() => { this.chat.startPullinghMessages(); }); } async checkUnseenDeals() { try { if (this.unseenChecking) return false; this.unseenChecking = true; const response = await fetch(`/rest/v2/p2p/deal/unseen`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.ok) { const data = await response.json(); this.unseenChecking = false; if (data.unseen > 0) { this.querySelector('#deal-unseen').innerHTML = data.unseen; this.querySelector('#deal-unseen').style.display = 'inline-block'; return true; } } } catch (e) { } this.unseenChecking = false; this.querySelector('#deal-unseen').style.display = 'none'; return false; } setupRouterListeners() { this.addEventListener('route-activated', async (e) => { setTimeout(async () => { if (await this.isConnected()) { this.querySelector('remex-p2p-header').headers = await this.buildRequestHeaders(); await this.checkUnseenDeals(); } }, 100); const route = e.detail.route; if (this.currentRoute == route && JSON.stringify(e.detail.params ?? {}) == JSON.stringify(this.currentRouteParams ?? {})) return; this.currentRoute = route; this.currentRouteParams = e.detail.params ?? null; if (!this.initialized) await this.renderLayout(); this.updateNavState(route); if (this.querySelector('.btn-navigation-back')) this.querySelector('.btn-navigation-back').classList.remove('hidden-lg'); if (this.isTelegram) { Telegram.WebApp.SettingsButton.onClick(() => { window.navigation.back(); }); Telegram.WebApp.BackButton.show(); Telegram.WebApp.SettingsButton.onClick(() => { this.router.navigate('/p2p/menu'); }); Telegram.WebApp.SettingsButton.show(); Telegram.WebApp.MainButton.hide(); } switch (route) { case '/p2p/announcements': if (this.querySelector('.btn-navigation-back')) this.querySelector('.btn-navigation-back').classList.add('hidden-lg'); if (this.isTelegram) { Telegram.WebApp.BackButton.hide(); } this.anouncementPg = 1; return await this.renderAnnouncementLists(); case '/p2p/my-announcements': this.anouncementPg = 1; return await this.renderMyAnnouncementLists(); case '/p2p/announcement/:id': return await this.renderAcnnouncementDetails(e.detail.params.id); case '/p2p/profile/:id': return await this.renderProfile(e.detail.params.id != 'me' ? e.detail.params.id : null); case '/p2p/announcements/new': return await this.renderAnnouncementForm(); case '/p2p/announcement/edit/:id': return await this.renderAnnouncementForm(e.detail.params.id); case '/p2p/deals': return await this.renderMyDealsList(); break; case '/p2p/deal/:id': return await this.renderDealDetails(e.detail.params.id); case '/p2p/menu': break; case '/p2p/payment-methods': return await this.renderPaymentMethodsSettings(); case '/p2p/auth': return await this.renderAuthForm(); default: break; } }); } async renderAnnouncementForm(id) { if (!await this.isConnected()) return this.redirectToAuth(); const container = this.container.querySelector(id ? `remex-router[route="/p2p/announcement/edit/:id"]` : `remex-router[route="/p2p/announcements/new"]`); container.innerHTML = `

${this.loadingHtml}`; await this.loadScript(`microfronts/remex-p2p/announcement-form.js`, 'remex-announcement-form'); container.innerHTML = ''; const cmp = document.createElement('remex-announcement-form'); cmp.i18n = this.i18n; cmp.headers = await this.buildRequestHeaders(); const announcement = id ? await this.fetchAnnouncementById(id) : null; if (announcement) cmp.announcement = announcement; container.appendChild(cmp); cmp.addEventListener('created', (e) => { console.log('Anuncio creado', e.detail); this.showMessage(this.trans('Anuncio creado correctamente')); this.router.navigate('/p2p/my-announcements'); }); cmp.addEventListener('updated', (e) => { this.showMessage(this.trans('El Anuncio ha sido actualizado')); this.router.navigate('/p2p/my-announcements'); }); cmp.addEventListener('removed', (e) => { this.showMessage(this.trans('El Anuncio ha sido eliminado')); this.router.navigate('/p2p/announcements'); }); } async renderPaymentMethodsSettings() { if (!await this.isConnected()) return this.redirectToAuth(); const container = this.container.querySelector(`remex-router[route="/p2p/payment-methods"]`); container.innerHTML = `

${this.loadingHtml}`; await this.loadScript(`microfronts/remex-p2p/payment-methods-settings.js`, 'remex-payment-methods-settings'); container.innerHTML = ''; const cmp = document.createElement('remex-payment-methods-settings'); cmp.i18n = this.i18n; cmp.headers = await this.buildRequestHeaders(); container.appendChild(cmp); } async renderDealDetails(id) { const container = this.container.querySelector(`remex-router[route="/p2p/deal/:id"]`); container.innerHTML = `

${this.loadingHtml}`; let deal = await this.fetchDeal(id); if (deal) { if (deal.forceAuth) return await this.redirectToAuth(); await this.loadScript(`microfronts/remex-p2p/deal-details.js`, 'remex-deal-details'); container.innerHTML = ``; const cmp = container.querySelector('remex-deal-details'); cmp.headers = await this.buildRequestHeaders(); cmp.data = deal; } else { this.showError('No se encontró el trato solicitado'); this.router.navigate('/p2p/deals'); } } async redirectToAuth() { this._redirectData = { route: this.currentRoute, params: this.currentRouteParams }; this.router.navigate('/p2p/auth'); } async renderAuthForm() { this.container.querySelector('form').setAttribute('action', window.location.href); window.auth.authFormCssSelector = `#${this.uniqueSuffix}p2p-auth-view .p2p-auth-container`; if (window.auth?.remexAuth?.rendered) window.auth.remexAuth.rendered = false; await window.auth.renderAuthForm(() => { if (this._redirectData) { let url = this._redirectData.route, params = this._redirectData.params ?? {}; if (this._redirectData.params) Object.keys(params).forEach(k => url = url.replace(`:${k}`, params[k])); this.router.navigate(url); setTimeout(async () => { this.querySelector('remex-p2p-header').headers = await this.buildRequestHeaders(); await this.querySelector('remex-p2p-header').refresh(); this.querySelector('remex-p2p-header').updateWalletInfo(); }, 10); } }); return true; } async isConnected() { if (window.auth?.isConnected()) return true; await window.auth.checkUserIsLogged(); const p = new Promise((resolve, reject) => { if (!window.auth.remexAuth) { const tik = setInterval(() => { if (window.auth.remexAuth) { clearInterval(tik); resolve(window.auth.remexAuth.isConnected() && window.auth?.user?.token); } }, 100) } else setTimeout(() => resolve(window.auth.isConnected()), 500); }); return await p; } async renderProfile(id) { if ((!id || 'me' == id) && !await this.isConnected()) return this.redirectToAuth(); const routeEl = this.querySelector(`remex-router[route="/p2p/profile/:id"]`); routeEl.innerHTML = `

${this.loadingHtml}`; try { const response = await fetch(`/rest/v2/p2p/profile/${id ?? window.auth?.user?.id}`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.ok) { const data = await response.json(); await this.loadScript(`microfronts/remex-p2p/profile.js`, 'remex-p2p-profile'); const profileCmp = document.createElement('remex-p2p-profile'); profileCmp.i18n = this.i18n; profileCmp.self = !id; profileCmp.headers = await this.buildRequestHeaders(); routeEl.innerHTML = ''; routeEl.appendChild(profileCmp); profileCmp.data = data; } } catch (error) { console.error('Error fetching announcement', error); } } updateNavState(route) { // Actualizar clases activas en la navegación const compareRoute = /announcement/.test(route) ? '/p2p/announcements' : (/deal/.test(route) ? '/p2p/deals' : route); const navItems = this.container.querySelectorAll('a.nav-link'); navItems.forEach(item => { item.classList.remove('active'); if (compareRoute == item.getAttribute('href')) { item.classList.add('active'); } }); } async showError(message) { await this.loadScript(`microfronts/remex-message/index.js`, 'remex-message'); const e = document.createElement('remex-message'); e.message = this.trans(message); e.duration = 3000; e.position = "bottom-left"; e.color = "danger"; document.body.appendChild(e); return e } async showMessage(message) { await this.loadScript(`microfronts/remex-message/index.js`, 'remex-message'); const e = document.createElement('remex-message'); e.message = this.trans(message); e.duration = 3000; e.position = "bottom-left"; e.color = "info"; document.body.appendChild(e); return e } async renderAcnnouncementDetails(id) { const detailContainer = this.container.querySelector(`remex-router[route="${this.currentRoute}"]`); detailContainer.innerHTML = `

${this.loadingHtml}`; let announcement = await this.fetchAnnouncementById(id); if (announcement) { if (announcement.isSelf) return this.router.navigate('/p2p/announcement/edit/' + id); await this.loadScript(`microfronts/remex-p2p/announcement-details.js`, 'remex-announcement-details'); detailContainer.innerHTML = ``; const detailComponent = detailContainer.querySelector('remex-announcement-details'); if (detailComponent.getAttribute('evlistened') != 'yes') { detailComponent.addEventListener('closed', (e) => { this.router.navigate('/p2p/announcements'); if (this.isTelegram) { Telegram.WebApp.MainButton.hide(); } }); detailComponent.addEventListener('accept', async (e) => { e.preventDefault(); if (this._creatingDeal) return; console.log(e.detail); if (!await this.isConnected()) return this.redirectToAuth(); this._creatingDeal = true; const deal = await this.createDeal(id, e.detail.paymentMethod, e.detail.amount, e.detail.rate ?? null); this._creatingDeal = false; if (deal.forceAuth) { return await this.redirectToAuth(); } else if (deal.error) { detailComponent.stopLoading(); await this.showError(deal.error); } else { this.deals = this.deals || []; this.deals.push(deal); this.router.navigate('/p2p/deal/' + deal.id); } }); detailComponent.addEventListener('error', (e) => { detailContainer.innerHTML = `

${this.loadingHtml}`; this.loadScript(`microfronts/remex-message/index.js`, 'remex-message').then(() => { const message = document.createElement('remex-message'); message.message = this.trans('Ocurrió un error cargando los detalles del anuncio, por eso te redireccionamos nuevamente a la lista de anuncios.'); message.duration = 3000; message.position = "bottom-left"; message.color = "danger"; document.body.appendChild(message); this.router.navigate('/p2p/announcements'); if (this.isTelegram) { Telegram.WebApp.MainButton.hide(); } }); }); detailComponent.setAttribute('evlistened', 'yes'); } //detailContainer.appendChild(detailComponent); detailComponent.headers = await this.buildRequestHeaders(); detailComponent.data = announcement; } else { this.showError('No se encontró el anuncio solicitado'); this.router.navigate('/p2p/announcements'); } } async renderMyDealsList() { if (!await this.isConnected()) return this.redirectToAuth(); const listContainer = this.container.querySelector(`remex-router[route="/p2p/deals"]`); listContainer.innerHTML = `

${this.loadingHtml}`; await this.loadScript(`microfronts/remex-p2p/deal-item.js`, 'remex-deal-item'); const header = `
${this.trans('Mis Tratos')}
`; listContainer.innerHTML = this.loadingHtml; listContainer.innerHTML = `${header}${this.loadingHtml}`; const deals = await this.fetchMyDeal(this.dealPg ?? 1, this.dealPgSize ?? 10); if (deals.items.length == 0) { listContainer.innerHTML = `${header}

${this.trans('No hay tratos para mostrar.')}

`; } else { listContainer.innerHTML = header; deals.items.forEach((item) => { const announcementItem = document.createElement('remex-deal-item'); announcementItem.setAttribute('unique-suffix', this.uniqueSuffix); announcementItem.i18n = this.i18n ?? {}; announcementItem.data = item; listContainer.appendChild(announcementItem); announcementItem.addEventListener('selected', (e) => { this.router.navigate('/p2p/deal/' + e.detail.id); }); }); } if (deals.total > 10) { const pagginatorContainer = document.createElement('div'); pagginatorContainer.innerHTML = `` listContainer.appendChild(pagginatorContainer); pagginatorContainer.querySelector('remex-pagginator').addEventListener('pagginate', async (e) => { this.dealPg = e.detail.page; await this.renderMyDealsList(); }); } } async renderMyAnnouncementLists() { if (!await this.isConnected()) return this.redirectToAuth(); const listContainer = this.container.querySelector(`remex-router[route="/p2p/my-announcements"]`); listContainer.innerHTML = `

${this.loadingHtml}`; await this.loadScript(`microfronts/remex-p2p/announcement-item.js`, 'remex-announcement-item'); listContainer.innerHTML = this.loadingHtml; const h5 = `
${this.trans('Mis anuncios P2P')}
`; listContainer.innerHTML = `${this.loadingHtml}`; const myAnnouncements = await this.fetchMyAnnouncements(); listContainer.innerHTML = `
${h5}
`; if (myAnnouncements.items.length == 0) { listContainer.innerHTML = `

${this.trans(myAnnouncements.total ? 'No hay anuncios en esta página.' : 'No tienes anuncios que administrar.')}

`; } else { myAnnouncements.items.forEach(async(item) => { const announcementItem = document.createElement('remex-announcement-item'); announcementItem.setAttribute('unique-suffix', this.uniqueSuffix); announcementItem.i18n = this.i18n ?? {}; announcementItem.data = item; announcementItem.headers =await this.buildRequestHeaders(); announcementItem.viewmode = 'self'; listContainer.appendChild(announcementItem); announcementItem.addEventListener('selected', (e) => { this.router.navigate('/p2p/announcement/edit/' + e.detail.id); }); announcementItem.addEventListener('showpeer', (e) => { this.router.navigate('/p2p/profile/' + e.detail); }); }); } if (myAnnouncements.total > 25) { const pagginatorContainer = document.createElement('div'); pagginatorContainer.innerHTML = `` listContainer.appendChild(pagginatorContainer); pagginatorContainer.querySelector('remex-pagginator').addEventListener('pagginate', async (e) => { this.anouncementPg = e.detail.page; await this.renderMyAnnouncementLists(); }); } } async renderAnnouncementLists() { this._filterState.pg = 1; this.tabBuy = this.container.querySelector('.btn-tab-buy'); this.tabSell = this.container.querySelector('.btn-tab-sell'); this.anouncementContainer = this.container.querySelector(`#${this.uniqueSuffix}anouncementsContainer`); this.anouncementContainer.innerHTML = `

${this.loadingHtml}`; [this.tabBuy, this.tabSell].filter(tab => null != tab).forEach((tab) => { tab.addEventListener('click', (e) => { this._filterState.pg = 1; const intent = e.target.classList.contains('btn-tab-buy') ? 'buy' : 'sell'; this.tabBuy.classList.toggle('btn-tab-active', intent == 'buy'); this.tabBuy.classList.toggle('btn-tab', intent != 'buy'); this.tabSell.classList.toggle('btn-tab-active', intent == 'sell'); this.tabSell.classList.toggle('btn-tab', intent != 'sell'); this.filterState.intent = intent; this.dispatchEvent(new CustomEvent('intent', { detail: intent })); }); }); this.methodIntent = this.container.querySelector('remex-select[name="intentPaymentMethod"]'); this.methodIntent.addEventListener('change', (e) => { this.filterState.paymentMethod = e.target.value; this._filterState.pg = 1; this.dispatchEvent(new CustomEvent('intentPaymentMethod', { detail: this.intentPaymentMethod })); }); this.amountIntent = this.container.querySelector('input[name="intentAmount"]'); this.amountIntent.addEventListener('input', (e) => { this._filterState.pg = 1; this.filterState.amount = e.target.value; this.dispatchEvent(new CustomEvent('intentAmount', { detail: e.target.value })); }); if (!this._paymentMethods) setTimeout(async () => { const options = [{ id: '*', name: this.trans('Todos los pagos (*)'), isActive: true }]; const items = await this.fetchPaymentMethods(); options.push(...items); this._paymentMethods = options.map((item) => ({ value: item.id, label: item.name.toUpperCase(), group: item.groupType, img: item.logo ? `https://remesita.s3.amazonaws.com/uploads/logos/${item.logo}` : null, disabled: !item.isActive })) this.methodIntent.items = this._paymentMethods; this.methodIntent.value = this._filterState.paymentMethod ?? '*'; this.handleFilterStateChanged(); }, 100); else { this.methodIntent.items = this._paymentMethods; this.methodIntent.value = this._filterState.paymentMethod ?? '*'; this.handleFilterStateChanged(); } } async loadScript(url, componentName) { return _loadScript(url, componentName); } async init() { this.cpl = document.querySelector('meta[name="x-cpl"]')?.getAttribute('content') ?? this.cookies?.fetch('x-cpl') ?? '1.0.0' this.initialized = true; this.overrideStyles = false; await window.auth.checkUserIsLogged(); this.dispatchEvent(new CustomEvent('initialized')); this.onInitialized(); setTimeout(async () => { if (this.isTelegram) { const initData = Telegram.WebApp.initData; const params = new URLSearchParams(initData); // Obtén el valor de startapp const startAppParam = params.get('start_param'); if (startAppParam) { this.router.navigate('/p2p/' + startAppParam.replace(/__/g, '/')); } } }, 1); } connectedCallback() { this.observer.observe(this, { attributes: true }); this.addEventListener('filterStateChanged', this.handleFilterStateChanged.bind(this)); this.refresh(); } disconnectedCallback() { this.observer.disconnect(); this.removeEventListener('filterStateChanged', this.handleFilterStateChanged.bind(this)); } attributeChangedCallback(name, oldValue, newValue) { if ('undefined' == typeof this.data[name] && oldValue != newValue) { this.data[name] = newValue; this.refresh(); } } trans(key, params) { let v = (this.i18n[key] !== undefined) ? (this.i18n[key] ?? key).trim() : key.trim(); if (params !== undefined) for (var key in params) v = v.replace(key, params[key]).trim(); return v.trim(); } createApiUrl(path) { return `https://${document.querySelector('meta[name="app-env"]') && document.querySelector('meta[name="app-env"]').getAttribute('content') == 'dev' ? 'dev-api' : 'api'}.remesita.com/rest/v1/${path}` } refresh() { if (this.overrideStyles) this.initialized = false; setTimeout(async () => await this.renderLayout(), 1); } async renderLayout() { if (!this.initialized) { let styles = ''; if (this.overrideStyles) { for (let i in this.overrideStyles) { let css = []; for (let j in this.overrideStyles[i]) { css.push(`${j}:${this.overrideStyles[i][j]};`); } styles += `.${this.uniqueSuffix} ${i} {${css.join('')}}`; } } //this.shadowRoot.innerHTML = ` this.innerHTML = ` `; this.container = document.createElement('div'); if (this.isTelegram) this.container.style.cssText = 'min-height: 100vh; display: flex ; align-content: flex-start;'; //this.shadowRoot.appendChild(this.container); this.appendChild(this.container); [this.uniqueSuffix, 'col-12', 'px-0', 'py-2', 'row', 'mx-0'].forEach(c => this.container.classList.add(c)); this.intent = 'buy'; } this.container.innerHTML = `
${'undefined' !== typeof Telegram && Telegram.WebApp ? '' : ``}
$RM


${this.loadingHtml}
${this.trans('Inicia sesión para operar en P2P')}

${this.trans('Para acceder a esta sección debes estar autenticado')}!

${this.trans('Serás redireccionado a la pantalla de autenticación')}

`; this.container.querySelectorAll('a.nav-link').forEach((l) => l.addEventListener('click', (e) => { e.preventDefault(); this.router.navigate(e.currentTarget.getAttribute('href')); })); this.container.querySelector('remex-p2p-menu').addEventListener('choiced', (e) => { e.preventDefault(); this.router.navigate(e.detail); }); await window.auth.checkUserIsLogged(); await this.init(); return true; } handleFilterStateChanged() { // Implementar debounce para optimizar las solicitudes if (this.debounceTimer) { clearTimeout(this.debounceTimer); } this.debounceTimer = setTimeout(async () => { this.anouncementContainer.innerHTML = `

${this.loadingHtml}`; const data = await this.fetchAnnouncements(); this.anouncementContainer.innerHTML = `
${this.loadingHtml}
` await _loadScript('microfronts/remex-p2p/announcement-item.js', 'remex-announcement-item'); const listContainer = this.anouncementContainer.querySelector(`#${this.uniqueSuffix}announcementsList`); if (listContainer) { listContainer.innerHTML = ''; if (data.items.length) { data.items.forEach(async (item) => { const announcementItem = document.createElement('remex-announcement-item'); announcementItem.setAttribute('unique-suffix', this.uniqueSuffix); announcementItem.i18n = this.i18n ?? {}; announcementItem.data = item; announcementItem.intentPaymentMethod = this._filterState.paymentMethod; announcementItem.intentAmount = this._filterState.amount; announcementItem.headers =await this.buildRequestHeaders(); listContainer.appendChild(announcementItem); // Escuchar el evento de selección de anuncio announcementItem.addEventListener('selected', (e) => { // this.showAnnouncementDetails(e.detail.announcement); if (e.detail.isSelf) this.router.navigate('/p2p/announcement/edit/' + e.detail.id); else this.router.navigate('/p2p/announcement/' + e.detail.id); }); announcementItem.addEventListener('showpeer', (e) => { this.router.navigate('/p2p/profile/' + e.detail); }); }); } else { listContainer.innerHTML = `

${this.trans(data.total ? 'No hay anuncios para mostrar en esta página.' : (this._filterState.intent == 'buy' ? 'Nadie esta vendiendo SRM bajo con este criterio.' : 'Nadie esta comprando SRM con este criterio.'))}

`; } if (data.total > 25) { const pagginatorContainer = document.createElement('div'); pagginatorContainer.classList.add('text-center', 'col-12', 'm-auto', 'py-2'); pagginatorContainer.innerHTML = `` listContainer.appendChild(pagginatorContainer); pagginatorContainer.querySelector('remex-pagginator').addEventListener('pagginate', (e) => { this._filterState.pg = e.detail.page; this.handleFilterStateChanged(); }); } } this.container.querySelectorAll('.btn-intent').forEach((btn) => btn.addEventListener('click', (e) => { const id = e.currentTarget.getAttribute('aid'); const announcement = data.items.filter(i => i.id == id)[0]; this.container.classList.add('choiced'); this.container.querySelectorAll('.anouncement').forEach((a) => a.classList.remove('choiced')); this.container.querySelector(`.anouncement[aid="${id}"]`).classList.add('choiced'); this.renderQuotationInput(announcement); this.dispatchEvent(new CustomEvent('expandAnouncement', { detail: announcement })); })); }, 100); // Ajusta el tiempo de debounce según tus necesidades } async buildRequestHeaders() { const generateHmacSignature = async (data, secretKey) => { // Convertir la clave secreta y los datos a un ArrayBuffer const encoder = new TextEncoder(); const keyBuffer = encoder.encode(secretKey); const dataBuffer = encoder.encode(data); // Importar la clave para usarla con HMAC const cryptoKey = await crypto.subtle.importKey( 'raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); // Generar la HMAC const signatureBuffer = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer); // Convertir el resultado a una cadena hexadecimal return Array.from(new Uint8Array(signatureBuffer)) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); }; if (!this.fingerprint) { if (!window.__loadingFp) { window.__loadingFp = true; await this.loadScript(`js/remex/fp.min.js`); } else { const p = new Promise((resolve) => { const t = setInterval(() => { if ('undefined' != typeof window.FingerprintHandler) { clearInterval(t); resolve(); } }, 100); }); await p; } try { this.fingerprint = await window.FingerprintHandler.generateFingerprint(); } catch (e) { } } const signature = this.fingerprint ? await generateHmacSignature(this.fingerprint, this.cpl) : null; const headers = { 'Content-Type': 'application/json', 'v-compilation': this.cpl, 'x-cpl': this.cpl, 'x-fingerprint': this.fingerprint, 'x-signature': signature, 'x-tlg': this.isTelegram ? Telegram.WebApp?.user?.id ?? Telegram.WebApp?.initDataUnsafe?.user?.id?? null: null, 'x-tlg-bot': this.isTelegram ? 'p2p' : null, }; if (await this.isConnected() && window.auth?.user?.token) { headers['token'] = window.auth.user.token; } return headers; } async fetchAnnouncementById(id) { const announcement = (this.announcements ?? []).filter(i => i.id == id)[0] ?? false; if (announcement) return announcement; try { const response = await fetch(`/rest/v2/p2p/announcement/${id}`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.status === 200) { return await response.json(); } } catch (error) { console.error('Error fetching announcement', error); return null; } } async createDeal(announcementId, paymentMethod, amount, rate) { let error = this.trans('Ocurrió un error al crear el trato, inténtelo luego'), forceAuth = false; try { const response = await fetch(`/rest/v2/p2p/deal`, { method: 'POST', headers: await this.buildRequestHeaders(), body: JSON.stringify({ announcementId, paymentMethod, amount, rate }) }); if (response.status === 200 || response.status === 201) { return await response.json(); } if (response.status === 401) { forceAuth = true; error = this.trans('Debes iniciar sesión para poder crear un trato'); } else { const res = await response.json(); error = this.trans(res.error ?? 'Ocurrió un error al crear el trato, inténtelo luego'); } } catch (error) { console.error('Error creating deal', error); } return { error, forceAuth }; } async fetchPaymentMethods() { try { const response = await fetch(`/rest/v2/p2p/payment-method/list`, { method: 'GET', headers: await this.buildRequestHeaders(), useCache: true }); if (response.status === 200) { return await response.json(); } } catch (error) { console.error('Error fetching payment methods', error); return []; } /* return new Promise((resolve, reject) => { const list = [ { id: 1, name: 'Zelle', logo: 'zelle.png', currency: 'USD', groupType: 'FIAT' }, { id: 2, name: 'MLC CARD', logo: 'transfermovil.png', currency: 'MLC', groupType: 'FIAT' }, { id: 3, name: 'BITCOIN', logo: 'btc.png', currency: 'BTC', groupType: 'CRIPYO' }, { id: 4, name: 'QVAPAY', logo: 'qvapay-logo.png', currency: 'SQP', groupType: 'FIAT' }, { id: 5, name: 'WSY', logo: 'zelle.png', groupType: 'FIAT' }, { id: 6, name: 'SEIS', logo: 'seis.png', currency: 'USD', groupType: 'CASH' }, { id: 7, name: 'USDT', logo: 'usdt.png', currency: 'USDT' }, { id: 8, name: 'BIZUM', logo: 'bizum.png', currency: 'EUR', groupType: 'FIAT' } ]; resolve(list) }); */ } async fetchDeal(id) { const deal = (this.deals ?? []).filter(i => i.id == id)[0] ?? false; if (deal) return deal; let forceAuth = false; try { const response = await fetch(`/rest/v2/p2p/deal/${id}`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.status === 200) { return await response.json(); } if (response.status === 401) { forceAuth = true; } } catch (error) { console.error('Error fetching announcement', error); } return { forceAuth }; } async fetchAnnouncements() { try { const response = await fetch(`/rest/v2/p2p/announcement/list?intent=${this._filterState.intent}&paymentMethod=${this._filterState.paymentMethod}&amountIntent=${this._filterState.amount}&pg=${this._filterState.pg ?? 1}&pgSize=${this._filterState.pgSize ?? 15}`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.status === 200) { const res = await response.json(); this.announcements = res.items; return res; } } catch (error) { console.error('Error fetching announcements', error); } return { pg: this._filterState.pg ?? 1, pgSize: this._filterState.pgSize ?? 15, total: 0, items: [] } } async fetchMyAnnouncements() { try { const response = await fetch(`/rest/v2/p2p/announcement/list?onlyself=true&pg=${this.anouncementPg ?? 1}&pgSize=25`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.status === 401) { this.dispatchEvent(new CustomEvent('forceauth', { bubbles: true, composed: true })); } if (response.ok) { return await response.json(); } } catch (error) { console.error('Error fetching announcements', error); } return { pg: 1, pgSize: 50, total: 0, items: [] } } async fetchMyDeal(pg = 1, pgSize = 10) { try { const response = await fetch(`/rest/v2/p2p/deal/list?pg=${pg}&pgSize=${pgSize}`, { method: 'GET', headers: await this.buildRequestHeaders() }); if (response.status === 401) { this.dispatchEvent(new CustomEvent('forceauth', { bubbles: true, composed: true })); } if (response.ok) { return await response.json(); } } catch (error) { console.error('Error fetching deals', error); } return { pg: 1, pgSize: 50, total: 0, items: [] } } } class Router { constructor() { this.routes = {}; this.currentRoute = null; window.addEventListener('popstate', () => this.handleRouting()); } addRoute(path, callback) { this.routes[path] = callback; } navigate(path, data = {}) { window.history.pushState(data, null, path); this.handleRouting(); // Actualizar todos los routers document.querySelectorAll('remex-router').forEach(router => { router?.checkActiveRoute(); }); } handleRouting() { const path = window.location.pathname; const handler = this.routes[path] || this.routes['*']; if (handler) { handler(); } // Actualizar todos los routers document.querySelectorAll('remex-router').forEach(router => { router?.checkActiveRoute(); }); } }; // Register the custom element customElements.define('remex-p2p', RemesitaP2P); /* Resscribe fetch add cache feature */ const originalFetch = window.fetch; const ongoingRequests = new Map(); const cachedResponses = new Map(); window.fetch = function (input, options = {}) { const { useCache = false, cacheDuration = 86400000, ...fetchOptions } = options; const cacheKey = generateCacheKey(input, fetchOptions); // Verificar caché solo si está habilitado if (useCache) { const cachedEntry = cachedResponses.get(cacheKey); if (cachedEntry && cachedEntry.expiration > Date.now()) { return Promise.resolve(createResponseFromCachedData(cachedEntry.data)); } // Reutilizar petición en curso si existe if (ongoingRequests.has(cacheKey)) { return ongoingRequests.get(cacheKey); } } // Crear nueva petición const fetchPromise = originalFetch(input, fetchOptions) .then(async response => { const clone = response.clone(); const body = await clone.arrayBuffer(); // Almacenar en caché siempre cachedResponses.set(cacheKey, { data: { body, status: clone.status, statusText: clone.statusText, headers: [...clone.headers.entries()], url: clone.url }, expiration: Date.now() + cacheDuration }); return createResponseFromCachedData(cachedResponses.get(cacheKey).data); }) .catch(error => { ongoingRequests.delete(cacheKey); throw error; }) .finally(() => { ongoingRequests.delete(cacheKey); }); // Registrar petición en curso para todas las solicitudes ongoingRequests.set(cacheKey, fetchPromise); return fetchPromise; }; // Función para generar clave de caché (sin cambios) function generateCacheKey(input, options) { const url = input instanceof Request ? input.url : input.toString(); // Excluir explícitamente parámetros de caché const { headers, body, useCache, // <-- Excluir cacheDuration, // <-- Excluir ...restOptions } = options; const normalizedHeaders = headers instanceof Headers ? [...headers.entries()].sort() : Object.entries(headers || {}).sort(); return JSON.stringify({ url, options: { ...restOptions, headers: normalizedHeaders, body: typeof body === 'string' ? body : null, } }); } // Función para crear respuesta desde caché (sin cambios) function createResponseFromCachedData(data) { return new Response(data.body, { status: data.status, statusText: data.statusText, headers: new Headers(data.headers), url: data.url, }); } // Añadir al código existente window.fetchClearCache = function (input, options = {}) { const { useCache, cacheDuration, ...fetchOptions } = options; const cacheKey = generateCacheKey(input, fetchOptions); // Eliminar de ambos almacenamientos cachedResponses.delete(cacheKey); ongoingRequests.delete(cacheKey); }; }