1class AppDownloadBar extends HTMLElement { 2 /** 3 * Publica cedo o estado esperado do banner na sessão para evitar conflitos de timing 4 * com outros banners do projeto (ex.: banners IRPF). 5 * 6 * @param {boolean} shouldShow 7 */ 8 setSessionShouldShow(shouldShow) { 9 try { 10 sessionStorage.setItem('app-download-should-show', shouldShow ? '1' : '0'); 11 } catch { 12 // storage pode estar indisponÃvel (Safari private mode / quota etc.) 13 } 14 } 15 16 async connectedCallback() { 17 await this.ensureAlpine(); 18 19 this.downloadAppUrl = 'null'; 20 this.laravelUrl = 'https://investidor10.com.br'; 21 this.isAndroid = 'null' === '1'; 22 this.isiOS = 'null' === '1'; 23 this.timeoutToShowBanner = '30000'; 24 this.isMobileDevice = 'false' === 'true'; 25 this.hasAppInstalled = 'false' === 'true'; 26 const breakpointValue = parseInt('768'); 27 this.breakpoint = isNaN(breakpointValue) ? 768 : breakpointValue; 28 29 const isMobile = window.innerWidth < this.breakpoint; 30 const isTouchDevice = window.matchMedia("(pointer: coarse)").matches; 31 32 this.shouldRenderMobile = isMobile && (isTouchDevice || this.isMobileDevice); 33 34 if (!this.shouldRenderMobile) { 35 this.setSessionShouldShow(false); 36 this.style.display = 'none'; 37 return; 38 } 39 40 if (this.hasAppInstalled) { 41 this.setSessionShouldShow(false); 42 this.style.display = 'none'; 43 return; 44 } 45 46 if (!this.downloadAppUrl || this.downloadAppUrl === 'null') { 47 this.setSessionShouldShow(false); 48 this.style.display = 'none'; 49 return; 50 } 51 52 const closedAt = localStorage.getItem('app-download-bar-closed-at'); 53 if (closedAt) { 54 const closedDate = new Date(closedAt); 55 const currentDate = new Date(); 56 const limitPeriod = parseInt(localStorage.getItem('limit-period') || '3', 10); 57 const diffInDays = Math.floor((currentDate - closedDate) / (1000 * 60 * 60 * 24)); 58 59 if (diffInDays < limitPeriod) { 60 this.setSessionShouldShow(false); 61 this.style.display = 'none'; 62 return; 63 } 64 } 65 66 this.setSessionShouldShow(true); 67 this.render(); 68 69 this.initAlpine(); 70 } 71 72 async ensureAlpine() { 73 if (window.Alpine && typeof window.Alpine.initTree === 'function') { 74 return Promise.resolve(); 75 } 76 77 return new Promise((resolve) => { 78 let attempts = 0; 79 const maxAttempts = 50; 80 81 const checkAlpine = () => { 82 attempts++; 83 84 if (window.Alpine && typeof window.Alpine.initTree === 'function') { 85 resolve(); 86 return; 87 } 88 89 if (attempts >= maxAttempts) { 90 this.loadAlpineFromCDN().then(resolve).catch(() => { 91 console.warn('[AppDownloadBar] Alpine.js not available, component may not work correctly'); 92 resolve(); 93 }); 94 return; 95 } 96 97 setTimeout(checkAlpine, 100); 98 }; 99 100 checkAlpine(); 101 }); 102 } 103 104 loadAlpineFromCDN() { 105 return new Promise((resolve, reject) => { 106 if (window.Alpine) { 107 resolve(); 108 return; 109 } 110 111 const existingScript = document.querySelector('script[src*="alpinejs"]'); 112 if (existingScript) { 113 const checkInterval = setInterval(() => { 114 if (window.Alpine) { 115 clearInterval(checkInterval); 116 resolve(); 117 } 118 }, 50); 119 120 setTimeout(() => { 121 clearInterval(checkInterval); 122 if (!window.Alpine) { 123 reject(new Error('Alpine.js failed to load')); 124 } else { 125 resolve(); 126 } 127 }, 5000); 128 return; 129 } 130 131 const script = document.createElement('script'); 132 script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js'; 133 script.defer = false;
134 script.onload = () => { 135 setTimeout(() => { 136 if (window.Alpine) { 137 resolve(); 138 } else { 139 reject(new Error('Alpine.js failed to initialize')); 140 } 141 }, 50); 142 }; 143 script.onerror = () => { 144 reject(new Error('Failed to load Alpine.js from CDN')); 145 }; 146 document.head.appendChild(script); 147 }); 148 } 149 150 initAlpine() { 151 if (!window.Alpine) { 152 console.error('[AppDownloadBar] Alpine.js is not available'); 153 return; 154 } 155 156 requestAnimationFrame(() => { 157 try { 158 if (window.Alpine && typeof window.Alpine.initTree === 'function') { 159 window.Alpine.initTree(this); 160 } 161 } catch (error) { 162 console.error('[AppDownloadBar] Failed to initialize Alpine:', error); 163 setTimeout(() => { 164 try { 165 if (window.Alpine && typeof window.Alpine.initTree === 'function') { 166 window.Alpine.initTree(this); 167 } 168 } catch (retryError) { 169 console.error('[AppDownloadBar] Retry failed:', retryError); 170 } 171 }, 100); 172 } 173 }); 174 } 175 176 getAlpineData() { 177 const closedAt = localStorage.getItem('app-download-bar-closed-at'); 178 const limitPeriod = parseInt(localStorage.getItem('limit-period') || '3', 10); 179 const currentDate = new Date(); 180 181 let shouldShow = true; 182 if (closedAt) { 183 const closedDate = new Date(closedAt); 184 const diffInDays = Math.floor((currentDate - closedDate) / (1000 * 60 * 60 * 24)); 185 shouldShow = diffInDays >= limitPeriod; 186 } 187 188 if (this.hasAppInstalled) { 189 shouldShow = false; 190 } 191 192 if (!this.downloadAppUrl || this.downloadAppUrl === 'null') { 193 shouldShow = false; 194 } 195 196 // Capturar o breakpoint da instância da classe 197 const breakpoint = this.breakpoint || 768; 198 const shouldRenderMobile = this.shouldRenderMobile; 199 200 return { 201 showBanner: false, 202 showModal: false, 203 shouldShow: shouldShow, 204 downloadAppUrl: this.downloadAppUrl, 205 laravelUrl: this.laravelUrl, 206 isAndroid: this.isAndroid, 207 isiOS: this.isiOS, 208 bannerVisible: false, 209 timeoutToShowBanner: this.timeoutToShowBanner, 210 isClosing: false, 211 breakpoint: breakpoint, 212 isMobile: shouldRenderMobile, 213 checkMobile() { 214 this.isMobile = shouldRenderMobile; 215 }, 216 init() { 217 this.checkMobile(); 218 // Listener para atualizar quando a tela mudar de tamanho 219 const handleResize = () => { 220 this.checkMobile(); 221 // Se a tela ficar maior que o breakpoint, esconder o banner 222 if (!this.isMobile && this.showBanner) { 223 this.showBanner = false; 224 } 225 }; 226 227 window.addEventListener('resize', handleResize); 228 229 // Monitorar mudanças no showBanner para garantir que só apareça em mobile 230 this.$watch('showBanner', (value) => { 231 if (value && !this.isMobile) { 232 this.showBanner = false; 233 } 234 }); 235 236 sessionStorage.setItem('app-download-should-show', this.shouldShow ? 1 : 0); 237 if (this.shouldShow && this.isMobile) { 238 setTimeout(() => { 239 // Verificar novamente antes de mostrar (caso a tela tenha mudado) 240 this.checkMobile(); 241 if (this.isMobile) { 242 this.showBanner = true; 243 this.trackImpression(); 244 } 245 }, this.timeoutToShowBanner); 246 } 247 }, 248 249 openModal() { 250 // Prevent opening if already open or currently closing 251 if (this.showModal || this.isClosing) return; 252 253 this.isClosing = false; // Reset flag if opening 254 this.showModal = true; 255 this.trackEvent('cliques_baixar_app'); 256 }, 257 258 closeModal() { 259 try { 260 // Prevent multiple calls 261 if (!this.showModal || this.isClosing) return; 262 263 this.isClosing = true; 264 this.showModal = false;
265 266 // Force hide the modal element to prevent reopening 267 const modalElement = document.getElementById('download_app_modal_c2'); 268 if (modalElement) { 269 modalElement.style.display = 'none'; 270 modalElement.classList.remove('show'); 271 } 272 273 // Reset closing flag after a short delay to prevent immediate reopening 274 setTimeout(() => { 275 this.isClosing = false; 276 }, 300); 277 } catch (error) { 278 console.error('[AppDownloadBar] Error closing modal:', error); 279 // Force hide even if there's an error 280 this.showModal = false; 281 this.isClosing = false; 282 const modalElement = document.getElementById('download_app_modal_c2'); 283 if (modalElement) { 284 modalElement.style.display = 'none'; 285 modalElement.classList.remove('show'); 286 } 287 } 288 }, 289 closeBanner() { 290 try { 291 this.showBanner = false; 292 sessionStorage.setItem('app-download-should-show', '0'); 293 window.dispatchEvent(new CustomEvent('app-download-banner-closed')); 294 this.fullyRemoveSelector(); 295 this.saveBannerState(7); 296 } catch (error) { 297 console.error('[AppDownloadBar] Error closing banner:', error); 298 this.showBanner = false; 299 sessionStorage.setItem('app-download-should-show', '0'); 300 window.dispatchEvent(new CustomEvent('app-download-banner-closed')); 301 } 302 }, 303 fullyRemoveSelector() { 304 setTimeout(() => { 305 const tag = document.querySelector('app-download-bar') 306 if (tag) { 307 tag.remove(); 308 } 309 }, 1500); 310 }, 311 saveBannerState(period) { 312 setTimeout(() => { 313 this.safeSet('app-download-bar-closed-at', new Date().toISOString()); 314 this.safeSet('limit-period', period.toString()); 315 }, 1500); 316 }, 317 safeSet(key, value) { 318 if (typeof window === 'undefined') return; 319 if (typeof window?.localStorage === 'undefined') return; 320 if (key === 'app-download-bar-closed-at' || key === 'limit-period') { 321 try { 322 localStorage.setItem(key, value); 323 } catch { 324 // 325 } 326 return; 327 } 328 }, 329 trackEvent(label) { 330 try { 331 window.dataLayer = window.dataLayer || []; 332 333 window.dataLayer.push({ 334 event: 'Smartbanner', 335 event_category: 'B2', 336 event_label: label 337 }); 338 339 } catch (error) { 340 console.error('[AppDownloadBar] Failed to track event:', error); 341 } 342 }, 343 344 trackImpression() { 345 if (this.bannerVisible) return; 346 this.bannerVisible = true; 347 this.trackEvent('impressoes_baixar_app'); 348 }, 349 350 handleDownloadAppClick() { 351 if (this.isAndroid) { 352 this.trackEvent('Google_Play'); 353 } else if (this.isiOS) { 354 this.trackEvent('App_Store'); 355 } 356 } 357 }; 358 } 359 360 render() { 361 const styles = `<style> 362 /* Hide elements with x-cloak until Alpine is ready */ 363 [x-cloak] { 364 display: none !important; 365 } 366 367 #android-app-download-bar-b2 { 368 z-index: 9999; 369 position: fixed; 370 left: 0; 371 right: 0; 372 width: 100%; 373 max-width: 100vw; 374 box-sizing: border-box; 375 z-index: 99; 376 opacity: 1; 377 height: 440px; 378 bottom: 0; 379 transform: translateY(100%); 380 border-radius: 16px 16px 0 0; 381 border-right: 1px solid #C2C0BC; 382 border-bottom: 1px solid #C2C0BC; 383 border-left: 1px solid #C2C0BC; 384 background: #ffffff; 385 transition: transform 0.5s ease-out; 386 display: none; 387 } 388 389 @media (max-width: ${this.breakpoint}px) { 390 #android-app-download-bar-b2 { 391 display: block; 392 } 393 } 394 395 #android-app-download-bar-b2.show-banner { 396 transform: translateY(0); 397 box-shadow: 0 0 80px 0 #594426; 398 } 399 400 #android-app-download-bar-b2 .container-download-app { 401 position: relative; 402 display: flex; 403 padding: 16px 12px 20px; 404 flex-direction: column; 405 align-items: center; 406 gap: 16px; 407 } 408 409 #android-app-download-bar-b2 .header-container-download-app { 410 display: flex; 411 justify-content: space-between; 412 align-items: center; 413 width: 100%; 414 } 415 416 #android-app-download-bar-b2 .header-container-download-app .logo { 417 width: 200px; 418 } 419 420 #android-app-download-bar-b2 .header-container-download-app .btn-close-container-download-app { 421 padding: 0 5px; 422 border-radius: 100%; 423 border: none; 424 background: #F3F4F7; 425 color: white; 426 cursor: pointer; 427 transition: background-color 0.2s ease; 428 width: 30px; 429 height: 30px; 430 display: flex; 431 justify-content: center; 432 align-items: center; 433 } 434 435 #android-app-download-bar-b2 .header-container-download-app .btn-close-container-download-app:hover { 436 background: #1f2329; 437 } 438 439 #android-app-download-bar-b2 .header-container-download-app .btn-close-container-download-app:focus { 440 outline: 2px solid #009974; 441 outline-offset: 2px; 442 } 443 444 #android-app-download-bar-b2 .description { 445 text-align: center; 446 font-family: Figtree, sans-serif; 447 font-size: 18px; 448 font-weight: 500; 449 line-height: 120%; 450 background: linear-gradient(91deg, #2F3137 0.94%, #54432B 136.28%); 451 background-clip: text; 452 -webkit-background-clip: text; 453 -webkit-text-fill-color: transparent; 454 } 455 456 #android-app-download-bar-b2 .content { 457 display: flex; 458 flex-direction: column; 459 justify-content: center; 460 align-items: center; 461 gap: 8px; 462 width: 100%; 463 } 464 465 #android-app-download-bar-b2 .content .reviews { 466 display: flex; 467 justify-content: center; 468 align-items: center; 469 gap: 10px; 470 } 471 472 #android-app-download-bar-b2 .content .reviews img { 473 width: 70px; 474 padding-bottom: 4px; 475 } 476 477 #android-app-download-bar-b2 .content .reviews .review { 478 color: #2F3137; 479 font-family: Figtree, sans-serif; 480 font-size: 14px; 481 font-weight: 600; 482 line-height: 20px; 483 } 484 485 #android-app-download-bar-b2 .content .downloads { 486 display: flex; 487 justify-content: center; 488 align-items: center; 489 gap: 12px; 490 } 491 492 #android-app-download-bar-b2 .content .downloads .text { 493 color: #2F3137; 494 font-family: Figtree, sans-serif; 495 font-size: 14px; 496 font-weight: 500; 497 line-height: 16px; 498 } 499 500 #android-app-download-bar-b2 .btn-download { 501 display: flex; 502 padding: 16px 24px; 503 justify-content: center; 504 align-items: center; 505 gap: 8px; 506 width: 100%; 507 border-radius: 100px; 508 border: none; 509 box-shadow: 0 2px 10px rgba(8, 33, 69, 0.25); 510 cursor: pointer; 511 font-family: Figtree, sans-serif; 512 font-weight: 700; 513 font-size: 16px; 514 line-height: 24px; 515 z-index: 1058; 516 background: #F7BA6E; 517 color: #000; 518 transition: transform 0.2s ease, box-shadow 0.2s ease; 519 } 520 521 #android-app-download-bar-b2 .btn-download:hover { 522 transform: translateY(-2px); 523 box-shadow: 0 4px 15px rgba(8, 33, 69, 0.35); 524 } 525 526 #android-app-download-bar-b2 .btn-download:active { 527 transform: translateY(0); 528 } 529 530 #android-app-download-bar-b2 .btn-download:focus { 531 outline: 2px solid #fff; 532 outline-offset: 2px; 533 } 534 535 #android-app-download-bar-b2 .img-download { 536 width: 270px; 537 height: 400px; 538 position: absolute; 539 top: 55%; 540 bottom: -150px; 541 } 542 543 /* MODAL STRUCTURE */
544 .modal-default { 545 position: fixed; 546 top: 0; 547 left: 0; 548 width: 100%; 549 height: 100%; 550 background-color: rgba(0, 0, 0, 0.66); 551 z-index: 1059; 552 display: none; 553 justify-content: center; 554 flex-direction: column; 555 align-items: center; 556 } 557 558 @media (max-width: 767px) { 559 .modal-default { 560 padding: 25px; 561 } 562 } 563 564 #download_app_modal_c2.show { 565 display: flex !important; 566 } 567 568 #download_app_modal_c2 .modal-default-container-download-app { 569 padding: 35px 26px 10px; 570 border-radius: 5px; 571 background: linear-gradient(180deg, #fff 0%, #b4b4b4 100%); 572 width: 100%; 573 max-width: 620px; 574 position: relative; 575 box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); 576 animation-delay: 0.3s; 577 } 578 579 #download_app_modal_c2 .modal-default-container-download-app .btn-close-container-download-app { 580 position: absolute; 581 right: 10px; 582 top: 3px; 583 border: none; 584 background: transparent; 585 line-height: 35px; 586 font-size: 50px; 587 font-weight: 100; 588 cursor: pointer; 589 color: #000; 590 transition: color 0.2s ease; 591 } 592 593 #download_app_modal_c2 .modal-default-container-download-app .btn-close-container-download-app:hover { 594 color: #666; 595 } 596 597 #download_app_modal_c2 .modal-default-container-download-app .btn-close-container-download-app:focus { 598 outline: 2px solid #009974; 599 outline-offset: 2px; 600 } 601 602 #download_app_modal_c2 .modal-body { 603 display: flex; 604 flex-direction: column; 605 align-items: center; 606 gap: 14px; 607 } 608 609 #download_app_modal_c2 .modal-body .logo { 610 height: 94px; 611 display: flex; 612 justify-content: center; 613 } 614 615 #download_app_modal_c2 .modal-body .logo img { 616 height: 100%; 617 width: auto; 618 } 619 620 #download_app_modal_c2 .modal-body .description { 621 font-family: "Nunito", sans-serif; 622 display: flex; 623 justify-content: center; 624 align-items: center; 625 color: #000; 626 font-size: 20px; 627 font-weight: 700; 628 gap: 4px; 629 } 630 631 #download_app_modal_c2 .modal-body .description strong { 632 font-weight: bolder !important; 633 } 634 635 #download_app_modal_c2 .modal-body .btns { 636 display: flex; 637 gap: 20px; 638 } 639 640 #download_app_modal_c2 .modal-body .btns a { 641 flex: 1; 642 transition: transform 0.2s ease; 643 } 644 645 #download_app_modal_c2 .modal-body .btns a:hover { 646 transform: scale(1.05); 647 } 648 649 #download_app_modal_c2 .modal-body .btns a:active { 650 transform: scale(0.98); 651 } 652 653 #download_app_modal_c2 .modal-body .btns img { 654 width: 100%; 655 } 656 </style>`; 657 658 this._alpineData = this.getAlpineData(); 659 660 const componentId = 'app-download-bar-6ab4e5077b2de'; 661 this._componentId = componentId; 662 663 window[`__alpineData_${componentId}`] = () => this.getAlpineData(); 664 665 const html = ` 666 <div 667 id="android-app-download-bar-b2" 668 x-data="window['__alpineData_${componentId}']()" 669 x-init="init()" 670 x-show="showBanner && shouldShow && isMobile" 671 x-cloak 672 x-transition:enter="transition ease-out duration-500" 673 x-transition:enter-start="transform translate-y-full" 674 x-transition:enter-end="transform translate-y-0" 675 x-transition:leave="transition ease-in duration-500" 676 x-transition:leave-start="transform translate-y-0" 677 x-transition:leave-end="transform translate-y-full"
678 :class="{ 'show-banner': showBanner && shouldShow && isMobile }" 679 role="banner" 680 aria-label="Baixar aplicativo" 681 > 682 <div class="container-download-app"> 683 <div class="header-container-download-app"> 684 <div style="overflow: hidden; width: 20px;" aria-hidden="true"></div> 685 <img 686 class="logo" 687 src="${this.laravelUrl}/images/i10-logo-2.svg" 688 alt="Investidor10" 689 loading="lazy" 690 > 691 <button 692 type="button" 693 class="btn-close-container-download-app" 694 @click.stop="closeBanner()" 695 aria-label="Fechar banner" 696 > 697 <svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 9 9" fill="none"> 698 <path d="M8.86813 8.23142C8.90994 8.27322 8.9431 8.32286 8.96573 8.37748C8.98835 8.4321 9 8.49065 9 8.54977C9 8.6089 8.98835 8.66744 8.96573 8.72207C8.9431 8.77669 8.90994 8.82632 8.86813 8.86813C8.82632 8.90994 8.77669 8.9431 8.72207 8.96573C8.66744 8.98835 8.6089 9 8.54977 9C8.49065 9 8.4321 8.98835 8.37748 8.96573C8.32286 8.9431 8.27322 8.90994 8.23142 8.86813L4.5 5.13615L0.768582 8.86813C0.684149 8.95257 0.569632 9 0.450225 9C0.330818 9 0.216301 8.95257 0.131868 8.86813C0.0474343 8.7837 2.3538e-09 8.66918 0 8.54977C-2.3538e-09 8.43037 0.0474343 8.31585 0.131868 8.23142L3.86385 4.5L0.131868 0.768582C0.0474343 0.684149 -8.89652e-10 0.569632 0 0.450225C8.89652e-10 0.330818 0.0474343 0.216301 0.131868 0.131868C0.216301 0.0474343 0.330818 8.89653e-10 0.450225 0C0.569632 -8.89652e-10 0.684149 0.0474343 0.768582 0.131868L4.5 3.86385L8.23142 0.131868C8.31585 0.0474343 8.43037 -2.3538e-09 8.54977 0C8.66918 2.3538e-09 8.7837 0.0474343 8.86813 0.131868C8.95257 0.216301 9 0.330818 9 0.450225C9 0.569632 8.95257 0.684149 8.86813 0.768582L5.13615 4.5L8.86813 8.23142Z" fill="#2F3137"/> 699 </svg> 700 </button> 701 </div> 702 703 <span class="description"> 704 Invista com mais informação e praticidade. Tudo o que você mais gosta no site,<br/>agora no app. 705 </span> 706 707 <div class="content"> 708 <div class="reviews"> 709 <img 710 src="${this.laravelUrl}/images/reviews.svg" 711 alt="Avaliações" 712 loading="lazy" 713 > 714 <span class="review">4.9</span> 715 </div> 716 <div class="downloads"> 717 <img 718 src="${this.laravelUrl}/assets/front/general-icons/app-store.svg" 719 alt="App Store" 720 loading="lazy" 721 > 722 <img 723 src="${this.laravelUrl}/assets/front/general-icons/google-play.svg" 724 alt="Google Play" 725 loading="lazy" 726 > 727 <span class="text">Mais de 1 milhão de downloads</span> 728 </div> 729 </div> 730 731 <a 732 href="${this.downloadAppUrl}" 733 class="btn-download" 734 @click="handleDownloadAppClick()" 735 aria-label="Baixar aplicativo grátis" 736 > 737 Quero baixar o app grátis 738 </a> 739 740 <img 741 class="img-download" 742 src="${this.laravelUrl}/images/popup-download-img-v6.svg" 743 alt="" 744 loading="lazy" 745 aria-hidden="true" 746 > 747 </div> 748 </div>`; 749 750 this.innerHTML = styles + html; 751 } 752 753 disconnectedCallback() { 754 if (this._componentId && window[`__alpineData_${this._componentId}`]) { 755 delete window[`__alpineData_${this._componentId}`]; 756 } 757 } 758} 759
760customElements.define('app-download-bar', AppDownloadBar);
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.