1/** 2 * Crypto Québec â Funnel Web Desktop & iPhone 16 Pro State Machine 3 * Version 2.0: Email as Cornerstone, 2FA OTP via AgentMail, 4 * Multi-jurisdiction Legal Consents (LCAP, CAN-SPAM, RGPD), 5 * Profile Linking with CryptoBot DB. 6 */ 7 8// API Base Configuration from WordPress Plugin 9const API_BASE = (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.apiUrl) ? window.CQ_FUNNEL_CONFIG.apiUrl.replace(/\/+$/, "") : ""; 10function getApiUrl(endpoint) { 11 return API_BASE + (endpoint.startsWith("/") ? "" : "/") + endpoint; 12} 13 14// URL Search Parameters & Attribution Extraction 15const urlParams = new URLSearchParams(window.location.search); 16const urlSessionId = urlParams.get('session_id'); 17 18// State Object 19const FunnelState = { 20 sessionId: urlSessionId || localStorage.getItem('cq_web_session_id') || `web_${Math.random().toString(36).substring(2, 11)}`, 21 language: localStorage.getItem('cq_web_lang') || 'fr', 22 funnelData: null, 23 email: localStorage.getItem('cq_web_email') || null, 24 emailVerified: false, 25 sessionToken: localStorage.getItem('cq_session_token') || null, 26 telegramId: null, 27 telegramUsername: null, 28 existingProfile: null, 29 profile: null, 30 weexUid: localStorage.getItem('cq_weex_uid') || null, 31 bitunixUid: localStorage.getItem('cq_bitunix_uid') || null, 32 tradingviewUsername: localStorage.getItem('cq_tv_username') || null, 33 // Advertising Attribution Tracking 34 utmSource: urlParams.get('utm_source') || localStorage.getItem('cq_utm_source') || null, 35 utmCampaign: urlParams.get('utm_campaign') || localStorage.getItem('cq_utm_campaign') || null, 36 utmMedium: urlParams.get('utm_medium') || localStorage.getItem('cq_utm_medium') || null, 37 utmTerm: urlParams.get('utm_term') || localStorage.getItem('cq_utm_term') || null, 38 utmContent: urlParams.get('utm_content') || localStorage.getItem('cq_utm_content') || null, 39 ref: urlParams.get('ref') || localStorage.getItem('cq_ref') || null, 40 fbclid: urlParams.get('fbclid') || localStorage.getItem('cq_fbclid') || null, 41 gclid: urlParams.get('gclid') || localStorage.getItem('cq_gclid') || null, 42 ttclid: urlParams.get('ttclid') || localStorage.getItem('cq_ttclid') || null 43}; 44 45// Persist ad attribution parameters 46if (FunnelState.utmSource) localStorage.setItem('cq_utm_source', FunnelState.utmSource); 47if (FunnelState.utmCampaign) localStorage.setItem('cq_utm_campaign', FunnelState.utmCampaign); 48if (FunnelState.utmMedium) localStorage.setItem('cq_utm_medium', FunnelState.utmMedium); 49if (FunnelState.utmTerm) localStorage.setItem('cq_utm_term', FunnelState.utmTerm); 50if (FunnelState.utmContent) localStorage.setItem('cq_utm_content', FunnelState.utmContent); 51if (FunnelState.ref) localStorage.setItem('cq_ref', FunnelState.ref); 52if (FunnelState.fbclid) localStorage.setItem('cq_fbclid', FunnelState.fbclid); 53if (FunnelState.gclid) localStorage.setItem('cq_gclid', FunnelState.gclid); 54if (FunnelState.ttclid) localStorage.setItem('cq_ttclid', FunnelState.ttclid); 55localStorage.setItem('cq_web_session_id', FunnelState.sessionId); 56 57// Ad Event Pixel Dispatch Helper (Meta, TikTok, Google) 58function trackAdEvent(eventName, params = {}) { 59 try { 60 if (window.fbq) window.fbq('track', eventName, params); 61 if (window.ttq) window.ttq.track(eventName, params); 62 if (window.gtag) window.gtag('event', eventName, params); 63 console.log(`[AdPixel] Dispatched event: ${eventName}`, params); 64 } catch(e) {} 65} 66 67// âââ Télémétrie First-Party & GA4 / GTM ââââââââââââââââââââââââââââââââââââââââââ 68function trackFunnelEvent(eventName, eventDetails = {}) { 69 try { 70 const isMobile = window.innerWidth <= 600; 71 const deviceType = isMobile ? 'mobile' : 'desktop'; 72 const payload = { 73 event_name: eventName, 74 details: eventDetails, 75 device: deviceType, 76 page: window.location.pathname || '/' 77 }; 78 79 // 1. Google Tag Manager / GA4 dataLayer 80 window.dataLayer = window.dataLayer || []; 81 window.dataLayer.push({ 82 event: 'cq_funnel_' + eventName, 83 cq_event: eventName, 84 cq_device: deviceType, 85 cq_page: payload.page, 86 ...eventDetails 87 }); 88 89 // 2. Pixel / Gtag dispatch 90 if (window.gtag) { 91 window.gtag('event', 'cq_funnel_' + eventName, { 92 event_category: 'CQ Funnel', 93 event_label: eventName, 94 device: deviceType, 95 ...eventDetails 96 }); 97 } 98 99 // 3. First-Party Native REST API (WordPress DB / Admin) 100 const baseApi = (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.apiUrl) ? window.CQ_FUNNEL_CONFIG.apiUrl : '/wp-json/cq-funnel/v1'; 101 const trackUrl = baseApi.replace(/\/$/, '') + '/track-event'; 102 const jsonStr = JSON.stringify(payload); 103 104 if (navigator.sendBeacon) { 105 const blob = new Blob([jsonStr], { type: 'application/json' }); 106 navigator.sendBeacon(trackUrl, blob); 107 } else { 108 fetch(trackUrl, { 109 method: 'POST', 110 headers: { 'Content-Type': 'application/json' }, 111 body: jsonStr, 112 keepalive: true 113 }).catch(() => {}); 114 } 115 } catch (err) { 116 // Fail-safe 117 } 118} 119 120// Synthesized Audio Feedback 121const AudioFX = { 122 ctx: null, 123 init() { 124 if (window.CQ_FUNNEL_CONFIG && window.CQ_FUNNEL_CONFIG.enableSound === false) return; 125 if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) { 126 this.ctx = new (window.AudioContext || window.webkitAudioContext)(); 127 } 128 }, 129 playPop() { 130 try { 131 this.init(); 132 if (!this.ctx) return; 133 if (this.ctx.state === 'suspended') this.ctx.resume(); 134 const osc = this.ctx.createOscillator(); 135 const gain = this.ctx.createGain(); 136 osc.type = 'sine'; 137 osc.frequency.setValueAtTime(480, this.ctx.currentTime); 138 osc.frequency.exponentialRampToValueAtTime(840, this.ctx.currentTime + 0.08); 139 gain.gain.setValueAtTime(0.12, this.ctx.currentTime); 140 gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.08); 141 osc.connect(gain); 142 gain.connect(this.ctx.destination); 143 osc.start(); 144 osc.stop(this.ctx.currentTime + 0.08); 145 } catch(e) {} 146 }, 147 playSuccess() { 148 try { 149 this.init(); 150 if (!this.ctx) return; 151 if (this.ctx.state === 'suspended') this.ctx.resume(); 152 const now = this.ctx.currentTime;
153 [523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => { 154 const osc = this.ctx.createOscillator(); 155 const gain = this.ctx.createGain(); 156 osc.type = 'triangle'; 157 osc.frequency.setValueAtTime(freq, now + i * 0.06); 158 gain.gain.setValueAtTime(0.08, now + i * 0.06); 159 gain.gain.exponentialRampToValueAtTime(0.01, now + i * 0.06 + 0.15); 160 osc.connect(gain); 161 gain.connect(this.ctx.destination); 162 osc.start(now + i * 0.06); 163 osc.stop(now + i * 0.06 + 0.15); 164 }); 165 } catch(e) {} 166 } 167}; 168 169// UI Elements 170const UI = { 171 drawer: document.getElementById('iphone-drawer'), 172 backdrop: document.getElementById('drawer-backdrop'), 173 triggerTab: document.getElementById('slide-trigger-tab'), 174 closeBtn: document.getElementById('close-drawer-btn'), 175 navOpenBtn: document.getElementById('nav-open-phone-btn'), 176 heroOpenBtn: document.getElementById('hero-open-phone-btn'), 177 chatStream: document.getElementById('chat-stream'), 178 inputDock: document.getElementById('input-dock'), 179 clock: document.getElementById('ios-clock'), 180 langFlag: document.getElementById('current-lang-flag'), 181 langCode: document.getElementById('current-lang-code'), 182 langMenuBtn: document.getElementById('lang-menu-btn'), 183 restartBtn: document.getElementById('restart-funnel-btn'), 184 drawerRestartBtn: document.getElementById('drawer-restart-btn'), 185 dockRestartBtn: document.getElementById('dock-restart-btn'), 186 botStatusText: document.getElementById('bot-status-text'), 187 // Legal Modal Elements 188 legalModal: document.getElementById('legal-modal'), 189 closeLegalBtn: document.getElementById('close-legal-modal-btn'), 190 acceptLegalBtn: document.getElementById('accept-legal-modal-btn'), 191 // UID Visual Help Modal Elements 192 uidHelpModal: document.getElementById('uid-help-modal'), 193 closeUidHelpBtn: document.getElementById('close-uid-help-btn'), 194 doneUidHelpBtn: document.getElementById('done-uid-help-btn'), 195 tabBtnWeex: document.getElementById('tab-btn-weex'), 196 tabBtnBitunix: document.getElementById('tab-btn-bitunix'), 197 // Font Size Controls 198 fontIncreaseBtn: document.getElementById('font-increase-btn'), 199 fontDecreaseBtn: document.getElementById('font-decrease-btn'), 200 // Inbox Elements 201 drawerInboxBtn: document.getElementById('drawer-inbox-btn'), 202 drawerInboxBadge: document.getElementById('drawer-inbox-badge'), 203 chatInboxBtn: document.getElementById('chat-inbox-btn'), 204 chatInboxBadge: document.getElementById('chat-inbox-badge'), 205 inboxModal: document.getElementById('inbox-modal'), 206 closeInboxBtn: document.getElementById('close-inbox-modal-btn'), 207 doneInboxBtn: document.getElementById('done-inbox-modal-btn'), 208 markAllReadBtn: document.getElementById('mark-all-read-btn'), 209 inboxList: document.getElementById('inbox-messages-list'), 210 inboxLoading: document.getElementById('inbox-loading'), 211 inboxEmpty: document.getElementById('inbox-empty'), 212 // VIP Feed & Podcasts Elements 213 drawerFeedBtn: document.getElementById('drawer-feed-btn'), 214 chatFeedBtn: document.getElementById('chat-feed-btn'), 215 feedModal: document.getElementById('vip-feed-modal'), 216 closeFeedBtn: document.getElementById('close-vip-feed-modal-btn'), 217 doneFeedBtn: document.getElementById('done-vip-feed-modal-btn'), 218 refreshFeedBtn: document.getElementById('refresh-vip-feed-btn'), 219 toggleExpelledTestBtn: document.getElementById('toggle-expelled-test-btn'), 220 feedSearch: document.getElementById('vip-feed-search'), 221 feedList: document.getElementById('vip-feed-list'), 222 feedLoading: document.getElementById('vip-feed-loading'), 223 feedExpelled: document.getElementById('vip-feed-expelled'), 224 feedEmpty: document.getElementById('vip-feed-empty'), 225}; 226 227// Digital Clock 228function updateClock() { 229 const now = new Date(); 230 const hrs = String(now.getHours()).padStart(2, '0'); 231 const mins = String(now.getMinutes()).padStart(2, '0'); 232 if (UI.clock) UI.clock.textContent = `${hrs}:${mins}`; 233} 234setInterval(updateClock, 1000); 235updateClock(); 236 237// Drawer Controls 238function openDrawer(source = 'unknown') { 239 UI.drawer.classList.add('open'); 240 UI.backdrop.classList.add('active'); 241 AudioFX.playPop(); 242 if (source !== 'side_tab') { 243 trackFunnelEvent('drawer_open', { trigger: source }); 244 } 245 if (UI.chatStream.children.length === 0) { 246 startFunnel(); 247 } 248} 249 250function closeDrawer() { 251 UI.drawer.classList.remove('open');
252 UI.backdrop.classList.remove('active'); 253 trackFunnelEvent('drawer_close'); 254} 255 256if (UI.triggerTab) { 257 UI.triggerTab.addEventListener('click', () => { 258 trackFunnelEvent('tab_click', { 259 trigger: 'side_tab', 260 title: 'ACCÃS VIP CQ', 261 badge: 'GRATUIT' 262 }); 263 openDrawer('side_tab'); 264 }); 265} 266if (UI.navOpenBtn) UI.navOpenBtn.addEventListener('click', () => openDrawer('nav_btn')); 267if (UI.heroOpenBtn) UI.heroOpenBtn.addEventListener('click', () => openDrawer('hero_btn')); 268if (UI.closeBtn) UI.closeBtn.addEventListener('click', closeDrawer); 269if (UI.backdrop) UI.backdrop.addEventListener('click', closeDrawer); 270 271// Custom site-wide trigger support (buttons with class .cq-open-funnel-btn or href='#open-cq-funnel') 272document.addEventListener('click', (e) => { 273 const trigger = e.target.closest('.cq-open-funnel-btn, [href="#open-cq-funnel"], [href="#cq-funnel"]'); 274 if (trigger) { 275 e.preventDefault(); 276 trackFunnelEvent('cta_button_click', { href: trigger.getAttribute('href') || 'button' }); 277 openDrawer('page_cta'); 278 } 279}); 280 281// If URL has session_id, automatically open drawer on launch 282if (urlSessionId) { 283 setTimeout(() => { 284 openDrawer('auto_session_url'); 285 }, 350); 286} 287 288// Legal Modal Controls 289function openLegalModal() { 290 if (UI.legalModal) UI.legalModal.classList.add('active'); 291} 292function closeLegalModal() { 293 if (UI.legalModal) UI.legalModal.classList.remove('active'); 294} 295if (UI.closeLegalBtn) UI.closeLegalBtn.addEventListener('click', closeLegalModal); 296if (UI.acceptLegalBtn) UI.acceptLegalBtn.addEventListener('click', closeLegalModal); 297 298// UID Visual Help Modal Controls 299function openUidHelpModal(initialTab = 'weex') { 300 const modal = UI.uidHelpModal || document.getElementById('uid-help-modal'); 301 if (modal) { 302 modal.classList.add('active'); 303 modal.style.setProperty('display', 'flex', 'important'); 304 switchUidHelpTab(initialTab); 305 if (window.AudioFX && typeof window.AudioFX.playPop === 'function') { 306 window.AudioFX.playPop(); 307 } 308 } 309} 310 311function closeUidHelpModal() { 312 const modal = UI.uidHelpModal || document.getElementById('uid-help-modal'); 313 if (modal) { 314 modal.classList.remove('active'); 315 modal.style.setProperty('display', 'none', 'important'); 316 } 317} 318 319function switchUidHelpTab(tab) { 320 const btnWeex = document.getElementById('tab-btn-weex'); 321 const btnBitunix = document.getElementById('tab-btn-bitunix'); 322 const paneWeex = document.getElementById('tab-content-weex'); 323 const paneBitunix = document.getElementById('tab-content-bitunix'); 324 325 if (tab === 'weex') { 326 if (btnWeex) btnWeex.classList.add('active'); 327 if (btnBitunix) btnBitunix.classList.remove('active'); 328 if (paneWeex) paneWeex.style.display = 'block'; 329 if (paneBitunix) paneBitunix.style.display = 'none'; 330 } else { 331 if (btnWeex) btnWeex.classList.remove('active'); 332 if (btnBitunix) btnBitunix.classList.add('active'); 333 if (paneWeex) paneWeex.style.display = 'none'; 334 if (paneBitunix) paneBitunix.style.display = 'block'; 335 } 336} 337 338if (UI.closeUidHelpBtn) UI.closeUidHelpBtn.addEventListener('click', closeUidHelpModal); 339if (UI.doneUidHelpBtn) UI.doneUidHelpBtn.addEventListener('click', closeUidHelpModal); 340if (UI.tabBtnWeex) UI.tabBtnWeex.addEventListener('click', () => switchUidHelpTab('weex')); 341if (UI.tabBtnBitunix) UI.tabBtnBitunix.addEventListener('click', () => switchUidHelpTab('bitunix')); 342 343// Global click event delegation for UID help button & modal controls 344document.addEventListener('click', (e) => { 345 const helpTrigger = e.target.closest('#btn-show-uid-help, .btn-uid-help'); 346 if (helpTrigger) { 347 e.preventDefault(); 348 e.stopPropagation(); 349 const currentExchange = (typeof FunnelState !== 'undefined' && FunnelState.exchange) ? FunnelState.exchange : 'weex'; 350 openUidHelpModal(currentExchange); 351 return; 352 } 353 354 const closeTrigger = e.target.closest('#close-uid-help-btn, #done-uid-help-btn'); 355 if (closeTrigger) { 356 e.preventDefault(); 357 closeUidHelpModal(); 358 return; 359 } 360 361 if (e.target && e.target.id === 'uid-help-modal') { 362 closeUidHelpModal(); 363 return; 364 } 365 366 const tabBtn = e.target.closest('.uid-tab-btn'); 367 if (tabBtn && tabBtn.dataset.tab) { 368 e.preventDefault(); 369 switchUidHelpTab(tabBtn.dataset.tab); 370 return; 371 } 372}); 373 374// âââ Font Size Adjustment (A+ / Aâ) âââââââââââââââââââââââââââââââââââââââââââ 375const FONT_SCALES = [0.82, 0.92, 1.0, 1.13, 1.28]; 376const FONT_SCALE_DEFAULT_IDX = 2; // 1.0 = default centre 377const FONT_SCALE_KEY = 'cq_chat_font_scale_idx'; 378 379const FontScale = { 380 idx: parseInt(localStorage.getItem(FONT_SCALE_KEY) ?? FONT_SCALE_DEFAULT_IDX, 10), 381 382 apply() { 383 const scale = FONT_SCALES[this.idx]; 384 if (UI.chatStream) { 385 UI.chatStream.style.setProperty('--chat-font-scale', scale); 386 } 387 // Clamp button disabled states 388 if (UI.fontDecreaseBtn) UI.fontDecreaseBtn.disabled = this.idx <= 0; 389 if (UI.fontIncreaseBtn) UI.fontIncreaseBtn.disabled = this.idx >= FONT_SCALES.length - 1; 390 // Persist 391 localStorage.setItem(FONT_SCALE_KEY, this.idx); 392 }, 393 394 increase() { 395 if (this.idx < FONT_SCALES.length - 1) { this.idx++; this.apply(); this.flash('+'); } 396 }, 397 398 decrease() { 399 if (this.idx > 0) { this.idx--; this.apply(); this.flash('â'); } 400 }, 401 402 flash(dir) { 403 const btn = dir === '+' ? UI.fontIncreaseBtn : UI.fontDecreaseBtn; 404 if (!btn) return; 405 btn.style.transition = 'transform 0.12s ease, background 0.12s ease'; 406 btn.style.transform = 'scale(1.35)'; 407 btn.style.background = dir === '+' ? 'rgba(16,185,129,0.28)' : 'rgba(255,255,255,0.18)'; 408 setTimeout(() => { 409 btn.style.transform = ''; 410 btn.style.background = ''; 411 }, 180); 412 } 413}; 414 415// Init on load 416FontScale.apply(); 417 418if (UI.fontIncreaseBtn) UI.fontIncreaseBtn.addEventListener('click', () => FontScale.increase()); 419if (UI.fontDecreaseBtn) UI.fontDecreaseBtn.addEventListener('click', () =>
419 FontScale.decrease()); 420// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 421 422window.addEventListener('keydown', (e) => { 423 if (e.key === 'Escape') { 424 if (UI.uidHelpModal && UI.uidHelpModal.style.display === 'flex') { 425 closeUidHelpModal(); 426 } else if (UI.legalModal && UI.legalModal.classList.contains('active')) { 427 closeLegalModal(); 428 } else if (UI.drawer.classList.contains('open')) { 429 closeDrawer(); 430 } 431 } 432}); 433 434// Chat Helpers 435function scrollChatToBottom() { 436 setTimeout(() => { 437 UI.chatStream.scrollTop = UI.chatStream.scrollHeight; 438 }, 50); 439} 440 441function showTypingIndicator() { 442 const typing = document.createElement('div'); 443 typing.className = 'typing-indicator-bubble'; 444 typing.id = 'active-typing'; 445 typing.innerHTML = ` 446 <span class="typing-dot"></span> 447 <span class="typing-dot"></span> 448 <span class="typing-dot"></span> 449 `; 450 UI.chatStream.appendChild(typing); 451 scrollChatToBottom(); 452} 453 454function hideTypingIndicator() { 455 const typing = document.getElementById('active-typing'); 456 if (typing) typing.remove(); 457} 458 459function appendBotMessage(htmlContent, delay = 500) { 460 return new Promise((resolve) => { 461 showTypingIndicator(); 462 setTimeout(() => { 463 hideTypingIndicator(); 464 const bubble = document.createElement('div'); 465 bubble.className = 'message-bubble bot-msg'; 466 bubble.innerHTML = htmlContent.replace(/\n/g, '<br>'); 467 UI.chatStream.appendChild(bubble); 468 scrollChatToBottom(); 469 AudioFX.playPop(); 470 resolve(bubble); 471 }, delay); 472 }); 473} 474 475function appendUserMessage(text) { 476 const bubble = document.createElement('div'); 477 bubble.className = 'message-bubble user-msg'; 478 bubble.textContent = text; 479 UI.chatStream.appendChild(bubble); 480 scrollChatToBottom(); 481 AudioFX.playPop(); 482} 483 484// API Communication 485async function syncSessionWithBackend() { 486 try { 487 const res = await fetch(getApiUrl('/api/session'), { 488 method: 'POST', 489 headers: { 'Content-Type': 'application/json' }, 490 body: JSON.stringify({ 491 session_id: FunnelState.sessionId, 492 language: FunnelState.language, 493 utm_source: FunnelState.utmSource || 'desktop_landing', 494 utm_campaign: FunnelState.utmCampaign, 495 utm_medium: FunnelState.utmMedium, 496 utm_term: FunnelState.utmTerm, 497 utm_content: FunnelState.utmContent, 498 ref: FunnelState.ref, 499 fbclid: FunnelState.fbclid, 500 gclid: FunnelState.gclid, 501 ttclid: FunnelState.ttclid 502 }) 503 }); 504 return await res.json(); 505 } catch (err) { 506 console.warn('Backend sync session fallback:', err); 507 } 508} 509 510async function fetchFunnelContent(lang = 'fr') { 511 try { 512 const res = await fetch(getApiUrl(`/api/funnel-data?lang=${lang}`)); 513 return await res.json(); 514 } catch(e) { 515 console.error('Failed to load funnel content:', e); 516 return null; 517 } 518} 519 520function updateLanguageUI() { 521 const flags = { fr: 'ð«ð·', en: 'ð¬ð§', es: 'ðªð¸' }; 522 UI.langFlag.textContent = flags[FunnelState.language] || 'ð«ð·'; 523 UI.langCode.textContent = FunnelState.language.toUpperCase(); 524} 525 526UI.langMenuBtn.addEventListener('click', () => { 527 const nextLang = FunnelState.language === 'fr' ? 'en' : FunnelState.language === 'en' ? 'es' : 'fr'; 528 setLanguage(nextLang); 529}); 530 531function restartBotToBeginning() { 532 AudioFX.playPop(); 533 534 // Clear any existing typing indicators or prompts 535 hideTypingIndicator(); 536 537 // Reset conversation stream 538 UI.chatStream.innerHTML = ''; 539 540 // Reset dynamic dock status 541 UI.inputDock.innerHTML = ` 542 <div class="dock-placeholder" id="dock-status"> 543 <span>Renseignez votre courriel ci-dessus pour débloquer le funnel</span> 544 </div> 545 `; 546 547 // Reset in-session progress attributes 548 FunnelState.weexUid = null; 549 FunnelState.bitunixUid = null; 550 FunnelState.tradingviewUsername = null; 551 FunnelState.profile = null; 552 FunnelState.existingProfile = null; 553 FunnelState.emailVerified = false;
554 555 // Flash Dynamic Island status 556 const islandText = document.querySelector('.island-text'); 557 if (islandText) { 558 const originalText = islandText.textContent; 559 islandText.textContent = '⺠Reset...'; 560 islandText.style.color = '#10b981'; 561 setTimeout(() => { 562 islandText.textContent = originalText; 563 islandText.style.color = ''; 564 }, 1800); 565 } 566 567 // Smoothly scroll chat container to top 568 UI.chatStream.scrollTop = 0; 569 570 // Start funnel afresh from Step 1 571 startFunnel(); 572} 573 574if (UI.restartBtn) UI.restartBtn.addEventListener('click', restartBotToBeginning); 575if (UI.drawerRestartBtn) UI.drawerRestartBtn.addEventListener('click', restartBotToBeginning); 576if (UI.dockRestartBtn) UI.dockRestartBtn.addEventListener('click', restartBotToBeginning); 577 578async function setLanguage(lang) { 579 FunnelState.language = lang; 580 localStorage.setItem('cq_web_lang', lang); 581 updateLanguageUI(); 582 FunnelState.funnelData = await fetchFunnelContent(lang); 583 UI.chatStream.innerHTML = ''; 584 startFunnel(); 585} 586 587 588// ========================================================================== 589// FUNNEL FLOW : EMAIL AS CORNERSTONE & 2FA OTP VIA AGENTMAIL 590// ========================================================================== 591 592async function checkAndResumeSession() { 593 const urlParams = new URLSearchParams(window.location.search); 594 const paramSessionId = urlParams.get('session_id'); 595 if (!paramSessionId) return false; 596 597 try { 598 const res = await fetch(getApiUrl(`/api/session-info?session_id=${paramSessionId}`)); 599 const data = await res.json(); 600 if (data.found && data.email && data.email_verified) { 601 FunnelState.sessionId = data.session_id; 602 FunnelState.email = data.email; 603 FunnelState.emailVerified = true; 604 FunnelState.language = data.language || 'fr'; 605 FunnelState.weexUid = data.weex_uid; 606 FunnelState.bitunixUid = data.bitunix_uid; 607 FunnelState.tradingviewUsername = data.tradingview_username; 608 FunnelState.profile = data.qualification_profile; 609 610 localStorage.setItem('cq_web_session_id', data.session_id); 611 localStorage.setItem('cq_web_email', data.email); 612 localStorage.setItem('cq_web_lang', FunnelState.language); 613 614 updateLanguageUI(); 615 FunnelState.funnelData = await fetchFunnelContent(FunnelState.language); 616 checkAndEnableVipBottomMenu(); 617 618 await appendBotMessage(`ð **Bon retour parmi nous !**\n\nVotre session pour <code>${data.email}</code> a été restaurée avec succès.\n\nVos accès réservés sont prêts à être activés :`); 619 620 if (!FunnelState.weexUid && !FunnelState.bitunixUid) { 621 step4_PartnerOffer(); 622 } else if (!FunnelState.tradingviewUsername) { 623 step6_TradingViewUsername(); 624 } else { 625 step7_TelegramVIPGateway(); 626 } 627 return true; 628 } 629 } catch(e) { 630 console.error('Session resume error:', e); 631 } 632 return false; 633} 634 635async function startFunnel() { 636 await syncSessionWithBackend(); 637 updateLanguageUI(); 638 FunnelState.funnelData = await fetchFunnelContent(FunnelState.language); 639 640 // Check if session can be resumed from magic rescue email link 641 const resumed = await checkAndResumeSession(); 642 if (resumed) return; 643 644 // Ãtape 1 : Demande de courriel (Pierre Angulaire) + Cases de consentement 645 await appendBotMessage(`ð **Bienvenue chez Crypto Québec !**\n\nPour sécuriser votre profil, débloquer les indicateurs TradingView et accéder à nos formations offertes, veuillez commencer par valider votre adresse courriel :`); 646 647 displayEmailCaptureCard(); 648} 649 650function displayEmailCaptureCard() { 651 const card = document.createElement('div'); 652 card.className = 'email-consent-card'; 653 card.id = 'email-capture-card'; 654 card.innerHTML = ` 655 <div class="consent-title"> 656 <span>ð</span> 657 <span>Validation Sécurisée par Courriel</span> 658 </div> 659 <div class="prompt-input-row" style="margin-bottom: 10px;"> 660 <input type="email" id="initial-email-input" class="prompt-input" placeholder="[email protected]" value="${FunnelState.email || ''}" autofocus> 661 </div> 662 663 <div class="consent-checkbox-list"> 664 <label class="consent-item"> 665 <input type="checkbox" id="chk-casl" class="consent-check-input">
666 <span class="consent-text"> 667 ð¨ð¦ <strong>Canada (LCAP) :</strong> J'accepte de recevoir la newsletter et communications de Crypto Québec. Désabonnement en 1 clic à tout moment. 668 </span> 669 </label> 670 671 <label class="consent-item"> 672 <input type="checkbox" id="chk-canspam" class="consent-check-input"> 673 <span class="consent-text"> 674 ðºð¸ <strong>USA (CAN-SPAM) :</strong> I agree to receive informational & trading updates from Crypto Québec. 675 </span> 676 </label> 677 678 <label class="consent-item"> 679 <input type="checkbox" id="chk-gdpr" class="consent-check-input"> 680 <span class="consent-text"> 681 ðªðº <strong>Europe (RGPD) :</strong> J'accepte le traitement de mes données personnelles conformément à la Politique de Confidentialité. 682 </span> 683 </label> 684 </div> 685 686 <div style="text-align: center; margin-bottom: 12px;"> 687 <span class="legal-popup-link" id="link-open-terms">ð Lire les Conditions Générales et Politique de Confidentialité</span> 688 </div> 689 690 <button id="btn-send-otp" class="prompt-submit-btn" style="width: 100%; padding: 10px; font-size: 12px;"> 691 Recevoir mon code de validation â 692 </button> 693 `; 694 695 UI.chatStream.appendChild(card); 696 scrollChatToBottom(); 697 698 // Wire Terms link 699 document.getElementById('link-open-terms').addEventListener('click', openLegalModal); 700 701 // Wire Send OTP Button 702 document.getElementById('btn-send-otp').addEventListener('click', handleSendOtp); 703 document.getElementById('initial-email-input').addEventListener('keydown', (e) => { 704 if (e.key === 'Enter') handleSendOtp(); 705 }); 706} 707 708// -------------------------------------------------------------------------- 709// TELEGRAM AUTH FLOW 710// -------------------------------------------------------------------------- 711 712function displayTelegramAuthCard() { 713 const oldCard = document.getElementById('email-capture-card'); 714 if (oldCard) oldCard.remove(); 715 716 const card = document.createElement('div'); 717 card.className = 'email-consent-card tg-auth-card'; 718 card.id = 'telegram-auth-card'; 719 card.innerHTML = ` 720 <div class="consent-title tg-title"> 721 <span class="tg-svg-icon-title"> 722 <svg viewBox="0 0 24 24" width="20" height="20"><path fill="#0088cc" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.75-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z"/></svg> 723 </span> 724 <span>Connexion Officielle Telegram</span> 725 </div> 726 <p class="tg-auth-desc"> 727 Identifiez-vous instantanément avec votre compte Telegram pour charger automatiquement vos formations, UIDs et accès VIP : 728 </p> 729 730 <!-- Option A: Code instantané envoyé par le Bot (Fonctionne 100% sur mobile & Tailscale) --> 731 <div class="tg-auth-mode-box"> 732 <label class="prompt-label">ð± Votre @pseudo ou ID Telegram</label> 733 <div class="prompt-input-row" style="margin-bottom: 8px;"> 734 <input type="text" id="tg-identifier-input" class="prompt-input" placeholder="@The_c1ark_Ken7 ou 7760324627" autofocus> 735 </div> 736 <button id="btn-send-tg-code" class="prompt-submit-btn tg-btn-submit" style="width: 100%; padding: 10px; font-size: 12px;"> 737 Recevoir mon code sur Telegram â 738 </button> 739 </div> 740 741 <!-- Option B: Widget Telegram officiel (si supporté par le domaine) --> 742 <div class="auth-divider-mini"> 743 <span>OU VIA LE WIDGET TELEGRAM</span> 744 </div> 745 <div id="tg-widget-container" class="tg-widget-box"></div> 746 747 <div style="text-align: center; margin-top: 14px;"> 748 <span class="legal-popup-link" id="link-back-email">âï¸ Revenir à la connexion par courriel</span> 749 </div> 750 `; 751 752 UI.chatStream.appendChild(card); 753 scrollChatToBottom(); 754 755 // Inject Telegram Widget script 756 try { 757 const script = document.createElement('script'); 758 script.async = true; 759 script.src = "https://telegram.org/js/telegram-widget.js?22"; 760 script.setAttribute('data-telegram-login', 'CryptoQc_Welcome_bot'); 761 script.setAttribute('data-size', 'large'); 762 script.setAttribute('data-radius', '10'); 763 script.setAttribute('data-onauth', 'onTelegramAuth(user)'); 764 script.setAttribute('data-request-access', 'write'); 765 const container = document.getElementById('tg-widget-container'); 766 if (container) container.appendChild(script); 767 } catch (e) { 768 console.warn('Could not inject Telegram widget:', e); 769 } 770 771 // Wire back link 772 document.getElementById('link-back-email').addEventListener('click', () => { 773 card.remove(); 774 displayEmailCaptureCard(); 775 }); 776 777 // Wire Send Telegram code 778 const inputEl = document.getElementById('tg-identifier-input'); 779 const btnEl = document.getElementById('btn-send-tg-code'); 780 781 btnEl.addEventListener('click', handleSendTelegramCode); 782 inputEl.addEventListener('keydown', (e) => { 783 if (e.key === 'Enter') handleSendTelegramCode(); 784 }); 785} 786 787async function handleSendTelegramCode() { 788 const inputEl = document.getElementById('tg-identifier-input'); 789 const val = (inputEl.value || '').trim(); 790 if (!val) { 791 alert('Veuillez entrer votre pseudo Telegram (@nom) ou votre ID Telegram.'); 792 return; 793 } 794 795 const card = document.getElementById('telegram-auth-card'); 796 const loadingPill = document.createElement('div'); 797 loadingPill.className = 'validation-pill valid-loading'; 798 loadingPill.innerHTML = `<span>â³</span><span>
798Recherche du compte Telegram et envoi du code...</span>`; 799 UI.chatStream.appendChild(loadingPill); 800 scrollChatToBottom(); 801 802 try { 803 const res = await fetch(getApiUrl('/api/auth/telegram-send-code'), { 804 method: 'POST', 805 headers: { 'Content-Type': 'application/json' }, 806 body: JSON.stringify({ 807 session_id: FunnelState.sessionId, 808 identifier: val 809 }) 810 }); 811 const data = await res.json(); 812 loadingPill.remove(); 813 814 if (!data.success) { 815 if (data.not_found) { 816 card.remove(); 817 await appendBotMessage(`â ï¸ **Compte Telegram non détecté dans notre bot.**\n\nPour pouvoir vous connecter, vous devez avoir démarré au moins une fois notre assistant officiel **@CryptoQc_Welcome_bot**.`); 818 const startBox = document.createElement('div'); 819 startBox.className = 'chat-btn-group'; 820 startBox.innerHTML = ` 821 <a href="${data.bot_url || 'https://cryptoqctools.com/go/web-button'}" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;"> 822 <span>ð Démarrer @CryptoQc_Welcome_bot</span> 823 </a> 824 <button id="retry-tg-btn" class="chat-btn chat-btn-secondary"> 825 <span>⺠Réessayer</span> 826 </button> 827 `; 828 UI.chatStream.appendChild(startBox); 829 scrollChatToBottom(); 830 document.getElementById('retry-tg-btn').addEventListener('click', () => { 831 startBox.remove(); 832 displayTelegramAuthCard(); 833 }); 834 } else { 835 alert(data.message || "Erreur lors de l'envoi du code."); 836 } 837 return; 838 } 839 840 // Success: code sent to their Telegram chat! 841 AudioFX.playPop(); 842 card.remove(); 843 displayTelegramOtpInputCard(data.telegram_id, data.first_name, data.username); 844 845 } catch(err) { 846 loadingPill.remove(); 847 console.error(err); 848 alert('Erreur réseau lors de la communication.'); 849 } 850} 851 852function displayTelegramOtpInputCard(telegramId, firstName, username) { 853 const card = document.createElement('div'); 854 card.className = 'email-consent-card tg-auth-card'; 855 card.id = 'tg-otp-card'; 856 card.innerHTML = ` 857 <div class="consent-title tg-title"> 858 <span class="tg-logo-glyph">âï¸</span> 859 <span>Code envoyé sur Telegram</span> 860 </div> 861 <p class="tg-auth-desc"> 862 Un code de sécurité à 4 chiffres vient d'être envoyé par <strong>@CryptoQc_Welcome_bot</strong> à <strong>${firstName}</strong> (@${username || telegramId}) : 863 </p> 864 865 <div class="otp-inputs-row" style="justify-content: center; gap: 10px; margin-bottom: 12px;"> 866 <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-1" autofocus> 867 <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-2"> 868 <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-3"> 869 <input type="text" class="otp-box tg-code-box" maxlength="1" id="tg-digit-4"> 870 </div> 871 872 <button id="btn-verify-tg-code" class="prompt-submit-btn tg-action-btn" style="width: 100%; padding: 10px; font-size: 12px;"> 873 Valider et se connecter â 874 </button> 875 876 <div style="text-align: center; margin-top: 10px;"> 877 <span class="legal-popup-link" id="btn-resend-tg-code">⺠Renvoyer un nouveau code</span> 878 </div> 879 `; 880 881 UI.chatStream.appendChild(card); 882 scrollChatToBottom(); 883 884 const boxes = [ 885 document.getElementById('tg-digit-1'), 886 document.getElementById('tg-digit-2'), 887 document.getElementById('tg-digit-3'), 888 document.getElementById('tg-digit-4') 889 ]; 890 891 boxes.forEach((b, idx) => { 892 b.addEventListener('input', (e) => { 893 if (e.target.value.length === 1 && idx < 3) { 894 boxes[idx + 1].focus(); 895 } 896 const full = boxes.map(bx => bx.value).join(''); 897 if (full.length === 4) handleVerifyTelegramCode(); 898 }); 899 b.addEventListener('keydown', (e) => { 900 if (e.key === 'Backspace' && !e.target.value && idx > 0) { 901 boxes[idx - 1].focus(); 902 } 903 if (e.key === 'Enter') handleVerifyTelegramCode(); 904 }); 905 }); 906 907 document.getElementById('btn-verify-tg-code').addEventListener('click', handleVerifyTelegramCode); 908 document.getElementById('btn-resend-tg-code').addEventListener('click', () => { 909 card.remove(); 910 displayTelegramAuthCard(); 911 }); 912 913 async function handleVerifyTelegramCode() { 914 const code = boxes.map(b => b.value).join('').trim(); 915 if (code.length < 4) { 916 alert('Veuillez entrer les 4 chiffres du code.'); 917 return; 918 } 919 920 card.remove(); 921 const loadingPill = document.createElement('div'); 922 loadingPill.className = 'validation-pill valid-loading'; 923 loadingPill.innerHTML = `<span>â³</span><span>
923Vérification du compte Telegram...</span>`; 924 UI.chatStream.appendChild(loadingPill); 925 scrollChatToBottom(); 926 927 try { 928 const res = await fetch(getApiUrl('/api/auth/telegram-verify-code'), { 929 method: 'POST', 930 headers: { 'Content-Type': 'application/json' }, 931 body: JSON.stringify({ 932 session_id: FunnelState.sessionId, 933 telegram_id: telegramId, 934 code: code 935 }) 936 }); 937 const data = await res.json(); 938 loadingPill.remove(); 939 940 if (!data.verified) { 941 alert(data.message || 'Code Telegram erroné.'); 942 displayTelegramOtpInputCard(telegramId, firstName, username); 943 return; 944 } 945 946 onTelegramLoginSuccess(data); 947 948 } catch(err) { 949 loadingPill.remove(); 950 console.error(err); 951 alert('Erreur serveur lors de la validation.'); 952 displayTelegramOtpInputCard(telegramId, firstName, username); 953 } 954 } 955} 956 957// Global Official Telegram Widget Callback 958window.onTelegramAuth = async function(user) { 959 console.log('Telegram Widget Auth received:', user); 960 const card = document.getElementById('telegram-auth-card'); 961 if (card) card.remove(); 962 963 const loadingPill = document.createElement('div'); 964 loadingPill.className = 'validation-pill valid-loading'; 965 loadingPill.innerHTML = `<span>â³</span><span>Authentification Telegram en cours...</span>`; 966 UI.chatStream.appendChild(loadingPill); 967 scrollChatToBottom(); 968 969 try { 970 const res = await fetch(getApiUrl('/api/auth/telegram-widget'), { 971 method: 'POST', 972 headers: { 'Content-Type': 'application/json' }, 973 body: JSON.stringify({ 974 session_id: FunnelState.sessionId, 975 ...user 976 }) 977 }); 978 const data = await res.json(); 979 loadingPill.remove(); 980 981 if (!data.verified) { 982 alert(data.message || 'Erreur lors de la validation Telegram.'); 983 displayTelegramAuthCard(); 984 return; 985 } 986 987 onTelegramLoginSuccess(data); 988 989 } catch (err) { 990 loadingPill.remove(); 991 console.error(err); 992 alert('Erreur de connexion au serveur.'); 993 } 994}; 995 996async function onTelegramLoginSuccess(data) { 997 AudioFX.playSuccess(); 998 FunnelState.telegramId = data.telegram_id; 999 FunnelState.telegramUsername = data.username; 1000 1001 // Flash Island status 1002 const islandText = document.querySelector('.island-text'); 1003 if (islandText) { 1004 islandText.textContent = `âï¸ ${data.first_name || 'Telegram'}`; 1005 islandText.style.color = '#38bdf8';
1006 } 1007 1008 const successPill = document.createElement('div'); 1009 successPill.className = 'validation-pill valid-success'; 1010 successPill.innerHTML = ` 1011 <span>â </span> 1012 <div> 1013 <strong>Authentifié avec succès via Telegram !</strong><br> 1014 <small>Membre : ${data.first_name || ''} (@${data.username || data.telegram_id})</small> 1015 </div> 1016 `; 1017 UI.chatStream.appendChild(successPill); 1018 1019 // If user already has profile details (UIDs, email, etc.) 1020 if (data.profile) { 1021 FunnelState.existingProfile = data.profile; 1022 if (data.profile.email) { 1023 FunnelState.email = data.profile.email; 1024 FunnelState.emailVerified = true; 1025 localStorage.setItem('cq_web_email', data.profile.email); 1026 } 1027 if (data.profile.weex_uid) { 1028 FunnelState.weexUid = String(data.profile.weex_uid).trim(); 1029 localStorage.setItem('cq_weex_uid', FunnelState.weexUid); 1030 } 1031 if (data.profile.bitunix_uid) { 1032 FunnelState.bitunixUid = String(data.profile.bitunix_uid).trim(); 1033 localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid); 1034 } 1035 if (data.profile.tradingview_username) { 1036 FunnelState.tradingviewUsername = data.profile.tradingview_username; 1037 localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername); 1038 } 1039 checkAndEnableVipBottomMenu(); 1040 displayRecognizedProfileCard(data.profile); 1041 } else if (data.has_email && data.email) { 1042 FunnelState.email = data.email; 1043 FunnelState.emailVerified = true; 1044 localStorage.setItem('cq_web_email', data.email); 1045 displayEmailCaptureCard(); 1046 } else { 1047 // New Telegram user or email missing 1048 await appendBotMessage(`ð **Bienvenue ${data.first_name || ''} !**\n\nVotre compte Telegram (@${data.username || data.telegram_id}) est vérifié.\n\nPour débloquer vos formations gratuites sur **Teachable** et vos indicateurs TradingView, veuillez renseigner votre courriel :`); 1049 displayEmailCaptureCard(); 1050 } 1051} 1052 1053async function handleSendOtp() { 1054 const emailInput = document.getElementById('initial-email-input'); 1055 const cleanEmail = (emailInput ? emailInput.value : (FunnelState.email || '')).trim().toLowerCase(); 1056 1057 if (!cleanEmail || !cleanEmail.includes('@') || !cleanEmail.includes('.')) { 1058 alert('Veuillez entrer une adresse courriel valide.'); 1059 return; 1060 } 1061 1062 const chkCasl = document.getElementById('chk-casl') ? document.getElementById('chk-casl').checked : false; 1063 const chkCanSpam = document.getElementById('chk-canspam') ? document.getElementById('chk-canspam').checked : false; 1064 const chkGdpr = document.getElementById('chk-gdpr') ? document.getElementById('chk-gdpr').checked : false; 1065 1066 // Si aucune case n'est cochée -> Deuxième chance explicative 1067 if (!chkCasl && !chkCanSpam && !chkGdpr) { 1068 displayConsentSecondChanceCard(cleanEmail); 1069 return; 1070 } 1071 1072 proceedSendOtp(cleanEmail, chkCasl, chkCanSpam, chkGdpr); 1073} 1074 1075function displayConsentSecondChanceCard(cleanEmail) { 1076 const oldCard = document.getElementById('email-capture-card'); 1077 if (oldCard) oldCard.remove(); 1078 1079 appendUserMessage(`Courriel : ${cleanEmail}`); 1080 1081 const card = document.createElement('div'); 1082 card.className = 'email-consent-card second-chance-card'; 1083 card.id = 'consent-second-chance-card'; 1084 card.innerHTML = ` 1085 <div class="consent-title" style="color: #f59e0b;"> 1086 <span>ð¢</span> 1087 <span>Restez connecté avec Crypto Québec</span> 1088 </div> 1089 <p class="second-chance-lead"> 1090 Vous n'avez coché aucune case de communication. Saviez-vous que <strong>c'est exclusivement de cette manière</strong> que Crypto Québec peut communiquer avec vous et vous transmettre : 1091 </p> 1092 1093 <div class="second-chance-benefits"> 1094 <div class="benefit-item"> 1095 <span class="benefit-icon">ð</span> 1096 <div class="benefit-text"> 1097 <strong>Analyses exclusives de marché</strong> 1098 <small>Points macro, niveaux clés Bitcoin, Ethereum & Altcoins</small> 1099 </div> 1100 </div> 1101 <div class="benefit-item"> 1102 <span class="benefit-icon">â¡</span> 1103 <div class="benefit-text"> 1104 <strong>Signaux de trading & alertes en direct</strong> 1105 <small>Configurations détectées par nos algorithmes propriétaires</small> 1106 </div> 1107 </div> 1108 <div class="benefit-item">
1109 <span class="benefit-icon">ð</span> 1110 <div class="benefit-text"> 1111 <strong>Mises à jour crypto & nouveaux indicateurs</strong> 1112 <small>La meilleure façon de rester en contact avec l'équipe et de ne rien manquer</small> 1113 </div> 1114 </div> 1115 </div> 1116 1117 <div class="second-chance-actions"> 1118 <button id="btn-second-chance-accept" class="second-chance-optin-btn"> 1119 â Oui, je souhaite recevoir les analyses & signaux â 1120 </button> 1121 <button id="btn-second-chance-skip" class="second-chance-skip-btn"> 1122 Continuer sans recevoir les emails d'analyses â 1123 </button> 1124 </div> 1125 1126 <div class="second-chance-tg-notice"> 1127 <div class="tg-notice-header"> 1128 <span>âï¸</span> 1129 <span>Communauté Officielle Telegram</span> 1130 </div> 1131 <p class="tg-notice-text"> 1132 Quel que soit votre choix, <strong>Telegram</strong> vous offre également la possibilité de suivre toute la communauté Crypto Québec en temps réel et d'échanger avec plus de 5,000 passionnés. 1133 </p> 1134 <div class="tg-dual-buttons-row"> 1135 <a href="https://cryptoqctools.com/go/web-button" target="_blank" class="tg-dual-btn tg-dual-telegram"> 1136 <span class="tg-btn-icon">âï¸</span> 1137 <span>Rejoindre sur Telegram</span> 1138 <span class="tg-btn-arrow">â</span> 1139 </a> 1140 <button id="btn-sc-continue-here" class="tg-dual-btn tg-dual-continue"> 1141 <span class="tg-btn-icon">ð¬</span> 1142 <span>Continuer sur ce bot</span> 1143 <span class="tg-btn-arrow">â</span> 1144 </button> 1145 </div> 1146 </div> 1147 `; 1148 1149 UI.chatStream.appendChild(card); 1150 scrollChatToBottom(); 1151 AudioFX.playPop(); 1152 1153 document.getElementById('btn-second-chance-accept').addEventListener('click', () => { 1154 card.remove(); 1155 proceedSendOtp(cleanEmail, true, true, true); 1156 }); 1157 1158 document.getElementById('btn-second-chance-skip').addEventListener('click', () => { 1159 card.remove(); 1160 proceedSendOtp(cleanEmail, false, false, false); 1161 }); 1162 1163 document.getElementById('btn-sc-continue-here').addEventListener('click', () => { 1164 card.remove(); 1165 proceedSendOtp(cleanEmail, false, false, false); 1166 }); 1167} 1168 1169async function proceedSendOtp(cleanEmail, chkCasl, chkCanSpam, chkGdpr) { 1170 FunnelState.email = cleanEmail; 1171 localStorage.setItem('cq_web_email', cleanEmail); 1172 trackFunnelEvent('email_submit', { 1173 domain: cleanEmail.split('@')[1] || '', 1174 casl: Boolean(chkCasl), 1175 canspam: Boolean(chkCanSpam), 1176 gdpr: Boolean(chkGdpr) 1177 }); 1178 1179 const oldCard = document.getElementById('email-capture-card'); 1180 if (oldCard) oldCard.remove(); 1181 const oldSecondChance = document.getElementById('consent-second-chance-card'); 1182 if (oldSecondChance) oldSecondChance.remove(); 1183 1184 // Loading pill 1185 const loadingPill = document.createElement('div'); 1186 loadingPill.className = 'validation-pill valid-loading'; 1187 loadingPill.innerHTML = `<span>â³</span><span>Envoi de votre code de validation...</span>`; 1188 UI.chatStream.appendChild(loadingPill); 1189 scrollChatToBottom(); 1190 1191 try { 1192 const res = await fetch(getApiUrl('/api/auth/send-otp'), { 1193 method: 'POST', 1194 headers: { 'Content-Type': 'application/json' }, 1195 body: JSON.stringify({ 1196 session_id: FunnelState.sessionId, 1197 email: cleanEmail, 1198 consent_casl: Boolean(chkCasl), 1199 consent_canspam: Boolean(chkCanSpam), 1200 consent_gdpr: Boolean(chkGdpr) 1201 }) 1202 }); 1203 const data = await res.json(); 1204 loadingPill.remove(); 1205 1206 await appendBotMessage(`ð **Code de validation envoyé !**\n\nNous venons de transmettre un code de sécurité à 6 chiffres à **${cleanEmail}**.\n\nVeuillez consulter votre boîte de réception et entrer le code ci-dessous :`); 1207 1208 displayOtpInputCard(); 1209 1210 // Information Telegram systématique (quel que soit le choix) 1211 const tgReminder = document.createElement('div'); 1212 tgReminder.className = 'second-chance-tg-notice'; 1213 tgReminder.style.margin = '10px 0 6px'; 1214 tgReminder.innerHTML = ` 1215 <div class="tg-notice-header">
1216 <span>âï¸</span> 1217 <span>Suivre la communauté en temps réel</span> 1218 </div> 1219 <p class="tg-notice-text"> 1220 Telegram vous offre également la possibilité de suivre toute la communauté Crypto Québec en temps réel pour des alertes instantanées. 1221 </p> 1222 <div class="tg-dual-buttons-row"> 1223 <a href="https://cryptoqctools.com/go/web-button" target="_blank" class="tg-dual-btn tg-dual-telegram"> 1224 <span class="tg-btn-icon">âï¸</span> 1225 <span>Rejoindre sur Telegram</span> 1226 <span class="tg-btn-arrow">â</span> 1227 </a> 1228 <button id="btn-reminder-continue" class="tg-dual-btn tg-dual-continue"> 1229 <span class="tg-btn-icon">ð¬</span> 1230 <span>Continuer sur ce bot</span> 1231 <span class="tg-btn-arrow">â</span> 1232 </button> 1233 </div> 1234 `; 1235 UI.chatStream.appendChild(tgReminder); 1236 scrollChatToBottom(); 1237 1238 const continueBtn = document.getElementById('btn-reminder-continue'); 1239 if (continueBtn) { 1240 continueBtn.addEventListener('click', () => { 1241 const codeInput = document.getElementById('otp-code-input'); 1242 if (codeInput) { 1243 codeInput.focus(); 1244 codeInput.scrollIntoView({ behavior: 'smooth', block: 'center' }); 1245 } 1246 }); 1247 } 1248 1249 } catch (err) { 1250 loadingPill.remove(); 1251 console.error('Error sending OTP:', err); 1252 alert('Erreur lors de l\'envoi du code. Veuillez réessayer.'); 1253 displayEmailCaptureCard(); 1254 } 1255} 1256 1257function displayOtpInputCard() { 1258 const card = document.createElement('div'); 1259 card.className = 'otp-input-card'; 1260 card.id = 'otp-input-card'; 1261 card.innerHTML = ` 1262 <div class="otp-title">Code de validation à 6 chiffres</div> 1263 <div class="otp-sub">Entrez le code reçu à <strong>${FunnelState.email}</strong></div> 1264 1265 <div style="display: flex; justify-content: center; gap: 8px; margin-bottom: 12px;"> 1266 <input type="text" id="otp-code-input" class="prompt-input" maxlength="6" placeholder="123456" style="text-align: center; font-size: 20px; letter-spacing: 6px; font-weight: 800; width: 180px; font-family: monospace;" autofocus> 1267 </div> 1268 1269 <button id="btn-verify-otp" class="prompt-submit-btn" style="width: 100%; padding: 10px;"> 1270 Valider le code â 1271 </button> 1272 1273 <div class="otp-resend-row"> 1274 Vous n'avez pas reçu le code ? 1275 <button id="btn-resend-otp" class="otp-resend-btn">Renvoyer</button> 1276 </div> 1277 `; 1278 1279 UI.chatStream.appendChild(card); 1280 scrollChatToBottom(); 1281 1282 const codeInput = document.getElementById('otp-code-input'); 1283 const verifyBtn = document.getElementById('btn-verify-otp'); 1284 const resendBtn = document.getElementById('btn-resend-otp'); 1285 1286 async function handleVerify() { 1287 const code = (codeInput.value || '').trim(); 1288 if (code.length !== 6 || isNaN(code)) { 1289 alert('Veuillez entrer les 6 chiffres du code.'); 1290 return; 1291 } 1292 1293 appendUserMessage(`Code : ${code}`); 1294 card.remove(); 1295 1296 const loadingPill = document.createElement('div'); 1297 loadingPill.className = 'validation-pill valid-loading'; 1298 loadingPill.innerHTML = `<span>â³</span><span>Vérification du code et recherche du profil...</span>`; 1299 UI.chatStream.appendChild(loadingPill); 1300 scrollChatToBottom(); 1301 1302 try { 1303 const res = await fetch(getApiUrl('/api/auth/verify-otp'), { 1304 method: 'POST', 1305 headers: { 'Content-Type': 'application/json' }, 1306 body: JSON.stringify({ 1307 session_id: FunnelState.sessionId, 1308 email: FunnelState.email, 1309 otp_code: code 1310 }) 1311 }); 1312 const data = await res.json(); 1313 loadingPill.remove(); 1314 1315 if (!data.verified) { 1316 alert(data.message || 'Code erroné ou expiré.'); 1317 displayOtpInputCard(); 1318 return; 1319 } 1320 1321 // OTP Validated! 1322 FunnelState.emailVerified = true; 1323 if (data.session_token) { 1324 FunnelState.sessionToken = data.session_token; 1325 localStorage.setItem('cq_session_token', data.session_token); 1326 } 1327 AudioFX.playSuccess(); 1328 trackAdEvent('Lead', { email: FunnelState.email }); 1329 trackFunnelEvent('otp_verified'); 1330 1331 const successPill = document.createElement('div'); 1332 successPill.className = 'validation-pill valid-success'; 1333 successPill.innerHTML = `
1334 <span>â </span> 1335 <div> 1336 <strong>Courriel validé avec succès !</strong><br> 1337 <small>Consentements archivés en conformité LCAP, CAN-SPAM et RGPD.</small> 1338 </div> 1339 `; 1340 UI.chatStream.appendChild(successPill); 1341 1342 // Check if user is recognized from existing DB 1343 if (data.profile) { 1344 FunnelState.existingProfile = data.profile; 1345 if (data.profile.weex_uid) { 1346 FunnelState.weexUid = String(data.profile.weex_uid).trim(); 1347 localStorage.setItem('cq_weex_uid', FunnelState.weexUid); 1348 } 1349 if (data.profile.bitunix_uid) { 1350 FunnelState.bitunixUid = String(data.profile.bitunix_uid).trim(); 1351 localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid); 1352 } 1353 if (data.profile.tradingview_username) { 1354 FunnelState.tradingviewUsername = data.profile.tradingview_username; 1355 localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername); 1356 } 1357 checkAndEnableVipBottomMenu(); 1358 } 1359 1360 if (data.is_existing && data.profile) { 1361 displayRecognizedProfileCard(data.profile); 1362 } else { 1363 await appendBotMessage(`ð **Votre profil est maintenant créé et sécurisé !**\n\nPoursuivons pour débloquer vos accès TradingView et vos formations offertes.`); 1364 step_LanguageSelector(); 1365 } 1366 } catch(err) { 1367 loadingPill.remove(); 1368 console.error('Error verifying OTP:', err); 1369 alert('Erreur serveur lors de la vérification.'); 1370 displayOtpInputCard(); 1371 } 1372 } 1373 1374 verifyBtn.addEventListener('click', handleVerify); 1375 codeInput.addEventListener('keydown', (e) => { 1376 if (e.key === 'Enter') handleVerify(); 1377 }); 1378 1379 resendBtn.addEventListener('click', () => { 1380 card.remove(); 1381 if (FunnelState.email) { 1382 proceedSendOtp(FunnelState.email, true, true, true); 1383 } else { 1384 displayEmailCaptureCard(); 1385 } 1386 }); 1387} 1388 1389// ========================================================================== 1390// VIP BOTTOM DOCK MENU (Demande d'Indicateurs & Accès Formations) 1391// ========================================================================== 1392 1393function hasValidatedUid() { 1394 if (FunnelState.weexUid && String(FunnelState.weexUid).trim()) return true; 1395 if (FunnelState.bitunixUid && String(FunnelState.bitunixUid).trim()) return true; 1396 if (FunnelState.existingProfile) { 1397 if (FunnelState.existingProfile.weex_uid && String(FunnelState.existingProfile.weex_uid).trim()) return true; 1398 if (FunnelState.existingProfile.bitunix_uid && String(FunnelState.existingProfile.bitunix_uid).trim()) return true; 1399 } 1400 const lsW = localStorage.getItem('cq_weex_uid'); 1401 if (lsW && lsW.trim()) { 1402 FunnelState.weexUid = lsW.trim(); 1403 return true; 1404 } 1405 const lsB = localStorage.getItem('cq_bitunix_uid'); 1406 if (lsB && lsB.trim()) { 1407 FunnelState.bitunixUid = lsB.trim(); 1408 return true; 1409 } 1410 return false; 1411} 1412 1413function getExistingUidSummary() { 1414 const weex = FunnelState.weexUid || (FunnelState.existingProfile && FunnelState.existingProfile.weex_uid); 1415 const bitunix = FunnelState.bitunixUid || (FunnelState.existingProfile && FunnelState.existingProfile.bitunix_uid); 1416 const parts = []; 1417 if (weex) parts.push(`WEEX (UID: <code>${weex}</code>)`); 1418 if (bitunix) parts.push(`Bitunix (UID: <code>${bitunix}</code>)`); 1419 return parts.length > 0 ? parts.join(' et ') : 'Compte Partenaire Validé'; 1420} 1421 1422function isValidatedMember() { 1423 return Boolean(FunnelState.emailVerified || FunnelState.telegramId); 1424} 1425 1426function checkAndEnableVipBottomMenu() { 1427 if (isValidatedMember() && hasValidatedUid()) { 1428 renderVipBottomMenu(); 1429 if (UI.drawerFeedBtn) UI.drawerFeedBtn.style.display = 'inline-flex'; 1430 if (UI.chatFeedBtn) UI.chatFeedBtn.style.display = 'inline-flex'; 1431 } 1432} 1433 1434function renderVipBottomMenu() { 1435 if (!UI.inputDock) return; 1436 1437 const currentUid = FunnelState.weexUid || FunnelState.bitunixUid || 1438 (FunnelState.existingProfile && (FunnelState.existingProfile.weex_uid || FunnelState.existingProfile.bitunix_uid)) || 'Validé'; 1439 1440 UI.inputDock.innerHTML = ` 1441 <div class="vip-dock-menu" id="vip-dock-menu"> 1442 <div class="vip-dock-header">
1443 <span class="vip-pill-badge">⨠ACCÃS VIP ACTIF</span> 1444 <span class="vip-uid-info">UID : <code>${currentUid}</code> ð¢</span> 1445 </div> 1446 <div class="vip-dock-buttons"> 1447 <button id="vip-btn-indicators" class="vip-dock-btn vip-indicators-btn" title="Faire une demande d'indicateurs TradingView"> 1448 <span class="vip-btn-icon">ð</span> 1449 <div class="vip-btn-text"> 1450 <strong>Indicateurs TV</strong> 1451 <small>Demande licences</small> 1452 </div> 1453 <span class="vip-btn-arrow">â</span> 1454 </button> 1455 1456 <button id="vip-btn-courses" class="vip-dock-btn vip-courses-btn" title="Accéder directement aux formations offertes"> 1457 <span class="vip-btn-icon">ð</span> 1458 <div class="vip-btn-text"> 1459 <strong>Formations</strong> 1460 <small>Teachable 0$</small> 1461 </div> 1462 <span class="vip-btn-arrow">â</span> 1463 </button> 1464 1465 <button id="vip-btn-feed" class="vip-dock-btn vip-feed-btn" title="Consulter les alertes Telegram VIP et les podcasts"> 1466 <span class="vip-btn-icon">ð¡</span> 1467 <div class="vip-btn-text"> 1468 <strong>Flux VIP</strong> 1469 <small>Telegram & Pods</small> 1470 </div> 1471 <span class="vip-btn-arrow">â</span> 1472 </button> 1473 </div> 1474 </div> 1475 `; 1476 1477 // Afficher les raccourcis dans les barres d'outils 1478 if (UI.drawerFeedBtn) UI.drawerFeedBtn.style.display = 'inline-flex'; 1479 if (UI.chatFeedBtn) UI.chatFeedBtn.style.display = 'inline-flex'; 1480 1481 // Wire bottom VIP buttons 1482 const btnInd = document.getElementById('vip-btn-indicators'); 1483 const btnCourses = document.getElementById('vip-btn-courses'); 1484 const btnFeed = document.getElementById('vip-btn-feed'); 1485 1486 if (btnInd) btnInd.addEventListener('click', triggerVipIndicatorRequest); 1487 if (btnCourses) btnCourses.addEventListener('click', triggerVipCoursesAccess); 1488 if (btnFeed) btnFeed.addEventListener('click', openVipFeedModal); 1489} 1490 1491async function triggerVipIndicatorRequest() { 1492 AudioFX.playPop(); 1493 appendUserMessage("Je souhaite faire ma demande d'indicateurs TradingView ð"); 1494 1495 const existingTv = FunnelState.tradingviewUsername || 1496 (FunnelState.existingProfile && FunnelState.existingProfile.tradingview_username); 1497 1498 if (existingTv) { 1499 await appendBotMessage(`ð **Attribution des Indicateurs TradingView VIP**\n\nVotre pseudo TradingView actuellement enregistré est **@${existingTv}**.\n\nNos algorithmes exclusifs (*Impulsion Scalping, Scanner MTF, Trend Index*) sont attribués directement sur ce compte.`); 1500 1501 const card = document.createElement('div'); 1502 card.className = 'chat-btn-group'; 1503 card.innerHTML = ` 1504 <button id="btn-confirm-tv" class="chat-btn chat-btn-primary"> 1505 <span>â Confirmer l'accès pour @${existingTv}</span> 1506 </button> 1507 <button id="btn-change-tv" class="chat-btn chat-btn-secondary"> 1508 <span>âï¸ Changer mon pseudo TradingView</span> 1509 </button> 1510 `; 1511 UI.chatStream.appendChild(card); 1512 scrollChatToBottom(); 1513 1514 document.getElementById('btn-confirm-tv').addEventListener('click', async () => { 1515 card.remove(); 1516 const loadingPill = document.createElement('div'); 1517 loadingPill.className = 'validation-pill valid-loading'; 1518 loadingPill.innerHTML = `<span>â³</span><span>Transmission de votre demande d'accès à nos administrateurs...</span>`; 1519 UI.chatStream.appendChild(loadingPill); 1520 scrollChatToBottom(); 1521 1522 try { 1523 await fetch(getApiUrl('/api/submit-tradingview'), { 1524 method: 'POST', 1525 headers: { 'Content-Type': 'application/json' }, 1526 body: JSON.stringify({ 1527 session_id: FunnelState.sessionId, 1528 tradingview_username: existingTv 1529 }) 1530 }); 1531 } catch(e) { 1532 console.error(e); 1533 } 1534 loadingPill.remove(); 1535 1536 AudioFX.playSuccess(); 1537 const successPill = document.createElement('div'); 1538 successPill.className = 'validation-pill valid-success'; 1539 successPill.innerHTML = `
1540 <span>ð¨</span> 1541 <div> 1542 <strong>Demande transmise avec succès !</strong><br> 1543 <small>Votre demande pour <code>@${existingTv}</code> est en cours de validation par nos administrateurs.</small> 1544 </div> 1545 `; 1546 UI.chatStream.appendChild(successPill); 1547 scrollChatToBottom(); 1548 1549 step7_TelegramVIPGateway(); 1550 }); 1551 document.getElementById('btn-change-tv').addEventListener('click', () => { 1552 card.remove(); 1553 step6_TradingViewUsername(); 1554 }); 1555 } else { 1556 step6_TradingViewUsername(); 1557 } 1558} 1559 1560async function triggerVipCoursesAccess() { 1561 AudioFX.playPop(); 1562 appendUserMessage("Je souhaite accéder à mes formations offertes ð"); 1563 1564 const courses = (FunnelState.existingProfile && FunnelState.existingProfile.teachable_courses) || []; 1565 const email = FunnelState.email || 1566 (FunnelState.existingProfile && FunnelState.existingProfile.email) || 'votre courriel vérifié'; 1567 1568 let coursesListHtml = ''; 1569 if (courses.length > 0) { 1570 coursesListHtml = courses.map(c => ` 1571 <div class="teachable-course-item"> 1572 <span class="chk">â</span> 1573 <span>${c}</span> 1574 </div> 1575 `).join(''); 1576 } else { 1577 coursesListHtml = ` 1578 <div class="teachable-course-item"> 1579 <span class="chk">â</span> 1580 <span>Formation Complète Trading Crypto Québec (Offerte 0$)</span> 1581 </div> 1582 <div class="teachable-course-item"> 1583 <span class="chk">â</span> 1584 <span>Masterclass Scalping & Indicateurs Algorithmiques</span> 1585 </div> 1586 `; 1587 } 1588 1589 const card = document.createElement('div'); 1590 card.className = 'teachable-hub-card'; 1591 card.innerHTML = ` 1592 <div class="recognized-hdr"> 1593 <span class="recognized-badge">ESPACE MEMBRE ACTIF</span> 1594 <span class="recognized-title">Vos Formations Offertes (0$)</span> 1595 </div> 1596 <p style="font-size: 11px; color: #94a3b8; margin: 6px 0 10px;"> 1597 Synchronisé avec le compte : <code>${email}</code> 1598 </p> 1599 <div class="teachable-course-list"> 1600 ${coursesListHtml} 1601 </div> 1602 <a href="https://formationcryptoquebec.com/sign_in" target="_blank" class="prompt-submit-btn" style="display: block; text-align: center; text-decoration: none; padding: 10px; font-size: 12px;"> 1603 ð Ouvrir mon Espace Académie Teachable â 1604 </a> 1605 `; 1606 UI.chatStream.appendChild(card); 1607 scrollChatToBottom(); 1608} 1609 1610function displayRecognizedProfileCard(profile) { 1611 if (profile.weex_uid) { 1612 FunnelState.weexUid = String(profile.weex_uid).trim(); 1613 localStorage.setItem('cq_weex_uid', FunnelState.weexUid); 1614 } 1615 if (profile.bitunix_uid) { 1616 FunnelState.bitunixUid = String(profile.bitunix_uid).trim(); 1617 localStorage.setItem('cq_bitunix_uid', FunnelState.bitunixUid); 1618 } 1619 if (profile.tradingview_username) { 1620 FunnelState.tradingviewUsername = profile.tradingview_username; 1621 localStorage.setItem('cq_tv_username', FunnelState.tradingviewUsername); 1622 } 1623 1624 checkAndEnableVipBottomMenu(); 1625 1626 const card = document.createElement('div'); 1627 card.className = 'recognized-profile-card'; 1628 card.innerHTML = ` 1629 <div class="recognized-hdr"> 1630 <span class="recognized-badge">COMPTE RETROUVÃ</span> 1631 <span class="recognized-title">Ravi de vous revoir ${profile.first_name || profile.username || 'Cher Membre'} !</span> 1632 </div> 1633 <div style="font-size: 11px; color: var(--text-secondary); line-height: 1.4;"> 1634 Votre profil Crypto Québec a été identifié dans notre base de données : 1635 </div> 1636 <div class="profile-pill-grid"> 1637 <div class="profile-pill-item"> 1638 Telegram : <strong>${profile.telegram_id ? `@${profile.username || profile.telegram_id}` : 'Non lié'}</strong> 1639 </div> 1640 <div class="profile-pill-item"> 1641 WEEX : <strong>${profile.weex_uid ? `${profile.weex_uid} (Validé ð¢)` : 'à renseigner'}</strong> 1642 </div> 1643 <div class="profile-pill-item"> 1644 Bitunix : <strong>${profile.bitunix_uid ? `${profile.bitunix_uid} (Validé ð¢)` : 'à renseigner'}</strong> 1645 </div> 1646 <div class="profile-pill-item"> 1647 Formations : <strong>${profile.teachable_courses ? profile.teachable_courses.length : (profile.teachable_verified ? 1 : 0)} active(s)</strong> 1648 </div> 1649 </div> 1650 `; 1651 1652 UI.chatStream.appendChild(card); 1653 scrollChatToBottom(); 1654 1655 setTimeout(() => { 1656 step_LanguageSelector(); 1657 }, 1000); 1658} 1659 1660 1661// ========================================================================== 1662// STEP: LANGUAGE SELECTOR & PROGRESSION (From Funnel 9) 1663// ========================================================================== 1664 1665async function step_LanguageSelector() { 1666 const data = FunnelState.funnelData; 1667 await appendBotMessage(data.welcome.select_language); 1668 1669 const langContainer = document.createElement('div'); 1670 langContainer.className = 'chat-btn-group'; 1671 langContainer.innerHTML = ` 1672 <button class="chat-btn" data-lang="fr">
1673 <span>ð«ð· Français</span> 1674 <span>â</span> 1675 </button> 1676 <button class="chat-btn" data-lang="en"> 1677 <span>ð¬ð§ English</span> 1678 <span>â</span> 1679 </button> 1680 <button class="chat-btn" data-lang="es"> 1681 <span>ðªð¸ Español</span> 1682 <span>â</span> 1683 </button> 1684 `; 1685 UI.chatStream.appendChild(langContainer); 1686 scrollChatToBottom(); 1687 1688 langContainer.querySelectorAll('button').forEach(btn => { 1689 btn.addEventListener('click', async (e) => { 1690 const selectedLang = e.currentTarget.getAttribute('data-lang'); 1691 appendUserMessage(e.currentTarget.textContent.trim()); 1692 langContainer.remove(); 1693 if (selectedLang !== FunnelState.language) { 1694 FunnelState.language = selectedLang; 1695 localStorage.setItem('cq_web_lang', selectedLang); 1696 updateLanguageUI(); 1697 FunnelState.funnelData = await fetchFunnelContent(selectedLang); 1698 } 1699 step2_IntroValue(); 1700 }); 1701 }); 1702} 1703 1704async function step2_IntroValue() { 1705 const data = FunnelState.funnelData; 1706 await appendBotMessage(data.intro.text); 1707 step3_Qualification(); 1708} 1709 1710async function step3_Qualification() { 1711 const data = FunnelState.funnelData; 1712 await appendBotMessage(data.qualification.question); 1713 1714 const qualGroup = document.createElement('div'); 1715 qualGroup.className = 'chat-btn-group'; 1716 1717 data.qualification.options.forEach(opt => { 1718 const btn = document.createElement('button'); 1719 btn.className = 'chat-btn'; 1720 btn.innerHTML = ` 1721 <div> 1722 <div style="font-weight: 700; color: #fff;">${opt.title}</div> 1723 <div style="font-size: 11px; color: #94a3b8; font-weight: normal; margin-top: 2px;">${opt.desc}</div> 1724 </div> 1725 <span style="color: var(--cq-emerald); font-weight: bold;">â</span> 1726 `; 1727 btn.addEventListener('click', async () => { 1728 trackFunnelEvent('profile_select', { profile_id: opt.id, profile_title: opt.title }); 1729 appendUserMessage(opt.title); 1730 qualGroup.remove(); 1731 FunnelState.profile = opt.id; 1732 1733 fetch(getApiUrl('/api/qualify'), { 1734 method: 'POST', 1735 headers: { 'Content-Type': 'application/json' }, 1736 body: JSON.stringify({ 1737 session_id: FunnelState.sessionId, 1738 profile_type: opt.id 1739 }) 1740 }); 1741 1742 const pathInfo = data.paths[opt.id] || data.paths['debutant']; 1743 await appendBotMessage(`⨠**${pathInfo.title}**\n\n${pathInfo.text}`); 1744 1745 // VÃRIFICATION D'UID EXISTANT : Ne pas redemander si déjà présent 1746 if (hasValidatedUid()) { 1747 const summary = getExistingUidSummary(); 1748 await appendBotMessage(`â **Compte Partenaire Déjà Enregistré !**\n\nVotre compte **${summary}** est déjà associé et validé dans notre base de données.\n\nIl n'est pas nécessaire de le redemander. Vos accès VIP sont directement débloqués ! ð`); 1749 checkAndEnableVipBottomMenu(); 1750 step5_TeachableActivation(); 1751 } else { 1752 step4_PartnerOffer(); 1753 } 1754 }); 1755 qualGroup.appendChild(btn); 1756 }); 1757 1758 UI.chatStream.appendChild(qualGroup); 1759 scrollChatToBottom(); 1760} 1761 1762async function step4_PartnerOffer() { 1763 const data = FunnelState.funnelData; 1764 if (hasValidatedUid()) { 1765 const summary = getExistingUidSummary(); 1766 await appendBotMessage(`â **Compte Partenaire Actif :** Vos accès **${summary}** sont déjà enregistrés sur votre profil. Inutile de les renseigner à nouveau.`); 1767 checkAndEnableVipBottomMenu(); 1768 step5_TeachableActivation(); 1769 return; 1770 } 1771 await appendBotMessage(data.partners.text); 1772 1773 const partnersContainer = document.createElement('div'); 1774 partnersContainer.className = 'system-card'; 1775 partnersContainer.innerHTML = ` 1776 <!-- WEEX Card --> 1777 <div class="exchange-select-card"> 1778 <div class="ex-card-hdr"> 1779 <span class="ex-name">${data.partners.weex.name}</span> 1780 <span class="ex-code-pill" title="Cliquez pour copier">CODE : ${data.partners.weex.code} ð</span> 1781 </div> 1782 <div style="font-size: 10px; color: var(--cq-emerald); font-weight: 700; margin-bottom: 4px;"> 1783 ${data.partners.weex.badge} 1784 </div> 1785 <div class="ex-desc">${data.partners.weex.description}</div> 1786 <div class="ex-card-actions"> 1787 <a href="${data.partners.weex.link}" target="_blank" class="ex-link-btn" data-partner="weex">Créer compte WEEX â</a> 1788 <button class="ex-validate-btn" id="btn-input-weex">Valider mon UID â¡</button> 1789 </div> 1790 </div> 1791 1792 <!-- Bitunix Card --> 1793 <div class="exchange-select-card"> 1794 <div class="ex-card-hdr">
1795 <span class="ex-name">${data.partners.bitunix.name}</span> 1796 <span class="ex-code-pill" title="Cliquez pour copier">CODE : ${data.partners.bitunix.code} ð</span> 1797 </div> 1798 <div style="font-size: 10px; color: var(--cq-purple); font-weight: 700; margin-bottom: 4px;"> 1799 ${data.partners.bitunix.badge} 1800 </div> 1801 <div class="ex-desc">${data.partners.bitunix.description}</div> 1802 <div class="ex-card-actions"> 1803 <a href="${data.partners.bitunix.link}" target="_blank" class="ex-link-btn" data-partner="bitunix">Créer compte Bitunix â</a> 1804 <button class="ex-validate-btn" style="background: var(--cq-purple); color: #fff;" id="btn-input-bitunix">Valider mon UID â¡</button> 1805 </div> 1806 </div> 1807 `; 1808 1809 UI.chatStream.appendChild(partnersContainer); 1810 scrollChatToBottom(); 1811 1812 partnersContainer.querySelectorAll('.ex-link-btn').forEach(link => { 1813 link.addEventListener('click', () => { 1814 const partner = link.getAttribute('data-partner') || (link.href.includes('weex') ? 'weex' : 'bitunix'); 1815 trackFunnelEvent('partner_click', { partner: partner, url: link.href }); 1816 }); 1817 }); 1818 1819 partnersContainer.querySelectorAll('.ex-code-pill').forEach(pill => { 1820 pill.addEventListener('click', (e) => { 1821 const text = e.target.textContent.includes('QUEBEC') ? 'QUEBEC' : 'gwzp'; 1822 navigator.clipboard.writeText(text); 1823 const original = e.target.textContent; 1824 e.target.textContent = 'COPIà ! â '; 1825 setTimeout(() => e.target.textContent = original, 1500); 1826 }); 1827 }); 1828 1829 document.getElementById('btn-input-weex').addEventListener('click', () => { 1830 trackFunnelEvent('uid_prompt_open', { partner: 'weex' }); 1831 partnersContainer.remove(); 1832 promptUidInput('weex'); 1833 }); 1834 1835 document.getElementById('btn-input-bitunix').addEventListener('click', () => { 1836 trackFunnelEvent('uid_prompt_open', { partner: 'bitunix' }); 1837 partnersContainer.remove(); 1838 promptUidInput('bitunix'); 1839 }); 1840} 1841 1842function renderAffiliationRecoveryCard(exchange, uid, dataRes) { 1843 const exName = exchange === 'weex' ? 'WEEX' : 'Bitunix'; 1844 const code = exchange === 'weex' ? 'QUEBEC' : 'gwzp'; 1845 const supportUrl = exchange === 'weex' ? 'https://support.weex.com' : 'https://bitunix.zendesk.com/hc/fr'; 1846 const prewrittenMsg = `Bonjour, je souhaite lier mon compte au partenaire officiel Crypto Québec (Code: ${code}). Pouvez-vous rattacher mon UID (${uid}) s'il vous plaît ?`; 1847 1848 const card = document.createElement('div'); 1849 card.className = 'affiliation-recovery-card'; 1850 card.innerHTML = ` 1851 <div class="recovery-header"> 1852 <span class="recovery-icon">â ï¸</span> 1853 <div class="recovery-title">Compte ${exName} non rattaché au code officiel (${code})</div> 1854 </div> 1855 <div class="recovery-desc"> 1856 Votre UID <code>${uid}</code> est bien enregistré. Cependant, pour activer automatiquement vos indicateurs TradingView et formations offertes, votre compte doit être lié à <strong>Crypto Québec</strong>. 1857 </div> 1858 1859 <div class="recovery-options-grid"> 1860 <!-- Option 1 : Demande de transfert 1-clic --> 1861 <div class="recovery-option-box"> 1862 <div class="recovery-option-title"> 1863 <span>â¡ Option 1 : Lier mon compte existant via le Support</span> 1864 </div> 1865 <div class="recovery-option-text"> 1866 Copiez ce message et collez-le au Live Chat du support ${exName} : 1867 </div> 1868 <div class="recovery-copy-row"> 1869 <div class="recovery-msg-box" id="msg-to-copy">${prewrittenMsg}</div> 1870 <button type="button" class="recovery-copy-btn" id="btn-copy-support-msg">ð Copier</button> 1871 </div> 1872 </div> 1873 1874 <!-- Option 2 : Créer un sous-compte --> 1875 <div class="recovery-option-box"> 1876 <div class="recovery-option-title"> 1877 <span>ð¥ Option 2 : Créer un Sous-Compte Rattaché (30 sec)</span> 1878 </div> 1879 <div class="recovery-option-text"> 1880 Dans votre appli ${exName}, créez un sous-compte avec le code <code>${code}</code> sans avoir à refaire de KYC complet. 1881 </div> 1882 </div> 1883 </div> 1884 1885 <div class="recovery-actions-group"> 1886 <a href="${supportUrl}" target="_blank" class="recovery-support-btn">
1887 <span>ð¬ Ouvrir le Support ${exName} â</span> 1888 </a> 1889 <div class="recovery-alt-buttons"> 1890 <button type="button" class="recovery-retry-btn" id="btn-recovery-retry"> 1891 ð Saisir un autre UID 1892 </button> 1893 <button type="button" class="recovery-continue-btn" id="btn-recovery-continue"> 1894 Continuer en attente â 1895 </button> 1896 </div> 1897 </div> 1898 `; 1899 1900 UI.chatStream.appendChild(card); 1901 scrollChatToBottom(); 1902 1903 // Wire copy button 1904 const copyBtn = card.querySelector('#btn-copy-support-msg'); 1905 if (copyBtn) { 1906 copyBtn.addEventListener('click', () => { 1907 navigator.clipboard.writeText(prewrittenMsg); 1908 copyBtn.textContent = 'Copié ! â '; 1909 setTimeout(() => copyBtn.textContent = 'ð Copier', 2000); 1910 }); 1911 } 1912 1913 // Wire retry button 1914 card.querySelector('#btn-recovery-retry').addEventListener('click', () => { 1915 card.remove(); 1916 promptUidInput(exchange); 1917 }); 1918 1919 // Wire continue button 1920 card.querySelector('#btn-recovery-continue').addEventListener('click', () => { 1921 card.remove(); 1922 step5_TeachableActivation(); 1923 }); 1924} 1925 1926function promptUidInput(exchange) { 1927 const data = FunnelState.funnelData; 1928 const exName = exchange === 'weex' ? 'WEEX' : 'Bitunix'; 1929 const code = exchange === 'weex' ? 'QUEBEC' : 'gwzp'; 1930 1931 const box = document.createElement('div'); 1932 box.className = 'input-prompt-box'; 1933 box.innerHTML = ` 1934 <label class="prompt-label">Saisie d'UID Partenaire ${exName} (Code ${code})</label> 1935 <div class="prompt-input-row"> 1936 <input type="text" id="uid-input" class="prompt-input" placeholder="Ex: 88472910" autofocus> 1937 <button id="submit-uid-btn" class="prompt-submit-btn">Vérifier â</button> 1938 </div> 1939 <div style="margin-top: 4px; text-align: left;"> 1940 <button type="button" class="btn-uid-help" id="btn-show-uid-help">ð Où trouver mon UID ? (Aide en 3 sec)</button> 1941 </div> 1942 `; 1943 UI.chatStream.appendChild(box); 1944 scrollChatToBottom(); 1945 1946 const helpBtn = box.querySelector('#btn-show-uid-help'); 1947 if (helpBtn) { 1948 helpBtn.addEventListener('click', () => { 1949 openUidHelpModal(exchange); 1950 }); 1951 } 1952 1953 const input = document.getElementById('uid-input'); 1954 const btn = document.getElementById('submit-uid-btn'); 1955 1956 async function handleSubmit() { 1957 const rawVal = input.value.trim(); 1958 if (!rawVal) return; 1959 appendUserMessage(`Mon UID ${exName} : ${rawVal}`); 1960 box.remove(); 1961 1962 const loadingPill = document.createElement('div'); 1963 loadingPill.className = 'validation-pill valid-loading'; 1964 loadingPill.innerHTML = `<span>â³</span><span>Interrogation en temps réel de l'API Partner ${exName}...</span>`; 1965 UI.chatStream.appendChild(loadingPill); 1966 scrollChatToBottom(); 1967 1968 try { 1969 const res = await fetch(getApiUrl('/api/validate-uid'), { 1970 method: 'POST', 1971 headers: { 'Content-Type': 'application/json' }, 1972 body: JSON.stringify({ 1973 session_id: FunnelState.sessionId, 1974 exchange: exchange, 1975 uid: rawVal, 1976 email: FunnelState.email 1977 }) 1978 }); 1979 const dataRes = await res.json(); 1980 loadingPill.remove(); 1981 1982 if (exchange === 'weex') { 1983 FunnelState.weexUid = rawVal; 1984 localStorage.setItem('cq_weex_uid', rawVal); 1985 } else if (exchange === 'bitunix') { 1986 FunnelState.bitunixUid = rawVal; 1987 localStorage.setItem('cq_bitunix_uid', rawVal); 1988 } 1989 checkAndEnableVipBottomMenu(); 1990 1991 if (dataRes.validation_status === 'valide' || dataRes.validation_status === 'recu') { 1992 AudioFX.playSuccess(); 1993 const activeEx = dataRes.exchange || exchange; 1994 trackAdEvent('CompleteRegistration', { exchange: activeEx, uid: rawVal }); 1995 const partnerCode = dataRes.partner_code || (activeEx === 'weex' ? 'QUEBEC' : 'gwzp'); 1996 1997 if (dataRes.validation_status === 'valide') { 1998 FunnelState.vipApproved = true; 1999 localStorage.setItem('cq_vip_approved', '1'); 2000 checkAndEnableVipBottomMenu(); 2001 2002 if (dataRes.cross_checked) { 2003 const crossExName = activeEx === 'weex' ? 'WEEX' : 'Bitunix'; 2004 await appendBotMessage(`ð **Plateforme détectée automatiquement : ${crossExName} !**\n\nVotre UID a été validé avec succès sous notre code partenaire officiel \`${partnerCode}\`.`); 2005 } 2006 // Format bot identique : Accès VIP Débloqué & Auto-Approuvé 2007 await appendBotMessage(`ð **ACCÃS VIP DÃBLOQUà & AUTO-APPROUVà !**\n\nFélicitations, votre affiliation partenaire a été confirmée en temps réel. Votre accès VIP est immédiatement activé ! Voici vos liens officiels aux Canaux VIP Crypto Québec :`); 2008 const vipCard = document.createElement('div'); 2009 vipCard.className = 'chat-btn-group'; 2010 vipCard.style.display = 'flex'; 2011 vipCard.style.flexDirection = 'column'; 2012 vipCard.style.gap = '8px'; 2013 vipCard.innerHTML = ` 2014 <a href="https://t.me/+XwuXH4tVr-I3MTBh" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;">
2015 <span>ð ð«ð· Rejoindre le Canal VIP (Français)</span> 2016 </a> 2017 <a href="https://t.me/+RVyXd4wpCIhhNzYx" target="_blank" class="chat-btn chat-btn-secondary" style="text-decoration: none;"> 2018 <span>ð ð¬ð§ Join VIP Channel (English)</span> 2019 </a> 2020 `; 2021 UI.chatStream.appendChild(vipCard); 2022 scrollChatToBottom(); 2023 } else { 2024 // Format bot standard : Statut reçu / En cours de validation 2025 await appendBotMessage(dataRes.message || `â **UID ${activeEx.toUpperCase()} (${rawVal}) Enregistré !**\n\nStatut : \`recu\` (En cours de validation par nos administrateurs).\nVous recevrez une notification dès que votre accès VIP sera débloqué !`); 2026 const noticePill = document.createElement('div'); 2027 noticePill.className = 'validation-pill valid-loading'; 2028 noticePill.innerHTML = ` 2029 <span>â³</span> 2030 <div> 2031 <strong>Demande transmise à nos administrateurs</strong><br> 2032 <small>UID ${rawVal} (${activeEx.toUpperCase()}) ⢠Code partenaire : <code>${partnerCode}</code></small> 2033 </div> 2034 `; 2035 UI.chatStream.appendChild(noticePill); 2036 scrollChatToBottom(); 2037 } 2038 2039 setTimeout(() => { 2040 step5_TeachableActivation(); 2041 }, 1200); 2042 } else { 2043 // Affiliation Recovery Card 2044 renderAffiliationRecoveryCard(exchange, rawVal, dataRes); 2045 } 2046 } catch(err) { 2047 loadingPill.remove(); 2048 console.error(err); 2049 step5_TeachableActivation(); 2050 } 2051 } 2052 2053 btn.addEventListener('click', handleSubmit); 2054 input.addEventListener('keydown', (e) => { 2055 if (e.key === 'Enter') handleSubmit(); 2056 }); 2057} 2058 2059// âââ Step 5: Choix et Activation des Formations Teachable âââââââââââââââââââââââ 2060async function step5_TeachableActivation() { 2061 await appendBotMessage(`ð **Choisissez vos Formations Offertes (0$ VIP)**\n\nEn tant que membre de l'équipe Crypto Québec, vous avez accès gratuitement à nos formations spécialisées (valeur de plus de 2,000$).\n\nActivez les cours de votre choix ci-dessous :`); 2062 2063 const coursesContainer = document.createElement('div'); 2064 coursesContainer.className = 'teachable-catalog-container'; 2065 coursesContainer.id = 'teachable-catalog-container'; 2066 coursesContainer.innerHTML = ` 2067 <div class="catalog-loading-spinner" style="text-align: center; padding: 20px; color: #94a3b8; font-size: 12px;"> 2068 <span>â³ Chargement de l'Académie Crypto Québec...</span> 2069 </div> 2070 `; 2071 UI.chatStream.appendChild(coursesContainer); 2072 scrollChatToBottom(); 2073 2074 let courses = []; 2075 try { 2076 const res = await fetch(getApiUrl('/api/teachable/catalog')); 2077 const data = await res.json(); 2078 if (data.success && Array.isArray(data.courses)) { 2079 courses = data.courses; 2080 } 2081 } catch(e) { 2082 console.warn('Fallback teachable catalog:', e); 2083 } 2084 2085 // Fallback direct si endpoint temporairement inaccessible 2086 if (courses.length === 0) { 2087 courses = [ 2088 { id: "2810752", title: "L'Indicateur Crypto Québec Impulsion Scalping", short_title: "Impulsion Scalping", icon: "ð¯", value: "GRATUIT (valeur de 397 $)", desc: "Algorithme propriétaire pour scalping rapide avec signaux d'impulsion clairs.", url: "https://crypto-quebec.teachable.com/courses/2810752" }, 2089 { id: "2733704", title: "L'Art du Trading sur Futures : De Débutant à Expert", short_title: "Futures Trading Pro", icon: "ð", value: "GRATUIT (valeur de 497 $)", desc: "Maîtrise complète du levier, gestion du risque et stratégies Futures.", url: "https://crypto-quebec.teachable.com/courses/2733704" }, 2090 { id: "2704829", title: "Stratégie de Confluences Statistiques", short_title: "Confluences Statistiques", icon: "â¡", value: "GRATUIT (valeur de 347 $)", desc: "Méthode rigoureuse d'association de signaux techniques et de calcul de ratio R:R.", url: "https://crypto-quebec.teachable.com/courses/2704829" }, 2091 { id: "2725787", title: "AI AGENTS Académie", short_title: "AI AGENTS Académie", icon: "ð¤", value: "GRATUIT (valeur de 697 $)", desc: "Déploiement et automatisation d'agents d'IA appliqués au trading.", url: "https://crypto-quebec.teachable.com/courses/2725787" }, 2092 { id: "2592696", title: "Meme Coin Domination Académie", short_title: "Meme Coin Académie", icon: "ð", value: "GRATUIT (valeur de 497 $)", desc: "Stratég
2092ies d'identification et de sniping des tokens viraux.", url: "https://crypto-quebec.teachable.com/courses/2592696" }, 2093 { id: "2547792", title: "Modélisation des Inflexions & Cycles Majeurs", short_title: "Inflexions & Cycles", icon: "ð", value: "GRATUIT (valeur de 297 $)", desc: "Lecture avancée des cycles de marché et identification des retournements majeurs.", url: "https://crypto-quebec.teachable.com/courses/2547792" }, 2094 { id: "2781029", title: "Pack Indicateurs Crypto Québec", short_title: "Pack Indicateurs CQ", icon: "ð¦", value: "GRATUIT (valeur de 197 $)", desc: "Prise en main guidée des indicateurs et templates officiels.", url: "https://crypto-quebec.teachable.com/courses/2781029" }, 2095 { id: "2514623", title: "Cours de Trading Crypto - Pour Débutant", short_title: "Trading Débutant", icon: "ð±", value: "GRATUIT (valeur de 147 $)", desc: "Bases solides : vocabulaire, ordres limites/market, gestion du capital.", url: "https://crypto-quebec.teachable.com/courses/2514623" }, 2096 { id: "2791297", title: "Formation Trading Gratuite", short_title: "Formation Fondations", icon: "ð", value: "GRATUIT", desc: "Fondations et principes clés de l'écosystème Crypto Québec.", url: "https://crypto-quebec.teachable.com/courses/2791297" } 2097 ]; 2098 } 2099 2100 // Render cards 2101 coursesContainer.innerHTML = ` 2102 <div class="courses-grid" style="display: flex; flex-direction: column; gap: 8px; margin: 10px 0;"> 2103 ${courses.map(c => ` 2104 <div class="course-picker-card" id="course-card-${c.id}" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px; display: flex; flex-direction: column; gap: 6px;"> 2105 <div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;"> 2106 <div style="display: flex; align-items: center; gap: 8px;"> 2107 <span style="font-size: 20px;">${c.icon}</span> 2108 <div> 2109 <div style="font-size: 12px; font-weight: 700; color: #fff;">${c.short_title || c.title}</div> 2110 <div style="font-size: 9px; font-weight: 700; color: #10b981; letter-spacing: 0.5px;">${c.value}</div> 2111 </div> 2112 </div> 2113 <button class="btn-unlock-course" data-id="${c.id}" data-url="${c.url}" style="background: rgba(16,185,129,0.15); border: 1px solid #10b981; color: #10b981; padding: 5px 9px; border-radius: 6px; font-size: 10px; font-weight: 700; cursor: pointer; white-space: nowrap;"> 2114 ð Débloquer (0$) 2115 </button> 2116 </div> 2117 <div style="font-size: 10px; color: #94a3b8; line-height: 1.35;">${c.desc}</div> 2118 </div> 2119 `).join('')} 2120 </div> 2121 <div style="margin-top: 12px;"> 2122 <button id="btn-done-courses" class="prompt-submit-btn" style="width: 100%; padding: 11px; font-size: 12px; font-weight: 700;"> 2123 Passer au choix de mes indicateurs TradingView â 2124 </button> 2125 </div> 2126 `; 2127 scrollChatToBottom(); 2128 2129 // Wire unlock buttons 2130 coursesContainer.querySelectorAll('.btn-unlock-course').forEach(btn => { 2131 btn.addEventListener('click', async (e) => { 2132 const b = e.currentTarget; 2133 const cId = b.getAttribute('data-id'); 2134 const cUrl = b.getAttribute('data-url'); 2135 b.disabled = true; 2136 b.textContent = 'â³ Activation...'; 2137 2138 try { 2139 await fetch(getApiUrl('/api/validate-teachable'), { 2140 method: 'POST', 2141 headers: { 'Content-Type': 'application/json' }, 2142 body: JSON.stringify({ 2143 session_id: FunnelState.sessionId, 2144 email: FunnelState.email, 2145 course_id: cId 2146 }) 2147 }); 2148 } catch(err) {} 2149 2150 AudioFX.playSuccess(); 2151 b.style.background = '#10b981'; 2152 b.style.color = '#000'; 2153 b.style.borderColor = '#10b981'; 2154 b.innerHTML = `â Débloqué â Ouvrir`; 2155 b.onclick = () => window.open(cUrl, '_blank'); 2156 }); 2157 }); 2158 2159 // Wire continue button 2160 document.getElementById('btn-done-courses').addEventListener('click', () => { 2161 coursesContainer.style.opacity = '0.7'; 2162 step6_TradingViewUsername(); 2163 }); 2164} 2165 2166// âââ Step 6: Nom TradingView & Sélecteur Interactif d'Indicateurs (Max 5) ââââââââ 2167async function step6_TradingViewUsername() { 2168 const existingTv = FunnelState.tradingviewUsername || 2169 (FunnelState.existingProfile && FunnelState.existingProfile.tradingview_username); 2170 2171 if (existingTv) { 2172 await appendBotMessage(`ð **Compte TradingView Actuel :**\n\nVotre pseudo enregistré est **@${existingTv}**.\n\nVoulez-vous choisir vos 5 indicateurs pour ce compte ?`); 2173 const confirmCard = document.createElement('div'); 2174 confirmCard.className = 'chat-btn-group'; 2175 confirmCard.innerHTML = ` 2176 <button id="btn-keep-tv" class="chat-btn chat-btn-primary">
2177 <span>â Choisir mes indicateurs pour @${existingTv}</span> 2178 </button> 2179 <button id="btn-change-tv" class="chat-btn chat-btn-secondary"> 2180 <span>âï¸ Changer de pseudo TradingView</span> 2181 </button> 2182 `; 2183 UI.chatStream.appendChild(confirmCard); 2184 scrollChatToBottom(); 2185 2186 document.getElementById('btn-keep-tv').addEventListener('click', () => { 2187 confirmCard.remove(); 2188 renderInteractiveIndicatorPicker(existingTv); 2189 }); 2190 document.getElementById('btn-change-tv').addEventListener('click', () => { 2191 confirmCard.remove(); 2192 renderTradingViewUsernamePrompt(); 2193 }); 2194 return; 2195 } 2196 2197 renderTradingViewUsernamePrompt(); 2198} 2199 2200function renderTradingViewUsernamePrompt() { 2201 const data = FunnelState.funnelData; 2202 appendBotMessage(`âï¸ **Saisie de votre Username TradingView**\n\nEntrez votre nom d'utilisateur TradingView ci-dessous pour nous permettre de vous accorder l'accès direct aux indicateurs algorithmiques :`); 2203 2204 const tvBox = document.createElement('div'); 2205 tvBox.className = 'input-prompt-box'; 2206 tvBox.innerHTML = ` 2207 <label class="prompt-label">ð Pseudo TradingView</label> 2208 <div class="prompt-input-row"> 2209 <input type="text" id="tv-input" class="prompt-input" placeholder="Ex: CryptoTrader_QC" autofocus> 2210 <button id="submit-tv-btn" class="prompt-submit-btn">Continuer â</button> 2211 </div> 2212 `; 2213 UI.chatStream.appendChild(tvBox); 2214 scrollChatToBottom(); 2215 2216 const tvInput = document.getElementById('tv-input'); 2217 const tvBtn = document.getElementById('submit-tv-btn'); 2218 2219 function handleTvProceed() { 2220 const tvVal = tvInput.value.trim().replace('@', ''); 2221 if (!tvVal) return; 2222 appendUserMessage(`Mon TradingView : @${tvVal}`); 2223 FunnelState.tradingviewUsername = tvVal; 2224 localStorage.setItem('cq_tv_username', tvVal); 2225 tvBox.remove(); 2226 renderInteractiveIndicatorPicker(tvVal); 2227 } 2228 2229 tvBtn.addEventListener('click', handleTvProceed); 2230 tvInput.addEventListener('keydown', (e) => { 2231 if (e.key === 'Enter') handleTvProceed(); 2232 }); 2233} 2234 2235// âââ Sélecteur Interactif d'Indicateurs (Max 5, recherche, validation) âââââ 2236async function renderInteractiveIndicatorPicker(tvUsername) { 2237 AudioFX.playPop(); 2238 2239 await appendBotMessage(`ð **Sélection de vos Indicateurs TradingView**\n\nVeuillez choisir **jusqu'à 5 indicateurs** pour votre compte TradingView (\`@${tvUsername}\`) :`); 2240 2241 const pickerContainer = document.createElement('div'); 2242 pickerContainer.className = 'indicators-picker-widget'; 2243 pickerContainer.id = 'indicators-picker-widget'; 2244 pickerContainer.innerHTML = ` 2245 <div style="background: rgba(18,20,28,0.95); border: 1px solid rgba(255,255,255,0.1); border-radius: 14px; padding: 14px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);"> 2246 2247 <!-- Top header with live counter --> 2248 <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;"> 2249 <span style="font-size: 11px; font-weight: 700; color: #10b981; letter-spacing: 0.5px;" id="ind-counter-label"> 2250 ð SÃLECTION ACTUELLE (0/5) 2251 </span> 2252 <button id="btn-ind-reset" style="background: none; border: none; color: #64748b; font-size: 11px; cursor: pointer; text-decoration: underline;"> 2253 ð Réinitialiser 2254 </button> 2255 </div> 2256 2257 <!-- Live Search Filter --> 2258 <div style="margin-bottom: 10px;"> 2259 <input type="text" id="ind-search-input" placeholder="ð Filtrer (Scalping, Vector, MTF, Scanner...)" style="width: 100%; background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; padding: 8px 12px; font-size: 11px; box-sizing: border-box;"> 2260 </div> 2261 2262 <!-- Limit warning toast --> 2263 <div id="ind-limit-toast" style="display: none; background: rgba(239,68,68,0.15); border: 1px solid #ef4444; color: #fca5a5; font-size: 10px; padding: 6px 10px; border-radius: 6px; margin-bottom: 8px;"> 2264 â ï¸ Limite atteinte ! Vous pouvez sélectionner un maximum de 5 indicateurs. 2265 </div> 2266 2267 <!-- Indicators scroll list --> 2268 <div id="ind-list-scroll" style="max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding-right: 4px;"> 2269 <div style="text-align: center; color: #94a3b8; font-size: 11px; padding: 20px;"> 2270 â³ Chargement du catalogue d'indicateurs... 2271 </div> 2272 </div> 2273 2274 <!-- Submit button --> 2275 <div style="margin-top: 12px;"> 2276 <button id="btn-validate-indicators" class="prompt-submit-btn" style="width: 100%; padding: 11px; font-size: 12px; font-weight: 700;" disabled> 2277 ⪠Choisissez au moins 1 indicateur 2278 </button> 2279 </div> 2280 </div> 2281 `; 2282 UI.chatStream.appendChild(pickerContainer); 2283 scrollChatToBottom(); 2284 2285 let catalog = []; 2286 try { 2287 const res = await fetch(getApiUrl('/api/indicators/catalog')); 2288 const data = await res.json(); 2289 if (data.success && Array.isArray(data.indicators)) { 2290 catalog = data.indicators; 2291 } 2292 } catch(e) { 2293 console.warn('Fallback indicator fetch:', e); 2294 } 2295 2296 if (catalog.length === 0) { 2297 catalog = [ 2298 { id: "ind_machine_learning_rsi_premium", title: "CQ MACHINE LEARNING RSI PREMIUM", desc: "Oscillateur RSI adaptatif propulsé par Machine Learning avec détection automatique des divergences régulières/cachées et tableau de bord en temps réel." }, 2299 { id: "ind_trendline_vector_divergence_auto", title: "CQ Trendline + Vector + Divergence AUTO LIVE v3.1", desc: "Confluence automatisée Trendline Break + Vector Candle + Divergence sur 1H / 4H / 1D." }, 2300 { id: "ind_auto_setup_watchlist_alert", title: "CQ AUTO SETUP WATCHLIST ALERT v1.0", desc: "Moteur d'alertes en temps réel scannant l'ensemble de votre watchlist TradingView pour setups confluents." }, 2301 { id: "ind_scanner_crypto_auto", title: "SCANNER CRYPTO 100% AUTO", desc: "Scanner algorithmique 100% automatique détectant en temps réel les opportunités de bull run." }, 2302 { id: "ind_bull_run_cycle_map", title: "CQ Bull Run Cycle Map PRO V1.1", desc: "Cartographie algorithmique des cycles de marché et bull runs." }, 2303 { id: "ind_ai_trend_index_v2", title: "CQ AI Trend Index v2", desc: "Analyse directionnelle intelligente multi-couches IA et momentum." }, 2304 { id: "ind_prime_flow_oscillator", title: "CQ Prime Flow Oscillator Divergences Pro", desc: "Oscillateur haut de gamme et détection automatique de divergences." }, 2305 { id: "ind_vector_flow_pro", title: "CQ Vector Flow PRO", desc: "Analyse vectorielle des flux d'ordres et pression acheteuse/vendeuse." }, 2306 { id: "ind_ai_strategy_optimizer", title: "CQ AI Strategy Optimizer PRO", desc: "Moteur d'optimisation IA pour identifier la stratég
2306ie la plus performante." }, 2307 { id: "ind_power_order_blocks", title: "CQ Power Order Blocks Auto Presets", desc: "Détecteur automatique de blocs d'ordres institutionnels (Order Blocks)." } 2308 ]; 2309 } 2310 2311 // Pre-select first 3 recommended by default 2312 const selectedIds = new Set(); 2313 if (catalog.length >= 3) { 2314 selectedIds.add(catalog[0].id); 2315 selectedIds.add(catalog[1].id); 2316 selectedIds.add(catalog[2].id); 2317 } 2318 2319 const listContainer = document.getElementById('ind-list-scroll'); 2320 const counterLabel = document.getElementById('ind-counter-label'); 2321 const validateBtn = document.getElementById('btn-validate-indicators'); 2322 const limitToast = document.getElementById('ind-limit-toast'); 2323 const searchInput = document.getElementById('ind-search-input'); 2324 const resetBtn = document.getElementById('btn-ind-reset'); 2325 2326 function updateUIState() { 2327 const count = selectedIds.size; 2328 counterLabel.textContent = `ð SÃLECTION ACTUELLE (${count}/5)`; 2329 2330 if (count === 0) { 2331 validateBtn.disabled = true; 2332 validateBtn.textContent = '⪠Choisissez au moins 1 indicateur'; 2333 validateBtn.style.opacity = '0.5'; 2334 } else { 2335 validateBtn.disabled = false; 2336 validateBtn.textContent = `â Valider ma sélection (${count}/5)`; 2337 validateBtn.style.opacity = '1'; 2338 } 2339 } 2340 2341 function renderList(items) { 2342 listContainer.innerHTML = items.map(ind => { 2343 const isChecked = selectedIds.has(ind.id); 2344 return ` 2345 <label class="ind-checkbox-item" data-id="${ind.id}" style="display: flex; align-items: flex-start; gap: 8px; padding: 7px 9px; border-radius: 8px; background: ${isChecked ? 'rgba(16,185,129,0.1)' : 'rgba(255,255,255,0.02)'}; border: 1px solid ${isChecked ? '#10b981' : 'rgba(255,255,255,0.06)'}; cursor: pointer; transition: all 0.15s ease;"> 2346 <input type="checkbox" value="${ind.id}" ${isChecked ? 'checked' : ''} style="accent-color: #10b981; margin-top: 2px;"> 2347 <div style="flex: 1; min-width: 0;"> 2348 <div style="font-size: 11px; font-weight: 700; color: ${isChecked ? '#fff' : '#cbd5e1'}; line-height: 1.3;"> 2349 ${ind.title} 2350 </div> 2351 <div style="font-size: 9px; color: #64748b; margin-top: 2px; line-height: 1.3;"> 2352 ${ind.desc || ''} 2353 </div> 2354 </div> 2355 </label> 2356 `; 2357 }).join(''); 2358 2359 // Wire individual checkboxes 2360 listContainer.querySelectorAll('input[type="checkbox"]').forEach(chk => { 2361 chk.addEventListener('change', (e) => { 2362 const id = e.target.value; 2363 if (e.target.checked) { 2364 if (selectedIds.size >= 5) { 2365 e.target.checked = false; 2366 limitToast.style.display = 'block'; 2367 setTimeout(() => limitToast.style.display = 'none', 3000); 2368 return; 2369 } 2370 selectedIds.add(id); 2371 AudioFX.playPop(); 2372 } else { 2373 selectedIds.delete(id); 2374 AudioFX.playPop(); 2375 } 2376 updateUIState(); 2377 renderList(filteredCatalog()); 2378 }); 2379 }); 2380 } 2381 2382 function filteredCatalog() { 2383 const q = (searchInput.value || '').trim().toLowerCase(); 2384 if (!q) return catalog; 2385 return catalog.filter(c => 2386 c.title.toLowerCase().includes(q) || (c.desc && c.desc.toLowerCase().includes(q)) 2387 ); 2388 } 2389 2390 searchInput.addEventListener('input', () => { 2391 renderList(filteredCatalog()); 2392 }); 2393 2394 resetBtn.addEventListener('click', () => { 2395 selectedIds.clear(); 2396 updateUIState(); 2397 renderList(filteredCatalog()); 2398 AudioFX.playPop(); 2399 }); 2400 2401 // Initial render 2402 updateUIState(); 2403 renderList(catalog); 2404 2405 // Wire submission 2406 validateBtn.addEventListener('click', async () => { 2407 if (selectedIds.size === 0) return; 2408 const idsArray = Array.from(selectedIds); 2409 pickerContainer.remove(); 2410 2411 const loadingPill = document.createElement('div'); 2412 loadingPill.className = 'validation-pill valid-loading'; 2413 loadingPill.innerHTML = `<span>â³</span><span>Transmission de vos ${idsArray.length} indicateurs aux administrateurs...</span>`; 2414 UI.chatStream.appendChild(loadingPill); 2415 scrollChatToBottom(); 2416 2417 try { 2418 const res = await fetch(getApiUrl('/api/submit-indicators'), { 2419 method: 'POST', 2420 headers: { 'Content-Type': 'application/json' }, 2421 body: JSON.stringify({ 2422 session_id: FunnelState.sessionId, 2423 tradingview_username: tvUsername, 2424 indicator_ids: idsArray, 2425 email: FunnelState.email, 2426 telegram_id: FunnelState.telegramId, 2427 lang: FunnelState.language || 'fr', 2428 weex_uid: FunnelState.weexUid || localStorage.getItem('cq_weex_uid') || '', 2429 bitunix_uid: FunnelState.bitunixUid || localStorage.getItem('cq_bitunix_uid') || '', 2430 vip_approved: FunnelState.vipApproved || (localStorage.getItem('cq_vip_approved') === '1') 2431 }) 2432 }); 2433 const dataRes = await res.json(); 2434 loadingPill.remove(); 2435 2436 // Nettoyer tout ancien waiting pill 2437 const oldWaitingPill = document.getElementById('tv-waiting-pill'); 2438 if (oldWaitingPill) oldWaitingPill.remove(); 2439 2440 const isApproved = (dataRes && (dataRes.status === 'approved' || dataRes.auto_approved)) || 2441 FunnelState.vipApproved || (localStorage.getItem('cq_vip_approved') === '1'); 2442 2443 if (isApproved) { 2444 AudioFX.playSuccess(); 2445 if (typeof confetti === 'function') { 2446 confetti({ particleCount: 90, spread: 80, origin: { y: 0.6 } }); 2447 } 2448 2449 // 1. Message direct d'approbation bot avec liens d'accès TradingView 2450 await appendBotMessage(dataRes.message || `ð **FÃLICITATIONS ! VOTRE ACCÃS TRADINGVIEW EST ACTIVÃ** ð\n\nð¤ **Compte :** \`@${tvUsername}\`\nVos ${idsArray.length} indicateurs ont été validés avec succès !`); 2451 2452 // 2. Badge vert de confirmation en direct 2453 const approvedBadge = document.createElement('div'); 2454 approvedBadge.className = 'validation-pill valid-success'; 2455 approvedBadge.innerHTML = `
2456 <span>â </span> 2457 <div> 2458 <strong>Accès TradingView Validé & Actif !</strong><br> 2459 <small>Compte : <code>@${tvUsername}</code> ⢠Validé par Crypto Québec</small> 2460 </div> 2461 `; 2462 UI.chatStream.appendChild(approvedBadge); 2463 scrollChatToBottom(); 2464 2465 // 3. Flash Dynamic Island 2466 const islandText = document.querySelector('.island-text'); 2467 if (islandText) { 2468 islandText.textContent = `ð¢ TV VALIDà (@${tvUsername})`; 2469 islandText.style.color = '#10b981'; 2470 } 2471 2472 // 4. Mettre à jour état VIP 2473 FunnelState.tradingviewApproved = true; 2474 localStorage.setItem('cq_tv_approved', '1'); 2475 renderVipBottomMenu(); 2476 2477 } else { 2478 AudioFX.playPop(); 2479 // Affichage de la boîte de confirmation initiale 2480 await appendBotMessage(dataRes.message || `â **Demande d'Indicateurs Enregistrée !**\n\nð **Compte TradingView :** \`@${tvUsername}\`\nð¯ **Indicateurs sélectionnés (${idsArray.length}/5)**\n\nâ³ **Statut :** En attente de validation par nos administrateurs.`); 2481 2482 const waitingPill = document.createElement('div'); 2483 waitingPill.className = 'validation-pill valid-loading'; 2484 waitingPill.id = 'tv-waiting-pill'; 2485 waitingPill.innerHTML = ` 2486 <span>â³</span> 2487 <div> 2488 <strong>En attente d'approbation administrateur...</strong><br> 2489 <small>Dès que l'administrateur valide votre demande, le message d'approbation et vos accès s'afficheront directement ici.</small> 2490 </div> 2491 `; 2492 UI.chatStream.appendChild(waitingPill); 2493 scrollChatToBottom(); 2494 2495 // Démarrer la surveillance en temps réel de l'approbation admin 2496 pollTradingViewApproval(tvUsername); 2497 } 2498 2499 } catch(err) { 2500 loadingPill.remove(); 2501 console.error(err); 2502 } 2503 2504 setTimeout(() => { 2505 step7_TelegramVIPGateway(); 2506 }, 2000); 2507 }); 2508} 2509 2510function pollTradingViewApproval(tvUsername) { 2511 if (FunnelState._tvPollTimer) { 2512 clearTimeout(FunnelState._tvPollTimer); 2513 FunnelState._tvPollTimer = null; 2514 } 2515 2516 let attempts = 0; 2517 const maxAttempts = 120; // ~15 minutes 2518 2519 function getNextInterval(attempt) { 2520 if (attempt < 10) return 3000; // 3s (premières 30s) 2521 if (attempt < 25) return 6000; // 6s (jusqu'à 2 min) 2522 return 12000; // 12s ensuite 2523 } 2524 2525 async function checkStatus() { 2526 // Si l'onglet est masqué, suspendre les requêtes et attendre le réveil 2527 if (document.visibilityState === 'hidden') { 2528 FunnelState._tvPollTimer = setTimeout(checkStatus, 6000); 2529 return; 2530 } 2531 2532 attempts++; 2533 if (attempts > maxAttempts) { 2534 FunnelState._tvPollTimer = null; 2535 return; 2536 } 2537 2538 try { 2539 const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || ''; 2540 const url = getApiUrl(`/api/tradingview-status?username=${encodeURIComponent(tvUsername)}&session_id=${encodeURIComponent(FunnelState.sessionId || '')}&email=${encodeURIComponent(FunnelState.email || '')}&session_token=${encodeURIComponent(sToken)}`); 2541 const res = await fetch(url, { 2542 headers: sToken ? { 'X-CQ-Session-Token': sToken } : {} 2543 }); 2544 const data = await res.json(); 2545 2546 if (data && data.status === 'approved') { 2547 FunnelState._tvPollTimer = null; 2548 2549 const waitingPill = document.getElementById('tv-waiting-pill'); 2550 if (waitingPill) waitingPill.remove(); 2551 2552 AudioFX.playSuccess(); 2553 if (typeof confetti === 'function') { 2554 confetti({ particleCount: 90, spread: 80, origin: { y: 0.6 } }); 2555 } 2556 2557 // ð¯ 1. LE MESSAGE D'APPROBATION REVIENT DIRECTEMENT DANS L'INTERFACE 2558 await appendBotMessage(data.message || `ð **FÃLICITATIONS ! VOTRE ACCÃS TRADINGVIEW EST ACTIVÃ** ð\n\nð¤ **Compte :** \`@${tvUsername}\`\nVos indicateurs ont été validés avec succès par l'équipe Crypto Québec !`); 2559 2560 // 2. Badge de confirmation en direct 2561 const approvedBadge = document.createElement('div'); 2562 approvedBadge.className = 'validation-pill valid-success'; 2563 approvedBadge.innerHTML = `
2564 <span>â </span> 2565 <div> 2566 <strong>Accès TradingView Validé & Actif !</strong><br> 2567 <small>Compte : <code>@${tvUsername}</code> ⢠Validé par Crypto Québec</small> 2568 </div> 2569 `; 2570 UI.chatStream.appendChild(approvedBadge); 2571 scrollChatToBottom(); 2572 2573 // 3. Flash Dynamic Island 2574 const islandText = document.querySelector('.island-text'); 2575 if (islandText) { 2576 islandText.textContent = `ð¢ TV VALIDà (@${tvUsername})`; 2577 islandText.style.color = '#10b981'; 2578 } 2579 2580 // 4. Mettre à jour le dock VIP 2581 FunnelState.tradingviewApproved = true; 2582 renderVipBottomMenu(); 2583 return; 2584 2585 } else if (data && data.status === 'refused') { 2586 FunnelState._tvPollTimer = null; 2587 2588 const waitingPill = document.getElementById('tv-waiting-pill'); 2589 if (waitingPill) waitingPill.remove(); 2590 2591 AudioFX.playError?.(); 2592 await appendBotMessage(data.message || `â **Votre demande d'accès TradingView a été refusée par l'administrateur**.\n\nVeuillez vérifier votre nom d'utilisateur TradingView ou contacter le support.`); 2593 return; 2594 } 2595 } catch (e) { 2596 console.debug('TV poll error:', e); 2597 } 2598 2599 const nextWait = getNextInterval(attempts); 2600 FunnelState._tvPollTimer = setTimeout(checkStatus, nextWait); 2601 } 2602 2603 FunnelState._tvPollTimer = setTimeout(checkStatus, 3000); 2604} 2605 2606// âââ Step 7: Passerelle VIP Telegram âââââââââââââââââââââââââââââââââââââââââââ 2607async function step7_TelegramVIPGateway() { 2608 const data = FunnelState.funnelData; 2609 const tgUrl = 'https://cryptoqctools.com/go/web-button'; 2610 2611 await appendBotMessage(`ð **Félicitations et bienvenue dans l'équipe Crypto Québec !**\n\nVous rejoignez une communauté de plus de 5,000 passionnés de trading.\n\n${(data && data.telegram_bridge && data.telegram_bridge.text) || "Rejoignez dès maintenant notre communauté Telegram pour suivre les alertes et points de marché :"}`); 2612 2613 const tgBox = document.createElement('div'); 2614 tgBox.className = 'chat-btn-group'; 2615 tgBox.innerHTML = ` 2616 <a href="${tgUrl}" target="_blank" class="chat-btn chat-btn-primary" style="text-decoration: none;"> 2617 <span>${(data && data.telegram_bridge && data.telegram_bridge.button) || "ð Rejoindre @CryptoQc_Welcome_bot"}</span> 2618 </a> 2619 `; 2620 UI.chatStream.appendChild(tgBox); 2621 scrollChatToBottom(); 2622} 2623 2624 2625// ========================================================================== 2626// INBOX & NOTIFICATION SYSTEM (v3.0.0) 2627// ========================================================================== 2628 2629let inboxPollingInterval = null; 2630let lastKnownUnreadCount = 0; 2631 2632async function checkAndShowInboxBadge(email) { 2633 const targetEmail = (email || FunnelState.email || localStorage.getItem('cq_web_email') || '').trim().toLowerCase(); 2634 if (!targetEmail || !targetEmail.includes('@')) return; 2635 2636 try { 2637 const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || ''; 2638 const res = await fetch(getApiUrl(`/api/user-inbox?email=${encodeURIComponent(targetEmail)}&session_id=${FunnelState.sessionId || ''}&session_token=${encodeURIComponent(sToken)}`), { 2639 headers: sToken ? { 'X-CQ-Session-Token': sToken } : {} 2640 }); 2641 if (res.status === 401) { 2642 // Non autorisé (en attente de validation OTP) 2643 return; 2644 } 2645 const data = await res.json(); 2646 if (!data.success) return; 2647 2648 const unread = data.unread || 0; 2649 const messages = data.messages || []; 2650 2651 // Afficher les boutons enveloppe 2652 if (UI.drawerInboxBtn) UI.drawerInboxBtn.style.display = 'inline-flex'; 2653 if (UI.chatInboxBtn) UI.chatInboxBtn.style.display = 'inline-flex'; 2654 2655 // Mettre à jour les badges 2656 const badgeText = unread > 9 ? '9+' : String(unread); 2657 const showBadge = unread > 0; 2658 2659 if (UI.drawerInboxBadge) { 2660 UI.drawerInboxBadge.textContent = badgeText; 2661 UI.drawerInboxBadge.style.display = showBadge ? 'block' : 'none'; 2662 } 2663 if (UI.chatInboxBadge) { 2664 UI.chatInboxBadge.textContent = badgeText; 2665 UI.chatInboxBadge.style.display = showBadge ? 'block' : 'none'; 2666 } 2667 2668 // Si nouveau message d'approbation reçu pendant la session 2669 if (unread > lastKnownUnreadCount && lastKnownUnreadCount > 0) { 2670 AudioFX.playSuccess(); 2671 const latest = messages[0]; 2672 if (latest) { 2673 appendBotMessage(`ð¬ **Nouveau message reçu !**\n\n**${latest.subject || 'Notification officielle'}**\nConsultez votre boîte de réception en cliquant sur l'enveloppe âï¸.`); 2674 } 2675 } 2676 lastKnownUnreadCount = unread; 2677 } catch (err) { 2678 console.debug('CQ Inbox polling notice:', err); 2679 } 2680} 2681 2682function startInboxPolling() { 2683 if (inboxPollingInterval) clearInterval(inboxPollingInterval); 2684 checkAndShowInboxBadge(); 2685 inboxPollingInterval = setInterval(() => { 2686 // Ne poller que si l'onglet est actif 2687 if (document.visibilityState === 'visible') { 2688 if (FunnelState.email || localStorage.getItem('cq_web_email')) { 2689 checkAndShowInboxBadge(); 2690 } 2691 } 2692 }, 25000); 2693} 2694 2695function openInboxModal() { 2696 if (!UI.inboxModal) return; 2697 UI.inboxModal.classList.add('active'); 2698 loadAndRenderInboxMessages(); 2699} 2700
2701function closeInboxModal() { 2702 if (!UI.inboxModal) return; 2703 UI.inboxModal.classList.remove('active'); 2704} 2705 2706async function loadAndRenderInboxMessages() { 2707 const targetEmail = (FunnelState.email || localStorage.getItem('cq_web_email') || '').trim().toLowerCase(); 2708 if (!targetEmail) return; 2709 2710 if (UI.inboxLoading) UI.inboxLoading.style.display = 'block'; 2711 if (UI.inboxEmpty) UI.inboxEmpty.style.display = 'none'; 2712 if (UI.inboxList) UI.inboxList.style.display = 'none'; 2713 2714 try { 2715 const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || ''; 2716 const res = await fetch(getApiUrl(`/api/user-inbox?email=${encodeURIComponent(targetEmail)}&session_id=${FunnelState.sessionId || ''}&session_token=${encodeURIComponent(sToken)}`), { 2717 headers: sToken ? { 'X-CQ-Session-Token': sToken } : {} 2718 }); 2719 const data = await res.json(); 2720 2721 if (UI.inboxLoading) UI.inboxLoading.style.display = 'none'; 2722 2723 const messages = data.messages || []; 2724 if (!messages.length) { 2725 if (UI.inboxEmpty) UI.inboxEmpty.style.display = 'block'; 2726 if (UI.markAllReadBtn) UI.markAllReadBtn.style.display = 'none'; 2727 return; 2728 } 2729 2730 if (UI.markAllReadBtn) UI.markAllReadBtn.style.display = (data.unread > 0) ? 'inline-block' : 'none'; 2731 2732 if (UI.inboxList) { 2733 UI.inboxList.innerHTML = ''; 2734 messages.forEach(msg => { 2735 const card = document.createElement('div'); 2736 card.className = `inbox-msg-card ${msg.read ? 'read' : 'unread'}`; 2737 2738 let badgeClass = 'inbox-badge-general'; 2739 let badgeLabel = 'ð¬ Message Officiel'; 2740 const t = (msg.type || '').toLowerCase(); 2741 if (t.includes('tradingview')) { 2742 badgeClass = 'inbox-badge-tradingview'; 2743 badgeLabel = 'ð TradingView'; 2744 } else if (t.includes('uid')) { 2745 badgeClass = 'inbox-badge-uid'; 2746 badgeLabel = 'ð¢ UID Partenaire'; 2747 } else if (t.includes('teachable')) { 2748 badgeClass = 'inbox-badge-teachable'; 2749 badgeLabel = 'ð Teachable'; 2750 } 2751 2752 const dateStr = msg.created_at ? msg.created_at.slice(0, 16).replace('T', ' ') : ''; 2753 const bodyFormatted = formatBotMarkdown(msg.body || ''); 2754 2755 card.innerHTML = ` 2756 <div class="inbox-msg-header"> 2757 <span class="inbox-msg-badge ${badgeClass}">${badgeLabel}</span> 2758 <span class="inbox-msg-time">${dateStr}</span> 2759 </div> 2760 <div class="inbox-msg-subject">${msg.subject || ''}</div> 2761 <div class="inbox-msg-body">${bodyFormatted}</div> 2762 `; 2763 UI.inboxList.appendChild(card); 2764 }); 2765 UI.inboxList.style.display = 'flex'; 2766 } 2767 2768 // Si messages non lus, marquer tout comme lu après affichage 2769 if (data.unread > 0) { 2770 markInboxAsRead(targetEmail); 2771 } 2772 } catch (err) { 2773 if (UI.inboxLoading) UI.inboxLoading.innerHTML = '<span style="color:#ef4444;">Erreur de chargement.</span>'; 2774 } 2775} 2776 2777async function markInboxAsRead(email, messageId = '') { 2778 try { 2779 const sToken = FunnelState.sessionToken || localStorage.getItem('cq_session_token') || ''; 2780 await fetch(getApiUrl('/api/mark-inbox-read'), { 2781 method: 'POST', 2782 headers: { 2783 'Content-Type': 'application/json', 2784 'X-CQ-Session-Token': sToken 2785 }, 2786 body: JSON.stringify({ email: email, message_id: messageId, session_token: sToken }) 2787 }); 2788 // Réinitialiser les badges UI 2789 if (UI.drawerInboxBadge) UI.drawerInboxBadge.style.display = 'none'; 2790 if (UI.chatInboxBadge) UI.chatInboxBadge.style.display = 'none'; 2791 lastKnownUnreadCount = 0; 2792 } catch(e) {} 2793} 2794 2795function initInboxEventListeners() { 2796 if (UI.drawerInboxBtn) UI.drawerInboxBtn.addEventListener('click', openInboxModal); 2797 if (UI.chatInboxBtn) UI.chatInboxBtn.addEventListener('click', openInboxModal); 2798 if (UI.closeInboxBtn) UI.closeInboxBtn.addEventListener('click', closeInboxModal); 2799 if (UI.doneInboxBtn) UI.doneInboxBtn.addEventListener('click', closeInboxModal); 2800 if (UI.markAllReadBtn) { 2801 UI.markAllReadBtn.addEventListener('click', () => { 2802 const email = FunnelState.email || localStorage.getItem('cq_web_email'); 2803 if (email) markInboxAsRead(email); 2804 loadAndRenderInboxMessages(); 2805 }); 2806 } 2807 if (UI.inboxModal) { 2808 UI.inboxModal.addEventListener('click', (e) => {
2809 if (e.target === UI.inboxModal) closeInboxModal(); 2810 }); 2811 } 2812 2813 // Ãcouteur de changement de visibilité d'onglet pour rafraîchir immédiatement à la reprise 2814 document.addEventListener('visibilitychange', () => { 2815 if (document.visibilityState === 'visible') { 2816 if (FunnelState.email || localStorage.getItem('cq_web_email')) { 2817 checkAndShowInboxBadge(); 2818 } 2819 } 2820 }); 2821} 2822 2823// ========================================================================== 2824// VIP FEED & PODCAST SYSTEM (Inspiré de CQ-mobile-app FeedScreen) 2825// Contrôle d'expulsion selon les critères de CryptoAds (J+15) 2826// ========================================================================== 2827 2828let currentFeedFilter = 'all'; 2829let currentFeedItems = []; 2830let isSimulatedExpelled = false; 2831 2832function openVipFeedModal() { 2833 if (!UI.feedModal) return; 2834 UI.feedModal.classList.add('active'); 2835 if (window.AudioFX && typeof window.AudioFX.playPop === 'function') { 2836 window.AudioFX.playPop(); 2837 } 2838 loadAndRenderVipFeed(isSimulatedExpelled); 2839} 2840 2841function closeVipFeedModal() { 2842 if (!UI.feedModal) return; 2843 UI.feedModal.classList.remove('active'); 2844 if (UI.feedList) { 2845 const iframes = UI.feedList.querySelectorAll('iframe'); 2846 iframes.forEach(f => { 2847 const src = f.src; 2848 f.src = src; 2849 }); 2850 } 2851} 2852 2853async function loadAndRenderVipFeed(simulateExpelled = false) { 2854 if (UI.feedLoading) UI.feedLoading.style.display = 'block'; 2855 if (UI.feedExpelled) UI.feedExpelled.style.display = 'none'; 2856 if (UI.feedEmpty) UI.feedEmpty.style.display = 'none'; 2857 if (UI.feedList) UI.feedList.style.display = 'none'; 2858 2859 const email = (FunnelState.email || localStorage.getItem('cq_web_email') || '').trim(); 2860 const tgId = (FunnelState.telegramId || localStorage.getItem('cq_telegram_id') || '').trim(); 2861 const uid = FunnelState.weexUid || FunnelState.bitunixUid || (FunnelState.existingProfile && (FunnelState.existingProfile.weex_uid || FunnelState.existingProfile.bitunix_uid)) || ''; 2862 const exch = FunnelState.selectedExchange || (FunnelState.weexUid ? 'weex' : (FunnelState.bitunixUid ? 'bitunix' : 'weex')); 2863 2864 try { 2865 const params = new URLSearchParams(); 2866 if (email) params.append('email', email); 2867 if (tgId) params.append('telegram_id', tgId); 2868 if (uid) params.append('uid', uid); 2869 if (exch) params.append('exchange', exch); 2870 if (simulateExpelled) params.append('simulate_expelled', '1'); 2871 2872 const res = await fetch(getApiUrl(`/api/vip/feed?${params.toString()}`)); 2873 const data = await res.json(); 2874 2875 if (UI.feedLoading) UI.feedLoading.style.display = 'none'; 2876 2877 // 1. CAS EXPULSION (Règles CryptoAds & Inactivité J+15) 2878 if (data.is_expelled) { 2879 renderExpulsionCard(data); 2880 return; 2881 } 2882 2883 // 2. CAS ACCÃS REFUSà (NON APPROUVÃ) 2884 if (!data.success && !data.is_approved) { 2885 if (UI.feedEmpty) { 2886 UI.feedEmpty.innerHTML = ` 2887 <div style="font-size: 38px; margin-bottom: 12px;">ð</div> 2888 <h4 style="margin: 0 0 8px; color: #f8fafc; font-size: 16px;">Accès Réservé aux Membres VIP</h4> 2889 <p style="margin: 0 0 16px; color: #94a3b8; font-size: 13px;">${data.message || 'Validez votre compte partenaire WEEX ou Bitunix pour déverrouiller ce flux.'}</p> 2890 <button class="primary-legal-btn" onclick="closeVipFeedModal();">Retourner au Funnel</button> 2891 `; 2892 UI.feedEmpty.style.display = 'block'; 2893 } 2894 return; 2895 } 2896 2897 // 3. CAS ACTIF : Affichage des publications & podcasts 2898 currentFeedItems = data.items || []; 2899 renderFilteredFeedItems(); 2900 2901 } catch(err) { 2902 console.error('Erreur chargement VIP Feed:', err); 2903 if (UI.feedLoading) UI.feedLoading.style.display = 'none'; 2904 if (UI.feedEmpty) { 2905 UI.feedEmpty.innerHTML = ` 2906 <div style="font-size: 38px; margin-bottom: 12px;">â ï¸</div> 2907 <h4 style="margin: 0 0 8px; color: #ef4444; font-size: 16px;">Erreur de connexion</h4> 2908 <p style="margin: 0; color: #94a3b8; font-size: 13px;">Impossible de récupérer le flux pour le moment. Veuillez réessayer.</p> 2909 `; 2910 UI.feedEmpty.style.display = 'block'; 2911 } 2912 } 2913} 2914 2915function renderExpulsionCard(data) { 2916 if (!UI.feedExpelled) return; 2917 UI.feedExpelled.style.display = 'block'; 2918 if (UI.feedList) UI.feedList.style.display = 'none'; 2919 if (UI.feedEmpty) UI.feedEmpty.style.display = 'none'; 2920 2921 const exchange = data.exchange || 'WEEX'; 2922 const uid = data.uid || 'Non renseigné'; 2923 const partnerUrl = data.partner_url || (exchange === 'Bitunix' ? 'https://www.bitunix.com/register?vipCode=gwzp' : 'https://trade.weex.com/register?vipCode=QUEBEC'); 2924 const supportUrl = data.support_url || 'https://t.me/crypto_quebec_channel'; 2925 2926 UI.feedExpelled.innerHTML = ` 2927 <div class="vip-expulsion-card"> 2928 <div class="expulsion-banner">
2929 <span class="expulsion-icon">â</span> 2930 <div class="expulsion-badge">COMPTE SUSPENDU ⢠SYSTÃME CRYPTOADS</div> 2931 </div> 2932 2933 <h3 class="expulsion-title">Accès VIP Révoqué (Règle Inactivité J+15)</h3> 2934 2935 <p class="expulsion-desc"> 2936 ${data.message || `Votre période de grâce de 15 jours sans transaction enregistrée sur votre compte partenaire ${exchange} est expirée. Conformément aux critères de Crypto Québec et du système CryptoAds, vos licences d'indicateurs TradingView et votre accès au flux VIP ont été révoqués.`} 2937 </p> 2938 2939 <div class="expulsion-account-box"> 2940 <div class="account-item"> 2941 <span class="acc-lbl">Plateforme :</span> 2942 <span class="acc-val"><strong>${exchange}</strong></span> 2943 </div> 2944 <div class="account-item"> 2945 <span class="acc-lbl">UID Associé :</span> 2946 <span class="acc-val"><code>${uid}</code></span> 2947 </div> 2948 <div class="account-item"> 2949 <span class="acc-lbl">Statut CRM :</span> 2950 <span class="acc-val" style="color:#ef4444; font-weight:700;">INACTIF (J+15)</span> 2951 </div> 2952 </div> 2953 2954 <div class="expulsion-steps"> 2955 <h4>â¡ Procédure de Réactivation Immédiate :</h4> 2956 <ol> 2957 <li>Ouvrez votre compte <strong>${exchange}</strong> (UID: <code>${uid}</code>).</li> 2958 <li>Exécutez au minimum un trade (Spot ou Futures) pour réactiver vos droits.</li> 2959 <li>Transmettez la preuve d'exécution à l'équipe Telegram pour lever la suspension de vos indicateurs.</li> 2960 </ol> 2961 </div> 2962 2963 <div class="expulsion-actions"> 2964 <a href="${partnerUrl}" target="_blank" rel="noopener noreferrer" class="chat-btn chat-btn-primary expulsion-btn-broker"> 2965 <span>ð Ouvrir mon compte ${exchange}</span> 2966 </a> 2967 <a href="${supportUrl}" target="_blank" rel="noopener noreferrer" class="chat-btn chat-btn-secondary expulsion-btn-support"> 2968 <span>ð¬ Contacter le Support Telegram</span> 2969 </a> 2970 </div> 2971 2972 <div class="expulsion-test-hint"> 2973 <small>ð¡ Note : Vous pouvez basculer l'état pour tester via le bouton « âï¸ Mode Test Expulsion » ci-dessous.</small> 2974 </div> 2975 </div> 2976 `; 2977} 2978 2979function renderFilteredFeedItems() { 2980 if (!UI.feedList) return; 2981 2982 const query = (UI.feedSearch ? UI.feedSearch.value : '').toLowerCase().trim(); 2983 const filter = currentFeedFilter; 2984 2985 const filtered = currentFeedItems.filter(item => { 2986 const matchesCat = (filter === 'all') 2987 ? true 2988 : (filter === 'news' ? (item.type === 'news' || item.type === 'podcast') : item.type === filter); 2989 2990 if (!matchesCat) return false; 2991 2992 if (!query) return true; 2993 const inTitle = (item.title || '').toLowerCase().includes(query); 2994 const inContent = (item.content || '').toLowerCase().includes(query); 2995 const inTags = item.tags && item.tags.some(t => t.toLowerCase().includes(query)); 2996 return inTitle || inContent || inTags; 2997 }); 2998 2999 if (filtered.length === 0) { 3000 if (UI.feedEmpty) UI.feedEmpty.style.display = 'block'; 3001 UI.feedList.style.display = 'none'; 3002 return; 3003 } 3004 3005 if (UI.feedEmpty) UI.feedEmpty.style.display = 'none'; 3006 UI.feedList.innerHTML = ''; 3007 UI.feedList.style.display = 'flex'; 3008 3009 filtered.forEach(item => { 3010 const card = document.createElement('div'); 3011 card.className = `vip-feed-card feed-type-${item.type || 'post'}`; 3012 3013 if (item.type === 'podcast') { 3014 card.innerHTML = buildPodcastCardHtml(item); 3015 } else if (item.type === 'signal') { 3016 card.innerHTML = buildSignalCardHtml(item); 3017 } else { 3018 card.innerHTML = buildAnalysisCardHtml(item); 3019 } 3020 3021 UI.feedList.appendChild(card); 3022 }); 3023 3024 wireFeedCardInteractions(); 3025} 3026 3027function buildPodcastCardHtml(item) { 3028 const epNumber = item.episodeNumber ? `Ãpisode #${item.episodeNumber}` : 'Ãpisode'; 3029 const duration = item.duration || '60 min'; 3030 const ytUrl = item.youtubeUrl || (item.youtubeId ? `https://www.youtube.com/watch?v=${item.youtubeId}` : 'https://youtube.com/@cryptoquebec'); 3031 const embedId = item.youtubeId || ''; 3032 3033 return ` 3034 <div class="feed-card-header">
3035 <span class="feed-badge feed-badge-podcast">ð§ PODCAST OFFICIEL</span> 3036 <span class="feed-badge-sub">${epNumber} ⢠${duration}</span> 3037 </div> 3038 <h4 class="feed-podcast-title">${item.title}</h4> 3039 <p class="feed-podcast-desc">${item.content || item.description || ''}</p> 3040 3041 ${embedId ? ` 3042 <div class="podcast-player-slot" id="player-slot-${item.id}"> 3043 <button class="podcast-play-btn" data-yt-id="${embedId}" data-target-slot="player-slot-${item.id}"> 3044 <span class="play-icon">â¶ï¸</span> 3045 <span>Ãcouter / Regarder l'émission</span> 3046 </button> 3047 </div> 3048 ` : ''} 3049 3050 <div class="feed-card-footer"> 3051 <span class="feed-timestamp">ð ${item.publishedAt ? item.publishedAt.slice(0, 10) : 'Récemment'}</span> 3052 <a href="${ytUrl}" target="_blank" rel="noopener noreferrer" class="feed-ext-link"> 3053 <span>Ouvrir sur YouTube â</span> 3054 </a> 3055 </div> 3056 `; 3057} 3058 3059function buildSignalCardHtml(item) { 3060 const title = item.title || 'ð¯ SIGNAL VIP'; 3061 const entry = item.entryPrice || 'Marché'; 3062 const sl = item.stopLoss || 'Invalidation'; 3063 const tp = item.takeProfit || 'Target 1'; 3064 const rr = item.riskReward || '1:2.5'; 3065 const tgUrl = item.channelUrl || 'https://t.me/crypto_quebec_channel'; 3066 const formattedContent = typeof formatBotMarkdown === 'function' ? formatBotMarkdown(item.content || '') : item.content; 3067 3068 return ` 3069 <div class="feed-card-header"> 3070 <span class="feed-badge feed-badge-signal">ð¯ SIGNAL VIP</span> 3071 <span class="feed-badge-dir">LONG / ACHAT</span> 3072 </div> 3073 <h4 class="feed-signal-title">${title}</h4> 3074 3075 <div class="signal-metrics-grid"> 3076 <div class="signal-metric-box"> 3077 <span class="metric-label">ENTRÃE</span> 3078 <span class="metric-val">${entry}</span> 3079 </div> 3080 <div class="signal-metric-box stop-loss"> 3081 <span class="metric-label">STOP LOSS</span> 3082 <span class="metric-val">${sl}</span> 3083 </div> 3084 <div class="signal-metric-box take-profit"> 3085 <span class="metric-label">OBJECTIF</span> 3086 <span class="metric-val">${tp}</span> 3087 </div> 3088 <div class="signal-metric-box"> 3089 <span class="metric-label">RATIO R:R</span> 3090 <span class="metric-val">${rr}</span> 3091 </div> 3092 </div> 3093 3094 <div class="feed-signal-content">${formattedContent}</div> 3095 3096 <div class="feed-card-actions"> 3097 <button class="feed-copy-setup-btn" data-setup="${encodeURIComponent(`CQ SIGNAL VIP:\n${title}\nEntrée: ${entry} | SL: ${sl} | TP: ${tp} | R:R: ${rr}`)}"> 3098 <span class="copy-icon">ð</span> 3099 <span class="copy-text">Copier le setup</span> 3100 </button> 3101 <a href="${tgUrl}" target="_blank" rel="noopener noreferrer" class="feed-tg-btn"> 3102 <span class="tg-icon">ð²</span> 3103 <span>Rejoindre sur Telegram</span> 3104 </a> 3105 </div> 3106 `; 3107} 3108 3109function buildAnalysisCardHtml(item) { 3110 const title = item.title || 'ð ANALYSE TECHNIQUE'; 3111 const author = item.author || 'Simon (Crypto Québec)'; 3112 const formattedContent = typeof formatBotMarkdown === 'function' ? formatBotMarkdown(item.content || '') : item.content; 3113 const tgUrl = item.channelUrl || 'https://t.me/crypto_quebec_channel'; 3114 3115 let tagsHtml = ''; 3116 if (item.tags && item.tags.length > 0) { 3117 tagsHtml = `<div class="feed-tags">${item.tags.map(t => `<span class="feed-tag">#${t}</span>`).join('')}</div>`; 3118 } 3119 3120 return ` 3121 <div class="feed-card-header"> 3122 <span class="feed-badge feed-badge-analysis">ð ANALYSE MARCHÃ</span> 3123 <span class="feed-badge-sub">Par ${author}</span> 3124 </div> 3125 <h4 class="feed-analysis-title">${title}</h4> 3126 <div class="feed-analysis-content">${formattedContent}</div> 3127 ${tagsHtml} 3128 <div class="feed-card-footer"> 3129 <span class="feed-timestamp">
3129ð¡ Source : Telegram VIP</span> 3130 <a href="${tgUrl}" target="_blank" rel="noopener noreferrer" class="feed-ext-link"> 3131 <span>Voir sur Telegram â</span> 3132 </a> 3133 </div> 3134 `; 3135} 3136 3137function wireFeedCardInteractions() { 3138 if (!UI.feedList) return; 3139 3140 const playBtns = UI.feedList.querySelectorAll('.podcast-play-btn'); 3141 playBtns.forEach(btn => { 3142 btn.addEventListener('click', () => { 3143 const ytId = btn.getAttribute('data-yt-id'); 3144 const targetSlotId = btn.getAttribute('data-target-slot'); 3145 const slot = document.getElementById(targetSlotId); 3146 if (slot && ytId) { 3147 slot.innerHTML = ` 3148 <div class="feed-video-wrapper"> 3149 <iframe 3150 src="https://www.youtube-nocookie.com/embed/${ytId}?autoplay=1&rel=0&modestbranding=1" 3151 title="Crypto Québec Podcast" 3152 frameborder="0" 3153 allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 3154 allowfullscreen> 3155 </iframe> 3156 </div> 3157 `; 3158 } 3159 }); 3160 }); 3161 3162 const copyBtns = UI.feedList.querySelectorAll('.feed-copy-setup-btn'); 3163 copyBtns.forEach(btn => { 3164 btn.addEventListener('click', () => { 3165 const textToCopy = decodeURIComponent(btn.getAttribute('data-setup') || ''); 3166 if (navigator.clipboard && navigator.clipboard.writeText) { 3167 navigator.clipboard.writeText(textToCopy).then(() => { 3168 const original = btn.innerHTML; 3169 btn.innerHTML = `<span>â</span><span>Copié !</span>`; 3170 btn.classList.add('copied'); 3171 setTimeout(() => { 3172 btn.innerHTML = original; 3173 btn.classList.remove('copied'); 3174 }, 2000); 3175 }); 3176 } 3177 }); 3178 }); 3179} 3180 3181function initVipFeedEventListeners() { 3182 if (UI.drawerFeedBtn) UI.drawerFeedBtn.addEventListener('click', openVipFeedModal); 3183 if (UI.chatFeedBtn) UI.chatFeedBtn.addEventListener('click', openVipFeedModal); 3184 if (UI.closeFeedBtn) UI.closeFeedBtn.addEventListener('click', closeVipFeedModal); 3185 if (UI.doneFeedBtn) UI.doneFeedBtn.addEventListener('click', closeVipFeedModal); 3186 3187 if (UI.refreshFeedBtn) { 3188 UI.refreshFeedBtn.addEventListener('click', () => { 3189 loadAndRenderVipFeed(isSimulatedExpelled); 3190 }); 3191 } 3192 3193 if (UI.toggleExpelledTestBtn) { 3194 UI.toggleExpelledTestBtn.addEventListener('click', () => { 3195 isSimulatedExpelled = !isSimulatedExpelled; 3196 if (isSimulatedExpelled) { 3197 UI.toggleExpelledTestBtn.innerHTML = `â Réactiver (Compte Actif)`; 3198 UI.toggleExpelledTestBtn.classList.add('active-test'); 3199 } else { 3200 UI.toggleExpelledTestBtn.innerHTML = `âï¸ Mode Test Expulsion`; 3201 UI.toggleExpelledTestBtn.classList.remove('active-test'); 3202 } 3203 loadAndRenderVipFeed(isSimulatedExpelled); 3204 }); 3205 } 3206 3207 if (UI.feedSearch) { 3208 UI.feedSearch.addEventListener('input', () => { 3209 renderFilteredFeedItems(); 3210 }); 3211 } 3212 3213 const filterPills = document.querySelectorAll('.vip-filter-pill'); 3214 filterPills.forEach(pill => { 3215 pill.addEventListener('click', () => { 3216 filterPills.forEach(p => p.classList.remove('active')); 3217 pill.classList.add('active'); 3218 currentFeedFilter = pill.getAttribute('data-filter') || 'all'; 3219 renderFilteredFeedItems(); 3220 }); 3221 }); 3222 3223 if (UI.feedModal) { 3224 UI.feedModal.addEventListener('click', (e) => { 3225 if (e.target === UI.feedModal) closeVipFeedModal(); 3226 }); 3227 } 3228} 3229 3230// Initialisations globales des écouteurs 3231initInboxEventListeners(); 3232initVipFeedEventListeners(); 3233
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.