PageSourceSearch

https://quatrocincoum.com.br/wp-content/themes/editora451_v2/sso/js/lib/pix-payment.js?ver=100.1.22

js quatrocincoum.com.br collected 2026-09-24 09:09:07 UTC 15,883 bytes, 380 lines download raw bytes

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('<', '&lt;')
94            .replaceAll('>', '&gt;')
95            .replaceAll('"', '&quot;')
96            .replaceAll("'", '&#039;');
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">&times;</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.