PageSourceSearch

https://envialosimple.com/js/fin-chat.js?v=1.1

js envialosimple.com collected 2026-09-24 18:10:47 UTC 11,570 bytes, 309 lines download raw bytes

1class IntercomManager {
2    constructor(config) {
3        this.APP_ID = config.appId;
4        this.API_BASE = config.apiBase;
5        this.AUTH_ENDPOINT = config.authEndpoint;
6
7        this.MAX_RETRIES = config.maxRetries ?? 50;
8        this.RETRY_INTERVAL = config.retryInterval ?? 100;
9        this.REQUEST_TIMEOUT = config.requestTimeout ?? 6000;
10        this.SESSION_DURATION = config.sessionDuration ?? 3600000; // 1h
11
12        this.IDENTITY_COOKIE = config.identityCookieName ?? 'ic_identity_v1';
13    }
14
15    /* ------------------ CARGA DEL WIDGET ------------------ */
16    async loadIntercomScript() {
17        return new Promise((resolve, reject) => {
18            // si ya existe el tag script del widget, no lo duplicamos
19            const src = `https://widget.intercom.io/widget/${this.APP_ID}`;
20            if (document.querySelector(`script[src="${src}"]`)) return resolve();
21
22            const script = document.createElement('script');
23            script.type = 'text/javascript';
24            script.async = true;
25            script.src = src;
26            script.onload = () => resolve();
27            script.onerror = () => reject(new Error('Failed to load Intercom script'));
28            document.head.appendChild(script);
29        });
30    }
31
32    async waitForIntercom() {
33        return new Promise((resolve, reject) => {
34            let attempts = 0;
35            const check = () => {
36                if (typeof window.Intercom === 'function') return resolve();
37                attempts++;
38                if (attempts >= this.MAX_RETRIES) return reject(new Error('Intercom failed to initialize'));
39                setTimeout(check, this.RETRY_INTERVAL);
40            };
41            check();
42        });
43    }
44
45    /* ------------------ AUTH DEL USUARIO ------------------ */
46    async fetchUserAuth() {
47        const controller = new AbortController();
48        const timeoutId = setTimeout(() => controller.abort(), this.REQUEST_TIMEOUT);
49
50        try {
51            const response = await fetch(this.AUTH_ENDPOINT, {
52                method: 'GET',
53                headers: { 'Content-Type': 'application/json' },
54                credentials: 'include',
55                signal: controller.signal
56            });
57
58            clearTimeout(timeoutId);
59
60            if (!response.ok) {
61                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
62            }
63
64            const data = await response.json();
65            if (!data?.jsonMC?.resultado) {
66                throw new Error('Authentication failed');
67            }
68
69            const token = data.jsonMC.respuesta || null;
70            const uidFromJwt = token ? this.extractUserIdFromJwt(token) : null;
71
72            return {
73                isAuthenticated: true,
74                token,
75                userId: uidFromJwt // puede ser null si no viene en el JWT
76            };
77
78        } catch (error) {
79            clearTimeout(timeoutId);
80            console.warn('User auth failed:', error?.message || error);
81            return { isAuthenticated: false };
82        }
83    }
84
85    extractUserIdFromJwt(jwt) {
86        try {
87            const parts = jwt.split('.');
88            if (parts.length < 2) return null;
89            const payload = JSON.parse(this.base64UrlDecode(parts[1]));
90            // Intercom usa normalmente "user_id"; a veces podés querer "sub"
91            return payload.user_id || payload.sub || null;
92        } catch {
93            return null;
94        }
95    }
96
97    base64UrlDecode(str) {
98        // polyfill simple para atob base64url
99        const pad = (s) => s + '==='.slice((s.length + 3) % 4);
100        const b64 = pad(str.replace(/-/g, '+').replace(/_/g, '/'));
101        return decodeURIComponent(
102            [...atob(b64)]
103                .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
104                .join('')
105        );
106    }
107
108    /* ------------------ BOOT/SHUTDOWN ------------------ */
109    bootIntercom(settings, { forceShutdown = false } = {}) {
110        try {
111            if (forceShutdown && window.Intercom) {
112                window.Intercom('shutdown');
113            }
114            window.Intercom('boot', settings);
115        } catch (error) {
116            console.error('Failed to boot Intercom:', error);
117            throw error;
118        }
119    }
120
121    /* ------------------ FLUJO PRINCIPAL ------------------ */
122    async initialize() {
123        try {
124            await this.loadIntercomScript();
125            await this.waitForIntercom();
126
127            const auth = await this.fetchUserAuth();
128            const countryData = await this.countryFromIP();
129
130            let iso = countryData?.iso?this.inCountryList(countryData.iso):"int";
131            let countryName = this.countryNameFromIso(iso);
132
133            const hostname = window.location.hostname;
134
135            // Atributos People (NO usar prefijo conversation_*)
136            const personAttrs = this.filterNullish({
137                dw_visit_pais_iso: iso,
138                dw_visit_pais_nombre: countryName,
139                dw_visit_website: hostname,
140                dw_items_carrito: this.getCookie('items_carrito'),
141                dw_utm_medium: this.getCookie('dw_medium'),
142                dw_utm_source: this.getCookie('dw_source')
143            });
144
145            const base = {
146                app_id: this.APP_ID,
147                api_base: this.API_BASE,
148                session_duration: this.SESSION_DURATION,
149                ...personAttrs
150            };
151
152            // Identidad deseada para decidir shutdown solo cuando sea necesario
153            const desiredIdentity = auth?.isAuthenticated && auth?.token
154                ? `user:${auth.userId || 'unknown'}`
155                : 'anon';
156
157            const prevIdentity = this.getCookie(this.IDENTITY_COOKIE);
158            const forceShutdown = this.shouldShutdown(prevIdentity, desiredIdentity);
159
160            const settings = (desiredIdentity.startsWith('user:'))
161                ? { ...base, intercom_user_jwt: auth.token }
162                : base;
163
164            this.bootIntercom(settings, { forceShutdown });
165            this.setCookie(this.IDENTITY_COOKIE, desiredIdentity, { maxAge: 31536000, path: '/', sameSite: 'Lax' });
166
167        } catch (error) {
168            console.error('Intercom initialization failed:', error);
169            // Fallback: boot como visitor mínimo
170            try {
171                this.bootIntercom({
172                    app_id: this.APP_ID,
173                    api_base: this.API_BASE,
174                    session_duration: this.SESSION_DURATION
175                }, { forceShutdown: true });
176                this.setCookie(this.IDENTITY_COOKIE, 'anon', { maxAge: 31536000, path: '/', sameSite: 'Lax' });
177            } catch (fallbackError) {
178                console.error('Fallback boot also failed:', fallbackError);
179            }
180        }
181    }
182
183    shouldShutdown(prev, desired) {
184        if (!prev) return false;             // primera vez, no cortamos
185        if (prev === desired) return false;  // misma identidad, conservamos
186        const wasUser = prev.startsWith('user:');
187        const isUser = desired.startsWith('user:');
188        // shutdown solo si cambia entre anon <-> user o cambia el userID
189        return (wasUser !== isUser) || (wasUser && isUser && prev !== desired);
190    }
191
192    /* ------------------ HELPERS ------------------ */
193    safeGlobal(varName) {
194        try {
195            // evita ReferenceError si la variable global no existe
196            return (typeof window[varName] !== 'undefined') ? window[varName] : null;
197        } catch {
198            return null;
199        }
200    }
201
202    filterNullish(obj) {
203        const out = {};
204        for (const [k, v] of Object.entries(obj)) {
205            if (v !== null && v !== undefined && v !== '') out[k] = v;
206        }
207        return out;
208    }
209
210    getCookie(name) {
211        const nameEq = name + '=';
212        const parts = document.cookie.split(';');
213        for (let c of parts) {
214            c = c.trim();
215            if (c.indexOf(nameEq) === 0) return decodeURIComponent(c.substring(nameEq.length));
216        }
217        return null;
218    }
219
220    setCookie(name, value, { maxAge, path = '/', sameSite = 'Lax' } = {}) {
221        let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; SameSite=${sameSite}`;
222        if (typeof maxAge === 'number') cookie += `; Max-Age=${maxAge}`;
223        document.cookie = cookie;
224    }
225
226
227    async countryFromIP() {
228        const providers = [
229            () => this.countryFromCloudflareTrace(),
230            () => this.countryFromIpwhois(),
231            () => this.countryFromCountryIs()
232        ];
233
234        for (const provider of providers) {
235            try {
236                const result = await provider();
237                if (result && result.iso) return result;
238            } catch (err) {
239                // proveedor caido o bloqueado, se prueba el siguiente
240            }
241        }
242
243        return { iso: null, name: null, source: 'ip', ok: false, error: 'IP service failed' };
244    }
245
246    // Cloudflare sirve /cdn-cgi/trace en el propio dominio: mismo origen, gratis y sin limite.
247    async countryFromCloudflareTrace() {
248        const res = await fetch('/cdn-cgi/trace', { signal: AbortSignal.timeout(2500) });
249        if (!res.ok) throw new Error('cloudflare trace failed');
250
251        const raw = {};
252        const text = await res.text();
253        for (const line of text.trim().split('\n')) {
254            const [key, ...rest] = line.split('=');
255            raw[key] = rest.join('=');
256        }
257
258        const iso = raw.loc && raw.loc !== 'XX' ? raw.loc : null;
259        return { iso, name: iso, source: 'ip', raw };
260    }
261
262    async countryFromIpwhois() {
263        const res = await fetch('https://ipwho.is/', { signal: AbortSignal.timeout(2500) });
264        if (!res.ok) throw new Error('ipwho.is failed');
265
266        const data = await res.json();
267        if (data && data.success === false) throw new Error(data.message || 'ipwho.is error');
268
269        return { iso: data.country_code || null, name: data.country || null, source: 'ip', raw: data };
270    }
271
272    async countryFromCountryIs() {
273        const res = await fetch('https://api.country.is/', { signal: AbortSignal.timeout(2500) });
274        if (!res.ok) throw new Error('country.is failed');
275
276        const data = await res.json();
277        return { iso: data.country || null, name: data.country || null, source: 'ip', raw: data };
278    }
279
280    inCountryList(iso) {
281        if (typeof iso !== 'string' || !iso) return "int";
282        const isoLower = iso.toLowerCase();
283        const list = ["ar", "bo", "br", "cl", "co", "es", "us", "mx", "pe", "uy", "ve"];
284        if (list.includes(isoLower)) {
285            return isoLower;
286        }
287        return "int";
288    }
289    inCountryNameList(iso,name) {
290        if (typeof iso !== 'string' || !iso) return "Internacional";
291        const isoLower = iso.toLowerCase();
292        const list = ["ar", "bo", "br", "cl", "co", "es", "us", "mx", "pe", "uy", "ve"];
293        if (list.includes(isoLower)) {
294            return name;
295        }
296        return "Internacional";
297    }
298
299    // Nombres locales: no dependemos de que el proveedor de IP devuelva el nombre del pais.
300    countryNameFromIso(iso) {
301        const names = {
302            ar: "Argentina", bo: "Bolivia", br: "Brazil", cl: "Chile",
303            co: "Colombia", es: "Spain", us: "United States", mx: "Mexico",
304            pe: "Peru", uy: "Uruguay", ve: "Venezuela"
305        };
306        if (typeof iso !== 'string' || !iso) return "Internacional";
307        return names[iso.toLowerCase()] || "Internacional";
308    }
309}

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.