1/** 2 * Apresentação compartilhada do PIX da Editora 451. 3 * 4 * A API do DSO devolve o QR Code, o código copia-e-cola e o vencimento em 5 * infosPayment. Alguns provedores invertem os campos url/barcode, então este 6 * módulo identifica cada valor pelo conteúdo e é reutilizado no checkout, no 7 * presente e em Minha Conta. 8 */ 9(function (window, document) { 10 'use strict'; 11 12 function paymentRoots(data) { 13 const roots = [ 14 data, 15 data?.data, 16 data?.result, 17 data?.order, 18 data?.data?.order, 19 data?.result?.order, 20 data?.transaction, 21 data?.order?.transaction, 22 ...(Array.isArray(data?.transactions) ? data.transactions : []) 23 ]; 24 25 return roots.filter((item, index, list) => item && typeof item === 'object' && list.indexOf(item) === index); 26 } 27 28 function isQrImageUrl(value) { 29 return /^(https?:\/\/|data:image\/|blob:)/i.test(String(value || '').trim()); 30 } 31 32 function firstMatchingValue(values, predicate) { 33 return values.find((value) => String(value || '').trim() && predicate(value)) || ''; 34 } 35 36 function getPaymentInfo(data) { 37 let qrUrl = ''; 38 let barcode = ''; 39 let expiration = ''; 40 41 paymentRoots(data).forEach((root) => { 42 const infos = root.infosPayment || root.infos_payment || root.paymentInfo || root.payment_info || {}; 43 44 const paymentValues = [ 45 // No /generator-orders da 451: barcode = imagem e url = Copia e Cola. 46 infos.barcode, 47 infos.url, 48 infos.qrCodeUrl, 49 infos.qrcodeUrl, 50 infos.qr_code_url, 51 infos.pixQrCodeUrl, 52 infos.pix_qr_code_url, 53 infos.qrCode, 54 infos.qrcode, 55 infos.copyPaste, 56 infos.copy_paste, 57 infos.pixCopyPaste, 58 infos.pixCopiaECola, 59 root.pixQrCodeUrl, 60 root.qrCodeUrl, 61 // Em /orders: boletoBarcode = imagem e boletoUrl = Copia e Cola. 62 root.boletoBarcode, 63 root.boletoUrl, 64 root.pixCopyPaste, 65 root.pixCopiaECola, 66 root.barcode 67 ]; 68 69 qrUrl ||= firstMatchingValue(paymentValues, isQrImageUrl); 70 barcode ||= firstMatchingValue(paymentValues, (value) => !isQrImageUrl(value)); 71 72 expiration ||= infos.expiration_date 73 || infos.expirationDate 74 || infos.expires_at 75 || infos.expiresAt 76 || root.expiration_date 77 || root.expirationDate 78 || root.expires_at 79 || root.expiresAt 80 || ''; 81 }); 82 83 return { 84 qrUrl: String(qrUrl || '').trim(), 85 barcode: String(barcode || '').trim(), 86 expiration: String(expiration || '').trim() 87 }; 88 } 89 90 function escapeHtml(value) { 91 return String(value ?? '') 92 .replaceAll('&', '&') 93 .replaceAll('<', '<') 94 .replaceAll('>', '>') 95 .replaceAll('"', '"') 96 .replaceAll("'", '''); 97 } 98 99 function formatExpiration(value) { 100 if (!value) return 'Confira o prazo de vencimento no aplicativo do seu banco.'; 101 102 const dateOnly = value.match(/^(\d{4})-(\d{2})-(\d{2})$/); 103 if (dateOnly) return `Pague até ${dateOnly[3]}/${dateOnly[2]}/${dateOnly[1]}.`; 104 105 const parsed = new Date(value); 106 if (Number.isNaN(parsed.getTime())) return `Vencimento: ${value}.`; 107 108 return `Pague até ${new Intl.DateTimeFormat('pt-BR', { 109 dateStyle: 'short', 110 timeStyle: 'short' 111 }).format(parsed)}.`; 112 } 113 114 function getOrderId(data) { 115 const order = data?.order; 116 if (typeof order === 'string' || typeof order === 'number') return String(order); 117 return String(order?.id || order?._id || data?.orderId || data?.order_id || data?.id || ''); 118 } 119 120 function normalizeStatus(value) { 121 return String(value || '') 122 .trim() 123 .toLocaleLowerCase('pt-BR') 124 .normalize('NFD') 125 .replace(/[\u0300-\u036f]/g, '') 126 .replace(/[_-]+/g, ' '); 127 } 128 129 function transactionStatuses(transaction) { 130 const statusTransaction = transaction?.statusTransaction || transaction?.status_transaction || {}; 131 132 return [ 133 transaction?.status, 134 transaction?.paymentStatus, 135 transaction?.payment_status, 136 statusTransaction?.name, 137 statusTransaction?.slug, 138 statusTransaction?.code, 139 statusTransaction?.status
140 ].filter(Boolean).map(normalizeStatus); 141 } 142 143 function transactionMatchesOrder(transaction, orderId) { 144 if (!orderId) return false; 145 146 return [transaction?.order, transaction?.orderId, transaction?.order_id, transaction?.id, transaction?._id] 147 .filter(value => value !== null && value !== undefined && value !== '') 148 .map(String) 149 .includes(String(orderId)); 150 } 151 152 function latestTransaction(transactions) { 153 return transactions.slice().sort((left, right) => { 154 const leftDate = Date.parse(left?.createdAt || left?.created_at || left?.updatedAt || left?.updated_at || ''); 155 const rightDate = Date.parse(right?.createdAt || right?.created_at || right?.updatedAt || right?.updated_at || ''); 156 157 if (Number.isNaN(leftDate) || Number.isNaN(rightDate)) return 0; 158 return rightDate - leftDate; 159 })[0] || null; 160 } 161 162 function orderIsPaid(order, orderId = '') { 163 const transactions = [ 164 order?.transaction, 165 ...(Array.isArray(order?.transactions) ? order.transactions : []) 166 ].filter(transaction => transaction && typeof transaction === 'object'); 167 const paidStatuses = ['paid', 'pago', 'paga', 'approved', 'aprovado', 'aprovada', 'confirmed', 'confirmado', 'confirmada', 'captured', 'succeeded', 'success', 'sucesso']; 168 const pendingStatuses = ['pending', 'pendente', 'waiting', 'aguardando', 'aguardando pagamento', 'processing', 'processando', 'created', 'criado', 'open', 'aberto', 'unpaid', 'nao pago']; 169 170 // Para PIX, o status "active" do plano não comprova pagamento. A 171 // transação é a fonte de verdade e pode continuar pendente. 172 if (transactions.length) { 173 const matchingTransaction = transactions.find(transaction => transactionMatchesOrder(transaction, orderId)); 174 const transaction = matchingTransaction || latestTransaction(transactions); 175 176 if (transaction.paymentAt || transaction.payment_at || transaction.paidAt || transaction.paid_at) { 177 return true; 178 } 179 180 const statuses = transactionStatuses(transaction); 181 if (statuses.some(status => pendingStatuses.includes(status))) return false; 182 return statuses.some(status => paidStatuses.includes(status)); 183 } 184 185 if (order?.paymentAt || order?.payment_at || order?.paidAt || order?.paid_at) return true; 186 187 const explicitPaymentStatuses = [order?.paymentStatus, order?.payment_status] 188 .filter(Boolean) 189 .map(normalizeStatus); 190 if (explicitPaymentStatuses.some(status => pendingStatuses.includes(status))) return false; 191 if (explicitPaymentStatuses.some(status => paidStatuses.includes(status))) return true; 192 193 // Sem transação, aceitamos apenas estados financeiros explÃcitos. Não 194 // usamos active/ativo, pois eles descrevem o plano, não o pagamento. 195 return paidStatuses.includes(normalizeStatus(order?.status)); 196 } 197 198 function orderMatches(order, orderId) { 199 const candidates = [ 200 order?.id, 201 order?._id, 202 order?.order, 203 order?.orderId, 204 order?.order_id, 205 order?.subscriptionId, 206 order?.transaction?.orderId, 207 order?.transaction?.order_id, 208 ...(Array.isArray(order?.transactions) 209 ? order.transactions.flatMap(transaction => [transaction?.orderId, transaction?.order_id]) 210 : []) 211 ] 212 .filter(Boolean) 213 .map(String); 214 return candidates.includes(String(orderId)); 215 } 216 217 async function copyText(text) { 218 if (navigator.clipboard && window.isSecureContext) { 219 await navigator.clipboard.writeText(text); 220 return; 221 } 222 223 const field = document.createElement('textarea'); 224 field.value = text; 225 field.setAttribute('readonly', ''); 226 field.style.position = 'fixed'; 227 field.style.opacity = '0'; 228 document.body.appendChild(field); 229 field.select(); 230 document.execCommand('copy'); 231 field.remove(); 232 } 233 234 function startStatusPolling(data, modal, redirectUrl) { 235 const orderId = getOrderId(data); 236 const token = typeof getCookie === 'function' ? getCookie('sso_auth_token') : ''; 237 const status = modal.querySelector('.pix-payment-modal__status'); 238 239 if (!orderId || !token || !status) return; 240 241 let attempts = 0; 242 const stop = () => { 243 if (modal._pixStatusInterval) { 244 window.clearInterval(modal._pixStatusInterval); 245 modal._pixStatusInterval = null; 246 } 247 }; 248 249 const check = async () => { 250 if (!document.body.contains(modal) || attempts >= 180) { 251 stop(); 252 if (document.body.contains(modal)) { 253 status.textContent = 'A confirmação ainda não chegou. Você pode acompanhar em Minha Conta.'; 254 } 255 return; 256 } 257 258 attempts += 1; 259 try { 260 const response = await fetch(urlsso + '/orders', { 261 method: 'GET', 262 headers: { 263 'Content-Type': 'application/json', 264 freeroute: true, 265 Authorization: 'Bearer ' + token 266 } 267 }); 268 if (!response.ok) return; 269 270 const payload = await response.json(); 271 const orders = payload?.data?.data || payload?.data || []; 272 const confirmed = Array.isArray(orders) 273 ? orders.find(order => orderMatches(order, orderId) && orderIsPaid(order, orderId)) 274 : null; 275 276 if (!confirmed) return; 277 278 stop(); 279 status.classList.add('is-paid'); 280 status.textContent = 'Pagamento confirmado. Sua assinatura está ativa.'; 281 282 if (redirectUrl) { 283 window.setTimeout(() => { 284 window.location.href = redirectUrl; 285 }, 2200); 286 } 287 } catch (error) { 288 console.warn('Não foi possÃvel consultar o status do PIX nesta tentativa.', error); 289 } 290 }; 291 292 modal._feedbackCleanup = stop; 293 check(); 294 modal._pixStatusInterval = window.setInterval(check, 5000); 295 } 296 297 function open(data, options = {}) { 298 const previous = document.querySelector('.pix-payment-modal'); 299 if (previous) { 300 if (typeof previous._feedbackCleanup === 'function') previous._feedbackCleanup(); 301 previous.remove(); 302 } 303 304 const payment = getPaymentInfo(data); 305 const title = options.title || 'Seu PIX está pronto'; 306 const description = options.description || 'Leia o QR Code ou copie o código para concluir o pagamento.'; 307 const accountUrl = options.accountUrl || '/minha-conta/?userpage=subscription'; 308 const showAccountLink = options.showAccountLink !== false; 309 const modal = document.createElement('div'); 310 311 modal.className = 'pix-payment-modal feedback-layer'; 312 modal.dataset.feedbackLabel = 'Pagamento PIX'; 313 modal.setAttribute('role', 'dialog'); 314 modal.setAttribute('aria-modal', 'true'); 315 modal.innerHTML = ` 316 <section class="pix-payment-modal__panel feedback-panel" aria-labelledby="pix-payment-title"> 317 <button class="pix-payment-modal__close" type="button" aria-label="Fechar pagamento PIX">×</button>
318 <span class="feedback-eyebrow">451 / Pagamento PIX</span> 319 <header class="pix-payment-modal__header"> 320 <span class="pix-payment-modal__stamp" aria-hidden="true"><span>PIX</span></span> 321 <div> 322 <h2 id="pix-payment-title">${escapeHtml(title)}</h2> 323 <p>${escapeHtml(description)}</p> 324 </div> 325 </header> 326 <div class="pix-payment-modal__grid"> 327 <div class="pix-payment-modal__visual"> 328 ${payment.qrUrl 329 ? `<figure class="pix-payment-modal__qr"><img src="${escapeHtml(payment.qrUrl)}" alt="QR Code para pagamento PIX"></figure>` 330 : '<div class="pix-payment-modal__missing">O QR Code não foi devolvido pela operadora. Use o código PIX ao lado.</div>'} 331 </div> 332 <div class="pix-payment-modal__content"> 333 <ol class="pix-payment-modal__steps"> 334 <li>Abra a área PIX do seu banco.</li> 335 <li>Escaneie o QR Code ou use Copia e Cola.</li> 336 <li>Confira o valor e confirme.</li> 337 </ol> 338 <div class="pix-payment-modal__copy"> 339 <span>PIX Copia e Cola</span> 340 <code>${escapeHtml(payment.barcode || 'Código PIX indisponÃvel.')}</code> 341 ${payment.barcode ? '<button type="button" class="pix-payment-modal__copy-button">Copiar código PIX</button>' : ''} 342 </div> 343 <p class="pix-payment-modal__expiration">${escapeHtml(formatExpiration(payment.expiration))}</p> 344 <p class="pix-payment-modal__status" role="status" aria-live="polite">Aguardando a confirmação do pagamento.</p> 345 ${showAccountLink 346 ? `<a class="pix-payment-modal__account" href="${escapeHtml(accountUrl)}">Acompanhar em Minha Conta</a>` 347 : ''} 348 </div> 349 </div> 350 </section>`; 351 352 document.body.appendChild(modal); 353 354 const image = modal.querySelector('.pix-payment-modal__qr img'); 355 image?.addEventListener('error', function () { 356 this.closest('.pix-payment-modal__qr').innerHTML = '<div class="pix-payment-modal__missing">Não foi possÃvel carregar o QR Code. Use o código PIX Copia e Cola.</div>'; 357 }); 358 359 const copyButton = modal.querySelector('.pix-payment-modal__copy-button'); 360 copyButton?.addEventListener('click', async function () { 361 try { 362 await copyText(payment.barcode); 363 this.classList.add('is-copied'); 364 this.textContent = 'Código copiado'; 365 } catch (error) { 366 this.textContent = 'Selecione e copie o código'; 367 } 368 }); 369 370 if (typeof enhanceFeedbackModal === 'function') enhanceFeedbackModal(modal); 371 if (options.poll !== false) startStatusPolling(data, modal, options.redirectUrl || accountUrl); 372 373 return modal; 374 } 375 376 window.Editora451Pix = { 377 getPaymentInfo, 378 open 379 }; 380})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.